@vaultproxy/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/README.md +63 -0
- package/bin/vp.js +521 -0
- package/lib/api.js +50 -0
- package/lib/config.js +38 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @vaultproxy/cli
|
|
2
|
+
|
|
3
|
+
CLI for [VaultProxy](https://vaultproxy.dev) — secure API key management with proxy and fetch modes.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @vaultproxy/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
vp login
|
|
15
|
+
# Paste your proxy token (from app.vaultproxy.dev > Tokens)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Fetch a key
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
vp fetch openai-1
|
|
24
|
+
# prints: sk-real-key...
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Inject keys into your shell
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
eval $(vp run openai-1=OPENAI_API_KEY)
|
|
31
|
+
# OPENAI_API_KEY is now set in your shell
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Inject and execute a command
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
vp run railway=RAILWAY_TOKEN -- railway deploy
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Proxy mode (key never leaves VaultProxy)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
eval $(vp run openai-1=OPENAI_API_KEY --proxy)
|
|
44
|
+
# Sets OPENAI_API_KEY (proxy token) + OPENAI_BASE_URL (proxy endpoint)
|
|
45
|
+
# Your app routes through VaultProxy — real key never exposed
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Dry run (see what would be set)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
vp run openai-1=OPENAI_API_KEY --proxy --dry-run
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Environment Variables
|
|
55
|
+
|
|
56
|
+
| Variable | Description |
|
|
57
|
+
|----------|-------------|
|
|
58
|
+
| `VAULTPROXY_TOKEN` | Override stored proxy token |
|
|
59
|
+
| `VAULTPROXY_API_URL` | Override API URL (default: https://api.vaultproxy.dev) |
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
package/bin/vp.js
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { loadConfig, saveConfig, getToken } from "../lib/config.js";
|
|
4
|
+
import { fetchKey, fetchProviders } from "../lib/api.js";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const command = args[0];
|
|
10
|
+
|
|
11
|
+
const HELP = `
|
|
12
|
+
vp — VaultProxy CLI
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
vp login Save your proxy token
|
|
16
|
+
vp fetch <alias> Print the decrypted key for an alias
|
|
17
|
+
vp env <alias>=<VAR> [...] Print export statements for key mappings
|
|
18
|
+
vp run <alias>=<VAR> [...] Inject keys into current shell (use with eval)
|
|
19
|
+
vp run <alias>=<VAR> [...] -- <command>
|
|
20
|
+
Inject keys and run a command
|
|
21
|
+
vp proxy start Start the HTTPS proxy (credential injection)
|
|
22
|
+
vp proxy stop Stop the running proxy
|
|
23
|
+
vp proxy status Check if the proxy is running
|
|
24
|
+
vp proxy trust Print instructions to trust the CA cert
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
vp --help Show this help
|
|
28
|
+
vp --version Show version
|
|
29
|
+
--proxy Use proxy token + base URL instead of real key
|
|
30
|
+
--dry-run Show what would be set without fetching real keys
|
|
31
|
+
|
|
32
|
+
Environment:
|
|
33
|
+
VAULTPROXY_TOKEN Override stored token
|
|
34
|
+
VAULTPROXY_API_URL Override API URL (default: https://api.vaultproxy.dev)
|
|
35
|
+
HTTPS_PROXY Set to http://localhost:10255 when proxy is running
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
vp login
|
|
39
|
+
vp fetch railway
|
|
40
|
+
eval $(vp run railway=RAILWAY_TOKEN)
|
|
41
|
+
vp run railway=RAILWAY_TOKEN -- railway status
|
|
42
|
+
eval $(vp run openai-1=OPENAI_API_KEY --proxy)
|
|
43
|
+
vp proxy start && export HTTPS_PROXY=http://localhost:10255
|
|
44
|
+
`.trim();
|
|
45
|
+
|
|
46
|
+
async function main() {
|
|
47
|
+
if (!command || command === "--help" || command === "-h") {
|
|
48
|
+
console.log(HELP);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (command === "--version" || command === "-v") {
|
|
53
|
+
console.log("0.1.0");
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
switch (command) {
|
|
58
|
+
case "login":
|
|
59
|
+
await cmdLogin();
|
|
60
|
+
break;
|
|
61
|
+
case "fetch":
|
|
62
|
+
await cmdFetch();
|
|
63
|
+
break;
|
|
64
|
+
case "env":
|
|
65
|
+
await cmdEnv();
|
|
66
|
+
break;
|
|
67
|
+
case "run":
|
|
68
|
+
await cmdRun();
|
|
69
|
+
break;
|
|
70
|
+
case "proxy":
|
|
71
|
+
await cmdProxy();
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
console.error(`Unknown command: ${command}\nRun 'vp --help' for usage.`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- login ---
|
|
80
|
+
|
|
81
|
+
async function cmdLogin() {
|
|
82
|
+
const cfg = loadConfig();
|
|
83
|
+
|
|
84
|
+
// Check if token provided as argument
|
|
85
|
+
let token = args[1];
|
|
86
|
+
|
|
87
|
+
if (!token) {
|
|
88
|
+
// Interactive prompt
|
|
89
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
90
|
+
token = await new Promise((resolve) => {
|
|
91
|
+
rl.question("Proxy token: ", (answer) => {
|
|
92
|
+
rl.close();
|
|
93
|
+
resolve(answer.trim());
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!token) {
|
|
99
|
+
console.error("No token provided.");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Optional: custom API URL
|
|
104
|
+
const urlFlag = args.find((a) => a.startsWith("--api-url="));
|
|
105
|
+
if (urlFlag) {
|
|
106
|
+
cfg.api_url = urlFlag.split("=").slice(1).join("=");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
cfg.token = token;
|
|
110
|
+
saveConfig(cfg);
|
|
111
|
+
console.error("✓ Token saved to ~/.vaultproxy/config.json");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- fetch ---
|
|
115
|
+
|
|
116
|
+
async function cmdFetch() {
|
|
117
|
+
const alias = args[1];
|
|
118
|
+
if (!alias) {
|
|
119
|
+
console.error("Usage: vp fetch <alias>");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const result = await fetchKey(alias);
|
|
125
|
+
// Print just the key to stdout (pipeable)
|
|
126
|
+
process.stdout.write(result.key);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error(`Error: ${err.message}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- env ---
|
|
134
|
+
|
|
135
|
+
async function cmdEnv() {
|
|
136
|
+
const mappings = parseMappings(args.slice(1));
|
|
137
|
+
if (mappings.length === 0) {
|
|
138
|
+
console.error("Usage: vp env <alias>=<ENV_VAR> [...]");
|
|
139
|
+
console.error("Example: vp env railway=RAILWAY_TOKEN openai=OPENAI_API_KEY");
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const results = await resolveAll(mappings);
|
|
144
|
+
for (const { envVar, key } of results) {
|
|
145
|
+
console.log(`export ${envVar}=${shellEscape(key)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- run ---
|
|
150
|
+
|
|
151
|
+
async function cmdRun() {
|
|
152
|
+
const dashIndex = args.indexOf("--");
|
|
153
|
+
|
|
154
|
+
// Flags live before the "--" separator (or anywhere if no separator)
|
|
155
|
+
const flagArgs = dashIndex === -1 ? args.slice(1) : args.slice(1, dashIndex);
|
|
156
|
+
const isProxyMode = flagArgs.includes("--proxy");
|
|
157
|
+
const isDryRun = flagArgs.includes("--dry-run");
|
|
158
|
+
|
|
159
|
+
// No "--" means inject-only mode (prints export statements)
|
|
160
|
+
const mappingArgs = dashIndex === -1 ? args.slice(1) : args.slice(1, dashIndex);
|
|
161
|
+
const cmdArgs = dashIndex === -1 ? [] : args.slice(dashIndex + 1);
|
|
162
|
+
|
|
163
|
+
const mappings = parseMappings(mappingArgs);
|
|
164
|
+
if (mappings.length === 0) {
|
|
165
|
+
console.error("Usage: vp run <alias>=<ENV_VAR> [...] [-- <command>]");
|
|
166
|
+
console.error(" Without --: prints export statements (use with eval)");
|
|
167
|
+
console.error(" With --: injects keys and runs the command");
|
|
168
|
+
console.error(" --proxy: use proxy token + base URL injection");
|
|
169
|
+
console.error(" --dry-run: show what would be set without fetching real keys");
|
|
170
|
+
console.error("Example: eval $(vp run railway=RAILWAY_TOKEN)");
|
|
171
|
+
console.error("Example: vp run railway=RAILWAY_TOKEN -- railway status");
|
|
172
|
+
console.error("Example: eval $(vp run openai-1=OPENAI_API_KEY --proxy)");
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- dry-run mode ---
|
|
177
|
+
if (isDryRun) {
|
|
178
|
+
if (isProxyMode) {
|
|
179
|
+
const token = getToken();
|
|
180
|
+
if (!token) {
|
|
181
|
+
console.error("Error: Not logged in. Run: vp login");
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
const cfg = loadConfig();
|
|
185
|
+
const proxyBase = cfg.proxy_url || "https://proxy.vaultproxy.dev";
|
|
186
|
+
const providers = await fetchProviders();
|
|
187
|
+
|
|
188
|
+
console.error("Would set:");
|
|
189
|
+
for (const { alias, envVar } of mappings) {
|
|
190
|
+
let keyInfo;
|
|
191
|
+
try {
|
|
192
|
+
keyInfo = await fetchKey(alias);
|
|
193
|
+
} catch {
|
|
194
|
+
keyInfo = null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const maskedToken = maskValue(token);
|
|
198
|
+
console.error(` ${envVar}=${maskedToken} (proxy token)`);
|
|
199
|
+
|
|
200
|
+
if (keyInfo) {
|
|
201
|
+
const provider = providers.find((p) => p.id === keyInfo.provider);
|
|
202
|
+
if (provider?.base_url_env) {
|
|
203
|
+
const baseUrl = `${proxyBase}/proxy/${alias}`;
|
|
204
|
+
console.error(` ${provider.base_url_env}=${baseUrl}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
// Dry-run in fetch mode: show alias->envVar without fetching real keys
|
|
210
|
+
console.error("Would set:");
|
|
211
|
+
for (const { alias, envVar } of mappings) {
|
|
212
|
+
console.error(` ${envVar}=<key for alias "${alias}">`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- proxy mode ---
|
|
219
|
+
let results;
|
|
220
|
+
if (isProxyMode) {
|
|
221
|
+
results = await resolveAllProxy(mappings);
|
|
222
|
+
} else {
|
|
223
|
+
results = await resolveAll(mappings);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (cmdArgs.length === 0) {
|
|
227
|
+
// Inject-only mode: print export statements
|
|
228
|
+
for (const { envVar, key, extraEnvVar, extraValue } of results) {
|
|
229
|
+
console.log(`export ${envVar}=${shellEscape(key)}`);
|
|
230
|
+
if (extraEnvVar && extraValue) {
|
|
231
|
+
console.log(`export ${extraEnvVar}=${shellEscape(extraValue)}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Execute mode: inject keys and run command
|
|
238
|
+
const env = { ...process.env };
|
|
239
|
+
for (const { envVar, key, extraEnvVar, extraValue } of results) {
|
|
240
|
+
env[envVar] = key;
|
|
241
|
+
if (extraEnvVar && extraValue) {
|
|
242
|
+
env[extraEnvVar] = extraValue;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const cmdString = cmdArgs.join(" ");
|
|
247
|
+
try {
|
|
248
|
+
execSync(cmdString, { env, stdio: "inherit" });
|
|
249
|
+
} catch (err) {
|
|
250
|
+
process.exit(err.status || 1);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// --- proxy ---
|
|
255
|
+
|
|
256
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
257
|
+
import { spawn } from "node:child_process";
|
|
258
|
+
import { homedir } from "node:os";
|
|
259
|
+
import { join } from "node:path";
|
|
260
|
+
|
|
261
|
+
const PROXY_PID_FILE = join(homedir(), ".vaultproxy", "proxy.pid");
|
|
262
|
+
const PROXY_PORT = process.env.VP_PROXY_PORT || "10255";
|
|
263
|
+
|
|
264
|
+
async function cmdProxy() {
|
|
265
|
+
const sub = args[1];
|
|
266
|
+
|
|
267
|
+
switch (sub) {
|
|
268
|
+
case "start":
|
|
269
|
+
await proxyStart();
|
|
270
|
+
break;
|
|
271
|
+
case "stop":
|
|
272
|
+
proxyStop();
|
|
273
|
+
break;
|
|
274
|
+
case "status":
|
|
275
|
+
proxyStatus();
|
|
276
|
+
break;
|
|
277
|
+
case "trust":
|
|
278
|
+
proxyTrust();
|
|
279
|
+
break;
|
|
280
|
+
default:
|
|
281
|
+
console.error("Usage: vp proxy <start|stop|status|trust>");
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function proxyStart() {
|
|
287
|
+
const token = getToken();
|
|
288
|
+
if (!token) {
|
|
289
|
+
console.error("Error: Not logged in. Run: vp login");
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check if already running
|
|
294
|
+
if (existsSync(PROXY_PID_FILE)) {
|
|
295
|
+
const pid = parseInt(readFileSync(PROXY_PID_FILE, "utf-8").trim());
|
|
296
|
+
try {
|
|
297
|
+
process.kill(pid, 0); // Check if process exists
|
|
298
|
+
console.error(`Proxy already running (PID ${pid}). Use 'vp proxy stop' first.`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
} catch {
|
|
301
|
+
// Process not running, clean up stale PID file
|
|
302
|
+
unlinkSync(PROXY_PID_FILE);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Find the proxy binary
|
|
307
|
+
const binaryName = "vaultproxy-proxy";
|
|
308
|
+
let binaryPath;
|
|
309
|
+
|
|
310
|
+
// Check common locations
|
|
311
|
+
const candidates = [
|
|
312
|
+
join(homedir(), ".vaultproxy", "bin", binaryName),
|
|
313
|
+
join(process.cwd(), "proxy", "target", "release", binaryName),
|
|
314
|
+
join(process.cwd(), binaryName),
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
for (const p of candidates) {
|
|
318
|
+
if (existsSync(p)) {
|
|
319
|
+
binaryPath = p;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Try PATH
|
|
325
|
+
if (!binaryPath) {
|
|
326
|
+
try {
|
|
327
|
+
execSync(`which ${binaryName}`, { stdio: "pipe" });
|
|
328
|
+
binaryPath = binaryName; // It's in PATH
|
|
329
|
+
} catch {
|
|
330
|
+
// Not found
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!binaryPath) {
|
|
335
|
+
console.error(`Error: ${binaryName} not found.`);
|
|
336
|
+
console.error("Install it from: https://github.com/Axis-Labs-HQ/vaultproxy-cloud/releases");
|
|
337
|
+
console.error("Or build from source: cd proxy && cargo build --release");
|
|
338
|
+
console.error(`Then place it in ~/.vaultproxy/bin/ or your PATH.`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const cfg = loadConfig();
|
|
343
|
+
const apiUrl = cfg.api_url || "https://api.vaultproxy.dev";
|
|
344
|
+
|
|
345
|
+
console.error(`Starting VaultProxy HTTPS proxy on port ${PROXY_PORT}...`);
|
|
346
|
+
|
|
347
|
+
const child = spawn(binaryPath, [
|
|
348
|
+
"--port", PROXY_PORT,
|
|
349
|
+
"--control-plane-url", apiUrl,
|
|
350
|
+
"--proxy-token", token,
|
|
351
|
+
], {
|
|
352
|
+
detached: true,
|
|
353
|
+
stdio: "ignore",
|
|
354
|
+
env: { ...process.env, RUST_LOG: "vaultproxy_proxy=info" },
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
child.unref();
|
|
358
|
+
|
|
359
|
+
// Save PID
|
|
360
|
+
writeFileSync(PROXY_PID_FILE, String(child.pid), { mode: 0o600 });
|
|
361
|
+
|
|
362
|
+
console.error(`✓ Proxy started (PID ${child.pid})`);
|
|
363
|
+
console.error(` Port: ${PROXY_PORT}`);
|
|
364
|
+
console.error(` CA cert: ~/.vaultproxy/ca/ca.crt`);
|
|
365
|
+
console.error("");
|
|
366
|
+
console.error("To use it:");
|
|
367
|
+
console.error(` export HTTPS_PROXY=http://localhost:${PROXY_PORT}`);
|
|
368
|
+
console.error("");
|
|
369
|
+
console.error("First time? Trust the CA cert:");
|
|
370
|
+
console.error(" vp proxy trust");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function proxyStop() {
|
|
374
|
+
if (!existsSync(PROXY_PID_FILE)) {
|
|
375
|
+
console.error("Proxy is not running.");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const pid = parseInt(readFileSync(PROXY_PID_FILE, "utf-8").trim());
|
|
380
|
+
try {
|
|
381
|
+
process.kill(pid, "SIGTERM");
|
|
382
|
+
console.error(`✓ Proxy stopped (PID ${pid})`);
|
|
383
|
+
} catch {
|
|
384
|
+
console.error(`Process ${pid} not found (already stopped).`);
|
|
385
|
+
}
|
|
386
|
+
unlinkSync(PROXY_PID_FILE);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function proxyStatus() {
|
|
390
|
+
if (!existsSync(PROXY_PID_FILE)) {
|
|
391
|
+
console.error("Proxy is not running.");
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const pid = parseInt(readFileSync(PROXY_PID_FILE, "utf-8").trim());
|
|
396
|
+
try {
|
|
397
|
+
process.kill(pid, 0);
|
|
398
|
+
console.error(`✓ Proxy is running (PID ${pid}, port ${PROXY_PORT})`);
|
|
399
|
+
} catch {
|
|
400
|
+
console.error(`✗ Proxy is not running (stale PID ${pid})`);
|
|
401
|
+
unlinkSync(PROXY_PID_FILE);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function proxyTrust() {
|
|
407
|
+
const caPath = join(homedir(), ".vaultproxy", "ca", "ca.crt");
|
|
408
|
+
const platform = process.platform;
|
|
409
|
+
|
|
410
|
+
console.error("Trust the VaultProxy CA certificate:\n");
|
|
411
|
+
|
|
412
|
+
if (platform === "darwin") {
|
|
413
|
+
console.error(" macOS:");
|
|
414
|
+
console.error(` sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ${caPath}`);
|
|
415
|
+
} else if (platform === "linux") {
|
|
416
|
+
console.error(" Ubuntu/Debian:");
|
|
417
|
+
console.error(` sudo cp ${caPath} /usr/local/share/ca-certificates/vaultproxy-ca.crt`);
|
|
418
|
+
console.error(" sudo update-ca-certificates");
|
|
419
|
+
console.error("");
|
|
420
|
+
console.error(" RHEL/Fedora:");
|
|
421
|
+
console.error(` sudo cp ${caPath} /etc/pki/ca-trust/source/anchors/vaultproxy-ca.crt`);
|
|
422
|
+
console.error(" sudo update-ca-trust");
|
|
423
|
+
} else {
|
|
424
|
+
console.error(" Windows:");
|
|
425
|
+
console.error(` certutil -addstore -user Root ${caPath}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
console.error("");
|
|
429
|
+
console.error(" Node.js (per-process, no system trust needed):");
|
|
430
|
+
console.error(` export NODE_EXTRA_CA_CERTS=${caPath}`);
|
|
431
|
+
console.error("");
|
|
432
|
+
console.error(" Python (per-process):");
|
|
433
|
+
console.error(` export REQUESTS_CA_BUNDLE=${caPath}`);
|
|
434
|
+
|
|
435
|
+
if (existsSync(caPath)) {
|
|
436
|
+
console.error(`\n CA cert location: ${caPath}`);
|
|
437
|
+
} else {
|
|
438
|
+
console.error(`\n ⚠ CA cert not found at ${caPath}`);
|
|
439
|
+
console.error(" Run 'vp proxy start' first to generate it.");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// --- helpers ---
|
|
444
|
+
|
|
445
|
+
function parseMappings(rawArgs) {
|
|
446
|
+
const mappings = [];
|
|
447
|
+
for (const arg of rawArgs) {
|
|
448
|
+
if (arg.startsWith("-")) continue; // skip flags
|
|
449
|
+
const eqIndex = arg.indexOf("=");
|
|
450
|
+
if (eqIndex === -1) {
|
|
451
|
+
// Shorthand: alias only, use ALIAS_KEY as env var name
|
|
452
|
+
mappings.push({ alias: arg, envVar: arg.toUpperCase().replace(/-/g, "_") + "_KEY" });
|
|
453
|
+
} else {
|
|
454
|
+
const alias = arg.slice(0, eqIndex);
|
|
455
|
+
const envVar = arg.slice(eqIndex + 1);
|
|
456
|
+
mappings.push({ alias, envVar });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return mappings;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function resolveAll(mappings) {
|
|
463
|
+
const results = await Promise.all(
|
|
464
|
+
mappings.map(async ({ alias, envVar }) => {
|
|
465
|
+
const result = await fetchKey(alias);
|
|
466
|
+
return { alias, envVar, key: result.key };
|
|
467
|
+
})
|
|
468
|
+
);
|
|
469
|
+
return results;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function resolveAllProxy(mappings) {
|
|
473
|
+
const token = getToken();
|
|
474
|
+
if (!token) {
|
|
475
|
+
throw new Error("Not logged in. Run: vp login");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const cfg = loadConfig();
|
|
479
|
+
const proxyBase = cfg.proxy_url || "https://proxy.vaultproxy.dev";
|
|
480
|
+
const providers = await fetchProviders();
|
|
481
|
+
|
|
482
|
+
const results = await Promise.all(
|
|
483
|
+
mappings.map(async ({ alias, envVar }) => {
|
|
484
|
+
// Always fetch key metadata to get provider info
|
|
485
|
+
const keyInfo = await fetchKey(alias);
|
|
486
|
+
|
|
487
|
+
// D4: warn if provider is not proxy_compatible and fall back to fetch mode
|
|
488
|
+
if (!keyInfo.proxy_compatible) {
|
|
489
|
+
console.error(
|
|
490
|
+
`Warning: ${keyInfo.provider} does not support proxy mode (uses request signing). Falling back to fetch mode.`
|
|
491
|
+
);
|
|
492
|
+
return { alias, envVar, key: keyInfo.key };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Find provider record for base_url_env
|
|
496
|
+
const provider = providers.find((p) => p.id === keyInfo.provider);
|
|
497
|
+
|
|
498
|
+
const extraEnvVar = provider?.base_url_env || null;
|
|
499
|
+
const extraValue = extraEnvVar
|
|
500
|
+
? `${proxyBase}/proxy/${alias}`
|
|
501
|
+
: null;
|
|
502
|
+
|
|
503
|
+
return { alias, envVar, key: token, extraEnvVar, extraValue };
|
|
504
|
+
})
|
|
505
|
+
);
|
|
506
|
+
return results;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function maskValue(value) {
|
|
510
|
+
if (!value || value.length <= 8) return value + "...";
|
|
511
|
+
return value.slice(0, 8) + "...";
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function shellEscape(s) {
|
|
515
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
main().catch((err) => {
|
|
519
|
+
console.error(`Fatal: ${err.message}`);
|
|
520
|
+
process.exit(1);
|
|
521
|
+
});
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getApiUrl, getToken } from "./config.js";
|
|
2
|
+
|
|
3
|
+
let _providersCache = null;
|
|
4
|
+
|
|
5
|
+
export async function fetchProviders() {
|
|
6
|
+
if (_providersCache) return _providersCache;
|
|
7
|
+
try {
|
|
8
|
+
const url = `${getApiUrl()}/api/v1/providers`;
|
|
9
|
+
const res = await fetch(url);
|
|
10
|
+
if (!res.ok) return [];
|
|
11
|
+
_providersCache = await res.json();
|
|
12
|
+
return _providersCache;
|
|
13
|
+
} catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function fetchKey(alias) {
|
|
19
|
+
const token = getToken();
|
|
20
|
+
if (!token) {
|
|
21
|
+
throw new Error("Not logged in. Run: vp login");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const url = `${getApiUrl()}/internal/fetch/${encodeURIComponent(alias)}`;
|
|
25
|
+
const res = await fetch(url, {
|
|
26
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (res.status === 401) throw new Error("Invalid or expired token. Run: vp login");
|
|
30
|
+
if (res.status === 404) throw new Error(`Key alias "${alias}" not found`);
|
|
31
|
+
if (res.status === 429) throw new Error("Rate limit exceeded");
|
|
32
|
+
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
33
|
+
|
|
34
|
+
return res.json();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function listKeys() {
|
|
38
|
+
const token = getToken();
|
|
39
|
+
if (!token) {
|
|
40
|
+
throw new Error("Not logged in. Run: vp login");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const url = `${getApiUrl()}/internal/keys`;
|
|
44
|
+
const res = await fetch(url, {
|
|
45
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!res.ok) return [];
|
|
49
|
+
return res.json();
|
|
50
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = join(homedir(), ".vaultproxy");
|
|
6
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
7
|
+
|
|
8
|
+
const DEFAULTS = {
|
|
9
|
+
api_url: "https://api.vaultproxy.dev",
|
|
10
|
+
proxy_url: "https://proxy.vaultproxy.dev",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function loadConfig() {
|
|
14
|
+
if (!existsSync(CONFIG_FILE)) return { ...DEFAULTS };
|
|
15
|
+
try {
|
|
16
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
17
|
+
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
18
|
+
} catch {
|
|
19
|
+
return { ...DEFAULTS };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function saveConfig(config) {
|
|
24
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
25
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
|
|
26
|
+
mode: 0o600,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getToken() {
|
|
31
|
+
const cfg = loadConfig();
|
|
32
|
+
// Env var takes precedence over config file
|
|
33
|
+
return process.env.VAULTPROXY_TOKEN || cfg.token || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getApiUrl() {
|
|
37
|
+
return process.env.VAULTPROXY_API_URL || loadConfig().api_url;
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vaultproxy/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "VaultProxy CLI — fetch, inject, and proxy API keys",
|
|
5
|
+
"bin": {
|
|
6
|
+
"vp": "./bin/vp.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"lib/"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/Axis-Labs-HQ/vaultproxy-cloud",
|
|
20
|
+
"directory": "cli"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://vaultproxy.dev",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"api-keys",
|
|
25
|
+
"secrets",
|
|
26
|
+
"proxy",
|
|
27
|
+
"rotation",
|
|
28
|
+
"vault",
|
|
29
|
+
"security",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|