lsh-framework 3.2.3 → 3.2.4

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.
@@ -15,6 +15,40 @@ import { IPFSClientManager } from '../../lib/ipfs-client-manager.js';
15
15
  function isOutputFormat(value) {
16
16
  return ['env', 'json', 'yaml', 'toml', 'export'].includes(value);
17
17
  }
18
+ /**
19
+ * Find existing LSH_SECRETS_KEY from environment, local .env, or global ~/.env
20
+ */
21
+ function findExistingKey() {
22
+ // 1. Check environment variable
23
+ const envKey = process.env[ENV_VARS.LSH_SECRETS_KEY];
24
+ if (envKey)
25
+ return envKey;
26
+ // 2. Check local .env
27
+ const localEnvPath = path.join(process.cwd(), '.env');
28
+ const localKey = readKeyFromEnvFile(localEnvPath);
29
+ if (localKey)
30
+ return localKey;
31
+ // 3. Check global ~/.env
32
+ const globalEnvPath = path.join(process.env.HOME || '~', '.env');
33
+ const globalKey = readKeyFromEnvFile(globalEnvPath);
34
+ if (globalKey)
35
+ return globalKey;
36
+ return null;
37
+ }
38
+ function readKeyFromEnvFile(envPath) {
39
+ try {
40
+ if (fs.existsSync(envPath)) {
41
+ const content = fs.readFileSync(envPath, 'utf-8');
42
+ const match = content.match(/^LSH_SECRETS_KEY=['"]?([^'"\n]+)['"]?/m);
43
+ if (match)
44
+ return match[1];
45
+ }
46
+ }
47
+ catch {
48
+ // Ignore read errors
49
+ }
50
+ return null;
51
+ }
18
52
  export async function init_secrets(program) {
19
53
  // Push secrets to cloud
20
54
  program
@@ -264,25 +298,153 @@ export async function init_secrets(program) {
264
298
  process.exit(1);
265
299
  }
266
300
  });
267
- // Generate encryption key
268
- program
301
+ // Key management command group
302
+ const keyCmd = program
269
303
  .command('key')
304
+ .description('Manage your LSH encryption key');
305
+ // Default action: show existing key or prompt to generate
306
+ keyCmd
307
+ .action(async () => {
308
+ const existingKey = findExistingKey();
309
+ if (existingKey) {
310
+ const masked = existingKey.slice(0, 8) + '...' + existingKey.slice(-4);
311
+ console.log(`\nšŸ”‘ LSH_SECRETS_KEY=${masked}\n`);
312
+ console.log(' lsh key show --no-mask Reveal full key');
313
+ console.log(' lsh key generate Generate a new key');
314
+ console.log(' lsh key import Import a key from a teammate\n');
315
+ }
316
+ else {
317
+ console.log('\nāš ļø No encryption key found.\n');
318
+ console.log(' lsh key generate Create a new key');
319
+ console.log(' lsh key import Import a key from a teammate\n');
320
+ }
321
+ });
322
+ // lsh key show
323
+ keyCmd
324
+ .command('show')
325
+ .description('Display the current encryption key')
326
+ .option('--no-mask', 'Show the full key (default: masked)')
327
+ .option('--export', 'Output in export format for shell evaluation')
328
+ .action(async (options) => {
329
+ const existingKey = findExistingKey();
330
+ if (!existingKey) {
331
+ console.error('āŒ No encryption key found. Run: lsh key generate');
332
+ process.exit(1);
333
+ }
334
+ if (options.export) {
335
+ console.log(`export LSH_SECRETS_KEY='${existingKey}'`);
336
+ return;
337
+ }
338
+ const displayKey = options.mask === false ? existingKey : existingKey.slice(0, 8) + '...' + existingKey.slice(-4);
339
+ console.log(`\nšŸ”‘ LSH_SECRETS_KEY=${displayKey}\n`);
340
+ if (options.mask !== false) {
341
+ console.log('šŸ’” Use --no-mask to reveal the full key');
342
+ }
343
+ console.log('šŸ’” Share this key securely with your team to sync secrets.');
344
+ console.log(' Never commit it to git!\n');
345
+ });
346
+ // lsh key generate
347
+ keyCmd
348
+ .command('generate')
270
349
  .description('Generate a new encryption key')
350
+ .option('--force', 'Overwrite existing key')
271
351
  .option('--export', 'Output in export format for shell evaluation')
