Naoyuki Kanezawa
9 years ago
committed by
GitHub
6 changed files with 319 additions and 6 deletions
@ -0,0 +1,210 @@ |
|||
#!/usr/bin/env node |
|||
import chalk from 'chalk'; |
|||
import table from 'text-table'; |
|||
import minimist from 'minimist'; |
|||
import * as cfg from '../lib/cfg'; |
|||
import { handleError, 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')} <ls | add | rename | rm> <secret> |
|||
|
|||
${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) { |
|||
handleError(err); |
|||
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 = 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(); |
|||
} |
|||
|
|||
if ('rm' === subcommand || 'remove' === subcommand) { |
|||
if (1 !== args.length) { |
|||
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret rm <id | name>`')}`); |
|||
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) { |
|||
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}]`)}`); |
|||
return secrets.close(); |
|||
} |
|||
|
|||
if ('rename' === subcommand) { |
|||
if (2 !== args.length) { |
|||
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret rename <old-name> <new-name>`')}`); |
|||
return exit(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.oldName)} ${chalk.gray(`(${secret.uid})`)} renamed to ${chalk.bold(args[1])} ${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 <name> <value>`')}`); |
|||
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 = await secrets.add(name, value); |
|||
const elapsed = ms(new Date() - start); |
|||
console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(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); |
|||
} |
|||
|
|||
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(); |
|||
}); |
|||
} |
@ -0,0 +1,104 @@ |
|||
import Now from '../lib'; |
|||
|
|||
export default class Secrets extends Now { |
|||
|
|||
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; |
|||
}); |
|||
} |
|||
|
|||
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')); |
|||
} |
|||
|
|||
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) { |
|||
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; |
|||
}); |
|||
} |
|||
|
|||
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/${nameOrId}`, { |
|||
method: 'PATCH', |
|||
body: { |
|||
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; |
|||
}); |
|||
} |
|||
|
|||
} |
Loading…
Reference in new issue