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,352 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { AuditQuery } from './audit-query.js';
6
+ import { AuditStorage } from './audit-storage.js';
7
+
8
+ describe('AuditQuery', () => {
9
+ let testDir;
10
+ let auditStorage;
11
+ let auditQuery;
12
+
13
+ beforeEach(async () => {
14
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-audit-query-test-'));
15
+ auditStorage = new AuditStorage(testDir);
16
+ auditQuery = new AuditQuery(auditStorage);
17
+
18
+ // Seed test data
19
+ await auditStorage.appendEntry({
20
+ action: 'user.login',
21
+ user: 'alice',
22
+ severity: 'info',
23
+ timestamp: new Date('2026-01-15T10:00:00Z').getTime(),
24
+ parameters: { ip: '192.168.1.1', browser: 'Chrome' },
25
+ });
26
+ await auditStorage.appendEntry({
27
+ action: 'user.logout',
28
+ user: 'alice',
29
+ severity: 'info',
30
+ timestamp: new Date('2026-01-15T11:00:00Z').getTime(),
31
+ parameters: { duration: '1h' },
32
+ });
33
+ await auditStorage.appendEntry({
34
+ action: 'config.update',
35
+ user: 'bob',
36
+ severity: 'warning',
37
+ timestamp: new Date('2026-01-16T09:00:00Z').getTime(),
38
+ parameters: { setting: 'timeout', oldValue: 30, newValue: 60 },
39
+ });
40
+ await auditStorage.appendEntry({
41
+ action: 'security.alert',
42
+ user: 'system',
43
+ severity: 'error',
44
+ timestamp: new Date('2026-01-16T14:00:00Z').getTime(),
45
+ parameters: { reason: 'brute force detected', ip: '10.0.0.1' },
46
+ });
47
+ await auditStorage.appendEntry({
48
+ action: 'user.login',
49
+ user: 'charlie',
50
+ severity: 'info',
51
+ timestamp: new Date('2026-01-17T08:00:00Z').getTime(),
52
+ parameters: { ip: '192.168.1.50', browser: 'Firefox' },
53
+ });
54
+ });
55
+
56
+ afterEach(() => {
57
+ fs.rmSync(testDir, { recursive: true, force: true });
58
+ vi.useRealTimers();
59
+ });
60
+
61
+ describe('query filters by date range', () => {
62
+ it('filters entries within date range', async () => {
63
+ const result = await auditQuery.query({
64
+ from: new Date('2026-01-16T00:00:00Z'),
65
+ to: new Date('2026-01-16T23:59:59Z'),
66
+ sort: 'asc',
67
+ });
68
+
69
+ expect(result.entries).toHaveLength(2);
70
+ expect(result.entries[0].user).toBe('bob');
71
+ expect(result.entries[1].user).toBe('system');
72
+ });
73
+
74
+ it('filters by from date only', async () => {
75
+ const result = await auditQuery.query({
76
+ from: new Date('2026-01-16T00:00:00Z'),
77
+ });
78
+
79
+ expect(result.entries).toHaveLength(3);
80
+ });
81
+
82
+ it('filters by to date only', async () => {
83
+ const result = await auditQuery.query({
84
+ to: new Date('2026-01-15T23:59:59Z'),
85
+ });
86
+
87
+ expect(result.entries).toHaveLength(2);
88
+ });
89
+ });
90
+
91
+ describe('query filters by action type', () => {
92
+ it('filters by exact action', async () => {
93
+ const result = await auditQuery.query({
94
+ action: 'user.login',
95
+ sort: 'asc',
96
+ });
97
+
98
+ expect(result.entries).toHaveLength(2);
99
+ expect(result.entries[0].user).toBe('alice');
100
+ expect(result.entries[1].user).toBe('charlie');
101
+ });
102
+
103
+ it('filters by action prefix', async () => {
104
+ const result = await auditQuery.query({
105
+ action: 'user.*',
106
+ });
107
+
108
+ expect(result.entries).toHaveLength(3);
109
+ });
110
+
111
+ it('returns empty for non-existent action', async () => {
112
+ const result = await auditQuery.query({
113
+ action: 'nonexistent.action',
114
+ });
115
+
116
+ expect(result.entries).toHaveLength(0);
117
+ });
118
+ });
119
+
120
+ describe('query filters by user', () => {
121
+ it('filters by single user', async () => {
122
+ const result = await auditQuery.query({
123
+ user: 'alice',
124
+ });
125
+
126
+ expect(result.entries).toHaveLength(2);
127
+ expect(result.entries.every((e) => e.user === 'alice')).toBe(true);
128
+ });
129
+
130
+ it('filters by multiple users', async () => {
131
+ const result = await auditQuery.query({
132
+ user: ['alice', 'bob'],
133
+ });
134
+
135
+ expect(result.entries).toHaveLength(3);
136
+ });
137
+ });
138
+
139
+ describe('query filters by severity', () => {
140
+ it('filters by single severity', async () => {
141
+ const result = await auditQuery.query({
142
+ severity: 'warning',
143
+ });
144
+
145
+ expect(result.entries).toHaveLength(1);
146
+ expect(result.entries[0].action).toBe('config.update');
147
+ });
148
+
149
+ it('filters by multiple severities', async () => {
150
+ const result = await auditQuery.query({
151
+ severity: ['warning', 'error'],
152
+ });
153
+
154
+ expect(result.entries).toHaveLength(2);
155
+ });
156
+
157
+ it('filters by severity level (minimum)', async () => {
158
+ const result = await auditQuery.query({
159
+ minSeverity: 'warning',
160
+ sort: 'asc',
161
+ });
162
+
163
+ expect(result.entries).toHaveLength(2);
164
+ expect(result.entries[0].severity).toBe('warning');
165
+ expect(result.entries[1].severity).toBe('error');
166
+ });
167
+ });
168
+
169
+ describe('query supports multiple filters combined', () => {
170
+ it('combines date range and action filter', async () => {
171
+ const result = await auditQuery.query({
172
+ from: new Date('2026-01-15T00:00:00Z'),
173
+ to: new Date('2026-01-15T23:59:59Z'),
174
+ action: 'user.login',
175
+ });
176
+
177
+ expect(result.entries).toHaveLength(1);
178
+ expect(result.entries[0].user).toBe('alice');
179
+ });
180
+
181
+ it('combines user, severity, and action filters', async () => {
182
+ const result = await auditQuery.query({
183
+ user: 'alice',
184
+ severity: 'info',
185
+ action: 'user.*',
186
+ });
187
+
188
+ expect(result.entries).toHaveLength(2);
189
+ });
190
+
191
+ it('combines all filters', async () => {
192
+ const result = await auditQuery.query({
193
+ from: new Date('2026-01-15T00:00:00Z'),
194
+ to: new Date('2026-01-16T23:59:59Z'),
195
+ user: ['bob', 'system'],
196
+ severity: ['warning', 'error'],
197
+ });
198
+
199
+ expect(result.entries).toHaveLength(2);
200
+ });
201
+ });
202
+
203
+ describe('query returns paginated results', () => {
204
+ it('returns first page with limit', async () => {
205
+ const result = await auditQuery.query({
206
+ limit: 2,
207
+ });
208
+
209
+ expect(result.entries).toHaveLength(2);
210
+ expect(result.total).toBe(5);
211
+ expect(result.hasMore).toBe(true);
212
+ expect(result.page).toBe(1);
213
+ });
214
+
215
+ it('returns second page with offset', async () => {
216
+ const result = await auditQuery.query({
217
+ limit: 2,
218
+ offset: 2,
219
+ });
220
+
221
+ expect(result.entries).toHaveLength(2);
222
+ expect(result.page).toBe(2);
223
+ });
224
+
225
+ it('returns last page correctly', async () => {
226
+ const result = await auditQuery.query({
227
+ limit: 2,
228
+ offset: 4,
229
+ });
230
+
231
+ expect(result.entries).toHaveLength(1);
232
+ expect(result.hasMore).toBe(false);
233
+ expect(result.page).toBe(3);
234
+ });
235
+
236
+ it('supports page parameter instead of offset', async () => {
237
+ const result = await auditQuery.query({
238
+ limit: 2,
239
+ page: 2,
240
+ });
241
+
242
+ expect(result.entries).toHaveLength(2);
243
+ expect(result.page).toBe(2);
244
+ expect(result.entries[0].user).toBe('bob');
245
+ });
246
+ });
247
+
248
+ describe('query searches parameter content', () => {
249
+ it('searches in parameter values', async () => {
250
+ const result = await auditQuery.query({
251
+ search: 'Chrome',
252
+ });
253
+
254
+ expect(result.entries).toHaveLength(1);
255
+ expect(result.entries[0].user).toBe('alice');
256
+ expect(result.entries[0].action).toBe('user.login');
257
+ });
258
+
259
+ it('searches in nested parameter values', async () => {
260
+ const result = await auditQuery.query({
261
+ search: 'brute force',
262
+ });
263
+
264
+ expect(result.entries).toHaveLength(1);
265
+ expect(result.entries[0].action).toBe('security.alert');
266
+ });
267
+
268
+ it('searches case-insensitively', async () => {
269
+ const result = await auditQuery.query({
270
+ search: 'firefox',
271
+ });
272
+
273
+ expect(result.entries).toHaveLength(1);
274
+ expect(result.entries[0].user).toBe('charlie');
275
+ });
276
+
277
+ it('searches across action and user fields too', async () => {
278
+ const result = await auditQuery.query({
279
+ search: 'config',
280
+ });
281
+
282
+ expect(result.entries).toHaveLength(1);
283
+ expect(result.entries[0].user).toBe('bob');
284
+ });
285
+ });
286
+
287
+ describe('query returns count without results', () => {
288
+ it('returns only count when countOnly is true', async () => {
289
+ const result = await auditQuery.query({
290
+ countOnly: true,
291
+ });
292
+
293
+ expect(result.total).toBe(5);
294
+ expect(result.entries).toBeUndefined();
295
+ });
296
+
297
+ it('returns filtered count', async () => {
298
+ const result = await auditQuery.query({
299
+ user: 'alice',
300
+ countOnly: true,
301
+ });
302
+
303
+ expect(result.total).toBe(2);
304
+ expect(result.entries).toBeUndefined();
305
+ });
306
+ });
307
+
308
+ describe('query handles empty results', () => {
309
+ it('returns empty array for no matches', async () => {
310
+ const result = await auditQuery.query({
311
+ user: 'nonexistent',
312
+ });
313
+
314
+ expect(result.entries).toHaveLength(0);
315
+ expect(result.total).toBe(0);
316
+ expect(result.hasMore).toBe(false);
317
+ });
318
+
319
+ it('handles empty storage gracefully', async () => {
320
+ const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-audit-empty-'));
321
+ const emptyStorage = new AuditStorage(emptyDir);
322
+ const emptyQuery = new AuditQuery(emptyStorage);
323
+
324
+ const result = await emptyQuery.query({});
325
+
326
+ expect(result.entries).toHaveLength(0);
327
+ expect(result.total).toBe(0);
328
+
329
+ fs.rmSync(emptyDir, { recursive: true, force: true });
330
+ });
331
+ });
332
+
333
+ describe('query sorting', () => {
334
+ it('sorts by timestamp descending by default', async () => {
335
+ const result = await auditQuery.query({});
336
+
337
+ // Most recent first
338
+ expect(result.entries[0].user).toBe('charlie');
339
+ expect(result.entries[4].user).toBe('alice');
340
+ });
341
+
342
+ it('sorts by timestamp ascending when specified', async () => {
343
+ const result = await auditQuery.query({
344
+ sort: 'asc',
345
+ });
346
+
347
+ // Oldest first
348
+ expect(result.entries[0].user).toBe('alice');
349
+ expect(result.entries[0].action).toBe('user.login');
350
+ });
351
+ });
352
+ });
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Audit Log Storage - Append-only audit log storage with tamper-evident checksums
3
+ *
4
+ * Features:
5
+ * - Append-only log files (no overwrites)
6
+ * - Each entry has SHA-256 checksum
7
+ * - Checksum chains to previous entry (blockchain-style)
8
+ * - Daily log rotation with configurable retention
9
+ * - Stores in .tlc/audit/ directory
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import crypto from 'crypto';
15
+
16
+ export const AUDIT_PATH = '.tlc/audit';
17
+
18
+ export class AuditStorage {
19
+ constructor(baseDir = process.cwd()) {
20
+ this.baseDir = baseDir;
21
+ this.auditDir = path.join(baseDir, AUDIT_PATH);
22
+ this.lastChecksum = null;
23
+ }
24
+
25
+ /**
26
+ * Ensure audit directory exists
27
+ */
28
+ ensureDirectory() {
29
+ if (!fs.existsSync(this.auditDir)) {
30
+ fs.mkdirSync(this.auditDir, { recursive: true });
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Get current date string for log file naming
36
+ * @returns {string} YYYY-MM-DD format
37
+ */
38
+ getDateString() {
39
+ return new Date().toISOString().split('T')[0];
40
+ }
41
+
42
+ /**
43
+ * Get current log file path
44
+ * @returns {string} Path to current day's log file
45
+ */
46
+ getCurrentLogFile() {
47
+ const dateStr = this.getDateString();
48
+ return path.join(this.auditDir, `audit-${dateStr}.jsonl`);
49
+ }
50
+
51
+ /**
52
+ * Calculate SHA-256 checksum for entry data
53
+ * @param {Object} entryData - Entry data without checksum
54
+ * @returns {string} Hex-encoded SHA-256 hash
55
+ */
56
+ calculateChecksum(entryData) {
57
+ return crypto
58
+ .createHash('sha256')
59
+ .update(JSON.stringify(entryData))
60
+ .digest('hex');
61
+ }
62
+
63
+ /**
64
+ * Get the last entry's checksum from the current log file
65
+ * @returns {string|null} Last checksum or null if no entries
66
+ */
67
+ getLastChecksum() {
68
+ const logFile = this.getCurrentLogFile();
69
+ if (!fs.existsSync(logFile)) {
70
+ return null;
71
+ }
72
+
73
+ const content = fs.readFileSync(logFile, 'utf-8').trim();
74
+ if (!content) {
75
+ return null;
76
+ }
77
+
78
+ const lines = content.split('\n');
79
+ const lastLine = lines[lines.length - 1];
80
+ try {
81
+ const entry = JSON.parse(lastLine);
82
+ return entry.checksum || null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Append an audit entry with checksum
90
+ * @param {Object} entry - Entry data to append
91
+ * @returns {Promise<Object>} Appended entry with checksum
92
+ */
93
+ async appendEntry(entry) {
94
+ this.ensureDirectory();
95
+
96
+ const logFile = this.getCurrentLogFile();
97
+ const previousChecksum = this.getLastChecksum();
98
+
99
+ // Build entry with metadata
100
+ const entryData = {
101
+ ...entry,
102
+ previousChecksum,
103
+ };
104
+
105
+ // Calculate checksum
106
+ const checksum = this.calculateChecksum(entryData);
107
+ const finalEntry = {
108
+ ...entryData,
109
+ checksum,
110
+ };
111
+
112
+ // Append to log file (append-only)
113
+ fs.appendFileSync(logFile, JSON.stringify(finalEntry) + '\n', 'utf-8');
114
+
115
+ this.lastChecksum = checksum;
116
+ return finalEntry;
117
+ }
118
+
119
+ /**
120
+ * Get all log files in the audit directory
121
+ * @returns {string[]} Array of log file paths sorted by date
122
+ */
123
+ getLogFiles() {
124
+ if (!fs.existsSync(this.auditDir)) {
125
+ return [];
126
+ }
127
+
128
+ return fs
129
+ .readdirSync(this.auditDir)
130
+ .filter((f) => f.startsWith('audit-') && f.endsWith('.jsonl'))
131
+ .sort()
132
+ .map((f) => path.join(this.auditDir, f));
133
+ }
134
+
135
+ /**
136
+ * Read entries from a single log file
137
+ * @param {string} logFile - Path to log file
138
+ * @returns {Object[]} Array of entries
139
+ */
140
+ readLogFile(logFile) {
141
+ if (!fs.existsSync(logFile)) {
142
+ return [];
143
+ }
144
+
145
+ const content = fs.readFileSync(logFile, 'utf-8').trim();
146
+ if (!content) {
147
+ return [];
148
+ }
149
+
150
+ return content.split('\n').map((line) => {
151
+ try {
152
+ return JSON.parse(line);
153
+ } catch {
154
+ return null;
155
+ }
156
+ }).filter(Boolean);
157
+ }
158
+
159
+ /**
160
+ * Get all entries, optionally filtered by date range
161
+ * @param {Object} options - Filter options
162
+ * @param {Date} options.from - Start date (inclusive)
163
+ * @param {Date} options.to - End date (inclusive)
164
+ * @returns {Promise<Object[]>} Array of entries sorted by timestamp
165
+ */
166
+ async getEntries(options = {}) {
167
+ const { from, to } = options;
168
+ const logFiles = this.getLogFiles();
169
+
170
+ let allEntries = [];
171
+ for (const logFile of logFiles) {
172
+ const entries = this.readLogFile(logFile);
173
+ allEntries = allEntries.concat(entries);
174
+ }
175
+
176
+ // Filter by date range if specified
177
+ if (from || to) {
178
+ allEntries = allEntries.filter((entry) => {
179
+ const ts = entry.timestamp;
180
+ if (from && ts < from.getTime()) return false;
181
+ if (to && ts > to.getTime()) return false;
182
+ return true;
183
+ });
184
+ }
185
+
186
+ // Sort by timestamp ascending
187
+ return allEntries.sort((a, b) => a.timestamp - b.timestamp);
188
+ }
189
+
190
+ /**
191
+ * Verify integrity of the audit log
192
+ * @returns {Promise<Object>} Verification result { valid: boolean, error?: string, entryCount: number }
193
+ */
194
+ async verifyIntegrity() {
195
+ const logFiles = this.getLogFiles();
196
+
197
+ let entryCount = 0;
198
+ let previousChecksum = null;
199
+
200
+ for (const logFile of logFiles) {
201
+ const entries = this.readLogFile(logFile);
202
+
203
+ for (let i = 0; i < entries.length; i++) {
204
+ const entry = entries[i];
205
+ entryCount++;
206
+
207
+ // Verify entry's own checksum
208
+ const { checksum, ...entryData } = entry;
209
+ const calculatedChecksum = this.calculateChecksum(entryData);
210
+
211
+ if (checksum !== calculatedChecksum) {
212
+ return {
213
+ valid: false,
214
+ error: `Entry ${entryCount} has invalid checksum (expected ${calculatedChecksum}, got ${checksum})`,
215
+ entryCount,
216
+ };
217
+ }
218
+
219
+ // Verify chain (previousChecksum matches previous entry's checksum)
220
+ if (entry.previousChecksum !== previousChecksum) {
221
+ return {
222
+ valid: false,
223
+ error: `Entry ${entryCount} has broken chain (expected previousChecksum ${previousChecksum}, got ${entry.previousChecksum})`,
224
+ entryCount,
225
+ };
226
+ }
227
+
228
+ previousChecksum = checksum;
229
+ }
230
+ }
231
+
232
+ return {
233
+ valid: true,
234
+ entryCount,
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Rotate logs, removing files older than retention period
240
+ * @param {Object} options - Rotation options
241
+ * @param {number} options.retentionDays - Number of days to retain (default: 30)
242
+ */
243
+ async rotateLog(options = {}) {
244
+ const { retentionDays = 30 } = options;
245
+
246
+ if (!fs.existsSync(this.auditDir)) {
247
+ return;
248
+ }
249
+
250
+ const now = new Date();
251
+ const cutoffDate = new Date(now.getTime() - retentionDays * 24 * 60 * 60 * 1000);
252
+ const cutoffStr = cutoffDate.toISOString().split('T')[0];
253
+
254
+ const files = fs.readdirSync(this.auditDir).filter(
255
+ (f) => f.startsWith('audit-') && f.endsWith('.jsonl')
256
+ );
257
+
258
+ for (const file of files) {
259
+ // Extract date from filename: audit-YYYY-MM-DD.jsonl
260
+ const match = file.match(/audit-(\d{4}-\d{2}-\d{2})\.jsonl/);
261
+ if (match) {
262
+ const fileDate = match[1];
263
+ if (fileDate < cutoffStr) {
264
+ fs.unlinkSync(path.join(this.auditDir, file));
265
+ }
266
+ }
267
+ }
268
+ }
269
+ }