272
352
  .action(async (options) => {
353
+ const existingKey = findExistingKey();
354
+ if (existingKey && !options.force) {
355
+ console.error('āŒ An encryption key already exists. Use --force to overwrite.');
356
+ console.error('āš ļø Warning: existing secrets will NOT be decryptable with a new key.\n');
357
+ process.exit(1);
358
+ }
273
359
  const { randomBytes } = await import('crypto');
274
360
  const key = randomBytes(32).toString('hex');
275
361
  if (options.export) {
276
- // Just output the export statement for eval
277
362
  console.log(`export LSH_SECRETS_KEY='${key}'`);
363
+ return;
278
364
  }
279
- else {
280
- // Interactive output with tips
281
- console.log('\nšŸ”‘ New encryption key (add to your .env):\n');
282
- console.log(`export LSH_SECRETS_KEY='${key}'\n`);
283
- console.log('šŸ’” Tip: Share this key securely with your team to sync secrets.');
284
- console.log(' Never commit it to git!\n');
285
- console.log('šŸ’” To load immediately: eval "$(lsh key --export)"\n');
365
+ if (existingKey && options.force) {
366
+ console.log('\nāš ļø Replacing existing key. Old secrets will NOT be decryptable with this new key!\n');
367
+ }
368
+ console.log('\nšŸ”‘ New encryption key (add to your .env):\n');
369
+ console.log(`export LSH_SECRETS_KEY='${key}'\n`);
370
+ console.log('šŸ’” Tip: Share this key securely with your team to sync secrets.');
371
+ console.log(' Never commit it to git!\n');
372
+ console.log('šŸ’” To load immediately: eval "$(lsh key generate --export)"\n');
373
+ });
374
+ // lsh key import
375
+ keyCmd
376
+ .command('import')
377
+ .description('Import an encryption key from a teammate')
378
+ .argument('[key]', 'The encryption key to import')
379
+ .option('-f, --file <path>', 'Path to .env file to save to', '.env')
380
+ .option('-g, --global', 'Save to global ~/.env instead of local')
381
+ .action(async (keyArg, options) => {
382
+ let keyValue = keyArg;
383
+ // If no key argument, prompt for it
384
+ if (!keyValue) {
385
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
386
+ keyValue = await new Promise((resolve) => {
387
+ rl.question('šŸ”‘ Paste the encryption key: ', (answer) => {
388
+ rl.close();
389
+ resolve(answer.trim());
390
+ });
391
+ });
392
+ }
393
+ if (!keyValue || keyValue.length === 0) {
394
+ console.error('āŒ No key provided.');
395
+ process.exit(1);
396
+ }
397
+ // Validate key format (should be 64 hex chars = 32 bytes)
398
+ if (!/^[0-9a-fA-F]{64}$/.test(keyValue)) {
399
+ console.error('āŒ Invalid key format. Expected 64 hex characters (256-bit key).');
400
+ process.exit(1);
401
+ }
402
+ // Check for existing key
403
+ const existingKey = findExistingKey();
404
+ if (existingKey) {
405
+ if (existingKey === keyValue) {
406
+ console.log('\nāœ… This key is already configured.\n');
407
+ return;
408
+ }
409
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
410
+ const confirm = await new Promise((resolve) => {
411
+ rl.question('āš ļø An existing key will be replaced. Continue? (y/N) ', (answer) => {
412
+ rl.close();
413
+ resolve(answer.trim().toLowerCase());
414
+ });
415
+ });
416
+ if (confirm !== 'y' && confirm !== 'yes') {
417
+ console.log('Aborted.');
418
+ return;
419
+ }
420
+ }
421
+ // Save to .env file
422
+ const envPath = options.global
423
+ ? path.join(process.env.HOME || '~', '.env')
424
+ : path.resolve(options.file);
425
+ try {
426
+ let content = '';
427
+ if (fs.existsSync(envPath)) {
428
+ content = fs.readFileSync(envPath, 'utf-8');
429
+ // Replace existing key if present
430
+ if (/^LSH_SECRETS_KEY=/m.test(content)) {
431
+ content = content.replace(/^LSH_SECRETS_KEY=.*/m, `LSH_SECRETS_KEY=${keyValue}`);
432
+ }
433
+ else {
434
+ content = content.trimEnd() + `\nLSH_SECRETS_KEY=${keyValue}\n`;
435
+ }
436
+ }
437
+ else {
438
+ content = `LSH_SECRETS_KEY=${keyValue}\n`;
439
+ }
440
+ fs.writeFileSync(envPath, content, { mode: 0o600 });
441
+ console.log(`\nāœ… Key saved to ${envPath}\n`);
442
+ console.log('šŸ’” Now pull secrets: lsh sync pull\n');
443
+ }
444
+ catch (error) {
445
+ const err = error;
446
+ console.error(`āŒ Failed to save key: ${err.message}`);
447
+ process.exit(1);
286
448
  }
287
449
  });
288
450
  // Create .env file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "3.2.3",
3
+ "version": "3.2.4",
4
4
  "description": "Simple, cross-platform encrypted secrets manager with automatic sync, IPFS audit logs, and multi-environment support. Just run lsh sync and start managing your secrets.",
5
5
  "main": "dist/app.js",
6
6
  "bin": {