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.
@@ -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
+ }