]> git.r.bdr.sh - rbdr/dead-drop/commitdiff
Handle Menus and Store / Recall recordings (#2)
authorRubén Beltrán del Río <redacted>
Sun, 19 Feb 2017 05:14:20 +0000 (23:14 -0600)
committerGitHub <redacted>
Sun, 19 Feb 2017 05:14:20 +0000 (23:14 -0600)
* Update calls for menu parsing in paw

* Add twilio and bodyparser dependencies

* Integrate menu routes

* Add redirects to all menu selections.

* Add pause to menu message

* Update paw to handle all recordings cases

* Add rec menu handler, recordings dummy handler

* Add redis related configs

* Add redis node module

* Add missing comma to config

* Remove dangling comma from config

* Update paw to validate input

* Add post validation dependencies

* Update yarn lock

* Store recordings in redis

* Say post id in spanish

* Use the word for the pound sign

* Use a timestamp shifted by 2 and truncated to 10

config/config.js
config/env.dist
docker-compose.yml
etc/dead_drop.paw
lib/controllers/main_menu.js [new file with mode: 0644]
lib/controllers/recording_menu.js [new file with mode: 0644]
lib/controllers/recordings.js [new file with mode: 0644]
lib/dead_drop.js
package.json
yarn.lock

index 5b29ec466a00252707a6db305c900026917e1eb3..16e9a9f017dad1f581b82aa03aa0150c40542b5c 100644 (file)
@@ -12,7 +12,22 @@ const internals = {};
  * @memberof DeadDrop
  * @typedef {object} tConfiguration
  * @property {number} [port=1988] the port where the app will listen on
+ * @property {DeadDrop.tRedisConfiguration} redis the configuration to
+ * connect to the redis server
  */
 module.exports = internals.Config = {
-  port: Getenv.int('DEAD_DROP_PORT', 1988)
+  port: Getenv.int('DEAD_DROP_PORT', 1988),
+
+  /**
+   * Information required to connect to the redis server
+   *
+   * @memberof DeadDrop
+   * @typedef {object} tRedisConfiguration
+   * @property {string} host the location of the redis host
+   * @property {string} [post=6379] port where redis server is listening
+   */
+  redis: {
+    host: Getenv('DEAD_DROP_REDIS_HOST'),
+    port: Getenv.int('DEAD_DROP_REDIS_PORT', 6379)
+  }
 };
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..11f79f10feb4e6d9ffc2a5a53d96307b75081184 100644 (file)
@@ -0,0 +1 @@
+DEAD_DROP_REDIS_HOST=location_of_redis_server
index a374a41f33200fc79caa0571d37b78cd723d3faa..d456b9e3353a02e0974626bda229ad65e202aaba 100644 (file)
@@ -5,5 +5,11 @@ services:
     build: .
     env_file: .env
     image: rbdr/dead-drop
+    environment:
+      - DEAD_DROP_REDIS_HOST=db
     ports:
       - "1988:1988"
+    depends_on:
+      - db
+  db:
+    image: redis:3.2.6
index 6e120f7548b52d3cb0cbd398460e9ac03de64a1e..4627b2d912558807bdae42d07341926173ba1d80 100644 (file)
Binary files a/etc/dead_drop.paw and b/etc/dead_drop.paw differ
diff --git a/lib/controllers/main_menu.js b/lib/controllers/main_menu.js
new file mode 100644 (file)
index 0000000..461cff8
--- /dev/null
@@ -0,0 +1,96 @@
+'use strict';
+
+const Twilio = require('twilio');
+
+const internals = {};
+
+internals.kMenuTimeout = 10; // timeout in seconds
+internals.kMenuDigits = 1; // number of digits in the menu
+internals.kContentType = 'application/xml'; // The content type used to respond
+internals.kMenuLanguage = 'es-mx'; // the language to use
+internals.kTimeoutMessage = 'Oh... está bien. Adios!';
+internals.kMainMenuRoute = '/menus/main';
+internals.kRecordingMenuRoute = '/menus/recording';
+internals.kRandomMessageRoute = '/recordings/0';
+internals.kLeaveMessageRoute = '/recordings';
+internals.kMenuMessage = 'Para dejar un mensaje, presiona 1. ' +
+                         'Para escuchar un mensaje al azar, presiona 2. ' +
+                         'Para escuchar un mensaje específico, presioan 3.'; // the message that will be shown
+internals.kMenuInvalidResponseMessage = 'No entendí... Volviendo al menu principal.'; // invalid selection message
+internals.kMenuOptions = {
+  leaveMessage: 1,
+  listenToRandomMessage: 2,
+  listenToSpecificMessage: 3
+}; // the menu options
+
+/**
+ * Handles the HTTP requests for the main menu
+ *
+ * @class MainMenuController
+ */
+module.exports = internals.MainMenuController = class MainMenuController {
+
+  /**
+   * Serves the menu
+   *
+   * @function serveMenu
+   * @memberof MainMenuController
+   * @instance
+   * @return {generator} a koa compatible handler generator function
+   */
+  serveMenu() {
+
+    return function * () {
+
+      const response = new Twilio.TwimlResponse();
+
+      // the action will default to post in the same URL, so no change
+      // required there.
+      response.gather({
+        timeout: internals.kMenuTimeout,
+        numDigits: internals.kMenuDigits
+      }, function nestedHandler() {
+
+        this.say(internals.kMenuMessage, { language: internals.kMenuLanguage });
+      }).say(internals.kTimeoutMessage, { language: internals.kMenuLanguage });
+
+      this.type = internals.kContentType;
+      this.body = response.toString();
+    };
+  }
+
+  /**
+   * Parses the selected main menu response
+   *
+   * @function parseMenuSelection
+   * @memberof MainMenuController
+   * @instance
+   * @return {generator} a koa compatible handler generator function
+   */
+  parseMenuSelection() {
+
+    return function * () {
+
+      const menuSelection = parseInt(this.request.body.Digits);
+
+      const response = new Twilio.TwimlResponse();
+
+      if (menuSelection === internals.kMenuOptions.leaveMessage) {
+        response.redirect(internals.kLeaveMessageRoute, { method: 'GET' });
+      }
+      else if (menuSelection === internals.kMenuOptions.listenToRandomMessage) {
+        response.redirect(internals.kRandomMessageRoute, { method: 'GET' });
+      }
+      else if (menuSelection === internals.kMenuOptions.listenToSpecificMessage) {
+        response.redirect(internals.kRecordingMenuRoute, { method: 'GET' });
+      }
+      else {
+        response.say(internals.kMenuInvalidResponseMessage, { language: internals.kMenuLanguage })
+            .redirect(internals.kMainMenuRoute, { method: 'GET' });
+      }
+
+      this.type = internals.kContentType;
+      this.body = response.toString();
+    };
+  }
+};
diff --git a/lib/controllers/recording_menu.js b/lib/controllers/recording_menu.js
new file mode 100644 (file)
index 0000000..55f2836
--- /dev/null
@@ -0,0 +1,82 @@
+'use strict';
+
+const Twilio = require('twilio');
+
+const internals = {};
+
+internals.kMenuTimeout = 10; // timeout in seconds
+internals.kContentType = 'application/xml'; // The content type used to respond
+internals.kMenuLanguage = 'es-mx'; // the language to use
+internals.kMainMenuRoute = '/menus/main';
+internals.kListenMessageRoute = '/recordings/';
+internals.kMenuMessage = 'Escribe el numero de mensaje y presiona gato para terminar.';
+internals.kTimeoutMessage = 'Bueno... volviendo al menú principal.';
+internals.kMenuInvalidResponseMessage = 'No entendí... Volviendo al menu principal.'; // invalid selection message
+
+/**
+ * Handles the HTTP requests for the recording menu
+ *
+ * @class RecordingMenuController
+ */
+module.exports = internals.RecordingMenuController = class RecordingMenuController {
+
+  /**
+   * Serves the menu
+   *
+   * @function serveMenu
+   * @memberof RecordingMenuController
+   * @instance
+   * @return {generator} a koa compatible handler generator function
+   */
+  serveMenu() {
+
+    return function * () {
+
+      const response = new Twilio.TwimlResponse();
+
+      // the action will default to post in the same URL, so no change
+      // required there.
+      response.gather({
+        timeout: internals.kMenuTimeout
+      }, function nestedHandler() {
+
+        this.say(internals.kMenuMessage, { language: internals.kMenuLanguage });
+      })
+      .say(internals.kTimeoutMessage, { language: internals.kMenuLanguage })
+      .redirect(internals.kMainMenuRoute, { method: 'GET' });
+
+      this.type = internals.kContentType;
+      this.body = response.toString();
+    };
+  }
+
+  /**
+   * Parses the selected recording id
+   *
+   * @function parseMenuSelection
+   * @memberof RecordingMenuController
+   * @instance
+   * @return {generator} a koa compatible handler generator function
+   */
+  parseMenuSelection() {
+
+    return function * () {
+
+      const messageId = parseInt(this.request.body.Digits);
+
+      const response = new Twilio.TwimlResponse();
+
+      if (messageId) {
+        response.redirect(`${internals.kListenMessageRoute}${messageId}`, { method: 'GET' });
+      }
+      else {
+        response.say(internals.kMenuInvalidResponseMessage, { language: internals.kMenuLanguage })
+            .redirect(internals.kMainMenuRoute, { method: 'GET' });
+      }
+
+      this.type = internals.kContentType;
+      this.body = response.toString();
+    };
+  }
+};
+
diff --git a/lib/controllers/recordings.js b/lib/controllers/recordings.js
new file mode 100644 (file)
index 0000000..591dbd5
--- /dev/null
@@ -0,0 +1,166 @@
+'use strict';
+
+const Joi = require('joi');
+const Pify = require('pify');
+const Redis = require('redis');
+const Twilio = require('twilio');
+
+const internals = {};
+
+internals.kContentType = 'application/xml'; // The content type used to respond
+internals.kLanguage = 'es-mx'; // the language to use
+internals.kMaxMessageLength = 30; // max message length in seconds
+internals.kIdDateFormat = 'YYMMDDHHmmssSSS'; // derive ids from current date. 15 digits.
+internals.kRecordingsSet = 'recordings';
+internals.kRecordMessage = 'Graba tu mensaje despues del bip. ' +
+                           'Presiona cualquier tecla para finalizar tu mensaje. '; // the recording message
+internals.kConfirmationMessage = 'Gracias. Tu mensaje es el número: ';
+internals.kNotFoundMessage = 'Mensaje no encontrado. Adiós!';
+
+internals.kRecordingSchema = Joi.object().keys({
+  url: Joi.string().required()
+});
+
+/**
+ * Handles the HTTP requests for the recording menu
+ *
+ * @class RecordingsController
+ * @param {DeadDrop.tConfiguration} config The configuration to
+ * initialize.
+ */
+module.exports = internals.RecordingsController = class RecordingsController {
+  constructor(config) {
+
+    this._redis = Redis.createClient(config.redis);
+
+    // Log an error if it happens.
+    this._redis.on('error', (err) => {
+
+      console.error(err);
+    });
+  }
+
+  /**
+   * Start recording process
+   *
+   * @function startRecording
+   * @memberof RecordingsController
+   * @instance
+   * @return {generator} a koa compatible handler generator function
+   */
+  startRecording() {
+
+    return function * () {
+
+      const response = new Twilio.TwimlResponse();
+
+      // the action will default to post in the same URL, so no change
+      // required there.
+      response.say(internals.kRecordMessage, { language: internals.kLanguage })
+      .record({
+        maxLength: internals.kMaxMessageLength
+      });
+
+      this.type = internals.kContentType;
+      this.body = response.toString();
+    };
+  }
+
+  /**
+   * Saves the recording for later use
+   *
+   * @function saveRecording
+   * @memberof RecordingsController
+   * @instance
+   * @return {generator} a koa compatible handler generator function
+   */
+  saveRecording() {
+
+    const self = this;
+
+    return function * () {
+
+      const zadd = Pify(self._redis.zadd.bind(self._redis));
+
+      const response = new Twilio.TwimlResponse();
+
+      const id = Date.now().toString().substr(2,10);
+      const url = this.request.body.RecordingUrl;
+      const separatedId = id.split('').join('. ');
+      const recording = {
+        url
+      };
+
+      yield self._validate(recording).catch((err) => {
+
+        this.throw(err.message, 422);
+      });
+
+      // Add to ordered set for quick fetches, and set for random fetches
+      yield zadd(internals.kRecordingsSet, parseInt(id), url);
+
+      response.say(`${internals.kConfirmationMessage}${separatedId}`, { language: internals.kLanguage });
+
+      this.type = internals.kContentType;
+      this.body = response.toString();
+    };
+  }
+
+  /**
+   * Gets a recording.
+   *
+   * @function getRecording
+   * @memberof RecordingsController
+   * @instance
+   * @return {generator} a koa compatible handler generator function
+   */
+  getRecording() {
+
+    const self = this;
+
+    return function * (id) {
+
+      id = parseInt(id);
+
+      const zcard = Pify(self._redis.zcard.bind(self._redis));
+      const zrange = Pify(self._redis.zrange.bind(self._redis));
+      const zscore = Pify(self._redis.zscore.bind(self._redis));
+      const zrangebyscore = Pify(self._redis.zrangebyscore.bind(self._redis));
+
+      const response = new Twilio.TwimlResponse();
+      let location = null;
+
+      if (!id) {
+        const maxNumber = yield zcard(internals.kRecordingsSet);
+        const index = Math.floor(Math.random() * maxNumber); // get random between 0 and cardinality
+        location = yield zrange(internals.kRecordingsSet, index, index);
+      }
+      else {
+        location = yield zrangebyscore(internals.kRecordingsSet, id, id);
+      }
+
+      if (location && location.length > 0) {
+        if (!id) {
+          id = yield zscore(internals.kRecordingsSet, location[0]);
+        }
+
+        const separatedId = id.toString().split('').join('. ');
+        response.play(location[0]).say(separatedId, { language: internals.kLanguage });
+      }
+      else {
+        response.say(internals.kNotFoundMessage, { language: internals.kLanguage });
+      }
+
+      this.type = internals.kContentType;
+      this.body = response.toString();
+    };
+  }
+
+  // Validates the post schema
+
+  _validate(post) {
+
+    const validate = Pify(Joi.validate.bind(Joi));
+    return validate(post, internals.kRecordingSchema);
+  }
+};
index 1ea86e8079f602b4f8a181b7c636501dba437a35..6f976459e18231d8ed5fbd0cb950b33b0024ca7d 100644 (file)
@@ -1,8 +1,13 @@
 'use strict';
 
 const Koa = require('koa');
+const KoaBodyParser = require('koa-bodyparser');
 const KoaRoute = require('koa-route');
 
+const MainMenuController = require('./controllers/main_menu');
+const RecordingMenuController = require('./controllers/recording_menu');
+const RecordingsController = require('./controllers/recordings');
+
 const internals = {};
 
 /**
@@ -42,60 +47,57 @@ module.exports = internals.DeadDrop = class DeadDrop {
 
     this._app = Koa();
 
-    this._app.use(KoaRoute.get('/menus/main', function * () {
-
-      this.body = 'I will return the main menu.';
-    }));
-
-    this._app.use(KoaRoute.post('/menus/main', function * () {
+    this._app.use(KoaBodyParser());
 
-      this.body = 'I will parse the main menu.';
-    }));
+    this._initializeMainMenuRoutes();
+    this._initializeRecordingMenuRoutes();
+    this._initializeRecordingsRoutes();
 
-    this._app.use(KoaRoute.get('/menus/recording', function * () {
+    this._app.use(function * () {
 
-      this.body = 'I will return the select recording menu.';
-    }));
+      this.body = 'How did you get here? Shoo!';
+    });
 
-    this._app.use(KoaRoute.post('/menus/recording', function * () {
+  }
 
-      this.body = 'I will parse the select recording menu.';
-    }));
+  // Starts listening
 
-    this._app.use(KoaRoute.get('/recordings', function * () {
+  _startServer() {
 
-      this.body = 'I will initiate recording process';
-    }));
+    this._app.listen(this.port);
+  }
 
-    this._app.use(KoaRoute.post('/recordings', function * () {
+  // Initializes the main menu routes.
 
-      this.body = 'I will create a new recording';
-    }));
+  _initializeMainMenuRoutes() {
 
-    this._app.use(KoaRoute.get('/recordings/:id', function * (id) {
+    const mainMenuController = new MainMenuController();
 
-      id = parseInt(id);
+    this._app.use(KoaRoute.get('/menus/main', mainMenuController.serveMenu()));
+    this._app.use(KoaRoute.post('/menus/main', mainMenuController.parseMenuSelection()));
+  }
 
-      if (id === 0) {
-        this.body = 'I will return a random recording';
-      }
-      else {
-        this.body = 'I will return a specific recording';
-      }
-    }));
+  // Initializes the recording menu routes.
 
-    this._app.use(function * () {
+  _initializeRecordingMenuRoutes() {
 
-      this.body = 'hello, world';
-    });
+    const recordingMenuController = new RecordingMenuController();
 
+    this._app.use(KoaRoute.get('/menus/recording', recordingMenuController.serveMenu()));
+    this._app.use(KoaRoute.post('/menus/recording', recordingMenuController.parseMenuSelection()));
   }
 
-  // Starts listening
+  // Initializes the recordings routes.
 
-  _startServer() {
+  _initializeRecordingsRoutes() {
 
-    this._app.listen(this.port);
+    const recordingsController = new RecordingsController({
+      redis: this.redis
+    });
+
+    this._app.use(KoaRoute.get('/recordings', recordingsController.startRecording()));
+    this._app.use(KoaRoute.post('/recordings', recordingsController.saveRecording()));
+    this._app.use(KoaRoute.get('/recordings/:id', recordingsController.getRecording()));
   }
 
   // Prints the banner.
index afaa2be8dbda4dd78f71d240c8082f8ba1fe780c..cb0405f45976fe42becd2ae8a24394fcd16479e5 100644 (file)
   },
   "dependencies": {
     "getenv": "^0.7.0",
+    "joi": "^10.2.2",
     "koa": "^1.2.5",
-    "koa-route": "^2.4.2"
+    "koa-bodyparser": "^2.3.0",
+    "koa-route": "^2.4.2",
+    "pify": "^2.3.0",
+    "redis": "^2.6.5",
+    "twilio": "^2.11.1"
   }
 }
index 2f178dc9a1656e0386fbd867c4d28422559ffcb5..109426a871f1a00732d110e2739bc4863efc7ea3 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -70,6 +70,32 @@ 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"
+
+async@^2.0.1:
+  version "2.1.4"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.1.4.tgz#2d2160c7788032e4dd6cbe2502f1f9a2c8f6cde4"
+  dependencies:
+    lodash "^4.14.0"
+
+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.6.0"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+
 babel-code-frame@^6.16.0:
   version "6.22.0"
   resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
@@ -82,10 +108,32 @@ balanced-match@^0.4.1:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
 
+base64url@2.0.0, base64url@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb"
+
+bcrypt-pbkdf@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+  dependencies:
+    tweetnacl "^0.14.3"
+
+bl@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/bl/-/bl-1.1.2.tgz#fdca871a99713aa00d19e3bbba41c44787a65398"
+  dependencies:
+    readable-stream "~2.0.5"
+
 bluebird@~3.4.6:
   version "3.4.7"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
 
+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"
@@ -93,10 +141,18 @@ brace-expansion@^1.0.0:
     balanced-match "^0.4.1"
     concat-map "0.0.1"
 
+buffer-equal-constant-time@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
+
 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-path@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
@@ -107,6 +163,10 @@ callsites@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
 
+caseless@~0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
+
 catharsis@~0.8.8:
   version "0.8.8"
   resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.8.8.tgz#693479f43aac549d806bd73e924cd0d944951a06"
@@ -137,6 +197,15 @@ cli-width@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
 
+co-body@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/co-body/-/co-body-4.2.0.tgz#74df20fa73262125dc45482af04e342ea8db3515"
+  dependencies:
+    inflation "~2.0.0"
+    qs "~4.0.0"
+    raw-body "~2.1.2"
+    type-is "~1.6.6"
+
 co@^4.0.2, co@^4.4.0, co@^4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -145,6 +214,18 @@ 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"
 
+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:
+  version "2.9.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
+  dependencies:
+    graceful-readlink ">= 1.0.0"
+
 composition@^2.1.1:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/composition/-/composition-2.3.0.tgz#742805374cab550c520a33662f5a732e0208d6f2"
@@ -179,16 +260,32 @@ cookies@~0.6.1:
     depd "~1.1.0"
     keygrip "~1.0.1"
 
+copy-to@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/copy-to/-/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5"
+
 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"
 
+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"
+
 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@*, debug@^2.1.1:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.1.tgz#79855090ba2c4e3115cc7d8769491d58f0491351"
@@ -215,6 +312,10 @@ del@^2.0.2:
     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"
@@ -223,6 +324,10 @@ depd@1.1.0, depd@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3"
 
+deprecate@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/deprecate/-/deprecate-0.1.0.tgz#c49058612dc6c8e5145eafe4839b8c2c7d041c14"
+
 destroy@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
@@ -238,6 +343,23 @@ doctrine@^1.2.2:
     esutils "^2.0.2"
     isarray "^1.0.0"
 
+double-ended-queue@^2.1.0-0:
+  version "2.1.0-0"
+  resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
+
+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"
+
+ecdsa-sig-formatter@1.0.9:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1"
+  dependencies:
+    base64url "^2.0.0"
+    safe-buffer "^5.0.1"
+
 ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -415,6 +537,14 @@ exit-hook@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
 
+extend@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4"
+
+extsprintf@1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550"
+
 fast-levenshtein@~2.0.4:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
@@ -442,6 +572,18 @@ flat-cache@^1.2.1:
     graceful-fs "^4.1.2"
     write "^0.2.1"
 
+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.1"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.1.tgz#ae315db9a4907fa065502304a66d7733475ee37c"
+  dependencies:
+    async "^2.0.1"
+    combined-stream "^1.0.5"
+    mime-types "^2.1.11"
+
 fresh@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f"
@@ -464,6 +606,12 @@ getenv@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/getenv/-/getenv-0.7.0.tgz#39b91838707e2086fd1cf6ef8777d1c93e14649e"
 
+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@^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"
@@ -494,6 +642,10 @@ graceful-fs@^4.1.2, 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"
+
 hapi-capitalize-modules@1.x.x:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/hapi-capitalize-modules/-/hapi-capitalize-modules-1.1.6.tgz#7991171415e15e6aa3231e64dda73c8146665318"
@@ -506,12 +658,38 @@ hapi-scope-start@2.x.x:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/hapi-scope-start/-/hapi-scope-start-2.1.1.tgz#7495a726fe72b7bca8de2cdcc1d87cd8ce6ab4f2"
 
+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"
 
+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"
+
+hoek@4.x.x:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.1.0.tgz#4a4557460f69842ed463aa00628cc26d2683afa7"
+
 http-assert@^1.1.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.2.0.tgz#d6392e6f6519def4e340266b35096db6d3feba00"
@@ -535,6 +713,18 @@ http-errors@~1.4.0:
     inherits "2.0.1"
     statuses ">= 1.2.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"
+
+iconv-lite@0.4.13:
+  version "0.4.13"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
+
 ignore@^3.2.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.2.tgz#1c51e1ef53bab6ddc15db4d9ac4ec139eceb3410"
@@ -543,6 +733,10 @@ imurmurhash@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
 
+inflation@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f"
+
 inflight@^1.0.4:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -590,7 +784,7 @@ 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-my-json-valid@^2.10.0:
+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:
@@ -625,6 +819,10 @@ is-resolvable@^1.0.0:
   dependencies:
     tryit "^1.0.1"
 
+is-typedarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
 isarray@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@@ -633,6 +831,33 @@ isarray@^1.0.0, isarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
 
+isemail@2.x.x:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/isemail/-/isemail-2.2.1.tgz#0353d3d9a62951080c262c2aa0a42b8ea8e9e2a6"
+
+isstream@~0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+items@2.x.x:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/items/-/items-2.1.1.tgz#8bd16d9c83b19529de5aea321acaada78364a198"
+
+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"
+
+joi@^10.2.2:
+  version "10.2.2"
+  resolved "https://registry.yarnpkg.com/joi/-/joi-10.2.2.tgz#dc5a792b7b4c6fffa562242a95b55d9d3f077e24"
+  dependencies:
+    hoek "4.x.x"
+    isemail "2.x.x"
+    items "2.x.x"
+    topo "2.x.x"
+
 js-tokens@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
@@ -648,6 +873,10 @@ js2xmlparser@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-1.0.0.tgz#5a170f2e8d6476ce45405e04823242513782fe30"
 
+jsbn@~0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+
 jsdoc@^3.4.3:
   version "3.4.3"
   resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.4.3.tgz#e5740d6145c681f6679e6c17783a88dbdd97ccd3"
@@ -665,12 +894,20 @@ jsdoc@^3.4.3:
     taffydb "2.6.2"
     underscore "~1.8.3"
 
+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:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+
 jsonify@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
@@ -679,6 +916,38 @@ jsonpointer@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
 
+jsonwebtoken@5.4.x:
+  version "5.4.1"
+  resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-5.4.1.tgz#2055c639195ffe56314fa6a51df02468186a9695"
+  dependencies:
+    jws "^3.0.0"
+    ms "^0.7.1"
+
+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"
+
+jwa@^1.1.4:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5"
+  dependencies:
+    base64url "2.0.0"
+    buffer-equal-constant-time "1.0.1"
+    ecdsa-sig-formatter "1.0.9"
+    safe-buffer "^5.0.1"
+
+jws@^3.0.0:
+  version "3.1.4"
+  resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2"
+  dependencies:
+    base64url "^2.0.0"
+    jwa "^1.1.4"
+    safe-buffer "^5.0.1"
+
 keygrip@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.1.tgz#b02fa4816eef21a8c4b35ca9e52921ffc89a30e9"
@@ -689,6 +958,13 @@ klaw@~1.3.0:
   optionalDependencies:
     graceful-fs "^4.1.9"
 
+koa-bodyparser@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/koa-bodyparser/-/koa-bodyparser-2.3.0.tgz#236ed90a16f562e79cade2b958f67c848824e818"
+  dependencies:
+    co-body "^4.2.0"
+    copy-to "^2.0.1"
+
 koa-compose@^2.3.0:
   version "2.5.1"
   resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-2.5.1.tgz#726cfb17694de5cb9fbf03c0adf172303f83f156"
@@ -741,6 +1017,10 @@ levn@^0.3.0, levn@~0.3.0:
     type-check "~0.3.2"
 
 lodash@^4.0.0, lodash@^4.3.0:
+  version "4.12.0"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.12.0.tgz#2bd6dc46a040f59e686c972ed21d93dc59053258"
+
+lodash@^4.14.0:
   version "4.17.4"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
 
@@ -760,7 +1040,7 @@ mime-db@~1.26.0:
   version "1.26.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff"
 
-mime-types@^2.0.7, mime-types@~2.1.11, mime-types@~2.1.13:
+mime-types@^2.0.7, mime-types@^2.1.11, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7:
   version "2.1.14"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee"
   dependencies:
@@ -782,7 +1062,7 @@ mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1:
   dependencies:
     minimist "0.0.8"
 
-ms@0.7.2:
+ms@0.7.2, ms@^0.7.1:
   version "0.7.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765"
 
@@ -802,10 +1082,18 @@ no-arrowception@1.x.x:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/no-arrowception/-/no-arrowception-1.0.0.tgz#5bf3e95eb9c41b57384a805333daa3b734ee327a"
 
+node-uuid@~1.4.7:
+  version "1.4.7"
+  resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.7.tgz#6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f"
+
 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@^4.0.1, object-assign@^4.1.0:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -863,7 +1151,7 @@ path-to-regexp@^1.2.0:
   dependencies:
     isarray "0.0.1"
 
-pify@^2.0.0:
+pify@^2.0.0, pify@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
 
@@ -893,6 +1181,30 @@ progress@^1.1.8:
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
 
+punycode@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
+q@0.9.7:
+  version "0.9.7"
+  resolved "https://registry.yarnpkg.com/q/-/q-0.9.7.tgz#4de2e6cb3b29088c9e4cbc03bf9d42fb96ce2f75"
+
+qs@~4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-4.0.0.tgz#c31d9b74ec27df75e543a86c78728ed8d4623607"
+
+qs@~6.2.0:
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.2.tgz#d506a5ad5b2cae1fd35c4f54ec182e267e3ef586"
+
+raw-body@~2.1.2:
+  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"
+
 readable-stream@^2.2.2:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e"
@@ -905,6 +1217,17 @@ readable-stream@^2.2.2:
     string_decoder "~0.10.x"
     util-deprecate "~1.0.1"
 
+readable-stream@~2.0.5:
+  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"
+
 readline2@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35"
@@ -919,6 +1242,48 @@ rechoir@^0.6.2:
   dependencies:
     resolve "^1.1.6"
 
+redis-commands@^1.2.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.1.tgz#81d826f45fa9c8b2011f4cd7a0fe597d241d442b"
+
+redis-parser@^2.0.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.4.0.tgz#018ea743077aae944d0b798b2fd12587320bf3c9"
+
+redis@^2.6.5:
+  version "2.6.5"
+  resolved "https://registry.yarnpkg.com/redis/-/redis-2.6.5.tgz#87c1eff4a489f94b70871f3d08b6988f23a95687"
+  dependencies:
+    double-ended-queue "^2.1.0-0"
+    redis-commands "^1.2.0"
+    redis-parser "^2.0.0"
+
+request@2.74.x:
+  version "2.74.0"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.74.0.tgz#7693ca768bbb0ea5c8ce08c084a45efa05b892ab"
+  dependencies:
+    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"
+
 require-uncached@^1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
@@ -963,6 +1328,14 @@ rx-lite@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
 
+safe-buffer@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
+
+scmp@0.0.3:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/scmp/-/scmp-0.0.3.tgz#3648df2d7294641e7f78673ffc29681d9bad9073"
+
 setprototypeof@1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08"
@@ -979,10 +1352,31 @@ slice-ansi@0.0.4:
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
 
+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"
+
 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.2"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.2.tgz#d5a804ce22695515638e798dbe23273de070a5fa"
+  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"
+
 "statuses@>= 1.2.1 < 2", "statuses@>= 1.3.1 < 2", statuses@^1.2.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
@@ -1002,10 +1396,18 @@ string-width@^2.0.0:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^3.0.0"
 
+string.prototype.startswith@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/string.prototype.startswith/-/string.prototype.startswith-0.2.0.tgz#da68982e353a4e9ac4a43b450a2045d1c445ae7b"
+
 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:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@@ -1047,17 +1449,49 @@ through@^2.3.6:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
 
+topo@2.x.x:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182"
+  dependencies:
+    hoek "4.x.x"
+
+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"
 
+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.5"
+  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+
+twilio@^2.11.1:
+  version "2.11.1"
+  resolved "https://registry.yarnpkg.com/twilio/-/twilio-2.11.1.tgz#451099467313c56b3767994df2d19062f10ef8c4"
+  dependencies:
+    deprecate "^0.1.0"
+    jsonwebtoken "5.4.x"
+    q "0.9.7"
+    request "2.74.x"
+    scmp "0.0.3"
+    string.prototype.startswith "^0.2.0"
+    underscore "1.x"
+
 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-is@^1.5.5:
+type-is@^1.5.5, type-is@~1.6.6:
   version "1.6.14"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2"
   dependencies:
@@ -1078,10 +1512,14 @@ underscore@1.6.0, underscore@~1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
 
-underscore@~1.8.3:
+underscore@1.x, underscore@~1.8.3:
   version "1.8.3"
   resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
 
+unpipe@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+
 user-home@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
@@ -1096,6 +1534,12 @@ vary@^1.0.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"
+
 wordwrap@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"