From 9c82267d92ea7dc664255c831c7816bf90c7636c Mon Sep 17 00:00:00 2001 From: Johann Og Date: Sun, 21 Aug 2016 15:45:25 +0200 Subject: [PATCH 1/8] Implement channel generation --- .gitignore | 42 ++++++++++++++++++++++++++++++++++++++++++ src/Setup.iced | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7c621c --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +policies +*.pub.asc +.localstorage +rethinkdb_data diff --git a/src/Setup.iced b/src/Setup.iced index f878ba1..c3e16f1 100644 --- a/src/Setup.iced +++ b/src/Setup.iced @@ -1,12 +1,16 @@ 'use strict' Config = require './Config' +Crypto = require './Crypto' inquirer = require 'inquirer' colors = require 'colors' fs = require 'fs' os = require 'os' kbpgp = require 'kbpgp' progress = require 'progress' +pgpWordList = require 'pgp-word-list-converter' +crypto = require 'crypto' +Vault = require './Vault' localStorage = new require 'node-localstorage' .LocalStorage(Config.local_storage) @@ -54,6 +58,28 @@ class Configure localStorage.setItem 'watcher_pub_key', watcher_pub_key localStorage.setItem 'client_pub_key', client_pub_key localStorage.setItem 'vault', answers.vault + + # test + + await new Crypto watcher_priv_key, client_pub_key, defer cryptoBox + watcherFP = cryptoBox.watcherKey.get_pgp_fingerprint().toString('hex') + clientFP = cryptoBox.clientKey.get_pgp_fingerprint().toString('hex') + + channel = + channel: @generateChannel() + creator: watcherFP + watcher: watcher_pub_key + expires: @getExpirationDate() + + await new Vault this, Config.vault, watcherFP, clientFP, defer vault + console.log "about to save" + await vault.save 'channel', channel, defer saved + console.log saved + + localStorage.setItem 'channel', JSON.stringify channel, null, 4 + # end test + + @done = true keygen : (identity, cb, pcb) => @@ -72,5 +98,14 @@ class Configure b = breakBefore and "\n" or "" console.log "#{b}! ".green + text.bold + generateChannel : () => + word = Math.random().toString(36).substring(2) + crypto.createHash('md5').update(word).digest("hex").substr(0, 8) + + getExpirationDate : () => + now = new Date() + now.setMinutes(now.getMinutes() + 5) + now.toString() + new Configure() From fe9da22e71a93a3b2f4ffce9803d5f5835e1adc9 Mon Sep 17 00:00:00 2001 From: Johann Og Date: Mon, 22 Aug 2016 23:40:46 +0200 Subject: [PATCH 2/8] Replace pgp key for words, basic functionality --- package.json | 2 +- src/Crypto.iced | 13 ++++++---- src/Export.iced | 8 ++++-- src/Main.iced | 2 +- src/Setup.iced | 68 ++++++++++++++++++++++++++++--------------------- src/Vault.iced | 15 ++++++----- 6 files changed, 64 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index f51ea0d..15de775 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "start": "./scripts/start", "export": "./scripts/export", "service": "./scripts/service", - "setup": "./scripts/setup && ./scripts/export && ./scripts/service" + "setup": "./scripts/setup && ./scripts/service" }, "bin": { "trailbot-watcher": "./scripts/start" diff --git a/src/Crypto.iced b/src/Crypto.iced index d708408..51b818a 100644 --- a/src/Crypto.iced +++ b/src/Crypto.iced @@ -9,16 +9,20 @@ class Crypto constructor : (watcherArmored, clientArmored, cb) -> esc = make_esc (err) -> console.error "[CRYPTO] #{err}" - mKey = {armored: clientArmored} + wKey = {armored: watcherArmored} - await kbpgp.KeyManager.import_from_armored_pgp mKey, esc defer @clientKey + await kbpgp.KeyManager.import_from_armored_pgp wKey, esc defer @watcherKey if @watcherKey.is_pgp_locked() await @watcherKey.unlock_pgp { passphrase: '' }, esc defer() @ring = new kbpgp.keyring.KeyRing - for km in [@clientKey, @watcherKey] - @ring.add_key_manager km + @ring.add_key_manager @watcherKey + + if clientArmored + mKey = {armored: clientArmored} + await kbpgp.KeyManager.import_from_armored_pgp mKey, esc defer @clientKey + @ring.add_key_manager @clientKey cb this @@ -47,4 +51,3 @@ class Crypto cb extend data, JSON.parse literals[0].toString() module.exports = Crypto - diff --git a/src/Export.iced b/src/Export.iced index 2abe201..33820f2 100644 --- a/src/Export.iced +++ b/src/Export.iced @@ -14,6 +14,7 @@ class Exporter return if process.argv[2] + console.log "indice argv ", process.argv[2] await fs.writeFile process.argv[2], Config.watcher_pub_key, {encoding: 'utf8'}, defer err, res if err console.error 'Invalid output path: directory does not exist or writing to it is not allowed.'.red @@ -36,7 +37,9 @@ class Exporter ] .then ({output}) -> if output is 'stdio' - console.log Config.watcher_pub_key + # console.log Config.watcher_pub_key + console.log "\nSentence:" + console.log "#{Config.sentence}\n".cyan.bold else if output is 'filesystem' inquirer.prompt [ name: 'path' @@ -46,7 +49,8 @@ class Exporter validate: (path) -> new Promise (next) -> console.log - await fs.writeFile path, Config.watcher_pub_key, {encoding: 'utf8'}, defer err, res + await fs.writeFile path, Config.sentence, {encoding: 'utf8'}, defer err, res + # await fs.writeFile path, Config.watcher_pub_key, {encoding: 'utf8'}, defer err, res next err && 'Invalid output path: directory does not exist or writing permission is not granted.' || true ] .then ({path}) -> diff --git a/src/Main.iced b/src/Main.iced index da39302..88b24ab 100644 --- a/src/Main.iced +++ b/src/Main.iced @@ -46,7 +46,7 @@ app = class App extends EventEmitter console.log '[WATCHER] Watcher fingerprint:', @watcherFP console.log '[WATCHER] Client fingerprint', @clientFP - await new Vault this, Config.vault, @watcherFP, @clientFP, defer @vault + await new Vault this, Config.vault, @watcherFP, defer @vault @emit 'vaultConnected' console.log '[WATCHER] Connected to vault' diff --git a/src/Setup.iced b/src/Setup.iced index c3e16f1..c8a80dd 100644 --- a/src/Setup.iced +++ b/src/Setup.iced @@ -11,12 +11,13 @@ progress = require 'progress' pgpWordList = require 'pgp-word-list-converter' crypto = require 'crypto' Vault = require './Vault' -localStorage = new require 'node-localstorage' - .LocalStorage(Config.local_storage) + class Configure constructor : -> + @localStorage = new require 'node-localstorage' + .LocalStorage(Config.local_storage) @done = false process.on 'exit', => unless @done @@ -29,20 +30,12 @@ class Configure message: "Choose a name for this watcher" type: 'input' default: os.hostname() - , - name: 'clientKey' - message: "Type the route for the client's public key" - type: 'input' - default: './trailbot_client.pub.asc' - validate: (path) -> - new Promise (next) -> - fs.readFile path, {encode: 'utf8'}, (err, content) -> - next err or true , name: 'vault' - message: "Type the FQDN and port of the vault server you want to use" + message: "Type the domain and port of the vault server you want to use" type: 'input' - default: 'vault.trailbot.io:8443' + #TODO set i.t back to production 'vault.trailbot.io:8443' + default: 'localhost:8443' ] .then (answers) => @alert "Ok, we are now generating a new PGP keypar for this watcher.", true @@ -53,34 +46,50 @@ class Configure incomplete: ' ' width: 50 await @keygen answers.hostname, defer watcher_priv_key, watcher_pub_key - await fs.readFile answers.clientKey, {encode: 'utf8'}, defer err, client_pub_key - localStorage.setItem 'watcher_priv_key', watcher_priv_key - localStorage.setItem 'watcher_pub_key', watcher_pub_key - localStorage.setItem 'client_pub_key', client_pub_key - localStorage.setItem 'vault', answers.vault + @localStorage.setItem 'watcher_priv_key', watcher_priv_key + @localStorage.setItem 'watcher_pub_key', watcher_pub_key + @localStorage.setItem 'vault', answers.vault - # test - await new Crypto watcher_priv_key, client_pub_key, defer cryptoBox + + # test + await new Crypto watcher_priv_key, null, defer cryptoBox watcherFP = cryptoBox.watcherKey.get_pgp_fingerprint().toString('hex') - clientFP = cryptoBox.clientKey.get_pgp_fingerprint().toString('hex') - channel = + exchange = channel: @generateChannel() creator: watcherFP watcher: watcher_pub_key expires: @getExpirationDate() - await new Vault this, Config.vault, watcherFP, clientFP, defer vault - console.log "about to save" - await vault.save 'channel', channel, defer saved - console.log saved + sentence = pgpWordList.toWords(exchange.channel).toString().replace(/,/g,' ') + + @done = true + @alert "Now install Trailbot Client in your computer and start the setup wizard." , true + @alert "Then you will be required to enter these 4 words:" + @alert "#{sentence}".cyan.bold, true + + await new Vault this, answers.vault, watcherFP, defer vault + await vault.save 'exchange', exchange, defer saved + + @alert "Waiting for confirmation from Trailbot Client..." , true + watch = vault.watch 'exchange', exchange, (change) => + if change.client + console.log "change" + @localStorage.setItem 'client_pub_key', change.client + return + + watch.unsubscribe() + console.log "remove id",change.id + vault.remove 'exchange', [change], (res) => + console.log res + + + console.log "exit" + - localStorage.setItem 'channel', JSON.stringify channel, null, 4 - # end test - @done = true keygen : (identity, cb, pcb) => opts = @@ -108,4 +117,5 @@ class Configure now.toString() + new Configure() diff --git a/src/Vault.iced b/src/Vault.iced index a38e8de..d9566d4 100644 --- a/src/Vault.iced +++ b/src/Vault.iced @@ -2,7 +2,7 @@ Config = require './Config' Horizon = require '@horizon/client/dist/horizon' class Vault - constructor : (app, host, watcherFP, clientFP, cb) -> + constructor : (app, host, watcherFP, cb) -> @app = app authType = @getToken() secure = Config.secure @@ -12,6 +12,7 @@ class Vault @users = @hz 'users' @settings = @hz 'settings' @events = @hz 'events' + @exchange = @hz 'exchange' @hz.onReady () => token = JSON.parse(@hz.utensils.tokenStorage._storage._storage.get('horizon-jwt')).horizon @@ -21,15 +22,15 @@ class Vault me.data = key: watcherFP @users.replace me - console.log 'Me:', me - @app.emit 'vaultLoggedIn', me + console.log 'Me:', me if @app.emit + @app.emit 'vaultLoggedIn', me if @app.emit cb and cb this @hz.onDisconnected (e) => unless @retried @retried = true @app.localStorage.removeItem 'horizon_jwt' - @constructor app, host, watcherFP, clientFP, cb + @constructor app, host, watcherFP, cb getToken : () -> jwt = @app.localStorage.getItem 'horizon_jwt' @@ -39,8 +40,8 @@ class Vault 'anonymous' save : (col, object, cb) -> - console.log "Saving into #{col}" - console.log 'SAVING', object + console.log "Saving into #{col}" if @app.emit + console.log 'SAVING', object if @app.emit this[col]?.store object cb and cb true @@ -62,4 +63,6 @@ class Vault res = this[col].removeAll(ids) cb and cb res + + module.exports = Vault From 0089f9d0cd18801fe0cb413c93174cf5a47078f2 Mon Sep 17 00:00:00 2001 From: Johann Og Date: Tue, 23 Aug 2016 00:39:55 +0200 Subject: [PATCH 3/8] delete exchange document --- src/Setup.iced | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Setup.iced b/src/Setup.iced index c8a80dd..dd32d1d 100644 --- a/src/Setup.iced +++ b/src/Setup.iced @@ -77,15 +77,11 @@ class Configure if change.client console.log "change" @localStorage.setItem 'client_pub_key', change.client - return - - watch.unsubscribe() - console.log "remove id",change.id - vault.remove 'exchange', [change], (res) => - console.log res - - - console.log "exit" + watch.unsubscribe() + console.log "remove id",change.id + vault.remove 'exchange', [change], (res) => + console.log "file deleted" + process.exit 0 From 0bb16dc8bce28d783f5129154418b4cf63ebdbf8 Mon Sep 17 00:00:00 2001 From: Johann Og Date: Tue, 23 Aug 2016 12:08:56 +0200 Subject: [PATCH 4/8] Fix bug horizon document not being deleted --- src/Setup.iced | 14 +++++++------- src/Vault.iced | 17 +++++++---------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/Setup.iced b/src/Setup.iced index dd32d1d..cbdef92 100644 --- a/src/Setup.iced +++ b/src/Setup.iced @@ -70,18 +70,18 @@ class Configure @alert "#{sentence}".cyan.bold, true await new Vault this, answers.vault, watcherFP, defer vault - await vault.save 'exchange', exchange, defer saved - + vault.save 'exchange', exchange @alert "Waiting for confirmation from Trailbot Client..." , true - watch = vault.watch 'exchange', exchange, (change) => - if change.client - console.log "change" + vault.watch 'exchange', exchange, (change) => + # if change is null the document was deleted + process.exit 0 unless change + if change && change.client @localStorage.setItem 'client_pub_key', change.client - watch.unsubscribe() console.log "remove id",change.id vault.remove 'exchange', [change], (res) => console.log "file deleted" - process.exit 0 + + diff --git a/src/Vault.iced b/src/Vault.iced index d9566d4..bf0579a 100644 --- a/src/Vault.iced +++ b/src/Vault.iced @@ -45,23 +45,20 @@ class Vault this[col]?.store object cb and cb true - replace : (col, object, cb) -> + replace : (col, object) -> console.log "Replacing into #{col}" this[col]?.replace object - cb and cb true get : (col, query, cb) -> - this[col]?.find(query).fetch().subscribe (items) -> - cb and cb items + this[col]?.find(query).fetch().defaultIfEmpty().subscribe(cb) - watch : (col, query, cb) -> - this[col]?.find(query).watch().subscribe (items) -> - cb and cb items + watch : (col, query, cb, err) -> + this[col]?.find(query).watch().subscribe(cb, err) - remove : (col, ids, cb) -> + remove : (col, ids) -> console.log "Removing from #{col}" - res = this[col].removeAll(ids) - cb and cb res + this[col].removeAll(ids) + From 0ca856863e3a88fd06a783ea1538ecae4517b807 Mon Sep 17 00:00:00 2001 From: Johann Og Date: Tue, 23 Aug 2016 12:17:46 +0200 Subject: [PATCH 5/8] Change from 4 words to 8 words validation --- src/Setup.iced | 8 ++------ src/Vault.iced | 7 +++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Setup.iced b/src/Setup.iced index cbdef92..7808024 100644 --- a/src/Setup.iced +++ b/src/Setup.iced @@ -50,9 +50,6 @@ class Configure @localStorage.setItem 'watcher_pub_key', watcher_pub_key @localStorage.setItem 'vault', answers.vault - - - # test await new Crypto watcher_priv_key, null, defer cryptoBox watcherFP = cryptoBox.watcherKey.get_pgp_fingerprint().toString('hex') @@ -66,7 +63,7 @@ class Configure @done = true @alert "Now install Trailbot Client in your computer and start the setup wizard." , true - @alert "Then you will be required to enter these 4 words:" + @alert "The following 8 words will be required by the Trailbot Client:" @alert "#{sentence}".cyan.bold, true await new Vault this, answers.vault, watcherFP, defer vault @@ -77,7 +74,6 @@ class Configure process.exit 0 unless change if change && change.client @localStorage.setItem 'client_pub_key', change.client - console.log "remove id",change.id vault.remove 'exchange', [change], (res) => console.log "file deleted" @@ -105,7 +101,7 @@ class Configure generateChannel : () => word = Math.random().toString(36).substring(2) - crypto.createHash('md5').update(word).digest("hex").substr(0, 8) + crypto.createHash('md5').update(word).digest("hex").substr(0, 16) getExpirationDate : () => now = new Date() diff --git a/src/Vault.iced b/src/Vault.iced index bf0579a..56bd775 100644 --- a/src/Vault.iced +++ b/src/Vault.iced @@ -39,14 +39,13 @@ class Vault else 'anonymous' - save : (col, object, cb) -> + save : (col, object) -> console.log "Saving into #{col}" if @app.emit console.log 'SAVING', object if @app.emit this[col]?.store object - cb and cb true replace : (col, object) -> - console.log "Replacing into #{col}" + console.log "Replacing into #{col}" if @app.emit this[col]?.replace object get : (col, query, cb) -> @@ -56,7 +55,7 @@ class Vault this[col]?.find(query).watch().subscribe(cb, err) remove : (col, ids) -> - console.log "Removing from #{col}" + console.log "Removing from #{col}" if @app.emit this[col].removeAll(ids) From aca222b7602185d63701c8f7750dbfa031268c22 Mon Sep 17 00:00:00 2001 From: Johann Og Date: Tue, 23 Aug 2016 22:05:50 +0200 Subject: [PATCH 6/8] Add security by generating new validation words every x minutes --- package.json | 2 +- src/Setup.iced | 33 ++++++++++++++++++++++----------- src/Vault.iced | 7 ++++--- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 15de775..25709fe 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "trailbot-watcher", "version": "0.1.5", "dependencies": { - "@horizon/client": "^1.1.1", + "@horizon/client": "^1.1.3", "chokidar": "^1.5.1", "coffee-script": "^1.10.0", "colors": "^1.1.2", diff --git a/src/Setup.iced b/src/Setup.iced index 7808024..7c674a4 100644 --- a/src/Setup.iced +++ b/src/Setup.iced @@ -34,12 +34,11 @@ class Configure name: 'vault' message: "Type the domain and port of the vault server you want to use" type: 'input' - #TODO set i.t back to production 'vault.trailbot.io:8443' - default: 'localhost:8443' + default: 'vault.trailbot.io:8443' ] .then (answers) => @alert "Ok, we are now generating a new PGP keypar for this watcher.", true - @alert "This may take up to a couple of minutes. Please wait while magic happens...\n " + @alert "This may take up to a couple of minutes. Please wait while the magic happens...\n " @progress = new progress ' Generating... [:bar] :percent'.bold, total: 330 complete: '=' @@ -59,26 +58,35 @@ class Configure watcher: watcher_pub_key expires: @getExpirationDate() - sentence = pgpWordList.toWords(exchange.channel).toString().replace(/,/g,' ') - @done = true + @alert "Now install Trailbot Client in your computer and start the setup wizard." , true @alert "The following 8 words will be required by the Trailbot Client:" - @alert "#{sentence}".cyan.bold, true + @alert "#{@channelToWords(exchange.channel)}".cyan.bold, true await new Vault this, answers.vault, watcherFP, defer vault - vault.save 'exchange', exchange + await vault.save 'exchange', exchange, defer {id} + process.exit 1 unless id + exchange.id = id + @alert "Waiting for confirmation from Trailbot Client..." , true - vault.watch 'exchange', exchange, (change) => + vault.watch 'exchange', exchange.id, (change) => # if change is null the document was deleted process.exit 0 unless change - if change && change.client + if change?.client + console.log "storage..." @localStorage.setItem 'client_pub_key', change.client vault.remove 'exchange', [change], (res) => console.log "file deleted" - - + # every 5 minutes generate new words + setInterval => + exchange.channel = @generateChannel() + vault.replace 'exchange', exchange + @alert "Time to get confirmation from Trailbot Client expired", true + @alert "New words generated" + @alert "#{@channelToWords(exchange.channel)}".cyan.bold, true + , 350000 @@ -108,6 +116,9 @@ class Configure now.setMinutes(now.getMinutes() + 5) now.toString() + channelToWords : (channel) => + pgpWordList.toWords(channel).toString().replace(/,/g,' ') + new Configure() diff --git a/src/Vault.iced b/src/Vault.iced index 56bd775..6914a0b 100644 --- a/src/Vault.iced +++ b/src/Vault.iced @@ -39,10 +39,10 @@ class Vault else 'anonymous' - save : (col, object) -> + save : (col, object, cb) -> console.log "Saving into #{col}" if @app.emit console.log 'SAVING', object if @app.emit - this[col]?.store object + this[col]?.store(object).subscribe(cb) replace : (col, object) -> console.log "Replacing into #{col}" if @app.emit @@ -58,7 +58,8 @@ class Vault console.log "Removing from #{col}" if @app.emit this[col].removeAll(ids) - + getCollection : () -> + @exchange module.exports = Vault From b342374d02d27bae0f26f67166da60380d96643a Mon Sep 17 00:00:00 2001 From: Johann Og Date: Wed, 24 Aug 2016 12:00:25 +0200 Subject: [PATCH 7/8] Set expiration time for biometric sentences --- package.json | 1 + src/Setup.iced | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 25709fe..f74a22f 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "mkdirp": "^0.5.1", "node-localstorage": "^1.3.0", "npm": "^3.9.5", + "pgp-word-list-converter": "^1.0.0", "progress": "^1.1.8", "simple-git": "^1.37.0", "sleep": "^3.0.1", diff --git a/src/Setup.iced b/src/Setup.iced index 7c674a4..ebc8f0a 100644 --- a/src/Setup.iced +++ b/src/Setup.iced @@ -8,7 +8,7 @@ fs = require 'fs' os = require 'os' kbpgp = require 'kbpgp' progress = require 'progress' -pgpWordList = require 'pgp-word-list-converter' +pgpWordList = require('pgp-word-list-converter')() crypto = require 'crypto' Vault = require './Vault' @@ -74,7 +74,6 @@ class Configure # if change is null the document was deleted process.exit 0 unless change if change?.client - console.log "storage..." @localStorage.setItem 'client_pub_key', change.client vault.remove 'exchange', [change], (res) => console.log "file deleted" @@ -86,7 +85,7 @@ class Configure @alert "Time to get confirmation from Trailbot Client expired", true @alert "New words generated" @alert "#{@channelToWords(exchange.channel)}".cyan.bold, true - , 350000 + , 300000 From 3d0ce67b788f33f3f7e09c6c42e73ee6823405eb Mon Sep 17 00:00:00 2001 From: Johann Og Date: Mon, 29 Aug 2016 12:51:56 +0200 Subject: [PATCH 8/8] Minor style change --- src/Setup.iced | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Setup.iced b/src/Setup.iced index ebc8f0a..8e78dcf 100644 --- a/src/Setup.iced +++ b/src/Setup.iced @@ -59,16 +59,17 @@ class Configure expires: @getExpirationDate() @done = true - - @alert "Now install Trailbot Client in your computer and start the setup wizard." , true - @alert "The following 8 words will be required by the Trailbot Client:" - @alert "#{@channelToWords(exchange.channel)}".cyan.bold, true + console.log '\n' await new Vault this, answers.vault, watcherFP, defer vault await vault.save 'exchange', exchange, defer {id} process.exit 1 unless id exchange.id = id + @alert "Now install Trailbot Client in your computer and start the setup wizard." , true + @alert "The following 8 words will be required by Trailbot Client:" + @alert "#{@channelToWords(exchange.channel)}".cyan.bold, true + @alert "Waiting for confirmation from Trailbot Client..." , true vault.watch 'exchange', exchange.id, (change) => # if change is null the document was deleted