lockbox-vault 1.0.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.
Files changed (50) hide show
  1. package/README.md +180 -0
  2. package/dist/cli/env-utils.d.ts +46 -0
  3. package/dist/cli/env-utils.d.ts.map +1 -0
  4. package/dist/cli/env-utils.js +97 -0
  5. package/dist/cli/env-utils.js.map +1 -0
  6. package/dist/cli/helpers.d.ts +49 -0
  7. package/dist/cli/helpers.d.ts.map +1 -0
  8. package/dist/cli/helpers.js +168 -0
  9. package/dist/cli/helpers.js.map +1 -0
  10. package/dist/cli/index.d.ts +6 -0
  11. package/dist/cli/index.d.ts.map +1 -0
  12. package/dist/cli/index.js +728 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/cli/session.d.ts +52 -0
  15. package/dist/cli/session.d.ts.map +1 -0
  16. package/dist/cli/session.js +188 -0
  17. package/dist/cli/session.js.map +1 -0
  18. package/dist/crypto/encryption.d.ts +32 -0
  19. package/dist/crypto/encryption.d.ts.map +1 -0
  20. package/dist/crypto/encryption.js +54 -0
  21. package/dist/crypto/encryption.js.map +1 -0
  22. package/dist/crypto/keyDerivation.d.ts +22 -0
  23. package/dist/crypto/keyDerivation.d.ts.map +1 -0
  24. package/dist/crypto/keyDerivation.js +47 -0
  25. package/dist/crypto/keyDerivation.js.map +1 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +35 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/mcp/audit.d.ts +31 -0
  31. package/dist/mcp/audit.d.ts.map +1 -0
  32. package/dist/mcp/audit.js +69 -0
  33. package/dist/mcp/audit.js.map +1 -0
  34. package/dist/mcp/server.d.ts +22 -0
  35. package/dist/mcp/server.d.ts.map +1 -0
  36. package/dist/mcp/server.js +189 -0
  37. package/dist/mcp/server.js.map +1 -0
  38. package/dist/types/index.d.ts +35 -0
  39. package/dist/types/index.d.ts.map +1 -0
  40. package/dist/types/index.js +5 -0
  41. package/dist/types/index.js.map +1 -0
  42. package/dist/vault/config.d.ts +29 -0
  43. package/dist/vault/config.d.ts.map +1 -0
  44. package/dist/vault/config.js +66 -0
  45. package/dist/vault/config.js.map +1 -0
  46. package/dist/vault/vault.d.ts +130 -0
  47. package/dist/vault/vault.d.ts.map +1 -0
  48. package/dist/vault/vault.js +296 -0
  49. package/dist/vault/vault.js.map +1 -0
  50. package/package.json +62 -0
