@trentapps/manager-protocol 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +639 -0
  3. package/dist/analyzers/ArchitectureDetector.d.ts +44 -0
  4. package/dist/analyzers/ArchitectureDetector.d.ts.map +1 -0
  5. package/dist/analyzers/ArchitectureDetector.js +218 -0
  6. package/dist/analyzers/ArchitectureDetector.js.map +1 -0
  7. package/dist/analyzers/CSSAnalyzer.d.ts +284 -0
  8. package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -0
  9. package/dist/analyzers/CSSAnalyzer.js +1180 -0
  10. package/dist/analyzers/CSSAnalyzer.js.map +1 -0
  11. package/dist/analyzers/index.d.ts +5 -0
  12. package/dist/analyzers/index.d.ts.map +1 -0
  13. package/dist/analyzers/index.js +5 -0
  14. package/dist/analyzers/index.js.map +1 -0
  15. package/dist/cli.d.ts +8 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +174 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/design-system/index.d.ts +6 -0
  20. package/dist/design-system/index.d.ts.map +1 -0
  21. package/dist/design-system/index.js +6 -0
  22. package/dist/design-system/index.js.map +1 -0
  23. package/dist/design-system/tokens.d.ts +106 -0
  24. package/dist/design-system/tokens.d.ts.map +1 -0
  25. package/dist/design-system/tokens.js +554 -0
  26. package/dist/design-system/tokens.js.map +1 -0
  27. package/dist/engine/AuditLogger.d.ts +506 -0
  28. package/dist/engine/AuditLogger.d.ts.map +1 -0
  29. package/dist/engine/AuditLogger.js +1491 -0
  30. package/dist/engine/AuditLogger.js.map +1 -0
  31. package/dist/engine/GitHubApprovalManager.d.ts +123 -0
  32. package/dist/engine/GitHubApprovalManager.d.ts.map +1 -0
  33. package/dist/engine/GitHubApprovalManager.js +347 -0
  34. package/dist/engine/GitHubApprovalManager.js.map +1 -0
  35. package/dist/engine/GitHubClient.d.ts +183 -0
  36. package/dist/engine/GitHubClient.d.ts.map +1 -0
  37. package/dist/engine/GitHubClient.js +411 -0
  38. package/dist/engine/GitHubClient.js.map +1 -0
  39. package/dist/engine/RateLimiter.d.ts +81 -0
  40. package/dist/engine/RateLimiter.d.ts.map +1 -0
  41. package/dist/engine/RateLimiter.js +215 -0
  42. package/dist/engine/RateLimiter.js.map +1 -0
  43. package/dist/engine/RuleDependencyAnalyzer.d.ts +73 -0
  44. package/dist/engine/RuleDependencyAnalyzer.d.ts.map +1 -0
  45. package/dist/engine/RuleDependencyAnalyzer.js +475 -0
  46. package/dist/engine/RuleDependencyAnalyzer.js.map +1 -0
  47. package/dist/engine/RulesEngine.d.ts +176 -0
  48. package/dist/engine/RulesEngine.d.ts.map +1 -0
  49. package/dist/engine/RulesEngine.js +705 -0
  50. package/dist/engine/RulesEngine.js.map +1 -0
  51. package/dist/engine/TaskManager.d.ts +174 -0
  52. package/dist/engine/TaskManager.d.ts.map +1 -0
  53. package/dist/engine/TaskManager.js +663 -0
  54. package/dist/engine/TaskManager.js.map +1 -0
  55. package/dist/engine/index.d.ts +11 -0
  56. package/dist/engine/index.d.ts.map +1 -0
  57. package/dist/engine/index.js +13 -0
  58. package/dist/engine/index.js.map +1 -0
  59. package/dist/index.d.ts +21 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +29 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/rules/architecture.d.ts +9 -0
  64. package/dist/rules/architecture.d.ts.map +1 -0
  65. package/dist/rules/architecture.js +322 -0
  66. package/dist/rules/architecture.js.map +1 -0
  67. package/dist/rules/azure.d.ts +7 -0
  68. package/dist/rules/azure.d.ts.map +1 -0
  69. package/dist/rules/azure.js +136 -0
  70. package/dist/rules/azure.js.map +1 -0
  71. package/dist/rules/compliance.d.ts +9 -0
  72. package/dist/rules/compliance.d.ts.map +1 -0
  73. package/dist/rules/compliance.js +286 -0
  74. package/dist/rules/compliance.js.map +1 -0
  75. package/dist/rules/condition-optimizer.d.ts +151 -0
  76. package/dist/rules/condition-optimizer.d.ts.map +1 -0
  77. package/dist/rules/condition-optimizer.js +479 -0
  78. package/dist/rules/condition-optimizer.js.map +1 -0
  79. package/dist/rules/css.d.ts +10 -0
  80. package/dist/rules/css.d.ts.map +1 -0
  81. package/dist/rules/css.js +1777 -0
  82. package/dist/rules/css.js.map +1 -0
  83. package/dist/rules/field-standards.d.ts +1172 -0
  84. package/dist/rules/field-standards.d.ts.map +1 -0
  85. package/dist/rules/field-standards.js +908 -0
  86. package/dist/rules/field-standards.js.map +1 -0
  87. package/dist/rules/flask.d.ts +7 -0
  88. package/dist/rules/flask.d.ts.map +1 -0
  89. package/dist/rules/flask.js +142 -0
  90. package/dist/rules/flask.js.map +1 -0
  91. package/dist/rules/index.d.ts +827 -0
  92. package/dist/rules/index.d.ts.map +1 -0
  93. package/dist/rules/index.js +556 -0
  94. package/dist/rules/index.js.map +1 -0
  95. package/dist/rules/ml-ai.d.ts +7 -0
  96. package/dist/rules/ml-ai.d.ts.map +1 -0
  97. package/dist/rules/ml-ai.js +148 -0
  98. package/dist/rules/ml-ai.js.map +1 -0
  99. package/dist/rules/operational.d.ts +9 -0
  100. package/dist/rules/operational.d.ts.map +1 -0
  101. package/dist/rules/operational.js +318 -0
  102. package/dist/rules/operational.js.map +1 -0
  103. package/dist/rules/patterns.d.ts +568 -0
  104. package/dist/rules/patterns.d.ts.map +1 -0
  105. package/dist/rules/patterns.js +1359 -0
  106. package/dist/rules/patterns.js.map +1 -0
  107. package/dist/rules/security.d.ts +9 -0
  108. package/dist/rules/security.d.ts.map +1 -0
  109. package/dist/rules/security.js +848 -0
  110. package/dist/rules/security.js.map +1 -0
  111. package/dist/rules/shared-patterns.d.ts +268 -0
  112. package/dist/rules/shared-patterns.d.ts.map +1 -0
  113. package/dist/rules/shared-patterns.js +556 -0
  114. package/dist/rules/shared-patterns.js.map +1 -0
  115. package/dist/rules/storage.d.ts +13 -0
  116. package/dist/rules/storage.d.ts.map +1 -0
  117. package/dist/rules/storage.js +672 -0
  118. package/dist/rules/storage.js.map +1 -0
  119. package/dist/rules/stripe.d.ts +7 -0
  120. package/dist/rules/stripe.d.ts.map +1 -0
  121. package/dist/rules/stripe.js +133 -0
  122. package/dist/rules/stripe.js.map +1 -0
  123. package/dist/rules/testing.d.ts +7 -0
  124. package/dist/rules/testing.d.ts.map +1 -0
  125. package/dist/rules/testing.js +135 -0
  126. package/dist/rules/testing.js.map +1 -0
  127. package/dist/rules/ux.d.ts +9 -0
  128. package/dist/rules/ux.d.ts.map +1 -0
  129. package/dist/rules/ux.js +280 -0
  130. package/dist/rules/ux.js.map +1 -0
  131. package/dist/rules/websocket.d.ts +7 -0
  132. package/dist/rules/websocket.d.ts.map +1 -0
  133. package/dist/rules/websocket.js +128 -0
  134. package/dist/rules/websocket.js.map +1 -0
  135. package/dist/server.d.ts +43 -0
  136. package/dist/server.d.ts.map +1 -0
  137. package/dist/server.js +1967 -0
  138. package/dist/server.js.map +1 -0
  139. package/dist/supervisor/AgentSupervisor.d.ts +195 -0
  140. package/dist/supervisor/AgentSupervisor.d.ts.map +1 -0
  141. package/dist/supervisor/AgentSupervisor.js +569 -0
  142. package/dist/supervisor/AgentSupervisor.js.map +1 -0
  143. package/dist/supervisor/ManagedServerRegistry.d.ts +185 -0
  144. package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -0
  145. package/dist/supervisor/ManagedServerRegistry.js +729 -0
  146. package/dist/supervisor/ManagedServerRegistry.js.map +1 -0
  147. package/dist/supervisor/ProjectTracker.d.ts +210 -0
  148. package/dist/supervisor/ProjectTracker.d.ts.map +1 -0
  149. package/dist/supervisor/ProjectTracker.js +709 -0
  150. package/dist/supervisor/ProjectTracker.js.map +1 -0
  151. package/dist/supervisor/index.d.ts +6 -0
  152. package/dist/supervisor/index.d.ts.map +1 -0
  153. package/dist/supervisor/index.js +6 -0
  154. package/dist/supervisor/index.js.map +1 -0
  155. package/dist/testing/index.d.ts +11 -0
  156. package/dist/testing/index.d.ts.map +1 -0
  157. package/dist/testing/index.js +12 -0
  158. package/dist/testing/index.js.map +1 -0
  159. package/dist/testing/rule-tester.d.ts +217 -0
  160. package/dist/testing/rule-tester.d.ts.map +1 -0
  161. package/dist/testing/rule-tester.examples.d.ts +57 -0
  162. package/dist/testing/rule-tester.examples.d.ts.map +1 -0
  163. package/dist/testing/rule-tester.examples.js +375 -0
  164. package/dist/testing/rule-tester.examples.js.map +1 -0
  165. package/dist/testing/rule-tester.js +381 -0
  166. package/dist/testing/rule-tester.js.map +1 -0
  167. package/dist/testing/rule-validator.d.ts +141 -0
  168. package/dist/testing/rule-validator.d.ts.map +1 -0
  169. package/dist/testing/rule-validator.js +640 -0
  170. package/dist/testing/rule-validator.js.map +1 -0
  171. package/dist/types/index.d.ts +1282 -0
  172. package/dist/types/index.d.ts.map +1 -0
  173. package/dist/types/index.js +386 -0
  174. package/dist/types/index.js.map +1 -0
  175. package/dist/utils/errors.d.ts +86 -0
  176. package/dist/utils/errors.d.ts.map +1 -0
  177. package/dist/utils/errors.js +171 -0
  178. package/dist/utils/errors.js.map +1 -0
  179. package/dist/utils/index.d.ts +7 -0
  180. package/dist/utils/index.d.ts.map +1 -0
  181. package/dist/utils/index.js +7 -0
  182. package/dist/utils/index.js.map +1 -0
  183. package/dist/utils/rate-limiting.d.ts +268 -0
  184. package/dist/utils/rate-limiting.d.ts.map +1 -0
  185. package/dist/utils/rate-limiting.js +403 -0
  186. package/dist/utils/rate-limiting.js.map +1 -0
  187. package/dist/utils/shared.d.ts +306 -0
  188. package/dist/utils/shared.d.ts.map +1 -0
  189. package/dist/utils/shared.js +464 -0
  190. package/dist/utils/shared.js.map +1 -0
  191. package/dist/utils/shell.d.ts +22 -0
  192. package/dist/utils/shell.d.ts.map +1 -0
  193. package/dist/utils/shell.js +29 -0
  194. package/dist/utils/shell.js.map +1 -0
  195. package/package.json +67 -0
