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.
- package/dist/services/secrets/secrets.js +172 -10
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
"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": {
|