From 4c1667e4d0c73ee68e5c08cd348e9f84697f874b Mon Sep 17 00:00:00 2001
From: Dan Walters <dan@walters.io>
Date: Mon, 8 Dec 2014 09:34:19 -0600
Subject: [PATCH] refactor: Separate class for secure lock session, lots of API
 cleanup.

---
 cli.js                     |  63 +++++-----------
 index.js                   |   4 +-
 lib/lock.js                | 146 +++++++++++++++++++------------------
 lib/lock_session.js        | 126 ++++++++++++++++----------------
 lib/scan.js                |  33 +++++++++
 lib/secure_lock_session.js |  46 ++++++++++++
 npm-shrinkwrap.json        |  45 ------------
 package.json               |  13 ++--
 8 files changed, 248 insertions(+), 228 deletions(-)
 mode change 100644 => 100755 cli.js
 create mode 100644 lib/scan.js
 create mode 100644 lib/secure_lock_session.js
 delete mode 100644 npm-shrinkwrap.json

diff --git a/cli.js b/cli.js
old mode 100644
new mode 100755
index 241df45..2bad60f
--- a/cli.js
+++ b/cli.js
@@ -2,53 +2,28 @@
 
 'use strict';
 
-var Lock = require('./lib/lock');
-var noble = require('noble');
-
-var argv = require('yargs')
-  .usage('Control an August Smart Lock.\nUsage: $0 [command]')
-  .example('$0 lock', 'closes the lock')
-  .example('$0 unlock', 'opens the lock')
-  .check(function(argv) {
-    if (argv._.length !== 1) {
-      return 'must specify an operation to perform';
-    }
-
-    var op = argv._[0];
-    if (typeof Lock.prototype[op] !== 'function') {
-      return 'invalid operation: ' + op;
-    }
-  })
-  .argv;
+var augustctl = require('./index');
 
 var config = require(process.env.AUGUSTCTL_CONFIG || './config.json');
 