@@ -0,0 +1,1491 @@
1
+ /**
2
+ * Enterprise Agent Supervisor - Audit Logger
3
+ *
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
16
+ */
17
+ import { v4 as uuidv4 } from 'uuid';
18
+ import { mkdirSync } from 'fs';
19
+ import path from 'path';
20
+ import Database from 'better-sqlite3';
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
+ }
247
+ export class AuditLogger {
248
+ events = [];
249
+ maxEvents;
250
+ enableConsoleLog;
251
+ webhookUrl;
252
+ webhookRetries;
253
+ webhookTimeoutMs;
254
+ onEvent;
255
+ onWebhookError;
256
+ dbPath;
257
+ db;
258
+ initialized = false;
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;
269
+ constructor(options = {}) {
270
+ this.maxEvents = options.maxEvents || 10000;
271
+ this.enableConsoleLog = options.enableConsoleLog || false;
272
+ this.webhookUrl = options.webhookUrl;
273
+ this.webhookRetries = options.webhookRetries ?? 3;
274
+ this.webhookTimeoutMs = options.webhookTimeoutMs ?? 5000;
275
+ this.onEvent = options.onEvent;
276
+ this.onWebhookError = options.onWebhookError;
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
281
+ }
282
+ /**
283
+ * Initialize and setup SQLite database if configured
284
+ * Uses Promise-based locking to prevent race conditions in concurrent scenarios
285
+ */
286
+ async initialize() {
287
+ // If already initialized, return immediately
288
+ if (this.initialized)
289
+ return;
290
+ // If initialization is in progress, wait for it to complete
291
+ if (this.initPromise) {
292
+ return this.initPromise;
293
+ }
294
+ // Start initialization and store the promise
295
+ this.initPromise = this.doInitialize();
296
+ try {
297
+ await this.initPromise;
298
+ }
299
+ finally {
300
+ // Clear the promise reference once done (success or failure)
301
+ this.initPromise = undefined;
302
+ }
303
+ }
304
+ /**
305
+ * Internal initialization implementation
306
+ */
307
+ async doInitialize() {
308
+ if (this.dbPath) {
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
+ }
316
+ }
317
+ this.initialized = true;
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
+ }
582
+ /**
583
+ * Initialize SQLite database
584
+ */
585
+ async initDatabase() {
586
+ if (!this.dbPath)
587
+ return;
588
+ try {
589
+ // Ensure directory exists with secure permissions (owner-only: 0o700)
590
+ const dir = path.dirname(this.dbPath);
591
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
592
+ // Open database
593
+ this.db = new Database(this.dbPath);
594
+ // Create table if not exists
595
+ this.db.exec(`
596
+ CREATE TABLE IF NOT EXISTS audit_events (
597
+ eventId TEXT PRIMARY KEY,
598
+ eventType TEXT NOT NULL,
599
+ action TEXT NOT NULL,
600
+ timestamp TEXT NOT NULL,
601
+ outcome TEXT NOT NULL,
602
+ agentId TEXT,
603
+ sessionId TEXT,
604
+ userId TEXT,
605
+ riskLevel TEXT,
606
+ details TEXT,
607
+ metadata TEXT,
608
+ correlationId TEXT,
609
+ parentEventId TEXT
610
+ );
611
+
612
+ CREATE INDEX IF NOT EXISTS idx_timestamp ON audit_events(timestamp);
613
+ CREATE INDEX IF NOT EXISTS idx_eventType ON audit_events(eventType);
614
+ CREATE INDEX IF NOT EXISTS idx_outcome ON audit_events(outcome);
615
+ CREATE INDEX IF NOT EXISTS idx_correlationId ON audit_events(correlationId);
616
+ CREATE INDEX IF NOT EXISTS idx_agentId ON audit_events(agentId);
617
+ CREATE INDEX IF NOT EXISTS idx_sessionId ON audit_events(sessionId);
618
+ `);
619
+ console.log('[AuditLogger] SQLite database initialized at', this.dbPath);
620
+ }
621
+ catch (error) {
622
+ console.error('[AuditLogger] Failed to initialize database:', error);
623
+ }
624
+ }
625
+ /**
626
+ * Reload events from database
627
+ */
628
+ async reload() {
629
+ if (this.db) {
630
+ this.events = this.loadFromDatabase(this.maxEvents);
631
+ }
632
+ }
633
+ /**
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)
640
+ */
641
+ async log(params) {
642
+ const event = {
643
+ eventId: uuidv4(),
644
+ eventType: params.eventType,
645
+ action: params.action,
646
+ timestamp: new Date().toISOString(),
647
+ outcome: params.outcome,
648
+ agentId: params.agentId,
649
+ sessionId: params.sessionId,
650
+ userId: params.userId,
651
+ riskLevel: params.riskLevel,
652
+ details: params.details,
653
+ metadata: params.metadata,
654
+ correlationId: params.correlationId,
655
+ parentEventId: params.parentEventId
656
+ };
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
+ }
674
+ }
675
+ else {
676
+ // No database - memory only mode
677
+ this.addToMemoryCache(event);
678
+ }
679
+ // Console logging (always happens regardless of DB status)
680
+ if (this.enableConsoleLog) {
681
+ this.logToConsole(event);
682
+ }
683
+ // Webhook notification
684
+ if (this.webhookUrl) {
685
+ await this.sendWebhook(event);
686
+ }
687
+ // Custom callback
688
+ if (this.onEvent) {
689
+ await this.onEvent(event);
690
+ }
691
+ return event;
692
+ }
693
+ /**
694
+ * Quick log helpers
695
+ */
696
+ async logActionEvaluated(action, outcome, details) {
697
+ return this.log({
698
+ eventType: 'action_evaluated',
699
+ action,
700
+ outcome,
701
+ details
702
+ });
703
+ }
704
+ async logActionApproved(action, details) {
705
+ return this.log({
706
+ eventType: 'action_approved',
707
+ action,
708
+ outcome: 'success',
709
+ details
710
+ });
711
+ }
712
+ async logActionDenied(action, reason, riskLevel) {
713
+ return this.log({
714
+ eventType: 'action_denied',
715
+ action,
716
+ outcome: 'failure',
717
+ riskLevel,
718
+ details: { reason }
719
+ });
720
+ }
721
+ async logApprovalRequested(action, requestId, reason) {
722
+ return this.log({
723
+ eventType: 'approval_requested',
724
+ action,
725
+ outcome: 'pending',
726
+ details: { requestId, reason }
727
+ });
728
+ }
729
+ async logRateLimitHit(action, limitId, agentId) {
730
+ return this.log({
731
+ eventType: 'rate_limit_hit',
732
+ action,
733
+ outcome: 'failure',
734
+ agentId,
735
+ details: { limitId }
736
+ });
737
+ }
738
+ async logSecurityAlert(action, alertType, severity, details) {
739
+ return this.log({
740
+ eventType: 'security_alert',
741
+ action,
742
+ outcome: 'failure',
743
+ riskLevel: severity,
744
+ details: { alertType, ...details }
745
+ });
746
+ }
747
+ async logComplianceViolation(action, violation, framework) {
748
+ return this.log({
749
+ eventType: 'compliance_violation',
750
+ action,
751
+ outcome: 'failure',
752
+ riskLevel: 'high',
753
+ details: { violation, framework }
754
+ });
755
+ }
756
+ async logRuleTriggered(action, ruleId, ruleName, outcome) {
757
+ return this.log({
758
+ eventType: 'rule_triggered',
759
+ action,
760
+ outcome,
761
+ details: { ruleId, ruleName }
762
+ });
763
+ }
764
+ async logConfigChanged(action, changeType, details) {
765
+ return this.log({
766
+ eventType: 'config_changed',
767
+ action,
768
+ outcome: 'success',
769
+ details: { changeType, ...details }
770
+ });
771
+ }
772
+ /**
773
+ * Save event to database
774
+ * @returns true if save was successful, false otherwise
775
+ */
776
+ saveToDatabase(event) {
777
+ if (!this.db)
778
+ return false;
779
+ try {
780
+ const stmt = this.db.prepare(`
781
+ INSERT INTO audit_events (
782
+ eventId, eventType, action, timestamp, outcome,
783
+ agentId, sessionId, userId, riskLevel,
784
+ details, metadata, correlationId, parentEventId
785
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
786
+ `);
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;
789
+ }
790
+ catch (error) {
791
+ console.error('[AuditLogger] Failed to save to database:', error);
792
+ return false;
793
+ }
794
+ }
795
+ /**
796
+ * Load events from database
797
+ */
798
+ loadFromDatabase(limit) {
799
+ if (!this.db)
800
+ return [];
801
+ try {
802
+ const stmt = this.db.prepare(`
803
+ SELECT * FROM audit_events
804
+ ORDER BY timestamp DESC
805
+ LIMIT ?
806
+ `);
807
+ const rows = stmt.all(limit);
808
+ return rows.map(row => ({
809
+ eventId: row.eventId,
810
+ eventType: row.eventType,
811
+ action: row.action,
812
+ timestamp: row.timestamp,
813
+ outcome: row.outcome,
814
+ agentId: row.agentId,
815
+ sessionId: row.sessionId,
816
+ userId: row.userId,
817
+ riskLevel: row.riskLevel,
818
+ details: row.details ? JSON.parse(row.details) : undefined,
819
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
820
+ correlationId: row.correlationId,
821
+ parentEventId: row.parentEventId
822
+ })).reverse(); // Reverse to get chronological order
823
+ }
824
+ catch (error) {
825
+ console.error('[AuditLogger] Failed to load from database:', error);
826
+ return [];
827
+ }
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
+ }
1197
+ /**
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
1307
+ */
1308
+ getEvents(filter) {
1309
+ // If database is available, query from it
1310
+ if (this.db && filter?.limit) {
1311
+ return this.loadFromDatabase(filter.limit);
1312
+ }
1313
+ // Otherwise use in-memory events
1314
+ let result = [...this.events];
1315
+ if (filter) {
1316
+ if (filter.eventType) {
1317
+ result = result.filter(e => e.eventType === filter.eventType);
1318
+ }
1319
+ if (filter.agentId) {
1320
+ result = result.filter(e => e.agentId === filter.agentId);
1321
+ }
1322
+ if (filter.sessionId) {
1323
+ result = result.filter(e => e.sessionId === filter.sessionId);
1324
+ }
1325
+ if (filter.userId) {
1326
+ result = result.filter(e => e.userId === filter.userId);
1327
+ }
1328
+ if (filter.outcome) {
1329
+ result = result.filter(e => e.outcome === filter.outcome);
1330
+ }
1331
+ if (filter.riskLevel) {
1332
+ result = result.filter(e => e.riskLevel === filter.riskLevel);
1333
+ }
1334
+ if (filter.since) {
1335
+ result = result.filter(e => e.timestamp >= filter.since);
1336
+ }
1337
+ if (filter.until) {
1338
+ result = result.filter(e => e.timestamp <= filter.until);
1339
+ }
1340
+ if (filter.limit) {
1341
+ result = result.slice(-filter.limit);
1342
+ }
1343
+ }
1344
+ return result;
1345
+ }
1346
+ /**
1347
+ * Get event by ID
1348
+ */
1349
+ getEvent(eventId) {
1350
+ return this.events.find(e => e.eventId === eventId);
1351
+ }
1352
+ /**
1353
+ * Get events by correlation ID
1354
+ */
1355
+ getCorrelatedEvents(correlationId) {
1356
+ return this.events.filter(e => e.correlationId === correlationId);
1357
+ }
1358
+ /**
1359
+ * Get event count
1360
+ */
1361
+ getEventCount() {
1362
+ return this.events.length;
1363
+ }
1364
+ /**
1365
+ * Get summary statistics
1366
+ * Uses shared calculateGroupedCounts utility for consistency
1367
+ */
1368
+ getStats(since) {
1369
+ let events = this.events;
1370
+ if (since) {
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);
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);
1379
+ return {
1380
+ total: events.length,
1381
+ byType,
1382
+ byOutcome,
1383
+ byRiskLevel
1384
+ };
1385
+ }
1386
+ /**
1387
+ * Export events as JSON
1388
+ */
1389
+ exportEvents(filter) {
1390
+ const events = this.getEvents(filter);
1391
+ return JSON.stringify(events, null, 2);
1392
+ }
1393
+ /**
1394
+ * Clear all events from memory cache
1395
+ * Note: This does NOT clear the database - use clearAll() for that
1396
+ */
1397
+ clear() {
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
+ };
1428
+ }
1429
+ /**
1430
+ * Log to console with formatting
1431
+ */
1432
+ logToConsole(event) {
1433
+ const levelColors = {
1434
+ critical: '\x1b[31m', // Red
1435
+ high: '\x1b[33m', // Yellow
1436
+ medium: '\x1b[36m', // Cyan
1437
+ low: '\x1b[32m', // Green
1438
+ minimal: '\x1b[37m' // White
1439
+ };
1440
+ const reset = '\x1b[0m';
1441
+ const color = event.riskLevel ? levelColors[event.riskLevel] : '\x1b[37m';
1442
+ console.log(`${color}[${event.timestamp}] ${event.eventType.toUpperCase()} | ` +
1443
+ `${event.action} | ${event.outcome}${reset}`, event.details ? JSON.stringify(event.details) : '');
1444
+ }
1445
+ /**
1446
+ * Send event to webhook with retry logic
1447
+ */
1448
+ async sendWebhook(event) {
1449
+ if (!this.webhookUrl) {
1450
+ return;
1451
+ }
1452
+ const url = this.webhookUrl;
1453
+ const timeoutMs = this.webhookTimeoutMs;
1454
+ try {
1455
+ await withRetry(async () => {
1456
+ const controller = new AbortController();
1457
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1458
+ try {
1459
+ const response = await fetch(url, {
1460
+ method: 'POST',
1461
+ headers: {
1462
+ 'Content-Type': 'application/json',
1463
+ 'X-Event-Id': event.eventId,
1464
+ 'X-Event-Type': event.eventType
1465
+ },
1466
+ body: JSON.stringify(event),
1467
+ signal: controller.signal
1468
+ });
1469
+ if (!response.ok) {
1470
+ throw new WebhookDeliveryError(url, `HTTP ${response.status}: ${response.statusText}`, response.status);
1471
+ }
1472
+ }
1473
+ finally {
1474
+ clearTimeout(timeoutId);
1475
+ }
1476
+ }, { maxRetries: this.webhookRetries });
1477
+ }
1478
+ catch (error) {
1479
+ const webhookError = error instanceof Error ? error : new Error(String(error));
1480
+ // Call error callback if provided
1481
+ if (this.onWebhookError) {
1482
+ this.onWebhookError(webhookError, event);
1483
+ }
1484
+ // Log to console with formatted error
1485
+ console.error(`Failed to send audit event to webhook: ${formatError(error)}`);
1486
+ }
1487
+ }
1488
+ }
1489
+ // Export singleton instance
1490
+ export const auditLogger = new AuditLogger();
1491
+ //# sourceMappingURL=AuditLogger.js.map