@trentapps/manager-protocol 1.1.3 → 1.2.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.
Files changed (142) hide show
  1. package/README.md +164 -17
  2. package/dist/analyzers/CSSAnalyzer.d.ts +180 -8
  3. package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -1
  4. package/dist/analyzers/CSSAnalyzer.js +561 -105
  5. package/dist/analyzers/CSSAnalyzer.js.map +1 -1
  6. package/dist/config/dashboard.d.ts +55 -0
  7. package/dist/config/dashboard.d.ts.map +1 -0
  8. package/dist/config/dashboard.js +103 -0
  9. package/dist/config/dashboard.js.map +1 -0
  10. package/dist/config/index.d.ts +7 -0
  11. package/dist/config/index.d.ts.map +1 -0
  12. package/dist/config/index.js +7 -0
  13. package/dist/config/index.js.map +1 -0
  14. package/dist/dashboard/httpDashboard.d.ts +100 -0
  15. package/dist/dashboard/httpDashboard.d.ts.map +1 -0
  16. package/dist/dashboard/httpDashboard.js +1276 -0
  17. package/dist/dashboard/httpDashboard.js.map +1 -0
  18. package/dist/dashboard/index.d.ts +6 -0
  19. package/dist/dashboard/index.d.ts.map +1 -0
  20. package/dist/dashboard/index.js +7 -0
  21. package/dist/dashboard/index.js.map +1 -0
  22. package/dist/engine/AuditLogger.d.ts +370 -2
  23. package/dist/engine/AuditLogger.d.ts.map +1 -1
  24. package/dist/engine/AuditLogger.js +1064 -24
  25. package/dist/engine/AuditLogger.js.map +1 -1
  26. package/dist/engine/GitHubClient.d.ts +183 -0
  27. package/dist/engine/GitHubClient.d.ts.map +1 -0
  28. package/dist/engine/GitHubClient.js +411 -0
  29. package/dist/engine/GitHubClient.js.map +1 -0
  30. package/dist/engine/RateLimiter.d.ts +5 -3
  31. package/dist/engine/RateLimiter.d.ts.map +1 -1
  32. package/dist/engine/RateLimiter.js +49 -72
  33. package/dist/engine/RateLimiter.js.map +1 -1
  34. package/dist/engine/RuleDependencyAnalyzer.d.ts +73 -0
  35. package/dist/engine/RuleDependencyAnalyzer.d.ts.map +1 -0
  36. package/dist/engine/RuleDependencyAnalyzer.js +475 -0
  37. package/dist/engine/RuleDependencyAnalyzer.js.map +1 -0
  38. package/dist/engine/RulesEngine.d.ts +102 -3
  39. package/dist/engine/RulesEngine.d.ts.map +1 -1
  40. package/dist/engine/RulesEngine.js +326 -21
  41. package/dist/engine/RulesEngine.js.map +1 -1
  42. package/dist/engine/TaskManager.d.ts +10 -14
  43. package/dist/engine/TaskManager.d.ts.map +1 -1
  44. package/dist/engine/TaskManager.js +169 -197
  45. package/dist/engine/TaskManager.js.map +1 -1
  46. package/dist/engine/index.d.ts +3 -0
  47. package/dist/engine/index.d.ts.map +1 -1
  48. package/dist/engine/index.js +5 -0
  49. package/dist/engine/index.js.map +1 -1
  50. package/dist/rules/azure.d.ts.map +1 -1
  51. package/dist/rules/azure.js +12 -14
  52. package/dist/rules/azure.js.map +1 -1
  53. package/dist/rules/compliance.d.ts.map +1 -1
  54. package/dist/rules/compliance.js +23 -41
  55. package/dist/rules/compliance.js.map +1 -1
  56. package/dist/rules/condition-optimizer.d.ts +151 -0
  57. package/dist/rules/condition-optimizer.d.ts.map +1 -0
  58. package/dist/rules/condition-optimizer.js +479 -0
  59. package/dist/rules/condition-optimizer.js.map +1 -0
  60. package/dist/rules/css.d.ts.map +1 -1
  61. package/dist/rules/css.js +538 -0
  62. package/dist/rules/css.js.map +1 -1
  63. package/dist/rules/field-standards.d.ts +1172 -0
  64. package/dist/rules/field-standards.d.ts.map +1 -0
  65. package/dist/rules/field-standards.js +908 -0
  66. package/dist/rules/field-standards.js.map +1 -0
  67. package/dist/rules/flask.d.ts.map +1 -1
  68. package/dist/rules/flask.js +18 -31
  69. package/dist/rules/flask.js.map +1 -1
  70. package/dist/rules/index.d.ts +220 -0
  71. package/dist/rules/index.d.ts.map +1 -1
  72. package/dist/rules/index.js +155 -0
  73. package/dist/rules/index.js.map +1 -1
  74. package/dist/rules/ml-ai.d.ts.map +1 -1
  75. package/dist/rules/ml-ai.js +11 -13
  76. package/dist/rules/ml-ai.js.map +1 -1
  77. package/dist/rules/patterns.d.ts +568 -0
  78. package/dist/rules/patterns.d.ts.map +1 -0
  79. package/dist/rules/patterns.js +1359 -0
  80. package/dist/rules/patterns.js.map +1 -0
  81. package/dist/rules/security.d.ts.map +1 -1
  82. package/dist/rules/security.js +580 -19
  83. package/dist/rules/security.js.map +1 -1
  84. package/dist/rules/shared-patterns.d.ts +268 -0
  85. package/dist/rules/shared-patterns.d.ts.map +1 -0
  86. package/dist/rules/shared-patterns.js +556 -0
  87. package/dist/rules/shared-patterns.js.map +1 -0
  88. package/dist/rules/storage.d.ts +8 -2
  89. package/dist/rules/storage.d.ts.map +1 -1
  90. package/dist/rules/storage.js +541 -3
  91. package/dist/rules/storage.js.map +1 -1
  92. package/dist/rules/stripe.d.ts.map +1 -1
  93. package/dist/rules/stripe.js +19 -26
  94. package/dist/rules/stripe.js.map +1 -1
  95. package/dist/rules/websocket.d.ts.map +1 -1
  96. package/dist/rules/websocket.js +32 -40
  97. package/dist/rules/websocket.js.map +1 -1
  98. package/dist/supervisor/AgentSupervisor.d.ts +52 -0
  99. package/dist/supervisor/AgentSupervisor.d.ts.map +1 -1
  100. package/dist/supervisor/AgentSupervisor.js +120 -1
  101. package/dist/supervisor/AgentSupervisor.js.map +1 -1
  102. package/dist/supervisor/ManagedServerRegistry.d.ts +139 -2
  103. package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -1
  104. package/dist/supervisor/ManagedServerRegistry.js +590 -6
  105. package/dist/supervisor/ManagedServerRegistry.js.map +1 -1
  106. package/dist/supervisor/ProjectTracker.d.ts +2 -1
  107. package/dist/supervisor/ProjectTracker.d.ts.map +1 -1
  108. package/dist/supervisor/ProjectTracker.js +5 -9
  109. package/dist/supervisor/ProjectTracker.js.map +1 -1
  110. package/dist/testing/index.d.ts +11 -0
  111. package/dist/testing/index.d.ts.map +1 -0
  112. package/dist/testing/index.js +12 -0
  113. package/dist/testing/index.js.map +1 -0
  114. package/dist/testing/rule-tester.d.ts +217 -0
  115. package/dist/testing/rule-tester.d.ts.map +1 -0
  116. package/dist/testing/rule-tester.examples.d.ts +57 -0
  117. package/dist/testing/rule-tester.examples.d.ts.map +1 -0
  118. package/dist/testing/rule-tester.examples.js +375 -0
  119. package/dist/testing/rule-tester.examples.js.map +1 -0
  120. package/dist/testing/rule-tester.js +381 -0
  121. package/dist/testing/rule-tester.js.map +1 -0
  122. package/dist/testing/rule-validator.d.ts +141 -0
  123. package/dist/testing/rule-validator.d.ts.map +1 -0
  124. package/dist/testing/rule-validator.js +640 -0
  125. package/dist/testing/rule-validator.js.map +1 -0
  126. package/dist/types/index.d.ts +265 -4
  127. package/dist/types/index.d.ts.map +1 -1
  128. package/dist/types/index.js +57 -2
  129. package/dist/types/index.js.map +1 -1
  130. package/dist/utils/index.d.ts +2 -0
  131. package/dist/utils/index.d.ts.map +1 -1
  132. package/dist/utils/index.js +2 -0
  133. package/dist/utils/index.js.map +1 -1
  134. package/dist/utils/rate-limiting.d.ts +268 -0
  135. package/dist/utils/rate-limiting.d.ts.map +1 -0
  136. package/dist/utils/rate-limiting.js +403 -0
  137. package/dist/utils/rate-limiting.js.map +1 -0
  138. package/dist/utils/shared.d.ts +306 -0
  139. package/dist/utils/shared.d.ts.map +1 -0
  140. package/dist/utils/shared.js +464 -0
  141. package/dist/utils/shared.js.map +1 -0
  142. package/package.json +3 -2
