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