speclock 1.7.0 → 2.1.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.
- package/README.md +28 -13
- package/package.json +9 -3
- package/src/cli/index.js +82 -5
- package/src/core/audit.js +237 -0
- package/src/core/compliance.js +291 -0
- package/src/core/engine.js +48 -68
- package/src/core/license.js +221 -0
- package/src/core/llm-checker.js +239 -0
- package/src/core/semantics.js +1096 -0
- package/src/core/storage.js +9 -0
- package/src/mcp/http-server.js +120 -5
- package/src/mcp/server.js +78 -2
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ No other tool does this. Not Claude's native memory. Not Mem0. Not CLAUDE.md fil
|
|
|
49
49
|
|---------|---------------------|------|--------------------------|--------------|
|
|
50
50
|
| Remembers context | Yes | Yes | Manual | **Yes** |
|
|
51
51
|
| **Stops the AI from breaking things** | No | No | No | **Yes — active enforcement** |
|
|
52
|
-
| **Semantic conflict detection** | No | No | No | **Yes —
|
|
52
|
+
| **Semantic conflict detection** | No | No | No | **Yes — semantic engine v2 (100% detection, 0% false positives)** |
|
|
53
53
|
| Works on Bolt.new | No | No | No | **Yes — npm file-based mode** |
|
|
54
54
|
| Works on Lovable | No | No | No | **Yes — MCP remote** |
|
|
55
55
|
| Structured decisions/locks | No | Tags only | Flat text | **Goals, locks, decisions, changes** |
|
|
@@ -155,18 +155,33 @@ AI: ⚠️ CONFLICT (HIGH — 100%): Violates lock "Never modify auth files"
|
|
|
155
155
|
Should I proceed or find another approach?
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
## Killer Feature: Semantic Conflict Detection
|
|
158
|
+
## Killer Feature: Semantic Conflict Detection v2
|
|
159
159
|
|
|
160
|
-
Not
|
|
160
|
+
Not keyword matching — **real semantic analysis**. Tested against 61 adversarial attack vectors across 7 categories. **100% detection rate, 0% false positives.**
|
|
161
|
+
|
|
162
|
+
SpecLock v2's semantic engine includes:
|
|
163
|
+
- **55 synonym groups** — "truncate" matches "delete", "flash" matches "overwrite", "sunset" matches "remove"
|
|
164
|
+
- **70+ euphemism map** — "clean up old data" detected as deletion, "streamline workflow" detected as removal
|
|
165
|
+
- **Domain concept maps** — "safety scanning" links to "CSAM detection", "PHI" links to "patient records"
|
|
166
|
+
- **Intent classifier** — "Enable audit logging" correctly allowed when lock says "Never disable audit logging"
|
|
167
|
+
- **Compound sentence splitter** — "Update UI and also delete patient records" — catches the hidden violation
|
|
168
|
+
- **Temporal evasion detection** — "temporarily disable" treated with same severity as "disable"
|
|
169
|
+
- **Optional LLM integration** — Enterprise-grade 99%+ accuracy with OpenAI/Anthropic API
|
|
161
170
|
|
|
162
171
|
```
|
|
163
|
-
Lock:
|
|
164
|
-
Action:
|
|
172
|
+
Lock: "Never delete patient records"
|
|
173
|
+
Action: "Clean up old patient data from cold storage"
|
|
165
174
|
|
|
166
|
-
Result:
|
|
167
|
-
-
|
|
175
|
+
Result: [HIGH] Conflict detected (confidence: 100%)
|
|
176
|
+
- euphemism detected: "clean up" (euphemism for delete)
|
|
177
|
+
- concept match: patient data → patient records
|
|
168
178
|
- lock prohibits this action (negation detected)
|
|
169
|
-
|
|
179
|
+
|
|
180
|
+
Lock: "Never disable audit logging"
|
|
181
|
+
Action: "Enable comprehensive audit logging"
|
|
182
|
+
|
|
183
|
+
Result: NO CONFLICT (confidence: 7%)
|
|
184
|
+
- intent alignment: "enable" is opposite of prohibited "disable" (compliant)
|
|
170
185
|
```
|
|
171
186
|
|
|
172
187
|
## Three Integration Modes
|
|
@@ -218,7 +233,7 @@ Result: [HIGH] Conflict detected (confidence: 85%)
|
|
|
218
233
|
| `speclock_detect_drift` | Scan changes for constraint violations |
|
|
219
234
|
| `speclock_health` | Health score + multi-agent timeline |
|
|
220
235
|
|
|
221
|
-
### Templates, Reports & Enforcement
|
|
236
|
+
### Templates, Reports & Enforcement
|
|
222
237
|
| Tool | Purpose |
|
|
223
238
|
|------|---------|
|
|
224
239
|
| `speclock_apply_template` | Apply pre-built constraint templates (nextjs, react, express, etc.) |
|
|
@@ -272,14 +287,14 @@ speclock check <text> # Check for lock conflicts
|
|
|
272
287
|
speclock guard <file> --lock "text" # Manually guard a specific file
|
|
273
288
|
speclock unguard <file> # Remove guard from file
|
|
274
289
|
|
|
275
|
-
# Templates
|
|
290
|
+
# Templates
|
|
276
291
|
speclock template list # List available templates
|
|
277
292
|
speclock template apply <name> # Apply: nextjs, react, express, supabase, stripe, security-hardened
|
|
278
293
|
|
|
279
|
-
# Violation Report
|
|
294
|
+
# Violation Report
|
|
280
295
|
speclock report # Show violation stats + most tested locks
|
|
281
296
|
|
|
282
|
-
# Git Pre-commit Hook
|
|
297
|
+
# Git Pre-commit Hook
|
|
283
298
|
speclock hook install # Install pre-commit hook
|
|
284
299
|
speclock hook remove # Remove pre-commit hook
|
|
285
300
|
speclock audit # Audit staged files against locks
|
|
@@ -337,4 +352,4 @@ MIT License - see [LICENSE](LICENSE) file.
|
|
|
337
352
|
|
|
338
353
|
---
|
|
339
354
|
|
|
340
|
-
*SpecLock
|
|
355
|
+
*SpecLock v2.0.0 — Real semantic conflict detection. 100% detection, 0% false positives. Because remembering isn't enough — AI needs to respect boundaries.*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speclock",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AI constraint engine
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "AI constraint engine with semantic conflict detection, HMAC audit chain, SOC 2/HIPAA compliance exports. 100% detection, 0% false positives. 24 MCP tools + CLI. Enterprise-ready memory + enforcement.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/mcp/server.js",
|
|
7
7
|
"bin": {
|
|
@@ -30,7 +30,13 @@
|
|
|
30
30
|
"ai-amnesia",
|
|
31
31
|
"model-context-protocol",
|
|
32
32
|
"drift-detection",
|
|
33
|
-
"constraint-enforcement"
|
|
33
|
+
"constraint-enforcement",
|
|
34
|
+
"enterprise",
|
|
35
|
+
"soc2",
|
|
36
|
+
"hipaa",
|
|
37
|
+
"compliance",
|
|
38
|
+
"audit-trail",
|
|
39
|
+
"hmac"
|
|
34
40
|
],
|
|
35
41
|
"author": "Sandeep Roy (https://github.com/sgroy10)",
|
|
36
42
|
"license": "MIT",
|
package/src/cli/index.js
CHANGED
|
@@ -20,6 +20,9 @@ import {
|
|
|
20
20
|
applyTemplate,
|
|
21
21
|
generateReport,
|
|
22
22
|
auditStagedFiles,
|
|
23
|
+
verifyAuditChain,
|
|
24
|
+
exportCompliance,
|
|
25
|
+
getLicenseInfo,
|
|
23
26
|
} from "../core/engine.js";
|
|
24
27
|
import { generateContext } from "../core/context.js";
|
|
25
28
|
import { readBrain } from "../core/storage.js";
|
|
@@ -79,7 +82,7 @@ function refreshContext(root) {
|
|
|
79
82
|
|
|
80
83
|
function printHelp() {
|
|
81
84
|
console.log(`
|
|
82
|
-
SpecLock
|
|
85
|
+
SpecLock v2.1.0 — AI Constraint Engine (Enterprise)
|
|
83
86
|
Developed by Sandeep Roy (github.com/sgroy10)
|
|
84
87
|
|
|
85
88
|
Usage: speclock <command> [options]
|
|
@@ -102,6 +105,9 @@ Commands:
|
|
|
102
105
|
hook install Install git pre-commit hook
|
|
103
106
|
hook remove Remove git pre-commit hook
|
|
104
107
|
audit Audit staged files against locks
|
|
108
|
+
audit-verify Verify HMAC audit chain integrity
|
|
109
|
+
export --format <soc2|hipaa|csv> Export compliance report
|
|
110
|
+
license Show license tier and usage info
|
|
105
111
|
context Generate and print context pack
|
|
106
112
|
facts deploy [--provider X] Set deployment facts
|
|
107
113
|
watch Start file watcher (auto-track changes)
|
|
@@ -115,18 +121,23 @@ Options:
|
|
|
115
121
|
--goal <text> Goal text (for setup command)
|
|
116
122
|
--template <name> Template to apply during setup
|
|
117
123
|
--lock <text> Lock text (for guard command)
|
|
124
|
+
--format <soc2|hipaa|csv> Compliance export format
|
|
118
125
|
--project <path> Project root (for serve)
|
|
119
126
|
|
|
120
127
|
Templates: nextjs, react, express, supabase, stripe, security-hardened
|
|
121
128
|
|
|
129
|
+
Enterprise:
|
|
130
|
+
SPECLOCK_AUDIT_SECRET HMAC secret for audit chain (env var)
|
|
131
|
+
SPECLOCK_LICENSE_KEY License key for Pro/Enterprise features
|
|
132
|
+
SPECLOCK_LLM_KEY API key for LLM-powered conflict detection
|
|
133
|
+
|
|
122
134
|
Examples:
|
|
123
135
|
npx speclock setup --goal "Build PawPalace pet shop" --template nextjs
|
|
124
136
|
npx speclock lock "Never modify auth files"
|
|
125
|
-
npx speclock template apply supabase
|
|
126
137
|
npx speclock check "Adding social login to auth page"
|
|
127
|
-
npx speclock
|
|
128
|
-
npx speclock
|
|
129
|
-
npx speclock
|
|
138
|
+
npx speclock audit-verify
|
|
139
|
+
npx speclock export --format soc2
|
|
140
|
+
npx speclock license
|
|
130
141
|
npx speclock status
|
|
131
142
|
`);
|
|
132
143
|
}
|
|
@@ -597,6 +608,72 @@ Tip: When starting a new chat, tell the AI:
|
|
|
597
608
|
}
|
|
598
609
|
}
|
|
599
610
|
|
|
611
|
+
// --- AUDIT-VERIFY (v2.1 enterprise) ---
|
|
612
|
+
if (cmd === "audit-verify") {
|
|
613
|
+
ensureInit(root);
|
|
614
|
+
const result = verifyAuditChain(root);
|
|
615
|
+
console.log(`\nAudit Chain Verification`);
|
|
616
|
+
console.log("=".repeat(50));
|
|
617
|
+
console.log(`Status: ${result.valid ? "VALID" : "BROKEN"}`);
|
|
618
|
+
console.log(`Total events: ${result.totalEvents}`);
|
|
619
|
+
console.log(`Hashed events: ${result.hashedEvents}`);
|
|
620
|
+
console.log(`Legacy events (pre-v2.1): ${result.unhashedEvents}`);
|
|
621
|
+
if (!result.valid && result.errors) {
|
|
622
|
+
console.log(`\nErrors:`);
|
|
623
|
+
for (const err of result.errors) {
|
|
624
|
+
console.log(` Line ${err.line}: ${err.error}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
console.log(`\n${result.message}`);
|
|
628
|
+
process.exit(result.valid ? 0 : 1);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// --- EXPORT (v2.1 enterprise) ---
|
|
632
|
+
if (cmd === "export") {
|
|
633
|
+
const flags = parseFlags(args);
|
|
634
|
+
const format = flags.format;
|
|
635
|
+
if (!format || !["soc2", "hipaa", "csv"].includes(format)) {
|
|
636
|
+
console.error("Error: Valid format is required.");
|
|
637
|
+
console.error("Usage: speclock export --format <soc2|hipaa|csv>");
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
ensureInit(root);
|
|
641
|
+
const result = exportCompliance(root, format);
|
|
642
|
+
if (result.error) {
|
|
643
|
+
console.error(result.error);
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
if (format === "csv") {
|
|
647
|
+
console.log(result.data);
|
|
648
|
+
} else {
|
|
649
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
650
|
+
}
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// --- LICENSE (v2.1 enterprise) ---
|
|
655
|
+
if (cmd === "license") {
|
|
656
|
+
const info = getLicenseInfo(root);
|
|
657
|
+
console.log(`\nSpecLock License Info`);
|
|
658
|
+
console.log("=".repeat(50));
|
|
659
|
+
console.log(`Tier: ${info.tier} (${info.tierKey})`);
|
|
660
|
+
if (info.expiresAt) console.log(`Expires: ${info.expiresAt}`);
|
|
661
|
+
if (info.expired) console.log(`STATUS: EXPIRED — reverted to Free tier`);
|
|
662
|
+
console.log(`\nUsage:`);
|
|
663
|
+
if (info.usage) {
|
|
664
|
+
const { locks, decisions, events } = info.usage;
|
|
665
|
+
console.log(` Locks: ${locks.current}/${locks.max === Infinity ? "unlimited" : locks.max}`);
|
|
666
|
+
console.log(` Decisions: ${decisions.current}/${decisions.max === Infinity ? "unlimited" : decisions.max}`);
|
|
667
|
+
console.log(` Events: ${events.current}/${events.max === Infinity ? "unlimited" : events.max}`);
|
|
668
|
+
}
|
|
669
|
+
if (info.warnings && info.warnings.length > 0) {
|
|
670
|
+
console.log(`\nWarnings:`);
|
|
671
|
+
for (const w of info.warnings) console.log(` - ${w}`);
|
|
672
|
+
}
|
|
673
|
+
console.log(`\nFeatures: ${info.features.join(", ")}`);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
600
677
|
// --- STATUS ---
|
|
601
678
|
if (cmd === "status") {
|
|
602
679
|
showStatus(root);
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecLock HMAC Audit Chain Engine
|
|
3
|
+
* Provides tamper-proof event logging via HMAC-SHA256 hash chains.
|
|
4
|
+
* Each event's hash depends on the previous event's hash, creating
|
|
5
|
+
* an immutable chain — any modification breaks verification.
|
|
6
|
+
*
|
|
7
|
+
* Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import crypto from "crypto";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
|
|
14
|
+
// Inline path helpers to avoid circular dependency with storage.js
|
|
15
|
+
function speclockDir(root) {
|
|
16
|
+
return path.join(root, ".speclock");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function eventsPath(root) {
|
|
20
|
+
return path.join(speclockDir(root), "events.log");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const AUDIT_KEY_FILE = ".audit-key";
|
|
24
|
+
const HMAC_ALGO = "sha256";
|
|
25
|
+
const GENESIS_HASH = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get or create the audit secret for HMAC signing.
|
|
29
|
+
* Priority: SPECLOCK_AUDIT_SECRET env var > .speclock/.audit-key file > auto-generate
|
|
30
|
+
*/
|
|
31
|
+
export function getAuditSecret(root) {
|
|
32
|
+
// 1. Environment variable (highest priority)
|
|
33
|
+
if (process.env.SPECLOCK_AUDIT_SECRET) {
|
|
34
|
+
return process.env.SPECLOCK_AUDIT_SECRET;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Key file in .speclock/
|
|
38
|
+
const keyPath = path.join(speclockDir(root), AUDIT_KEY_FILE);
|
|
39
|
+
if (fs.existsSync(keyPath)) {
|
|
40
|
+
return fs.readFileSync(keyPath, "utf8").trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 3. Auto-generate and store
|
|
44
|
+
const secret = crypto.randomBytes(32).toString("hex");
|
|
45
|
+
const dir = speclockDir(root);
|
|
46
|
+
if (fs.existsSync(dir)) {
|
|
47
|
+
fs.writeFileSync(keyPath, secret, { mode: 0o600 });
|
|
48
|
+
}
|
|
49
|
+
return secret;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if audit chain is enabled (secret exists or can be created).
|
|
54
|
+
*/
|
|
55
|
+
export function isAuditEnabled(root) {
|
|
56
|
+
if (process.env.SPECLOCK_AUDIT_SECRET) return true;
|
|
57
|
+
const keyPath = path.join(speclockDir(root), AUDIT_KEY_FILE);
|
|
58
|
+
if (fs.existsSync(keyPath)) return true;
|
|
59
|
+
// Check if .speclock dir exists (we can create key)
|
|
60
|
+
return fs.existsSync(speclockDir(root));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Compute HMAC-SHA256 hash for an event, chained to the previous hash.
|
|
65
|
+
* The hash covers the entire event JSON (excluding the hash field itself).
|
|
66
|
+
*/
|
|
67
|
+
export function computeEventHash(prevHash, eventData, secret) {
|
|
68
|
+
// Create a clean copy without the hash field
|
|
69
|
+
const { hash: _, ...cleanEvent } = eventData;
|
|
70
|
+
const payload = prevHash + JSON.stringify(cleanEvent);
|
|
71
|
+
return crypto.createHmac(HMAC_ALGO, secret).update(payload).digest("hex");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the last hash from the events log.
|
|
76
|
+
* Returns GENESIS_HASH if no events exist or none have hashes.
|
|
77
|
+
*/
|
|
78
|
+
export function getLastHash(root) {
|
|
79
|
+
const p = eventsPath(root);
|
|
80
|
+
if (!fs.existsSync(p)) return GENESIS_HASH;
|
|
81
|
+
|
|
82
|
+
const raw = fs.readFileSync(p, "utf8").trim();
|
|
83
|
+
if (!raw) return GENESIS_HASH;
|
|
84
|
+
|
|
85
|
+
const lines = raw.split("\n");
|
|
86
|
+
// Walk backward to find the last event with a hash
|
|
87
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
88
|
+
try {
|
|
89
|
+
const event = JSON.parse(lines[i]);
|
|
90
|
+
if (event.hash) return event.hash;
|
|
91
|
+
} catch {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return GENESIS_HASH;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Sign an event with HMAC, chaining to the previous event's hash.
|
|
100
|
+
* Mutates the event object by adding a `hash` field.
|
|
101
|
+
* Returns the event with hash attached.
|
|
102
|
+
*/
|
|
103
|
+
export function signEvent(root, event) {
|
|
104
|
+
const secret = getAuditSecret(root);
|
|
105
|
+
const prevHash = getLastHash(root);
|
|
106
|
+
event.hash = computeEventHash(prevHash, event, secret);
|
|
107
|
+
return event;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Verify the integrity of the entire audit chain.
|
|
112
|
+
* Returns a detailed verification result.
|
|
113
|
+
*/
|
|
114
|
+
export function verifyAuditChain(root) {
|
|
115
|
+
const secret = getAuditSecret(root);
|
|
116
|
+
const p = eventsPath(root);
|
|
117
|
+
|
|
118
|
+
if (!fs.existsSync(p)) {
|
|
119
|
+
return {
|
|
120
|
+
valid: true,
|
|
121
|
+
totalEvents: 0,
|
|
122
|
+
hashedEvents: 0,
|
|
123
|
+
unhashedEvents: 0,
|
|
124
|
+
brokenAt: null,
|
|
125
|
+
message: "No events log found — chain is trivially valid.",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const raw = fs.readFileSync(p, "utf8").trim();
|
|
130
|
+
if (!raw) {
|
|
131
|
+
return {
|
|
132
|
+
valid: true,
|
|
133
|
+
totalEvents: 0,
|
|
134
|
+
hashedEvents: 0,
|
|
135
|
+
unhashedEvents: 0,
|
|
136
|
+
brokenAt: null,
|
|
137
|
+
message: "Events log is empty — chain is trivially valid.",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const lines = raw.split("\n");
|
|
142
|
+
let prevHash = GENESIS_HASH;
|
|
143
|
+
let valid = true;
|
|
144
|
+
let brokenAt = null;
|
|
145
|
+
let hashedEvents = 0;
|
|
146
|
+
let unhashedEvents = 0;
|
|
147
|
+
let totalEvents = 0;
|
|
148
|
+
const errors = [];
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < lines.length; i++) {
|
|
151
|
+
let event;
|
|
152
|
+
try {
|
|
153
|
+
event = JSON.parse(lines[i]);
|
|
154
|
+
} catch {
|
|
155
|
+
errors.push({ line: i + 1, error: "Invalid JSON" });
|
|
156
|
+
valid = false;
|
|
157
|
+
if (brokenAt === null) brokenAt = i;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
totalEvents++;
|
|
162
|
+
|
|
163
|
+
// Events without hash are pre-v2.1 (backward compatible)
|
|
164
|
+
if (!event.hash) {
|
|
165
|
+
unhashedEvents++;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
hashedEvents++;
|
|
170
|
+
const expectedHash = computeEventHash(prevHash, event, secret);
|
|
171
|
+
|
|
172
|
+
if (event.hash !== expectedHash) {
|
|
173
|
+
valid = false;
|
|
174
|
+
if (brokenAt === null) brokenAt = i;
|
|
175
|
+
errors.push({
|
|
176
|
+
line: i + 1,
|
|
177
|
+
eventId: event.eventId || "unknown",
|
|
178
|
+
error: "Hash mismatch — event may have been tampered with",
|
|
179
|
+
expected: expectedHash.substring(0, 16) + "...",
|
|
180
|
+
actual: event.hash.substring(0, 16) + "...",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
prevHash = event.hash;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const message = valid
|
|
188
|
+
? `Audit chain verified: ${hashedEvents} hashed events, ${unhashedEvents} legacy events (pre-v2.1). No tampering detected.`
|
|
189
|
+
: `AUDIT CHAIN BROKEN at event ${brokenAt + 1}. ${errors.length} error(s) found. Possible tampering or corruption.`;
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
valid,
|
|
193
|
+
totalEvents,
|
|
194
|
+
hashedEvents,
|
|
195
|
+
unhashedEvents,
|
|
196
|
+
brokenAt,
|
|
197
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
198
|
+
message,
|
|
199
|
+
verifiedAt: new Date().toISOString(),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Initialize audit chain for an existing project.
|
|
205
|
+
* Creates the audit key if it doesn't exist.
|
|
206
|
+
* Returns info about the setup.
|
|
207
|
+
*/
|
|
208
|
+
export function initAuditChain(root) {
|
|
209
|
+
const secret = getAuditSecret(root); // This auto-generates if needed
|
|
210
|
+
const keyPath = path.join(speclockDir(root), AUDIT_KEY_FILE);
|
|
211
|
+
const keyExists = fs.existsSync(keyPath);
|
|
212
|
+
const fromEnv = !!process.env.SPECLOCK_AUDIT_SECRET;
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
enabled: true,
|
|
216
|
+
keySource: fromEnv ? "environment" : keyExists ? "file" : "auto-generated",
|
|
217
|
+
keyPath: fromEnv ? "(env: SPECLOCK_AUDIT_SECRET)" : keyPath,
|
|
218
|
+
message: `Audit chain initialized. Secret source: ${fromEnv ? "environment variable" : "local key file"}.`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Ensure .audit-key is in .gitignore.
|
|
224
|
+
* Call this during init to prevent accidental key commits.
|
|
225
|
+
*/
|
|
226
|
+
export function ensureAuditKeyGitignored(root) {
|
|
227
|
+
const gitignorePath = path.join(speclockDir(root), ".gitignore");
|
|
228
|
+
const entry = AUDIT_KEY_FILE;
|
|
229
|
+
|
|
230
|
+
if (fs.existsSync(gitignorePath)) {
|
|
231
|
+
const content = fs.readFileSync(gitignorePath, "utf8");
|
|
232
|
+
if (content.includes(entry)) return; // Already there
|
|
233
|
+
fs.appendFileSync(gitignorePath, `\n${entry}\n`);
|
|
234
|
+
} else {
|
|
235
|
+
fs.writeFileSync(gitignorePath, `${entry}\n`);
|
|
236
|
+
}
|
|
237
|
+
}
|