loki-mode 5.49.4 → 5.50.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 (54) hide show
  1. package/README.md +2 -2
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/dashboard/__init__.py +1 -1
  5. package/docs/INSTALLATION.md +1 -1
  6. package/docs/alternative-installations.md +3 -3
  7. package/mcp/__init__.py +1 -1
  8. package/package.json +4 -3
  9. package/src/audit/compliance.js +226 -0
  10. package/src/audit/index.js +147 -0
  11. package/src/audit/log.js +153 -0
  12. package/src/audit/residency.js +150 -0
  13. package/src/integrations/adapter.js +128 -0
  14. package/src/integrations/github/action-handler.js +267 -0
  15. package/src/integrations/github/reporter.js +485 -0
  16. package/src/integrations/github/templates/execution-summary.md +16 -0
  17. package/src/integrations/github/templates/quality-report.md +18 -0
  18. package/src/integrations/jira/api-client.js +152 -0
  19. package/src/integrations/jira/epic-converter.js +157 -0
  20. package/src/integrations/jira/index.js +45 -0
  21. package/src/integrations/jira/sync-manager.js +143 -0
  22. package/src/integrations/jira/webhook-handler.js +101 -0
  23. package/src/integrations/linear/client.js +382 -0
  24. package/src/integrations/linear/config.js +168 -0
  25. package/src/integrations/linear/sync.js +305 -0
  26. package/src/observability/index.js +144 -0
  27. package/src/observability/metrics.js +181 -0
  28. package/src/observability/otel.js +662 -0
  29. package/src/observability/spans.js +199 -0
  30. package/src/policies/approval.js +366 -0
  31. package/src/policies/cost.js +296 -0
  32. package/src/policies/engine.js +615 -0
  33. package/src/policies/index.js +216 -0
  34. package/src/policies/types.js +303 -0
  35. package/src/protocols/a2a/agent-card.js +101 -0
  36. package/src/protocols/a2a/artifacts.js +61 -0
  37. package/src/protocols/a2a/client.js +167 -0
  38. package/src/protocols/a2a/index.js +40 -0
  39. package/src/protocols/a2a/streaming.js +120 -0
  40. package/src/protocols/a2a/task-manager.js +183 -0
  41. package/src/protocols/auth/oauth.js +220 -0
  42. package/src/protocols/mcp-circuit-breaker.js +165 -0
  43. package/src/protocols/mcp-client-manager.js +264 -0
  44. package/src/protocols/mcp-client.js +383 -0
  45. package/src/protocols/mcp-server.js +367 -0
  46. package/src/protocols/resources/continuity.js +49 -0
  47. package/src/protocols/resources/memory.js +128 -0
  48. package/src/protocols/tools/agent-metrics.js +164 -0
  49. package/src/protocols/tools/checkpoint-restore.js +225 -0
  50. package/src/protocols/tools/project-status.js +110 -0
  51. package/src/protocols/tools/quality-report.js +141 -0
  52. package/src/protocols/tools/start-project.js +165 -0
  53. package/src/protocols/transport/sse.js +191 -0
  54. package/src/protocols/transport/stdio.js +85 -0
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  [![Agent Types](https://img.shields.io/badge/Agent%20Types-41-blue)]()
12
12
  [![Benchmarks](https://img.shields.io/badge/Benchmarks-Infrastructure%20Ready-blue)](benchmarks/)
13
13
 
14
- **Current Version: v5.49.4**
14
+ **Current Version: v5.50.0**
15
15
 
16
16
  **[Autonomi](https://www.autonomi.dev/)** | **[Documentation](https://www.autonomi.dev/docs)** | **[GitHub](https://github.com/asklokesh/loki-mode)**
17
17
 
@@ -85,7 +85,7 @@ gemini
85
85
  ### Verify Installation
86
86
 
87
87
  ```bash
88
- loki --version # Should print 5.49.4
88
+ loki --version # Should print 5.50.0
89
89
  loki doctor # Check skill symlinks and provider availability
90
90
  ```
91
91
 
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v5.49.4
6
+ # Loki Mode v5.50.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -263,4 +263,4 @@ The following features are documented in skill modules but not yet fully automat
263
263
  | Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
264
264
  | Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
265
265
 
266
- **v5.49.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
266
+ **v5.50.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 5.49.4
1
+ 5.50.0
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "5.49.4"
10
+ __version__ = "5.50.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v5.49.4
5
+ **Version:** v5.50.0
6
6
 
7
7
  ---
8
8
 
@@ -28,7 +28,7 @@ ln -sf ~/.claude/skills/loki-mode/autonomy/loki /usr/local/bin/loki
28
28
 
29
29
  ## Docker
30
30
 
31
- **Status:** Image exists on Docker Hub. Tags: `latest`, version-specific (e.g., `5.49.4`).
31
+ **Status:** Image exists on Docker Hub. Tags: `latest`, version-specific (e.g., `5.50.0`).
32
32
 
33
33
  ```bash
34
34
  docker pull asklokesh/loki-mode:latest
@@ -108,8 +108,8 @@ jobs:
108
108
 
109
109
  ```bash
110
110
  # Download and extract to skills directory
111
- curl -sL https://github.com/asklokesh/loki-mode/archive/refs/tags/v5.49.4.tar.gz | tar xz
112
- mv loki-mode-5.49.4 ~/.claude/skills/loki-mode
111
+ curl -sL https://github.com/asklokesh/loki-mode/archive/refs/tags/v5.50.0.tar.gz | tar xz
112
+ mv loki-mode-5.50.0 ~/.claude/skills/loki-mode
113
113
  ```
114
114
 
115
115
  **Best for:** Offline or air-gapped environments, pinned version deployments.
package/mcp/__init__.py CHANGED
@@ -21,4 +21,4 @@ try:
21
21
  except ImportError:
22
22
  __all__ = ['mcp']
23
23
 
24
- __version__ = '5.49.4'
24
+ __version__ = '5.50.0'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "5.49.4",
3
+ "version": "5.50.0",
4
4
  "description": "Loki Mode by Autonomi - Multi-agent autonomous startup system for Claude Code, Codex CLI, and Gemini CLI",
5
5
  "keywords": [
6
6
  "autonomi",
@@ -45,13 +45,14 @@
45
45
  "dashboard/static/",
46
46
  "dashboard/requirements.txt",
47
47
  "mcp/",
48
- "completions/"
48
+ "completions/",
49
+ "src/"
49
50
  ],
50
51
  "scripts": {
51
52
  "postinstall": "node bin/postinstall.js",
52
53
  "prepack": "find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null; find . -name '*.pyc' -delete 2>/dev/null; true",
53
54
  "prepublishOnly": "cd dashboard-ui && npm ci && npm run build:all",
54
- "test": "bash -n autonomy/run.sh && bash -n autonomy/loki && bash -n autonomy/completion-council.sh && bash -n autonomy/app-runner.sh && bash -n autonomy/prd-checklist.sh && bash -n autonomy/playwright-verify.sh && echo 'All syntax checks passed'",
55
+ "test": "bash -n autonomy/run.sh && bash -n autonomy/loki && bash -n autonomy/completion-council.sh && bash -n autonomy/app-runner.sh && bash -n autonomy/prd-checklist.sh && bash -n autonomy/playwright-verify.sh && node --test 'tests/protocols/*.test.js' && node --test 'tests/protocols/a2a/*.test.js' && node --test 'tests/observability/*.test.js' && node --test 'tests/policies/*.test.js' && node --test 'tests/audit/*.test.js' && node --test 'tests/integrations/*.test.js' && node --test 'tests/integrations/jira/*.test.js' && node --test 'tests/integrations/github/*.test.js' && echo 'All checks passed'",
55
56
  "test:visual": "node --experimental-vm-modules node_modules/jest/bin/jest.js dashboard-ui/tests/visual-regression.test.js",
56
57
  "test:parity": "node --experimental-vm-modules dashboard-ui/scripts/check-parity.js",
57
58
  "test:parity:json": "node --experimental-vm-modules dashboard-ui/scripts/check-parity.js --json",
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+ var { AuditLog } = require('./log');
6
+
7
+ /**
8
+ * SOC 2 Type II control mappings.
9
+ */
10
+ var SOC2_CONTROLS = {
11
+ CC6_1: { name: 'Logical Access', description: 'Access to data and systems is restricted to authorized users' },
12
+ CC6_2: { name: 'System Operations', description: 'System operations are monitored for anomalies' },
13
+ CC6_3: { name: 'Change Management', description: 'Changes are authorized, tested, and approved before deployment' },
14
+ CC7_1: { name: 'Risk Assessment', description: 'Risks are identified and mitigated' },
15
+ CC7_2: { name: 'Monitoring', description: 'System components are monitored for anomalies' },
16
+ CC8_1: { name: 'Incident Response', description: 'Security incidents are identified and responded to' },
17
+ };
18
+
19
+ /**
20
+ * ISO 27001 Annex A control mappings.
21
+ */
22
+ var ISO27001_CONTROLS = {
23
+ 'A.9.2': { name: 'User Access Management', description: 'Formal access provisioning and de-provisioning' },
24
+ 'A.12.4': { name: 'Logging and Monitoring', description: 'Events are recorded and evidence is generated' },
25
+ 'A.12.5': { name: 'Control of Operational Software', description: 'Installation of software is controlled' },
26
+ 'A.14.2': { name: 'Security in Development', description: 'Security requirements in SDLC' },
27
+ 'A.16.1': { name: 'Incident Management', description: 'Consistent approach to security incidents' },
28
+ 'A.18.1': { name: 'Compliance', description: 'Compliance with legal and contractual requirements' },
29
+ };
30
+
31
+ /**
32
+ * Map audit actions to SOC 2 controls.
33
+ */
34
+ var ACTION_TO_SOC2 = {
35
+ 'agent_start': ['CC6_1', 'CC6_2'],
36
+ 'agent_stop': ['CC6_1', 'CC6_2'],
37
+ 'file_write': ['CC6_3', 'CC7_2'],
38
+ 'file_read': ['CC6_1'],
39
+ 'command_execute': ['CC6_3', 'CC7_2'],
40
+ 'deploy': ['CC6_3', 'CC8_1'],
41
+ 'policy_evaluate': ['CC7_1', 'CC7_2'],
42
+ 'approval_request': ['CC6_3'],
43
+ 'approval_resolve': ['CC6_3'],
44
+ 'test_run': ['CC6_3', 'CC7_1'],
45
+ };
46
+
47
+ /**
48
+ * Map audit actions to ISO 27001 controls.
49
+ */
50
+ var ACTION_TO_ISO = {
51
+ 'agent_start': ['A.9.2', 'A.12.4'],
52
+ 'agent_stop': ['A.9.2', 'A.12.4'],
53
+ 'file_write': ['A.12.5', 'A.14.2'],
54
+ 'file_read': ['A.9.2'],
55
+ 'command_execute': ['A.12.5', 'A.14.2'],
56
+ 'deploy': ['A.12.5', 'A.14.2'],
57
+ 'policy_evaluate': ['A.18.1'],
58
+ 'approval_request': ['A.9.2', 'A.16.1'],
59
+ 'approval_resolve': ['A.9.2', 'A.16.1'],
60
+ 'test_run': ['A.14.2'],
61
+ };
62
+
63
+ /**
64
+ * Generate a SOC 2 Type II evidence report from audit log entries.
65
+ *
66
+ * @param {object[]} entries - Audit log entries
67
+ * @param {object} [opts]
68
+ * @param {string} [opts.projectName] - Project name for the report
69
+ * @param {string} [opts.period] - Reporting period description
70
+ * @returns {object} Structured SOC 2 report
71
+ */
72
+ function generateSoc2Report(entries, opts) {
73
+ opts = opts || {};
74
+ var controlEvidence = {};
75
+
76
+ // Initialize all controls
77
+ Object.keys(SOC2_CONTROLS).forEach(function (id) {
78
+ controlEvidence[id] = {
79
+ control: SOC2_CONTROLS[id],
80
+ evidenceCount: 0,
81
+ sampleEntries: [],
82
+ };
83
+ });
84
+
85
+ // Map entries to controls
86
+ for (var i = 0; i < entries.length; i++) {
87
+ var entry = entries[i];
88
+ var controls = ACTION_TO_SOC2[entry.what] || [];
89
+ for (var j = 0; j < controls.length; j++) {
90
+ var cid = controls[j];
91
+ if (controlEvidence[cid]) {
92
+ controlEvidence[cid].evidenceCount++;
93
+ if (controlEvidence[cid].sampleEntries.length < 5) {
94
+ controlEvidence[cid].sampleEntries.push({
95
+ seq: entry.seq,
96
+ timestamp: entry.timestamp,
97
+ who: entry.who,
98
+ what: entry.what,
99
+ });
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ return {
106
+ reportType: 'SOC2_TYPE_II',
107
+ generatedAt: new Date().toISOString(),
108
+ projectName: opts.projectName || 'Loki Mode',
109
+ period: opts.period || 'Current',
110
+ totalAuditEntries: entries.length,
111
+ controls: controlEvidence,
112
+ chainIntegrity: null, // Caller should set after verifyChain()
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Generate an ISO 27001 mapping report.
118
+ */
119
+ function generateIso27001Report(entries, opts) {
120
+ opts = opts || {};
121
+ var controlEvidence = {};
122
+
123
+ Object.keys(ISO27001_CONTROLS).forEach(function (id) {
124
+ controlEvidence[id] = {
125
+ control: ISO27001_CONTROLS[id],
126
+ evidenceCount: 0,
127
+ sampleEntries: [],
128
+ };
129
+ });
130
+
131
+ for (var i = 0; i < entries.length; i++) {
132
+ var entry = entries[i];
133
+ var controls = ACTION_TO_ISO[entry.what] || [];
134
+ for (var j = 0; j < controls.length; j++) {
135
+ var cid = controls[j];
136
+ if (controlEvidence[cid]) {
137
+ controlEvidence[cid].evidenceCount++;
138
+ if (controlEvidence[cid].sampleEntries.length < 5) {
139
+ controlEvidence[cid].sampleEntries.push({
140
+ seq: entry.seq,
141
+ timestamp: entry.timestamp,
142
+ who: entry.who,
143
+ what: entry.what,
144
+ });
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ return {
151
+ reportType: 'ISO27001',
152
+ generatedAt: new Date().toISOString(),
153
+ projectName: opts.projectName || 'Loki Mode',
154
+ period: opts.period || 'Current',
155
+ totalAuditEntries: entries.length,
156
+ controls: controlEvidence,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Generate a GDPR data processing record.
162
+ */
163
+ function generateGdprReport(entries, opts) {
164
+ opts = opts || {};
165
+
166
+ // Extract unique data subjects (actors) and processing activities
167
+ var subjects = {};
168
+ var activities = {};
169
+ var dataCategories = {};
170
+
171
+ for (var i = 0; i < entries.length; i++) {
172
+ var entry = entries[i];
173
+ subjects[entry.who] = (subjects[entry.who] || 0) + 1;
174
+ activities[entry.what] = (activities[entry.what] || 0) + 1;
175
+
176
+ // Categorize data types from metadata
177
+ if (entry.metadata) {
178
+ if (entry.metadata.dataType) {
179
+ dataCategories[entry.metadata.dataType] = true;
180
+ }
181
+ if (entry.metadata.containsPii) {
182
+ dataCategories['personal_data'] = true;
183
+ }
184
+ }
185
+ }
186
+
187
+ return {
188
+ reportType: 'GDPR_DATA_PROCESSING_RECORD',
189
+ generatedAt: new Date().toISOString(),
190
+ projectName: opts.projectName || 'Loki Mode',
191
+ controller: opts.controller || 'Organization',
192
+ purposes: ['Autonomous software development', 'Quality assurance', 'Deployment'],
193
+ legalBasis: 'Legitimate interest (automated software development)',
194
+ dataSubjects: Object.keys(subjects).map(function (s) {
195
+ return { id: s, activityCount: subjects[s] };
196
+ }),
197
+ processingActivities: Object.keys(activities).map(function (a) {
198
+ return { activity: a, count: activities[a] };
199
+ }),
200
+ dataCategories: Object.keys(dataCategories),
201
+ retentionPolicy: opts.retentionDays ? opts.retentionDays + ' days' : 'Project lifetime',
202
+ securityMeasures: [
203
+ 'Hash-chain tamper evidence on audit log',
204
+ 'Policy-based access controls',
205
+ 'Data residency enforcement',
206
+ ],
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Export a report as JSON string.
212
+ */
213
+ function exportReportJson(report) {
214
+ return JSON.stringify(report, null, 2);
215
+ }
216
+
217
+ module.exports = {
218
+ generateSoc2Report: generateSoc2Report,
219
+ generateIso27001Report: generateIso27001Report,
220
+ generateGdprReport: generateGdprReport,
221
+ exportReportJson: exportReportJson,
222
+ SOC2_CONTROLS: SOC2_CONTROLS,
223
+ ISO27001_CONTROLS: ISO27001_CONTROLS,
224
+ ACTION_TO_SOC2: ACTION_TO_SOC2,
225
+ ACTION_TO_ISO: ACTION_TO_ISO,
226
+ };
@@ -0,0 +1,147 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Loki Mode Audit Trail - Public API
5
+ *
6
+ * Enterprise audit logging with tamper-evident hash chains,
7
+ * compliance report generation, and data residency enforcement.
8
+ *
9
+ * Usage:
10
+ * var audit = require('./src/audit');
11
+ * audit.init('/path/to/project');
12
+ * audit.record({ who: 'agent-1', what: 'file_write', where: 'src/app.js', why: 'implement feature' });
13
+ * var result = audit.verifyChain();
14
+ * var report = audit.generateReport('soc2');
15
+ * var allowed = audit.checkProvider('anthropic', 'us');
16
+ */
17
+
18
+ var { AuditLog } = require('./log');
19
+ var compliance = require('./compliance');
20
+ var { ResidencyController } = require('./residency');
21
+
22
+ var _log = null;
23
+ var _residency = null;
24
+ var _initialized = false;
25
+ var _projectDir = null;
26
+
27
+ /**
28
+ * Initialize the audit trail system.
29
+ */
30
+ function init(projectDir) {
31
+ var dir = projectDir || process.cwd();
32
+ if (_initialized && _projectDir === dir) return;
33
+ if (_initialized) destroy();
34
+
35
+ _projectDir = dir;
36
+ _log = new AuditLog({ projectDir: dir });
37
+ _residency = new ResidencyController({ projectDir: dir });
38
+ _initialized = true;
39
+ }
40
+
41
+ /**
42
+ * Record an audit entry.
43
+ */
44
+ function record(entry) {
45
+ if (!_initialized) init();
46
+ return _log.record(entry);
47
+ }
48
+
49
+ /**
50
+ * Verify the hash chain integrity.
51
+ */
52
+ function verifyChain() {
53
+ if (!_initialized) init();
54
+ return _log.verifyChain();
55
+ }
56
+
57
+ /**
58
+ * Generate a compliance report.
59
+ * @param {string} type - 'soc2', 'iso27001', or 'gdpr'
60
+ * @param {object} [opts] - Report options
61
+ */
62
+ function generateReport(type, opts) {
63
+ if (!_initialized) init();
64
+ var entries = _log.readEntries();
65
+ switch (type) {
66
+ case 'soc2':
67
+ return compliance.generateSoc2Report(entries, opts);
68
+ case 'iso27001':
69
+ return compliance.generateIso27001Report(entries, opts);
70
+ case 'gdpr':
71
+ return compliance.generateGdprReport(entries, opts);
72
+ default:
73
+ throw new Error('Unknown report type: ' + type + '. Supported: soc2, iso27001, gdpr');
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Export a report as JSON string.
79
+ */
80
+ function exportReport(type, opts) {
81
+ var report = generateReport(type, opts);
82
+ return compliance.exportReportJson(report);
83
+ }
84
+
85
+ /**
86
+ * Check if a provider is allowed by data residency policy.
87
+ */
88
+ function checkProvider(provider, region) {
89
+ if (!_initialized) init();
90
+ return _residency.checkProvider(provider, region);
91
+ }
92
+
93
+ /**
94
+ * Check if air-gapped mode is enabled.
95
+ */
96
+ function isAirGapped() {
97
+ if (!_initialized) init();
98
+ return _residency.isAirGapped();
99
+ }
100
+
101
+ /**
102
+ * Read filtered audit entries.
103
+ */
104
+ function readEntries(filter) {
105
+ if (!_initialized) init();
106
+ return _log.readEntries(filter);
107
+ }
108
+
109
+ /**
110
+ * Get audit log summary.
111
+ */
112
+ function getSummary() {
113
+ if (!_initialized) init();
114
+ return _log.getSummary();
115
+ }
116
+
117
+ /**
118
+ * Flush pending entries to disk.
119
+ */
120
+ function flush() {
121
+ if (_log) _log.flush();
122
+ }
123
+
124
+ /**
125
+ * Destroy audit trail (for testing).
126
+ */
127
+ function destroy() {
128
+ if (_log) _log.destroy();
129
+ _log = null;
130
+ _residency = null;
131
+ _initialized = false;
132
+ _projectDir = null;
133
+ }
134
+
135
+ module.exports = {
136
+ init: init,
137
+ record: record,
138
+ verifyChain: verifyChain,
139
+ generateReport: generateReport,
140
+ exportReport: exportReport,
141
+ checkProvider: checkProvider,
142
+ isAirGapped: isAirGapped,
143
+ readEntries: readEntries,
144
+ getSummary: getSummary,
145
+ flush: flush,
146
+ destroy: destroy,
147
+ };
@@ -0,0 +1,153 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+
7
+ const MAX_MEMORY_ENTRIES = 1000;
8
+ const HASH_ALGO = 'sha256';
9
+
10
+ class AuditLog {
11
+ constructor(opts) {
12
+ const projectDir = (opts && opts.projectDir) || process.cwd();
13
+ this._logDir = (opts && opts.logDir) || path.join(projectDir, '.loki', 'audit');
14
+ this._logFile = path.join(this._logDir, 'audit.jsonl');
15
+ this._entries = [];
16
+ this._lastHash = 'GENESIS';
17
+ this._entryCount = 0;
18
+ this._loadChainTip();
19
+ }
20
+
21
+ record(entry) {
22
+ if (!entry || !entry.who || !entry.what) {
23
+ throw new Error('Audit entry requires at least "who" and "what" fields');
24
+ }
25
+ var auditEntry = {
26
+ seq: this._entryCount,
27
+ timestamp: new Date().toISOString(),
28
+ who: String(entry.who),
29
+ what: String(entry.what),
30
+ where: entry.where ? String(entry.where) : null,
31
+ why: entry.why ? String(entry.why) : null,
32
+ metadata: entry.metadata ? JSON.parse(JSON.stringify(entry.metadata)) : null,
33
+ previousHash: this._lastHash,
34
+ hash: null,
35
+ };
36
+ auditEntry.hash = this._computeHash(auditEntry);
37
+ this._lastHash = auditEntry.hash;
38
+ this._entryCount++;
39
+ this._entries.push(auditEntry);
40
+ if (this._entries.length >= MAX_MEMORY_ENTRIES) {
41
+ this.flush();
42
+ }
43
+ return auditEntry;
44
+ }
45
+
46
+ flush() {
47
+ if (this._entries.length === 0) return;
48
+ if (!fs.existsSync(this._logDir)) {
49
+ fs.mkdirSync(this._logDir, { recursive: true });
50
+ }
51
+ var lines = this._entries.map(function (e) { return JSON.stringify(e); }).join('\n') + '\n';
52
+ fs.appendFileSync(this._logFile, lines, 'utf8');
53
+ this._entries = [];
54
+ }
55
+
56
+ verifyChain() {
57
+ this.flush();
58
+ if (!fs.existsSync(this._logFile)) {
59
+ return { valid: true, entries: 0, brokenAt: null, error: null };
60
+ }
61
+ var content = fs.readFileSync(this._logFile, 'utf8').trim();
62
+ if (!content) {
63
+ return { valid: true, entries: 0, brokenAt: null, error: null };
64
+ }
65
+ var lines = content.split('\n');
66
+ var expectedPrevHash = 'GENESIS';
67
+ var count = 0;
68
+ for (var i = 0; i < lines.length; i++) {
69
+ var entry;
70
+ try { entry = JSON.parse(lines[i]); } catch (e) {
71
+ return { valid: false, entries: count, brokenAt: i, error: 'Invalid JSON at line ' + i };
72
+ }
73
+ if (entry.previousHash !== expectedPrevHash) {
74
+ return { valid: false, entries: count, brokenAt: i,
75
+ error: 'Hash chain broken at entry ' + i };
76
+ }
77
+ var computedHash = this._computeHash(entry);
78
+ if (computedHash !== entry.hash) {
79
+ return { valid: false, entries: count, brokenAt: i,
80
+ error: 'Hash mismatch at entry ' + i + ': entry has been tampered with' };
81
+ }
82
+ expectedPrevHash = entry.hash;
83
+ count++;
84
+ }
85
+ return { valid: true, entries: count, brokenAt: null, error: null };
86
+ }
87
+
88
+ readEntries(filter) {
89
+ this.flush();
90
+ if (!fs.existsSync(this._logFile)) return [];
91
+ var content = fs.readFileSync(this._logFile, 'utf8').trim();
92
+ if (!content) return [];
93
+ var entries = content.split('\n').map(function (line) {
94
+ try { return JSON.parse(line); } catch (_) { return null; }
95
+ }).filter(Boolean);
96
+ if (filter) {
97
+ if (filter.who) entries = entries.filter(function (e) { return e.who === filter.who; });
98
+ if (filter.what) entries = entries.filter(function (e) { return e.what === filter.what; });
99
+ if (filter.since) entries = entries.filter(function (e) { return e.timestamp >= filter.since; });
100
+ if (filter.until) entries = entries.filter(function (e) { return e.timestamp <= filter.until; });
101
+ }
102
+ return entries;
103
+ }
104
+
105
+ getSummary() {
106
+ var entries = this.readEntries();
107
+ var actors = {};
108
+ var actions = {};
109
+ for (var i = 0; i < entries.length; i++) {
110
+ actors[entries[i].who] = true;
111
+ actions[entries[i].what] = true;
112
+ }
113
+ return {
114
+ totalEntries: entries.length,
115
+ actors: Object.keys(actors),
116
+ actions: Object.keys(actions),
117
+ firstEntry: entries.length > 0 ? entries[0].timestamp : null,
118
+ lastEntry: entries.length > 0 ? entries[entries.length - 1].timestamp : null,
119
+ };
120
+ }
121
+
122
+ destroy() {
123
+ this.flush();
124
+ this._entries = [];
125
+ }
126
+
127
+ _computeHash(entry) {
128
+ var data = JSON.stringify({
129
+ seq: entry.seq, timestamp: entry.timestamp, who: entry.who,
130
+ what: entry.what, where: entry.where, why: entry.why,
131
+ metadata: entry.metadata, previousHash: entry.previousHash,
132
+ });
133
+ return crypto.createHash(HASH_ALGO).update(data).digest('hex');
134
+ }
135
+
136
+ _loadChainTip() {
137
+ if (!fs.existsSync(this._logFile)) return;
138
+ try {
139
+ var content = fs.readFileSync(this._logFile, 'utf8').trim();
140
+ if (!content) return;
141
+ var lines = content.split('\n');
142
+ var lastLine = lines[lines.length - 1];
143
+ var lastEntry = JSON.parse(lastLine);
144
+ this._lastHash = lastEntry.hash;
145
+ this._entryCount = (lastEntry.seq || 0) + 1;
146
+ } catch (_) {
147
+ console.warn('[audit] Warning: corrupted chain tip detected, starting fresh chain');
148
+ this._chainCorrupted = true;
149
+ }
150
+ }
151
+ }
152
+
153
+ module.exports = { AuditLog, HASH_ALGO, MAX_MEMORY_ENTRIES };