lsh-framework 2.2.0 → 2.2.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.
- package/dist/commands/init.js +21 -2
- package/dist/lib/ipfs-secrets-storage.js +40 -1
- package/dist/lib/storacha-client.js +151 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -233,7 +233,7 @@ async function checkCloudSecretsExist() {
|
|
|
233
233
|
}
|
|
234
234
|
const repoName = gitInfo.repoName;
|
|
235
235
|
const environment = repoName; // Default environment for repo
|
|
236
|
-
//
|
|
236
|
+
// First check local metadata (fast path)
|
|
237
237
|
const paths = getPlatformPaths();
|
|
238
238
|
const metadataPath = path.join(paths.dataDir, 'secrets-metadata.json');
|
|
239
239
|
try {
|
|
@@ -246,7 +246,26 @@ async function checkCloudSecretsExist() {
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
catch {
|
|
249
|
-
// Metadata file doesn't exist or can't be read
|
|
249
|
+
// Metadata file doesn't exist or can't be read - continue to network check
|
|
250
|
+
}
|
|
251
|
+
// Check Storacha network for registry file (works on new machines)
|
|
252
|
+
try {
|
|
253
|
+
const { getStorachaClient } = await import('../lib/storacha-client.js');
|
|
254
|
+
const storacha = getStorachaClient();
|
|
255
|
+
// Only check network if Storacha is enabled and authenticated
|
|
256
|
+
if (storacha.isEnabled() && await storacha.isAuthenticated()) {
|
|
257
|
+
const spinner = ora('Checking Storacha network for existing secrets...').start();
|
|
258
|
+
const registryExists = await storacha.checkRegistry(repoName);
|
|
259
|
+
spinner.stop();
|
|
260
|
+
if (registryExists) {
|
|
261
|
+
return { exists: true, repoName, environment };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
// Network check failed, but that's okay - just means no secrets found
|
|
267
|
+
const err = error;
|
|
268
|
+
console.log(chalk.gray(` (Network check skipped: ${err.message})`));
|
|
250
269
|
}
|
|
251
270
|
return { exists: false, repoName, environment };
|
|
252
271
|
}
|
|
@@ -72,6 +72,20 @@ export class IPFSSecretsStorage {
|
|
|
72
72
|
// encryptedData is already a Buffer, pass it directly
|
|
73
73
|
await storacha.upload(Buffer.from(encryptedData), filename);
|
|
74
74
|
logger.info(` ☁️ Synced to Storacha network`);
|
|
75
|
+
// Upload registry file if this is a git repo
|
|
76
|
+
// This allows detection on new machines without local metadata
|
|
77
|
+
// Include the secrets CID so other hosts can fetch the latest version
|
|
78
|
+
if (gitRepo) {
|
|
79
|
+
try {
|
|
80
|
+
await storacha.uploadRegistry(gitRepo, environment, cid);
|
|
81
|
+
logger.debug(` 📝 Registry uploaded for ${gitRepo} (CID: ${cid})`);
|
|
82
|
+
}
|
|
83
|
+
catch (regError) {
|
|
84
|
+
// Registry upload failed, but secrets are still uploaded
|
|
85
|
+
const _regErr = regError;
|
|
86
|
+
logger.debug(` Registry upload failed: ${_regErr.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
90
|
catch (error) {
|
|
77
91
|
const err = error;
|
|
@@ -93,10 +107,35 @@ export class IPFSSecretsStorage {
|
|
|
93
107
|
async pull(environment, encryptionKey, gitRepo) {
|
|
94
108
|
try {
|
|
95
109
|
const metadataKey = this.getMetadataKey(gitRepo, environment);
|
|
96
|
-
|
|
110
|
+
let metadata = this.metadata[metadataKey];
|
|
97
111
|
if (!metadata) {
|
|
98
112
|
throw new Error(`No secrets found for environment: ${environment}`);
|
|
99
113
|
}
|
|
114
|
+
// Check if there's a newer version in the registry (for git repos)
|
|
115
|
+
if (gitRepo) {
|
|
116
|
+
try {
|
|
117
|
+
const storacha = getStorachaClient();
|
|
118
|
+
if (storacha.isEnabled() && await storacha.isAuthenticated()) {
|
|
119
|
+
const latestCid = await storacha.getLatestCID(gitRepo);
|
|
120
|
+
if (latestCid && latestCid !== metadata.cid) {
|
|
121
|
+
logger.info(` 🔄 Found newer version in registry (CID: ${latestCid})`);
|
|
122
|
+
// Update metadata with latest CID
|
|
123
|
+
metadata = {
|
|
124
|
+
...metadata,
|
|
125
|
+
cid: latestCid,
|
|
126
|
+
timestamp: new Date().toISOString(),
|
|
127
|
+
};
|
|
128
|
+
this.metadata[metadataKey] = metadata;
|
|
129
|
+
this.saveMetadata();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
// Registry check failed, continue with local metadata
|
|
135
|
+
const err = error;
|
|
136
|
+
logger.debug(` Registry check failed: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
100
139
|
// Try to load from local cache
|
|
101
140
|
let cachedData = await this.loadLocally(metadata.cid);
|
|
102
141
|
// If not in cache, try downloading from Storacha
|
|
@@ -233,6 +233,157 @@ export class StorachaClient {
|
|
|
233
233
|
throw new Error(`Failed to download from Storacha: ${err.message}`);
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Upload registry file for a repo
|
|
238
|
+
* Registry files mark that secrets exist and include the latest secrets CID
|
|
239
|
+
*/
|
|
240
|
+
async uploadRegistry(repoName, environment, secretsCid) {
|
|
241
|
+
if (!this.isEnabled()) {
|
|
242
|
+
throw new Error('Storacha is not enabled');
|
|
243
|
+
}
|
|
244
|
+
if (!await this.isAuthenticated()) {
|
|
245
|
+
throw new Error('Not authenticated');
|
|
246
|
+
}
|
|
247
|
+
const registry = {
|
|
248
|
+
repoName,
|
|
249
|
+
environment,
|
|
250
|
+
cid: secretsCid, // Include the secrets CID
|
|
251
|
+
timestamp: new Date().toISOString(),
|
|
252
|
+
version: '2.2.2',
|
|
253
|
+
};
|
|
254
|
+
const content = JSON.stringify(registry, null, 2);
|
|
255
|
+
const buffer = Buffer.from(content, 'utf-8');
|
|
256
|
+
const filename = `lsh-registry-${repoName}.json`;
|
|
257
|
+
const client = await this.getClient();
|
|
258
|
+
const uint8Array = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
260
|
+
const file = new File([uint8Array], filename, { type: 'application/json' });
|
|
261
|
+
const cid = await client.uploadFile(file);
|
|
262
|
+
logger.debug(`📝 Uploaded registry for ${repoName} (secrets CID: ${secretsCid}): ${cid}`);
|
|
263
|
+
return cid.toString();
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get the latest secrets CID from registry
|
|
267
|
+
* Returns the CID of the latest secrets if registry exists, null otherwise
|
|
268
|
+
*/
|
|
269
|
+
async getLatestCID(repoName) {
|
|
270
|
+
if (!this.isEnabled()) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
if (!await this.isAuthenticated()) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const client = await this.getClient();
|
|
278
|
+
// Only check recent uploads (limit to 20 for performance)
|
|
279
|
+
const pageSize = 20;
|
|
280
|
+
// Get first page of uploads
|
|
281
|
+
const results = await client.capability.upload.list({
|
|
282
|
+
cursor: '',
|
|
283
|
+
size: pageSize,
|
|
284
|
+
});
|
|
285
|
+
// Check recent uploads for registry file
|
|
286
|
+
for (const upload of results.results) {
|
|
287
|
+
try {
|
|
288
|
+
const cid = upload.root.toString();
|
|
289
|
+
// Download with timeout
|
|
290
|
+
const downloadPromise = this.download(cid);
|
|
291
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000));
|
|
292
|
+
const content = await Promise.race([downloadPromise, timeoutPromise]);
|
|
293
|
+
// Skip large files (registry should be < 1KB)
|
|
294
|
+
if (content.length > 1024) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
// Try to parse as JSON
|
|
298
|
+
const json = JSON.parse(content.toString('utf-8'));
|
|
299
|
+
// Check if it's an LSH registry file for our repo
|
|
300
|
+
if (json.repoName === repoName && json.version && json.cid) {
|
|
301
|
+
logger.debug(`✅ Found latest CID for ${repoName}: ${json.cid}`);
|
|
302
|
+
return json.cid;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// Not an LSH registry file or failed to download
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// No registry found
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
const err = error;
|
|
315
|
+
logger.debug(`Failed to get latest CID: ${err.message}`);
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Check if registry exists for a repo by listing uploads
|
|
321
|
+
* Returns true if a registry file for this repo exists in Storacha
|
|
322
|
+
*
|
|
323
|
+
* NOTE: This is optimized to check only recent small files (likely registry files)
|
|
324
|
+
* to avoid downloading large encrypted secret files.
|
|
325
|
+
*/
|
|
326
|
+
async checkRegistry(repoName) {
|
|
327
|
+
if (!this.isEnabled()) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
if (!await this.isAuthenticated()) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const client = await this.getClient();
|
|
335
|
+
// Only check recent uploads (limit to 20 for performance)
|
|
336
|
+
const pageSize = 20;
|
|
337
|
+
// Get first page of uploads
|
|
338
|
+
const results = await client.capability.upload.list({
|
|
339
|
+
cursor: '',
|
|
340
|
+
size: pageSize,
|
|
341
|
+
});
|
|
342
|
+
// Track checked count for logging
|
|
343
|
+
let checked = 0;
|
|
344
|
+
let skipped = 0;
|
|
345
|
+
// Check if any uploads match our registry pattern
|
|
346
|
+
// Registry files are small JSON files (~200 bytes)
|
|
347
|
+
// Skip large files (encrypted secrets are much larger)
|
|
348
|
+
for (const upload of results.results) {
|
|
349
|
+
try {
|
|
350
|
+
const cid = upload.root.toString();
|
|
351
|
+
// Quick heuristic: registry files are tiny (<1KB)
|
|
352
|
+
// Skip if this looks like a large encrypted file based on CID
|
|
353
|
+
// We'll attempt download with a timeout
|
|
354
|
+
const downloadPromise = this.download(cid);
|
|
355
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000) // 5s timeout per file
|
|
356
|
+
);
|
|
357
|
+
const content = await Promise.race([downloadPromise, timeoutPromise]);
|
|
358
|
+
// Skip large files (registry should be < 1KB)
|
|
359
|
+
if (content.length > 1024) {
|
|
360
|
+
skipped++;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
checked++;
|
|
364
|
+
// Try to parse as JSON
|
|
365
|
+
const json = JSON.parse(content.toString('utf-8'));
|
|
366
|
+
// Check if it's an LSH registry file for our repo
|
|
367
|
+
if (json.repoName === repoName && json.version) {
|
|
368
|
+
logger.debug(`✅ Found registry for ${repoName} at CID: ${cid} (checked ${checked} files, skipped ${skipped})`);
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// Not an LSH registry file, timed out, or failed to download - continue
|
|
374
|
+
skipped++;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// No registry found
|
|
378
|
+
logger.debug(`❌ No registry found for ${repoName} (checked ${checked} files, skipped ${skipped})`);
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
const err = error;
|
|
383
|
+
logger.debug(`Failed to check registry: ${err.message}`);
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
236
387
|
/**
|
|
237
388
|
* Enable Storacha network sync
|
|
238
389
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lsh-framework",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.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": {
|