@@ -0,0 +1,728 @@
1
+ /**
2
+ * CLI command registration — wires up all lockbox commands
3
+ */
4
+ import { existsSync, statSync, readFileSync, writeFileSync } from 'node:fs';
5
+ import { extname } from 'node:path';
6
+ import { spawnSync } from 'node:child_process';
7
+ import ora from 'ora';
8
+ import { Vault } from '../vault/vault.js';
9
+ import { loadConfig, getPaths } from '../vault/config.js';
10
+ import { saveSession, loadSession, clearSession, sessionTimeRemaining, checkRateLimit, recordFailedAttempt, clearRateLimit, } from './session.js';
11
+ import { promptPassword, promptPasswordWithConfirm, promptConfirm, promptChoice, copyAndScheduleClear, formatTable, shortDate, exitWithError, chalk, } from './helpers.js';
12
+ import { envKeyName, splitKeyName, parseEnvFile as parseEnvImport, parseJsonImport, parseProxyUri, buildProxyUri, PROXY_PREFIX, } from './env-utils.js';
13
+ import { auditLog, readAuditLog } from '../mcp/audit.js';
14
+ // ─── Helper: load vault from active session ──────────────────────────────────
15
+ function loadVaultFromSession() {
16
+ const session = loadSession();
17
+ if (!session) {
18
+ throw new Error('Vault is locked. Run `lockbox unlock` first.');
19
+ }
20
+ return Vault.openWithKey(session.vaultPath, session.key);
21
+ }
22
+ // ─── Commands ────────────────────────────────────────────────────────────────
23
+ export function registerCommands(program) {
24
+ // ── init ─────────────────────────────────────────────────────────────────
25
+ program
26
+ .command('init')
27
+ .description('Create a new encrypted vault')
28
+ .addHelpText('after', `
29
+ Examples:
30
+ $ lockbox init
31
+ `)
32
+ .action(async () => {
33
+ try {
34
+ const config = loadConfig();
35
+ const { vaultFile } = getPaths(config);
36
+ // Check for existing vault
37
+ if (existsSync(vaultFile)) {
38
+ const overwrite = await promptConfirm(chalk.yellow('A vault already exists. Overwrite it?'));
39
+ if (!overwrite) {
40
+ process.stderr.write(chalk.dim('Aborted.\n'));
41
+ return;
42
+ }
43
+ }
44
+ // Get master password
45
+ const password = await promptPasswordWithConfirm();
46
+ const spinner = ora('Creating encrypted vault...').start();
47
+ const vault = await Vault.create(vaultFile, password);
48
+ // Auto-unlock after init
49
+ saveSession(vault.getDerivedKey(), vaultFile);
50
+ vault.lock();
51
+ spinner.succeed('Vault created and unlocked!');
52
+ auditLog('init', { vaultPath: vaultFile }, 'cli');
53
+ process.stderr.write(chalk.dim(` Location: ${vaultFile}\n`));
54
+ process.stderr.write(chalk.dim(` Auto-locks in 15 minutes.\n`));
55
+ }
56
+ catch (err) {
57
+ exitWithError(err.message);
58
+ }
59
+ });
60
+ // ── unlock ───────────────────────────────────────────────────────────────
61
+ program
62
+ .command('unlock')
63
+ .description('Unlock the vault with your master password')
64
+ .addHelpText('after', `
65
+ Examples:
66
+ $ lockbox unlock
67
+ $ lockbox unlock # session lasts 15 minutes
68
+ `)
69
+ .action(async () => {
70
+ try {
71
+ const config = loadConfig();
72
+ const { vaultFile } = getPaths(config);
73
+ if (!existsSync(vaultFile)) {
74
+ throw new Error('No vault found. Run `lockbox init` first.');
75
+ }
76
+ // Check rate limit
77
+ checkRateLimit();
78
+ const password = await promptPassword(chalk.cyan('Master password: '));
79
+ const spinner = ora('Deriving encryption key...').start();
80
+ let vault;
81
+ try {
82
+ vault = await Vault.open(vaultFile, password);
83
+ }
84
+ catch (err) {
85
+ spinner.fail('Unlock failed.');
86
+ recordFailedAttempt();
87
+ throw err;
88
+ }
89
+ // Success — save session and clear rate limit
90
+ saveSession(vault.getDerivedKey(), vaultFile);
91
+ clearRateLimit();
92
+ vault.lock();
93
+ spinner.succeed('Vault unlocked!');
94
+ auditLog('unlock', {}, 'cli');
95
+ process.stderr.write(chalk.dim(' Auto-locks in 15 minutes.\n'));
96
+ }
97
+ catch (err) {
98
+ exitWithError(err.message);
99
+ }
100
+ });
101
+ // ── lock ─────────────────────────────────────────────────────────────────
102
+ program
103
+ .command('lock')
104
+ .description('Lock the vault and clear session')
105
+ .addHelpText('after', `
106
+ Examples:
107
+ $ lockbox lock
108
+ `)
109
+ .action(() => {
110
+ try {
111
+ clearSession();
112
+ auditLog('lock', {}, 'cli');
113
+ process.stderr.write(chalk.green('✓ Vault locked.\n'));
114
+ }
115
+ catch (err) {
116
+ exitWithError(err.message);
117
+ }
118
+ });
119
+ // ── add ──────────────────────────────────────────────────────────────────
120
+ program
121
+ .command('add')
122
+ .description('Add a secret to the vault')
123
+ .argument('<service>', 'Service name (e.g. openai)')
124
+ .argument('<key-name>', 'Key identifier (e.g. API_KEY)')
125
+ .argument('[value]', 'Secret value (prompted if omitted)')
126
+ .option('-p, --project <name>', 'Project name', 'default')
127
+ .option('-n, --notes <text>', 'Notes about this key', '')
128
+ .option('--expires <date>', 'Expiration date (ISO format)')
129
+ .addHelpText('after', `
130
+ Examples:
131
+ $ lockbox add openai API_KEY sk-abc123
132
+ $ lockbox add stripe SECRET_KEY --project myapp
133
+ $ lockbox add aws ACCESS_KEY # prompts for value
134
+ $ lockbox add github TOKEN --notes "PAT for CI"
135
+ `)
136
+ .action(async (service, keyName, value, opts) => {
137
+ try {
138
+ const vault = loadVaultFromSession();
139
+ // If no value provided, prompt for it (hidden)
140
+ if (!value) {
141
+ value = await promptPassword(chalk.cyan('Secret value: '));
142
+ if (!value)
143
+ throw new Error('Value cannot be empty');
144
+ }
145
+ vault.addKey(service, keyName, value, {
146
+ project: opts.project,
147
+ notes: opts.notes,
148
+ expiresAt: opts.expires ?? null,
149
+ });
150
+ await vault.save();
151
+ vault.lock();
152
+ auditLog('add', { service, key_name: keyName, project: opts.project }, 'cli');
153
+ process.stderr.write(chalk.green(`✓ Added ${service}/${keyName} to vault\n`));
154
+ }
155
+ catch (err) {
156
+ exitWithError(err.message);
157
+ }
158
+ });
159
+ // ── get ──────────────────────────────────────────────────────────────────
160
+ program
161
+ .command('get')
162
+ .description('Retrieve a secret from the vault')
163
+ .argument('<service>', 'Service name')
164
+ .argument('<key-name>', 'Key identifier')
165
+ .option('-c, --copy', 'Copy to clipboard (auto-clears after 30s)')
166
+ .addHelpText('after', `
167
+ Examples:
168
+ $ lockbox get openai API_KEY
169
+ $ lockbox get openai API_KEY --copy # copies to clipboard, clears in 30s
170
+ $ lockbox get stripe SECRET | pbcopy # pipe to other commands
171
+ `)
172
+ .action(async (service, keyName, opts) => {
173
+ try {
174
+ const vault = loadVaultFromSession();
175
+ const value = vault.getKey(service, keyName);
176
+ vault.lock();
177
+ auditLog('get', { service, key_name: keyName, copy: !!opts.copy }, 'cli');
178
+ if (opts.copy) {
179
+ await copyAndScheduleClear(value, 30);
180
+ }
181
+ else {
182
+ // Output plain value to stdout (for piping)
183
+ process.stdout.write(value);
184
+ // Add newline only if stdout is a terminal
185
+ if (process.stdout.isTTY) {
186
+ process.stdout.write('\n');
187
+ }
188
+ }
189
+ }
190
+ catch (err) {
191
+ exitWithError(err.message);
192
+ }
193
+ });
194
+ // ── list ─────────────────────────────────────────────────────────────────
195
+ program
196
+ .command('list')
197
+ .description('List all stored keys (names only, never values)')
198
+ .option('-p, --project <name>', 'Filter by project name')
199
+ .addHelpText('after', `
200
+ Examples:
201
+ $ lockbox list
202
+ $ lockbox list --project myapp
203
+ `)
204
+ .action((opts) => {
205
+ try {
206
+ const vault = loadVaultFromSession();
207
+ const keys = vault.listKeys(opts.project);
208
+ vault.lock();
209
+ auditLog('list', { project: opts.project ?? 'all' }, 'cli');
210
+ if (keys.length === 0) {
211
+ process.stderr.write(chalk.dim(opts.project
212
+ ? `No keys found in project "${opts.project}".\n`
213
+ : 'Vault is empty. Add keys with `lockbox add`.\n'));
214
+ return;
215
+ }
216
+ const rows = keys.map((k) => {
217
+ const [service, ...rest] = k.name.split('/');
218
+ return [service, rest.join('/'), k.project, shortDate(k.createdAt), shortDate(k.expiresAt)];
219
+ });
220
+ process.stderr.write(formatTable(['Service', 'Key Name', 'Project', 'Added', 'Expires'], rows) + '\n');
221
+ process.stderr.write(chalk.dim(`\n${keys.length} key(s) total\n`));
222
+ }
223
+ catch (err) {
224
+ exitWithError(err.message);
225
+ }
226
+ });
227
+ // ── remove ───────────────────────────────────────────────────────────────
228
+ program
229
+ .command('remove')
230
+ .description('Remove a secret from the vault')
231
+ .argument('<service>', 'Service name')
232
+ .argument('<key-name>', 'Key identifier')
233
+ .option('-f, --force', 'Skip confirmation')
234
+ .addHelpText('after', `
235
+ Examples:
236
+ $ lockbox remove openai API_KEY
237
+ $ lockbox remove stripe SECRET --force # skip confirmation
238
+ `)
239
+ .action(async (service, keyName, opts) => {
240
+ try {
241
+ const vault = loadVaultFromSession();
242
+ const fullKey = `${service}/${keyName}`;
243
+ // Confirm deletion unless --force
244
+ if (!opts.force) {
245
+ const confirmed = await promptConfirm(chalk.yellow(`Remove ${fullKey}?`));
246
+ if (!confirmed) {
247
+ vault.lock();
248
+ process.stderr.write(chalk.dim('Aborted.\n'));
249
+ return;
250
+ }
251
+ }
252
+ const removed = vault.removeKey(service, keyName);
253
+ if (!removed) {
254
+ vault.lock();
255
+ throw new Error(`Key not found: ${fullKey}`);
256
+ }
257
+ await vault.save();
258
+ vault.lock();
259
+ auditLog('remove', { service, key_name: keyName }, 'cli');
260
+ process.stderr.write(chalk.green(`✓ Removed ${fullKey}\n`));
261
+ }
262
+ catch (err) {
263
+ exitWithError(err.message);
264
+ }
265
+ });
266
+ // ── search ───────────────────────────────────────────────────────────────
267
+ program
268
+ .command('search')
269
+ .description('Search keys by name, service, project, or notes')
270
+ .argument('<query>', 'Search term')
271
+ .addHelpText('after', `
272
+ Examples:
273
+ $ lockbox search openai
274
+ $ lockbox search production
275
+ `)
276
+ .action((query) => {
277
+ try {
278
+ const vault = loadVaultFromSession();
279
+ const allKeys = vault.listKeys();
280
+ vault.lock();
281
+ const q = query.toLowerCase();
282
+ const matches = allKeys.filter((k) => {
283
+ const searchable = [k.name, k.project, k.notes].join(' ').toLowerCase();
284
+ return searchable.includes(q);
285
+ });
286
+ if (matches.length === 0) {
287
+ process.stderr.write(chalk.dim(`No keys matching "${query}".\n`));
288
+ return;
289
+ }
290
+ const rows = matches.map((k) => {
291
+ const [service, ...rest] = k.name.split('/');
292
+ return [service, rest.join('/'), k.project, k.notes || '—'];
293
+ });
294
+ process.stderr.write(formatTable(['Service', 'Key Name', 'Project', 'Notes'], rows) + '\n');
295
+ process.stderr.write(chalk.dim(`\n${matches.length} result(s)\n`));
296
+ }
297
+ catch (err) {
298
+ exitWithError(err.message);
299
+ }
300
+ });
301
+ // ── status ───────────────────────────────────────────────────────────────
302
+ program
303
+ .command('status')
304
+ .description('Show vault lock state, key count, and session info')
305
+ .addHelpText('after', `
306
+ Examples:
307
+ $ lockbox status
308
+ `)
309
+ .action(() => {
310
+ try {
311
+ const config = loadConfig();
312
+ const { vaultFile } = getPaths(config);
313
+ process.stderr.write(chalk.bold('Lockbox Status\n\n'));
314
+ // Vault path
315
+ process.stderr.write(` Vault: ${vaultFile}\n`);
316
+ // File exists?
317
+ if (!existsSync(vaultFile)) {
318
+ process.stderr.write(chalk.yellow(' Status: No vault found. Run `lockbox init`.\n'));
319
+ return;
320
+ }
321
+ // File size
322
+ const stats = statSync(vaultFile);
323
+ const sizeKB = (stats.size / 1024).toFixed(1);
324
+ process.stderr.write(` Size: ${sizeKB} KB\n`);
325
+ // Lock state
326
+ const session = loadSession();
327
+ if (session) {
328
+ const remaining = sessionTimeRemaining();
329
+ const mins = remaining ? Math.floor(remaining / 60) : 0;
330
+ const secs = remaining ? remaining % 60 : 0;
331
+ process.stderr.write(chalk.green(` Status: Unlocked`) +
332
+ chalk.dim(` (auto-locks in ${mins}m ${secs}s)\n`));
333
+ // Key count and project count
334
+ try {
335
+ const vault = Vault.openWithKey(session.vaultPath, session.key);
336
+ const keys = vault.listKeys();
337
+ const projects = vault.listProjects();
338
+ vault.lock();
339
+ process.stderr.write(` Keys: ${keys.length}\n`);
340
+ process.stderr.write(` Projects: ${projects.length}\n`);
341
+ }
342
+ catch {
343
+ // Session may be stale
344
+ process.stderr.write(chalk.dim(' (Could not read vault details)\n'));
345
+ }
346
+ }
347
+ else {
348
+ process.stderr.write(chalk.red(' Status: Locked\n'));
349
+ }
350
+ }
351
+ catch (err) {
352
+ exitWithError(err.message);
353
+ }
354
+ });
355
+ // ── export ─────────────────────────────────────────────────────────────
356
+ program
357
+ .command('export')
358
+ .description('Export keys to stdout (env, json, or shell format)')
359
+ .option('-p, --project <name>', 'Only export keys from this project')
360
+ .option('-f, --format <format>', 'Output format: env, json, shell', 'env')
361
+ .addHelpText('after', `
362
+ Examples:
363
+ $ lockbox export > .env
364
+ $ lockbox export --project myapp --format json
365
+ $ lockbox export --format shell >> ~/.bashrc
366
+ `)
367
+ .action((opts) => {
368
+ try {
369
+ const vault = loadVaultFromSession();
370
+ const pairs = vault.exportKeys(opts.project);
371
+ vault.lock();
372
+ auditLog('export', { project: opts.project ?? 'all', format: opts.format, count: pairs.length }, 'cli');
373
+ if (pairs.length === 0) {
374
+ process.stderr.write(chalk.dim(opts.project
375
+ ? `No keys found in project "${opts.project}".\n`
376
+ : 'Vault is empty.\n'));
377
+ return;
378
+ }
379
+ const format = opts.format;
380
+ let output;
381
+ switch (format) {
382
+ case 'json': {
383
+ const obj = {};
384
+ for (const { name, value } of pairs) {
385
+ // Use the full "service/KEY_NAME" as the JSON key
386
+ obj[name] = value;
387
+ }
388
+ output = JSON.stringify(obj, null, 2) + '\n';
389
+ break;
390
+ }
391
+ case 'shell': {
392
+ output = pairs
393
+ .map(({ name, value }) => {
394
+ // Escape double quotes and backslashes inside the value
395
+ const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
396
+ return `export ${envKeyName(name)}="${escaped}"`;
397
+ })
398
+ .join('\n') + '\n';
399
+ break;
400
+ }
401
+ case 'env':
402
+ default: {
403
+ output = pairs
404
+ .map(({ name, value }) => `${envKeyName(name)}=${value}`)
405
+ .join('\n') + '\n';
406
+ break;
407
+ }
408
+ }
409
+ // Write to stdout so users can pipe: lockbox export > .env
410
+ process.stdout.write(output);
411
+ process.stderr.write(chalk.dim(`Exported ${pairs.length} key(s) in ${format} format.\n`));
412
+ }
413
+ catch (err) {
414
+ exitWithError(err.message);
415
+ }
416
+ });
417
+ // ── import ─────────────────────────────────────────────────────────────
418
+ program
419
+ .command('import')
420
+ .description('Import keys from a .env or JSON file')
421
+ .argument('<file>', 'Path to .env or .json file')
422
+ .option('-p, --project <name>', 'Assign imported keys to this project', 'default')
423
+ .addHelpText('after', `
424
+ Examples:
425
+ $ lockbox import .env
426
+ $ lockbox import credentials.json --project myapp
427
+ `)
428
+ .action(async (file, opts) => {
429
+ try {
430
+ if (!existsSync(file)) {
431
+ throw new Error(`File not found: ${file}`);
432
+ }
433
+ const vault = loadVaultFromSession();
434
+ const raw = readFileSync(file, 'utf8');
435
+ const ext = extname(file).toLowerCase();
436
+ // Parse the file
437
+ let entries;
438
+ if (ext === '.json') {
439
+ entries = parseJsonImport(raw);
440
+ }
441
+ else {
442
+ entries = parseEnvImport(raw);
443
+ }
444
+ if (entries.length === 0) {
445
+ vault.lock();
446
+ process.stderr.write(chalk.dim('No keys found in file.\n'));
447
+ return;
448
+ }
449
+ let imported = 0;
450
+ let skipped = 0;
451
+ for (const { key, value } of entries) {
452
+ // Determine service/keyName — if key contains '/', use as-is; else use 'imported/KEY'
453
+ const [service, keyName] = splitKeyName(key);
454
+ if (vault.hasKey(service, keyName)) {
455
+ // Key already exists — ask user
456
+ const choice = await promptChoice(chalk.yellow(`Key ${service}/${keyName} already exists`), [
457
+ { key: 'o', label: 'Overwrite' },
458
+ { key: 's', label: 'Skip' },
459
+ ]);
460
+ if (choice === 's') {
461
+ skipped++;
462
+ continue;
463
+ }
464
+ }
465
+ vault.addKey(service, keyName, value, { project: opts.project });
466
+ imported++;
467
+ }
468
+ await vault.save();
469
+ vault.lock();
470
+ auditLog('import', { file, project: opts.project, imported, skipped }, 'cli');
471
+ process.stderr.write(chalk.green(`✓ Imported ${imported} key(s) into project '${opts.project}'`)
472
+ + (skipped > 0 ? chalk.dim(` (${skipped} skipped)`) : '')
473
+ + '\n');
474
+ }
475
+ catch (err) {
476
+ exitWithError(err.message);
477
+ }
478
+ });
479
+ // ── run ───────────────────────────────────────────────────────────────
480
+ program
481
+ .command('run')
482
+ .description('Run a command with lockbox:// proxy env vars resolved from the vault')
483
+ .argument('<command...>', 'Command to run (e.g. "npm start")')
484
+ .option('--env-file <path>', 'Path to env file', '.env')
485
+ .addHelpText('after', `
486
+ Examples:
487
+ $ lockbox run "npm start"
488
+ $ lockbox run --env-file .env.proxy "python app.py"
489
+ $ lockbox run "env | grep OPENAI" # verify resolution
490
+ `)
491
+ .action(async (commandParts, opts) => {
492
+ try {
493
+ const envFilePath = opts.envFile;
494
+ // 1. Read the env file
495
+ if (!existsSync(envFilePath)) {
496
+ throw new Error(`Env file not found: ${envFilePath}`);
497
+ }
498
+ const raw = readFileSync(envFilePath, 'utf8');
499
+ const entries = parseEnvImport(raw);
500
+ if (entries.length === 0) {
501
+ throw new Error(`No environment variables found in ${envFilePath}`);
502
+ }
503
+ // 2. Separate proxy refs from passthrough values
504
+ const proxyEntries = [];
505
+ const env = { ...process.env };
506
+ for (const { key, value } of entries) {
507
+ const proxy = parseProxyUri(value);
508
+ if (proxy) {
509
+ proxyEntries.push({ envKey: key, ...proxy });
510
+ }
511
+ else {
512
+ env[key] = value;
513
+ }
514
+ }
515
+ // 3. Resolve proxy references from the vault
516
+ if (proxyEntries.length > 0) {
517
+ const vault = loadVaultFromSession();
518
+ for (const { envKey, service, keyName } of proxyEntries) {
519
+ if (!vault.hasKey(service, keyName)) {
520
+ vault.lock();
521
+ throw new Error(`Proxy reference ${PROXY_PREFIX}${service}/${keyName} not found in vault`);
522
+ }
523
+ env[envKey] = vault.getKey(service, keyName);
524
+ }
525
+ vault.lock();
526
+ auditLog('run', {
527
+ envFile: envFilePath,
528
+ proxiesResolved: proxyEntries.length,
529
+ keys: proxyEntries.map(({ service, keyName }) => `${service}/${keyName}`),
530
+ }, 'cli');
531
+ process.stderr.write(chalk.dim(`Resolved ${proxyEntries.length} proxy reference(s) from vault.\n`));
532
+ }
533
+ // 4. Spawn the child process
534
+ const cmd = commandParts.join(' ');
535
+ const { status } = spawnSync(cmd, {
536
+ shell: true,
537
+ stdio: 'inherit',
538
+ env,
539
+ });
540
+ process.exit(status ?? 1);
541
+ }
542
+ catch (err) {
543
+ exitWithError(err.message);
544
+ }
545
+ });
546
+ // ── proxy-init ────────────────────────────────────────────────────────
547
+ program
548
+ .command('proxy-init')
549
+ .description('Generate a .env.proxy file with lockbox:// references (safe to commit)')
550
+ .requiredOption('-p, --project <name>', 'Project name to generate proxy for')
551
+ .option('-o, --output <path>', 'Output file path', '.env.proxy')
552
+ .addHelpText('after', `
553
+ Examples:
554
+ $ lockbox proxy-init --project myapp
555
+ $ lockbox proxy-init -p myapp -o .env.staging
556
+ $ git add .env.proxy # safe to commit!
557
+ `)
558
+ .action(async (opts) => {
559
+ try {
560
+ const vault = loadVaultFromSession();
561
+ const keys = vault.listKeys(opts.project);
562
+ vault.lock();
563
+ if (keys.length === 0) {
564
+ throw new Error(`No keys found in project "${opts.project}"`);
565
+ }
566
+ // Build the proxy env file content
567
+ const lines = [
568
+ `# Lockbox proxy env — project: ${opts.project}`,
569
+ `# Generated: ${new Date().toISOString()}`,
570
+ `# Safe to commit — contains only lockbox:// references, no real secrets`,
571
+ '',
572
+ ];
573
+ for (const key of keys) {
574
+ const slashIdx = key.name.indexOf('/');
575
+ const service = key.name.slice(0, slashIdx);
576
+ const keyName = key.name.slice(slashIdx + 1);
577
+ const envName = envKeyName(key.name);
578
+ lines.push(`${envName}=${buildProxyUri(service, keyName)}`);
579
+ }
580
+ lines.push(''); // trailing newline
581
+ const content = lines.join('\n');
582
+ const outputPath = opts.output;
583
+ // Check if file already exists
584
+ if (existsSync(outputPath)) {
585
+ const overwrite = await promptConfirm(chalk.yellow(`${outputPath} already exists. Overwrite?`));
586
+ if (!overwrite) {
587
+ process.stderr.write(chalk.dim('Aborted.\n'));
588
+ return;
589
+ }
590
+ }
591
+ writeFileSync(outputPath, content, 'utf8');
592
+ auditLog('proxy-init', { project: opts.project, output: outputPath, count: keys.length }, 'cli');
593
+ process.stderr.write(chalk.green(`✓ Generated ${outputPath}\n`));
594
+ process.stderr.write(chalk.dim(` ${keys.length} proxy reference(s) for project "${opts.project}"\n`));
595
+ process.stderr.write(chalk.dim(` Run: lockbox run --env-file ${outputPath} "your-command"\n`));
596
+ }
597
+ catch (err) {
598
+ exitWithError(err.message);
599
+ }
600
+ });
601
+ // ── audit ──────────────────────────────────────────────────────────────
602
+ program
603
+ .command('audit')
604
+ .description('Show recent audit log entries')
605
+ .option('--since <date>', 'Only show entries after this date (ISO 8601)')
606
+ .option('-n, --limit <number>', 'Number of entries to show', '50')
607
+ .addHelpText('after', `
608
+ Examples:
609
+ $ lockbox audit
610
+ $ lockbox audit --since 2025-01-01
611
+ $ lockbox audit -n 10
612
+ `)
613
+ .action((opts) => {
614
+ try {
615
+ const limit = parseInt(opts.limit, 10) || 50;
616
+ const entries = readAuditLog(opts.since, limit);
617
+ if (entries.length === 0) {
618
+ process.stderr.write(chalk.dim(opts.since
619
+ ? `No audit entries found since ${opts.since}.\n`
620
+ : 'No audit entries found.\n'));
621
+ return;
622
+ }
623
+ const rows = entries.map((e) => [
624
+ shortDate(e.timestamp) + ' ' + e.timestamp.slice(11, 19),
625
+ e.source,
626
+ e.operation,
627
+ e.params.length > 60 ? e.params.slice(0, 57) + '...' : e.params,
628
+ ]);
629
+ process.stderr.write(formatTable(['Time', 'Source', 'Operation', 'Details'], rows) + '\n');
630
+ process.stderr.write(chalk.dim(`\n${entries.length} entries shown\n`));
631
+ }
632
+ catch (err) {
633
+ exitWithError(err.message);
634
+ }
635
+ });
636
+ // ── doctor ─────────────────────────────────────────────────────────────
637
+ program
638
+ .command('doctor')
639
+ .description('Run health checks on the vault')
640
+ .addHelpText('after', `
641
+ Examples:
642
+ $ lockbox doctor
643
+ `)
644
+ .action(async () => {
645
+ let issues = 0;
646
+ process.stderr.write(chalk.bold('Lockbox Doctor\n\n'));
647
+ // 1. Check vault file exists and is readable
648
+ const config = loadConfig();
649
+ const { vaultFile } = getPaths(config);
650
+ if (!existsSync(vaultFile)) {
651
+ process.stderr.write(chalk.red(' ✗ Vault file not found\n'));
652
+ process.stderr.write(chalk.dim(` Expected at: ${vaultFile}\n`));
653
+ process.stderr.write(chalk.dim(' Run `lockbox init` to create a vault.\n'));
654
+ return;
655
+ }
656
+ const stats = statSync(vaultFile);
657
+ if (stats.size === 0) {
658
+ process.stderr.write(chalk.red(' ✗ Vault file is empty\n'));
659
+ issues++;
660
+ }
661
+ else {
662
+ process.stderr.write(chalk.green(' ✓ Vault file exists and is readable\n'));
663
+ }
664
+ // 2. Check vault JSON is parseable
665
+ let vaultJson = null;
666
+ try {
667
+ const raw = readFileSync(vaultFile, 'utf8');
668
+ vaultJson = JSON.parse(raw);
669
+ const requiredFields = ['salt', 'iv', 'tag', 'ciphertext', 'hmac'];
670
+ const missing = requiredFields.filter((f) => !(f in vaultJson));
671
+ if (missing.length > 0) {
672
+ process.stderr.write(chalk.red(` ✗ Vault file missing fields: ${missing.join(', ')}\n`));
673
+ issues++;
674
+ }
675
+ else {
676
+ process.stderr.write(chalk.green(' ✓ Vault file structure is valid\n'));
677
+ }
678
+ }
679
+ catch {
680
+ process.stderr.write(chalk.red(' ✗ Vault file is not valid JSON\n'));
681
+ issues++;
682
+ }
683
+ // 3. Verify HMAC integrity (requires unlocked session)
684
+ const session = loadSession();
685
+ if (session && vaultJson) {
686
+ try {
687
+ const vault = Vault.openWithKey(session.vaultPath, session.key);
688
+ vault.lock();
689
+ process.stderr.write(chalk.green(' ✓ HMAC integrity verified\n'));
690
+ process.stderr.write(chalk.green(' ✓ Decryption round-trip successful\n'));
691
+ }
692
+ catch (err) {
693
+ const msg = err.message;
694
+ if (msg.includes('integrity') || msg.includes('tampered')) {
695
+ process.stderr.write(chalk.red(' ✗ Vault file has been tampered with or corrupted\n'));
696
+ }
697
+ else {
698
+ process.stderr.write(chalk.red(` ✗ Vault decryption failed: ${msg}\n`));
699
+ }
700
+ issues++;
701
+ }
702
+ }
703
+ else if (!session) {
704
+ process.stderr.write(chalk.yellow(' ⚠ Vault is locked — cannot verify HMAC or decryption\n'));
705
+ process.stderr.write(chalk.dim(' Run `lockbox unlock` to enable full health check.\n'));
706
+ }
707
+ // 4. Check session file status
708
+ if (session) {
709
+ const remaining = sessionTimeRemaining();
710
+ const mins = remaining ? Math.floor(remaining / 60) : 0;
711
+ const secs = remaining ? remaining % 60 : 0;
712
+ process.stderr.write(chalk.green(` ✓ Session active (${mins}m ${secs}s remaining)\n`));
713
+ }
714
+ else {
715
+ process.stderr.write(chalk.dim(' ○ No active session (vault is locked)\n'));
716
+ }
717
+ // 5. Summary
718
+ process.stderr.write('\n');
719
+ if (issues === 0) {
720
+ process.stderr.write(chalk.green.bold(' ✓ Vault is healthy\n'));
721
+ }
722
+ else {
723
+ process.stderr.write(chalk.red.bold(` ✗ ${issues} issue(s) found\n`));
724
+ process.exit(1);
725
+ }
726
+ });
727
+ }
728
+ //# sourceMappingURL=index.js.map