lsh-framework 0.8.2 ā 0.8.3
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/lib/secrets-manager.js +159 -145
- package/package.json +2 -1
|
@@ -6,7 +6,7 @@ import * as fs from 'fs';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import * as crypto from 'crypto';
|
|
8
8
|
import DatabasePersistence from './database-persistence.js';
|
|
9
|
-
import { createLogger } from './logger.js';
|
|
9
|
+
import { createLogger, LogLevel } from './logger.js';
|
|
10
10
|
import { getGitRepoInfo, hasEnvExample, ensureEnvInGitignore } from './git-utils.js';
|
|
11
11
|
const logger = createLogger('SecretsManager');
|
|
12
12
|
export class SecretsManager {
|
|
@@ -476,117 +476,161 @@ LSH_SECRETS_KEY=${this.encryptionKey}
|
|
|
476
476
|
* This is the new enhanced sync that does everything automatically
|
|
477
477
|
*/
|
|
478
478
|
async smartSync(envFilePath = '.env', environment = 'dev', autoExecute = true, loadMode = false) {
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
out(`\nš Smart sync for: ${displayEnv}\n`);
|
|
485
|
-
// Show git repo context if detected
|
|
486
|
-
if (this.gitInfo?.isGitRepo) {
|
|
487
|
-
out('š Git Repository:');
|
|
488
|
-
out(` Repo: ${this.gitInfo.repoName || 'unknown'}`);
|
|
489
|
-
if (this.gitInfo.currentBranch) {
|
|
490
|
-
out(` Branch: ${this.gitInfo.currentBranch}`);
|
|
491
|
-
}
|
|
492
|
-
out();
|
|
493
|
-
}
|
|
494
|
-
// Step 1: Ensure encryption key exists
|
|
495
|
-
if (!process.env.LSH_SECRETS_KEY) {
|
|
496
|
-
logger.info('š No encryption key found...');
|
|
497
|
-
await this.ensureEncryptionKey();
|
|
498
|
-
out();
|
|
499
|
-
}
|
|
500
|
-
// Step 2: Ensure .gitignore includes .env
|
|
501
|
-
if (this.gitInfo?.isGitRepo) {
|
|
502
|
-
ensureEnvInGitignore(process.cwd());
|
|
503
|
-
}
|
|
504
|
-
// Step 3: Check current status
|
|
505
|
-
const status = await this.status(envFilePath, effectiveEnv);
|
|
506
|
-
out('š Current Status:');
|
|
507
|
-
out(` Encryption key: ${status.keySet ? 'ā
' : 'ā'}`);
|
|
508
|
-
out(` Local ${envFilePath}: ${status.localExists ? `ā
(${status.localKeys} keys)` : 'ā'}`);
|
|
509
|
-
out(` Cloud storage: ${status.cloudExists ? `ā
(${status.cloudKeys} keys)` : 'ā'}`);
|
|
510
|
-
if (status.cloudExists && status.keyMatches !== undefined) {
|
|
511
|
-
out(` Key matches: ${status.keyMatches ? 'ā
' : 'ā'}`);
|
|
512
|
-
}
|
|
513
|
-
out();
|
|
514
|
-
// Step 4: Determine action and execute if auto mode
|
|
515
|
-
let action = 'in-sync';
|
|
516
|
-
if (status.cloudExists && status.keyMatches === false) {
|
|
517
|
-
action = 'key-mismatch';
|
|
518
|
-
out('ā ļø Encryption key mismatch!');
|
|
519
|
-
out(' The local key does not match the cloud storage.');
|
|
520
|
-
out(' Please use the original key or push new secrets with:');
|
|
521
|
-
out(` lsh lib secrets push -f ${envFilePath} -e ${environment}`);
|
|
522
|
-
out();
|
|
523
|
-
return;
|
|
479
|
+
// In load mode, suppress all logger output to prevent zsh glob interpretation
|
|
480
|
+
// Save original level and restore at the end
|
|
481
|
+
const originalLogLevel = loadMode ? logger['config'].level : undefined;
|
|
482
|
+
if (loadMode) {
|
|
483
|
+
logger.setLevel(LogLevel.NONE);
|
|
524
484
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
485
|
+
try {
|
|
486
|
+
// Use repo-aware environment if in git repo
|
|
487
|
+
const effectiveEnv = this.getRepoAwareEnvironment(environment);
|
|
488
|
+
const displayEnv = this.gitInfo?.repoName ? `${this.gitInfo.repoName}/${environment}` : environment;
|
|
489
|
+
// In load mode, suppress all output except the final export commands
|
|
490
|
+
const out = loadMode ? () => { } : console.log;
|
|
491
|
+
out(`\nš Smart sync for: ${displayEnv}\n`);
|
|
492
|
+
// Show git repo context if detected
|
|
493
|
+
if (this.gitInfo?.isGitRepo) {
|
|
494
|
+
out('š Git Repository:');
|
|
495
|
+
out(` Repo: ${this.gitInfo.repoName || 'unknown'}`);
|
|
496
|
+
if (this.gitInfo.currentBranch) {
|
|
497
|
+
out(` Branch: ${this.gitInfo.currentBranch}`);
|
|
498
|
+
}
|
|
533
499
|
out();
|
|
534
|
-
out('ā
Setup complete! Edit your .env and run sync again to update.');
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
out('š” Run: lsh lib secrets create && lsh lib secrets push');
|
|
538
500
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
501
|
+
// Step 1: Ensure encryption key exists
|
|
502
|
+
if (!process.env.LSH_SECRETS_KEY) {
|
|
503
|
+
logger.info('š No encryption key found...');
|
|
504
|
+
await this.ensureEncryptionKey();
|
|
505
|
+
out();
|
|
543
506
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
action = 'push';
|
|
548
|
-
out('ā¬ļø Local .env exists but not in cloud');
|
|
549
|
-
if (autoExecute) {
|
|
550
|
-
out(' Pushing to cloud...');
|
|
551
|
-
await this.push(envFilePath, effectiveEnv);
|
|
552
|
-
out('ā
Secrets pushed to cloud!');
|
|
507
|
+
// Step 2: Ensure .gitignore includes .env
|
|
508
|
+
if (this.gitInfo?.isGitRepo) {
|
|
509
|
+
ensureEnvInGitignore(process.cwd());
|
|
553
510
|
}
|
|
554
|
-
|
|
555
|
-
|
|
511
|
+
// Step 3: Check current status
|
|
512
|
+
const status = await this.status(envFilePath, effectiveEnv);
|
|
513
|
+
out('š Current Status:');
|
|
514
|
+
out(` Encryption key: ${status.keySet ? 'ā
' : 'ā'}`);
|
|
515
|
+
out(` Local ${envFilePath}: ${status.localExists ? `ā
(${status.localKeys} keys)` : 'ā'}`);
|
|
516
|
+
out(` Cloud storage: ${status.cloudExists ? `ā
(${status.cloudKeys} keys)` : 'ā'}`);
|
|
517
|
+
if (status.cloudExists && status.keyMatches !== undefined) {
|
|
518
|
+
out(` Key matches: ${status.keyMatches ? 'ā
' : 'ā'}`);
|
|
556
519
|
}
|
|
557
520
|
out();
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
|
|
521
|
+
// Step 4: Determine action and execute if auto mode
|
|
522
|
+
let action = 'in-sync';
|
|
523
|
+
if (status.cloudExists && status.keyMatches === false) {
|
|
524
|
+
action = 'key-mismatch';
|
|
525
|
+
out('ā ļø Encryption key mismatch!');
|
|
526
|
+
out(' The local key does not match the cloud storage.');
|
|
527
|
+
out(' Please use the original key or push new secrets with:');
|
|
528
|
+
out(` lsh lib secrets push -f ${envFilePath} -e ${environment}`);
|
|
529
|
+
out();
|
|
530
|
+
return;
|
|
561
531
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
532
|
+
if (!status.localExists && !status.cloudExists) {
|
|
533
|
+
action = 'create-and-push';
|
|
534
|
+
out('š No secrets found locally or in cloud');
|
|
535
|
+
out(' Creating new .env file...');
|
|
536
|
+
if (autoExecute) {
|
|
537
|
+
await this.createEnvFromExample(envFilePath);
|
|
538
|
+
out(' Pushing to cloud...');
|
|
539
|
+
await this.push(envFilePath, effectiveEnv);
|
|
540
|
+
out();
|
|
541
|
+
out('ā
Setup complete! Edit your .env and run sync again to update.');
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
out('š” Run: lsh lib secrets create && lsh lib secrets push');
|
|
545
|
+
}
|
|
546
|
+
out();
|
|
547
|
+
// Output export commands in load mode
|
|
548
|
+
if (loadMode && fs.existsSync(envFilePath)) {
|
|
549
|
+
console.log(this.generateExportCommands(envFilePath));
|
|
550
|
+
}
|
|
551
|
+
return;
|
|
571
552
|
}
|
|
572
|
-
|
|
573
|
-
|
|
553
|
+
if (status.localExists && !status.cloudExists) {
|
|
554
|
+
action = 'push';
|
|
555
|
+
out('ā¬ļø Local .env exists but not in cloud');
|
|
556
|
+
if (autoExecute) {
|
|
557
|
+
out(' Pushing to cloud...');
|
|
558
|
+
await this.push(envFilePath, effectiveEnv);
|
|
559
|
+
out('ā
Secrets pushed to cloud!');
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
out(`š” Run: lsh lib secrets push -f ${envFilePath} -e ${environment}`);
|
|
563
|
+
}
|
|
564
|
+
out();
|
|
565
|
+
// Output export commands in load mode
|
|
566
|
+
if (loadMode && fs.existsSync(envFilePath)) {
|
|
567
|
+
console.log(this.generateExportCommands(envFilePath));
|
|
568
|
+
}
|
|
569
|
+
return;
|
|
574
570
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
571
|
+
if (!status.localExists && status.cloudExists && status.keyMatches) {
|
|
572
|
+
action = 'pull';
|
|
573
|
+
out('ā¬ļø Cloud secrets available but no local file');
|
|
574
|
+
if (autoExecute) {
|
|
575
|
+
out(' Pulling from cloud...');
|
|
576
|
+
await this.pull(envFilePath, effectiveEnv, false);
|
|
577
|
+
out('ā
Secrets pulled from cloud!');
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
out(`š” Run: lsh lib secrets pull -f ${envFilePath} -e ${environment}`);
|
|
581
|
+
}
|
|
582
|
+
out();
|
|
583
|
+
// Output export commands in load mode
|
|
584
|
+
if (loadMode && fs.existsSync(envFilePath)) {
|
|
585
|
+
console.log(this.generateExportCommands(envFilePath));
|
|
586
|
+
}
|
|
587
|
+
return;
|
|
579
588
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
589
|
+
if (status.localExists && status.cloudExists && status.keyMatches) {
|
|
590
|
+
if (status.localModified && status.cloudModified) {
|
|
591
|
+
const localNewer = status.localModified > status.cloudModified;
|
|
592
|
+
const timeDiff = Math.abs(status.localModified.getTime() - status.cloudModified.getTime());
|
|
593
|
+
const minutesDiff = Math.floor(timeDiff / (1000 * 60));
|
|
594
|
+
// If difference is less than 1 minute, consider in sync
|
|
595
|
+
if (minutesDiff < 1) {
|
|
596
|
+
out('ā
Local and cloud are in sync!');
|
|
597
|
+
out();
|
|
598
|
+
if (!loadMode) {
|
|
599
|
+
this.showLoadInstructions(envFilePath);
|
|
600
|
+
}
|
|
601
|
+
else if (fs.existsSync(envFilePath)) {
|
|
602
|
+
console.log(this.generateExportCommands(envFilePath));
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (localNewer) {
|
|
607
|
+
action = 'push';
|
|
608
|
+
out('ā¬ļø Local file is newer than cloud');
|
|
609
|
+
out(` Local: ${status.localModified.toLocaleString()}`);
|
|
610
|
+
out(` Cloud: ${status.cloudModified.toLocaleString()}`);
|
|
611
|
+
if (autoExecute) {
|
|
612
|
+
out(' Pushing to cloud...');
|
|
613
|
+
await this.push(envFilePath, effectiveEnv);
|
|
614
|
+
out('ā
Secrets synced to cloud!');
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
out(`š” Run: lsh lib secrets push -f ${envFilePath} -e ${environment}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
action = 'pull';
|
|
622
|
+
out('ā¬ļø Cloud is newer than local file');
|
|
623
|
+
out(` Local: ${status.localModified.toLocaleString()}`);
|
|
624
|
+
out(` Cloud: ${status.cloudModified.toLocaleString()}`);
|
|
625
|
+
if (autoExecute) {
|
|
626
|
+
out(' Pulling from cloud (backup created)...');
|
|
627
|
+
await this.pull(envFilePath, effectiveEnv, false);
|
|
628
|
+
out('ā
Secrets synced from cloud!');
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
out(`š” Run: lsh lib secrets pull -f ${envFilePath} -e ${environment}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
590
634
|
out();
|
|
591
635
|
if (!loadMode) {
|
|
592
636
|
this.showLoadInstructions(envFilePath);
|
|
@@ -596,52 +640,22 @@ LSH_SECRETS_KEY=${this.encryptionKey}
|
|
|
596
640
|
}
|
|
597
641
|
return;
|
|
598
642
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
out(`š” Run: lsh lib secrets push -f ${envFilePath} -e ${environment}`);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
else {
|
|
614
|
-
action = 'pull';
|
|
615
|
-
out('ā¬ļø Cloud is newer than local file');
|
|
616
|
-
out(` Local: ${status.localModified.toLocaleString()}`);
|
|
617
|
-
out(` Cloud: ${status.cloudModified.toLocaleString()}`);
|
|
618
|
-
if (autoExecute) {
|
|
619
|
-
out(' Pulling from cloud (backup created)...');
|
|
620
|
-
await this.pull(envFilePath, effectiveEnv, false);
|
|
621
|
-
out('ā
Secrets synced from cloud!');
|
|
622
|
-
}
|
|
623
|
-
else {
|
|
624
|
-
out(`š” Run: lsh lib secrets pull -f ${envFilePath} -e ${environment}`);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
out();
|
|
628
|
-
if (!loadMode) {
|
|
629
|
-
this.showLoadInstructions(envFilePath);
|
|
630
|
-
}
|
|
631
|
-
else if (fs.existsSync(envFilePath)) {
|
|
632
|
-
console.log(this.generateExportCommands(envFilePath));
|
|
633
|
-
}
|
|
634
|
-
return;
|
|
643
|
+
}
|
|
644
|
+
// Default: everything is in sync
|
|
645
|
+
out('ā
Secrets are synchronized!');
|
|
646
|
+
out();
|
|
647
|
+
if (!loadMode) {
|
|
648
|
+
this.showLoadInstructions(envFilePath);
|
|
649
|
+
}
|
|
650
|
+
else if (fs.existsSync(envFilePath)) {
|
|
651
|
+
console.log(this.generateExportCommands(envFilePath));
|
|
635
652
|
}
|
|
636
653
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
else if (fs.existsSync(envFilePath)) {
|
|
644
|
-
console.log(this.generateExportCommands(envFilePath));
|
|
654
|
+
finally {
|
|
655
|
+
// Restore original logger level if it was changed
|
|
656
|
+
if (loadMode && originalLogLevel !== undefined) {
|
|
657
|
+
logger.setLevel(originalLogLevel);
|
|
658
|
+
}
|
|
645
659
|
}
|
|
646
660
|
}
|
|
647
661
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lsh-framework",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Encrypted secrets manager with automatic rotation, team sync, and multi-environment support. Built on a powerful shell with daemon scheduling and CI/CD integration.",
|
|
5
5
|
"main": "dist/app.js",
|
|
6
6
|
"bin": {
|
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
"ioredis": "^5.8.0",
|
|
107
107
|
"jsonwebtoken": "^9.0.2",
|
|
108
108
|
"lodash": "^4.17.21",
|
|
109
|
+
"lsh-framework": "^0.8.2",
|
|
109
110
|
"node-cron": "^3.0.3",
|
|
110
111
|
"node-fetch": "^3.3.2",
|
|
111
112
|
"ora": "^8.0.1",
|