@@ -2,12 +2,248 @@
2
2
  * Enterprise Agent Supervisor - Audit Logger
3
3
  *
4
4
  * Comprehensive audit logging for compliance, security, and operational visibility.
5
+ *
6
+ * Storage Strategy:
7
+ * - Uses write-through caching: events are only added to memory after successful DB write
8
+ * - Failed writes are queued for retry
9
+ * - Periodic sync checks ensure memory/DB consistency
10
+ *
11
+ * Query System (Task #49):
12
+ * - QueryBuilder provides fluent API for complex queries
13
+ * - Supports cursor-based pagination for large result sets
14
+ * - Aggregation queries for analytics and dashboards
15
+ * - Full-text search in metadata and details JSON fields
5
16
  */
6
17
  import { v4 as uuidv4 } from 'uuid';
7
18
  import { mkdirSync } from 'fs';
8
19
  import path from 'path';
9
20
  import Database from 'better-sqlite3';
10
21
  import { withRetry, WebhookDeliveryError, formatError } from '../utils/errors.js';
22
+ import { calculateGroupedCounts } from '../utils/shared.js';
23
+ /**
24
+ * QueryBuilder - Fluent API for building audit event queries
25
+ *
26
+ * Usage:
27
+ * ```typescript
28
+ * const results = await logger.query()
29
+ * .where('eventType', 'action_executed')
30
+ * .where('outcome', 'success')
31
+ * .since(new Date('2024-01-01'))
32
+ * .until(new Date('2024-12-31'))
33
+ * .orderBy('timestamp', 'desc')
34
+ * .limit(100)
35
+ * .execute();
36
+ *
37
+ * // With pagination
38
+ * const page1 = await logger.query()
39
+ * .eventType('action_evaluated')
40
+ * .limit(50)
41
+ * .executePaginated();
42
+ *
43
+ * // Get next page
44
+ * const page2 = await logger.query()
45
+ * .eventType('action_evaluated')
46
+ * .cursor(page1.pagination.nextCursor!)
47
+ * .limit(50)
48
+ * .executePaginated();
49
+ *
50
+ * // Aggregation
51
+ * const stats = await logger.query()
52
+ * .since(new Date('2024-01-01'))
53
+ * .aggregate('day');
54
+ * ```
55
+ */
56
+ export class QueryBuilder {
57
+ filters = {};
58
+ paginationOpts = {};
59
+ orderByField = 'timestamp';
60
+ orderDirection = 'desc';
61
+ logger;
62
+ constructor(logger) {
63
+ this.logger = logger;
64
+ }
65
+ /**
66
+ * Add a filter condition
67
+ */
68
+ where(field, value) {
69
+ this.filters[field] = value;
70
+ return this;
71
+ }
72
+ /**
73
+ * Filter by event type(s)
74
+ */
75
+ eventType(type) {
76
+ this.filters.eventType = type;
77
+ return this;
78
+ }
79
+ /**
80
+ * Filter by agent ID(s)
81
+ */
82
+ agentId(id) {
83
+ this.filters.agentId = id;
84
+ return this;
85
+ }
86
+ /**
87
+ * Filter by session ID(s)
88
+ */
89
+ sessionId(id) {
90
+ this.filters.sessionId = id;
91
+ return this;
92
+ }
93
+ /**
94
+ * Filter by user ID(s)
95
+ */
96
+ userId(id) {
97
+ this.filters.userId = id;
98
+ return this;
99
+ }
100
+ /**
101
+ * Filter by outcome(s)
102
+ */
103
+ outcome(outcome) {
104
+ this.filters.outcome = outcome;
105
+ return this;
106
+ }
107
+ /**
108
+ * Filter by risk level(s)
109
+ */
110
+ riskLevel(level) {
111
+ this.filters.riskLevel = level;
112
+ return this;
113
+ }
114
+ /**
115
+ * Filter events after this date (inclusive)
116
+ */
117
+ since(date) {
118
+ this.filters.since = date instanceof Date ? date.toISOString() : date;
119
+ return this;
120
+ }
121
+ /**
122
+ * Filter events before this date (inclusive)
123
+ */
124
+ until(date) {
125
+ this.filters.until = date instanceof Date ? date.toISOString() : date;
126
+ return this;
127
+ }
128
+ /**
129
+ * Filter by correlation ID
130
+ */
131
+ correlatedWith(correlationId) {
132
+ this.filters.correlationId = correlationId;
133
+ return this;
134
+ }
135
+ /**
136
+ * Filter by action (partial match, case-insensitive)
137
+ */
138
+ action(actionPattern) {
139
+ this.filters.action = actionPattern;
140
+ return this;
141
+ }
142
+ /**
143
+ * Full-text search in metadata JSON
144
+ */
145
+ searchMetadata(searchTerm) {
146
+ this.filters.metadataSearch = searchTerm;
147
+ return this;
148
+ }
149
+ /**
150
+ * Full-text search in details JSON
151
+ */
152
+ searchDetails(searchTerm) {
153
+ this.filters.detailsSearch = searchTerm;
154
+ return this;
155
+ }
156
+ /**
157
+ * Set ordering
158
+ */
159
+ orderBy(field, direction = 'desc') {
160
+ this.orderByField = field;
161
+ this.orderDirection = direction;
162
+ return this;
163
+ }
164
+ /**
165
+ * Set result limit
166
+ */
167
+ limit(count) {
168
+ this.paginationOpts.limit = count;
169
+ return this;
170
+ }
171
+ /**
172
+ * Set offset for offset-based pagination
173
+ */
174
+ offset(count) {
175
+ this.paginationOpts.offset = count;
176
+ return this;
177
+ }
178
+ /**
179
+ * Set cursor for cursor-based pagination
180
+ */
181
+ cursor(cursorString) {
182
+ this.paginationOpts.cursor = cursorString;
183
+ return this;
184
+ }
185
+ /**
186
+ * Execute the query and return results
187
+ */
188
+ async execute() {
189
+ return this.logger.executeQuery(this.filters, this.paginationOpts, this.orderByField, this.orderDirection);
190
+ }
191
+ /**
192
+ * Execute query with pagination metadata
193
+ */
194
+ async executePaginated() {
195
+ return this.logger.executeQueryPaginated(this.filters, this.paginationOpts, this.orderByField, this.orderDirection);
196
+ }
197
+ /**
198
+ * Get aggregated statistics for the filtered events
199
+ */
200
+ async aggregate(interval) {
201
+ return this.logger.aggregateQuery(this.filters, interval);
202
+ }
203
+ /**
204
+ * Get count of matching events
205
+ */
206
+ async count() {
207
+ return this.logger.countQuery(this.filters);
208
+ }
209
+ /**
210
+ * Get the first matching event
211
+ */
212
+ async first() {
213
+ const results = await this.limit(1).execute();
214
+ return results[0];
215
+ }
216
+ /**
217
+ * Check if any events match
218
+ */
219
+ async exists() {
220
+ const count = await this.count();
221
+ return count > 0;
222
+ }
223
+ /**
224
+ * Get filters for inspection/debugging
225
+ */
226
+ getFilters() {
227
+ return { ...this.filters };
228
+ }
229
+ /**
230
+ * Get pagination options for inspection/debugging
231
+ */
232
+ getPaginationOptions() {
233
+ return { ...this.paginationOpts };
234
+ }
235
+ /**
236
+ * Clone the query builder for branching queries
237
+ */
238
+ clone() {
239
+ const cloned = new QueryBuilder(this.logger);
240
+ cloned.filters = { ...this.filters };
241
+ cloned.paginationOpts = { ...this.paginationOpts };
242
+ cloned.orderByField = this.orderByField;
243
+ cloned.orderDirection = this.orderDirection;
244
+ return cloned;
245
+ }
246
+ }
11
247
  export class AuditLogger {
12
248
  events = [];
13
249
  maxEvents;
@@ -21,6 +257,15 @@ export class AuditLogger {
21
257
  db;
22
258
  initialized = false;
23
259
  initPromise;
260
+ // Retry queue for failed database writes
261
+ failedWrites = [];
262
+ retryIntervalMs;
263
+ maxRetryAttempts;
264
+ retryTimer;
265
+ // Sync mechanism
266
+ syncIntervalMs;
267
+ syncTimer;
268
+ lastSyncTime;
24
269
  constructor(options = {}) {
25
270
  this.maxEvents = options.maxEvents || 10000;
26
271
  this.enableConsoleLog = options.enableConsoleLog || false;
@@ -30,6 +275,9 @@ export class AuditLogger {
30
275
  this.onEvent = options.onEvent;
31
276
  this.onWebhookError = options.onWebhookError;
32
277
  this.dbPath = options.dbPath || options.logFile; // Support legacy logFile option
278
+ this.retryIntervalMs = options.retryIntervalMs ?? 5000;
279
+ this.maxRetryAttempts = options.maxRetryAttempts ?? 5;
280
+ this.syncIntervalMs = options.syncIntervalMs ?? 60000; // Default: check every minute
33
281
  }
34
282
  /**
35
283
  * Initialize and setup SQLite database if configured
@@ -59,9 +307,278 @@ export class AuditLogger {
59
307
  async doInitialize() {
60
308
  if (this.dbPath) {
61
309
  await this.initDatabase();
310
+ // Start retry timer for failed writes
311
+ this.startRetryTimer();
312
+ // Start sync timer if enabled
313
+ if (this.syncIntervalMs > 0) {
314
+ this.startSyncTimer();
315
+ }
62
316
  }
63
317
  this.initialized = true;
64
318
  }
319
+ /**
320
+ * Start the retry timer for failed database writes
321
+ */
322
+ startRetryTimer() {
323
+ if (this.retryTimer)
324
+ return;
325
+ this.retryTimer = setInterval(() => {
326
+ this.processRetryQueue();
327
+ }, this.retryIntervalMs);
328
+ // Don't prevent Node from exiting
329
+ if (this.retryTimer.unref) {
330
+ this.retryTimer.unref();
331
+ }
332
+ }
333
+ /**
334
+ * Start the periodic sync timer
335
+ */
336
+ startSyncTimer() {
337
+ if (this.syncTimer)
338
+ return;
339
+ this.syncTimer = setInterval(() => {
340
+ this.checkSync().catch(err => {
341
+ console.error('[AuditLogger] Sync check failed:', err);
342
+ });
343
+ }, this.syncIntervalMs);
344
+ // Don't prevent Node from exiting
345
+ if (this.syncTimer.unref) {
346
+ this.syncTimer.unref();
347
+ }
348
+ }
349
+ /**
350
+ * Stop all timers (for cleanup/shutdown)
351
+ */
352
+ stopTimers() {
353
+ if (this.retryTimer) {
354
+ clearInterval(this.retryTimer);
355
+ this.retryTimer = undefined;
356
+ }
357
+ if (this.syncTimer) {
358
+ clearInterval(this.syncTimer);
359
+ this.syncTimer = undefined;
360
+ }
361
+ }
362
+ /**
363
+ * Process the retry queue for failed database writes
364
+ */
365
+ processRetryQueue() {
366
+ if (this.failedWrites.length === 0 || !this.db)
367
+ return;
368
+ const toRetry = [...this.failedWrites];
369
+ this.failedWrites = [];
370
+ for (const item of toRetry) {
371
+ const success = this.saveToDatabase(item.event);
372
+ if (success) {
373
+ // Successfully written - add to memory cache
374
+ this.addToMemoryCache(item.event);
375
+ console.log(`[AuditLogger] Retry succeeded for event ${item.event.eventId} after ${item.attempts + 1} attempts`);
376
+ }
377
+ else {
378
+ // Still failing
379
+ item.attempts++;
380
+ item.lastError = 'Database write failed';
381
+ if (item.attempts < this.maxRetryAttempts) {
382
+ this.failedWrites.push(item);
383
+ }
384
+ else {
385
+ console.error(`[AuditLogger] Giving up on event ${item.event.eventId} after ${item.attempts} attempts. ` +
386
+ `First attempt: ${item.firstAttempt}, Last error: ${item.lastError}`);
387
+ // Still add to memory cache so event isn't completely lost
388
+ // but mark it as potentially inconsistent
389
+ this.addToMemoryCache(item.event);
390
+ }
391
+ }
392
+ }
393
+ }
394
+ /**
395
+ * Check consistency between memory cache and database
396
+ * Returns sync status information
397
+ */
398
+ async checkSync() {
399
+ const result = {
400
+ inSync: true,
401
+ memoryCount: this.events.length,
402
+ dbCount: 0,
403
+ missingInDb: [],
404
+ missingInMemory: [],
405
+ pendingRetries: this.failedWrites.length
406
+ };
407
+ if (!this.db) {
408
+ // No database, memory is source of truth
409
+ return result;
410
+ }
411
+ try {
412
+ // Get count from database
413
+ const countStmt = this.db.prepare('SELECT COUNT(*) as count FROM audit_events');
414
+ const countResult = countStmt.get();
415
+ result.dbCount = countResult.count;
416
+ // Check recent events for consistency (last 100)
417
+ const recentMemory = this.events.slice(-100);
418
+ const recentMemoryIds = new Set(recentMemory.map(e => e.eventId));
419
+ const recentDbStmt = this.db.prepare(`
420
+ SELECT eventId FROM audit_events
421
+ ORDER BY timestamp DESC
422
+ LIMIT 100
423
+ `);
424
+ const recentDbRows = recentDbStmt.all();
425
+ const recentDbIds = new Set(recentDbRows.map(r => r.eventId));
426
+ // Find events in memory but not in DB
427
+ for (const event of recentMemory) {
428
+ if (!recentDbIds.has(event.eventId)) {
429
+ result.missingInDb.push(event.eventId);
430
+ }
431
+ }
432
+ // Find events in DB but not in memory
433
+ for (const row of recentDbRows) {
434
+ if (!recentMemoryIds.has(row.eventId)) {
435
+ result.missingInMemory.push(row.eventId);
436
+ }
437
+ }
438
+ result.inSync = result.missingInDb.length === 0 &&
439
+ result.missingInMemory.length === 0 &&
440
+ result.pendingRetries === 0;
441
+ this.lastSyncTime = new Date().toISOString();
442
+ if (!result.inSync) {
443
+ console.warn('[AuditLogger] Sync check found inconsistencies:', {
444
+ missingInDb: result.missingInDb.length,
445
+ missingInMemory: result.missingInMemory.length,
446
+ pendingRetries: result.pendingRetries
447
+ });
448
+ }
449
+ return result;
450
+ }
451
+ catch (error) {
452
+ console.error('[AuditLogger] Sync check error:', error);
453
+ result.inSync = false;
454
+ return result;
455
+ }
456
+ }
457
+ /**
458
+ * Force synchronization between memory and database
459
+ * Reconciles any differences by:
460
+ * 1. Writing memory-only events to database
461
+ * 2. Loading database-only events to memory
462
+ * 3. Processing any pending retries
463
+ */
464
+ async sync() {
465
+ const result = {
466
+ eventsSyncedToDb: 0,
467
+ eventsSyncedToMemory: 0,
468
+ retriesProcessed: 0,
469
+ errors: []
470
+ };
471
+ if (!this.db) {
472
+ return result;
473
+ }
474
+ try {
475
+ // First, process any pending retries
476
+ const pendingCount = this.failedWrites.length;
477
+ this.processRetryQueue();
478
+ result.retriesProcessed = pendingCount - this.failedWrites.length;
479
+ // Check current sync status
480
+ const status = await this.checkSync();
481
+ // Write memory-only events to database
482
+ for (const eventId of status.missingInDb) {
483
+ const event = this.events.find(e => e.eventId === eventId);
484
+ if (event) {
485
+ const success = this.saveToDatabase(event);
486
+ if (success) {
487
+ result.eventsSyncedToDb++;
488
+ }
489
+ else {
490
+ result.errors.push(`Failed to sync event ${eventId} to database`);
491
+ }
492
+ }
493
+ }
494
+ // Load database-only events to memory
495
+ if (status.missingInMemory.length > 0) {
496
+ const placeholders = status.missingInMemory.map(() => '?').join(',');
497
+ const stmt = this.db.prepare(`
498
+ SELECT * FROM audit_events
499
+ WHERE eventId IN (${placeholders})
500
+ `);
501
+ const rows = stmt.all(...status.missingInMemory);
502
+ for (const row of rows) {
503
+ const event = {
504
+ eventId: row.eventId,
505
+ eventType: row.eventType,
506
+ action: row.action,
507
+ timestamp: row.timestamp,
508
+ outcome: row.outcome,
509
+ agentId: row.agentId,
510
+ sessionId: row.sessionId,
511
+ userId: row.userId,
512
+ riskLevel: row.riskLevel,
513
+ details: row.details ? JSON.parse(row.details) : undefined,
514
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
515
+ correlationId: row.correlationId,
516
+ parentEventId: row.parentEventId
517
+ };
518
+ // Insert in correct position based on timestamp
519
+ this.insertEventSorted(event);
520
+ result.eventsSyncedToMemory++;
521
+ }
522
+ }
523
+ console.log('[AuditLogger] Sync completed:', result);
524
+ return result;
525
+ }
526
+ catch (error) {
527
+ const errorMsg = error instanceof Error ? error.message : String(error);
528
+ result.errors.push(`Sync error: ${errorMsg}`);
529
+ console.error('[AuditLogger] Sync failed:', error);
530
+ return result;
531
+ }
532
+ }
533
+ /**
534
+ * Insert event into memory cache in sorted order by timestamp
535
+ */
536
+ insertEventSorted(event) {
537
+ // Find insertion point
538
+ let insertIdx = this.events.length;
539
+ for (let i = this.events.length - 1; i >= 0; i--) {
540
+ if (this.events[i].timestamp <= event.timestamp) {
541
+ insertIdx = i + 1;
542
+ break;
543
+ }
544
+ if (i === 0) {
545
+ insertIdx = 0;
546
+ }
547
+ }
548
+ this.events.splice(insertIdx, 0, event);
549
+ // Trim if exceeds max
550
+ if (this.events.length > this.maxEvents) {
551
+ this.events = this.events.slice(-this.maxEvents);
552
+ }
553
+ }
554
+ /**
555
+ * Add event to memory cache (internal helper)
556
+ */
557
+ addToMemoryCache(event) {
558
+ // Check if already in cache (to avoid duplicates from retries)
559
+ if (this.events.some(e => e.eventId === event.eventId)) {
560
+ return;
561
+ }
562
+ this.events.push(event);
563
+ // Trim if exceeds max
564
+ if (this.events.length > this.maxEvents) {
565
+ this.events = this.events.slice(-this.maxEvents);
566
+ }
567
+ }
568
+ /**
569
+ * Get retry queue status
570
+ */
571
+ getRetryQueueStatus() {
572
+ return {
573
+ pendingCount: this.failedWrites.length,
574
+ oldestAttempt: this.failedWrites.length > 0 ? this.failedWrites[0].firstAttempt : null,
575
+ events: this.failedWrites.map(fw => ({
576
+ eventId: fw.event.eventId,
577
+ attempts: fw.attempts,
578
+ lastError: fw.lastError
579
+ }))
580
+ };
581
+ }
65
582
  /**
66
583
  * Initialize SQLite database
67
584
  */
@@ -115,6 +632,11 @@ export class AuditLogger {
115
632
  }
116
633
  /**
117
634
  * Log an audit event
635
+ *
636
+ * Storage Strategy (Write-Through Caching):
637
+ * - If database is available: write to DB first, then add to memory on success
638
+ * - If DB write fails: queue for retry, do NOT add to memory (prevents drift)
639
+ * - If no database: add directly to memory (memory-only mode)
118
640
  */
119
641
  async log(params) {
120
642
  const event = {
@@ -132,13 +654,29 @@ export class AuditLogger {
132
654
  correlationId: params.correlationId,
133
655
  parentEventId: params.parentEventId
134
656
  };
135
- // Add to in-memory store
136
- this.events.push(event);
137
- // Trim if exceeds max
138
- if (this.events.length > this.maxEvents) {
139
- this.events = this.events.slice(-this.maxEvents);
657
+ // Write-through caching: DB first, then memory
658
+ if (this.db) {
659
+ const dbWriteSuccess = this.saveToDatabase(event);
660
+ if (dbWriteSuccess) {
661
+ // DB write succeeded - safe to add to memory
662
+ this.addToMemoryCache(event);
663
+ }
664
+ else {
665
+ // DB write failed - queue for retry, don't add to memory yet
666
+ this.failedWrites.push({
667
+ event,
668
+ attempts: 1,
669
+ lastError: 'Initial database write failed',
670
+ firstAttempt: new Date().toISOString()
671
+ });
672
+ console.warn(`[AuditLogger] Event ${event.eventId} queued for retry after initial DB write failure`);
673
+ }
140
674
  }
141
- // Console logging
675
+ else {
676
+ // No database - memory only mode
677
+ this.addToMemoryCache(event);
678
+ }
679
+ // Console logging (always happens regardless of DB status)
142
680
  if (this.enableConsoleLog) {
143
681
  this.logToConsole(event);
144
682
  }
@@ -150,10 +688,6 @@ export class AuditLogger {
150
688
  if (this.onEvent) {
151
689
  await this.onEvent(event);
152
690
  }
153
- // Database logging
154
- if (this.db) {
155
- this.saveToDatabase(event);
156
- }
157
691
  return event;
158
692
  }
159
693
  /**
@@ -237,10 +771,11 @@ export class AuditLogger {
237
771
  }
238
772
  /**
239
773
  * Save event to database
774
+ * @returns true if save was successful, false otherwise
240
775
  */
241
776
  saveToDatabase(event) {
242
777
  if (!this.db)
243
- return;
778
+ return false;
244
779
  try {
245
780
  const stmt = this.db.prepare(`
246
781
  INSERT INTO audit_events (
@@ -250,9 +785,11 @@ export class AuditLogger {
250
785
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
251
786
  `);
252
787
  stmt.run(event.eventId, event.eventType, event.action, event.timestamp, event.outcome, event.agentId || null, event.sessionId || null, event.userId || null, event.riskLevel || null, event.details ? JSON.stringify(event.details) : null, event.metadata ? JSON.stringify(event.metadata) : null, event.correlationId || null, event.parentEventId || null);
788
+ return true;
253
789
  }
254
790
  catch (error) {
255
791
  console.error('[AuditLogger] Failed to save to database:', error);
792
+ return false;
256
793
  }
257
794
  }
258
795
  /**
@@ -289,8 +826,484 @@ export class AuditLogger {
289
826
  return [];
290
827
  }
291
828
  }
829
+ // ============================================================================
830
+ // QUERY BUILDER METHODS (Task #49)
831
+ // ============================================================================
832
+ /**
833
+ * Create a new QueryBuilder for fluent query construction
834
+ *
835
+ * Usage:
836
+ * ```typescript
837
+ * const events = await logger.query()
838
+ * .eventType('action_executed')
839
+ * .since(new Date('2024-01-01'))
840
+ * .limit(100)
841
+ * .execute();
842
+ * ```
843
+ */
844
+ query() {
845
+ return new QueryBuilder(this);
846
+ }
847
+ /**
848
+ * Execute a query with filters (called by QueryBuilder)
849
+ */
850
+ executeQuery(filters, pagination, orderByField, orderDirection) {
851
+ // Use database if available for better performance
852
+ if (this.db) {
853
+ return this.executeDbQuery(filters, pagination, orderByField, orderDirection);
854
+ }
855
+ // Fall back to memory-based filtering
856
+ return this.executeMemoryQuery(filters, pagination, orderByField, orderDirection);
857
+ }
858
+ /**
859
+ * Execute query with pagination metadata (called by QueryBuilder)
860
+ */
861
+ async executeQueryPaginated(filters, pagination, orderByField, orderDirection) {
862
+ const totalCount = this.countQuery(filters);
863
+ const limit = pagination.limit || 100;
864
+ // If cursor is provided, decode it
865
+ let decodedCursor;
866
+ if (pagination.cursor) {
867
+ try {
868
+ decodedCursor = JSON.parse(Buffer.from(pagination.cursor, 'base64').toString('utf-8'));
869
+ }
870
+ catch {
871
+ // Invalid cursor, ignore it
872
+ }
873
+ }
874
+ // Adjust filters for cursor-based pagination
875
+ const adjustedFilters = { ...filters };
876
+ if (decodedCursor) {
877
+ if (decodedCursor.direction === 'forward') {
878
+ if (orderDirection === 'desc') {
879
+ adjustedFilters.until = decodedCursor.timestamp;
880
+ }
881
+ else {
882
+ adjustedFilters.since = decodedCursor.timestamp;
883
+ }
884
+ }
885
+ }
886
+ const data = this.executeQuery(adjustedFilters, { ...pagination, limit: limit + 1 }, orderByField, orderDirection);
887
+ // Check if there are more results
888
+ const hasMore = data.length > limit;
889
+ if (hasMore) {
890
+ data.pop(); // Remove the extra item
891
+ }
892
+ // Generate cursors
893
+ let nextCursor;
894
+ let previousCursor;
895
+ if (data.length > 0) {
896
+ const lastEvent = data[data.length - 1];
897
+ if (hasMore) {
898
+ const cursor = {
899
+ timestamp: lastEvent.timestamp,
900
+ eventId: lastEvent.eventId,
901
+ direction: 'forward'
902
+ };
903
+ nextCursor = Buffer.from(JSON.stringify(cursor)).toString('base64');
904
+ }
905
+ if (decodedCursor) {
906
+ const firstEvent = data[0];
907
+ const cursor = {
908
+ timestamp: firstEvent.timestamp,
909
+ eventId: firstEvent.eventId,
910
+ direction: 'backward'
911
+ };
912
+ previousCursor = Buffer.from(JSON.stringify(cursor)).toString('base64');
913
+ }
914
+ }
915
+ return {
916
+ data,
917
+ pagination: {
918
+ totalCount,
919
+ hasMore,
920
+ nextCursor,
921
+ previousCursor,
922
+ limit,
923
+ offset: pagination.offset
924
+ }
925
+ };
926
+ }
927
+ /**
928
+ * Get count of events matching filters (called by QueryBuilder)
929
+ */
930
+ countQuery(filters) {
931
+ if (this.db) {
932
+ const { whereClause, params } = this.buildWhereClause(filters);
933
+ const sql = `SELECT COUNT(*) as count FROM audit_events ${whereClause}`;
934
+ try {
935
+ const stmt = this.db.prepare(sql);
936
+ const result = stmt.get(...params);
937
+ return result.count;
938
+ }
939
+ catch (error) {
940
+ console.error('[AuditLogger] Count query failed:', error);
941
+ return 0;
942
+ }
943
+ }
944
+ // Memory-based count
945
+ return this.filterMemoryEvents(filters).length;
946
+ }
947
+ /**
948
+ * Execute aggregation query (called by QueryBuilder)
949
+ */
950
+ aggregateQuery(filters, interval) {
951
+ const events = this.db
952
+ ? this.executeDbQuery(filters, {}, 'timestamp', 'asc')
953
+ : this.filterMemoryEvents(filters);
954
+ const result = {
955
+ byEventType: {},
956
+ byOutcome: {},
957
+ byRiskLevel: {},
958
+ byAgent: {},
959
+ byUser: {},
960
+ timeSeries: [],
961
+ total: events.length
962
+ };
963
+ // Build aggregations
964
+ const timeSeriesMap = new Map();
965
+ for (const event of events) {
966
+ // By event type
967
+ result.byEventType[event.eventType] = (result.byEventType[event.eventType] || 0) + 1;
968
+ // By outcome
969
+ result.byOutcome[event.outcome] = (result.byOutcome[event.outcome] || 0) + 1;
970
+ // By risk level
971
+ if (event.riskLevel) {
972
+ result.byRiskLevel[event.riskLevel] = (result.byRiskLevel[event.riskLevel] || 0) + 1;
973
+ }
974
+ // By agent
975
+ if (event.agentId) {
976
+ result.byAgent[event.agentId] = (result.byAgent[event.agentId] || 0) + 1;
977
+ }
978
+ // By user
979
+ if (event.userId) {
980
+ result.byUser[event.userId] = (result.byUser[event.userId] || 0) + 1;
981
+ }
982
+ // Time series
983
+ if (interval) {
984
+ const bucket = this.truncateTimestamp(event.timestamp, interval);
985
+ let entry = timeSeriesMap.get(bucket);
986
+ if (!entry) {
987
+ entry = { count: 0, byOutcome: {} };
988
+ timeSeriesMap.set(bucket, entry);
989
+ }
990
+ entry.count++;
991
+ entry.byOutcome[event.outcome] = (entry.byOutcome[event.outcome] || 0) + 1;
992
+ }
993
+ }
994
+ // Convert time series map to array
995
+ if (interval) {
996
+ result.timeSeries = Array.from(timeSeriesMap.entries())
997
+ .map(([timestamp, data]) => ({
998
+ timestamp,
999
+ count: data.count,
1000
+ byOutcome: data.byOutcome
1001
+ }))
1002
+ .sort((a, b) => a.timestamp.localeCompare(b.timestamp));
1003
+ }
1004
+ return result;
1005
+ }
1006
+ /**
1007
+ * Truncate timestamp to the specified interval for time series grouping
1008
+ */
1009
+ truncateTimestamp(timestamp, interval) {
1010
+ const date = new Date(timestamp);
1011
+ switch (interval) {
1012
+ case 'minute':
1013
+ date.setSeconds(0, 0);
1014
+ break;
1015
+ case 'hour':
1016
+ date.setMinutes(0, 0, 0);
1017
+ break;
1018
+ case 'day':
1019
+ date.setHours(0, 0, 0, 0);
1020
+ break;
1021
+ case 'week': {
1022
+ const day = date.getDay();
1023
+ date.setDate(date.getDate() - day);
1024
+ date.setHours(0, 0, 0, 0);
1025
+ break;
1026
+ }
1027
+ case 'month':
1028
+ date.setDate(1);
1029
+ date.setHours(0, 0, 0, 0);
1030
+ break;
1031
+ }
1032
+ return date.toISOString();
1033
+ }
1034
+ /**
1035
+ * Execute query against database
1036
+ */
1037
+ executeDbQuery(filters, pagination, orderByField, orderDirection) {
1038
+ if (!this.db)
1039
+ return [];
1040
+ const { whereClause, params } = this.buildWhereClause(filters);
1041
+ // Validate order field to prevent SQL injection
1042
+ const allowedOrderFields = ['timestamp', 'eventType', 'action', 'outcome', 'riskLevel', 'agentId', 'userId'];
1043
+ const safeOrderField = allowedOrderFields.includes(orderByField) ? orderByField : 'timestamp';
1044
+ const safeDirection = orderDirection === 'asc' ? 'ASC' : 'DESC';
1045
+ let sql = `SELECT * FROM audit_events ${whereClause} ORDER BY ${safeOrderField} ${safeDirection}`;
1046
+ // Add pagination
1047
+ if (pagination.limit) {
1048
+ sql += ` LIMIT ?`;
1049
+ params.push(pagination.limit);
1050
+ }
1051
+ if (pagination.offset) {
1052
+ sql += ` OFFSET ?`;
1053
+ params.push(pagination.offset);
1054
+ }
1055
+ try {
1056
+ const stmt = this.db.prepare(sql);
1057
+ const rows = stmt.all(...params);
1058
+ return rows.map(row => this.rowToEvent(row));
1059
+ }
1060
+ catch (error) {
1061
+ console.error('[AuditLogger] Database query failed:', error);
1062
+ return [];
1063
+ }
1064
+ }
1065
+ /**
1066
+ * Build WHERE clause from filters with parameterized queries (SQL injection safe)
1067
+ */
1068
+ buildWhereClause(filters) {
1069
+ const conditions = [];
1070
+ const params = [];
1071
+ // Event type filter
1072
+ if (filters.eventType) {
1073
+ if (Array.isArray(filters.eventType)) {
1074
+ const placeholders = filters.eventType.map(() => '?').join(', ');
1075
+ conditions.push(`eventType IN (${placeholders})`);
1076
+ params.push(...filters.eventType);
1077
+ }
1078
+ else {
1079
+ conditions.push('eventType = ?');
1080
+ params.push(filters.eventType);
1081
+ }
1082
+ }
1083
+ // Agent ID filter
1084
+ if (filters.agentId) {
1085
+ if (Array.isArray(filters.agentId)) {
1086
+ const placeholders = filters.agentId.map(() => '?').join(', ');
1087
+ conditions.push(`agentId IN (${placeholders})`);
1088
+ params.push(...filters.agentId);
1089
+ }
1090
+ else {
1091
+ conditions.push('agentId = ?');
1092
+ params.push(filters.agentId);
1093
+ }
1094
+ }
1095
+ // Session ID filter
1096
+ if (filters.sessionId) {
1097
+ if (Array.isArray(filters.sessionId)) {
1098
+ const placeholders = filters.sessionId.map(() => '?').join(', ');
1099
+ conditions.push(`sessionId IN (${placeholders})`);
1100
+ params.push(...filters.sessionId);
1101
+ }
1102
+ else {
1103
+ conditions.push('sessionId = ?');
1104
+ params.push(filters.sessionId);
1105
+ }
1106
+ }
1107
+ // User ID filter
1108
+ if (filters.userId) {
1109
+ if (Array.isArray(filters.userId)) {
1110
+ const placeholders = filters.userId.map(() => '?').join(', ');
1111
+ conditions.push(`userId IN (${placeholders})`);
1112
+ params.push(...filters.userId);
1113
+ }
1114
+ else {
1115
+ conditions.push('userId = ?');
1116
+ params.push(filters.userId);
1117
+ }
1118
+ }
1119
+ // Outcome filter
1120
+ if (filters.outcome) {
1121
+ if (Array.isArray(filters.outcome)) {
1122
+ const placeholders = filters.outcome.map(() => '?').join(', ');
1123
+ conditions.push(`outcome IN (${placeholders})`);
1124
+ params.push(...filters.outcome);
1125
+ }
1126
+ else {
1127
+ conditions.push('outcome = ?');
1128
+ params.push(filters.outcome);
1129
+ }
1130
+ }
1131
+ // Risk level filter
1132
+ if (filters.riskLevel) {
1133
+ if (Array.isArray(filters.riskLevel)) {
1134
+ const placeholders = filters.riskLevel.map(() => '?').join(', ');
1135
+ conditions.push(`riskLevel IN (${placeholders})`);
1136
+ params.push(...filters.riskLevel);
1137
+ }
1138
+ else {
1139
+ conditions.push('riskLevel = ?');
1140
+ params.push(filters.riskLevel);
1141
+ }
1142
+ }
1143
+ // Date range filters
1144
+ if (filters.since) {
1145
+ const sinceStr = filters.since instanceof Date ? filters.since.toISOString() : filters.since;
1146
+ conditions.push('timestamp >= ?');
1147
+ params.push(sinceStr);
1148
+ }
1149
+ if (filters.until) {
1150
+ const untilStr = filters.until instanceof Date ? filters.until.toISOString() : filters.until;
1151
+ conditions.push('timestamp <= ?');
1152
+ params.push(untilStr);
1153
+ }
1154
+ // Correlation ID filter
1155
+ if (filters.correlationId) {
1156
+ conditions.push('correlationId = ?');
1157
+ params.push(filters.correlationId);
1158
+ }
1159
+ // Action partial match (case-insensitive)
1160
+ if (filters.action) {
1161
+ conditions.push('LOWER(action) LIKE ?');
1162
+ params.push(`%${filters.action.toLowerCase()}%`);
1163
+ }
1164
+ // Full-text search in metadata
1165
+ if (filters.metadataSearch) {
1166
+ conditions.push('LOWER(metadata) LIKE ?');
1167
+ params.push(`%${filters.metadataSearch.toLowerCase()}%`);
1168
+ }
1169
+ // Full-text search in details
1170
+ if (filters.detailsSearch) {
1171
+ conditions.push('LOWER(details) LIKE ?');
1172
+ params.push(`%${filters.detailsSearch.toLowerCase()}%`);
1173
+ }
1174
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
1175
+ return { whereClause, params };
1176
+ }
1177
+ /**
1178
+ * Convert database row to AuditEvent
1179
+ */
1180
+ rowToEvent(row) {
1181
+ return {
1182
+ eventId: row.eventId,
1183
+ eventType: row.eventType,
1184
+ action: row.action,
1185
+ timestamp: row.timestamp,
1186
+ outcome: row.outcome,
1187
+ agentId: row.agentId || undefined,
1188
+ sessionId: row.sessionId || undefined,
1189
+ userId: row.userId || undefined,
1190
+ riskLevel: row.riskLevel || undefined,
1191
+ details: row.details ? JSON.parse(row.details) : undefined,
1192
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
1193
+ correlationId: row.correlationId || undefined,
1194
+ parentEventId: row.parentEventId || undefined
1195
+ };
1196
+ }
292
1197
  /**
293
- * Query events
1198
+ * Execute query against memory cache
1199
+ */
1200
+ executeMemoryQuery(filters, pagination, orderByField, orderDirection) {
1201
+ let results = this.filterMemoryEvents(filters);
1202
+ // Sort
1203
+ results.sort((a, b) => {
1204
+ const aVal = a[orderByField] || '';
1205
+ const bVal = b[orderByField] || '';
1206
+ const comparison = String(aVal).localeCompare(String(bVal));
1207
+ return orderDirection === 'asc' ? comparison : -comparison;
1208
+ });
1209
+ // Pagination
1210
+ if (pagination.offset) {
1211
+ results = results.slice(pagination.offset);
1212
+ }
1213
+ if (pagination.limit) {
1214
+ results = results.slice(0, pagination.limit);
1215
+ }
1216
+ return results;
1217
+ }
1218
+ /**
1219
+ * Filter events from memory cache
1220
+ */
1221
+ filterMemoryEvents(filters) {
1222
+ return this.events.filter(event => {
1223
+ // Event type filter
1224
+ if (filters.eventType) {
1225
+ const types = Array.isArray(filters.eventType) ? filters.eventType : [filters.eventType];
1226
+ if (!types.includes(event.eventType))
1227
+ return false;
1228
+ }
1229
+ // Agent ID filter
1230
+ if (filters.agentId) {
1231
+ const ids = Array.isArray(filters.agentId) ? filters.agentId : [filters.agentId];
1232
+ if (!event.agentId || !ids.includes(event.agentId))
1233
+ return false;
1234
+ }
1235
+ // Session ID filter
1236
+ if (filters.sessionId) {
1237
+ const ids = Array.isArray(filters.sessionId) ? filters.sessionId : [filters.sessionId];
1238
+ if (!event.sessionId || !ids.includes(event.sessionId))
1239
+ return false;
1240
+ }
1241
+ // User ID filter
1242
+ if (filters.userId) {
1243
+ const ids = Array.isArray(filters.userId) ? filters.userId : [filters.userId];
1244
+ if (!event.userId || !ids.includes(event.userId))
1245
+ return false;
1246
+ }
1247
+ // Outcome filter
1248
+ if (filters.outcome) {
1249
+ const outcomes = Array.isArray(filters.outcome) ? filters.outcome : [filters.outcome];
1250
+ if (!outcomes.includes(event.outcome))
1251
+ return false;
1252
+ }
1253
+ // Risk level filter
1254
+ if (filters.riskLevel) {
1255
+ const levels = Array.isArray(filters.riskLevel) ? filters.riskLevel : [filters.riskLevel];
1256
+ if (!event.riskLevel || !levels.includes(event.riskLevel))
1257
+ return false;
1258
+ }
1259
+ // Date range filters
1260
+ if (filters.since) {
1261
+ const sinceStr = filters.since instanceof Date ? filters.since.toISOString() : filters.since;
1262
+ if (event.timestamp < sinceStr)
1263
+ return false;
1264
+ }
1265
+ if (filters.until) {
1266
+ const untilStr = filters.until instanceof Date ? filters.until.toISOString() : filters.until;
1267
+ if (event.timestamp > untilStr)
1268
+ return false;
1269
+ }
1270
+ // Correlation ID filter
1271
+ if (filters.correlationId) {
1272
+ if (event.correlationId !== filters.correlationId)
1273
+ return false;
1274
+ }
1275
+ // Action partial match
1276
+ if (filters.action) {
1277
+ if (!event.action.toLowerCase().includes(filters.action.toLowerCase()))
1278
+ return false;
1279
+ }
1280
+ // Full-text search in metadata
1281
+ if (filters.metadataSearch && event.metadata) {
1282
+ const metadataStr = JSON.stringify(event.metadata).toLowerCase();
1283
+ if (!metadataStr.includes(filters.metadataSearch.toLowerCase()))
1284
+ return false;
1285
+ }
1286
+ else if (filters.metadataSearch && !event.metadata) {
1287
+ return false;
1288
+ }
1289
+ // Full-text search in details
1290
+ if (filters.detailsSearch && event.details) {
1291
+ const detailsStr = JSON.stringify(event.details).toLowerCase();
1292
+ if (!detailsStr.includes(filters.detailsSearch.toLowerCase()))
1293
+ return false;
1294
+ }
1295
+ else if (filters.detailsSearch && !event.details) {
1296
+ return false;
1297
+ }
1298
+ return true;
1299
+ });
1300
+ }
1301
+ // ============================================================================
1302
+ // LEGACY QUERY METHODS (preserved for backward compatibility)
1303
+ // ============================================================================
1304
+ /**
1305
+ * Query events (legacy method - consider using query() instead)
1306
+ * @deprecated Use query() for more powerful filtering options
294
1307
  */
295
1308
  getEvents(filter) {
296
1309
  // If database is available, query from it
@@ -350,22 +1363,19 @@ export class AuditLogger {
350
1363
  }
351
1364
  /**
352
1365
  * Get summary statistics
1366
+ * Uses shared calculateGroupedCounts utility for consistency
353
1367
  */
354
1368
  getStats(since) {
355
1369
  let events = this.events;
356
1370
  if (since) {
357
- events = events.filter(e => e.timestamp >= since);
358
- }
359
- const byType = {};
360
- const byOutcome = {};
361
- const byRiskLevel = {};
362
- for (const event of events) {
363
- byType[event.eventType] = (byType[event.eventType] || 0) + 1;
364
- byOutcome[event.outcome] = (byOutcome[event.outcome] || 0) + 1;
365
- if (event.riskLevel) {
366
- byRiskLevel[event.riskLevel] = (byRiskLevel[event.riskLevel] || 0) + 1;
367
- }
1371
+ // Use time window for filtering if a time-based since is provided
1372
+ const sinceTime = new Date(since).getTime();
1373
+ events = events.filter(e => new Date(e.timestamp).getTime() >= sinceTime);
368
1374
  }
1375
+ // Use shared utility for grouped counts
1376
+ const byType = calculateGroupedCounts(events, e => e.eventType);
1377
+ const byOutcome = calculateGroupedCounts(events, e => e.outcome);
1378
+ const byRiskLevel = calculateGroupedCounts(events, e => e.riskLevel);
369
1379
  return {
370
1380
  total: events.length,
371
1381
  byType,
@@ -381,10 +1391,40 @@ export class AuditLogger {
381
1391
  return JSON.stringify(events, null, 2);
382
1392
  }
383
1393
  /**
384
- * Clear all events
1394
+ * Clear all events from memory cache
1395
+ * Note: This does NOT clear the database - use clearAll() for that
385
1396
  */
386
1397
  clear() {
387
1398
  this.events = [];
1399
+ this.failedWrites = [];
1400
+ }
1401
+ /**
1402
+ * Clear all events from both memory and database
1403
+ */
1404
+ clearAll() {
1405
+ this.events = [];
1406
+ this.failedWrites = [];
1407
+ if (this.db) {
1408
+ try {
1409
+ this.db.exec('DELETE FROM audit_events');
1410
+ console.log('[AuditLogger] Cleared all events from database');
1411
+ }
1412
+ catch (error) {
1413
+ console.error('[AuditLogger] Failed to clear database:', error);
1414
+ }
1415
+ }
1416
+ }
1417
+ /**
1418
+ * Get extended statistics including sync status
1419
+ */
1420
+ getExtendedStats() {
1421
+ const basicStats = this.getStats();
1422
+ return {
1423
+ ...basicStats,
1424
+ memoryCount: this.events.length,
1425
+ pendingRetries: this.failedWrites.length,
1426
+ lastSyncTime: this.lastSyncTime || null
1427
+ };
388
1428
  }
389
1429
  /**
390
1430
  * Log to console with formatting