lsh-framework 3.2.2 → 3.2.3
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/sync-history.js +8 -2
- package/dist/commands/sync.js +22 -58
- package/dist/lib/ipfs-client-manager.js +78 -0
- package/dist/lib/ipfs-secrets-storage.js +61 -121
- package/dist/lib/ipfs-sync-logger.js +4 -4
- package/dist/lib/ipfs-sync.js +30 -20
- package/dist/lib/lsh-config.js +7 -0
- package/dist/lib/secrets-manager.js +7 -4
- package/dist/services/secrets/secrets.js +5 -0
- package/package.json +1 -1
|
@@ -45,8 +45,14 @@ export function registerSyncHistoryCommands(program) {
|
|
|
45
45
|
if (options.url) {
|
|
46
46
|
// Show URLs only
|
|
47
47
|
for (const record of records) {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
if (record.cid) {
|
|
49
|
+
console.log(`ipfs://${record.cid}`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Old records without real CID
|
|
53
|
+
const recordId = record.key_fingerprint.substring(0, 16);
|
|
54
|
+
console.log(`lsh-record://${recordId} (local)`);
|
|
55
|
+
}
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
else {
|
package/dist/commands/sync.js
CHANGED
|
@@ -143,57 +143,15 @@ export function registerSyncCommands(program) {
|
|
|
143
143
|
const ipfsSync = getIPFSSync();
|
|
144
144
|
const gitInfo = getGitRepoInfo();
|
|
145
145
|
// Step 1: Ensure IPFS is set up and daemon is running
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// Check if IPFS is installed
|
|
149
|
-
const clientInfo = await manager.detect();
|
|
150
|
-
if (!clientInfo.installed) {
|
|
151
|
-
const installSpinner = ora('Installing IPFS client...').start();
|
|
152
|
-
try {
|
|
153
|
-
await manager.install();
|
|
154
|
-
installSpinner.succeed(chalk.green('IPFS client installed'));
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
const err = error;
|
|
158
|
-
installSpinner.fail(chalk.red('Failed to install IPFS'));
|
|
159
|
-
console.error(chalk.red(err.message));
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
// Initialize repo if needed
|
|
164
|
-
const initSpinner = ora('Initializing IPFS...').start();
|
|
165
|
-
try {
|
|
166
|
-
await manager.init();
|
|
167
|
-
initSpinner.succeed(chalk.green('IPFS initialized'));
|
|
168
|
-
}
|
|
169
|
-
catch (error) {
|
|
170
|
-
const err = error;
|
|
171
|
-
if (!err.message.includes('already') && !err.message.includes('exists')) {
|
|
172
|
-
initSpinner.fail(chalk.red('Failed to initialize IPFS'));
|
|
173
|
-
console.error(chalk.red(err.message));
|
|
174
|
-
process.exit(1);
|
|
175
|
-
}
|
|
176
|
-
initSpinner.succeed(chalk.green('IPFS already initialized'));
|
|
177
|
-
}
|
|
178
|
-
// Start daemon
|
|
179
|
-
const startSpinner = ora('Starting IPFS daemon...').start();
|
|
180
|
-
try {
|
|
181
|
-
await manager.start();
|
|
182
|
-
// Give daemon a moment to start accepting connections
|
|
183
|
-
await new Promise(resolve => { setTimeout(resolve, 2000); });
|
|
184
|
-
startSpinner.succeed(chalk.green('IPFS daemon started'));
|
|
185
|
-
daemonReady = true;
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
const err = error;
|
|
189
|
-
startSpinner.fail(chalk.red('Failed to start daemon'));
|
|
190
|
-
console.error(chalk.red(err.message));
|
|
191
|
-
process.exit(1);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
146
|
+
try {
|
|
147
|
+
await manager.ensureDaemonRunning();
|
|
195
148
|
console.log(chalk.green('✓ IPFS daemon running'));
|
|
196
149
|
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
const err = error;
|
|
152
|
+
console.error(chalk.red(err.message));
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
197
155
|
// Step 2: Read and validate .env file
|
|
198
156
|
const envPath = path.resolve(options.file);
|
|
199
157
|
if (!fs.existsSync(envPath)) {
|
|
@@ -287,12 +245,14 @@ export function registerSyncCommands(program) {
|
|
|
287
245
|
try {
|
|
288
246
|
const ipfsSync = getIPFSSync();
|
|
289
247
|
const gitInfo = getGitRepoInfo();
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
248
|
+
// Ensure daemon is running (auto-setup if needed)
|
|
249
|
+
try {
|
|
250
|
+
const ipfsManager = new IPFSClientManager();
|
|
251
|
+
await ipfsManager.ensureDaemonRunning();
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const err = error;
|
|
255
|
+
spinner.fail(chalk.red(err.message));
|
|
296
256
|
process.exit(1);
|
|
297
257
|
}
|
|
298
258
|
// Read .env file
|
|
@@ -391,9 +351,13 @@ export function registerSyncCommands(program) {
|
|
|
391
351
|
if (!cid) {
|
|
392
352
|
try {
|
|
393
353
|
const ipfsSync = getIPFSSync();
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
354
|
+
try {
|
|
355
|
+
const ipfsManager = new IPFSClientManager();
|
|
356
|
+
await ipfsManager.ensureDaemonRunning();
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
const err = error;
|
|
360
|
+
spinner.fail(chalk.red(err.message));
|
|
397
361
|
process.exit(1);
|
|
398
362
|
}
|
|
399
363
|
// Get encryption key
|
|
@@ -7,8 +7,10 @@ import * as path from 'path';
|
|
|
7
7
|
import * as os from 'os';
|
|
8
8
|
import { exec, spawn } from 'child_process';
|
|
9
9
|
import { promisify } from 'util';
|
|
10
|
+
import * as readline from 'readline';
|
|
10
11
|
import { createLogger } from './logger.js';
|
|
11
12
|
import { getPlatformInfo } from './platform-utils.js';
|
|
13
|
+
import { getLshConfig } from './lsh-config.js';
|
|
12
14
|
const execAsync = promisify(exec);
|
|
13
15
|
const logger = createLogger('IPFSClientManager');
|
|
14
16
|
/**
|
|
@@ -364,6 +366,82 @@ export class IPFSClientManager {
|
|
|
364
366
|
fs.unlinkSync(zipPath);
|
|
365
367
|
fs.rmSync(path.join(this.ipfsDir, 'kubo'), { recursive: true });
|
|
366
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Ensure IPFS daemon is installed and running.
|
|
371
|
+
* Prompts once for install consent, then auto-manages daemon lifecycle.
|
|
372
|
+
*/
|
|
373
|
+
async ensureDaemonRunning() {
|
|
374
|
+
// Step 1: Check if daemon is already running
|
|
375
|
+
const isRunning = await this.isDaemonRunning();
|
|
376
|
+
if (isRunning) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// Step 2: Check if Kubo is installed
|
|
380
|
+
const clientInfo = await this.detect();
|
|
381
|
+
const config = getLshConfig();
|
|
382
|
+
if (!clientInfo.installed) {
|
|
383
|
+
if (config.getIpfsConsent()) {
|
|
384
|
+
logger.info('📦 Installing IPFS client (Kubo)...');
|
|
385
|
+
await this.install();
|
|
386
|
+
await this.init();
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
if (!process.stdin.isTTY) {
|
|
390
|
+
throw new Error('IPFS (Kubo) is required for sync but is not installed.\n' +
|
|
391
|
+
'Install manually: lsh ipfs install && lsh ipfs start');
|
|
392
|
+
}
|
|
393
|
+
const consent = await this.promptUser('IPFS (Kubo) is required for sync. Install now? [Y/n] ');
|
|
394
|
+
if (consent.toLowerCase() === 'n') {
|
|
395
|
+
throw new Error('IPFS is required for push/pull.\n' +
|
|
396
|
+
'Install manually: lsh ipfs install && lsh ipfs start');
|
|
397
|
+
}
|
|
398
|
+
logger.info('📦 Installing IPFS client (Kubo)...');
|
|
399
|
+
await this.install();
|
|
400
|
+
await this.init();
|
|
401
|
+
config.setIpfsConsent(true);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// Step 3: Start daemon
|
|
405
|
+
logger.info('🚀 Starting IPFS daemon...');
|
|
406
|
+
await this.start();
|
|
407
|
+
// Step 4: Wait for API readiness
|
|
408
|
+
const ready = await this.waitForDaemon(15000);
|
|
409
|
+
if (!ready) {
|
|
410
|
+
throw new Error('IPFS daemon started but API is not responding.\n' +
|
|
411
|
+
'Try manually: lsh ipfs start');
|
|
412
|
+
}
|
|
413
|
+
logger.info('✅ IPFS daemon ready');
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Check if daemon API is responding
|
|
417
|
+
*/
|
|
418
|
+
async isDaemonRunning() {
|
|
419
|
+
try {
|
|
420
|
+
const response = await fetch('http://127.0.0.1:5001/api/v0/id', {
|
|
421
|
+
method: 'POST',
|
|
422
|
+
signal: AbortSignal.timeout(3000),
|
|
423
|
+
});
|
|
424
|
+
return response.ok;
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Prompt user for input via readline
|
|
432
|
+
*/
|
|
433
|
+
promptUser(question) {
|
|
434
|
+
return new Promise((resolve) => {
|
|
435
|
+
const rl = readline.createInterface({
|
|
436
|
+
input: process.stdin,
|
|
437
|
+
output: process.stdout,
|
|
438
|
+
});
|
|
439
|
+
rl.question(question, (answer) => {
|
|
440
|
+
rl.close();
|
|
441
|
+
resolve(answer || 'y');
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}
|
|
367
445
|
/**
|
|
368
446
|
* Ensure required directories exist
|
|
369
447
|
*/
|
|
@@ -63,11 +63,16 @@ export class IPFSSecretsStorage {
|
|
|
63
63
|
try {
|
|
64
64
|
// Encrypt secrets
|
|
65
65
|
const encryptedData = this.encryptSecrets(secrets, encryptionKey);
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
|
|
66
|
+
// Upload to IPFS (daemon guaranteed running by ensureDaemonRunning)
|
|
67
|
+
const ipfsSync = getIPFSSync();
|
|
68
|
+
const filename = `lsh-secrets-${environment}.encrypted`;
|
|
69
|
+
const cid = await ipfsSync.upload(Buffer.from(encryptedData, 'utf-8'), filename, { environment, gitRepo });
|
|
70
|
+
if (!cid) {
|
|
71
|
+
throw new Error('IPFS upload failed. Is the daemon running? Check: lsh ipfs status');
|
|
72
|
+
}
|
|
73
|
+
// Cache locally for fast re-reads
|
|
69
74
|
await this.storeLocally(cid, encryptedData, environment);
|
|
70
|
-
// Update metadata
|
|
75
|
+
// Update metadata with real CID
|
|
71
76
|
const metadata = {
|
|
72
77
|
environment,
|
|
73
78
|
git_repo: gitRepo,
|
|
@@ -84,40 +89,15 @@ export class IPFSSecretsStorage {
|
|
|
84
89
|
if (gitRepo) {
|
|
85
90
|
logger.info(` Repository: ${gitRepo}/${gitBranch || 'main'}`);
|
|
86
91
|
}
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
let uploadedToNetwork = false;
|
|
90
|
-
let realCid = null;
|
|
91
|
-
if (await ipfsSync.checkDaemon()) {
|
|
92
|
-
try {
|
|
93
|
-
const filename = `lsh-secrets-${environment}.encrypted`;
|
|
94
|
-
realCid = await ipfsSync.upload(Buffer.from(encryptedData, 'utf-8'), filename, { environment, gitRepo });
|
|
95
|
-
if (realCid) {
|
|
96
|
-
// Update CID to the real IPFS CID
|
|
97
|
-
logger.info(` 🌐 Synced to IPFS (CID: ${realCid})`);
|
|
98
|
-
uploadedToNetwork = true;
|
|
99
|
-
// Update metadata with real CID if different
|
|
100
|
-
if (realCid !== cid) {
|
|
101
|
-
metadata.cid = realCid;
|
|
102
|
-
this.metadata[this.getMetadataKey(gitRepo, environment)] = metadata;
|
|
103
|
-
await this.saveMetadata();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
catch (error) {
|
|
108
|
-
const err = error;
|
|
109
|
-
logger.warn(` ⚠️ IPFS upload failed: ${err.message}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// Publish to IPNS if we uploaded to the network
|
|
113
|
-
if (uploadedToNetwork && realCid && encryptionKey) {
|
|
92
|
+
// Publish to IPNS
|
|
93
|
+
if (encryptionKey) {
|
|
114
94
|
try {
|
|
115
95
|
const repoName = gitRepo || DEFAULTS.DEFAULT_ENVIRONMENT;
|
|
116
96
|
const env = environment || DEFAULTS.DEFAULT_ENVIRONMENT;
|
|
117
97
|
const keyInfo = deriveKeyInfo(encryptionKey, repoName, env);
|
|
118
98
|
const ipnsName = await ensureKeyImported(ipfsSync.getApiUrl(), keyInfo);
|
|
119
99
|
if (ipnsName) {
|
|
120
|
-
const publishedName = await ipfsSync.publishToIPNS(
|
|
100
|
+
const publishedName = await ipfsSync.publishToIPNS(cid, keyInfo.keyName);
|
|
121
101
|
if (publishedName) {
|
|
122
102
|
metadata.ipns_name = publishedName;
|
|
123
103
|
this.metadata[this.getMetadataKey(gitRepo, environment)] = metadata;
|
|
@@ -128,14 +108,11 @@ export class IPFSSecretsStorage {
|
|
|
128
108
|
}
|
|
129
109
|
catch (error) {
|
|
130
110
|
const err = error;
|
|
131
|
-
logger.
|
|
111
|
+
logger.error(`Content uploaded (CID: ${cid}) but IPNS publish failed: ${err.message}\n` +
|
|
112
|
+
`Other machines won't find it via 'lsh pull' until you re-push.`);
|
|
132
113
|
}
|
|
133
114
|
}
|
|
134
|
-
|
|
135
|
-
logger.warn(` 📁 Secrets cached locally only (no network sync)`);
|
|
136
|
-
logger.warn(` 💡 Start IPFS daemon for network sync: lsh ipfs start`);
|
|
137
|
-
}
|
|
138
|
-
return realCid || cid;
|
|
115
|
+
return cid;
|
|
139
116
|
}
|
|
140
117
|
catch (error) {
|
|
141
118
|
const err = error;
|
|
@@ -148,89 +125,61 @@ export class IPFSSecretsStorage {
|
|
|
148
125
|
*/
|
|
149
126
|
async pull(environment, encryptionKey, gitRepo) {
|
|
150
127
|
try {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
? (environment ? `${gitRepo}_${environment}` : gitRepo)
|
|
156
|
-
: (environment || 'default');
|
|
157
|
-
// If no local metadata, try IPNS resolution first
|
|
158
|
-
if (!metadata && encryptionKey) {
|
|
128
|
+
const ipfsSync = getIPFSSync();
|
|
129
|
+
// Step 1: Always resolve via IPNS (source of truth)
|
|
130
|
+
let resolvedCid = null;
|
|
131
|
+
if (encryptionKey) {
|
|
159
132
|
try {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
await this.saveMetadata();
|
|
182
|
-
}
|
|
133
|
+
const repoName = gitRepo || DEFAULTS.DEFAULT_ENVIRONMENT;
|
|
134
|
+
const env = environment || DEFAULTS.DEFAULT_ENVIRONMENT;
|
|
135
|
+
const keyInfo = deriveKeyInfo(encryptionKey, repoName, env);
|
|
136
|
+
const ipnsName = await ensureKeyImported(ipfsSync.getApiUrl(), keyInfo);
|
|
137
|
+
if (ipnsName) {
|
|
138
|
+
logger.info(` 🔍 Resolving via IPNS: ${ipnsName.substring(0, 20)}...`);
|
|
139
|
+
resolvedCid = await ipfsSync.resolveIPNS(ipnsName);
|
|
140
|
+
if (resolvedCid) {
|
|
141
|
+
logger.info(` ✅ IPNS resolved to CID: ${resolvedCid}`);
|
|
142
|
+
// Update local metadata
|
|
143
|
+
const metadataKey = this.getMetadataKey(gitRepo, environment);
|
|
144
|
+
this.metadata[metadataKey] = {
|
|
145
|
+
environment,
|
|
146
|
+
git_repo: gitRepo,
|
|
147
|
+
cid: resolvedCid,
|
|
148
|
+
ipns_name: ipnsName,
|
|
149
|
+
timestamp: new Date().toISOString(),
|
|
150
|
+
keys_count: 0,
|
|
151
|
+
encrypted: true,
|
|
152
|
+
};
|
|
153
|
+
await this.saveMetadata();
|
|
183
154
|
}
|
|
184
155
|
}
|
|
185
156
|
}
|
|
186
157
|
catch (error) {
|
|
187
158
|
const err = error;
|
|
188
|
-
logger.debug(` IPNS resolution
|
|
159
|
+
logger.debug(` IPNS resolution error: ${err.message}`);
|
|
189
160
|
}
|
|
190
161
|
}
|
|
191
|
-
//
|
|
192
|
-
if (!
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
timestamp: new Date().toISOString(),
|
|
204
|
-
keys_count: 0, // Unknown until decrypted
|
|
205
|
-
encrypted: true,
|
|
206
|
-
};
|
|
207
|
-
this.metadata[metadataKey] = metadata;
|
|
208
|
-
await this.saveMetadata();
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
// History check failed, continue to error
|
|
213
|
-
const err = error;
|
|
214
|
-
logger.debug(` IPFS history check failed: ${err.message}`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
if (!metadata) {
|
|
218
|
-
throw new Error(`No secrets found for environment: ${displayEnv}\n\n` +
|
|
219
|
-
`💡 Tip: Check available environments with: lsh env\n` +
|
|
220
|
-
` Or push secrets first with: lsh push\n` +
|
|
221
|
-
` Or pull by CID with: lsh sync pull <cid>`);
|
|
162
|
+
// No fallback to local metadata — IPNS is the source of truth
|
|
163
|
+
if (!resolvedCid) {
|
|
164
|
+
throw new Error('Could not resolve secrets from network.\n\n' +
|
|
165
|
+
'Possible causes:\n' +
|
|
166
|
+
' - The machine that pushed secrets is offline or IPFS daemon stopped\n' +
|
|
167
|
+
' - IPNS record hasn\'t propagated yet (try again in 30s)\n' +
|
|
168
|
+
' - Network connectivity issue\n' +
|
|
169
|
+
' - IPNS record expired from DHT (records are cached ~24-48h;\n' +
|
|
170
|
+
' the publishing machine must be online periodically)\n\n' +
|
|
171
|
+
'Troubleshooting:\n' +
|
|
172
|
+
' lsh ipfs status # Check local daemon\n' +
|
|
173
|
+
' lsh doctor # Full health check');
|
|
222
174
|
}
|
|
223
|
-
//
|
|
224
|
-
let cachedData = await this.loadLocally(
|
|
225
|
-
// If not in cache, try downloading from IPFS
|
|
175
|
+
// Step 2: Load content — try cache first, then IPFS download
|
|
176
|
+
let cachedData = await this.loadLocally(resolvedCid);
|
|
226
177
|
if (!cachedData) {
|
|
227
|
-
const ipfsSync = getIPFSSync();
|
|
228
178
|
try {
|
|
229
179
|
logger.info(` 🌐 Downloading from IPFS...`);
|
|
230
|
-
const downloadedData = await ipfsSync.download(
|
|
180
|
+
const downloadedData = await ipfsSync.download(resolvedCid);
|
|
231
181
|
if (downloadedData) {
|
|
232
|
-
|
|
233
|
-
await this.storeLocally(metadata.cid, downloadedData.toString('utf-8'), environment);
|
|
182
|
+
await this.storeLocally(resolvedCid, downloadedData.toString('utf-8'), environment);
|
|
234
183
|
cachedData = downloadedData.toString('utf-8');
|
|
235
184
|
logger.info(` ✅ Downloaded and cached from IPFS`);
|
|
236
185
|
}
|
|
@@ -241,14 +190,13 @@ export class IPFSSecretsStorage {
|
|
|
241
190
|
}
|
|
242
191
|
}
|
|
243
192
|
if (!cachedData) {
|
|
244
|
-
throw new Error(`Secrets
|
|
245
|
-
`💡
|
|
246
|
-
` Or pull directly by CID: lsh sync pull <cid>`);
|
|
193
|
+
throw new Error(`Secrets resolved via IPNS (CID: ${resolvedCid}) but download failed.\n\n` +
|
|
194
|
+
`💡 The source machine may be offline. Try again when it's online.`);
|
|
247
195
|
}
|
|
248
|
-
// Decrypt
|
|
196
|
+
// Step 3: Decrypt
|
|
249
197
|
const secrets = this.decryptSecrets(cachedData, encryptionKey);
|
|
250
198
|
logger.info(`📥 Retrieved ${secrets.length} secrets from IPFS`);
|
|
251
|
-
logger.info(` CID: ${
|
|
199
|
+
logger.info(` CID: ${resolvedCid}`);
|
|
252
200
|
logger.info(` Environment: ${environment}`);
|
|
253
201
|
return secrets;
|
|
254
202
|
}
|
|
@@ -344,14 +292,6 @@ export class IPFSSecretsStorage {
|
|
|
344
292
|
throw error;
|
|
345
293
|
}
|
|
346
294
|
}
|
|
347
|
-
/**
|
|
348
|
-
* Generate IPFS-compatible CID from content
|
|
349
|
-
*/
|
|
350
|
-
generateCID(content) {
|
|
351
|
-
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
352
|
-
// Format like IPFS CIDv1 (bafkreixxx...)
|
|
353
|
-
return `bafkrei${hash.substring(0, 52)}`;
|
|
354
|
-
}
|
|
355
295
|
/**
|
|
356
296
|
* Store encrypted data locally
|
|
357
297
|
*/
|
|
@@ -64,7 +64,7 @@ export class IPFSSyncLogger {
|
|
|
64
64
|
};
|
|
65
65
|
// Use file-based storage with IPFS-like CIDs
|
|
66
66
|
// Records are stored locally and can optionally be uploaded to IPFS
|
|
67
|
-
const cid = this.
|
|
67
|
+
const cid = this.generateRecordId(record);
|
|
68
68
|
const repoEnv = this.getRepoEnvKey(record.git_repo, record.environment);
|
|
69
69
|
// Store the record locally
|
|
70
70
|
await this.storeRecordLocally(cid, record);
|
|
@@ -73,9 +73,9 @@ export class IPFSSyncLogger {
|
|
|
73
73
|
this.syncLog[repoEnv] = [];
|
|
74
74
|
}
|
|
75
75
|
this.syncLog[repoEnv].push({
|
|
76
|
-
cid,
|
|
76
|
+
cid, // This is the record ID (file key), NOT the IPFS CID
|
|
77
77
|
timestamp: record.timestamp,
|
|
78
|
-
url: `ipfs://${cid}`,
|
|
78
|
+
url: record.cid ? `ipfs://${record.cid}` : `lsh-record://${cid}`,
|
|
79
79
|
action: record.action,
|
|
80
80
|
});
|
|
81
81
|
// Save sync log
|
|
@@ -138,7 +138,7 @@ export class IPFSSyncLogger {
|
|
|
138
138
|
/**
|
|
139
139
|
* Generate a content-addressed ID (like IPFS CID)
|
|
140
140
|
*/
|
|
141
|
-
|
|
141
|
+
generateRecordId(record) {
|
|
142
142
|
const content = JSON.stringify(record);
|
|
143
143
|
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
144
144
|
// Format like IPFS CIDv1 (bafkreixxx...)
|
package/dist/lib/ipfs-sync.js
CHANGED
|
@@ -346,31 +346,41 @@ export class IPFSSync {
|
|
|
346
346
|
/**
|
|
347
347
|
* Publish a CID to IPNS under the given key name.
|
|
348
348
|
* The key must already be imported into Kubo.
|
|
349
|
+
* Publishes to DHT and blocks until confirmed.
|
|
350
|
+
* Retries once on failure.
|
|
349
351
|
* Returns the IPNS name on success, null on failure.
|
|
350
352
|
*/
|
|
351
353
|
async publishToIPNS(cid, keyName) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
354
|
+
const url = `${this.LOCAL_IPFS_API}/name/publish?arg=${cid}&key=${encodeURIComponent(keyName)}&lifetime=87600h&resolve=false&offline=false&allow-offline=false`;
|
|
355
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
356
|
+
try {
|
|
357
|
+
logger.info('📡 Publishing to IPNS (waiting for network confirmation)...');
|
|
358
|
+
const response = await fetch(url, {
|
|
359
|
+
method: 'POST',
|
|
360
|
+
signal: AbortSignal.timeout(120000),
|
|
361
|
+
});
|
|
362
|
+
if (!response.ok) {
|
|
363
|
+
const errorText = await response.text();
|
|
364
|
+
logger.warn(`IPNS publish failed (attempt ${attempt}): ${errorText}`);
|
|
365
|
+
if (attempt === 2)
|
|
366
|
+
return null;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const data = await response.json();
|
|
370
|
+
logger.info(`📡 IPNS published: ${data.Name} → ${data.Value}`);
|
|
371
|
+
return data.Name;
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
const err = error;
|
|
375
|
+
logger.warn(`IPNS publish error (attempt ${attempt}): ${err.message}`);
|
|
376
|
+
if (attempt === 2) {
|
|
377
|
+
logger.error(`IPNS publish failed after retry. Content is uploaded (CID: ${cid}) ` +
|
|
378
|
+
`but other machines won't find it via 'lsh pull' until you re-push.`);
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
364
381
|
}
|
|
365
|
-
const data = await response.json();
|
|
366
|
-
logger.info(`📡 IPNS published: ${data.Name} → ${data.Value}`);
|
|
367
|
-
return data.Name;
|
|
368
|
-
}
|
|
369
|
-
catch (error) {
|
|
370
|
-
const err = error;
|
|
371
|
-
logger.debug(`IPNS publish error: ${err.message}`);
|
|
372
|
-
return null;
|
|
373
382
|
}
|
|
383
|
+
return null;
|
|
374
384
|
}
|
|
375
385
|
/**
|
|
376
386
|
* Resolve an IPNS name to its current CID.
|
package/dist/lib/lsh-config.js
CHANGED
|
@@ -126,6 +126,13 @@ export class LshConfigManager {
|
|
|
126
126
|
getConfigPath() {
|
|
127
127
|
return this.configPath;
|
|
128
128
|
}
|
|
129
|
+
getIpfsConsent() {
|
|
130
|
+
return this.config.ipfs_consent === true;
|
|
131
|
+
}
|
|
132
|
+
setIpfsConsent(value) {
|
|
133
|
+
this.config.ipfs_consent = value;
|
|
134
|
+
this.saveConfig();
|
|
135
|
+
}
|
|
129
136
|
}
|
|
130
137
|
// Singleton instance
|
|
131
138
|
let _configManager = null;
|
|
@@ -299,7 +299,7 @@ export class SecretsManager {
|
|
|
299
299
|
logger.info(`✅ Pushed ${secrets.length} secrets from ${filename} to IPFS`);
|
|
300
300
|
console.log(`📦 IPFS CID: ${cid}`);
|
|
301
301
|
// Log to IPFS for immutable audit record
|
|
302
|
-
await this.logToIPFS('push', effectiveEnv, secrets.length);
|
|
302
|
+
await this.logToIPFS('push', effectiveEnv, secrets.length, cid);
|
|
303
303
|
}
|
|
304
304
|
/**
|
|
305
305
|
* Pull .env from IPFS
|
|
@@ -961,7 +961,7 @@ LSH_SECRETS_KEY=${this.encryptionKey}
|
|
|
961
961
|
/**
|
|
962
962
|
* Log sync operation to IPFS for immutable record
|
|
963
963
|
*/
|
|
964
|
-
async logToIPFS(action, environment, keysCount) {
|
|
964
|
+
async logToIPFS(action, environment, keysCount, realCid) {
|
|
965
965
|
try {
|
|
966
966
|
const ipfsLogger = new IPFSSyncLogger();
|
|
967
967
|
if (!ipfsLogger.isEnabled()) {
|
|
@@ -971,10 +971,13 @@ LSH_SECRETS_KEY=${this.encryptionKey}
|
|
|
971
971
|
action,
|
|
972
972
|
environment: this.getRepoAwareEnvironment(environment),
|
|
973
973
|
keys_count: keysCount,
|
|
974
|
+
cid: realCid,
|
|
974
975
|
});
|
|
975
976
|
if (cid) {
|
|
976
|
-
|
|
977
|
-
|
|
977
|
+
if (realCid) {
|
|
978
|
+
console.log(`📝 Recorded on IPFS: ipfs://${realCid}`);
|
|
979
|
+
console.log(` View: https://ipfs.io/ipfs/${realCid}`);
|
|
980
|
+
}
|
|
978
981
|
}
|
|
979
982
|
}
|
|
980
983
|
catch (error) {
|
|
@@ -8,6 +8,7 @@ import * as path from 'path';
|
|
|
8
8
|
import * as readline from 'readline';
|
|
9
9
|
import { getGitRepoInfo } from '../../lib/git-utils.js';
|
|
10
10
|
import { ENV_VARS } from '../../constants/index.js';
|
|
11
|
+
import { IPFSClientManager } from '../../lib/ipfs-client-manager.js';
|
|
11
12
|
/**
|
|
12
13
|
* Type guard to check if a string is a valid OutputFormat.
|
|
13
14
|
*/
|
|
@@ -26,6 +27,8 @@ export async function init_secrets(program) {
|
|
|
26
27
|
.action(async (options) => {
|
|
27
28
|
const manager = new SecretsManager({ globalMode: options.global });
|
|
28
29
|
try {
|
|
30
|
+
const ipfsManager = new IPFSClientManager();
|
|
31
|
+
await ipfsManager.ensureDaemonRunning();
|
|
29
32
|
// Resolve file path (handles global mode)
|
|
30
33
|
const filePath = manager.resolveFilePath(options.file);
|
|
31
34
|
// v2.0: Use context-aware default environment
|
|
@@ -53,6 +56,8 @@ export async function init_secrets(program) {
|
|
|
53
56
|
.action(async (options) => {
|
|
54
57
|
const manager = new SecretsManager({ globalMode: options.global });
|
|
55
58
|
try {
|
|
59
|
+
const ipfsManager = new IPFSClientManager();
|
|
60
|
+
await ipfsManager.ensureDaemonRunning();
|
|
56
61
|
// Resolve file path (handles global mode)
|
|
57
62
|
const filePath = manager.resolveFilePath(options.file);
|
|
58
63
|
// v2.0: Use context-aware default environment
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lsh-framework",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.3",
|
|
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": {
|