lsh-framework 0.6.0 → 0.8.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.
@@ -3,6 +3,7 @@
3
3
  * Sync .env files across machines using encrypted Supabase storage
4
4
  */
5
5
  import * as fs from 'fs';
6
+ import * as path from 'path';
6
7
  import * as crypto from 'crypto';
7
8
  import DatabasePersistence from './database-persistence.js';
8
9
  import { createLogger } from './logger.js';
@@ -45,14 +46,30 @@ export class SecretsManager {
45
46
  * Decrypt a value
46
47
  */
47
48
  decrypt(text) {
48
- const parts = text.split(':');
49
- const iv = Buffer.from(parts[0], 'hex');
50
- const encryptedText = parts[1];
51
- const key = Buffer.from(this.encryptionKey, 'hex');
52
- const decipher = crypto.createDecipheriv('aes-256-cbc', key.slice(0, 32), iv);
53
- let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
54
- decrypted += decipher.final('utf8');
55
- return decrypted;
49
+ try {
50
+ const parts = text.split(':');
51
+ if (parts.length !== 2) {
52
+ throw new Error('Invalid encrypted format');
53
+ }
54
+ const iv = Buffer.from(parts[0], 'hex');
55
+ const encryptedText = parts[1];
56
+ const key = Buffer.from(this.encryptionKey, 'hex');
57
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key.slice(0, 32), iv);
58
+ let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
59
+ decrypted += decipher.final('utf8');
60
+ return decrypted;
61
+ }
62
+ catch (error) {
63
+ if (error.message.includes('bad decrypt') || error.message.includes('wrong final block length')) {
64
+ throw new Error('Decryption failed. This usually means:\n' +
65
+ ' 1. You need to set LSH_SECRETS_KEY environment variable\n' +
66
+ ' 2. The key must match the one used during encryption\n' +
67
+ ' 3. Generate a shared key with: lsh secrets key\n' +
68
+ ' 4. Add it to your .env: LSH_SECRETS_KEY=<key>\n' +
69
+ '\nOriginal error: ' + error.message);
70
+ }
71
+ throw error;
72
+ }
56
73
  }
57
74
  /**
58
75
  * Parse .env file into key-value pairs
@@ -100,6 +117,13 @@ export class SecretsManager {
100
117
  if (!fs.existsSync(envFilePath)) {
101
118
  throw new Error(`File not found: ${envFilePath}`);
102
119
  }
120
+ // Warn if using default key
121
+ if (!process.env.LSH_SECRETS_KEY) {
122
+ logger.warn('⚠️ Warning: No LSH_SECRETS_KEY set. Using machine-specific key.');
123
+ logger.warn(' To share secrets across machines, generate a key with: lsh secrets key');
124
+ logger.warn(' Then add LSH_SECRETS_KEY=<key> to your .env on all machines');
125
+ console.log();
126
+ }
103
127
  logger.info(`Pushing ${envFilePath} to Supabase (${environment})...`);
104
128
  const content = fs.readFileSync(envFilePath, 'utf8');
105
129
  const env = this.parseEnvFile(content);
@@ -121,7 +145,7 @@ export class SecretsManager {
121
145
  /**
122
146
  * Pull .env from Supabase
123
147
  */
