lsh-framework 2.3.0 → 2.3.2

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.
@@ -108,8 +108,43 @@ export class IPFSSecretsStorage {
108
108
  try {
109
109
  const metadataKey = this.getMetadataKey(gitRepo, environment);
110
110
  let metadata = this.metadata[metadataKey];
111
+ // Construct display name for error messages
112
+ const displayEnv = gitRepo
113
+ ? (environment ? `${gitRepo}_${environment}` : gitRepo)
114
+ : (environment || 'default');
115
+ // If no local metadata, try to fetch from Storacha registry first (for git repos)
116
+ if (!metadata && gitRepo) {
117
+ try {
118
+ const storacha = getStorachaClient();
119
+ if (storacha.isEnabled() && await storacha.isAuthenticated()) {
120
+ logger.info(` 🔍 No local metadata found, checking Storacha registry...`);
121
+ const latestCid = await storacha.getLatestCID(gitRepo);
122
+ if (latestCid) {
123
+ logger.info(` ✅ Found secrets in registry (CID: ${latestCid})`);
124
+ // Create metadata from registry
125
+ metadata = {
126
+ environment,
127
+ git_repo: gitRepo,
128
+ cid: latestCid,
129
+ timestamp: new Date().toISOString(),
130
+ keys_count: 0, // Unknown until decrypted
131
+ encrypted: true,
132
+ };
133
+ this.metadata[metadataKey] = metadata;
134
+ this.saveMetadata();
135
+ }
136
+ }
137
+ }
138
+ catch (error) {
139
+ // Registry check failed, continue to error
140
+ const err = error;
141
+ logger.debug(` Registry check failed: ${err.message}`);
142
+ }
143
+ }
111
144
  if (!metadata) {
112
- throw new Error(`No secrets found for environment: ${environment}`);
145
+ throw new Error(`No secrets found for environment: ${displayEnv}\n\n` +
146
+ `💡 Tip: Check available environments with: lsh env\n` +
147
+ ` Or push secrets first with: lsh push`);
113
148
  }
114
149
  // Check if there's a newer version in the registry (for git repos)
115
150
  if (gitRepo) {
@@ -253,7 +288,7 @@ export class IPFSSecretsStorage {
253
288
  /**
254
289
  * Store encrypted data locally
255
290
  */
256
- async storeLocally(cid, encryptedData, environment) {
291
+ async storeLocally(cid, encryptedData, _environment) {
257
292
  const cachePath = path.join(this.cacheDir, `${cid}.encrypted`);
258
293
  fs.writeFileSync(cachePath, encryptedData, 'utf8');
259
294
  logger.debug(`Cached secrets locally: ${cachePath}`);
@@ -282,7 +282,7 @@ export class SecretsManager {
282
282
  ` Or push secrets first with: lsh push --env ${environment}`);
283
283
  }
284
284
  // Preserve local LSH-internal keys before overwriting
285
- let localLshKeys = {};
285
+ const localLshKeys = {};
286
286
  if (fs.existsSync(envFilePath)) {
287
287
  const existingContent = fs.readFileSync(envFilePath, 'utf8');
288
288
  const existingEnv = this.parseEnvFile(existingContent);
@@ -244,12 +244,24 @@ export class StorachaClient {
244
244
  if (!await this.isAuthenticated()) {
245
245
  throw new Error('Not authenticated');
246
246
  }
247
+ // Get the latest registry version and increment it
248
+ let registryVersion = 1;
249
+ try {
250
+ const latestRegistry = await this.getLatestRegistry(repoName);
251
+ if (latestRegistry && latestRegistry.registryVersion) {
252
+ registryVersion = latestRegistry.registryVersion + 1;
253
+ }
254
+ }
255
+ catch (err) {
256
+ logger.debug(`Could not fetch latest registry version, using version 1: ${err.message}`);
257
+ }
247
258
  const registry = {
248
259
  repoName,
249
260
  environment,
250
261
  cid: secretsCid, // Include the secrets CID
251
262
  timestamp: new Date().toISOString(),
252
- version: '2.2.2',
263
+ version: '2.3.0', // LSH version
264
+ registryVersion, // Incremental version counter
253
265
  };
254
266
  const content = JSON.stringify(registry, null, 2);
255
267
  const buffer = Buffer.from(content, 'utf-8');
@@ -259,7 +271,7 @@ export class StorachaClient {
259
271
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
260
272
  const file = new File([uint8Array], filename, { type: 'application/json' });
261
273
  const cid = await client.uploadFile(file);
262
- logger.debug(`📝 Uploaded registry for ${repoName} (secrets CID: ${secretsCid}): ${cid}`);
274
+ logger.debug(`📝 Uploaded registry v${registryVersion} for ${repoName} (secrets CID: ${secretsCid}): ${cid}`);
263
275
  return cid.toString();
264
276
  }
265
277
  /**
@@ -303,6 +315,7 @@ export class StorachaClient {
303
315
  cid: cid,
304
316
  timestamp: json.timestamp,
305
317
  secretsCid: json.cid,
318
+ registryVersion: json.registryVersion || 0, // Default to 0 for old registries without version
306
319
  });
307
320
  }
308
321
  }
@@ -311,11 +324,18 @@ export class StorachaClient {
311
324
  continue;
312
325
  }
313
326
  }
314
- // Sort by timestamp (newest first) and return the most recent secrets CID
327
+ // Sort by registryVersion (highest first), then timestamp as tie-breaker
315
328
  if (registries.length > 0) {
316
- registries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
329
+ registries.sort((a, b) => {
330
+ // First compare by registryVersion (higher is newer)
331
+ if (b.registryVersion !== a.registryVersion) {
332
+ return b.registryVersion - a.registryVersion;
333
+ }
334
+ // If versions match, use timestamp as tie-breaker
335
+ return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
336
+ });
317
337
  const latest = registries[0];
318
- logger.debug(`✅ Found latest CID for ${repoName}: ${latest.secretsCid} (timestamp: ${latest.timestamp})`);
338
+ logger.debug(`✅ Found latest CID for ${repoName}: ${latest.secretsCid} (v${latest.registryVersion}, timestamp: ${latest.timestamp})`);
319
339
  return latest.secretsCid;
320
340
  }
321
341
  // No registry found
@@ -327,6 +347,76 @@ export class StorachaClient {
327
347
  return null;
328
348
  }
329
349
  }
350
+ /**
351
+ * Get the latest registry object for a repo
352
+ * Returns the full registry object including registryVersion
353
+ */
354
+ async getLatestRegistry(repoName) {
355
+ if (!this.isEnabled()) {
356
+ return null;
357
+ }
358
+ if (!await this.isAuthenticated()) {
359
+ return null;
360
+ }
361
+ try {
362
+ const client = await this.getClient();
363
+ // Only check recent uploads (limit to 20 for performance)
364
+ const pageSize = 20;
365
+ // Get first page of uploads
366
+ const results = await client.capability.upload.list({
367
+ cursor: '',
368
+ size: pageSize,
369
+ });
370
+ // Collect all registry files for this repo
371
+ const registries = [];
372
+ for (const upload of results.results) {
373
+ try {
374
+ const cid = upload.root.toString();
375
+ // Download with timeout
376
+ const downloadPromise = this.download(cid);
377
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000));
378
+ const content = await Promise.race([downloadPromise, timeoutPromise]);
379
+ // Skip large files (registry should be < 1KB)
380
+ if (content.length > 1024) {
381
+ continue;
382
+ }
383
+ // Try to parse as JSON
384
+ const json = JSON.parse(content.toString('utf-8'));
385
+ // Check if it's an LSH registry file for our repo
386
+ if (json.repoName === repoName && json.version && json.cid && json.timestamp) {
387
+ registries.push({
388
+ repoName: json.repoName,
389
+ environment: json.environment,
390
+ cid: json.cid,
391
+ timestamp: json.timestamp,
392
+ version: json.version,
393
+ registryVersion: json.registryVersion || 0,
394
+ });
395
+ }
396
+ }
397
+ catch {
398
+ // Not an LSH registry file or failed to download
399
+ continue;
400
+ }
401
+ }
402
+ // Sort by registryVersion (highest first), then timestamp as tie-breaker
403
+ if (registries.length > 0) {
404
+ registries.sort((a, b) => {
405
+ if (b.registryVersion !== a.registryVersion) {
406
+ return b.registryVersion - a.registryVersion;
407
+ }
408
+ return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
409
+ });
410
+ return registries[0];
411
+ }
412
+ return null;
413
+ }
414
+ catch (error) {
415
+ const err = error;
416
+ logger.debug(`Failed to get latest registry: ${err.message}`);
417
+ return null;
418
+ }
419
+ }
330
420
  /**
331
421
  * Check if registry exists for a repo by listing uploads
332
422
  * Returns true if a registry file for this repo exists in Storacha
@@ -18,53 +18,6 @@ export async function loadCommands() {
18
18
  const cmdMap = await parseCommands(files);
19
19
  return cmdMap;
20
20
  }
21
- async function _makeCommand(commander) {
22
- const _commands = await loadCommands();
23
- commander.command("jug").action(() => {
24
- console.log("heat jug");
25
- });
26
- commander.command("pot").action(() => {
27
- console.log("heat pot");
28
- });
29
- return commander;
30
- }
31
- // export async function init_lib_cmd(program: Command) {
32
- // const brew = program.command("lib");
33
- // // const commands = await loadCommands();
34
- // // await set(lsh.commands, commands);
35
- // brew.command("tea").action(() => {
36
- // console.log("brew tea");
37
- // });
38
- // brew.command("coffee").action(() => {
39
- // console.log("brew coffee");
40
- // });
41
- // await makeCommand(brew);
42
- // // for (let c in commands) {
43
- // // brew.command(c).action(() => console.log(c));
44
- // // }
45
- // // .command("lib").description("lsh lib commands");
46
- // // lib
47
- // // .showHelpAfterError(true)
48
- // // .showSuggestionAfterError(true);
49
- // // const commands = await loadCommands();
50
- // // set(lsh.commands, commands);
51
- // // for (const [key, value] of Object.entries(get(lsh.commands))) {
52
- // // // console.log(`${key} : ${value}`);
53
- // // lib.command(key).action(() => {console.log(value)});
54
- // // };
55
- // // .action(async (type: String, action: String, spec: Spec) => {
56
- // // const commands = await loadCommands();
57
- // // set(lsh.commands, commands);
58
- // // switch (type) {
59
- // // case "ls":
60
- // // // console.log("lsh called");
61
- // // // console.log(get(lsh.commands)['rand']());
62
- // // break;
63
- // // default:
64
- // // console.log("default");
65
- // // }
66
- // // });
67
- // }
68
21
  export async function init_lib(program) {
69
22
  const lib = program
70
23
  .command("lib")
@@ -842,5 +842,208 @@ API_KEY=
842
842
  process.exit(1);
843
843
  }
844
844
  });
845
+ // Clear stuck registries and local metadata
846
+ program
847
+ .command('clear')
848
+ .description('Clear local metadata and cache to resolve stuck registries')
849
+ .option('--repo <name>', 'Clear metadata for specific repo only')
850
+ .option('--cache', 'Also clear local encrypted secrets cache')
851
+ .option('--storacha', 'Also delete old Storacha uploads (registries and secrets)')
852
+ .option('--all', 'Clear all metadata and cache (requires confirmation)')
853
+ .option('-y, --yes', 'Skip confirmation prompts')
854
+ .action(async (options) => {
855
+ try {
856
+ const lshDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.lsh');
857
+ const metadataPath = path.join(lshDir, 'secrets-metadata.json');
858
+ const cacheDir = path.join(lshDir, 'secrets-cache');
859
+ // Determine what we're clearing
860
+ if (!options.repo && !options.all) {
861
+ console.error('❌ Please specify either --repo <name> or --all');
862
+ console.log('');
863
+ console.log('Examples:');
864
+ console.log(' lsh clear --repo lsh_test_repo # Clear metadata for specific repo');
865
+ console.log(' lsh clear --all # Clear all metadata');
866
+ console.log(' lsh clear --all --cache # Clear metadata and cache');
867
+ process.exit(1);
868
+ }
869
+ // Load metadata
870
+ if (!fs.existsSync(metadataPath)) {
871
+ console.log('â„šī¸ No metadata file found - nothing to clear');
872
+ return;
873
+ }
874
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
875
+ const keys = Object.keys(metadata);
876
+ if (keys.length === 0) {
877
+ console.log('â„šī¸ Metadata is already empty');
878
+ return;
879
+ }
880
+ // Show what will be cleared
881
+ console.log('📋 Current metadata entries:');
882
+ console.log('');
883
+ if (options.repo) {
884
+ const repoKeys = keys.filter(k => metadata[k].git_repo === options.repo);
885
+ if (repoKeys.length === 0) {
886
+ console.log(`â„šī¸ No metadata found for repo: ${options.repo}`);
887
+ return;
888
+ }
889
+ console.log(`Repo: ${options.repo}`);
890
+ repoKeys.forEach(key => {
891
+ console.log(` - ${key} (CID: ${metadata[key].cid.substring(0, 12)}...)`);
892
+ });
893
+ console.log('');
894
+ console.log(`Will clear ${repoKeys.length} ${repoKeys.length === 1 ? 'entry' : 'entries'}`);
895
+ }
896
+ else {
897
+ const repoCount = new Set(keys.map(k => metadata[k].git_repo)).size;
898
+ console.log(`Total entries: ${keys.length} across ${repoCount} ${repoCount === 1 ? 'repo' : 'repos'}`);
899
+ }
900
+ if (options.cache) {
901
+ if (fs.existsSync(cacheDir)) {
902
+ const cacheFiles = fs.readdirSync(cacheDir);
903
+ console.log(`Cache files: ${cacheFiles.length}`);
904
+ }
905
+ }
906
+ console.log('');
907
+ // Confirmation
908
+ if (!options.yes) {
909
+ console.log('âš ī¸ WARNING: This will clear local metadata!');
910
+ console.log('');
911
+ console.log('This is useful when:');
912
+ console.log(' â€ĸ Registry is returning stale/old CIDs');
913
+ console.log(' â€ĸ Pull fails with "bad decrypt" errors');
914
+ console.log(' â€ĸ You need to force a fresh sync');
915
+ console.log('');
916
+ console.log('After clearing, you will need to push secrets again.');
917
+ console.log('');
918
+ const rl = readline.createInterface({
919
+ input: process.stdin,
920
+ output: process.stdout,
921
+ });
922
+ const answer = await new Promise((resolve) => {
923
+ rl.question('Continue? (yes/no): ', (ans) => {
924
+ rl.close();
925
+ resolve(ans.trim().toLowerCase());
926
+ });
927
+ });
928
+ if (answer !== 'yes' && answer !== 'y') {
929
+ console.log('');
930
+ console.log('❌ Cancelled');
931
+ return;
932
+ }
933
+ }
934
+ console.log('');
935
+ // Clear metadata
936
+ if (options.repo) {
937
+ const repoKeys = keys.filter(k => metadata[k].git_repo === options.repo);
938
+ repoKeys.forEach(key => delete metadata[key]);
939
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
940
+ console.log(`✅ Cleared ${repoKeys.length} metadata ${repoKeys.length === 1 ? 'entry' : 'entries'} for ${options.repo}`);
941
+ }
942
+ else {
943
+ fs.writeFileSync(metadataPath, '{}');
944
+ console.log(`✅ Cleared all ${keys.length} metadata ${keys.length === 1 ? 'entry' : 'entries'}`);
945
+ }
946
+ // Clear cache if requested
947
+ if (options.cache && fs.existsSync(cacheDir)) {
948
+ const cacheFiles = fs.readdirSync(cacheDir);
949
+ let cleared = 0;
950
+ for (const file of cacheFiles) {
951
+ const filePath = path.join(cacheDir, file);
952
+ if (fs.statSync(filePath).isFile()) {
953
+ fs.unlinkSync(filePath);
954
+ cleared++;
955
+ }
956
+ }
957
+ console.log(`✅ Cleared ${cleared} cache ${cleared === 1 ? 'file' : 'files'}`);
958
+ }
959
+ // Clear Storacha uploads if requested
960
+ if (options.storacha && options.repo) {
961
+ console.log('');
962
+ console.log('🌐 Clearing Storacha uploads...');
963
+ try {
964
+ const { StorachaClient } = await import('../../lib/storacha-client.js');
965
+ const storacha = new StorachaClient();
966
+ if (!storacha.isEnabled()) {
967
+ console.log('â„šī¸ Storacha is not enabled - skipping cloud cleanup');
968
+ }
969
+ else if (!(await storacha.isAuthenticated())) {
970
+ console.log('â„šī¸ Not authenticated with Storacha - skipping cloud cleanup');
971
+ }
972
+ else {
973
+ // Get all uploads
974
+ const client = await storacha.getClient();
975
+ const pageSize = 50;
976
+ const results = await client.capability.upload.list({
977
+ cursor: '',
978
+ size: pageSize,
979
+ });
980
+ // Find LSH-related uploads for this repo
981
+ const toDelete = [];
982
+ for (const upload of results.results) {
983
+ try {
984
+ const cid = upload.root.toString();
985
+ // Download with timeout
986
+ const downloadPromise = storacha.download(cid);
987
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000));
988
+ const content = await Promise.race([downloadPromise, timeoutPromise]);
989
+ // Check if it's a registry file for this repo
990
+ if (content.length < 2048) {
991
+ try {
992
+ const json = JSON.parse(content.toString('utf-8'));
993
+ if (json.repoName === options.repo) {
994
+ toDelete.push({ cid, type: 'registry', size: content.length });
995
+ }
996
+ }
997
+ catch {
998
+ // Not JSON, might be encrypted secrets
999
+ // Check filename pattern
1000
+ const _filename = `lsh-secrets-${options.repo}`;
1001
+ if (cid.includes(options.repo) || content.toString().includes(options.repo)) {
1002
+ toDelete.push({ cid, type: 'secrets', size: content.length });
1003
+ }
1004
+ }
1005
+ }
1006
+ }
1007
+ catch {
1008
+ // Failed to download or parse, skip
1009
+ continue;
1010
+ }
1011
+ }
1012
+ if (toDelete.length > 0) {
1013
+ console.log(`Found ${toDelete.length} Storacha ${toDelete.length === 1 ? 'upload' : 'uploads'} for ${options.repo}:`);
1014
+ toDelete.forEach((item) => {
1015
+ console.log(` - ${item.type}: ${item.cid.substring(0, 16)}... (${item.size} bytes)`);
1016
+ });
1017
+ // Note: Storacha doesn't currently support deletion via SDK
1018
+ // The uploads will remain but won't be used after metadata is cleared
1019
+ console.log('');
1020
+ console.log('âš ī¸ Note: Storacha uploads cannot be deleted programmatically.');
1021
+ console.log(' These files will remain in Storacha but won\'t be used after metadata is cleared.');
1022
+ console.log(' To fully remove them, use the Storacha web console:');
1023
+ console.log(' https://console.storacha.network/');
1024
+ }
1025
+ else {
1026
+ console.log(`â„šī¸ No Storacha uploads found for ${options.repo}`);
1027
+ }
1028
+ }
1029
+ }
1030
+ catch (storageError) {
1031
+ const storErr = storageError;
1032
+ console.error(`âš ī¸ Failed to check Storacha uploads: ${storErr.message}`);
1033
+ console.log(' Local metadata has been cleared, but cloud uploads may remain.');
1034
+ }
1035
+ }
1036
+ console.log('');
1037
+ console.log('💡 Next steps:');
1038
+ console.log(' 1. lsh push .env # Push secrets with current key');
1039
+ console.log(' 2. lsh pull .env # Verify pull works');
1040
+ console.log('');
1041
+ }
1042
+ catch (error) {
1043
+ const err = error;
1044
+ console.error('❌ Failed to clear metadata:', err.message);
1045
+ process.exit(1);
1046
+ }
1047
+ });
845
1048
  }
846
1049
  export default init_secrets;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
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": {