tlc-claude-code 1.2.29 → 1.4.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 (182) hide show
  1. package/dashboard/dist/components/AuditPane.d.ts +30 -0
  2. package/dashboard/dist/components/AuditPane.js +127 -0
  3. package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/AuditPane.test.js +339 -0
  5. package/dashboard/dist/components/CompliancePane.d.ts +39 -0
  6. package/dashboard/dist/components/CompliancePane.js +96 -0
  7. package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
  8. package/dashboard/dist/components/CompliancePane.test.js +183 -0
  9. package/dashboard/dist/components/SSOPane.d.ts +36 -0
  10. package/dashboard/dist/components/SSOPane.js +71 -0
  11. package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
  12. package/dashboard/dist/components/SSOPane.test.js +155 -0
  13. package/dashboard/dist/components/UsagePane.d.ts +13 -0
  14. package/dashboard/dist/components/UsagePane.js +51 -0
  15. package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
  16. package/dashboard/dist/components/UsagePane.test.js +142 -0
  17. package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
  18. package/dashboard/dist/components/WorkspaceDocsPane.js +130 -0
  19. package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
  20. package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
  21. package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
  22. package/dashboard/dist/components/WorkspacePane.js +17 -0
  23. package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
  24. package/dashboard/dist/components/WorkspacePane.test.js +84 -0
  25. package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
  26. package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
  27. package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
  28. package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
  29. package/package.json +1 -1
  30. package/server/lib/access-control-doc.js +541 -0
  31. package/server/lib/access-control-doc.test.js +672 -0
  32. package/server/lib/adr-generator.js +423 -0
  33. package/server/lib/adr-generator.test.js +586 -0
  34. package/server/lib/agent-progress-monitor.js +223 -0
  35. package/server/lib/agent-progress-monitor.test.js +202 -0
  36. package/server/lib/architecture-command.js +450 -0
  37. package/server/lib/architecture-command.test.js +754 -0
  38. package/server/lib/ast-analyzer.js +324 -0
  39. package/server/lib/ast-analyzer.test.js +437 -0
  40. package/server/lib/audit-attribution.js +191 -0
  41. package/server/lib/audit-attribution.test.js +359 -0
  42. package/server/lib/audit-classifier.js +202 -0
  43. package/server/lib/audit-classifier.test.js +209 -0
  44. package/server/lib/audit-command.js +275 -0
  45. package/server/lib/audit-command.test.js +325 -0
  46. package/server/lib/audit-exporter.js +380 -0
  47. package/server/lib/audit-exporter.test.js +464 -0
  48. package/server/lib/audit-logger.js +236 -0
  49. package/server/lib/audit-logger.test.js +364 -0
  50. package/server/lib/audit-query.js +257 -0
  51. package/server/lib/audit-query.test.js +352 -0
  52. package/server/lib/audit-storage.js +269 -0
  53. package/server/lib/audit-storage.test.js +272 -0
  54. package/server/lib/auth-system.test.js +4 -1
  55. package/server/lib/boundary-detector.js +427 -0
  56. package/server/lib/boundary-detector.test.js +320 -0
  57. package/server/lib/budget-alerts.js +138 -0
  58. package/server/lib/budget-alerts.test.js +235 -0
  59. package/server/lib/bulk-repo-init.js +342 -0
  60. package/server/lib/bulk-repo-init.test.js +388 -0
  61. package/server/lib/candidates-tracker.js +210 -0
  62. package/server/lib/candidates-tracker.test.js +300 -0
  63. package/server/lib/checkpoint-manager.js +251 -0
  64. package/server/lib/checkpoint-manager.test.js +474 -0
  65. package/server/lib/circular-detector.js +337 -0
  66. package/server/lib/circular-detector.test.js +353 -0
  67. package/server/lib/cohesion-analyzer.js +310 -0
  68. package/server/lib/cohesion-analyzer.test.js +447 -0
  69. package/server/lib/compliance-checklist.js +866 -0
  70. package/server/lib/compliance-checklist.test.js +476 -0
  71. package/server/lib/compliance-command.js +616 -0
  72. package/server/lib/compliance-command.test.js +551 -0
  73. package/server/lib/compliance-reporter.js +692 -0
  74. package/server/lib/compliance-reporter.test.js +707 -0
  75. package/server/lib/contract-testing.js +625 -0
  76. package/server/lib/contract-testing.test.js +342 -0
  77. package/server/lib/conversion-planner.js +469 -0
  78. package/server/lib/conversion-planner.test.js +361 -0
  79. package/server/lib/convert-command.js +351 -0
  80. package/server/lib/convert-command.test.js +608 -0
  81. package/server/lib/coupling-calculator.js +189 -0
  82. package/server/lib/coupling-calculator.test.js +509 -0
  83. package/server/lib/data-flow-doc.js +665 -0
  84. package/server/lib/data-flow-doc.test.js +659 -0
  85. package/server/lib/dependency-graph.js +367 -0
  86. package/server/lib/dependency-graph.test.js +516 -0
  87. package/server/lib/duplication-detector.js +349 -0
  88. package/server/lib/duplication-detector.test.js +401 -0
  89. package/server/lib/ephemeral-storage.js +249 -0
  90. package/server/lib/ephemeral-storage.test.js +254 -0
  91. package/server/lib/evidence-collector.js +627 -0
  92. package/server/lib/evidence-collector.test.js +901 -0
  93. package/server/lib/example-service.js +616 -0
  94. package/server/lib/example-service.test.js +397 -0
  95. package/server/lib/flow-diagram-generator.js +474 -0
  96. package/server/lib/flow-diagram-generator.test.js +446 -0
  97. package/server/lib/idp-manager.js +626 -0
  98. package/server/lib/idp-manager.test.js +587 -0
  99. package/server/lib/impact-scorer.js +184 -0
  100. package/server/lib/impact-scorer.test.js +211 -0
  101. package/server/lib/memory-exclusion.js +326 -0
  102. package/server/lib/memory-exclusion.test.js +241 -0
  103. package/server/lib/mermaid-generator.js +358 -0
  104. package/server/lib/mermaid-generator.test.js +301 -0
  105. package/server/lib/messaging-patterns.js +750 -0
  106. package/server/lib/messaging-patterns.test.js +213 -0
  107. package/server/lib/mfa-handler.js +452 -0
  108. package/server/lib/mfa-handler.test.js +490 -0
  109. package/server/lib/microservice-template.js +386 -0
  110. package/server/lib/microservice-template.test.js +325 -0
  111. package/server/lib/new-project-microservice.js +450 -0
  112. package/server/lib/new-project-microservice.test.js +600 -0
  113. package/server/lib/oauth-flow.js +375 -0
  114. package/server/lib/oauth-flow.test.js +487 -0
  115. package/server/lib/oauth-registry.js +190 -0
  116. package/server/lib/oauth-registry.test.js +306 -0
  117. package/server/lib/readme-generator.js +490 -0
  118. package/server/lib/readme-generator.test.js +493 -0
  119. package/server/lib/refactor-command.js +326 -0
  120. package/server/lib/refactor-command.test.js +528 -0
  121. package/server/lib/refactor-executor.js +254 -0
  122. package/server/lib/refactor-executor.test.js +305 -0
  123. package/server/lib/refactor-observer.js +292 -0
  124. package/server/lib/refactor-observer.test.js +422 -0
  125. package/server/lib/refactor-progress.js +193 -0
  126. package/server/lib/refactor-progress.test.js +251 -0
  127. package/server/lib/refactor-reporter.js +237 -0
  128. package/server/lib/refactor-reporter.test.js +247 -0
  129. package/server/lib/repo-dependency-tracker.js +261 -0
  130. package/server/lib/repo-dependency-tracker.test.js +350 -0
  131. package/server/lib/retention-policy.js +281 -0
  132. package/server/lib/retention-policy.test.js +486 -0
  133. package/server/lib/role-mapper.js +236 -0
  134. package/server/lib/role-mapper.test.js +395 -0
  135. package/server/lib/saml-provider.js +765 -0
  136. package/server/lib/saml-provider.test.js +643 -0
  137. package/server/lib/security-policy-generator.js +682 -0
  138. package/server/lib/security-policy-generator.test.js +544 -0
  139. package/server/lib/semantic-analyzer.js +198 -0
  140. package/server/lib/semantic-analyzer.test.js +474 -0
  141. package/server/lib/sensitive-detector.js +112 -0
  142. package/server/lib/sensitive-detector.test.js +209 -0
  143. package/server/lib/service-interaction-diagram.js +700 -0
  144. package/server/lib/service-interaction-diagram.test.js +638 -0
  145. package/server/lib/service-scaffold.js +486 -0
  146. package/server/lib/service-scaffold.test.js +373 -0
  147. package/server/lib/service-summary.js +553 -0
  148. package/server/lib/service-summary.test.js +619 -0
  149. package/server/lib/session-purge.js +460 -0
  150. package/server/lib/session-purge.test.js +312 -0
  151. package/server/lib/shared-kernel.js +578 -0
  152. package/server/lib/shared-kernel.test.js +255 -0
  153. package/server/lib/sso-command.js +544 -0
  154. package/server/lib/sso-command.test.js +552 -0
  155. package/server/lib/sso-session.js +492 -0
  156. package/server/lib/sso-session.test.js +670 -0
  157. package/server/lib/traefik-config.js +282 -0
  158. package/server/lib/traefik-config.test.js +312 -0
  159. package/server/lib/usage-command.js +218 -0
  160. package/server/lib/usage-command.test.js +391 -0
  161. package/server/lib/usage-formatter.js +192 -0
  162. package/server/lib/usage-formatter.test.js +267 -0
  163. package/server/lib/usage-history.js +122 -0
  164. package/server/lib/usage-history.test.js +206 -0
  165. package/server/lib/workspace-command.js +249 -0
  166. package/server/lib/workspace-command.test.js +264 -0
  167. package/server/lib/workspace-config.js +270 -0
  168. package/server/lib/workspace-config.test.js +312 -0
  169. package/server/lib/workspace-docs-command.js +547 -0
  170. package/server/lib/workspace-docs-command.test.js +692 -0
  171. package/server/lib/workspace-memory.js +451 -0
  172. package/server/lib/workspace-memory.test.js +403 -0
  173. package/server/lib/workspace-scanner.js +452 -0
  174. package/server/lib/workspace-scanner.test.js +677 -0
  175. package/server/lib/workspace-test-runner.js +315 -0
  176. package/server/lib/workspace-test-runner.test.js +294 -0
  177. package/server/lib/zero-retention-command.js +439 -0
  178. package/server/lib/zero-retention-command.test.js +448 -0
  179. package/server/lib/zero-retention.js +322 -0
  180. package/server/lib/zero-retention.test.js +258 -0
  181. package/server/package-lock.json +14 -0
  182. package/server/package.json +1 -0
