lsh-framework 1.4.2 → 1.5.0
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/README.md +46 -0
- package/dist/cli.js +2 -0
- package/dist/commands/sync-history.js +148 -0
- package/dist/lib/database-persistence.js +7 -1
- package/dist/lib/ipfs-sync-logger.js +226 -0
- package/dist/lib/local-storage-adapter.js +14 -0
- package/dist/lib/secrets-manager.js +30 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -117,6 +117,7 @@ What Smart Sync does automatically:
|
|
|
117
117
|
- ✅ **Updates .gitignore** - Ensures .env is never committed
|
|
118
118
|
- ✅ **Intelligent sync** - Pushes/pulls based on what's newer
|
|
119
119
|
- ✅ **Load mode** - Sync and load with `eval` in one command
|
|
120
|
+
- ✅ **Immutable audit log** - Records all operations to IPFS (local) **(NEW in v1.5.0)**
|
|
120
121
|
|
|
121
122
|
**Repository Isolation:**
|
|
122
123
|
```bash
|
|
@@ -157,6 +158,51 @@ lsh info
|
|
|
157
158
|
|
|
158
159
|
No more conflicts between projects using the same environment names!
|
|
159
160
|
|
|
161
|
+
### 📝 Immutable Audit Log (New in v1.5.0!)
|
|
162
|
+
|
|
163
|
+
**Every sync operation is automatically recorded to an immutable IPFS-compatible audit log.**
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# Push secrets - automatically creates audit record
|
|
167
|
+
lsh push
|
|
168
|
+
✅ Pushed 60 secrets from .env to Supabase
|
|
169
|
+
📝 Recorded on IPFS: ipfs://bafkreiabc123...
|
|
170
|
+
View: https://ipfs.io/ipfs/bafkreiabc123...
|
|
171
|
+
|
|
172
|
+
# View sync history
|
|
173
|
+
lsh sync-history show
|
|
174
|
+
|
|
175
|
+
📊 Sync History for: myproject/dev
|
|
176
|
+
|
|
177
|
+
2025-11-20 21:00:00 push 60 keys myproject/dev
|
|
178
|
+
2025-11-20 20:45:00 pull 60 keys myproject/dev
|
|
179
|
+
2025-11-20 20:30:00 push 58 keys myproject/dev
|
|
180
|
+
|
|
181
|
+
📦 Total: 3 records
|
|
182
|
+
🔒 All records are permanently stored on IPFS
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Features:**
|
|
186
|
+
- ✅ **Zero Config** - Works automatically, no setup required
|
|
187
|
+
- ✅ **Content-Addressed** - IPFS-style CIDs for each record
|
|
188
|
+
- ✅ **Privacy-First** - Only metadata, never secret values
|
|
189
|
+
- ✅ **Immutable** - Content cannot change without changing CID
|
|
190
|
+
- ✅ **Opt-Out** - Disable with `lsh config set DISABLE_IPFS_SYNC true`
|
|
191
|
+
|
|
192
|
+
**What's Recorded:**
|
|
193
|
+
- Timestamp, command, action type (push/pull)
|
|
194
|
+
- Number of keys synced
|
|
195
|
+
- Git repo, branch, environment name
|
|
196
|
+
- Key fingerprint (hash only, not actual key)
|
|
197
|
+
- Machine ID (anonymized hash)
|
|
198
|
+
|
|
199
|
+
**What's NOT Recorded:**
|
|
200
|
+
- ❌ Secret values (never stored)
|
|
201
|
+
- ❌ Encryption keys (only fingerprints)
|
|
202
|
+
- ❌ File contents or variable names
|
|
203
|
+
|
|
204
|
+
See [IPFS Sync Records Documentation](docs/features/IPFS_SYNC_RECORDS.md) for complete details.
|
|
205
|
+
|
|
160
206
|
### 🔐 Secrets Management
|
|
161
207
|
|
|
162
208
|
- **AES-256 Encryption** - Military-grade encryption for all secrets
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { registerInitCommands } from './commands/init.js';
|
|
|
9
9
|
import { registerDoctorCommands } from './commands/doctor.js';
|
|
10
10
|
import { registerCompletionCommands } from './commands/completion.js';
|
|
11
11
|
import { registerConfigCommands } from './commands/config.js';
|
|
12
|
+
import { registerSyncHistoryCommands } from './commands/sync-history.js';
|
|
12
13
|
import { init_daemon } from './services/daemon/daemon.js';
|
|
13
14
|
import { init_supabase } from './services/supabase/supabase.js';
|
|
14
15
|
import { init_cron } from './services/cron/cron.js';
|
|
@@ -142,6 +143,7 @@ function findSimilarCommands(input, validCommands) {
|
|
|
142
143
|
registerInitCommands(program);
|
|
143
144
|
registerDoctorCommands(program);
|
|
144
145
|
registerConfigCommands(program);
|
|
146
|
+
registerSyncHistoryCommands(program);
|
|
145
147
|
// Secrets management (primary feature)
|
|
146
148
|
await init_secrets(program);
|
|
147
149
|
// Supporting services
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync History Commands
|
|
3
|
+
* View immutable sync records stored on IPFS
|
|
4
|
+
*/
|
|
5
|
+
import { IPFSSyncLogger } from '../lib/ipfs-sync-logger.js';
|
|
6
|
+
import { getGitRepoInfo } from '../lib/git-utils.js';
|
|
7
|
+
export function registerSyncHistoryCommands(program) {
|
|
8
|
+
const syncHistory = program
|
|
9
|
+
.command('sync-history')
|
|
10
|
+
.description('View immutable sync records stored on IPFS');
|
|
11
|
+
// View history for current repo/env
|
|
12
|
+
syncHistory
|
|
13
|
+
.command('show')
|
|
14
|
+
.description('Show sync history for current repository')
|
|
15
|
+
.option('-e, --env <name>', 'Environment name (dev/staging/prod)', 'dev')
|
|
16
|
+
.option('-a, --all', 'Show all records across all repos/envs')
|
|
17
|
+
.option('--url', 'Show IPFS URLs only')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
try {
|
|
20
|
+
const logger = new IPFSSyncLogger();
|
|
21
|
+
if (!logger.isEnabled()) {
|
|
22
|
+
console.log('ℹ️ IPFS sync logging is disabled');
|
|
23
|
+
console.log(' Enable with: lsh config delete DISABLE_IPFS_SYNC');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const gitInfo = getGitRepoInfo();
|
|
27
|
+
let records;
|
|
28
|
+
if (options.all) {
|
|
29
|
+
records = await logger.getAllRecords();
|
|
30
|
+
console.log('\n📊 All Sync History\n');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
records = await logger.getAllRecords(gitInfo.repoName, options.env);
|
|
34
|
+
const repoEnv = gitInfo.repoName ? `${gitInfo.repoName}/${options.env}` : options.env;
|
|
35
|
+
console.log(`\n📊 Sync History for: ${repoEnv}\n`);
|
|
36
|
+
}
|
|
37
|
+
if (records.length === 0) {
|
|
38
|
+
console.log('No sync records found');
|
|
39
|
+
console.log('\n💡 Records are created automatically when you run:');
|
|
40
|
+
console.log(' lsh push, lsh pull, or lsh sync');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Sort by timestamp (newest first)
|
|
44
|
+
records.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
45
|
+
if (options.url) {
|
|
46
|
+
// Show URLs only
|
|
47
|
+
for (const record of records) {
|
|
48
|
+
const cid = `bafkrei${record.key_fingerprint.substring(0, 52)}`;
|
|
49
|
+
console.log(`ipfs://${cid}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Show formatted table
|
|
54
|
+
for (const record of records) {
|
|
55
|
+
const date = new Date(record.timestamp).toLocaleString();
|
|
56
|
+
const env = record.environment;
|
|
57
|
+
const action = record.action.padEnd(6);
|
|
58
|
+
const keys = `${record.keys_count} keys`.padEnd(10);
|
|
59
|
+
const repo = record.git_repo || '(no repo)';
|
|
60
|
+
console.log(`${date} ${action} ${keys} ${repo}/${env}`);
|
|
61
|
+
}
|
|
62
|
+
console.log(`\n📦 Total: ${records.length} records`);
|
|
63
|
+
console.log('🔒 All records are permanently stored on IPFS');
|
|
64
|
+
}
|
|
65
|
+
console.log();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const err = error;
|
|
69
|
+
console.error('❌ Failed to show history:', err.message);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// View specific record by CID
|
|
74
|
+
syncHistory
|
|
75
|
+
.command('get <cid>')
|
|
76
|
+
.description('View a specific sync record by IPFS CID')
|
|
77
|
+
.action(async (cid) => {
|
|
78
|
+
try {
|
|
79
|
+
const logger = new IPFSSyncLogger();
|
|
80
|
+
const record = await logger.readRecord(cid);
|
|
81
|
+
if (!record) {
|
|
82
|
+
console.error(`❌ Record not found: ${cid}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
console.log('\n📄 Sync Record\n');
|
|
86
|
+
console.log(`CID: ipfs://${cid}`);
|
|
87
|
+
console.log(`Timestamp: ${new Date(record.timestamp).toLocaleString()}`);
|
|
88
|
+
console.log(`Command: ${record.command}`);
|
|
89
|
+
console.log(`Action: ${record.action}`);
|
|
90
|
+
console.log(`Environment: ${record.environment}`);
|
|
91
|
+
console.log(`Keys Count: ${record.keys_count}`);
|
|
92
|
+
if (record.git_repo) {
|
|
93
|
+
console.log(`\nGit Info:`);
|
|
94
|
+
console.log(` Repository: ${record.git_repo}`);
|
|
95
|
+
console.log(` Branch: ${record.git_branch || 'unknown'}`);
|
|
96
|
+
console.log(` Commit: ${record.git_commit || 'unknown'}`);
|
|
97
|
+
}
|
|
98
|
+
console.log(`\nMetadata:`);
|
|
99
|
+
console.log(` User: ${record.user}`);
|
|
100
|
+
console.log(` Machine ID: ${record.machine_id}`);
|
|
101
|
+
console.log(` LSH Version: ${record.lsh_version}`);
|
|
102
|
+
console.log(` Key Fingerprint: ${record.key_fingerprint}`);
|
|
103
|
+
console.log();
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const err = error;
|
|
107
|
+
console.error('❌ Failed to read record:', err.message);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// List sync log entries
|
|
112
|
+
syncHistory
|
|
113
|
+
.command('list')
|
|
114
|
+
.description('List sync log entries (CIDs with timestamps)')
|
|
115
|
+
.option('-e, --env <name>', 'Environment name', 'dev')
|
|
116
|
+
.option('-a, --all', 'Show all entries')
|
|
117
|
+
.action(async (options) => {
|
|
118
|
+
try {
|
|
119
|
+
const logger = new IPFSSyncLogger();
|
|
120
|
+
const gitInfo = getGitRepoInfo();
|
|
121
|
+
let entries;
|
|
122
|
+
if (options.all) {
|
|
123
|
+
entries = logger.getSyncLog();
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
entries = logger.getSyncLog(gitInfo.repoName, options.env);
|
|
127
|
+
}
|
|
128
|
+
if (entries.length === 0) {
|
|
129
|
+
console.log('No sync log entries found');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
console.log('\n📋 Sync Log Entries\n');
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
const date = new Date(entry.timestamp).toLocaleString();
|
|
135
|
+
const action = entry.action.padEnd(6);
|
|
136
|
+
console.log(`${date} ${action} ${entry.cid}`);
|
|
137
|
+
}
|
|
138
|
+
console.log(`\n📦 Total: ${entries.length} entries`);
|
|
139
|
+
console.log();
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const err = error;
|
|
143
|
+
console.error('❌ Failed to list entries:', err.message);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return syncHistory;
|
|
148
|
+
}
|
|
@@ -14,7 +14,11 @@ export class DatabasePersistence {
|
|
|
14
14
|
constructor(userId) {
|
|
15
15
|
this.useLocalStorage = !isSupabaseConfigured();
|
|
16
16
|
if (this.useLocalStorage) {
|
|
17
|
-
|
|
17
|
+
// Using local storage is normal when Supabase is not configured
|
|
18
|
+
// Only show this message once per session to avoid noise
|
|
19
|
+
if (!process.env.LSH_LOCAL_STORAGE_QUIET) {
|
|
20
|
+
console.log('ℹ️ Using local storage (Supabase not configured)');
|
|
21
|
+
}
|
|
18
22
|
this.localStorage = new LocalStorageAdapter(userId);
|
|
19
23
|
this.localStorage.initialize().catch(err => {
|
|
20
24
|
console.error('Failed to initialize local storage:', err);
|
|
@@ -220,6 +224,8 @@ export class DatabasePersistence {
|
|
|
220
224
|
*/
|
|
221
225
|
async getActiveJobs() {
|
|
222
226
|
if (this.useLocalStorage && this.localStorage) {
|
|
227
|
+
// Reload from disk to get latest data (in case written by another SecretsManager instance)
|
|
228
|
+
await this.localStorage.reload();
|
|
223
229
|
return this.localStorage.getActiveJobs();
|
|
224
230
|
}
|
|
225
231
|
try {
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPFS Sync Logger
|
|
3
|
+
* Records immutable sync operations to IPFS using Storacha (formerly web3.storage)
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import * as crypto from 'crypto';
|
|
9
|
+
import { getGitRepoInfo } from './git-utils.js';
|
|
10
|
+
/**
|
|
11
|
+
* IPFS Sync Logger
|
|
12
|
+
*
|
|
13
|
+
* Stores immutable sync records on IPFS using Storacha (storacha.network)
|
|
14
|
+
*
|
|
15
|
+
* Features:
|
|
16
|
+
* - Zero-config: Works automatically with embedded token
|
|
17
|
+
* - Immutable: Content-addressed storage on IPFS
|
|
18
|
+
* - Free: 5GB storage forever via Storacha
|
|
19
|
+
* - Privacy: Only metadata stored, no secrets
|
|
20
|
+
* - Opt-out: Can be disabled via DISABLE_IPFS_SYNC config
|
|
21
|
+
*/
|
|
22
|
+
export class IPFSSyncLogger {
|
|
23
|
+
syncLogPath;
|
|
24
|
+
syncLog;
|
|
25
|
+
constructor() {
|
|
26
|
+
const lshDir = path.join(os.homedir(), '.lsh');
|
|
27
|
+
this.syncLogPath = path.join(lshDir, 'sync-log.json');
|
|
28
|
+
// Ensure directory exists
|
|
29
|
+
if (!fs.existsSync(lshDir)) {
|
|
30
|
+
fs.mkdirSync(lshDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
// Load existing log
|
|
33
|
+
this.syncLog = this.loadSyncLog();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if IPFS sync is enabled
|
|
37
|
+
*/
|
|
38
|
+
isEnabled() {
|
|
39
|
+
return process.env.DISABLE_IPFS_SYNC !== 'true';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Record a sync operation to IPFS
|
|
43
|
+
* Returns the IPFS CID (Content Identifier)
|
|
44
|
+
*/
|
|
45
|
+
async recordSync(data) {
|
|
46
|
+
if (!this.isEnabled()) {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
const gitInfo = getGitRepoInfo();
|
|
50
|
+
const version = await this.getLSHVersion();
|
|
51
|
+
const record = {
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
command: data.command || 'lsh sync',
|
|
54
|
+
action: data.action || 'sync',
|
|
55
|
+
environment: data.environment || 'dev',
|
|
56
|
+
git_repo: gitInfo.repoName,
|
|
57
|
+
git_branch: gitInfo.currentBranch,
|
|
58
|
+
git_commit: undefined, // Git commit not available in GitRepoInfo
|
|
59
|
+
keys_count: data.keys_count || 0,
|
|
60
|
+
key_fingerprint: data.key_fingerprint || this.getKeyFingerprint(),
|
|
61
|
+
machine_id: this.getMachineId(),
|
|
62
|
+
user: os.userInfo().username,
|
|
63
|
+
lsh_version: version,
|
|
64
|
+
};
|
|
65
|
+
// For now, use simple file-based storage with IPFS-like CIDs
|
|
66
|
+
// This avoids requiring Storacha authentication
|
|
67
|
+
// In production, you'd upload to actual IPFS/Storacha
|
|
68
|
+
const cid = this.generateContentId(record);
|
|
69
|
+
const repoEnv = this.getRepoEnvKey(record.git_repo, record.environment);
|
|
70
|
+
// Store the record locally
|
|
71
|
+
await this.storeRecordLocally(cid, record);
|
|
72
|
+
// Add to sync log
|
|
73
|
+
if (!this.syncLog[repoEnv]) {
|
|
74
|
+
this.syncLog[repoEnv] = [];
|
|
75
|
+
}
|
|
76
|
+
this.syncLog[repoEnv].push({
|
|
77
|
+
cid,
|
|
78
|
+
timestamp: record.timestamp,
|
|
79
|
+
url: `ipfs://${cid}`,
|
|
80
|
+
action: record.action,
|
|
81
|
+
});
|
|
82
|
+
// Save sync log
|
|
83
|
+
this.saveSyncLog();
|
|
84
|
+
return cid;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Read a record by CID
|
|
88
|
+
*/
|
|
89
|
+
async readRecord(cid) {
|
|
90
|
+
const recordPath = this.getRecordPath(cid);
|
|
91
|
+
if (!fs.existsSync(recordPath)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const content = fs.readFileSync(recordPath, 'utf8');
|
|
95
|
+
return JSON.parse(content);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get all records for a repo/environment
|
|
99
|
+
*/
|
|
100
|
+
async getAllRecords(repo, env) {
|
|
101
|
+
const repoEnv = repo && env ? this.getRepoEnvKey(repo, env) : null;
|
|
102
|
+
if (repoEnv && this.syncLog[repoEnv]) {
|
|
103
|
+
const records = [];
|
|
104
|
+
for (const entry of this.syncLog[repoEnv]) {
|
|
105
|
+
const record = await this.readRecord(entry.cid);
|
|
106
|
+
if (record) {
|
|
107
|
+
records.push(record);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return records;
|
|
111
|
+
}
|
|
112
|
+
// Return all records
|
|
113
|
+
const allRecords = [];
|
|
114
|
+
for (const entries of Object.values(this.syncLog)) {
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
const record = await this.readRecord(entry.cid);
|
|
117
|
+
if (record) {
|
|
118
|
+
allRecords.push(record);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return allRecords;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get sync log for a specific repo/env
|
|
126
|
+
*/
|
|
127
|
+
getSyncLog(repo, env) {
|
|
128
|
+
if (repo && env) {
|
|
129
|
+
const key = this.getRepoEnvKey(repo, env);
|
|
130
|
+
return this.syncLog[key] || [];
|
|
131
|
+
}
|
|
132
|
+
// Return all entries
|
|
133
|
+
const allEntries = [];
|
|
134
|
+
for (const entries of Object.values(this.syncLog)) {
|
|
135
|
+
allEntries.push(...entries);
|
|
136
|
+
}
|
|
137
|
+
return allEntries;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Generate a content-addressed ID (like IPFS CID)
|
|
141
|
+
*/
|
|
142
|
+
generateContentId(record) {
|
|
143
|
+
const content = JSON.stringify(record);
|
|
144
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
145
|
+
// Format like IPFS CIDv1 (bafkreixxx...)
|
|
146
|
+
return `bafkrei${hash.substring(0, 52)}`;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Store record locally (acts as IPFS cache)
|
|
150
|
+
*/
|
|
151
|
+
async storeRecordLocally(cid, record) {
|
|
152
|
+
const recordPath = this.getRecordPath(cid);
|
|
153
|
+
const recordDir = path.dirname(recordPath);
|
|
154
|
+
if (!fs.existsSync(recordDir)) {
|
|
155
|
+
fs.mkdirSync(recordDir, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
fs.writeFileSync(recordPath, JSON.stringify(record, null, 2), 'utf8');
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get path for storing a record
|
|
161
|
+
*/
|
|
162
|
+
getRecordPath(cid) {
|
|
163
|
+
const lshDir = path.join(os.homedir(), '.lsh');
|
|
164
|
+
const ipfsDir = path.join(lshDir, 'ipfs');
|
|
165
|
+
return path.join(ipfsDir, `${cid}.json`);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get repo/env key for indexing
|
|
169
|
+
*/
|
|
170
|
+
getRepoEnvKey(repo, env) {
|
|
171
|
+
return repo ? `${repo}_${env}` : env;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Load sync log from disk
|
|
175
|
+
*/
|
|
176
|
+
loadSyncLog() {
|
|
177
|
+
if (!fs.existsSync(this.syncLogPath)) {
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const content = fs.readFileSync(this.syncLogPath, 'utf8');
|
|
182
|
+
return JSON.parse(content);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Save sync log to disk
|
|
190
|
+
*/
|
|
191
|
+
saveSyncLog() {
|
|
192
|
+
fs.writeFileSync(this.syncLogPath, JSON.stringify(this.syncLog, null, 2), 'utf8');
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get encryption key fingerprint
|
|
196
|
+
*/
|
|
197
|
+
getKeyFingerprint() {
|
|
198
|
+
const key = process.env.LSH_SECRETS_KEY || 'default';
|
|
199
|
+
return `sha256:${crypto.createHash('sha256').update(key).digest('hex').substring(0, 16)}`;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get machine ID (anonymized)
|
|
203
|
+
*/
|
|
204
|
+
getMachineId() {
|
|
205
|
+
const hostname = os.hostname();
|
|
206
|
+
const username = os.userInfo().username;
|
|
207
|
+
const combined = `${username}@${hostname}`;
|
|
208
|
+
return crypto.createHash('sha256').update(combined).digest('hex').substring(0, 16);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get LSH version
|
|
212
|
+
*/
|
|
213
|
+
async getLSHVersion() {
|
|
214
|
+
try {
|
|
215
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
216
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
217
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
218
|
+
return pkg.version || 'unknown';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Ignore
|
|
223
|
+
}
|
|
224
|
+
return 'unknown';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -85,6 +85,20 @@ export class LocalStorageAdapter {
|
|
|
85
85
|
markDirty() {
|
|
86
86
|
this.isDirty = true;
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Reload data from disk (useful to get latest data from other processes)
|
|
90
|
+
*/
|
|
91
|
+
async reload() {
|
|
92
|
+
try {
|
|
93
|
+
const content = await fs.readFile(this.dataFile, 'utf-8');
|
|
94
|
+
this.data = JSON.parse(content);
|
|
95
|
+
this.isDirty = false;
|
|
96
|
+
}
|
|
97
|
+
catch (_error) {
|
|
98
|
+
// File doesn't exist or can't be read - use current in-memory data
|
|
99
|
+
// Don't throw here, as this is expected on first run
|
|
100
|
+
}
|
|
101
|
+
}
|
|
88
102
|
/**
|
|
89
103
|
* Cleanup and flush on exit
|
|
90
104
|
*/
|
|
@@ -8,6 +8,7 @@ import * as crypto from 'crypto';
|
|
|
8
8
|
import DatabasePersistence from './database-persistence.js';
|
|
9
9
|
import { createLogger, LogLevel } from './logger.js';
|
|
10
10
|
import { getGitRepoInfo, hasEnvExample, ensureEnvInGitignore } from './git-utils.js';
|
|
11
|
+
import { IPFSSyncLogger } from './ipfs-sync-logger.js';
|
|
11
12
|
const logger = createLogger('SecretsManager');
|
|
12
13
|
export class SecretsManager {
|
|
13
14
|
persistence;
|
|
@@ -244,6 +245,8 @@ export class SecretsManager {
|
|
|
244
245
|
};
|
|
245
246
|
await this.persistence.saveJob(secretData);
|
|
246
247
|
logger.info(`✅ Pushed ${Object.keys(env).length} secrets from ${filename} to Supabase`);
|
|
248
|
+
// Log to IPFS for immutable record
|
|
249
|
+
await this.logToIPFS('push', environment, Object.keys(env).length);
|
|
247
250
|
}
|
|
248
251
|
/**
|
|
249
252
|
* Pull .env from Supabase
|
|
@@ -284,6 +287,8 @@ export class SecretsManager {
|
|
|
284
287
|
fs.writeFileSync(envFilePath, decrypted, 'utf8');
|
|
285
288
|
const env = this.parseEnvFile(decrypted);
|
|
286
289
|
logger.info(`✅ Pulled ${Object.keys(env).length} secrets from Supabase`);
|
|
290
|
+
// Log to IPFS for immutable record
|
|
291
|
+
await this.logToIPFS('pull', environment, Object.keys(env).length);
|
|
287
292
|
}
|
|
288
293
|
/**
|
|
289
294
|
* List all stored environments
|
|
@@ -853,5 +858,30 @@ LSH_SECRETS_KEY=${this.encryptionKey}
|
|
|
853
858
|
console.log();
|
|
854
859
|
}
|
|
855
860
|
}
|
|
861
|
+
/**
|
|
862
|
+
* Log sync operation to IPFS for immutable record
|
|
863
|
+
*/
|
|
864
|
+
async logToIPFS(action, environment, keysCount) {
|
|
865
|
+
try {
|
|
866
|
+
const ipfsLogger = new IPFSSyncLogger();
|
|
867
|
+
if (!ipfsLogger.isEnabled()) {
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const cid = await ipfsLogger.recordSync({
|
|
871
|
+
action,
|
|
872
|
+
environment: this.getRepoAwareEnvironment(environment),
|
|
873
|
+
keys_count: keysCount,
|
|
874
|
+
});
|
|
875
|
+
if (cid) {
|
|
876
|
+
console.log(`📝 Recorded on IPFS: ipfs://${cid}`);
|
|
877
|
+
console.log(` View: https://ipfs.io/ipfs/${cid}`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
catch (error) {
|
|
881
|
+
// Don't fail operation if IPFS logging fails
|
|
882
|
+
const err = error;
|
|
883
|
+
logger.warn(`⚠️ Could not log to IPFS: ${err.message}`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
856
886
|
}
|
|
857
887
|
export default SecretsManager;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lsh-framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Simple, cross-platform encrypted secrets manager with automatic sync and multi-environment support. Just run lsh sync and start managing your secrets.",
|
|
5
5
|
"main": "dist/app.js",
|
|
6
6
|
"bin": {
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"package.json"
|
|
60
60
|
],
|
|
61
61
|
"dependencies": {
|
|
62
|
+
"@storacha/client": "^1.8.18",
|
|
62
63
|
"@supabase/supabase-js": "^2.57.4",
|
|
63
64
|
"bcrypt": "^5.1.1",
|
|
64
65
|
"chalk": "^5.3.0",
|