124
- async pull(envFilePath = '.env', environment = 'dev') {
148
+ async pull(envFilePath = '.env', environment = 'dev', force = false) {
125
149
  logger.info(`Pulling ${environment} secrets from Supabase...`);
126
150
  // Get latest secrets
127
151
  const jobs = await this.persistence.getActiveJobs();
@@ -136,8 +160,8 @@ export class SecretsManager {
136
160
  throw new Error(`No encrypted data found for environment: ${environment}`);
137
161
  }
138
162
  const decrypted = this.decrypt(latestSecret.output);
139
- // Backup existing .env if it exists
140
- if (fs.existsSync(envFilePath)) {
163
+ // Backup existing .env if it exists (unless force is true)
164
+ if (fs.existsSync(envFilePath) && !force) {
141
165
  const backup = `${envFilePath}.backup.${Date.now()}`;
142
166
  fs.copyFileSync(envFilePath, backup);
143
167
  logger.info(`Backed up existing .env to ${backup}`);
@@ -189,5 +213,131 @@ export class SecretsManager {
189
213
  }
190
214
  console.log();
191
215
  }
216
+ /**
217
+ * Get status of secrets for an environment
218
+ */
219
+ async status(envFilePath = '.env', environment = 'dev') {
220
+ const status = {
221
+ localExists: false,
222
+ localKeys: 0,
223
+ localModified: undefined,
224
+ cloudExists: false,
225
+ cloudKeys: 0,
226
+ cloudModified: undefined,
227
+ keySet: !!process.env.LSH_SECRETS_KEY,
228
+ keyMatches: undefined,
229
+ suggestions: [],
230
+ };
231
+ // Check local file
232
+ if (fs.existsSync(envFilePath)) {
233
+ status.localExists = true;
234
+ const stat = fs.statSync(envFilePath);
235
+ status.localModified = stat.mtime;
236
+ const content = fs.readFileSync(envFilePath, 'utf8');
237
+ const env = this.parseEnvFile(content);
238
+ status.localKeys = Object.keys(env).length;
239
+ }
240
+ // Check cloud storage
241
+ try {
242
+ const jobs = await this.persistence.getActiveJobs();
243
+ const secretsJobs = jobs
244
+ .filter(j => j.command === 'secrets_sync' && j.job_id.includes(environment))
245
+ .sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime());
246
+ if (secretsJobs.length > 0) {
247
+ status.cloudExists = true;
248
+ const latestSecret = secretsJobs[0];
249
+ status.cloudModified = new Date(latestSecret.completed_at || latestSecret.started_at);
250
+ // Try to decrypt to check if key matches
251
+ if (latestSecret.output) {
252
+ try {
253
+ const decrypted = this.decrypt(latestSecret.output);
254
+ const env = this.parseEnvFile(decrypted);
255
+ status.cloudKeys = Object.keys(env).length;
256
+ status.keyMatches = true;
257
+ }
258
+ catch (error) {
259
+ status.keyMatches = false;
260
+ }
261
+ }
262
+ }
263
+ }
264
+ catch (error) {
265
+ // Cloud check failed, likely no connection
266
+ }
267
+ return status;
268
+ }
269
+ /**
270
+ * Sync command - check status and suggest actions
271
+ */
272
+ async sync(envFilePath = '.env', environment = 'dev') {
273
+ console.log(`\n🔍 Checking secrets status for environment: ${environment}\n`);
274
+ const status = await this.status(envFilePath, environment);
275
+ // Display status
276
+ console.log('📊 Status:');
277
+ console.log(` Encryption key set: ${status.keySet ? '✅' : '❌'}`);
278
+ console.log(` Local .env file: ${status.localExists ? `✅ (${status.localKeys} keys)` : '❌'}`);
279
+ console.log(` Cloud storage: ${status.cloudExists ? `✅ (${status.cloudKeys} keys)` : '❌'}`);
280
+ if (status.cloudExists && status.keyMatches !== undefined) {
281
+ console.log(` Key matches cloud: ${status.keyMatches ? '✅' : '❌'}`);
282
+ }
283
+ console.log();
284
+ // Generate suggestions
285
+ const suggestions = [];
286
+ if (!status.keySet) {
287
+ suggestions.push('⚠️ No encryption key set!');
288
+ suggestions.push(' Generate a key: lsh lib secrets key');
289
+ suggestions.push(' Add it to .env: LSH_SECRETS_KEY=<your-key>');
290
+ suggestions.push(' Load it: export $(cat .env | xargs)');
291
+ }
292
+ if (status.cloudExists && status.keyMatches === false) {
293
+ suggestions.push('⚠️ Encryption key does not match cloud storage!');
294
+ suggestions.push(' Either use the original key, or push new secrets:');
295
+ suggestions.push(` lsh lib secrets push -f ${envFilePath} -e ${environment}`);
296
+ }
297
+ if (!status.localExists && status.cloudExists && status.keyMatches) {
298
+ suggestions.push('💡 Cloud secrets available but no local file');
299
+ suggestions.push(` Pull from cloud: lsh lib secrets pull -f ${envFilePath} -e ${environment}`);
300
+ }
301
+ if (status.localExists && !status.cloudExists) {
302
+ suggestions.push('💡 Local .env exists but not in cloud');
303
+ suggestions.push(` Push to cloud: lsh lib secrets push -f ${envFilePath} -e ${environment}`);
304
+ }
305
+ if (status.localExists && status.cloudExists && status.keyMatches) {
306
+ if (status.localModified && status.cloudModified) {
307
+ const localNewer = status.localModified > status.cloudModified;
308
+ const timeDiff = Math.abs(status.localModified.getTime() - status.cloudModified.getTime());
309
+ const daysDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
310
+ if (localNewer && daysDiff > 0) {
311
+ suggestions.push('💡 Local file is newer than cloud');
312
+ suggestions.push(` Push to cloud: lsh lib secrets push -f ${envFilePath} -e ${environment}`);
313
+ }
314
+ else if (!localNewer && daysDiff > 0) {
315
+ suggestions.push('💡 Cloud is newer than local file');
316
+ suggestions.push(` Pull from cloud: lsh lib secrets pull -f ${envFilePath} -e ${environment}`);
317
+ }
318
+ else {
319
+ suggestions.push('✅ Local and cloud are in sync!');
320
+ }
321
+ }
322
+ }
323
+ // Show how to load secrets in current shell
324
+ if (status.localExists && status.keySet) {
325
+ suggestions.push('');
326
+ suggestions.push('📝 To load secrets in your current shell:');
327
+ suggestions.push(` export $(cat ${envFilePath} | grep -v '^#' | xargs)`);
328
+ suggestions.push('');
329
+ suggestions.push(' Or for safer loading (with quotes):');
330
+ suggestions.push(` set -a; source ${envFilePath}; set +a`);
331
+ suggestions.push('');
332
+ suggestions.push('💡 Add to your shell profile for auto-loading:');
333
+ suggestions.push(` echo "set -a; source ${path.resolve(envFilePath)}; set +a" >> ~/.zshrc`);
334
+ }
335
+ // Display suggestions
336
+ if (suggestions.length > 0) {
337
+ console.log('📋 Recommendations:\n');
338
+ suggestions.forEach(s => console.log(s));
339
+ console.log();
340
+ }
341
+ }
192
342
  }
193
343
  export default SecretsManager;
@@ -66,7 +66,9 @@ async function _makeCommand(commander) {
66
66
  // // });
67
67
  // }
68
68
  export async function init_lib(program) {
69
- const lib = program.command("lib");
69
+ const lib = program
70
+ .command("lib")
71
+ .description("LSH library and service commands");
70
72
  // Load and register dynamic commands
71
73
  const commands = await loadCommands();
72
74
  for (const commandName of Object.keys(commands)) {
@@ -3,6 +3,9 @@
3
3
  * Sync .env files across development environments
4
4
  */
5
5
  import SecretsManager from '../../lib/secrets-manager.js';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as readline from 'readline';
6
9
  export async function init_secrets(program) {
7
10
  const secretsCmd = program
8
11
  .command('secrets')
@@ -29,10 +32,11 @@ export async function init_secrets(program) {
29
32
  .description('Pull .env from encrypted cloud storage')
30
33
  .option('-f, --file <path>', 'Path to .env file', '.env')
31
34
  .option('-e, --env <name>', 'Environment name (dev/staging/prod)', 'dev')
35
+ .option('--force', 'Overwrite without creating backup')
32
36
  .action(async (options) => {
33
37
  try {
34
38
  const manager = new SecretsManager();
35
- await manager.pull(options.file, options.env);
39
+ await manager.pull(options.file, options.env, options.force);
36
40
  }
37
41
  catch (error) {
38
42
  console.error('❌ Failed to pull secrets:', error.message);
@@ -41,12 +45,18 @@ export async function init_secrets(program) {
41
45
  });
42
46
  // List environments
43
47
  secretsCmd
44
- .command('list')
48
+ .command('list [environment]')
45
49
  .alias('ls')
46
- .description('List all stored environments')
47
- .action(async () => {
50
+ .description('List all stored environments or show secrets for specific environment')
51
+ .action(async (environment) => {
48
52
  try {
49
53
  const manager = new SecretsManager();
54
+ // If environment specified, show secrets for that environment
55
+ if (environment) {
56
+ await manager.show(environment);
57
+ return;
58
+ }
59
+ // Otherwise, list all environments
50
60
  const envs = await manager.listEnvironments();
51
61
  if (envs.length === 0) {
52
62
  console.log('No environments found. Push your first .env with: lsh secrets push');
@@ -90,5 +100,146 @@ export async function init_secrets(program) {
90
100
  console.log('💡 Tip: Share this key securely with your team to sync secrets.');
91
101
  console.log(' Never commit it to git!\n');
92
102
  });
103
+ // Create .env file
104
+ secretsCmd
105
+ .command('create')
106
+ .description('Create a new .env file')
107
+ .option('-f, --file <path>', 'Path to .env file', '.env')
108
+ .option('-t, --template', 'Create with common template variables')
109
+ .action(async (options) => {
110
+ try {
111
+ const envPath = path.resolve(options.file);
112
+ // Check if file already exists
113
+ if (fs.existsSync(envPath)) {
114
+ console.log(`❌ File already exists: ${envPath}`);
115
+ console.log('💡 Use a different path or delete the existing file first.');
116
+ process.exit(1);
117
+ }
118
+ // Create template content if requested
119
+ let content = '';
120
+ if (options.template) {
121
+ content = `# Environment Configuration
122
+ # Generated by LSH Secrets Manager
123
+
124
+ # Application
125
+ NODE_ENV=development
126
+ PORT=3000
127
+
128
+ # Database
129
+ DATABASE_URL=
130
+
131
+ # API Keys
132
+ API_KEY=
133
+
134
+ # LSH Secrets (for cross-machine sync)
135
+ # LSH_SECRETS_KEY=
136
+
137
+ # Add your environment variables below
138
+ `;
139
+ }
140
+ // Create the file
141
+ fs.writeFileSync(envPath, content, 'utf8');
142
+ console.log(`✅ Created .env file: ${envPath}`);
143
+ if (options.template) {
144
+ console.log('📝 Template variables added - update with your values');
145
+ }
146
+ console.log('');
147
+ console.log('Next steps:');
148
+ console.log(` 1. Edit the file: ${options.file}`);
149
+ console.log(` 2. Push to cloud: lsh lib secrets push -f ${options.file}`);
150
+ console.log('');
151
+ }
152
+ catch (error) {
153
+ console.error('❌ Failed to create .env file:', error.message);
154
+ process.exit(1);
155
+ }
156
+ });
157
+ // Sync command - check status and suggest actions
158
+ secretsCmd
159
+ .command('sync')
160
+ .description('Check secrets sync status and show recommended actions')
161
+ .option('-f, --file <path>', 'Path to .env file', '.env')
162
+ .option('-e, --env <name>', 'Environment name', 'dev')
163
+ .action(async (options) => {
164
+ try {
165
+ const manager = new SecretsManager();
166
+ await manager.sync(options.file, options.env);
167
+ }
168
+ catch (error) {
169
+ console.error('❌ Failed to check sync status:', error.message);
170
+ process.exit(1);
171
+ }
172
+ });
173
+ // Status command - get detailed status info
174
+ secretsCmd
175
+ .command('status')
176
+ .description('Get detailed secrets status (JSON output)')
177
+ .option('-f, --file <path>', 'Path to .env file', '.env')
178
+ .option('-e, --env <name>', 'Environment name', 'dev')
179
+ .action(async (options) => {
180
+ try {
181
+ const manager = new SecretsManager();
182
+ const status = await manager.status(options.file, options.env);
183
+ console.log(JSON.stringify(status, null, 2));
184
+ }
185
+ catch (error) {
186
+ console.error('❌ Failed to get status:', error.message);
187
+ process.exit(1);
188
+ }
189
+ });
190
+ // Delete .env file with confirmation
191
+ secretsCmd
192
+ .command('delete')
193
+ .description('Delete .env file (requires confirmation)')
194
+ .option('-f, --file <path>', 'Path to .env file', '.env')
195
+ .option('-y, --yes', 'Skip confirmation prompt')
196
+ .action(async (options) => {
197
+ try {
198
+ const envPath = path.resolve(options.file);
199
+ // Check if file exists
200
+ if (!fs.existsSync(envPath)) {
201
+ console.log(`❌ File not found: ${envPath}`);
202
+ process.exit(1);
203
+ }
204
+ console.log('⚠️ WARNING: You are about to delete a .env file!');
205
+ console.log('');
206
+ console.log(`File: ${envPath}`);
207
+ console.log('');
208
+ // Skip confirmation if --yes flag is provided
209
+ if (!options.yes) {
210
+ console.log('To confirm deletion, please type the full path of the file:');
211
+ console.log(`Expected: ${envPath}`);
212
+ console.log('');
213
+ const rl = readline.createInterface({
214
+ input: process.stdin,
215
+ output: process.stdout,
216
+ });
217
+ const answer = await new Promise((resolve) => {
218
+ rl.question('Enter path to confirm: ', (ans) => {
219
+ rl.close();
220
+ resolve(ans.trim());
221
+ });
222
+ });
223
+ if (answer !== envPath) {
224
+ console.log('');
225
+ console.log('❌ Confirmation failed - path does not match');
226
+ console.log('Deletion cancelled');
227
+ process.exit(1);
228
+ }
229
+ }
230
+ // Delete the file
231
+ fs.unlinkSync(envPath);
232
+ console.log('');
233
+ console.log(`✅ Deleted: ${envPath}`);
234
+ console.log('');
235
+ console.log('💡 Tip: You can still pull from cloud if you pushed previously:');
236
+ console.log(` lsh lib secrets pull -f ${options.file}`);
237
+ console.log('');
238
+ }
239
+ catch (error) {
240
+ console.error('❌ Failed to delete .env file:', error.message);
241
+ process.exit(1);
242
+ }
243
+ });
93
244
  }
94
245
  export default init_secrets;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "0.6.0",
4
- "description": "A powerful, extensible shell with advanced job management, database persistence, and modern CLI features",
3
+ "version": "0.8.0",
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": {
7
7
  "lsh": "./dist/cli.js"
@@ -36,21 +36,25 @@
36
36
  "audit:security": "npm audit --audit-level moderate"
37
37
  },
38
38
  "keywords": [
39
- "cli",
39
+ "secrets-manager",
40
+ "secrets",
41
+ "env-manager",
42
+ "dotenv",
43
+ "environment-variables",
44
+ "encryption",
45
+ "credential-management",
46
+ "team-sync",
47
+ "secrets-rotation",
48
+ "multi-environment",
49
+ "devops",
50
+ "security",
40
51
  "shell",
41
- "terminal",
42
- "job-manager",
52
+ "automation",
43
53
  "cron",
44
54
  "daemon",
45
- "zsh",
46
- "bash",
47
- "posix",
48
- "database-persistence",
49
- "task-scheduler",
50
- "command-line",
51
- "automation",
52
- "devops",
53
- "cicd"
55
+ "job-scheduler",
56
+ "cicd",
57
+ "cli"
54
58
  ],
55
59
  "engines": {
56
60
  "node": ">=20.18.0",
@@ -100,15 +104,10 @@
100
104
  "ink-text-input": "^5.0.1",
101
105
  "inquirer": "^9.2.12",
102
106
  "ioredis": "^5.8.0",
103
- "jest": "^29.7.0",
104
107
  "jsonwebtoken": "^9.0.2",
105
108
  "lodash": "^4.17.21",
106
- "mocha": "^10.3.0",
107
- "ncc": "^0.3.6",
108
- "nexe": "^4.0.0-rc.2",
109
109
  "node-cron": "^3.0.3",
110
110
  "node-fetch": "^3.3.2",
111
- "nodemon": "^3.0.1",
112
111
  "ora": "^8.0.1",
113
112
  "path": "^0.12.7",
114
113
  "pg": "^8.16.3",
@@ -118,7 +117,6 @@
118
117
  "socket.io": "^4.8.1",
119
118
  "uuid": "^10.0.0",
120
119
  "xstate": "^5.9.1",
121
- "zapier-platform-core": "15.4.1",
122
120
  "zx": "^7.2.3"
123
121
  },
124
122
  "devDependencies": {
@@ -137,8 +135,14 @@
137
135
  "eslint": "^9.36.0",
138
136
  "eslint-plugin-react": "^7.37.5",
139
137
  "eslint-plugin-react-hooks": "^5.2.0",
138
+ "jest": "^29.7.0",
139
+ "mocha": "^10.3.0",
140
+ "ncc": "^0.3.6",
141
+ "nexe": "^4.0.0-rc.2",
142
+ "nodemon": "^3.0.1",
140
143
  "supertest": "^7.1.4",
141
- "ts-jest": "^29.4.4",
142
- "typescript": "^5.4.5"
144
+ "ts-jest": "^29.2.5",
145
+ "typescript": "^5.4.5",
146
+ "zapier-platform-core": "15.4.1"
143
147
  }
144
148
  }