lsh-framework 0.8.0 → 0.8.1

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/README.md CHANGED
@@ -126,7 +126,7 @@ lsh lib secrets pull --env prod # for production debugging
126
126
  | `lsh lib secrets create` | Create new .env file |
127
127
  | `lsh lib secrets delete` | Delete .env file (with confirmation) |
128
128
 
129
- See the complete guide: [SECRETS_GUIDE.md](SECRETS_GUIDE.md)
129
+ See the complete guide: [SECRETS_GUIDE.md](docs/features/secrets/SECRETS_GUIDE.md)
130
130
 
131
131
  ## Installation
132
132
 
@@ -536,10 +536,10 @@ lsh lib daemon start
536
536
 
537
537
  ## Documentation
538
538
 
539
- - **[SECRETS_GUIDE.md](SECRETS_GUIDE.md)** - Complete secrets management guide
540
- - **[SECRETS_QUICK_REFERENCE.md](SECRETS_QUICK_REFERENCE.md)** - Quick reference for daily use
539
+ - **[SECRETS_GUIDE.md](docs/features/secrets/SECRETS_GUIDE.md)** - Complete secrets management guide
540
+ - **[SECRETS_QUICK_REFERENCE.md](docs/features/secrets/SECRETS_QUICK_REFERENCE.md)** - Quick reference for daily use
541
541
  - **[SECRETS_CHEATSHEET.txt](SECRETS_CHEATSHEET.txt)** - Command cheatsheet
542
- - **[INSTALL.md](INSTALL.md)** - Detailed installation instructions
542
+ - **[INSTALL.md](docs/deployment/INSTALL.md)** - Detailed installation instructions
543
543
  - **[CLAUDE.md](CLAUDE.md)** - Developer guide for contributors
544
544
 
545
545
  ## Architecture
