lsh-framework 3.1.7 → 3.1.9

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
  * Stores encrypted secrets on IPFS using Storacha (formerly web3.storage)
4
4
  */
5
5
  import * as fs from 'fs';
6
+ import * as fsPromises from 'fs/promises';
6
7
  import * as path from 'path';
7
8
  import * as os from 'os';
8
9
  import * as crypto from 'crypto';
@@ -30,12 +31,19 @@ export class IPFSSecretsStorage {
30
31
  const lshDir = path.join(homeDir, '.lsh');
31
32
  this.cacheDir = path.join(lshDir, 'secrets-cache');
32
33
  this.metadataPath = path.join(lshDir, 'secrets-metadata.json');
34
+ // Initialize metadata - will be loaded on first use
35
+ this.metadata = {};
36
+ }
37
+ /**
38
+ * Initialize async parts
39
+ */
40
+ async initialize() {
33
41
  // Ensure directories exist
34
42
  if (!fs.existsSync(this.cacheDir)) {
35
43
  fs.mkdirSync(this.cacheDir, { recursive: true });
36
44
  }
37
45
  // Load metadata
38
- this.metadata = this.loadMetadata();
46
+ this.metadata = await this.loadMetadataAsync();
39
47
  }
40
48
  /**
41
49
  * Store secrets on IPFS
@@ -59,7 +67,7 @@ export class IPFSSecretsStorage {
59
67
  encrypted: true,
60
68
  };
61
69
  this.metadata[this.getMetadataKey(gitRepo, environment)] = metadata;
62
- this.saveMetadata();
70
+ await this.saveMetadata();
63
71
  logger.info(`📦 Stored ${secrets.length} secrets on IPFS: ${cid}`);
64
72
  logger.info(` Environment: ${environment}`);
65
73
  if (gitRepo) {
@@ -132,7 +140,7 @@ export class IPFSSecretsStorage {
132
140
  encrypted: true,
133
141
  };
134
142
  this.metadata[metadataKey] = metadata;
135
- this.saveMetadata();
143
+ await this.saveMetadata();
136
144
  }
137
145
  }
138
146
  }
@@ -162,7 +170,7 @@ export class IPFSSecretsStorage {
162
170
  timestamp: new Date().toISOString(),
163
171
  };
164
172
  this.metadata[metadataKey] = metadata;
165
- this.saveMetadata();
173
+ await this.saveMetadata();
166
174
  }
167
175
  }
168
176
  }
@@ -236,20 +244,24 @@ export class IPFSSecretsStorage {
236
244
  return Object.values(this.metadata);
237
245
  }
238
246
  /**
239
- * Delete secrets for environment
247
+ * Delete local cached secrets for an environment
240
248
  */
