From 092d994ec541b5dbeea2ee87958de6f477afbdaf Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 11 Aug 2016 17:04:09 -0700 Subject: [PATCH 1/7] initial sketch --- bin/now | 6 +- bin/now-deploy | 1 + bin/now-domains | 2 +- bin/now-secrets | 175 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/secrets.js | 36 ++++++++++ 5 files changed, 216 insertions(+), 4 deletions(-) create mode 100755 bin/now-secrets create mode 100644 lib/secrets.js diff --git a/bin/now b/bin/now index f160437..1de2cdf 100755 --- a/bin/now +++ b/bin/now @@ -19,8 +19,8 @@ const exit = (code) => { }; const defaultCommand = 'deploy'; -const commands = new Set([defaultCommand, 'list', 'ls', 'rm', 'remove', 'alias', 'aliases', 'ln', 'domain', 'domains']); -const aliases = new Map([['ls', 'list'], ['rm', 'remove'], ['ln', 'alias'], ['aliases', 'alias'], ['domain', 'domains']]); +const commands = new Set([defaultCommand, 'list', 'ls', 'rm', 'remove', 'alias', 'aliases', 'ln', 'domain', 'domains', 'secret', 'secrets']); +const aliases = new Map([['ls', 'list'], ['rm', 'remove'], ['ln', 'alias'], ['aliases', 'alias'], ['domain', 'domains'], ['secret', 'secrets']]); let cmd = argv._[0]; let args = []; @@ -41,7 +41,7 @@ if (commands.has(cmd)) { let bin = resolve(__dirname, 'now-' + cmd); if (process.enclose) { - args.unshift("--entrypoint", bin); + args.unshift('--entrypoint', bin); bin = process.execPath; } diff --git a/bin/now-deploy b/bin/now-deploy index 6f7d8ec..1d6f277 100755 --- a/bin/now-deploy +++ b/bin/now-deploy @@ -41,6 +41,7 @@ const help = () => { rm | remove [id] remove a deployment ln | alias [id] [url] configures aliases for deployments domains [name] manages your domain names + secrets [name] manages your env secrets help [cmd] displays complete help for [cmd] ${chalk.dim('Options:')} diff --git a/bin/now-domains b/bin/now-domains index e989c94..b2491d9 100755 --- a/bin/now-domains +++ b/bin/now-domains @@ -21,7 +21,7 @@ const subcommand = argv._[0]; // options const help = () => { console.log(` - ${chalk.bold('𝚫 now domains')} + ${chalk.bold('𝚫 now domains')} ${chalk.dim('Options:')} diff --git a/bin/now-secrets b/bin/now-secrets new file mode 100755 index 0000000..a94a5da --- /dev/null +++ b/bin/now-secrets @@ -0,0 +1,175 @@ +#!/usr/bin/env node +import chalk from 'chalk'; +import minimist from 'minimist'; +import * as cfg from '../lib/cfg'; +import { error } from '../lib/error'; +import NowSecrets from '../lib/secrets'; +import ms from 'ms'; + +const argv = minimist(process.argv.slice(2), { + boolean: ['help', 'debug', 'base64'], + alias: { + help: 'h', + debug: 'd', + base64: 'b' + } +}); +const subcommand = argv._[0]; + +// options +const help = () => { + console.log(` + ${chalk.bold('𝚫 now secrets')} + + ${chalk.dim('Options:')} + + -h, --help output usage information + -b, --base64 treat value as base64-encoded + -d, --debug debug mode [off] + + ${chalk.dim('Examples:')} + + ${chalk.gray('–')} Lists all your secrets: + + ${chalk.cyan('$ now secrets ls')} + + ${chalk.gray('–')} Adds a new secret: + + ${chalk.cyan('$ now secrets add my-secret "my value"')} + + ${chalk.gray('–')} Once added, a secret's value can't be retrieved in plaintext anymore + ${chalk.gray('–')} If the secret's value is more than one word, wrap it in quotes + ${chalk.gray('–')} Actually, when in doubt, wrap your value in quotes + + ${chalk.gray('–')} Exposes a secret as an env variable: + + ${chalk.cyan(`$ now -e MY_SECRET=${chalk.bold('@my-secret')}`)} + + Notice the ${chalk.cyan.bold('`@`')} symbol which makes the value a secret reference. + + ${chalk.gray('–')} Renames a secret: + + ${chalk.cyan(`$ now secrets rename my-secret my-renamed-secret`)} + + ${chalk.gray('–')} Removes a secret: + + ${chalk.cyan(`$ now secrets rm my-secret`)} +`); +}; + +// options +const debug = argv.debug; +const apiUrl = argv.url || 'https://api.zeit.co'; + +const exit = (code) => { + // we give stdout some time to flush out + // because there's a node bug where + // stdout writes are asynchronous + // https://github.com/nodejs/node/issues/6456 + setTimeout(() => process.exit(code || 0), 100); +}; + +if (argv.help || !subcommand) { + help(); + exit(0); +} else { + const config = cfg.read(); + + Promise.resolve(config.token || login(apiUrl)) + .then(async (token) => { + try { + await run(token); + } catch (err) { + if (err.userError) { + error(err.message); + } else { + error(`Unknown error: ${err.stack}`); + } + exit(1); + } + }) + .catch((e) => { + error(`Authentication error – ${e.message}`); + exit(1); + }); +} + +async function run (token) { + const secrets = new NowSecrets(apiUrl, token, { debug }); + const args = argv._.slice(1); + const start = Date.now(); + + if ('ls' === subcommand || 'list' === subcommand) { + if (0 !== args.length) { + error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret ls`')}`); + return exit(1); + } + + const list = []; + + const elapsed = ms(new Date() - start); + console.log(`> ${list.length} secrets found ${chalk.gray(`[${elapsed}]`)}`); + return secrets.close(); + } + + if ('rm' === subcommand || 'remove' === subcommand) { + if (1 !== args.length) { + error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret rm `')}`); + return exit(1); + } + + const elapsed = ms(new Date() - start); + console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.name)} ${chalk.gray(`(${secret.uid})`)} removed ${chalk.gray(`[${elapsed}]`)}`); + return secrets.close(); + } + + if ('rename' === subcommand) { + if (2 !== args.length) { + error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret rename `')}`); + return exit(1); + } + + const secret = { + name: 'my-password', + uid: 'sec_iuh32u23bfigf2gu' + }; + + const elapsed = ms(new Date() - start); + console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.name)} ${chalk.gray(`(${secret.uid})`)} renamed to ${chalk.bold('my-secret-name')} ${chalk.gray(`[${elapsed}]`)}`); + return secrets.close(); + } + + if ('add' === subcommand || 'set' === subcommand) { + if (2 !== args.length) { + error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret add `')}`); + if (args.length > 2) { + const [, ...rest] = args; + console.log('> If your secret has spaces, make sure to wrap it in quotes. Example: \n' + + ` ${chalk.cyan('$ now secret add ${args[0]} "${escaped}"')} `); + } + return exit(1); + } + + const [name, value_] = args; + + let value; + if (argv.base64) { + value = { base64: value_ }; + } else { + value = value_; + } + + const secret = { + name: 'my-password', + uid: 'sec_iuh32u23bfigf2gu' + }; + + const elapsed = ms(new Date() - start); + console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.name)} ${chalk.gray(`(${secret.uid})`)} added ${chalk.gray(`[${elapsed}]`)}`); + return secrets.close(); + } + + error('Please specify a valid subcommand: ls | add | rename | rm'); + help(); + exit(1); +} diff --git a/lib/secrets.js b/lib/secrets.js new file mode 100644 index 0000000..b4b2820 --- /dev/null +++ b/lib/secrets.js @@ -0,0 +1,36 @@ +import Now from '../lib'; + +export default class Secrets extends Now { + + async ls () { + return this.retry(async (bail, attempt) => { + if (this._debug) console.time(`> [debug] #${attempt} GET /secrets`); + const res = await this._fetch('/secrets'); + if (this._debug) console.timeEnd(`> [debug] #${attempt} GET /secrets`); + const body = await res.json(); + return body.secrets; + }); + } + + async rm (nameOrId) { + return this.retry(async (bail, attempt) => { + if (this._debug) console.time(`> [debug] #${attempt} DELETE /secrets/${nameOrId}`); + const res = await this._fetch(`/secrets/${nameOrId}`, { method: 'DELETE' }); + if (this._debug) console.timeEnd(`> [debug] #${attempt} DELETE /secrets/${nameOrId}`); + + if (403 === res.status) { + return bail(new Error('Unauthorized')); + } + + if (res.status !== 200) { + const body = await res.json(); + throw new Error(body.error.message); + } + }); + } + + async add (name, value) { + + } + +} From 3025a091a5c65094916bca42d66cdd2fab15154a Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Aug 2016 09:00:21 -0700 Subject: [PATCH 2/7] fix try/catch in async with older babel-runtime :\ --- bin/now-secrets | 48 +++++++++++++++--------------- lib/secrets.js | 78 +++++++++++++++++++++++++++++++++++++++++++++---- package.json | 4 +-- 3 files changed, 97 insertions(+), 33 deletions(-) diff --git a/bin/now-secrets b/bin/now-secrets index a94a5da..0effad1 100755 --- a/bin/now-secrets +++ b/bin/now-secrets @@ -1,8 +1,9 @@ #!/usr/bin/env node import chalk from 'chalk'; +import table from 'text-table'; import minimist from 'minimist'; import * as cfg from '../lib/cfg'; -import { error } from '../lib/error'; +import { handleError, error } from '../lib/error'; import NowSecrets from '../lib/secrets'; import ms from 'ms'; @@ -80,11 +81,7 @@ if (argv.help || !subcommand) { try { await run(token); } catch (err) { - if (err.userError) { - error(err.message); - } else { - error(`Unknown error: ${err.stack}`); - } + handleError(err); exit(1); } }) @@ -104,11 +101,19 @@ async function run (token) { error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret ls`')}`); return exit(1); } - - const list = []; - + const list = await secrets.ls(); const elapsed = ms(new Date() - start); console.log(`> ${list.length} secrets found ${chalk.gray(`[${elapsed}]`)}`); + const cur = Date.now(); + const out = table(list.map((secret) => { + return [ + '', + secret.uid, + chalk.bold(secret.name), + chalk.gray(ms(cur - new Date(secret.created)) + ' ago') + ]; + }), { align: ['l', 'r', 'l'], hsep: ' '.repeat(3) }); + if (out) console.log('\n' + out + '\n'); return secrets.close(); } @@ -117,7 +122,7 @@ async function run (token) { error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret rm `')}`); return exit(1); } - + const secret = await secrets.rm(args[0]); const elapsed = ms(new Date() - start); console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.name)} ${chalk.gray(`(${secret.uid})`)} removed ${chalk.gray(`[${elapsed}]`)}`); return secrets.close(); @@ -128,12 +133,7 @@ async function run (token) { error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret rename `')}`); return exit(1); } - - const secret = { - name: 'my-password', - uid: 'sec_iuh32u23bfigf2gu' - }; - + const secret = await secrets.patch(args[0], args[1]); const elapsed = ms(new Date() - start); console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.name)} ${chalk.gray(`(${secret.uid})`)} renamed to ${chalk.bold('my-secret-name')} ${chalk.gray(`[${elapsed}]`)}`); return secrets.close(); @@ -149,23 +149,16 @@ async function run (token) { } return exit(1); } - const [name, value_] = args; - let value; if (argv.base64) { value = { base64: value_ }; } else { value = value_; } - - const secret = { - name: 'my-password', - uid: 'sec_iuh32u23bfigf2gu' - }; - + const secret = await secrets.add(name, value); const elapsed = ms(new Date() - start); - console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.name)} ${chalk.gray(`(${secret.uid})`)} added ${chalk.gray(`[${elapsed}]`)}`); + console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(name)} ${chalk.gray(`(${secret.uid})`)} added ${chalk.gray(`[${elapsed}]`)}`); return secrets.close(); } @@ -173,3 +166,8 @@ async function run (token) { help(); exit(1); } + +process.on('uncaughtException', (err) => { + handleError(err); + exit(1); +}); diff --git a/lib/secrets.js b/lib/secrets.js index b4b2820..3c3dc1e 100644 --- a/lib/secrets.js +++ b/lib/secrets.js @@ -2,7 +2,7 @@ import Now from '../lib'; export default class Secrets extends Now { - async ls () { + ls () { return this.retry(async (bail, attempt) => { if (this._debug) console.time(`> [debug] #${attempt} GET /secrets`); const res = await this._fetch('/secrets'); @@ -12,7 +12,7 @@ export default class Secrets extends Now { }); } - async rm (nameOrId) { + rm (nameOrId) { return this.retry(async (bail, attempt) => { if (this._debug) console.time(`> [debug] #${attempt} DELETE /secrets/${nameOrId}`); const res = await this._fetch(`/secrets/${nameOrId}`, { method: 'DELETE' }); @@ -22,15 +22,83 @@ export default class Secrets extends Now { return bail(new Error('Unauthorized')); } + const body = await res.json(); + + if (res.status !== 200) { + if (404 === res.status || 400 === res.status) { + const err = new Error(body.error.message); + err.userError = true; + return bail(err); + } else { + throw new Error(body.error.message); + } + } + + return body; + }); + } + + add (name, value) { + return this.retry(async (bail, attempt) => { + if (this._debug) console.time(`> [debug] #${attempt} POST /secrets`); + const res = await this._fetch('/secrets', { + method: 'POST', + body: { + name, + value + } + }); + if (this._debug) console.timeEnd(`> [debug] #${attempt} POST /secrets`); + + if (403 === res.status) { + return bail(new Error('Unauthorized')); + } + + const body = await res.json(); + if (res.status !== 200) { - const body = await res.json(); - throw new Error(body.error.message); + if (404 === res.status || 400 === res.status) { + const err = new Error(body.error.message); + err.userError = true; + return bail(err); + } else { + throw new Error(body.error.message); + } } + + return body; }); } - async add (name, value) { + rename (nameOrId, newName) { + return this.retry(async (bail, attempt) => { + if (this._debug) console.time(`> [debug] #${attempt} PATCH /secrets/${nameOrId}`); + const res = await this._fetch('/secrets', { + method: 'PATCH', + body: JSON.stringify({ + name: newName + }) + }); + if (this._debug) console.timeEnd(`> [debug] #${attempt} PATCH /secrets/${nameOrId}`); + + if (403 === res.status) { + return bail(new Error('Unauthorized')); + } + + const body = await res.json(); + if (res.status !== 200) { + if (404 === res.status || 400 === res.status) { + const err = new Error(body.error.message); + err.userError = true; + return bail(err); + } else { + throw new Error(body.error.message); + } + } + + return body; + }); } } diff --git a/package.json b/package.json index 2e0aaa4..6d999b5 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ ], "plugins": [ "transform-runtime", - "syntax-async-functions", "transform-async-to-generator" ] }, @@ -55,7 +54,7 @@ "arr-flatten": "1.0.1", "array-unique": "0.3.2", "async-retry": "0.2.1", - "babel-runtime": "6.11.6", + "babel-runtime": "6.5.0", "bytes": "2.4.0", "chalk": "1.1.3", "copy-paste": "1.3.0", @@ -84,7 +83,6 @@ "alpha-sort": "1.0.2", "ava": "^0.16.0", "babel-eslint": "6.1.2", - "babel-plugin-syntax-async-functions": "6.13.0", "babel-plugin-transform-async-to-generator": "6.8.0", "babel-plugin-transform-runtime": "6.12.0", "babel-preset-es2015": "6.13.2", From 187686d9818b0af0e058bfc6a6b7a8faaff2dbd3 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 16 Aug 2016 19:59:52 -0700 Subject: [PATCH 3/7] fixes for rm --- bin/now-secrets | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/bin/now-secrets b/bin/now-secrets index 0effad1..f25fb86 100755 --- a/bin/now-secrets +++ b/bin/now-secrets @@ -122,6 +122,24 @@ async function run (token) { error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret rm `')}`); return exit(1); } + const list = await secrets.ls(); + const theSecret = list.filter((secret) => { + return secret.uid === args[0] + || secret.name === args[0]; + })[0]; + + if (theSecret) { + const yes = await readConfirmation(theSecret); + if (!yes) { + console.log('woot'); + error('User abort'); + return exit(0); + } + } else { + error(`No secret found by id or name "${args[0]}"`); + return exit(1); + } + const secret = await secrets.rm(args[0]); const elapsed = ms(new Date() - start); console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.name)} ${chalk.gray(`(${secret.uid})`)} removed ${chalk.gray(`[${elapsed}]`)}`); @@ -171,3 +189,23 @@ process.on('uncaughtException', (err) => { handleError(err); exit(1); }); + +function readConfirmation (secret) { + return new Promise((resolve, reject) => { + const time = chalk.gray(ms(new Date() - new Date(secret.created)) + ' ago'); + const tbl = table( + [[secret.uid, chalk.bold(secret.name), time]], + { align: ['l', 'r', 'l'], hsep: ' '.repeat(6) } + ); + + process.stdout.write('> The following secret will be removed permanently\n'); + process.stdout.write(' ' + tbl + '\n'); + + process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[yN] ')}`); + + process.stdin.on('data', (d) => { + process.stdin.pause(); + resolve('y' === d.toString().trim().toLowerCase()); + }).resume(); + }); +} From 8354ee8cea813b0158d00f3a62d117eaaedf8e22 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 18 Aug 2016 20:20:07 -0700 Subject: [PATCH 4/7] fix rename --- bin/now-secrets | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bin/now-secrets b/bin/now-secrets index f25fb86..ca72110 100755 --- a/bin/now-secrets +++ b/bin/now-secrets @@ -131,7 +131,6 @@ async function run (token) { if (theSecret) { const yes = await readConfirmation(theSecret); if (!yes) { - console.log('woot'); error('User abort'); return exit(0); } @@ -151,9 +150,9 @@ async function run (token) { error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret rename `')}`); return exit(1); } - const secret = await secrets.patch(args[0], args[1]); + const secret = await secrets.rename(args[0], args[1]); const elapsed = ms(new Date() - start); - console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.name)} ${chalk.gray(`(${secret.uid})`)} renamed to ${chalk.bold('my-secret-name')} ${chalk.gray(`[${elapsed}]`)}`); + console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.oldName)} ${chalk.gray(`(${secret.uid})`)} renamed to ${chalk.bold(args[1])} ${chalk.gray(`[${elapsed}]`)}`); return secrets.close(); } From 20e0cd77789b7d3c52d5067e48da321dbcd5787e Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 18 Aug 2016 20:26:41 -0700 Subject: [PATCH 5/7] secrets: fix rename --- lib/secrets.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/secrets.js b/lib/secrets.js index 3c3dc1e..a196dfb 100644 --- a/lib/secrets.js +++ b/lib/secrets.js @@ -73,11 +73,11 @@ export default class Secrets extends Now { rename (nameOrId, newName) { return this.retry(async (bail, attempt) => { if (this._debug) console.time(`> [debug] #${attempt} PATCH /secrets/${nameOrId}`); - const res = await this._fetch('/secrets', { + const res = await this._fetch(`/secrets/${nameOrId}`, { method: 'PATCH', - body: JSON.stringify({ + body: { name: newName - }) + } }); if (this._debug) console.timeEnd(`> [debug] #${attempt} PATCH /secrets/${nameOrId}`); From cf0b8d260a3e81d42f7c8aabe1864607b58bb8f5 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 18 Aug 2016 20:38:49 -0700 Subject: [PATCH 6/7] fix help --- bin/now-secrets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/now-secrets b/bin/now-secrets index ca72110..c03c420 100755 --- a/bin/now-secrets +++ b/bin/now-secrets @@ -20,7 +20,7 @@ const subcommand = argv._[0]; // options const help = () => { console.log(` - ${chalk.bold('𝚫 now secrets')} + ${chalk.bold('𝚫 now secrets')} ${chalk.dim('Options:')} From 2c28348ed99213f14d5c62a27b94576c4fc0f9b0 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 18 Aug 2016 20:39:13 -0700 Subject: [PATCH 7/7] help: add `rename` --- bin/now-secrets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/now-secrets b/bin/now-secrets index c03c420..33924df 100755 --- a/bin/now-secrets +++ b/bin/now-secrets @@ -20,7 +20,7 @@ const subcommand = argv._[0]; // options const help = () => { console.log(` - ${chalk.bold('𝚫 now secrets')} + ${chalk.bold('𝚫 now secrets')} ${chalk.dim('Options:')}