@@ -117,6 +117,11 @@ export class SecretsManager {
117
117
  if (!fs.existsSync(envFilePath)) {
118
118
  throw new Error(`File not found: ${envFilePath}`);
119
119
  }
120
+ // Validate filename pattern for custom files
121
+ const filename = path.basename(envFilePath);
122
+ if (filename !== '.env' && !filename.startsWith('.env.')) {
123
+ throw new Error(`Invalid filename: ${filename}. Must be '.env' or start with '.env.'`);
124
+ }
120
125
  // Warn if using default key
121
126
  if (!process.env.LSH_SECRETS_KEY) {
122
127
  logger.warn('⚠️ Warning: No LSH_SECRETS_KEY set. Using machine-specific key.');
@@ -129,9 +134,10 @@ export class SecretsManager {
129
134
  const env = this.parseEnvFile(content);
130
135
  // Encrypt entire .env content
131
136
  const encrypted = this.encrypt(content);
132
- // Store in Supabase (using job system for now)
137
+ // Include filename in job_id for tracking multiple .env files
138
+ const safeFilename = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
133
139
  const secretData = {
134
- job_id: `secrets_${environment}_${Date.now()}`,
140
+ job_id: `secrets_${environment}_${safeFilename}_${Date.now()}`,
135
141
  command: 'secrets_sync',
136
142
  status: 'completed',
137
143
  output: encrypted,
@@ -140,20 +146,31 @@ export class SecretsManager {
140
146
  working_directory: process.cwd(),
141
147
  };
142
148
  await this.persistence.saveJob(secretData);
143
- logger.info(`✅ Pushed ${Object.keys(env).length} secrets to Supabase`);
149
+ logger.info(`✅ Pushed ${Object.keys(env).length} secrets from ${filename} to Supabase`);
144
150
  }
145
151
  /**
146
152
  * Pull .env from Supabase
147
153
  */
148
154
  async pull(envFilePath = '.env', environment = 'dev', force = false) {
149
- logger.info(`Pulling ${environment} secrets from Supabase...`);
150
- // Get latest secrets
155
+ // Validate filename pattern for custom files
156
+ const filename = path.basename(envFilePath);
157
+ if (filename !== '.env' && !filename.startsWith('.env.')) {
158
+ throw new Error(`Invalid filename: ${filename}. Must be '.env' or start with '.env.'`);
159
+ }
160
+ logger.info(`Pulling ${filename} (${environment}) from Supabase...`);
161
+ // Get latest secrets for this specific file
151
162
  const jobs = await this.persistence.getActiveJobs();
163
+ const safeFilename = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
152
164
  const secretsJobs = jobs
153
- .filter(j => j.command === 'secrets_sync' && j.job_id.includes(environment))
165
+ .filter(j => {
166
+ // Match secrets for this environment and filename
167
+ return j.command === 'secrets_sync' &&
168
+ j.job_id.includes(environment) &&
169
+ j.job_id.includes(safeFilename);
170
+ })
154
171
  .sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime());
155
172
  if (secretsJobs.length === 0) {
156
- throw new Error(`No secrets found for environment: ${environment}`);
173
+ throw new Error(`No secrets found for file '${filename}' in environment: ${environment}`);
157
174
  }
158
175
  const latestSecret = secretsJobs[0];
159
176
  if (!latestSecret.output) {
@@ -179,13 +196,57 @@ export class SecretsManager {
179
196
  const secretsJobs = jobs.filter(j => j.command === 'secrets_sync');
180
197
  const envs = new Set();
181
198
  for (const job of secretsJobs) {
182
- const match = job.job_id.match(/secrets_(.+?)_\d+/);
199
+ // Updated regex to handle new format with filename
200
+ const match = job.job_id.match(/secrets_([^_]+)_/);
183
201
  if (match) {
184
202
  envs.add(match[1]);
185
203
  }
186
204
  }
187
205
  return Array.from(envs).sort();
188
206
  }
207
+ /**
208
+ * List all tracked .env files
209
+ */
210
+ async listAllFiles() {
211
+ const jobs = await this.persistence.getActiveJobs();
212
+ const secretsJobs = jobs.filter(j => j.command === 'secrets_sync');
213
+ // Group by environment and filename to get latest of each
214
+ const fileMap = new Map();
215
+ for (const job of secretsJobs) {
216
+ // Parse job_id: secrets_${environment}_${safeFilename}_${timestamp}
217
+ const parts = job.job_id.split('_');
218
+ if (parts.length >= 3 && parts[0] === 'secrets') {
219
+ const environment = parts[1];
220
+ // Handle both old and new format
221
+ let filename = '.env';
222
+ if (parts.length >= 4) {
223
+ // New format with filename
224
+ const timestamp = parts[parts.length - 1];
225
+ // Reconstruct filename from middle parts
226
+ const filenameParts = parts.slice(2, -1);
227
+ if (filenameParts.length > 0) {
228
+ // Convert underscores back to dots for the extension
229
+ filename = filenameParts.join('_');
230
+ // Fix the extension dots that were replaced
231
+ filename = filename.replace(/^env_/, '.env.');
232
+ if (filename === 'env') {
233
+ filename = '.env';
234
+ }
235
+ }
236
+ }
237
+ const key = `${environment}_${filename}`;
238
+ const existing = fileMap.get(key);
239
+ if (!existing || new Date(job.completed_at || job.started_at) > new Date(existing.updated)) {
240
+ fileMap.set(key, {
241
+ filename,
242
+ environment,
243
+ updated: new Date(job.completed_at || job.started_at).toLocaleString()
244
+ });
245
+ }
246
+ }
247
+ }
248
+ return Array.from(fileMap.values()).sort((a, b) => a.filename.localeCompare(b.filename) || a.environment.localeCompare(b.environment));
249
+ }
189
250
  /**
190
251
  * Show secrets (masked)
191
252
  */
@@ -48,9 +48,24 @@ export async function init_secrets(program) {
48
48
  .command('list [environment]')
49
49
  .alias('ls')
50
50
  .description('List all stored environments or show secrets for specific environment')
51
- .action(async (environment) => {
51
+ .option('--all-files', 'List all tracked .env files across environments')
52
+ .action(async (environment, options) => {
52
53
  try {
53
54
  const manager = new SecretsManager();
55
+ // If --all-files flag is set, list all tracked files
56
+ if (options.allFiles) {
57
+ const files = await manager.listAllFiles();
58
+ if (files.length === 0) {
59
+ console.log('No .env files found. Push your first file with: lsh secrets push --file <filename>');
60
+ return;
61
+ }
62
+ console.log('\n📦 Tracked .env files:\n');
63
+ for (const file of files) {
64
+ console.log(` • ${file.filename} (${file.environment}) - Last updated: ${file.updated}`);
65
+ }
66
+ console.log();
67
+ return;
68
+ }
54
69
  // If environment specified, show secrets for that environment
55
70
  if (environment) {
56
71
  await manager.show(environment);
@@ -187,6 +202,95 @@ API_KEY=
187
202
  process.exit(1);
188
203
  }
189
204
  });
205
+ // Get a specific secret value
206
+ secretsCmd
207
+ .command('get <key>')
208
+ .description('Get a specific secret value from .env file')
209
+ .option('-f, --file <path>', 'Path to .env file', '.env')
210
+ .action(async (key, options) => {
211
+ try {
212
+ const envPath = path.resolve(options.file);
213
+ if (!fs.existsSync(envPath)) {
214
+ console.error(`❌ File not found: ${envPath}`);
215
+ process.exit(1);
216
+ }
217
+ const content = fs.readFileSync(envPath, 'utf8');
218
+ const lines = content.split('\n');
219
+ for (const line of lines) {
220
+ if (line.trim().startsWith('#') || !line.trim())
221
+ continue;
222
+ const match = line.match(/^([^=]+)=(.*)$/);
223
+ if (match && match[1].trim() === key) {
224
+ let value = match[2].trim();
225
+ // Remove quotes if present
226
+ if ((value.startsWith('"') && value.endsWith('"')) ||
227
+ (value.startsWith("'") && value.endsWith("'"))) {
228
+ value = value.slice(1, -1);
229
+ }
230
+ console.log(value);
231
+ return;
232
+ }
233
+ }
234
+ console.error(`❌ Key '${key}' not found in ${options.file}`);
235
+ process.exit(1);
236
+ }
237
+ catch (error) {
238
+ console.error('❌ Failed to get secret:', error.message);
239
+ process.exit(1);
240
+ }
241
+ });
242
+ // Set a specific secret value
243
+ secretsCmd
244
+ .command('set <key> <value>')
245
+ .description('Set a specific secret value in .env file')
246
+ .option('-f, --file <path>', 'Path to .env file', '.env')
247
+ .action(async (key, value, options) => {
248
+ try {
249
+ const envPath = path.resolve(options.file);
250
+ // Validate key format
251
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
252
+ console.error(`❌ Invalid key format: ${key}. Must be a valid environment variable name.`);
253
+ process.exit(1);
254
+ }
255
+ let content = '';
256
+ let found = false;
257
+ if (fs.existsSync(envPath)) {
258
+ content = fs.readFileSync(envPath, 'utf8');
259
+ const lines = content.split('\n');
260
+ const newLines = [];
261
+ for (const line of lines) {
262
+ if (line.trim().startsWith('#') || !line.trim()) {
263
+ newLines.push(line);
264
+ continue;
265
+ }
266
+ const match = line.match(/^([^=]+)=(.*)$/);
267
+ if (match && match[1].trim() === key) {
268
+ // Quote values with spaces or special characters
269
+ const needsQuotes = /[\s#]/.test(value);
270
+ const quotedValue = needsQuotes ? `"${value}"` : value;
271
+ newLines.push(`${key}=${quotedValue}`);
272
+ found = true;
273
+ }
274
+ else {
275
+ newLines.push(line);
276
+ }
277
+ }
278
+ content = newLines.join('\n');
279
+ }
280
+ // If key wasn't found, append it
281
+ if (!found) {
282
+ const needsQuotes = /[\s#]/.test(value);
283
+ const quotedValue = needsQuotes ? `"${value}"` : value;
284
+ content = content.trimRight() + `\n${key}=${quotedValue}\n`;
285
+ }
286
+ fs.writeFileSync(envPath, content, 'utf8');
287
+ console.log(`✅ Set ${key} in ${options.file}`);
288
+ }
289
+ catch (error) {
290
+ console.error('❌ Failed to set secret:', error.message);
291
+ process.exit(1);
292
+ }
293
+ });
190
294
  // Delete .env file with confirmation
191
295
  secretsCmd
192
296
  .command('delete')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
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": {
@@ -1,58 +0,0 @@
1
- import AsyncLock from 'async-lock';
2
- import request from 'request';
3
- import { CONFIG } from './config.js';
4
- import { FILE } from './file.js';
5
- const semaphore = new AsyncLock();
6
- let pkgId;
7
- export const makePOSTRequest = async (typeName, method, data, onSuccess) => {
8
- console.log("makePostRequest");
9
- const url = CONFIG.URL + '/api/8' + '/' + typeName + '/' + method;
10
- console.log(url);
11
- // Prevent parallel writes/deletions
12
- return semaphore.acquire('request', (done) => {
13
- return request.post(url, {
14
- method: 'POST',
15
- body: data,
16
- json: true,
17
- headers: {
18
- Authorization: CONFIG.AUTH_TOKEN,
19
- },
20
- }, (err, response, body) => {
21
- console.log(body);
22
- onSuccess?.(response);
23
- done();
24
- });
25
- });
26
- };
27
- const getMetadataPath = (path) => {
28
- console.log("getMetadataPath");
29
- return path.substring(path.indexOf(CONFIG.PATH_TO_PACKAGE_REPO) + CONFIG.PATH_TO_PACKAGE_REPO.length);
30
- };
31
- const getPkgId = async () => {
32
- console.log("getPkgId");
33
- if (pkgId) {
34
- return pkgId;
35
- }
36
- await makePOSTRequest('Pkg', 'inst', ['Pkg'], (body) => {
37
- pkgId = body;
38
- });
39
- return pkgId;
40
- };
41
- const _writeContent = async (path) => {
42
- console.log("writeContent");
43
- const pkgId = await getPkgId();
44
- const metadataPath = getMetadataPath(path);
45
- const content = FILE.encodeContent(path);
46
- if (await content === FILE.NO_CHANGE_TO_FILE) {
47
- return;
48
- }
49
- return makePOSTRequest('Pkg', 'writeContent', [pkgId, metadataPath, {
50
- type: 'ContentValue',
51
- content,
52
- }], () => console.log("Success"));
53
- };
54
- const _deleteContent = async (path) => {
55
- const pkgId = await getPkgId();
56
- const metadataPath = getMetadataPath(path);
57
- return makePOSTRequest('Pkg', 'deleteContent', [pkgId, metadataPath, true], () => console.log("deleted!"));
58
- };