241
- async delete(environment, gitRepo) {
249
+ async deleteLocal(environment, gitRepo) {
242
250
  const metadataKey = this.getMetadataKey(gitRepo, environment);
243
251
  const metadata = this.metadata[metadataKey];
244
252
  if (metadata) {
245
253
  // Delete local cache
246
254
  const cachePath = path.join(this.cacheDir, `${metadata.cid}.encrypted`);
247
- if (fs.existsSync(cachePath)) {
248
- fs.unlinkSync(cachePath);
255
+ try {
256
+ await fsPromises.access(cachePath);
257
+ await fsPromises.unlink(cachePath);
258
+ }
259
+ catch {
260
+ // File doesn't exist, which is fine
249
261
  }
250
262
  // Remove metadata
251
263
  delete this.metadata[metadataKey];
252
- this.saveMetadata();
264
+ await this.saveMetadata();
253
265
  logger.info(`🗑️ Deleted secrets for ${environment}`);
254
266
  }
255
267
  }
@@ -310,7 +322,10 @@ export class IPFSSecretsStorage {
310
322
  */
311
323
  async storeLocally(cid, encryptedData, _environment) {
312
324
  const cachePath = path.join(this.cacheDir, `${cid}.encrypted`);
313
- fs.writeFileSync(cachePath, encryptedData, 'utf8');
325
+ // Ensure parent directory exists
326
+ await fsPromises.mkdir(this.cacheDir, { recursive: true });
327
+ // Write file without locking (simpler approach)
328
+ await fsPromises.writeFile(cachePath, encryptedData, 'utf8');
314
329
  logger.debug(`Cached secrets locally: ${cachePath}`);
315
330
  }
316
331
  /**
@@ -318,10 +333,14 @@ export class IPFSSecretsStorage {
318
333
  */
319
334
  async loadLocally(cid) {
320
335
  const cachePath = path.join(this.cacheDir, `${cid}.encrypted`);
321
- if (!fs.existsSync(cachePath)) {
336
+ try {
337
+ await fsPromises.access(cachePath);
338
+ }
339
+ catch {
322
340
  return null;
323
341
  }
324
- return fs.readFileSync(cachePath, 'utf8');
342
+ // Simple read without locking for now
343
+ return await fsPromises.readFile(cachePath, 'utf8');
325
344
  }
326
345
  /**
327
346
  * Get metadata key for environment
@@ -344,10 +363,31 @@ export class IPFSSecretsStorage {
344
363
  return {};
345
364
  }
346
365
  }
366
+ /**
367
+ * Load metadata from disk asynchronously
368
+ */
369
+ async loadMetadataAsync() {
370
+ try {
371
+ await fsPromises.access(this.metadataPath);
372
+ }
373
+ catch {
374
+ return {};
375
+ }
376
+ try {
377
+ const content = await fsPromises.readFile(this.metadataPath, 'utf8');
378
+ return JSON.parse(content);
379
+ }
380
+ catch {
381
+ return {};
382
+ }
383
+ }
347
384
  /**
348
385
  * Save metadata to disk
349
386
  */
350
- saveMetadata() {
351
- fs.writeFileSync(this.metadataPath, JSON.stringify(this.metadata, null, 2), 'utf8');
387
+ async saveMetadata() {
388
+ // Ensure parent directory exists
389
+ const parentDir = path.dirname(this.metadataPath);
390
+ await fsPromises.mkdir(parentDir, { recursive: true });
391
+ await fsPromises.writeFile(this.metadataPath, JSON.stringify(this.metadata, null, 2), 'utf8');
352
392
  }
353
393
  }
@@ -313,6 +313,72 @@ API_KEY=
313
313
  await manager.cleanup();
314
314
  }
315
315
  });
316
+ // Load command - sync and output export commands for shell evaluation
317
+ program
318
+ .command('load')
319
+ .description('Sync secrets and output export commands (use with eval)')
320
+ .option('-f, --file <path>', 'Path to .env file', '.env')
321
+ .option('-e, --env <name>', 'Environment name', 'dev')
322
+ .option('-g, --global', 'Use global workspace ($HOME)')
323
+ .option('--no-sync', 'Skip sync, just output exports from local file')
324
+ .option('--quiet', 'Suppress hints (for scripting)')
325
+ .action(async (options) => {
326
+ const manager = new SecretsManager({ globalMode: options.global });
327
+ try {
328
+ const filePath = manager.resolveFilePath(options.file);
329
+ const env = options.env === 'dev' ? manager.getDefaultEnvironment() : options.env;
330
+ // Sync first (unless --no-sync)
331
+ if (options.sync !== false) {
332
+ // Use smartSync in load mode (suppresses output, returns exports)
333
+ await manager.smartSync(filePath, env, true, true, false, false);
334
+ }
335
+ else {
336
+ // Just output exports from local file
337
+ const envPath = path.resolve(filePath);
338
+ if (!fs.existsSync(envPath)) {
339
+ console.error(`❌ File not found: ${envPath}`);
340
+ process.exit(1);
341
+ }
342
+ const content = fs.readFileSync(envPath, 'utf8');
343
+ const lines = content.split('\n');
344
+ for (const line of lines) {
345
+ if (line.trim().startsWith('#') || !line.trim())
346
+ continue;
347
+ const match = line.match(/^([^=]+)=(.*)$/);
348
+ if (match) {
349
+ const key = match[1].trim();
350
+ let value = match[2].trim();
351
+ // Remove quotes if present
352
+ if ((value.startsWith('"') && value.endsWith('"')) ||
353
+ (value.startsWith("'") && value.endsWith("'"))) {
354
+ value = value.slice(1, -1);
355
+ }
356
+ // Escape single quotes in value
357
+ const escapedValue = value.replace(/'/g, "'\\''");
358
+ console.log(`export ${key}='${escapedValue}'`);
359
+ }
360
+ }
361
+ }
362
+ // Show hint to stderr (doesn't interfere with eval)
363
+ if (!options.quiet) {
364
+ console.error('');
365
+ console.error('💡 To load these into your shell:');
366
+ console.error(' eval "$(lsh load)"');
367
+ console.error('');
368
+ console.error('💡 Add to ~/.zshrc for auto-load:');
369
+ console.error(' lsh-load() { eval "$(lsh load --quiet "$@")"; echo "✅ Secrets loaded"; }');
370
+ }
371
+ }
372
+ catch (error) {
373
+ const err = error;
374
+ console.error('❌ Failed to load secrets:', err.message);
375
+ await manager.cleanup();
376
+ process.exit(1);
377
+ }
378
+ finally {
379
+ await manager.cleanup();
380
+ }
381
+ });
316
382
  // Status command - get detailed status info
317
383
  program
318
384
  .command('status')
@@ -458,6 +524,7 @@ API_KEY=
458
524
  .option('--export', 'Output in export format for shell evaluation (alias for --format export)')
459
525
  .option('--format <type>', 'Output format: env, json, yaml, toml, export', 'env')
460
526
  .option('--exact', 'Require exact key match (disable fuzzy matching)')
527
+ .option('--no-mask', 'Show full values in fuzzy match results')
461
528
  .action(async (key, options) => {
462
529
  try {
463
530
  const manager = new SecretsManager({ globalMode: options.global });
@@ -546,11 +613,13 @@ API_KEY=
546
613
  // Multiple matches - show all matches for user to choose
547
614
  console.error(`🔍 Found ${matches.length} matches for '${key}':\n`);
548
615
  for (const match of matches) {
549
- // Mask value for display
550
- const maskedValue = match.value.length > 4
551
- ? match.value.substring(0, 4) + '*'.repeat(Math.min(match.value.length - 4, 10))
552
- : '****';
553
- console.error(` ${match.key}=${maskedValue}`);
616
+ // Mask value for display unless --no-mask is set
617
+ const displayValue = options.mask === false
618
+ ? match.value
619
+ : (match.value.length > 4
620
+ ? match.value.substring(0, 4) + '*'.repeat(Math.min(match.value.length - 4, 10))
621
+ : '****');
622
+ console.error(` ${match.key}=${displayValue}`);
554
623
  }
555
624
  console.error('');
556
625
  console.error('💡 Please specify the exact key name or use one of:');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "3.1.7",
3
+ "version": "3.1.9",
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": {
@@ -65,7 +65,8 @@
65
65
  "dependencies": {
66
66
  "@storacha/client": "^1.8.18",
67
67
  "@supabase/supabase-js": "^2.57.4",
68
- "bcrypt": "^5.1.1",
68
+ "@types/proper-lockfile": "^4.1.4",
69
+ "bcrypt": "^6.0.0",
69
70
  "chalk": "^5.3.0",
70
71
  "chokidar": "^5.0.0",
71
72
  "commander": "^14.0.2",
@@ -80,6 +81,7 @@
80
81
  "node-cron": "^4.2.1",
81
82
  "ora": "^9.0.0",
82
83
  "pg": "^8.16.3",
84
+ "proper-lockfile": "^4.1.2",
83
85
  "smol-toml": "^1.3.1",
84
86
  "uuid": "^13.0.0"
85
87
  },