@@ -0,0 +1,364 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+
3
+ // Mock dependencies before importing
4
+ vi.mock('./audit-storage.js', () => {
5
+ class MockAuditStorage {
6
+ constructor() {
7
+ this.entries = [];
8
+ }
9
+ async appendEntry(entry) {
10
+ this.entries.push(entry);
11
+ return { ...entry, checksum: 'mock-checksum' };
12
+ }
13
+ async getEntries() {
14
+ return this.entries;
15
+ }
16
+ async verifyIntegrity() {
17
+ return { valid: true };
18
+ }
19
+ }
20
+ return {
21
+ AuditStorage: MockAuditStorage,
22
+ };
23
+ });
24
+
25
+ vi.mock('./audit-classifier.js', () => ({
26
+ classifyAction: vi.fn().mockReturnValue('file:read'),
27
+ detectSensitive: vi.fn().mockReturnValue({ isSensitive: false, reason: null }),
28
+ getSeverity: vi.fn().mockReturnValue('info'),
29
+ }));
30
+
31
+ vi.mock('./audit-attribution.js', () => ({
32
+ getAttribution: vi.fn().mockResolvedValue({
33
+ user: { name: 'testuser', email: 'test@example.com' },
34
+ source: 'git',
35
+ timestamp: '2026-01-15T10:00:00.000Z',
36
+ }),
37
+ identifySource: vi.fn().mockReturnValue('agent'),
38
+ createSessionId: vi.fn().mockReturnValue('test-session-123'),
39
+ }));
40
+
41
+ import { AuditLogger, sanitizeParams } from './audit-logger.js';
42
+ import { classifyAction, detectSensitive, getSeverity } from './audit-classifier.js';
43
+ import { getAttribution, identifySource, createSessionId } from './audit-attribution.js';
44
+
45
+ describe('AuditLogger', () => {
46
+ let logger;
47
+
48
+ beforeEach(() => {
49
+ vi.clearAllMocks();
50
+ vi.useFakeTimers();
51
+ vi.setSystemTime(new Date('2026-01-15T10:00:00.000Z'));
52
+
53
+ logger = new AuditLogger();
54
+ });
55
+
56
+ afterEach(() => {
57
+ vi.useRealTimers();
58
+ });
59
+
60
+ describe('logAction creates complete audit entry', () => {
61
+ it('creates entry with tool name, params, and result', async () => {
62
+ const entry = await logger.logAction('Read', { file_path: '/test/file.js' }, { content: 'file content' });
63
+
64
+ expect(entry).toBeDefined();
65
+ expect(entry.tool).toBe('Read');
66
+ expect(entry.params).toBeDefined();
67
+ expect(entry.result).toEqual({ content: 'file content' });
68
+ });
69
+
70
+ it('stores complete context for replay', async () => {
71
+ const params = { file_path: '/test/file.js', offset: 0, limit: 100 };
72
+ const result = { content: 'file content', lines: 100 };
73
+
74
+ const entry = await logger.logAction('Read', params, result);
75
+
76
+ expect(entry.tool).toBe('Read');
77
+ expect(entry.result).toEqual(result);
78
+ expect(entry.context).toBeDefined();
79
+ });
80
+ });
81
+
82
+ describe('logAction includes classification', () => {
83
+ it('includes classification from classifier module', async () => {
84
+ classifyAction.mockReturnValueOnce('file:write');
85
+ getSeverity.mockReturnValueOnce('warning');
86
+ detectSensitive.mockReturnValueOnce({ isSensitive: false, reason: null });
87
+
88
+ const entry = await logger.logAction('Write', { file_path: '/test/file.js', content: 'new content' }, { success: true });
89
+
90
+ expect(classifyAction).toHaveBeenCalledWith({ tool: 'Write', params: expect.any(Object) });
91
+ expect(entry.classification).toBe('file:write');
92
+ expect(entry.severity).toBe('warning');
93
+ });
94
+
95
+ it('includes sensitive flag from classifier', async () => {
96
+ detectSensitive.mockReturnValueOnce({ isSensitive: true, reason: 'Accessing .env file' });
97
+
98
+ const entry = await logger.logAction('Read', { file_path: '.env' }, { content: 'secrets' });
99
+
100
+ expect(detectSensitive).toHaveBeenCalled();
101
+ expect(entry.sensitive).toEqual({ isSensitive: true, reason: 'Accessing .env file' });
102
+ });
103
+ });
104
+
105
+ describe('logAction includes attribution', () => {
106
+ it('includes user attribution from attribution module', async () => {
107
+ getAttribution.mockResolvedValueOnce({
108
+ user: { name: 'alice', email: 'alice@example.com' },
109
+ source: 'git',
110
+ timestamp: '2026-01-15T10:00:00.000Z',
111
+ });
112
+
113
+ const entry = await logger.logAction('Read', { file_path: '/test/file.js' }, { content: 'data' });
114
+
115
+ expect(getAttribution).toHaveBeenCalled();
116
+ expect(entry.attribution.user.name).toBe('alice');
117
+ expect(entry.attribution.source).toBe('git');
118
+ });
119
+
120
+ it('includes source identification', async () => {
121
+ identifySource.mockReturnValueOnce('human');
122
+
123
+ const entry = await logger.logAction('Bash', { command: 'ls -la' }, { output: 'files' });
124
+
125
+ expect(identifySource).toHaveBeenCalled();
126
+ expect(entry.sourceType).toBe('human');
127
+ });
128
+
129
+ it('includes session ID for correlating actions', async () => {
130
+ createSessionId.mockReturnValueOnce('session-abc-123');
131
+
132
+ const loggerWithSession = new AuditLogger();
133
+ const entry = await loggerWithSession.logAction('Read', { file_path: '/test/file.js' }, { content: 'data' });
134
+
135
+ expect(entry.sessionId).toBeDefined();
136
+ });
137
+ });
138
+
139
+ describe('logAction includes timestamp', () => {
140
+ it('adds timestamp in ISO 8601 format', async () => {
141
+ const entry = await logger.logAction('Read', { file_path: '/test/file.js' }, { content: 'data' });
142
+
143
+ expect(entry.timestamp).toBe('2026-01-15T10:00:00.000Z');
144
+ });
145
+
146
+ it('uses current time for timestamp', async () => {
147
+ vi.setSystemTime(new Date('2026-02-20T15:30:45.123Z'));
148
+
149
+ const loggerNew = new AuditLogger();
150
+ const entry = await loggerNew.logAction('Read', { file_path: '/test/file.js' }, { content: 'data' });
151
+
152
+ expect(entry.timestamp).toBe('2026-02-20T15:30:45.123Z');
153
+ });
154
+ });
155
+
156
+ describe('logAction stores tool parameters (sanitized)', () => {
157
+ it('stores sanitized parameters', async () => {
158
+ const params = {
159
+ file_path: '/test/file.js',
160
+ content: 'API_KEY=sk-secret123\nPASSWORD=mypassword',
161
+ };
162
+
163
+ const entry = await logger.logAction('Write', params, { success: true });
164
+
165
+ // Params should be sanitized
166
+ expect(entry.params.content).not.toContain('sk-secret123');
167
+ expect(entry.params.content).not.toContain('mypassword');
168
+ });
169
+
170
+ it('preserves non-sensitive parameters', async () => {
171
+ const params = {
172
+ file_path: '/test/file.js',
173
+ offset: 10,
174
+ limit: 100,
175
+ };
176
+
177
+ const entry = await logger.logAction('Read', params, { content: 'data' });
178
+
179
+ expect(entry.params.file_path).toBe('/test/file.js');
180
+ expect(entry.params.offset).toBe(10);
181
+ expect(entry.params.limit).toBe(100);
182
+ });
183
+ });
184
+
185
+ describe('logAction handles async batch mode', () => {
186
+ it('batches entries when batch mode is enabled', async () => {
187
+ const batchLogger = new AuditLogger({ batchMode: true, batchSize: 3 });
188
+
189
+ await batchLogger.logAction('Read', { file_path: '/file1.js' }, { content: 'a' });
190
+ await batchLogger.logAction('Read', { file_path: '/file2.js' }, { content: 'b' });
191
+
192
+ // Should not have written yet (batch size is 3)
193
+ expect(batchLogger.getPendingCount()).toBe(2);
194
+ });
195
+
196
+ it('flushes when batch size is reached', async () => {
197
+ const batchLogger = new AuditLogger({ batchMode: true, batchSize: 2 });
198
+
199
+ await batchLogger.logAction('Read', { file_path: '/file1.js' }, { content: 'a' });
200
+ await batchLogger.logAction('Read', { file_path: '/file2.js' }, { content: 'b' });
201
+
202
+ // Should have flushed after reaching batch size
203
+ expect(batchLogger.getPendingCount()).toBe(0);
204
+ });
205
+ });
206
+
207
+ describe('flushBatch writes pending entries', () => {
208
+ it('writes all pending entries to storage', async () => {
209
+ const batchLogger = new AuditLogger({ batchMode: true, batchSize: 10 });
210
+
211
+ await batchLogger.logAction('Read', { file_path: '/file1.js' }, { content: 'a' });
212
+ await batchLogger.logAction('Read', { file_path: '/file2.js' }, { content: 'b' });
213
+ await batchLogger.logAction('Read', { file_path: '/file3.js' }, { content: 'c' });
214
+
215
+ expect(batchLogger.getPendingCount()).toBe(3);
216
+
217
+ await batchLogger.flushBatch();
218
+
219
+ expect(batchLogger.getPendingCount()).toBe(0);
220
+ });
221
+
222
+ it('handles empty batch gracefully', async () => {
223
+ const batchLogger = new AuditLogger({ batchMode: true, batchSize: 10 });
224
+
225
+ // Should not throw
226
+ await expect(batchLogger.flushBatch()).resolves.not.toThrow();
227
+ });
228
+ });
229
+ });
230
+
231
+ describe('sanitizeParams', () => {
232
+ describe('sanitizeParams removes sensitive values', () => {
233
+ it('removes API keys', () => {
234
+ const params = {
235
+ content: 'API_KEY=sk-secret123abcdef',
236
+ };
237
+
238
+ const sanitized = sanitizeParams(params);
239
+
240
+ expect(sanitized.content).not.toContain('sk-secret123abcdef');
241
+ expect(sanitized.content).toContain('[REDACTED]');
242
+ });
243
+
244
+ it('removes passwords', () => {
245
+ const params = {
246
+ content: 'password=supersecret123',
247
+ };
248
+
249
+ const sanitized = sanitizeParams(params);
250
+
251
+ expect(sanitized.content).not.toContain('supersecret123');
252
+ expect(sanitized.content).toContain('[REDACTED]');
253
+ });
254
+
255
+ it('removes tokens', () => {
256
+ const params = {
257
+ content: 'token=ghp_1234567890abcdef',
258
+ };
259
+
260
+ const sanitized = sanitizeParams(params);
261
+
262
+ expect(sanitized.content).not.toContain('ghp_1234567890abcdef');
263
+ expect(sanitized.content).toContain('[REDACTED]');
264
+ });
265
+
266
+ it('removes secrets from command', () => {
267
+ const params = {
268
+ command: 'export SECRET=mysecretvalue && npm run build',
269
+ };
270
+
271
+ const sanitized = sanitizeParams(params);
272
+
273
+ expect(sanitized.command).not.toContain('mysecretvalue');
274
+ expect(sanitized.command).toContain('[REDACTED]');
275
+ });
276
+
277
+ it('removes Bearer tokens', () => {
278
+ const params = {
279
+ content: 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
280
+ };
281
+
282
+ const sanitized = sanitizeParams(params);
283
+
284
+ expect(sanitized.content).not.toContain('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9');
285
+ expect(sanitized.content).toContain('[REDACTED]');
286
+ });
287
+ });
288
+
289
+ describe('sanitizeParams preserves structure', () => {
290
+ it('preserves object structure', () => {
291
+ const params = {
292
+ file_path: '/test/file.js',
293
+ offset: 10,
294
+ limit: 100,
295
+ nested: {
296
+ value: 'safe',
297
+ deep: {
298
+ key: 'value',
299
+ },
300
+ },
301
+ };
302
+
303
+ const sanitized = sanitizeParams(params);
304
+
305
+ expect(sanitized.file_path).toBe('/test/file.js');
306
+ expect(sanitized.offset).toBe(10);
307
+ expect(sanitized.limit).toBe(100);
308
+ expect(sanitized.nested.value).toBe('safe');
309
+ expect(sanitized.nested.deep.key).toBe('value');
310
+ });
311
+
312
+ it('preserves arrays', () => {
313
+ const params = {
314
+ files: ['/file1.js', '/file2.js'],
315
+ numbers: [1, 2, 3],
316
+ };
317
+
318
+ const sanitized = sanitizeParams(params);
319
+
320
+ expect(sanitized.files).toEqual(['/file1.js', '/file2.js']);
321
+ expect(sanitized.numbers).toEqual([1, 2, 3]);
322
+ });
323
+
324
+ it('handles null and undefined values', () => {
325
+ const params = {
326
+ nullValue: null,
327
+ undefinedValue: undefined,
328
+ validValue: 'test',
329
+ };
330
+
331
+ const sanitized = sanitizeParams(params);
332
+
333
+ expect(sanitized.nullValue).toBeNull();
334
+ expect(sanitized.undefinedValue).toBeUndefined();
335
+ expect(sanitized.validValue).toBe('test');
336
+ });
337
+
338
+ it('sanitizes sensitive values in nested objects', () => {
339
+ const params = {
340
+ config: {
341
+ api_key: 'sk-secret123',
342
+ endpoint: 'https://api.example.com',
343
+ },
344
+ };
345
+
346
+ const sanitized = sanitizeParams(params);
347
+
348
+ expect(sanitized.config.api_key).toBe('[REDACTED]');
349
+ expect(sanitized.config.endpoint).toBe('https://api.example.com');
350
+ });
351
+
352
+ it('sanitizes sensitive values in arrays', () => {
353
+ const params = {
354
+ items: ['password=secret1', 'normal value', 'token=abc123'],
355
+ };
356
+
357
+ const sanitized = sanitizeParams(params);
358
+
359
+ expect(sanitized.items[0]).toContain('[REDACTED]');
360
+ expect(sanitized.items[1]).toBe('normal value');
361
+ expect(sanitized.items[2]).toContain('[REDACTED]');
362
+ });
363
+ });
364
+ });
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Audit Query Engine - Search and filter audit logs
3
+ *
4
+ * Features:
5
+ * - Filter by date range
6
+ * - Filter by action type (exact match or wildcard)
7
+ * - Filter by user (single or multiple)
8
+ * - Filter by severity (single, multiple, or minimum level)
9
+ * - Full-text search in parameters
10
+ * - Pagination support
11
+ * - Count-only queries
12
+ */
13
+
14
+ // Severity levels in order from lowest to highest
15
+ const SEVERITY_LEVELS = ['debug', 'info', 'warning', 'error', 'critical'];
16
+
17
+ export class AuditQuery {
18
+ /**
19
+ * Create an AuditQuery instance
20
+ * @param {AuditStorage} storage - AuditStorage instance to query
21
+ */
22
+ constructor(storage) {
23
+ this.storage = storage;
24
+ }
25
+
26
+ /**
27
+ * Query audit log entries with filters
28
+ * @param {Object} options - Query options
29
+ * @param {Date} options.from - Start date (inclusive)
30
+ * @param {Date} options.to - End date (inclusive)
31
+ * @param {string} options.action - Action type filter (supports wildcard with *)
32
+ * @param {string|string[]} options.user - User filter (single or array)
33
+ * @param {string|string[]} options.severity - Severity filter (single or array)
34
+ * @param {string} options.minSeverity - Minimum severity level
35
+ * @param {string} options.search - Full-text search in parameters
36
+ * @param {number} options.limit - Maximum entries to return
37
+ * @param {number} options.offset - Number of entries to skip
38
+ * @param {number} options.page - Page number (alternative to offset)
39
+ * @param {string} options.sort - Sort order: 'asc' or 'desc' (default: 'desc')
40
+ * @param {boolean} options.countOnly - Return only count, not entries
41
+ * @returns {Promise<Object>} Query result { entries, total, hasMore, page }
42
+ */
43
+ async query(options = {}) {
44
+ const {
45
+ from,
46
+ to,
47
+ action,
48
+ user,
49
+ severity,
50
+ minSeverity,
51
+ search,
52
+ limit,
53
+ offset,
54
+ page,
55
+ sort = 'desc',
56
+ countOnly = false,
57
+ } = options;
58
+
59
+ // Get all entries from storage (with basic date filtering if supported)
60
+ let entries = await this.storage.getEntries({ from, to });
61
+
62
+ // Apply action filter
63
+ if (action) {
64
+ entries = this.filterByAction(entries, action);
65
+ }
66
+
67
+ // Apply user filter
68
+ if (user) {
69
+ entries = this.filterByUser(entries, user);
70
+ }
71
+
72
+ // Apply severity filter
73
+ if (severity) {
74
+ entries = this.filterBySeverity(entries, severity);
75
+ }
76
+
77
+ // Apply minimum severity filter
78
+ if (minSeverity) {
79
+ entries = this.filterByMinSeverity(entries, minSeverity);
80
+ }
81
+
82
+ // Apply search filter
83
+ if (search) {
84
+ entries = this.filterBySearch(entries, search);
85
+ }
86
+
87
+ // Sort entries
88
+ entries = this.sortEntries(entries, sort);
89
+
90
+ // Get total count
91
+ const total = entries.length;
92
+
93
+ // If count only, return early
94
+ if (countOnly) {
95
+ return { total };
96
+ }
97
+
98
+ // Apply pagination
99
+ const { paginatedEntries, hasMore, currentPage } = this.paginate(
100
+ entries,
101
+ limit,
102
+ offset,
103
+ page
104
+ );
105
+
106
+ return {
107
+ entries: paginatedEntries,
108
+ total,
109
+ hasMore,
110
+ page: currentPage,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Filter entries by action type
116
+ * @param {Object[]} entries - Entries to filter
117
+ * @param {string} action - Action filter (supports * wildcard)
118
+ * @returns {Object[]} Filtered entries
119
+ */
120
+ filterByAction(entries, action) {
121
+ if (action.endsWith('*')) {
122
+ // Wildcard prefix match
123
+ const prefix = action.slice(0, -1);
124
+ return entries.filter((e) => e.action && e.action.startsWith(prefix));
125
+ }
126
+ // Exact match
127
+ return entries.filter((e) => e.action === action);
128
+ }
129
+
130
+ /**
131
+ * Filter entries by user
132
+ * @param {Object[]} entries - Entries to filter
133
+ * @param {string|string[]} user - User or array of users
134
+ * @returns {Object[]} Filtered entries
135
+ */
136
+ filterByUser(entries, user) {
137
+ const users = Array.isArray(user) ? user : [user];
138
+ return entries.filter((e) => users.includes(e.user));
139
+ }
140
+
141
+ /**
142
+ * Filter entries by severity
143
+ * @param {Object[]} entries - Entries to filter
144
+ * @param {string|string[]} severity - Severity or array of severities
145
+ * @returns {Object[]} Filtered entries
146
+ */
147
+ filterBySeverity(entries, severity) {
148
+ const severities = Array.isArray(severity) ? severity : [severity];
149
+ return entries.filter((e) => severities.includes(e.severity));
150
+ }
151
+
152
+ /**
153
+ * Filter entries by minimum severity level
154
+ * @param {Object[]} entries - Entries to filter
155
+ * @param {string} minSeverity - Minimum severity level
156
+ * @returns {Object[]} Filtered entries
157
+ */
158
+ filterByMinSeverity(entries, minSeverity) {
159
+ const minIndex = SEVERITY_LEVELS.indexOf(minSeverity);
160
+ if (minIndex === -1) {
161
+ return entries; // Invalid severity, return all
162
+ }
163
+ return entries.filter((e) => {
164
+ const entryIndex = SEVERITY_LEVELS.indexOf(e.severity);
165
+ return entryIndex >= minIndex;
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Filter entries by full-text search
171
+ * @param {Object[]} entries - Entries to filter
172
+ * @param {string} search - Search term
173
+ * @returns {Object[]} Filtered entries
174
+ */
175
+ filterBySearch(entries, search) {
176
+ const searchLower = search.toLowerCase();
177
+ return entries.filter((e) => {
178
+ // Search in action field
179
+ if (e.action && e.action.toLowerCase().includes(searchLower)) {
180
+ return true;
181
+ }
182
+ // Search in user field
183
+ if (e.user && e.user.toLowerCase().includes(searchLower)) {
184
+ return true;
185
+ }
186
+ // Search in parameters
187
+ if (e.parameters) {
188
+ const paramStr = JSON.stringify(e.parameters).toLowerCase();
189
+ if (paramStr.includes(searchLower)) {
190
+ return true;
191
+ }
192
+ }
193
+ return false;
194
+ });
195
+ }
196
+
197
+ /**
198
+ * Sort entries by timestamp
199
+ * @param {Object[]} entries - Entries to sort
200
+ * @param {string} order - Sort order: 'asc' or 'desc'
201
+ * @returns {Object[]} Sorted entries
202
+ */
203
+ sortEntries(entries, order) {
204
+ const sorted = [...entries];
205
+ if (order === 'asc') {
206
+ sorted.sort((a, b) => a.timestamp - b.timestamp);
207
+ } else {
208
+ sorted.sort((a, b) => b.timestamp - a.timestamp);
209
+ }
210
+ return sorted;
211
+ }
212
+
213
+ /**
214
+ * Paginate entries
215
+ * @param {Object[]} entries - Entries to paginate
216
+ * @param {number} limit - Maximum entries per page
217
+ * @param {number} offset - Number of entries to skip
218
+ * @param {number} page - Page number (1-based)
219
+ * @returns {Object} { paginatedEntries, hasMore, currentPage }
220
+ */
221
+ paginate(entries, limit, offset, page) {
222
+ if (!limit) {
223
+ return {
224
+ paginatedEntries: entries,
225
+ hasMore: false,
226
+ currentPage: 1,
227
+ };
228
+ }
229
+
230
+ let startIndex;
231
+ let currentPage;
232
+
233
+ if (page !== undefined) {
234
+ // Use page number (1-based)
235
+ currentPage = page;
236
+ startIndex = (page - 1) * limit;
237
+ } else if (offset !== undefined) {
238
+ // Use offset
239
+ startIndex = offset;
240
+ currentPage = Math.floor(offset / limit) + 1;
241
+ } else {
242
+ // Default to first page
243
+ startIndex = 0;
244
+ currentPage = 1;
245
+ }
246
+
247
+ const endIndex = startIndex + limit;
248
+ const paginatedEntries = entries.slice(startIndex, endIndex);
249
+ const hasMore = endIndex < entries.length;
250
+
251
+ return {
252
+ paginatedEntries,
253
+ hasMore,
254
+ currentPage,
255
+ };
256
+ }
257
+ }