lsh-framework 3.1.8 ā 3.1.14
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/cli.js +4 -2
- package/dist/commands/context.js +578 -0
- package/dist/commands/init.js +40 -273
- package/dist/commands/ipfs.js +47 -10
- package/dist/commands/sync.js +365 -0
- package/dist/constants/config.js +0 -1
- package/dist/lib/ipfs-secrets-storage.js +70 -92
- package/dist/lib/ipfs-sync-logger.js +5 -7
- package/dist/lib/ipfs-sync.js +349 -0
- package/dist/services/secrets/secrets.js +52 -111
- package/package.json +1 -2
- package/dist/commands/storacha.js +0 -208
- package/dist/lib/storacha-client.js +0 -516
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Commands
|
|
3
|
+
* Native IPFS sync for secrets management (mirrors mcli pattern)
|
|
4
|
+
*
|
|
5
|
+
* Usage: lsh sync <command>
|
|
6
|
+
*/
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as crypto from 'crypto';
|
|
12
|
+
import { getIPFSSync } from '../lib/ipfs-sync.js';
|
|
13
|
+
import { getGitRepoInfo } from '../lib/git-utils.js';
|
|
14
|
+
import { ENV_VARS } from '../constants/index.js';
|
|
15
|
+
/**
|
|
16
|
+
* Register sync commands
|
|
17
|
+
*/
|
|
18
|
+
export function registerSyncCommands(program) {
|
|
19
|
+
const syncCommand = program
|
|
20
|
+
.command('sync')
|
|
21
|
+
.description('Sync secrets via native IPFS')
|
|
22
|
+
.action(() => {
|
|
23
|
+
// Show help when running `lsh sync` without subcommand
|
|
24
|
+
console.log(chalk.bold.cyan('\nš LSH Sync - IPFS Secrets Sync\n'));
|
|
25
|
+
console.log(chalk.gray('Sync encrypted secrets via native IPFS (no auth required)\n'));
|
|
26
|
+
console.log(chalk.bold('Commands:'));
|
|
27
|
+
console.log(` ${chalk.cyan('init')} š Initialize and start the IPFS daemon`);
|
|
28
|
+
console.log(` ${chalk.cyan('push')} ā¬ļø Push encrypted secrets to IPFS`);
|
|
29
|
+
console.log(` ${chalk.cyan('pull')} ā¬ļø Pull secrets from IPFS by CID`);
|
|
30
|
+
console.log(` ${chalk.cyan('status')} š Show IPFS daemon and sync status`);
|
|
31
|
+
console.log(` ${chalk.cyan('history')} š Show IPFS sync history`);
|
|
32
|
+
console.log(` ${chalk.cyan('verify')} ā
Verify that a CID is accessible on IPFS`);
|
|
33
|
+
console.log(` ${chalk.cyan('clear')} šļø Clear sync history`);
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(chalk.bold('Examples:'));
|
|
36
|
+
console.log(chalk.gray(' lsh sync init # Set up IPFS for first time'));
|
|
37
|
+
console.log(chalk.gray(' lsh sync push # Push secrets, get CID'));
|
|
38
|
+
console.log(chalk.gray(' lsh sync pull <cid> # Pull secrets by CID'));
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log(chalk.gray('Run "lsh sync <command> --help" for more info.'));
|
|
41
|
+
console.log('');
|
|
42
|
+
});
|
|
43
|
+
// lsh sync init
|
|
44
|
+
syncCommand
|
|
45
|
+
.command('init')
|
|
46
|
+
.description('š Initialize and start the IPFS daemon')
|
|
47
|
+
.action(async () => {
|
|
48
|
+
console.log(chalk.bold.cyan('\nš Initializing IPFS for sync...\n'));
|
|
49
|
+
const ipfsSync = getIPFSSync();
|
|
50
|
+
// Check if daemon is already running
|
|
51
|
+
if (await ipfsSync.checkDaemon()) {
|
|
52
|
+
const info = await ipfsSync.getDaemonInfo();
|
|
53
|
+
console.log(chalk.green('ā
IPFS daemon is already running!'));
|
|
54
|
+
if (info) {
|
|
55
|
+
console.log(chalk.gray(` Peer ID: ${info.peerId.substring(0, 16)}...`));
|
|
56
|
+
console.log(chalk.gray(` Version: ${info.version}`));
|
|
57
|
+
}
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(chalk.gray('You can now sync secrets:'));
|
|
60
|
+
console.log(chalk.cyan(' lsh sync push'));
|
|
61
|
+
console.log('');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Daemon not running, show instructions
|
|
65
|
+
console.log(chalk.yellow('ā ļø IPFS daemon not running'));
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(chalk.gray('To start IPFS:'));
|
|
68
|
+
console.log('');
|
|
69
|
+
console.log(chalk.bold('1. Install IPFS (if not installed):'));
|
|
70
|
+
console.log(chalk.cyan(' lsh ipfs install'));
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log(chalk.bold('2. Initialize IPFS repository:'));
|
|
73
|
+
console.log(chalk.cyan(' lsh ipfs init'));
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log(chalk.bold('3. Start the daemon:'));
|
|
76
|
+
console.log(chalk.cyan(' lsh ipfs start'));
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log(chalk.gray('Or run all at once:'));
|
|
79
|
+
console.log(chalk.cyan(' lsh ipfs install && lsh ipfs init && lsh ipfs start'));
|
|
80
|
+
console.log('');
|
|
81
|
+
});
|
|
82
|
+
// lsh sync push
|
|
83
|
+
syncCommand
|
|
84
|
+
.command('push')
|
|
85
|
+
.description('ā¬ļø Push encrypted secrets to IPFS, returns CID')
|
|
86
|
+
.option('-f, --file <path>', 'Path to .env file', '.env')
|
|
87
|
+
.option('-e, --env <name>', 'Environment name', '')
|
|
88
|
+
.action(async (options) => {
|
|
89
|
+
const spinner = ora('Uploading to IPFS...').start();
|
|
90
|
+
try {
|
|
91
|
+
const ipfsSync = getIPFSSync();
|
|
92
|
+
const gitInfo = getGitRepoInfo();
|
|
93
|
+
// Check daemon
|
|
94
|
+
if (!await ipfsSync.checkDaemon()) {
|
|
95
|
+
spinner.fail(chalk.red('IPFS daemon not running'));
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log(chalk.gray('Initialize IPFS first:'));
|
|
98
|
+
console.log(chalk.cyan(' lsh sync init'));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
// Read .env file
|
|
102
|
+
const envPath = path.resolve(options.file);
|
|
103
|
+
if (!fs.existsSync(envPath)) {
|
|
104
|
+
spinner.fail(chalk.red(`File not found: ${envPath}`));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const content = fs.readFileSync(envPath, 'utf-8');
|
|
108
|
+
// Get encryption key
|
|
109
|
+
const encryptionKey = process.env[ENV_VARS.LSH_SECRETS_KEY];
|
|
110
|
+
if (!encryptionKey) {
|
|
111
|
+
spinner.fail(chalk.red('LSH_SECRETS_KEY not set'));
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log(chalk.gray('Generate a key with:'));
|
|
114
|
+
console.log(chalk.cyan(' lsh key'));
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log(chalk.gray('Then set it:'));
|
|
117
|
+
console.log(chalk.cyan(' export LSH_SECRETS_KEY=<your-key>'));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
// Encrypt content
|
|
121
|
+
const key = crypto.createHash('sha256').update(encryptionKey).digest();
|
|
122
|
+
const iv = crypto.randomBytes(16);
|
|
123
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
|
124
|
+
let encrypted = cipher.update(content, 'utf8', 'hex');
|
|
125
|
+
encrypted += cipher.final('hex');
|
|
126
|
+
const encryptedData = iv.toString('hex') + ':' + encrypted;
|
|
127
|
+
// Upload to IPFS
|
|
128
|
+
const filename = `lsh-secrets-${options.env || 'default'}.encrypted`;
|
|
129
|
+
const cid = await ipfsSync.upload(Buffer.from(encryptedData, 'utf-8'), filename, {
|
|
130
|
+
environment: options.env || undefined,
|
|
131
|
+
gitRepo: gitInfo?.repoName || undefined,
|
|
132
|
+
});
|
|
133
|
+
if (!cid) {
|
|
134
|
+
spinner.fail(chalk.red('Upload failed'));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
spinner.succeed(chalk.green('Uploaded to IPFS!'));
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log(chalk.bold('CID:'), chalk.cyan(cid));
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log(chalk.gray('Share this CID with teammates to pull secrets:'));
|
|
142
|
+
console.log(chalk.cyan(` lsh sync pull ${cid}`));
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(chalk.gray('Public gateway URLs:'));
|
|
145
|
+
ipfsSync.getGatewayUrls(cid).slice(0, 2).forEach(url => {
|
|
146
|
+
console.log(chalk.gray(` ${url}`));
|
|
147
|
+
});
|
|
148
|
+
console.log('');
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
const err = error;
|
|
152
|
+
spinner.fail(chalk.red('Push failed'));
|
|
153
|
+
console.error(chalk.red(err.message));
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// lsh sync pull <cid>
|
|
158
|
+
syncCommand
|
|
159
|
+
.command('pull <cid>')
|
|
160
|
+
.description('ā¬ļø Pull secrets from IPFS by CID')
|
|
161
|
+
.option('-o, --output <path>', 'Output file path', '.env')
|
|
162
|
+
.option('--force', 'Overwrite existing file without backup')
|
|
163
|
+
.action(async (cid, options) => {
|
|
164
|
+
const spinner = ora('Downloading from IPFS...').start();
|
|
165
|
+
try {
|
|
166
|
+
const ipfsSync = getIPFSSync();
|
|
167
|
+
// Download from IPFS
|
|
168
|
+
const data = await ipfsSync.download(cid);
|
|
169
|
+
if (!data) {
|
|
170
|
+
spinner.fail(chalk.red('Download failed'));
|
|
171
|
+
console.log('');
|
|
172
|
+
console.log(chalk.gray('The CID might not be available on public gateways yet.'));
|
|
173
|
+
console.log(chalk.gray('Make sure the source machine is online with IPFS daemon running.'));
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
// Get encryption key
|
|
177
|
+
const encryptionKey = process.env[ENV_VARS.LSH_SECRETS_KEY];
|
|
178
|
+
if (!encryptionKey) {
|
|
179
|
+
spinner.fail(chalk.red('LSH_SECRETS_KEY not set'));
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log(chalk.gray('You need the same encryption key used to push.'));
|
|
182
|
+
console.log(chalk.gray('Set it in your environment:'));
|
|
183
|
+
console.log(chalk.cyan(' export LSH_SECRETS_KEY=<key-from-teammate>'));
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
// Decrypt content
|
|
187
|
+
const encryptedData = data.toString('utf-8');
|
|
188
|
+
const [ivHex, encrypted] = encryptedData.split(':');
|
|
189
|
+
if (!ivHex || !encrypted) {
|
|
190
|
+
spinner.fail(chalk.red('Invalid encrypted data format'));
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
const key = crypto.createHash('sha256').update(encryptionKey).digest();
|
|
194
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
195
|
+
let decrypted;
|
|
196
|
+
try {
|
|
197
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
198
|
+
decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
199
|
+
decrypted += decipher.final('utf8');
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
spinner.fail(chalk.red('Decryption failed'));
|
|
203
|
+
console.log('');
|
|
204
|
+
console.log(chalk.red('Wrong encryption key!'));
|
|
205
|
+
console.log(chalk.gray('Make sure LSH_SECRETS_KEY matches the key used to push.'));
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
// Write output file
|
|
209
|
+
const outputPath = path.resolve(options.output);
|
|
210
|
+
// Backup existing file if it exists (unless --force)
|
|
211
|
+
if (fs.existsSync(outputPath) && !options.force) {
|
|
212
|
+
const backupPath = `${outputPath}.backup.${Date.now()}`;
|
|
213
|
+
fs.copyFileSync(outputPath, backupPath);
|
|
214
|
+
console.log(chalk.gray(`Backed up existing file to: ${backupPath}`));
|
|
215
|
+
}
|
|
216
|
+
fs.writeFileSync(outputPath, decrypted, 'utf-8');
|
|
217
|
+
spinner.succeed(chalk.green('Downloaded and decrypted!'));
|
|
218
|
+
console.log('');
|
|
219
|
+
console.log(chalk.bold('Output:'), chalk.cyan(outputPath));
|
|
220
|
+
console.log(chalk.bold('CID:'), chalk.gray(cid));
|
|
221
|
+
console.log('');
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
const err = error;
|
|
225
|
+
spinner.fail(chalk.red('Pull failed'));
|
|
226
|
+
console.error(chalk.red(err.message));
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
// lsh sync status
|
|
231
|
+
syncCommand
|
|
232
|
+
.command('status')
|
|
233
|
+
.description('š Show IPFS daemon and sync status')
|
|
234
|
+
.action(async () => {
|
|
235
|
+
try {
|
|
236
|
+
const ipfsSync = getIPFSSync();
|
|
237
|
+
const daemonInfo = await ipfsSync.getDaemonInfo();
|
|
238
|
+
console.log(chalk.bold.cyan('\nš Sync Status\n'));
|
|
239
|
+
console.log(chalk.gray('ā'.repeat(50)));
|
|
240
|
+
console.log('');
|
|
241
|
+
if (daemonInfo) {
|
|
242
|
+
console.log(chalk.green('ā
IPFS daemon running'));
|
|
243
|
+
console.log(` Peer ID: ${daemonInfo.peerId.substring(0, 16)}...`);
|
|
244
|
+
console.log(` Version: ${daemonInfo.version}`);
|
|
245
|
+
console.log('');
|
|
246
|
+
console.log(chalk.gray('Ready to sync:'));
|
|
247
|
+
console.log(chalk.cyan(' lsh sync push # Push secrets'));
|
|
248
|
+
console.log(chalk.cyan(' lsh sync pull <cid> # Pull by CID'));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
console.log(chalk.yellow('ā ļø IPFS daemon not running'));
|
|
252
|
+
console.log('');
|
|
253
|
+
console.log(chalk.gray('Start with:'));
|
|
254
|
+
console.log(chalk.cyan(' lsh sync init'));
|
|
255
|
+
}
|
|
256
|
+
console.log('');
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
const err = error;
|
|
260
|
+
console.error(chalk.red('Failed to check status:'), err.message);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
// lsh sync history
|
|
265
|
+
syncCommand
|
|
266
|
+
.command('history')
|
|
267
|
+
.description('š Show IPFS sync history')
|
|
268
|
+
.option('-n, --limit <number>', 'Number of entries to show', '10')
|
|
269
|
+
.option('--json', 'Output as JSON')
|
|
270
|
+
.action(async (options) => {
|
|
271
|
+
try {
|
|
272
|
+
const ipfsSync = getIPFSSync();
|
|
273
|
+
const limit = parseInt(options.limit, 10);
|
|
274
|
+
const history = await ipfsSync.getHistory(limit);
|
|
275
|
+
if (options.json) {
|
|
276
|
+
console.log(JSON.stringify(history, null, 2));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
console.log(chalk.bold.cyan('\nš Sync History\n'));
|
|
280
|
+
console.log(chalk.gray('ā'.repeat(60)));
|
|
281
|
+
console.log('');
|
|
282
|
+
if (history.length === 0) {
|
|
283
|
+
console.log(chalk.gray('No sync history found.'));
|
|
284
|
+
console.log('');
|
|
285
|
+
console.log(chalk.gray('Push your first secrets with:'));
|
|
286
|
+
console.log(chalk.cyan(' lsh sync push'));
|
|
287
|
+
console.log('');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
for (const entry of history) {
|
|
291
|
+
const date = new Date(entry.timestamp);
|
|
292
|
+
const dateStr = date.toLocaleString();
|
|
293
|
+
console.log(chalk.bold(`${entry.cid.substring(0, 16)}...`));
|
|
294
|
+
console.log(` File: ${entry.filename}`);
|
|
295
|
+
console.log(` Size: ${entry.size} bytes`);
|
|
296
|
+
console.log(` Time: ${dateStr}`);
|
|
297
|
+
if (entry.gitRepo) {
|
|
298
|
+
console.log(` Repo: ${entry.gitRepo}`);
|
|
299
|
+
}
|
|
300
|
+
if (entry.environment) {
|
|
301
|
+
console.log(` Env: ${entry.environment}`);
|
|
302
|
+
}
|
|
303
|
+
console.log('');
|
|
304
|
+
}
|
|
305
|
+
console.log(chalk.gray(`Showing ${history.length} entries. Use -n to show more.`));
|
|
306
|
+
console.log('');
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
const err = error;
|
|
310
|
+
console.error(chalk.red('Failed to get history:'), err.message);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
// lsh sync verify <cid>
|
|
315
|
+
syncCommand
|
|
316
|
+
.command('verify <cid>')
|
|
317
|
+
.description('ā
Verify that a CID is accessible on IPFS')
|
|
318
|
+
.action(async (cid) => {
|
|
319
|
+
const spinner = ora('Verifying CID accessibility...').start();
|
|
320
|
+
try {
|
|
321
|
+
const ipfsSync = getIPFSSync();
|
|
322
|
+
const result = await ipfsSync.verifyCid(cid);
|
|
323
|
+
if (result.available) {
|
|
324
|
+
spinner.succeed(chalk.green('CID is accessible!'));
|
|
325
|
+
console.log('');
|
|
326
|
+
console.log(chalk.bold('CID:'), chalk.cyan(cid));
|
|
327
|
+
console.log(chalk.bold('Source:'), chalk.gray(result.source));
|
|
328
|
+
console.log('');
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
spinner.fail(chalk.red('CID not accessible'));
|
|
332
|
+
console.log('');
|
|
333
|
+
console.log(chalk.gray('The CID could not be found on the network.'));
|
|
334
|
+
console.log(chalk.gray('Possible reasons:'));
|
|
335
|
+
console.log(chalk.gray(' - Source machine is offline'));
|
|
336
|
+
console.log(chalk.gray(' - Content not yet propagated to gateways'));
|
|
337
|
+
console.log(chalk.gray(' - Invalid CID'));
|
|
338
|
+
console.log('');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
const err = error;
|
|
343
|
+
spinner.fail(chalk.red('Verification failed'));
|
|
344
|
+
console.error(chalk.red(err.message));
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
// lsh sync clear
|
|
349
|
+
syncCommand
|
|
350
|
+
.command('clear')
|
|
351
|
+
.description('šļø Clear sync history')
|
|
352
|
+
.action(async () => {
|
|
353
|
+
try {
|
|
354
|
+
const ipfsSync = getIPFSSync();
|
|
355
|
+
await ipfsSync.clearHistory();
|
|
356
|
+
console.log(chalk.green('ā
Sync history cleared'));
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
const err = error;
|
|
360
|
+
console.error(chalk.red('Failed to clear history:'), err.message);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
export default registerSyncCommands;
|
package/dist/constants/config.js
CHANGED
|
@@ -33,7 +33,6 @@ export const ENV_VARS = {
|
|
|
33
33
|
// Feature flags
|
|
34
34
|
LSH_LOCAL_STORAGE_QUIET: 'LSH_LOCAL_STORAGE_QUIET',
|
|
35
35
|
LSH_V1_COMPAT: 'LSH_V1_COMPAT',
|
|
36
|
-
LSH_STORACHA_ENABLED: 'LSH_STORACHA_ENABLED',
|
|
37
36
|
DISABLE_IPFS_SYNC: 'DISABLE_IPFS_SYNC',
|
|
38
37
|
// Logging
|
|
39
38
|
LSH_LOG_LEVEL: 'LSH_LOG_LEVEL',
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* IPFS Secrets Storage Adapter
|
|
3
|
-
* Stores encrypted secrets on IPFS
|
|
3
|
+
* Stores encrypted secrets on IPFS via native Kubo daemon
|
|
4
|
+
*
|
|
5
|
+
* Priority order:
|
|
6
|
+
* 1. Native IPFS (Kubo daemon on port 5001) - zero-config, no auth
|
|
7
|
+
* 2. Local cache - always available for offline access
|
|
4
8
|
*/
|
|
5
9
|
import * as fs from 'fs';
|
|
6
10
|
import * as fsPromises from 'fs/promises';
|
|
@@ -8,7 +12,7 @@ import * as path from 'path';
|
|
|
8
12
|
import * as os from 'os';
|
|
9
13
|
import * as crypto from 'crypto';
|
|
10
14
|
import { createLogger } from './logger.js';
|
|
11
|
-
import {
|
|
15
|
+
import { getIPFSSync } from './ipfs-sync.js';
|
|
12
16
|
import { ENV_VARS } from '../constants/index.js';
|
|
13
17
|
const logger = createLogger('IPFSSecretsStorage');
|
|
14
18
|
/**
|
|
@@ -31,8 +35,14 @@ export class IPFSSecretsStorage {
|
|
|
31
35
|
const lshDir = path.join(homeDir, '.lsh');
|
|
32
36
|
this.cacheDir = path.join(lshDir, 'secrets-cache');
|
|
33
37
|
this.metadataPath = path.join(lshDir, 'secrets-metadata.json');
|
|
34
|
-
//
|
|
35
|
-
|
|
38
|
+
// Load metadata synchronously to ensure we have all existing entries
|
|
39
|
+
// This fixes the bug where sequential pushes from different repos
|
|
40
|
+
// would overwrite each other's metadata
|
|
41
|
+
this.metadata = this.loadMetadata();
|
|
42
|
+
// Ensure cache directory exists
|
|
43
|
+
if (!fs.existsSync(this.cacheDir)) {
|
|
44
|
+
fs.mkdirSync(this.cacheDir, { recursive: true });
|
|
45
|
+
}
|
|
36
46
|
}
|
|
37
47
|
/**
|
|
38
48
|
* Initialize async parts
|
|
@@ -73,36 +83,36 @@ export class IPFSSecretsStorage {
|
|
|
73
83
|
if (gitRepo) {
|
|
74
84
|
logger.info(` Repository: ${gitRepo}/${gitBranch || 'main'}`);
|
|
75
85
|
}
|
|
76
|
-
//
|
|
77
|
-
const
|
|
78
|
-
|
|
86
|
+
// Try native IPFS upload
|
|
87
|
+
const ipfsSync = getIPFSSync();
|
|
88
|
+
let uploadedToNetwork = false;
|
|
89
|
+
let realCid = null;
|
|
90
|
+
if (await ipfsSync.checkDaemon()) {
|
|
79
91
|
try {
|
|
80
|
-
const filename = `lsh-secrets-${environment}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
catch (regError) {
|
|
93
|
-
// Registry upload failed, but secrets are still uploaded
|
|
94
|
-
const _regErr = regError;
|
|
95
|
-
logger.debug(` Registry upload failed: ${_regErr.message}`);
|
|
92
|
+
const filename = `lsh-secrets-${environment}.encrypted`;
|
|
93
|
+
realCid = await ipfsSync.upload(Buffer.from(encryptedData, 'utf-8'), filename, { environment, gitRepo });
|
|
94
|
+
if (realCid) {
|
|
95
|
+
// Update CID to the real IPFS CID
|
|
96
|
+
logger.info(` š Synced to IPFS (CID: ${realCid})`);
|
|
97
|
+
uploadedToNetwork = true;
|
|
98
|
+
// Update metadata with real CID if different
|
|
99
|
+
if (realCid !== cid) {
|
|
100
|
+
metadata.cid = realCid;
|
|
101
|
+
this.metadata[this.getMetadataKey(gitRepo, environment)] = metadata;
|
|
102
|
+
await this.saveMetadata();
|
|
96
103
|
}
|
|
97
104
|
}
|
|
98
105
|
}
|
|
99
106
|
catch (error) {
|
|
100
107
|
const err = error;
|
|
101
|
-
logger.warn(` ā ļø
|
|
102
|
-
logger.warn(` Secrets are still cached locally`);
|
|
108
|
+
logger.warn(` ā ļø IPFS upload failed: ${err.message}`);
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
|
-
|
|
111
|
+
if (!uploadedToNetwork) {
|
|
112
|
+
logger.warn(` š Secrets cached locally only (no network sync)`);
|
|
113
|
+
logger.warn(` š” Start IPFS daemon for network sync: lsh ipfs start`);
|
|
114
|
+
}
|
|
115
|
+
return realCid || cid;
|
|
106
116
|
}
|
|
107
117
|
catch (error) {
|
|
108
118
|
const err = error;
|
|
@@ -121,94 +131,62 @@ export class IPFSSecretsStorage {
|
|
|
121
131
|
const displayEnv = gitRepo
|
|
122
132
|
? (environment ? `${gitRepo}_${environment}` : gitRepo)
|
|
123
133
|
: (environment || 'default');
|
|
124
|
-
// If no local metadata,
|
|
134
|
+
// If no local metadata, check IPFS sync history
|
|
125
135
|
if (!metadata && gitRepo) {
|
|
126
136
|
try {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.metadata[metadataKey] = metadata;
|
|
143
|
-
await this.saveMetadata();
|
|
144
|
-
}
|
|
137
|
+
const ipfsSync = getIPFSSync();
|
|
138
|
+
const latestCid = await ipfsSync.getLatestCid(gitRepo, environment);
|
|
139
|
+
if (latestCid) {
|
|
140
|
+
logger.info(` ā
Found secrets in IPFS history (CID: ${latestCid})`);
|
|
141
|
+
// Create metadata from history
|
|
142
|
+
metadata = {
|
|
143
|
+
environment,
|
|
144
|
+
git_repo: gitRepo,
|
|
145
|
+
cid: latestCid,
|
|
146
|
+
timestamp: new Date().toISOString(),
|
|
147
|
+
keys_count: 0, // Unknown until decrypted
|
|
148
|
+
encrypted: true,
|
|
149
|
+
};
|
|
150
|
+
this.metadata[metadataKey] = metadata;
|
|
151
|
+
await this.saveMetadata();
|
|
145
152
|
}
|
|
146
153
|
}
|
|
147
154
|
catch (error) {
|
|
148
|
-
//
|
|
155
|
+
// History check failed, continue to error
|
|
149
156
|
const err = error;
|
|
150
|
-
logger.debug(`
|
|
157
|
+
logger.debug(` IPFS history check failed: ${err.message}`);
|
|
151
158
|
}
|
|
152
159
|
}
|
|
153
160
|
if (!metadata) {
|
|
154
161
|
throw new Error(`No secrets found for environment: ${displayEnv}\n\n` +
|
|
155
162
|
`š” Tip: Check available environments with: lsh env\n` +
|
|
156
|
-
` Or push secrets first with: lsh push`
|
|
157
|
-
|
|
158
|
-
// Check if there's a newer version in the registry (for git repos)
|
|
159
|
-
if (gitRepo) {
|
|
160
|
-
try {
|
|
161
|
-
const storacha = getStorachaClient();
|
|
162
|
-
if (storacha.isEnabled() && await storacha.isAuthenticated()) {
|
|
163
|
-
const latestCid = await storacha.getLatestCID(gitRepo);
|
|
164
|
-
if (latestCid && latestCid !== metadata.cid) {
|
|
165
|
-
logger.info(` š Found newer version in registry (CID: ${latestCid})`);
|
|
166
|
-
// Update metadata with latest CID
|
|
167
|
-
metadata = {
|
|
168
|
-
...metadata,
|
|
169
|
-
cid: latestCid,
|
|
170
|
-
timestamp: new Date().toISOString(),
|
|
171
|
-
};
|
|
172
|
-
this.metadata[metadataKey] = metadata;
|
|
173
|
-
await this.saveMetadata();
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
catch (error) {
|
|
178
|
-
// Registry check failed, continue with local metadata
|
|
179
|
-
const err = error;
|
|
180
|
-
logger.debug(` Registry check failed: ${err.message}`);
|
|
181
|
-
}
|
|
163
|
+
` Or push secrets first with: lsh push\n` +
|
|
164
|
+
` Or pull by CID with: lsh sync pull <cid>`);
|
|
182
165
|
}
|
|
183
166
|
// Try to load from local cache
|
|
184
167
|
let cachedData = await this.loadLocally(metadata.cid);
|
|
185
|
-
// If not in cache, try downloading from
|
|
168
|
+
// If not in cache, try downloading from IPFS
|
|
186
169
|
if (!cachedData) {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
170
|
+
const ipfsSync = getIPFSSync();
|
|
171
|
+
try {
|
|
172
|
+
logger.info(` š Downloading from IPFS...`);
|
|
173
|
+
const downloadedData = await ipfsSync.download(metadata.cid);
|
|
174
|
+
if (downloadedData) {
|
|
192
175
|
// Store in local cache for future use
|
|
193
176
|
await this.storeLocally(metadata.cid, downloadedData.toString('utf-8'), environment);
|
|
194
177
|
cachedData = downloadedData.toString('utf-8');
|
|
195
|
-
logger.info(` ā
Downloaded and cached from
|
|
196
|
-
}
|
|
197
|
-
catch (error) {
|
|
198
|
-
const err = error;
|
|
199
|
-
throw new Error(`Secrets not in cache and Storacha download failed: ${err.message}`);
|
|
178
|
+
logger.info(` ā
Downloaded and cached from IPFS`);
|
|
200
179
|
}
|
|
201
180
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
` export LSH_STORACHA_ENABLED=true\n` +
|
|
206
|
-
` Or set up Supabase: lsh supabase init`);
|
|
181
|
+
catch (error) {
|
|
182
|
+
const err = error;
|
|
183
|
+
logger.debug(` IPFS download failed: ${err.message}`);
|
|
207
184
|
}
|
|
208
185
|
}
|
|
209
|
-
// At this point cachedData is guaranteed to be a string
|
|
210
186
|
if (!cachedData) {
|
|
211
|
-
throw new Error(`
|
|
187
|
+
throw new Error(`Secrets not found in cache or IPFS. CID: ${metadata.cid}\n\n` +
|
|
188
|
+
`š” Tip: Start IPFS daemon: lsh ipfs start\n` +
|
|
189
|
+
` Or pull directly by CID: lsh sync pull <cid>`);
|
|
212
190
|
}
|
|
213
191
|
// Decrypt secrets
|
|
214
192
|
const secrets = this.decryptSecrets(cachedData, encryptionKey);
|
|
@@ -302,7 +280,7 @@ export class IPFSSecretsStorage {
|
|
|
302
280
|
throw new Error('Decryption failed. This usually means:\n' +
|
|
303
281
|
' 1. You need to set LSH_SECRETS_KEY environment variable\n' +
|
|
304
282
|
' 2. The key must match the one used during encryption\n' +
|
|
305
|
-
' 3. Generate a shared key with: lsh
|
|
283
|
+
' 3. Generate a shared key with: lsh key\n' +
|
|
306
284
|
' 4. Add it to your .env: LSH_SECRETS_KEY=<key>\n' +
|
|
307
285
|
'\nOriginal error: ' + err.message);
|
|
308
286
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* IPFS Sync Logger
|
|
3
|
-
* Records immutable sync operations to IPFS
|
|
3
|
+
* Records immutable sync operations to IPFS via native Kubo daemon
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
@@ -11,12 +11,11 @@ import { ENV_VARS } from '../constants/index.js';
|
|
|
11
11
|
/**
|
|
12
12
|
* IPFS Sync Logger
|
|
13
13
|
*
|
|
14
|
-
* Stores immutable sync records on IPFS
|
|
14
|
+
* Stores immutable sync records on IPFS via native daemon
|
|
15
15
|
*
|
|
16
16
|
* Features:
|
|
17
|
-
* - Zero-config: Works
|
|
17
|
+
* - Zero-config: Works with local IPFS daemon
|
|
18
18
|
* - Immutable: Content-addressed storage on IPFS
|
|
19
|
-
* - Free: 5GB storage forever via Storacha
|
|
20
19
|
* - Privacy: Only metadata stored, no secrets
|
|
21
20
|
* - Opt-out: Can be disabled via DISABLE_IPFS_SYNC config
|
|
22
21
|
*/
|
|
@@ -63,9 +62,8 @@ export class IPFSSyncLogger {
|
|
|
63
62
|
user: os.userInfo().username,
|
|
64
63
|
lsh_version: version,
|
|
65
64
|
};
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// In production, you'd upload to actual IPFS/Storacha
|
|
65
|
+
// Use file-based storage with IPFS-like CIDs
|
|
66
|
+
// Records are stored locally and can optionally be uploaded to IPFS
|
|
69
67
|
const cid = this.generateContentId(record);
|
|
70
68
|
const repoEnv = this.getRepoEnvKey(record.git_repo, record.environment);
|
|
71
69
|
// Store the record locally
|