lsh-framework 1.3.2 → 1.4.1
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/.env.example +43 -3
- package/README.md +25 -4
- package/dist/cli.js +6 -0
- package/dist/commands/config.js +240 -0
- package/dist/daemon/saas-api-routes.js +778 -0
- package/dist/daemon/saas-api-server.js +225 -0
- package/dist/lib/config-manager.js +321 -0
- package/dist/lib/database-persistence.js +75 -3
- package/dist/lib/env-validator.js +17 -0
- package/dist/lib/local-storage-adapter.js +493 -0
- package/dist/lib/saas-audit.js +213 -0
- package/dist/lib/saas-auth.js +427 -0
- package/dist/lib/saas-billing.js +402 -0
- package/dist/lib/saas-email.js +402 -0
- package/dist/lib/saas-encryption.js +220 -0
- package/dist/lib/saas-organizations.js +592 -0
- package/dist/lib/saas-secrets.js +378 -0
- package/dist/lib/saas-types.js +108 -0
- package/dist/lib/supabase-client.js +77 -11
- package/package.json +13 -2
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local File-Based Storage Adapter
|
|
3
|
+
* Provides persistence when Supabase/PostgreSQL is not available
|
|
4
|
+
* Uses JSON files for storage - suitable for development and single-user deployments
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs/promises';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
/**
|
|
10
|
+
* Local file-based storage adapter
|
|
11
|
+
* Implements same interface as DatabasePersistence but uses local JSON files
|
|
12
|
+
*/
|
|
13
|
+
export class LocalStorageAdapter {
|
|
14
|
+
dataDir;
|
|
15
|
+
dataFile;
|
|
16
|
+
data;
|
|
17
|
+
userId;
|
|
18
|
+
sessionId;
|
|
19
|
+
autoFlush;
|
|
20
|
+
flushInterval;
|
|
21
|
+
isDirty = false;
|
|
22
|
+
constructor(userId, config = {}) {
|
|
23
|
+
this.userId = userId;
|
|
24
|
+
this.sessionId = this.generateSessionId();
|
|
25
|
+
this.dataDir = config.dataDir || path.join(os.homedir(), '.lsh', 'data');
|
|
26
|
+
this.dataFile = path.join(this.dataDir, 'storage.json');
|
|
27
|
+
this.autoFlush = config.autoFlush !== false; // default true
|
|
28
|
+
// Initialize empty data structure
|
|
29
|
+
this.data = {
|
|
30
|
+
shell_history: [],
|
|
31
|
+
shell_jobs: [],
|
|
32
|
+
shell_configuration: [],
|
|
33
|
+
shell_sessions: [],
|
|
34
|
+
shell_aliases: [],
|
|
35
|
+
shell_functions: [],
|
|
36
|
+
shell_completions: [],
|
|
37
|
+
};
|
|
38
|
+
// Start auto-flush if enabled
|
|
39
|
+
if (this.autoFlush) {
|
|
40
|
+
const interval = config.flushInterval || 5000; // default 5s
|
|
41
|
+
this.flushInterval = setInterval(() => this.flush(), interval);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Initialize storage directory and load existing data
|
|
46
|
+
*/
|
|
47
|
+
async initialize() {
|
|
48
|
+
try {
|
|
49
|
+
// Create data directory if it doesn't exist
|
|
50
|
+
await fs.mkdir(this.dataDir, { recursive: true });
|
|
51
|
+
// Load existing data if file exists
|
|
52
|
+
try {
|
|
53
|
+
const content = await fs.readFile(this.dataFile, 'utf-8');
|
|
54
|
+
this.data = JSON.parse(content);
|
|
55
|
+
}
|
|
56
|
+
catch (_error) {
|
|
57
|
+
// File doesn't exist yet, use empty data
|
|
58
|
+
await this.flush();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error('Failed to initialize local storage:', error);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Flush in-memory data to disk
|
|
68
|
+
*/
|
|
69
|
+
async flush() {
|
|
70
|
+
if (!this.isDirty) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await fs.writeFile(this.dataFile, JSON.stringify(this.data, null, 2), 'utf-8');
|
|
75
|
+
this.isDirty = false;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error('Failed to flush data to disk:', error);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Mark data as dirty (needs flush)
|
|
84
|
+
*/
|
|
85
|
+
markDirty() {
|
|
86
|
+
this.isDirty = true;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Cleanup and flush on exit
|
|
90
|
+
*/
|
|
91
|
+
async cleanup() {
|
|
92
|
+
if (this.flushInterval) {
|
|
93
|
+
clearInterval(this.flushInterval);
|
|
94
|
+
}
|
|
95
|
+
await this.flush();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Generate a unique session ID
|
|
99
|
+
*/
|
|
100
|
+
generateSessionId() {
|
|
101
|
+
return `lsh_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Generate a unique ID
|
|
105
|
+
*/
|
|
106
|
+
generateId() {
|
|
107
|
+
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Save shell history entry
|
|
111
|
+
*/
|
|
112
|
+
async saveHistoryEntry(entry) {
|
|
113
|
+
try {
|
|
114
|
+
const newEntry = {
|
|
115
|
+
...entry,
|
|
116
|
+
id: this.generateId(),
|
|
117
|
+
user_id: this.userId,
|
|
118
|
+
session_id: this.sessionId,
|
|
119
|
+
created_at: new Date().toISOString(),
|
|
120
|
+
updated_at: new Date().toISOString(),
|
|
121
|
+
};
|
|
122
|
+
this.data.shell_history.push(newEntry);
|
|
123
|
+
this.markDirty();
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error('Error saving history entry:', error);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get shell history entries
|
|
133
|
+
*/
|
|
134
|
+
async getHistoryEntries(limit = 100, offset = 0) {
|
|
135
|
+
try {
|
|
136
|
+
const filtered = this.data.shell_history.filter(entry => this.userId ? entry.user_id === this.userId : entry.user_id === undefined || entry.user_id === null);
|
|
137
|
+
// Sort by timestamp descending
|
|
138
|
+
filtered.sort((a, b) => {
|
|
139
|
+
const timeA = new Date(a.timestamp).getTime();
|
|
140
|
+
const timeB = new Date(b.timestamp).getTime();
|
|
141
|
+
return timeB - timeA;
|
|
142
|
+
});
|
|
143
|
+
return filtered.slice(offset, offset + limit);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error('Error getting history entries:', error);
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Save shell job
|
|
152
|
+
*/
|
|
153
|
+
async saveJob(job) {
|
|
154
|
+
try {
|
|
155
|
+
const newJob = {
|
|
156
|
+
...job,
|
|
157
|
+
id: this.generateId(),
|
|
158
|
+
user_id: this.userId,
|
|
159
|
+
session_id: this.sessionId,
|
|
160
|
+
created_at: new Date().toISOString(),
|
|
161
|
+
updated_at: new Date().toISOString(),
|
|
162
|
+
};
|
|
163
|
+
this.data.shell_jobs.push(newJob);
|
|
164
|
+
this.markDirty();
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error('Error saving job:', error);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Update shell job status
|
|
174
|
+
*/
|
|
175
|
+
async updateJobStatus(jobId, status, exitCode) {
|
|
176
|
+
try {
|
|
177
|
+
const job = this.data.shell_jobs.find(j => j.job_id === jobId &&
|
|
178
|
+
(this.userId ? j.user_id === this.userId : j.user_id === undefined || j.user_id === null));
|
|
179
|
+
if (!job) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
job.status = status;
|
|
183
|
+
job.updated_at = new Date().toISOString();
|
|
184
|
+
if (status === 'completed' || status === 'failed') {
|
|
185
|
+
job.completed_at = new Date().toISOString();
|
|
186
|
+
if (exitCode !== undefined) {
|
|
187
|
+
job.exit_code = exitCode;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
this.markDirty();
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.error('Error updating job status:', error);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get active jobs
|
|
200
|
+
*/
|
|
201
|
+
async getActiveJobs() {
|
|
202
|
+
try {
|
|
203
|
+
return this.data.shell_jobs
|
|
204
|
+
.filter(job => ['running', 'stopped', 'completed', 'failed'].includes(job.status) &&
|
|
205
|
+
(this.userId ? job.user_id === this.userId : job.user_id === undefined || job.user_id === null))
|
|
206
|
+
.sort((a, b) => {
|
|
207
|
+
const timeA = new Date(a.created_at || 0).getTime();
|
|
208
|
+
const timeB = new Date(b.created_at || 0).getTime();
|
|
209
|
+
return timeB - timeA;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
console.error('Error getting active jobs:', error);
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Save shell configuration
|
|
219
|
+
*/
|
|
220
|
+
async saveConfiguration(config) {
|
|
221
|
+
try {
|
|
222
|
+
// Find existing config
|
|
223
|
+
const existingIndex = this.data.shell_configuration.findIndex(c => c.user_id === (this.userId || null) &&
|
|
224
|
+
c.config_key === config.config_key);
|
|
225
|
+
const newConfig = {
|
|
226
|
+
...config,
|
|
227
|
+
id: existingIndex >= 0 ? this.data.shell_configuration[existingIndex].id : this.generateId(),
|
|
228
|
+
user_id: this.userId,
|
|
229
|
+
created_at: existingIndex >= 0 ? this.data.shell_configuration[existingIndex].created_at : new Date().toISOString(),
|
|
230
|
+
updated_at: new Date().toISOString(),
|
|
231
|
+
};
|
|
232
|
+
if (existingIndex >= 0) {
|
|
233
|
+
this.data.shell_configuration[existingIndex] = newConfig;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
this.data.shell_configuration.push(newConfig);
|
|
237
|
+
}
|
|
238
|
+
this.markDirty();
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
console.error('Error saving configuration:', error);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Get shell configuration
|
|
248
|
+
*/
|
|
249
|
+
async getConfiguration(key) {
|
|
250
|
+
try {
|
|
251
|
+
let filtered = this.data.shell_configuration.filter(config => this.userId ? config.user_id === this.userId : config.user_id === undefined || config.user_id === null);
|
|
252
|
+
if (key) {
|
|
253
|
+
filtered = filtered.filter(config => config.config_key === key);
|
|
254
|
+
}
|
|
255
|
+
return filtered;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
console.error('Error getting configuration:', error);
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Save shell alias
|
|
264
|
+
*/
|
|
265
|
+
async saveAlias(alias) {
|
|
266
|
+
try {
|
|
267
|
+
const existingIndex = this.data.shell_aliases.findIndex(a => a.user_id === (this.userId || null) &&
|
|
268
|
+
a.alias_name === alias.alias_name);
|
|
269
|
+
const newAlias = {
|
|
270
|
+
...alias,
|
|
271
|
+
id: existingIndex >= 0 ? this.data.shell_aliases[existingIndex].id : this.generateId(),
|
|
272
|
+
user_id: this.userId,
|
|
273
|
+
created_at: existingIndex >= 0 ? this.data.shell_aliases[existingIndex].created_at : new Date().toISOString(),
|
|
274
|
+
updated_at: new Date().toISOString(),
|
|
275
|
+
};
|
|
276
|
+
if (existingIndex >= 0) {
|
|
277
|
+
this.data.shell_aliases[existingIndex] = newAlias;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
this.data.shell_aliases.push(newAlias);
|
|
281
|
+
}
|
|
282
|
+
this.markDirty();
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
console.error('Error saving alias:', error);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Get shell aliases
|
|
292
|
+
*/
|
|
293
|
+
async getAliases() {
|
|
294
|
+
try {
|
|
295
|
+
return this.data.shell_aliases.filter(alias => alias.is_active &&
|
|
296
|
+
(this.userId ? alias.user_id === this.userId : alias.user_id === undefined || alias.user_id === null));
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
console.error('Error getting aliases:', error);
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Save shell function
|
|
305
|
+
*/
|
|
306
|
+
async saveFunction(func) {
|
|
307
|
+
try {
|
|
308
|
+
const existingIndex = this.data.shell_functions.findIndex(f => f.user_id === (this.userId || null) &&
|
|
309
|
+
f.function_name === func.function_name);
|
|
310
|
+
const newFunc = {
|
|
311
|
+
...func,
|
|
312
|
+
id: existingIndex >= 0 ? this.data.shell_functions[existingIndex].id : this.generateId(),
|
|
313
|
+
user_id: this.userId,
|
|
314
|
+
created_at: existingIndex >= 0 ? this.data.shell_functions[existingIndex].created_at : new Date().toISOString(),
|
|
315
|
+
updated_at: new Date().toISOString(),
|
|
316
|
+
};
|
|
317
|
+
if (existingIndex >= 0) {
|
|
318
|
+
this.data.shell_functions[existingIndex] = newFunc;
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
this.data.shell_functions.push(newFunc);
|
|
322
|
+
}
|
|
323
|
+
this.markDirty();
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
console.error('Error saving function:', error);
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Get shell functions
|
|
333
|
+
*/
|
|
334
|
+
async getFunctions() {
|
|
335
|
+
try {
|
|
336
|
+
return this.data.shell_functions.filter(func => func.is_active &&
|
|
337
|
+
(this.userId ? func.user_id === this.userId : func.user_id === undefined || func.user_id === null));
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
console.error('Error getting functions:', error);
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Start a new shell session
|
|
346
|
+
*/
|
|
347
|
+
async startSession(workingDirectory, environmentVariables) {
|
|
348
|
+
try {
|
|
349
|
+
const newSession = {
|
|
350
|
+
id: this.generateId(),
|
|
351
|
+
user_id: this.userId,
|
|
352
|
+
session_id: this.sessionId,
|
|
353
|
+
hostname: os.hostname(),
|
|
354
|
+
working_directory: workingDirectory,
|
|
355
|
+
environment_variables: environmentVariables,
|
|
356
|
+
started_at: new Date().toISOString(),
|
|
357
|
+
is_active: true,
|
|
358
|
+
created_at: new Date().toISOString(),
|
|
359
|
+
updated_at: new Date().toISOString(),
|
|
360
|
+
};
|
|
361
|
+
this.data.shell_sessions.push(newSession);
|
|
362
|
+
this.markDirty();
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
console.error('Error starting session:', error);
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* End the current shell session
|
|
372
|
+
*/
|
|
373
|
+
async endSession() {
|
|
374
|
+
try {
|
|
375
|
+
const session = this.data.shell_sessions.find(s => s.session_id === this.sessionId &&
|
|
376
|
+
(this.userId ? s.user_id === this.userId : s.user_id === undefined || s.user_id === null));
|
|
377
|
+
if (!session) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
session.ended_at = new Date().toISOString();
|
|
381
|
+
session.is_active = false;
|
|
382
|
+
session.updated_at = new Date().toISOString();
|
|
383
|
+
this.markDirty();
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
console.error('Error ending session:', error);
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Test storage connectivity (always succeeds for local storage)
|
|
393
|
+
*/
|
|
394
|
+
async testConnection() {
|
|
395
|
+
try {
|
|
396
|
+
await fs.access(this.dataDir);
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
catch (_error) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Get session ID
|
|
405
|
+
*/
|
|
406
|
+
getSessionId() {
|
|
407
|
+
return this.sessionId;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Get latest rows from all tables
|
|
411
|
+
*/
|
|
412
|
+
async getLatestRows(limit = 5) {
|
|
413
|
+
const result = {};
|
|
414
|
+
try {
|
|
415
|
+
// Get latest shell history entries
|
|
416
|
+
const history = this.data.shell_history
|
|
417
|
+
.filter(entry => this.userId ? entry.user_id === this.userId : entry.user_id === undefined || entry.user_id === null)
|
|
418
|
+
.sort((a, b) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime())
|
|
419
|
+
.slice(0, limit);
|
|
420
|
+
result.shell_history = history;
|
|
421
|
+
// Get latest shell jobs
|
|
422
|
+
const jobs = this.data.shell_jobs
|
|
423
|
+
.filter(job => this.userId ? job.user_id === this.userId : job.user_id === undefined || job.user_id === null)
|
|
424
|
+
.sort((a, b) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime())
|
|
425
|
+
.slice(0, limit);
|
|
426
|
+
result.shell_jobs = jobs;
|
|
427
|
+
// Get latest shell configuration
|
|
428
|
+
const config = this.data.shell_configuration
|
|
429
|
+
.filter(cfg => this.userId ? cfg.user_id === this.userId : cfg.user_id === undefined || cfg.user_id === null)
|
|
430
|
+
.sort((a, b) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime())
|
|
431
|
+
.slice(0, limit);
|
|
432
|
+
result.shell_configuration = config;
|
|
433
|
+
// Get latest shell sessions
|
|
434
|
+
const sessions = this.data.shell_sessions
|
|
435
|
+
.filter(session => this.userId ? session.user_id === this.userId : session.user_id === undefined || session.user_id === null)
|
|
436
|
+
.sort((a, b) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime())
|
|
437
|
+
.slice(0, limit);
|
|
438
|
+
result.shell_sessions = sessions;
|
|
439
|
+
// Get latest shell aliases
|
|
440
|
+
const aliases = this.data.shell_aliases
|
|
441
|
+
.filter(alias => this.userId ? alias.user_id === this.userId : alias.user_id === undefined || alias.user_id === null)
|
|
442
|
+
.sort((a, b) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime())
|
|
443
|
+
.slice(0, limit);
|
|
444
|
+
result.shell_aliases = aliases;
|
|
445
|
+
// Get latest shell functions
|
|
446
|
+
const functions = this.data.shell_functions
|
|
447
|
+
.filter(func => this.userId ? func.user_id === this.userId : func.user_id === undefined || func.user_id === null)
|
|
448
|
+
.sort((a, b) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime())
|
|
449
|
+
.slice(0, limit);
|
|
450
|
+
result.shell_functions = functions;
|
|
451
|
+
// Get latest shell completions
|
|
452
|
+
const completions = this.data.shell_completions
|
|
453
|
+
.filter(comp => this.userId ? comp.user_id === this.userId : comp.user_id === undefined || comp.user_id === null)
|
|
454
|
+
.sort((a, b) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime())
|
|
455
|
+
.slice(0, limit);
|
|
456
|
+
result.shell_completions = completions;
|
|
457
|
+
return result;
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
console.error('Error getting latest rows:', error);
|
|
461
|
+
return {};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Get latest rows from a specific table
|
|
466
|
+
*/
|
|
467
|
+
async getLatestRowsFromTable(tableName, limit = 5) {
|
|
468
|
+
try {
|
|
469
|
+
const validTables = [
|
|
470
|
+
'shell_history',
|
|
471
|
+
'shell_jobs',
|
|
472
|
+
'shell_configuration',
|
|
473
|
+
'shell_sessions',
|
|
474
|
+
'shell_aliases',
|
|
475
|
+
'shell_functions',
|
|
476
|
+
'shell_completions',
|
|
477
|
+
];
|
|
478
|
+
if (!validTables.includes(tableName)) {
|
|
479
|
+
throw new Error(`Invalid table name: ${tableName}`);
|
|
480
|
+
}
|
|
481
|
+
const table = this.data[tableName];
|
|
482
|
+
return table
|
|
483
|
+
.filter(row => this.userId ? row.user_id === this.userId : row.user_id === undefined || row.user_id === null)
|
|
484
|
+
.sort((a, b) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime())
|
|
485
|
+
.slice(0, limit);
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
console.error(`Error getting latest rows from ${tableName}:`, error);
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
export default LocalStorageAdapter;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSH SaaS Audit Logging Service
|
|
3
|
+
* Comprehensive audit trail for all actions
|
|
4
|
+
*/
|
|
5
|
+
import { getSupabaseClient } from './supabase-client.js';
|
|
6
|
+
/**
|
|
7
|
+
* Audit Logger Service
|
|
8
|
+
*/
|
|
9
|
+
export class AuditLogger {
|
|
10
|
+
supabase = getSupabaseClient();
|
|
11
|
+
/**
|
|
12
|
+
* Log an audit event
|
|
13
|
+
*/
|
|
14
|
+
async log(input) {
|
|
15
|
+
try {
|
|
16
|
+
const { error } = await this.supabase.from('audit_logs').insert({
|
|
17
|
+
organization_id: input.organizationId,
|
|
18
|
+
team_id: input.teamId || null,
|
|
19
|
+
user_id: input.userId || null,
|
|
20
|
+
user_email: input.userEmail || null,
|
|
21
|
+
action: input.action,
|
|
22
|
+
resource_type: input.resourceType,
|
|
23
|
+
resource_id: input.resourceId || null,
|
|
24
|
+
ip_address: input.ipAddress || null,
|
|
25
|
+
user_agent: input.userAgent || null,
|
|
26
|
+
metadata: input.metadata || {},
|
|
27
|
+
old_value: input.oldValue || null,
|
|
28
|
+
new_value: input.newValue || null,
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
});
|
|
31
|
+
if (error) {
|
|
32
|
+
console.error('Failed to write audit log:', error);
|
|
33
|
+
// Don't throw - audit logging should not break the main operation
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error('Audit logging error:', error);
|
|
38
|
+
// Don't throw - audit logging should not break the main operation
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get audit logs for organization
|
|
43
|
+
*/
|
|
44
|
+
async getOrganizationLogs(organizationId, options = {}) {
|
|
45
|
+
let query = this.supabase
|
|
46
|
+
.from('audit_logs')
|
|
47
|
+
.select('*', { count: 'exact' })
|
|
48
|
+
.eq('organization_id', organizationId);
|
|
49
|
+
if (options.startDate) {
|
|
50
|
+
query = query.gte('timestamp', options.startDate.toISOString());
|
|
51
|
+
}
|
|
52
|
+
if (options.endDate) {
|
|
53
|
+
query = query.lte('timestamp', options.endDate.toISOString());
|
|
54
|
+
}
|
|
55
|
+
if (options.action) {
|
|
56
|
+
query = query.eq('action', options.action);
|
|
57
|
+
}
|
|
58
|
+
if (options.userId) {
|
|
59
|
+
query = query.eq('user_id', options.userId);
|
|
60
|
+
}
|
|
61
|
+
if (options.teamId) {
|
|
62
|
+
query = query.eq('team_id', options.teamId);
|
|
63
|
+
}
|
|
64
|
+
query = query.order('timestamp', { ascending: false });
|
|
65
|
+
if (options.limit) {
|
|
66
|
+
query = query.limit(options.limit);
|
|
67
|
+
}
|
|
68
|
+
if (options.offset) {
|
|
69
|
+
query = query.range(options.offset, options.offset + (options.limit || 50) - 1);
|
|
70
|
+
}
|
|
71
|
+
const { data, count, error } = await query;
|
|
72
|
+
if (error) {
|
|
73
|
+
throw new Error(`Failed to get audit logs: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
logs: (data || []).map(this.mapDbLogToLog),
|
|
77
|
+
total: count || 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get audit logs for a specific resource
|
|
82
|
+
*/
|
|
83
|
+
async getResourceLogs(organizationId, resourceType, resourceId, limit = 50) {
|
|
84
|
+
const { data, error } = await this.supabase
|
|
85
|
+
.from('audit_logs')
|
|
86
|
+
.select('*')
|
|
87
|
+
.eq('organization_id', organizationId)
|
|
88
|
+
.eq('resource_type', resourceType)
|
|
89
|
+
.eq('resource_id', resourceId)
|
|
90
|
+
.order('timestamp', { ascending: false })
|
|
91
|
+
.limit(limit);
|
|
92
|
+
if (error) {
|
|
93
|
+
throw new Error(`Failed to get resource logs: ${error.message}`);
|
|
94
|
+
}
|
|
95
|
+
return (data || []).map(this.mapDbLogToLog);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get audit logs for a team
|
|
99
|
+
*/
|
|
100
|
+
async getTeamLogs(teamId, options = {}) {
|
|
101
|
+
let query = this.supabase
|
|
102
|
+
.from('audit_logs')
|
|
103
|
+
.select('*', { count: 'exact' })
|
|
104
|
+
.eq('team_id', teamId);
|
|
105
|
+
if (options.startDate) {
|
|
106
|
+
query = query.gte('timestamp', options.startDate.toISOString());
|
|
107
|
+
}
|
|
108
|
+
if (options.endDate) {
|
|
109
|
+
query = query.lte('timestamp', options.endDate.toISOString());
|
|
110
|
+
}
|
|
111
|
+
query = query.order('timestamp', { ascending: false });
|
|
112
|
+
if (options.limit) {
|
|
113
|
+
query = query.limit(options.limit);
|
|
114
|
+
}
|
|
115
|
+
if (options.offset) {
|
|
116
|
+
query = query.range(options.offset, options.offset + (options.limit || 50) - 1);
|
|
117
|
+
}
|
|
118
|
+
const { data, count, error } = await query;
|
|
119
|
+
if (error) {
|
|
120
|
+
throw new Error(`Failed to get team logs: ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
logs: (data || []).map(this.mapDbLogToLog),
|
|
124
|
+
total: count || 0,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get audit logs for a user
|
|
129
|
+
*/
|
|
130
|
+
async getUserLogs(userId, options = {}) {
|
|
131
|
+
let query = this.supabase
|
|
132
|
+
.from('audit_logs')
|
|
133
|
+
.select('*', { count: 'exact' })
|
|
134
|
+
.eq('user_id', userId);
|
|
135
|
+
if (options.startDate) {
|
|
136
|
+
query = query.gte('timestamp', options.startDate.toISOString());
|
|
137
|
+
}
|
|
138
|
+
if (options.endDate) {
|
|
139
|
+
query = query.lte('timestamp', options.endDate.toISOString());
|
|
140
|
+
}
|
|
141
|
+
query = query.order('timestamp', { ascending: false });
|
|
142
|
+
if (options.limit) {
|
|
143
|
+
query = query.limit(options.limit);
|
|
144
|
+
}
|
|
145
|
+
if (options.offset) {
|
|
146
|
+
query = query.range(options.offset, options.offset + (options.limit || 50) - 1);
|
|
147
|
+
}
|
|
148
|
+
const { data, count, error } = await query;
|
|
149
|
+
if (error) {
|
|
150
|
+
throw new Error(`Failed to get user logs: ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
logs: (data || []).map(this.mapDbLogToLog),
|
|
154
|
+
total: count || 0,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Delete old audit logs (for retention policy)
|
|
159
|
+
*/
|
|
160
|
+
async deleteOldLogs(organizationId, retentionDays) {
|
|
161
|
+
const cutoffDate = new Date();
|
|
162
|
+
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
|
|
163
|
+
const { count, error } = await this.supabase
|
|
164
|
+
.from('audit_logs')
|
|
165
|
+
.delete({ count: 'exact' })
|
|
166
|
+
.eq('organization_id', organizationId)
|
|
167
|
+
.lt('timestamp', cutoffDate.toISOString());
|
|
168
|
+
if (error) {
|
|
169
|
+
throw new Error(`Failed to delete old logs: ${error.message}`);
|
|
170
|
+
}
|
|
171
|
+
return count || 0;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Map database log to AuditLog type
|
|
175
|
+
*/
|
|
176
|
+
mapDbLogToLog(dbLog) {
|
|
177
|
+
return {
|
|
178
|
+
id: dbLog.id,
|
|
179
|
+
organizationId: dbLog.organization_id,
|
|
180
|
+
teamId: dbLog.team_id,
|
|
181
|
+
userId: dbLog.user_id,
|
|
182
|
+
userEmail: dbLog.user_email,
|
|
183
|
+
action: dbLog.action,
|
|
184
|
+
resourceType: dbLog.resource_type,
|
|
185
|
+
resourceId: dbLog.resource_id,
|
|
186
|
+
ipAddress: dbLog.ip_address,
|
|
187
|
+
userAgent: dbLog.user_agent,
|
|
188
|
+
metadata: dbLog.metadata || {},
|
|
189
|
+
oldValue: dbLog.old_value,
|
|
190
|
+
newValue: dbLog.new_value,
|
|
191
|
+
timestamp: new Date(dbLog.timestamp),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Singleton instance
|
|
197
|
+
*/
|
|
198
|
+
export const auditLogger = new AuditLogger();
|
|
199
|
+
/**
|
|
200
|
+
* Helper function to extract IP from request
|
|
201
|
+
*/
|
|
202
|
+
export function getIpFromRequest(req) {
|
|
203
|
+
return (req.headers['x-forwarded-for']?.split(',')[0].trim() ||
|
|
204
|
+
req.headers['x-real-ip'] ||
|
|
205
|
+
req.connection?.remoteAddress ||
|
|
206
|
+
req.socket?.remoteAddress);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Helper function to extract user agent from request
|
|
210
|
+
*/
|
|
211
|
+
export function getUserAgentFromRequest(req) {
|
|
212
|
+
return req.headers['user-agent'];
|
|
213
|
+
}
|