lsh-framework 3.2.3 → 3.2.5
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/commands/sync.js +2 -1
- package/dist/services/secrets/secrets.js +172 -10
- package/package.json +1 -1
package/dist/commands/sync.js
CHANGED
|
@@ -344,6 +344,7 @@ export function registerSyncCommands(program) {
|
|
|
344
344
|
.description('⬇️ Pull secrets from IPFS (auto-resolves via IPNS if no CID given)')
|
|
345
345
|
.option('-o, --output <path>', 'Output file path', '.env')
|
|
346
346
|
.option('-e, --env <name>', 'Environment name', '')
|
|
347
|
+
.option('-r, --repo <name>', 'Source repo name for IPNS resolution (overrides auto-detected repo)')
|
|
347
348
|
.option('--force', 'Overwrite existing file without backup')
|
|
348
349
|
.action(async (cid, options) => {
|
|
349
350
|
const spinner = ora(cid ? 'Downloading from IPFS...' : 'Resolving latest secrets via IPNS...').start();
|
|
@@ -378,7 +379,7 @@ export function registerSyncCommands(program) {
|
|
|
378
379
|
process.exit(1);
|
|
379
380
|
}
|
|
380
381
|
const gitInfo = getGitRepoInfo();
|
|
381
|
-
const repoName = gitInfo?.repoName || DEFAULTS.DEFAULT_ENVIRONMENT;
|
|
382
|
+
const repoName = options.repo || gitInfo?.repoName || DEFAULTS.DEFAULT_ENVIRONMENT;
|
|
382
383
|
const environment = options.env || DEFAULTS.DEFAULT_ENVIRONMENT;
|
|
383
384
|
const keyInfo = deriveKeyInfo(ipnsKey, repoName, environment);
|
|
384
385
|
const ipnsName = await ensureKeyImported(ipfsSync.getApiUrl(), keyInfo);
|
|
@@ -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.5",
|
|
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": {
|