vaulter-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +31 -0
- package/postinstall.js +15 -0
- package/src/assets/logo.js +30 -0
- package/src/commands/add.js +46 -0
- package/src/commands/help.js +40 -0
- package/src/commands/ls.js +66 -0
- package/src/commands/remove.js +88 -0
- package/src/commands/sign-in.js +107 -0
- package/src/commands/web-app.js +9 -0
- package/src/index.js +74 -0
- package/src/lib/api.js +31 -0
- package/src/lib/auth.js +38 -0
- package/src/lib/config.js +5 -0
- package/src/lib/ui.js +27 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vaulter-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for Vaulter - Secure API Key Manager",
|
|
5
|
+
"author": "faris-sait",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/faris-sait/Vaulter"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"vaulter": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"postinstall.js"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"postinstall": "node postinstall.js"
|
|
20
|
+
},
|
|
21
|
+
"keywords": ["vaulter", "api-keys", "cli", "vault", "security", "env", "secrets"],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"chalk": "^5.3.0",
|
|
25
|
+
"cli-table3": "^0.6.3",
|
|
26
|
+
"commander": "^12.0.0",
|
|
27
|
+
"inquirer": "^9.2.0",
|
|
28
|
+
"open": "^10.0.0",
|
|
29
|
+
"ora": "^8.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// postinstall.js - Uses raw ANSI escape codes (no ESM imports needed)
|
|
2
|
+
const purple = '\x1b[38;2;146;0;255m';
|
|
3
|
+
const purple2 = '\x1b[38;2;122;0;230m';
|
|
4
|
+
const purple3 = '\x1b[38;2;102;0;204m';
|
|
5
|
+
const purple4 = '\x1b[38;2;94;0;255m';
|
|
6
|
+
const reset = '\x1b[0m';
|
|
7
|
+
|
|
8
|
+
console.log('');
|
|
9
|
+
console.log(purple + ' ╦ ╦╔═╗╦ ╦╦ ╔╦╗╔═╗╦═╗' + reset);
|
|
10
|
+
console.log(purple2 + ' ╚╗╔╝╠═╣║ ║║ ║ ║╣ ╠╦╝' + reset);
|
|
11
|
+
console.log(purple3 + ' ╚╝ ╩ ╩╚═╝╩═╝╩ ╚═╝╩╚═' + reset);
|
|
12
|
+
console.log(purple4 + ' Your keys. Your vault.' + reset);
|
|
13
|
+
console.log('');
|
|
14
|
+
console.log(' Run \x1b[1mvaulter sign-in\x1b[0m to get started.');
|
|
15
|
+
console.log('');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
const purple1 = chalk.hex('#9200ff');
|
|
4
|
+
const purple2 = chalk.hex('#7a00e6');
|
|
5
|
+
const purple3 = chalk.hex('#6600cc');
|
|
6
|
+
const purple4 = chalk.hex('#5e00ff');
|
|
7
|
+
|
|
8
|
+
function sleep(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function typeLine(text, colorFn, charDelay = 18) {
|
|
13
|
+
for (let i = 0; i <= text.length; i++) {
|
|
14
|
+
const partial = colorFn(text.slice(0, i));
|
|
15
|
+
// Clear line fully then write partial
|
|
16
|
+
process.stdout.write(`\x1b[2K\r${partial}`);
|
|
17
|
+
await sleep(charDelay);
|
|
18
|
+
}
|
|
19
|
+
process.stdout.write('\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function printLogo() {
|
|
23
|
+
console.log('');
|
|
24
|
+
await typeLine(' ╦ ╦╔═╗╦ ╦╦ ╔╦╗╔═╗╦═╗', purple1, 18);
|
|
25
|
+
await typeLine(' ╚╗╔╝╠═╣║ ║║ ║ ║╣ ╠╦╝', purple2, 18);
|
|
26
|
+
await typeLine(' ╚╝ ╩ ╩╚═╝╩═╝╩ ╚═╝╩╚═', purple3, 18);
|
|
27
|
+
await sleep(80);
|
|
28
|
+
await typeLine(' Your keys. Your vault.', purple4, 30);
|
|
29
|
+
console.log('');
|
|
30
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { apiFetch } from '../lib/api.js';
|
|
4
|
+
import { success, error, info, purple } from '../lib/ui.js';
|
|
5
|
+
|
|
6
|
+
export async function addKey(name) {
|
|
7
|
+
console.log('');
|
|
8
|
+
info(`Adding key: ${name}`);
|
|
9
|
+
console.log('');
|
|
10
|
+
|
|
11
|
+
const answers = await inquirer.prompt([
|
|
12
|
+
{
|
|
13
|
+
type: 'password',
|
|
14
|
+
name: 'apiKey',
|
|
15
|
+
message: purple('API Key:'),
|
|
16
|
+
mask: '*',
|
|
17
|
+
validate: (input) => input.length > 0 || 'API key cannot be empty',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
type: 'input',
|
|
21
|
+
name: 'tags',
|
|
22
|
+
message: purple('Tags (comma-separated, optional):'),
|
|
23
|
+
},
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const tags = answers.tags
|
|
27
|
+
? answers.tags.split(',').map((t) => t.trim()).filter(Boolean)
|
|
28
|
+
: [];
|
|
29
|
+
|
|
30
|
+
const spinner = ora({ text: 'Encrypting and saving...', color: 'magenta' }).start();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await apiFetch('/api/keys', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
body: JSON.stringify({ name, apiKey: answers.apiKey, tags }),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
spinner.succeed('Key added to vault!');
|
|
39
|
+
console.log('');
|
|
40
|
+
success(`"${name}" has been securely stored.`);
|
|
41
|
+
console.log('');
|
|
42
|
+
} catch (err) {
|
|
43
|
+
spinner.fail('Failed to add key');
|
|
44
|
+
error(err.message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { printLogo } from '../assets/logo.js';
|
|
2
|
+
import { isAuthenticated } from '../lib/auth.js';
|
|
3
|
+
import { purple, dim, bold, white, green, yellow } from '../lib/ui.js';
|
|
4
|
+
|
|
5
|
+
export async function showHelp() {
|
|
6
|
+
await printLogo();
|
|
7
|
+
|
|
8
|
+
const status = isAuthenticated()
|
|
9
|
+
? green(' Signed in')
|
|
10
|
+
: yellow(' Not signed in') + dim(' — run `vaulter sign-in`');
|
|
11
|
+
|
|
12
|
+
console.log(status);
|
|
13
|
+
console.log('');
|
|
14
|
+
|
|
15
|
+
console.log(purple.bold(' COMMANDS'));
|
|
16
|
+
console.log('');
|
|
17
|
+
|
|
18
|
+
const commands = [
|
|
19
|
+
{ name: 'sign-in', desc: 'Authenticate with Vaulter via browser' },
|
|
20
|
+
{ name: 'ls', desc: 'List all API keys in your vault' },
|
|
21
|
+
{ name: 'add <name>', desc: 'Add a new API key to your vault' },
|
|
22
|
+
{ name: 'remove <name-or-id>', desc: 'Remove an API key from your vault' },
|
|
23
|
+
{ name: 'web-app', desc: 'Open the Vaulter web app in your browser' },
|
|
24
|
+
{ name: 'help', desc: 'Show this help message' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
for (const cmd of commands) {
|
|
28
|
+
const padded = cmd.name.padEnd(22);
|
|
29
|
+
console.log(` ${white.bold(padded)} ${dim(cmd.desc)}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log(purple.bold(' OPTIONS'));
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(` ${white.bold('-V, --version'.padEnd(22))} ${dim('Output the version number')}`);
|
|
36
|
+
console.log(` ${white.bold('-h, --help'.padEnd(22))} ${dim('Display help for a command')}`);
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(dim(' https://vaulter-nine.vercel.app'));
|
|
39
|
+
console.log('');
|
|
40
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import Table from 'cli-table3';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { apiFetch } from '../lib/api.js';
|
|
5
|
+
import { purple, dim, error } from '../lib/ui.js';
|
|
6
|
+
|
|
7
|
+
export async function listKeys() {
|
|
8
|
+
const spinner = ora({ text: 'Fetching keys...', color: 'magenta' }).start();
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const data = await apiFetch('/api/keys');
|
|
12
|
+
const keys = data.keys || [];
|
|
13
|
+
|
|
14
|
+
spinner.stop();
|
|
15
|
+
|
|
16
|
+
if (keys.length === 0) {
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log(dim(' No keys found. Run `vaulter add <name>` to add one.'));
|
|
19
|
+
console.log('');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const table = new Table({
|
|
24
|
+
head: [
|
|
25
|
+
purple.bold('Name'),
|
|
26
|
+
purple.bold('Masked Key'),
|
|
27
|
+
purple.bold('Tags'),
|
|
28
|
+
purple.bold('Created'),
|
|
29
|
+
purple.bold('Usage'),
|
|
30
|
+
],
|
|
31
|
+
style: {
|
|
32
|
+
head: [],
|
|
33
|
+
border: ['dim'],
|
|
34
|
+
},
|
|
35
|
+
chars: {
|
|
36
|
+
'top': '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐',
|
|
37
|
+
'bottom': '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘',
|
|
38
|
+
'left': '│', 'left-mid': '├', 'mid': '─', 'mid-mid': '┼',
|
|
39
|
+
'right': '│', 'right-mid': '┤', 'middle': '│',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
for (const key of keys) {
|
|
44
|
+
const tags = (key.tags || []).join(', ') || dim('none');
|
|
45
|
+
const created = new Date(key.created_at).toLocaleDateString();
|
|
46
|
+
const usage = key.usage_count || 0;
|
|
47
|
+
|
|
48
|
+
table.push([
|
|
49
|
+
chalk.white(key.name),
|
|
50
|
+
chalk.hex('#a78bfa')(key.masked_key),
|
|
51
|
+
dim(tags),
|
|
52
|
+
dim(created),
|
|
53
|
+
dim(`${usage}x`),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log(table.toString());
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(dim(` ${keys.length} key(s) in your vault`));
|
|
61
|
+
console.log('');
|
|
62
|
+
} catch (err) {
|
|
63
|
+
spinner.fail('Failed to fetch keys');
|
|
64
|
+
error(err.message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { apiFetch } from '../lib/api.js';
|
|
5
|
+
import { success, error, warn, purple, dim } from '../lib/ui.js';
|
|
6
|
+
|
|
7
|
+
export async function removeKey(nameOrId) {
|
|
8
|
+
const spinner = ora({ text: 'Looking up key...', color: 'magenta' }).start();
|
|
9
|
+
|
|
10
|
+
let keys;
|
|
11
|
+
try {
|
|
12
|
+
const data = await apiFetch('/api/keys');
|
|
13
|
+
keys = data.keys || [];
|
|
14
|
+
} catch (err) {
|
|
15
|
+
spinner.fail('Failed to fetch keys');
|
|
16
|
+
error(err.message);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Match by name (case-insensitive) or UUID prefix
|
|
21
|
+
const matches = keys.filter(
|
|
22
|
+
(k) =>
|
|
23
|
+
k.name.toLowerCase() === nameOrId.toLowerCase() ||
|
|
24
|
+
k.id.startsWith(nameOrId)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
spinner.stop();
|
|
28
|
+
|
|
29
|
+
if (matches.length === 0) {
|
|
30
|
+
console.log('');
|
|
31
|
+
warn(`No key found matching "${nameOrId}".`);
|
|
32
|
+
console.log('');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let target;
|
|
37
|
+
|
|
38
|
+
if (matches.length === 1) {
|
|
39
|
+
target = matches[0];
|
|
40
|
+
} else {
|
|
41
|
+
console.log('');
|
|
42
|
+
const answer = await inquirer.prompt([
|
|
43
|
+
{
|
|
44
|
+
type: 'list',
|
|
45
|
+
name: 'key',
|
|
46
|
+
message: purple('Multiple keys found. Select one:'),
|
|
47
|
+
choices: matches.map((k) => ({
|
|
48
|
+
name: `${k.name} ${dim(`(${k.masked_key})`)}`,
|
|
49
|
+
value: k,
|
|
50
|
+
})),
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
target = answer.key;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(` ${chalk.white.bold(target.name)}`);
|
|
58
|
+
console.log(` ${dim(target.masked_key)}`);
|
|
59
|
+
console.log(` ${dim(`Created: ${new Date(target.created_at).toLocaleDateString()}`)}`);
|
|
60
|
+
console.log('');
|
|
61
|
+
|
|
62
|
+
const confirm = await inquirer.prompt([
|
|
63
|
+
{
|
|
64
|
+
type: 'confirm',
|
|
65
|
+
name: 'yes',
|
|
66
|
+
message: chalk.red('Delete this key? This cannot be undone.'),
|
|
67
|
+
default: false,
|
|
68
|
+
},
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
if (!confirm.yes) {
|
|
72
|
+
warn('Cancelled.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const deleteSpinner = ora({ text: 'Deleting...', color: 'magenta' }).start();
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await apiFetch(`/api/keys/${target.id}`, { method: 'DELETE' });
|
|
80
|
+
deleteSpinner.succeed('Key deleted!');
|
|
81
|
+
console.log('');
|
|
82
|
+
success(`"${target.name}" has been removed from your vault.`);
|
|
83
|
+
console.log('');
|
|
84
|
+
} catch (err) {
|
|
85
|
+
deleteSpinner.fail('Failed to delete key');
|
|
86
|
+
error(err.message);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { saveToken, getToken } from '../lib/auth.js';
|
|
6
|
+
import { getApiUrl } from '../lib/config.js';
|
|
7
|
+
import { printLogo } from '../assets/logo.js';
|
|
8
|
+
import { success, error, info } from '../lib/ui.js';
|
|
9
|
+
|
|
10
|
+
export async function signIn() {
|
|
11
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
12
|
+
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const server = http.createServer(async (req, res) => {
|
|
15
|
+
const url = new URL(req.url, `http://localhost`);
|
|
16
|
+
|
|
17
|
+
if (url.pathname === '/callback') {
|
|
18
|
+
const token = url.searchParams.get('token');
|
|
19
|
+
const returnedState = url.searchParams.get('state');
|
|
20
|
+
const denied = url.searchParams.get('error');
|
|
21
|
+
|
|
22
|
+
if (denied === 'denied') {
|
|
23
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
24
|
+
res.end('<html><body style="background:#0f172a;color:#c4b5fd;font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0"><h2>Authorization denied. You can close this tab.</h2></body></html>');
|
|
25
|
+
spinner.fail('Authorization denied.');
|
|
26
|
+
server.close();
|
|
27
|
+
resolve();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (returnedState !== state) {
|
|
32
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
33
|
+
res.end('<html><body style="background:#0f172a;color:#ef4444;font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0"><h2>State mismatch. Please try again.</h2></body></html>');
|
|
34
|
+
spinner.fail('State mismatch - possible CSRF attack. Try again.');
|
|
35
|
+
server.close();
|
|
36
|
+
resolve();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (token) {
|
|
41
|
+
saveToken(token);
|
|
42
|
+
|
|
43
|
+
// Verify the token works
|
|
44
|
+
try {
|
|
45
|
+
const verifyRes = await fetch(`${getApiUrl()}/api/keys`, {
|
|
46
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
47
|
+
});
|
|
48
|
+
if (!verifyRes.ok) throw new Error('Verification failed');
|
|
49
|
+
} catch {
|
|
50
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
51
|
+
res.end('<html><body style="background:#0f172a;color:#ef4444;font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0"><h2>Token verification failed. Please try again.</h2></body></html>');
|
|
52
|
+
spinner.fail('Token verification failed.');
|
|
53
|
+
server.close();
|
|
54
|
+
resolve();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
59
|
+
res.end('<html><body style="background:#0f172a;color:#a78bfa;font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0"><div style="text-align:center"><h2 style="color:#22c55e">Authenticated!</h2><p>You can close this tab and return to your terminal.</p></div></body></html>');
|
|
60
|
+
spinner.succeed('Authenticated successfully!');
|
|
61
|
+
await printLogo();
|
|
62
|
+
success('You are now signed in to Vaulter.');
|
|
63
|
+
info('Run `vaulter ls` to list your keys.');
|
|
64
|
+
server.close();
|
|
65
|
+
resolve();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
70
|
+
res.end('<html><body style="background:#0f172a;color:#ef4444;font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0"><h2>Missing token. Please try again.</h2></body></html>');
|
|
71
|
+
spinner.fail('No token received.');
|
|
72
|
+
server.close();
|
|
73
|
+
resolve();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
res.writeHead(404);
|
|
78
|
+
res.end('Not found');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
server.listen(0, () => {
|
|
82
|
+
const port = server.address().port;
|
|
83
|
+
const authUrl = `${getApiUrl()}/cli-auth?port=${port}&state=${state}`;
|
|
84
|
+
|
|
85
|
+
console.log('');
|
|
86
|
+
info('Opening browser for authentication...');
|
|
87
|
+
console.log('');
|
|
88
|
+
info(`If the browser doesn't open, visit:`);
|
|
89
|
+
console.log(` ${authUrl}`);
|
|
90
|
+
console.log('');
|
|
91
|
+
|
|
92
|
+
open(authUrl);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const spinner = ora({
|
|
96
|
+
text: 'Waiting for browser authorization...',
|
|
97
|
+
color: 'magenta',
|
|
98
|
+
}).start();
|
|
99
|
+
|
|
100
|
+
// 120 second timeout
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
spinner.fail('Authorization timed out (120s). Please try again.');
|
|
103
|
+
server.close();
|
|
104
|
+
resolve();
|
|
105
|
+
}, 120000);
|
|
106
|
+
});
|
|
107
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { signIn } from './commands/sign-in.js';
|
|
5
|
+
import { listKeys } from './commands/ls.js';
|
|
6
|
+
import { addKey } from './commands/add.js';
|
|
7
|
+
import { removeKey } from './commands/remove.js';
|
|
8
|
+
import { openWebApp } from './commands/web-app.js';
|
|
9
|
+
import { showHelp } from './commands/help.js';
|
|
10
|
+
import { printLogo } from './assets/logo.js';
|
|
11
|
+
import { isAuthenticated } from './lib/auth.js';
|
|
12
|
+
import { error } from './lib/ui.js';
|
|
13
|
+
|
|
14
|
+
const program = new Command();
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.name('vaulter')
|
|
18
|
+
.description('Vaulter CLI - Secure API Key Manager')
|
|
19
|
+
.version('0.1.0')
|
|
20
|
+
.action(async () => {
|
|
21
|
+
await showHelp();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.command('sign-in')
|
|
26
|
+
.description('Authenticate with Vaulter via browser')
|
|
27
|
+
.action(signIn);
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('ls')
|
|
31
|
+
.description('List all API keys in your vault')
|
|
32
|
+
.action(async () => {
|
|
33
|
+
if (!isAuthenticated()) {
|
|
34
|
+
error('Not authenticated. Run `vaulter sign-in` first.');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
await listKeys();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.command('add <name>')
|
|
42
|
+
.description('Add a new API key to your vault')
|
|
43
|
+
.action(async (name) => {
|
|
44
|
+
if (!isAuthenticated()) {
|
|
45
|
+
error('Not authenticated. Run `vaulter sign-in` first.');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
await addKey(name);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command('remove <name-or-id>')
|
|
53
|
+
.description('Remove an API key from your vault')
|
|
54
|
+
.action(async (nameOrId) => {
|
|
55
|
+
if (!isAuthenticated()) {
|
|
56
|
+
error('Not authenticated. Run `vaulter sign-in` first.');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
await removeKey(nameOrId);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
program
|
|
63
|
+
.command('web-app')
|
|
64
|
+
.description('Open the Vaulter web app in your browser')
|
|
65
|
+
.action(openWebApp);
|
|
66
|
+
|
|
67
|
+
program
|
|
68
|
+
.command('help')
|
|
69
|
+
.description('Show all available commands')
|
|
70
|
+
.action(async () => {
|
|
71
|
+
await showHelp();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
program.parse();
|
package/src/lib/api.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getToken } from './auth.js';
|
|
2
|
+
import { getApiUrl } from './config.js';
|
|
3
|
+
|
|
4
|
+
export async function apiFetch(path, options = {}) {
|
|
5
|
+
const token = getToken();
|
|
6
|
+
if (!token) {
|
|
7
|
+
throw new Error('Not authenticated. Run `vaulter sign-in` first.');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const url = `${getApiUrl()}${path}`;
|
|
11
|
+
const res = await fetch(url, {
|
|
12
|
+
...options,
|
|
13
|
+
headers: {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
'Authorization': `Bearer ${token}`,
|
|
16
|
+
...options.headers,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (res.status === 401) {
|
|
21
|
+
throw new Error('Session expired. Run `vaulter sign-in` to re-authenticate.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
throw new Error(data.error || `Request failed with status ${res.status}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return data;
|
|
31
|
+
}
|
package/src/lib/auth.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const VAULTER_DIR = path.join(os.homedir(), '.vaulter');
|
|
6
|
+
const CREDENTIALS_FILE = path.join(VAULTER_DIR, 'credentials.json');
|
|
7
|
+
|
|
8
|
+
export function getToken() {
|
|
9
|
+
try {
|
|
10
|
+
const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
|
|
11
|
+
return data.token || null;
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function saveToken(token) {
|
|
18
|
+
if (!fs.existsSync(VAULTER_DIR)) {
|
|
19
|
+
fs.mkdirSync(VAULTER_DIR, { mode: 0o700 });
|
|
20
|
+
}
|
|
21
|
+
fs.writeFileSync(
|
|
22
|
+
CREDENTIALS_FILE,
|
|
23
|
+
JSON.stringify({ token }, null, 2),
|
|
24
|
+
{ mode: 0o600 }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function clearToken() {
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
31
|
+
fs.unlinkSync(CREDENTIALS_FILE);
|
|
32
|
+
}
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isAuthenticated() {
|
|
37
|
+
return !!getToken();
|
|
38
|
+
}
|
package/src/lib/ui.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export const purple = chalk.hex('#9200ff');
|
|
4
|
+
export const deepPurple = chalk.hex('#5e00ff');
|
|
5
|
+
export const dim = chalk.dim;
|
|
6
|
+
export const bold = chalk.bold;
|
|
7
|
+
export const white = chalk.white;
|
|
8
|
+
export const red = chalk.red;
|
|
9
|
+
export const green = chalk.green;
|
|
10
|
+
export const yellow = chalk.yellow;
|
|
11
|
+
export const cyan = chalk.cyan;
|
|
12
|
+
|
|
13
|
+
export function success(msg) {
|
|
14
|
+
console.log(green(' ' + msg));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function error(msg) {
|
|
18
|
+
console.log(red(' ' + msg));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function info(msg) {
|
|
22
|
+
console.log(purple(' ' + msg));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function warn(msg) {
|
|
26
|
+
console.log(yellow(' ' + msg));
|
|
27
|
+
}
|