-noble.on('stateChange', function(state) {
-  if (state === 'poweredOn') {
-    noble.startScanning([ Lock.BLE_COMMAND_SERVICE ]);
-  } else {
-    noble.stopScanning();
-  }
-});
-
-noble.on('discover', function(peripheral) {
-  if (config.uuid === undefined || peripheral.uuid === config.uuid) {
-    noble.stopScanning();
-
-    peripheral.on('disconnect', function() {
+var op = process.argv[2];
+if (typeof augustctl.Lock.prototype[op] !== 'function') {
+  throw new Error('invalid operation: ' + op);
+}
+
+augustctl.scan(config.lockUuid).then(function(peripheral) {
+  var lock = new augustctl.Lock(
+    peripheral,
+    config.offlineKey,
+    config.offlineKeyOffset
+  );
+  lock.connect().then(function() {
+    return lock[op]();
+  }).catch(function(e) {
+    console.error(e.toString());
+  }).finally(function() {
+    return lock.disconnect().finally(function() {
       process.exit(0);
     });
-
-    var lock = new Lock(
-      peripheral,
-      config.offlineKey,
-      config.offlineKeyOffset
-    );
-    lock.connect().then(function() {
-      var op = argv._[0];
-      return lock[op]();
-    }).finally(function() {
-      return lock.disconnect();
-    });
-  }
+  });
 });
diff --git a/index.js b/index.js
index 0c671a8..1a83747 100644
--- a/index.js
+++ b/index.js
@@ -1,7 +1,9 @@
 'use strict';
 
 var Lock = require('./lib/lock');
+var scan = require('./lib/scan');
 
 module.exports = {
-	Lock: Lock
+	Lock: Lock,
+	scan: scan
 };
diff --git a/lib/lock.js b/lib/lock.js
index ee2c88e..6fb7050 100644
--- a/lib/lock.js
+++ b/lib/lock.js
@@ -1,62 +1,70 @@
 'use strict';
 
-var LockSession = require('./lock_session');
+var debug = require('debug')('august:lock');
 
-// relevant UUIDs - w/ this library, must be lowercase and without hyphens
-const BLE_COMMAND_SERVICE = "bd4ac6100b4511e38ffd0800200c9a66";
-const BLE_COMMAND_WRITE_CHARACTERISTIC = "bd4ac6110b4511e38ffd0800200c9a66";
-const BLE_COMMAND_READ_CHARACTERISTIC = "bd4ac6120b4511e38ffd0800200c9a66";
-const BLE_COMMAND_SECURE_WRITE_CHARACTERISTIC = "bd4ac6130b4511e38ffd0800200c9a66";
-const BLE_COMMAND_SECURE_READ_CHARACTERISTIC = "bd4ac6140b4511e38ffd0800200c9a66";
-
-///
-// LockCommand
-// basically, a zero initialized 18 byte Buffer
-
-function LockCommand() {
-  var cmd = new Buffer(0x12);
-  cmd.fill(0x00);
-  return cmd;
-}
+var Promise = require('bluebird');
+var crypto = require('crypto');
+var util = require('util');
 
-///
-// Lock object.
+var LockSession = require('./lock_session');
+var SecureLockSession = require('./secure_lock_session');
 
 function Lock(peripheral, offlineKey, offlineKeyOffset) {
+  if (!offlineKey) {
+    throw new Error('offlineKey must be specified when creating lock');
+  }
+  if (!offlineKeyOffset) {
+    throw new Error('offlineKeyOffset must be specified when creating lock');
+  }
+
   this._peripheral = peripheral;
-  this._offlineKey = offlineKey;
+  this._offlineKey = new Buffer(offlineKey, 'hex');
   this._offlineKeyOffset = offlineKeyOffset;
 
   debug('peripheral: ' + util.inspect(peripheral));
 }
 
+// service uuid, exposed for scanning
+Lock.BLE_COMMAND_SERVICE = "bd4ac6100b4511e38ffd0800200c9a66";
+
 Lock.prototype.connect = function() {
-  var handshakeKeys;
+  var handshakeKeys = crypto.randomBytes(16);
+  this._isSecure = false;
   return this._peripheral.connectAsync().then(function() {
     debug('connected.');
-    return this._peripheral.discoverServicesAsync([ BLE_COMMAND_SERVICE ]);
-  }.bind(this)).then(function(services) {
+
+    // run discovery; would be quicker if we could skip this step, and on linux writing
+    // directly to the appropriate handles seems to work, but unfortunately not on mac.
+    // a better approach may very well be some sort of OS level caching (ala Android), or
+    // maybe caching services in the noble library.
+    return this._peripheral.discoverSomeServicesAndCharacteristicsAsync([ Lock.BLE_COMMAND_SERVICE ], []);
+  }.bind(this)).spread(function(services, characteristics) {
     debug('services: ' + util.inspect(services));
-    if (services.length !== 1) {
-      throw new Error("expected exactly one service");
-    }
-    return services[0].discoverCharacteristicsAsync([]);
-  }).then(function(characteristics) {
     debug('characteristics: ' + util.inspect(characteristics));
 
+    function characteristicByUuid(uuid) {
+      for (var i = 0; i < characteristics.length; i++) {
+        if (characteristics[i].uuid === uuid) {
+          return characteristics[i];
+        }
+      }
+      throw new Error("could not find required characteristic with uuid: " + uuid);
+    }
+
     // initialize the secure session
-    this._secureSession = new LockSession(
-      _.findWhere(characteristics, {uuid: BLE_COMMAND_SECURE_WRITE_CHARACTERISTIC}),
-      _.findWhere(characteristics, {uuid: BLE_COMMAND_SECURE_READ_CHARACTERISTIC}),
-      true
+    this._secureSession = new SecureLockSession(
+      this._peripheral,
+      characteristicByUuid("bd4ac6130b4511e38ffd0800200c9a66"),
+      characteristicByUuid("bd4ac6140b4511e38ffd0800200c9a66"),
+      this._offlineKeyOffset
     );
-    this._secureSession.setKey(new Buffer(this._offlineKey, 'hex'));
+    this._secureSession.setKey(this._offlineKey);
 
     // intialize the session
     this._session = new LockSession(
-      _.findWhere(characteristics, {uuid: BLE_COMMAND_WRITE_CHARACTERISTIC}),
-      _.findWhere(characteristics, {uuid: BLE_COMMAND_READ_CHARACTERISTIC}),
-      false
+      this._peripheral,
+      characteristicByUuid("bd4ac6110b4511e38ffd0800200c9a66"),
+      characteristicByUuid("bd4ac6120b4511e38ffd0800200c9a66")
     );
 
     // start the sessions
@@ -65,17 +73,18 @@ Lock.prototype.connect = function() {
       this._session.start()
     );
   }.bind(this)).then(function() {
-    // generate handshake keys
-    handshakeKeys = crypto.randomBytes(16);
-
     // send SEC_LOCK_TO_MOBILE_KEY_EXCHANGE
-    var cmd = new LockCommand();
-    cmd.writeUInt8(0x01, 0x00);    // cmdSecuritySendMobileKeyWithIndex
+    var cmd = this._secureSession.buildCommand(0x01);
     handshakeKeys.copy(cmd, 0x04, 0x00, 0x08);
-    cmd.writeUInt8(0x0f, 0x10);
-    cmd.writeUInt8(this._offlineKeyOffset, 0x11);
     return this._secureSession.execute(cmd);
   }.bind(this)).then(function(response) {
+    if (response[0] !== 0x02) {
+      throw new Error("unexpected response to SEC_LOCK_TO_MOBILE_KEY_EXCHANGE: " + response.toString('hex'));
+    }
+
+    // secure session established
+    this._isSecure = true;
+
     // setup the session key
     var sessionKey = new Buffer(16);
     handshakeKeys.copy(sessionKey, 0x00, 0x00, 0x08);
@@ -86,49 +95,48 @@ Lock.prototype.connect = function() {
     this._secureSession.setKey(sessionKey);
 
     // send SEC_INITIALIZATION_COMMAND
-    var cmd = new LockCommand();
-    cmd.writeUInt8(0x03, 0x00);    // cmdSecurityInitializationCommandWithIndex
+    var cmd = this._secureSession.buildCommand(0x03);
     handshakeKeys.copy(cmd, 0x04, 0x08, 0x10);
-    cmd.writeUInt8(0x0f, 0x10);
-    cmd.writeUInt8(this._offlineKeyOffset, 0x11);
     return this._secureSession.execute(cmd);
-  }.bind(this));
+  }.bind(this)).then(function(response) {
+    if (response[0] !== 0x04) {
+      throw new Error("unexpected response to SEC_INITIALIZATION_COMMAND: " + response.toString('hex'));
+    }
+    return true;
+  });
 };
 
 Lock.prototype.lock = function() {
   debug('locking...');
-
-  var cmd = new LockCommand();
-  cmd.writeUInt8(0xee, 0x00); // magic
-  cmd.writeUInt8(0x0b, 0x01); // cmdLock
-  cmd.writeUInt8(0x05, 0x03); // simpleChecksum
-  cmd.writeUInt8(0x02, 0x10);
+  var cmd = this._session.buildCommand(0x0b);
   return this._session.execute(cmd);
 };
 
 Lock.prototype.unlock = function() {
   debug('unlocking...');
-
-  var cmd = new LockCommand();
-  cmd.writeUInt8(0xee, 0x00); // magic
-  cmd.writeUInt8(0x0a, 0x01); // cmdUnlock
-  cmd.writeUInt8(0x06, 0x03); // simpleChecksum
-  cmd.writeUInt8(0x02, 0x10);
+  var cmd = this._session.buildCommand(0x0a);
   return this._session.execute(cmd);
 };
 
 Lock.prototype.disconnect = function() {
   debug('disconnecting...');
 
-  var cmd = new LockCommand();
-  cmd.writeUInt8(0x05, 0x00);  // cmdSecurityTerminate
-  cmd.writeUInt8(0x0f, 0x10);
-  return this._secureSession.execute(cmd).finally(function() {
-    this._peripheral.disconnect();
-  }.bind(this));
+  var disconnect = function() {
+    return this._peripheral.disconnectAsync();
+  }.bind(this);
+
+  if (this._isSecure) {
+    var cmd = this._secureSession.buildCommand(0x05);
+    cmd.writeUInt8(0x00, 0x11); // zero offline key for security terminate - not sure if necessary
+    return this._secureSession.execute(cmd).then(function(response) {
+      if (response[0] !== 0x8b) {
+        throw new Error("unexpected response to DISCONNECT: " + response.toString('hex'));
+      }
+      return true;
+    }).finally(disconnect);
+  } else {
+    return disconnect();
+  }
 };
 
-// expose the service uuid
-Lock.BLE_COMMAND_SERVICE = BLE_COMMAND_SERVICE;
-
 module.exports = Lock;
diff --git a/lib/lock_session.js b/lib/lock_session.js
index c81b42e..6531f09 100644
--- a/lib/lock_session.js
+++ b/lib/lock_session.js
@@ -1,62 +1,43 @@
 'use strict';
 
+var debug = require('debug')('august:lock_session');
+
 var Promise = require('bluebird');
 var crypto = require('crypto');
-var debug = require('debug')('august');
 var events = require('events');
-var noble = require('noble');
 var util = require('util');
-var _ = require('underscore');
 
-// promisification of noble
-Promise.promisifyAll(require('noble/lib/characteristic').prototype);
-Promise.promisifyAll(require('noble/lib/peripheral').prototype);
+// promisify noble
 Promise.promisifyAll(require('noble/lib/service').prototype);
+Promise.promisifyAll(require('noble/lib/peripheral').prototype);
+Promise.promisifyAll(require('noble/lib/characteristic').prototype);
 
-// Calculates the security checksum of a command buffer.
-function securityChecksum(buffer) {
-  return (0 - (buffer.readUInt32LE(0x00) + buffer.readUInt32LE(0x04) + buffer.readUInt32LE(0x08))) >>> 0;
-}
-
-///
-// LockSession
-
-function LockSession(writeCharacteristic, readCharacteristic, isSecure) {
-  if (!writeCharacteristic || !readCharacteristic) {
-    throw new Error('write and/or read characteristic not found');
-  }
+function LockSession(peripheral, writeCharacteristic, readCharacteristic) {
+  this._peripheral = peripheral;
   this._writeCharacteristic = writeCharacteristic;
   this._readCharacteristic = readCharacteristic;
-  this._isSecure = isSecure;
   return this;
 }
 
 util.inherits(LockSession, events.EventEmitter);
 
-LockSession.prototype.setKey = function(key) {
-  var cipherSuite, iv;
-  if (this._isSecure) {
-    cipherSuite = 'aes-128-ecb';
-    iv = '';
-  } else {
-    cipherSuite = 'aes-128-cbc';
-    iv = new Buffer(0x10);
-    iv.fill(0);
-  }
+LockSession.prototype._cipherSuite = 'aes-128-cbc';
+LockSession.prototype._iv = (function() {
+  var buf = new Buffer(0x10);
+  buf.fill(0);
+  return buf;
+})();
 
-  this._encryptCipher = crypto.createCipheriv(cipherSuite, key, iv);
+LockSession.prototype.setKey = function(key) {
+  this._encryptCipher = crypto.createCipheriv(this._cipherSuite, key, this._iv);
   this._encryptCipher.setAutoPadding(false);
-  this._decryptCipher = crypto.createDecipheriv(cipherSuite, key, iv);
+  this._decryptCipher = crypto.createDecipheriv(this._cipherSuite, key, this._iv);
   this._decryptCipher.setAutoPadding(false);
 };
 
 LockSession.prototype.start = function() {
   // decrypt all reads, modifying the buffer in place
-  this._readCharacteristic.on('read', function(data, isNotify) {
-    if (!data) {
-      throw new Error('read returned no data');
-    }
-
+  this._readCharacteristic.on('read', function(data) {
     debug('read data: ' + data.toString('hex'));
 
     if (this._decryptCipher) {
@@ -67,60 +48,77 @@ LockSession.prototype.start = function() {
       debug('decrypted data: ' + data.toString('hex'));
     }
 
-    // the notification flag is not being set properly on OSX Yosemite, so just
-    // forcing it to true.
-    if (process.platform === 'darwin') {
-      isNotify = true;
-    }
-
-    if (isNotify) {
-      this.emit('notification', data);
-    }
+    this.emit('notification', data);
   }.bind(this));
 
-  // enable notifications on the read characterestic
-  debug('enabling notifications on ' + this._readCharacteristic);
+  // enable indications
+  debug('enabling indications on ' + this._readCharacteristic);
   return this._readCharacteristic.notifyAsync(true);
 };
 
-LockSession.prototype.execute = function(command) {
-  // write the security checksum if on the secure channel
-  if (this._isSecure) {
-    var checksum = securityChecksum(command);
-    command.writeUInt32LE(checksum, 0x0c);
+LockSession.prototype.buildCommand = function(opcode) {
+  var cmd = new Buffer(0x12);
+  cmd.fill(0);
+  cmd.writeUInt8(0xee, 0x00);   // magic
+  cmd.writeUInt8(opcode, 0x01);
+  cmd.writeUInt8(0x02, 0x10);   // unknown?
+  return cmd;
+};
+
+// Calculates the simple checksum of a command buffer.
+function simpleChecksum(buf) {
+  var cs = 0;
+  for (var i = 0; i < 0x12; i++) {
+    cs = (cs + buf[i]) & 0xff;
   }
+  return (-cs) & 0xff;
+}
+
+LockSession.prototype._writeChecksum = function(command) {
+  var checksum = simpleChecksum(command);
+  command.writeUInt8(checksum, 0x03);
+};
 
-  debug((this._isSecure ? 'secure ' : '') + 'execute command: ' + command.toString('hex'));
+LockSession.prototype._validateResponse = function(response) {
+  if (simpleChecksum(response) !== 0) {
+    throw new Error("simple checksum mismatch");
+  }
+  if (response[0] !== 0xbb && response[0] !== 0xaa) {
+    throw new Error("unexpected magic in response");
+  }
+};
 
+LockSession.prototype._write = function(command) {
   // NOTE: the last two bytes are not encrypted
   // general idea seems to be that if the last byte of the command indicates an offline key offset (is non-zero), the command is "secure" and encrypted with the offline key
   if (this._encryptCipher) {
     var plainText = command.slice(0x00, 0x10);
     var cipherText = this._encryptCipher.update(plainText);
     cipherText.copy(plainText);
-    debug('execute command (encrypted): ' + command.toString('hex'));
+    debug('write (encrypted): ' + command.toString('hex'));
   }
 
+  // write the command to the write characteristic
+  return this._writeCharacteristic.writeAsync(command, false);
+};
+
+LockSession.prototype.execute = function(command) {
+  this._writeChecksum(command);
+
+  debug('execute command: ' + command.toString('hex'));
+
   // register the notification event listener here, before issuing the write, as the
   // response notification arrives before the write response.
   var waitForNotification = new Promise(function(resolve) {
     this.once('notification', resolve);
   }.bind(this));
 
-  return this._writeCharacteristic.writeAsync(command, false).then(function() {
+  return this._write(command).then(function() {
     debug('write successful, waiting for notification...');
     return waitForNotification;
   }).then(function(data) {
     // perform some basic validation before passing it on
-    if (this._isSecure) {
-      if (securityChecksum(data) !== data.readUInt32LE(0x0c)) {
-        throw new Error("security checksum mismatch");
-      }
-    } else {
-      if (data[0] !== 0xbb && data[0] !== 0xaa) {
-        throw new Error("unexpected magic in response");
-      }
-    }
+    this._validateResponse(data);
 
     return data;
   }.bind(this));
diff --git a/lib/scan.js b/lib/scan.js
new file mode 100644
index 0000000..e2247d8
--- /dev/null
+++ b/lib/scan.js
@@ -0,0 +1,33 @@
+'use strict';
+
+var Promise = require('bluebird');
+var noble = require('noble');
+
+var Lock = require('./lock');
+
+var firstRun = true;
+
+function scan(uuid) {
+  if (firstRun) {
+    firstRun = false;
+
+    noble.on('stateChange', function(state) {
+      if (state === 'poweredOn') {
+        noble.startScanning([ Lock.BLE_COMMAND_SERVICE ]);
+      } else {
+        noble.stopScanning();
+      }
+    });
+  }
+
+  return new Promise(function(resolve) {
+    noble.on('discover', function(peripheral) {
+      if (uuid === undefined || peripheral.uuid === uuid) {
+        noble.stopScanning();
+        resolve(peripheral);
+      }
+    });
+  });
+}
+
+module.exports = scan;
diff --git a/lib/secure_lock_session.js b/lib/secure_lock_session.js
new file mode 100644
index 0000000..3a88dda
--- /dev/null
+++ b/lib/secure_lock_session.js
@@ -0,0 +1,46 @@
+'use strict';
+
+var util = require('util');
+
+var LockSession = require('./lock_session');
+
+function SecureLockSession(peripheral, writeCharacteristic, readCharacteristic, offlineKeyOffset) {
+  SecureLockSession.super_.call(this, peripheral, writeCharacteristic, readCharacteristic);
+  if (!offlineKeyOffset) {
+    throw new Error("offline key offset not specified");
+  }
+  this._offlineKeyOffset = offlineKeyOffset;
+  return this;
+}
+
+util.inherits(SecureLockSession, LockSession);
+
+SecureLockSession.prototype._cipherSuite = 'aes-128-ecb';
+SecureLockSession.prototype._iv = '';
+
+SecureLockSession.prototype.buildCommand = function(opcode) {
+  var cmd = new Buffer(0x12);
+  cmd.fill(0);
+  cmd.writeUInt8(opcode, 0x00);
+  cmd.writeUInt8(0x0f, 0x10);   // unknown
+  cmd.writeUInt8(this._offlineKeyOffset, 0x11);
+  return cmd;
+};
+
+// Calculates the security checksum of a command buffer.
+function securityChecksum(buffer) {
+  return (0 - (buffer.readUInt32LE(0x00) + buffer.readUInt32LE(0x04) + buffer.readUInt32LE(0x08))) >>> 0;
+}
+
+SecureLockSession.prototype._writeChecksum = function(command) {
+  var checksum = securityChecksum(command);
+  command.writeUInt32LE(checksum, 0x0c);
+};
+
+SecureLockSession.prototype._validateResponse = function(data) {
+  if (securityChecksum(data) !== data.readUInt32LE(0x0c)) {
+    throw new Error("security checksum mismatch");
+  }
+};
+
+module.exports = SecureLockSession;
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
deleted file mode 100644
index 7ca3dd5..0000000
--- a/npm-shrinkwrap.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
-  "name": "augustctl",
-  "version": "0.0.1",
-  "dependencies": {
-    "bluebird": {
-      "version": "2.3.11",
-      "from": "bluebird@>=2.3.10 <3.0.0",
-      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.3.11.tgz"
-    },
-    "debug": {
-      "version": "2.1.0",
-      "from": "debug@>=2.1.0 <3.0.0",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.0.tgz",
-      "dependencies": {
-        "ms": {
-          "version": "0.6.2",
-          "from": "ms@0.6.2",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz"
-        }
-      }
-    },
-    "noble": {
-      "version": "0.3.6",
-      "from": "noble@>=0.3.6 <0.4.0",
-      "resolved": "https://registry.npmjs.org/noble/-/noble-0.3.6.tgz",
-      "dependencies": {
-        "debug": {
-          "version": "0.7.4",
-          "from": "debug@>=0.7.2 <0.8.0",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz"
-        }
-      }
-    },
-    "underscore": {
-      "version": "1.7.0",
-      "from": "underscore@>=1.7.0 <2.0.0",
-      "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz"
-    },
-    "yargs": {
-      "version": "1.3.3",
-      "from": "yargs@>=1.3.3 <2.0.0",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-1.3.3.tgz"
-    }
-  }
-}
diff --git a/package.json b/package.json
index bb689cf..7af3d5e 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,11 @@
   "description": "Controller for August Smart Lock",
   "author": "Dan Walters <dan@walters.io>",
   "license": "MIT",
-  "main": "august.js",
-  "bin": "./cli.js",
+  "main": "index.js",
+  "bin": {
+    "augustctl": "./cli.js",
+    "augustctld": "./server.js"
+  },
   "keywords": [
     "august",
     "smartlock",
@@ -14,9 +17,9 @@
   "dependencies": {
     "bluebird": "^2.3.10",
     "debug": "^2.1.0",
-    "noble": "^0.3.6",
-    "underscore": "^1.7.0",
-    "yargs": "^1.3.3"
+    "express": "^4.10.2",
+    "morgan": "^1.5.0",
+    "noble": "^0.3.6"
   },
   "repository": {
     "type": "git",
-- 
GitLab