speclock 2.0.0 → 2.1.1
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 +73 -6
- 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 +11 -0
- package/src/core/license.js +221 -0
- package/src/core/storage.js +9 -0
- package/src/mcp/http-server.js +118 -4
- package/src/mcp/server.js +76 -1
package/README.md
CHANGED
|
@@ -50,11 +50,14 @@ No other tool does this. Not Claude's native memory. Not Mem0. Not CLAUDE.md fil
|
|
|
50
50
|
| Remembers context | Yes | Yes | Manual | **Yes** |
|
|
51
51
|
| **Stops the AI from breaking things** | No | No | No | **Yes — active enforcement** |
|
|
52
52
|
| **Semantic conflict detection** | No | No | No | **Yes — semantic engine v2 (100% detection, 0% false positives)** |
|
|
53
|
+
| **Tamper-proof audit trail** | No | No | No | **Yes — HMAC-SHA256 hash chain** |
|
|
54
|
+
| **Compliance exports** | No | No | No | **Yes — SOC 2, HIPAA, CSV** |
|
|
53
55
|
| Works on Bolt.new | No | No | No | **Yes — npm file-based mode** |
|
|
54
56
|
| Works on Lovable | No | No | No | **Yes — MCP remote** |
|
|
55
57
|
| Structured decisions/locks | No | Tags only | Flat text | **Goals, locks, decisions, changes** |
|
|
56
58
|
| Git-aware (checkpoints, rollback) | No | No | No | **Yes** |
|
|
57
59
|
| Drift detection | No | No | No | **Yes — scans changes against locks** |
|
|
60
|
+
| CI/CD integration | No | No | No | **Yes — GitHub Actions** |
|
|
58
61
|
| Multi-agent timeline | No | No | No | **Yes** |
|
|
59
62
|
| Cross-platform | Claude only | MCP only | Tool-specific | **Universal (MCP + npm)** |
|
|
60
63
|
|
|
@@ -189,10 +192,10 @@ Result: NO CONFLICT (confidence: 7%)
|
|
|
189
192
|
| Mode | Platforms | How It Works |
|
|
190
193
|
|------|-----------|--------------|
|
|
191
194
|
| **MCP Remote** | Lovable, bolt.diy, Base44 | Connect via URL — no install needed |
|
|
192
|
-
| **MCP Local** | Claude Code, Cursor, Windsurf, Cline | `npx speclock serve` —
|
|
195
|
+
| **MCP Local** | Claude Code, Cursor, Windsurf, Cline | `npx speclock serve` — 24 tools via MCP |
|
|
193
196
|
| **npm File-Based** | Bolt.new, Aider, Rocket.new | `npx speclock setup` — AI reads SPECLOCK.md + uses CLI |
|
|
194
197
|
|
|
195
|
-
##
|
|
198
|
+
## 24 MCP Tools
|
|
196
199
|
|
|
197
200
|
### Memory Management
|
|
198
201
|
| Tool | Purpose |
|
|
@@ -240,6 +243,12 @@ Result: NO CONFLICT (confidence: 7%)
|
|
|
240
243
|
| `speclock_report` | Violation report — blocked change stats |
|
|
241
244
|
| `speclock_audit` | Audit staged files against active locks |
|
|
242
245
|
|
|
246
|
+
### Enterprise (v2.1)
|
|
247
|
+
| Tool | Purpose |
|
|
248
|
+
|------|---------|
|
|
249
|
+
| `speclock_verify_audit` | Verify HMAC audit chain integrity — tamper detection |
|
|
250
|
+
| `speclock_export_compliance` | Generate SOC 2 / HIPAA / CSV compliance reports |
|
|
251
|
+
|
|
243
252
|
## Auto-Guard: Locks That Actually Work
|
|
244
253
|
|
|
245
254
|
When you add a lock, SpecLock **automatically finds and guards related files**:
|
|
@@ -303,12 +312,68 @@ speclock audit # Audit staged files against locks
|
|
|
303
312
|
speclock log-change <text> --files x # Log a change
|
|
304
313
|
speclock context # Regenerate context file
|
|
305
314
|
|
|
315
|
+
# Enterprise (v2.1)
|
|
316
|
+
speclock audit-verify # Verify HMAC audit chain integrity
|
|
317
|
+
speclock export --format <soc2|hipaa|csv> # Compliance export
|
|
318
|
+
speclock license # Show license tier and usage
|
|
319
|
+
|
|
306
320
|
# Other
|
|
307
321
|
speclock status # Show brain summary
|
|
308
322
|
speclock serve [--project <path>] # Start MCP server
|
|
309
323
|
speclock watch # Start file watcher
|
|
310
324
|
```
|
|
311
325
|
|
|
326
|
+
## Enterprise Features (v2.1)
|
|
327
|
+
|
|
328
|
+
### HMAC Audit Chain
|
|
329
|
+
Every event in `events.log` gets an HMAC-SHA256 hash chained to the previous event. Modify any event and the chain breaks — instant tamper detection.
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
$ npx speclock audit-verify
|
|
333
|
+
|
|
334
|
+
Audit Chain Verification
|
|
335
|
+
==================================================
|
|
336
|
+
Status: VALID
|
|
337
|
+
Total events: 47
|
|
338
|
+
Hashed events: 47
|
|
339
|
+
Legacy events (pre-v2.1): 0
|
|
340
|
+
Audit chain verified. No tampering detected.
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Compliance Exports
|
|
344
|
+
Generate audit-ready reports for regulated industries:
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
npx speclock export --format soc2 # SOC 2 Type II JSON report
|
|
348
|
+
npx speclock export --format hipaa # HIPAA PHI protection report
|
|
349
|
+
npx speclock export --format csv # All events as CSV spreadsheet
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
SOC 2 reports include: constraint change history, access logs, decision audit trail, audit chain integrity verification. HIPAA reports filter for PHI-related constraints and check encryption/access control status.
|
|
353
|
+
|
|
354
|
+
### License Tiers
|
|
355
|
+
| Tier | Price | Locks | Features |
|
|
356
|
+
|------|-------|-------|----------|
|
|
357
|
+
| **Free** | $0 | 10 | Conflict detection, MCP, CLI, context |
|
|
358
|
+
| **Pro** | $19/mo | Unlimited | + LLM detection, HMAC audit, compliance exports |
|
|
359
|
+
| **Enterprise** | $99/mo | Unlimited | + RBAC, encrypted storage, SSO |
|
|
360
|
+
|
|
361
|
+
### HTTP Server Hardening
|
|
362
|
+
- Rate limiting: 100 req/min per IP (configurable via `SPECLOCK_RATE_LIMIT`)
|
|
363
|
+
- CORS: configurable origins via `SPECLOCK_CORS_ORIGINS`
|
|
364
|
+
- Health endpoint: `GET /health` with uptime and audit chain status
|
|
365
|
+
|
|
366
|
+
### GitHub Actions
|
|
367
|
+
```yaml
|
|
368
|
+
# In your workflow:
|
|
369
|
+
- uses: sgroy10/speclock-check@v2
|
|
370
|
+
with:
|
|
371
|
+
fail-on-conflict: true
|
|
372
|
+
```
|
|
373
|
+
Audits changed files against locks, posts PR comments, fails workflow on violations.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
312
377
|
## Architecture
|
|
313
378
|
|
|
314
379
|
```
|
|
@@ -317,18 +382,20 @@ speclock watch # Start file watcher
|
|
|
317
382
|
└──────────────┬──────────────────┬────────────────────┘
|
|
318
383
|
│ │
|
|
319
384
|
MCP Protocol File-Based (npm)
|
|
320
|
-
(
|
|
385
|
+
(24 tool calls) (reads SPECLOCK.md +
|
|
321
386
|
.speclock/context/latest.md,
|
|
322
387
|
runs CLI commands)
|
|
323
388
|
│ │
|
|
324
389
|
┌──────────────▼──────────────────▼────────────────────┐
|
|
325
390
|
│ SpecLock Core Engine │
|
|
326
|
-
│
|
|
391
|
+
│ Memory | Tracking | Enforcement | Git | Intelligence │
|
|
392
|
+
│ Audit | Compliance | License │
|
|
327
393
|
└──────────────────────┬───────────────────────────────┘
|
|
328
394
|
│
|
|
329
395
|
.speclock/
|
|
330
396
|
├── brain.json (structured memory)
|
|
331
|
-
├── events.log (
|
|
397
|
+
├── events.log (HMAC-signed audit trail)
|
|
398
|
+
├── .audit-key (HMAC secret — gitignored)
|
|
332
399
|
├── patches/ (git diffs per event)
|
|
333
400
|
└── context/
|
|
334
401
|
└── latest.md (human-readable context)
|
|
@@ -352,4 +419,4 @@ MIT License - see [LICENSE](LICENSE) file.
|
|
|
352
419
|
|
|
353
420
|
---
|
|
354
421
|
|
|
355
|
-
*SpecLock v2.
|
|
422
|
+
*SpecLock v2.1.0 — Semantic conflict detection + enterprise audit & compliance. 100% detection, 0% false positives. HMAC audit chain, SOC 2/HIPAA exports. 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": "2.
|
|
4
|
-
"description": "AI constraint engine with
|
|
3
|
+
"version": "2.1.1",
|
|
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 v2.
|
|
85
|
+
SpecLock v2.1.1 — 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
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecLock Compliance Export Engine
|
|
3
|
+
* Generates audit-ready reports for SOC 2, HIPAA, and CSV formats.
|
|
4
|
+
* Designed for enterprise compliance teams and auditors.
|
|
5
|
+
*
|
|
6
|
+
* Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readBrain, readEvents } from "./storage.js";
|
|
10
|
+
import { verifyAuditChain } from "./audit.js";
|
|
11
|
+
|
|
12
|
+
const VERSION = "2.1.1";
|
|
13
|
+
|
|
14
|
+
// PHI-related keywords for HIPAA filtering
|
|
15
|
+
const PHI_KEYWORDS = [
|
|
16
|
+
"patient", "phi", "health", "medical", "hipaa", "ehr", "emr",
|
|
17
|
+
"diagnosis", "treatment", "prescription", "clinical", "healthcare",
|
|
18
|
+
"protected health", "health record", "medical record", "patient data",
|
|
19
|
+
"health information", "insurance", "claims", "billing",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Security-related event types
|
|
23
|
+
const SECURITY_EVENT_TYPES = [
|
|
24
|
+
"lock_added", "lock_removed", "decision_added",
|
|
25
|
+
"goal_updated", "init", "session_started", "session_ended",
|
|
26
|
+
"checkpoint_created", "revert_detected",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if text matches any PHI keywords (case-insensitive).
|
|
31
|
+
*/
|
|
32
|
+
function isPHIRelated(text) {
|
|
33
|
+
if (!text) return false;
|
|
34
|
+
const lower = text.toLowerCase();
|
|
35
|
+
return PHI_KEYWORDS.some((kw) => lower.includes(kw));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate SOC 2 Type II compliance report.
|
|
40
|
+
* Covers: constraint changes, access events, change management, audit integrity.
|
|
41
|
+
*/
|
|
42
|
+
export function exportSOC2(root) {
|
|
43
|
+
const brain = readBrain(root);
|
|
44
|
+
if (!brain) {
|
|
45
|
+
return { error: "SpecLock not initialized. Run speclock init first." };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const events = readEvents(root, { limit: 10000 });
|
|
49
|
+
const auditResult = verifyAuditChain(root);
|
|
50
|
+
const activeLocks = (brain.specLock?.items || []).filter((l) => l.active);
|
|
51
|
+
const allLocks = brain.specLock?.items || [];
|
|
52
|
+
|
|
53
|
+
// Group events by type for analysis
|
|
54
|
+
const eventsByType = {};
|
|
55
|
+
for (const e of events) {
|
|
56
|
+
if (!eventsByType[e.type]) eventsByType[e.type] = [];
|
|
57
|
+
eventsByType[e.type].push(e);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Calculate constraint change history
|
|
61
|
+
const constraintChanges = events
|
|
62
|
+
.filter((e) => ["lock_added", "lock_removed"].includes(e.type))
|
|
63
|
+
.map((e) => ({
|
|
64
|
+
timestamp: e.at || e.ts,
|
|
65
|
+
action: e.type === "lock_added" ? "ADDED" : "REMOVED",
|
|
66
|
+
lockId: e.lockId || e.summary?.match(/\[([^\]]+)\]/)?.[1] || "unknown",
|
|
67
|
+
text: e.lockText || e.summary || "",
|
|
68
|
+
source: e.source || "unknown",
|
|
69
|
+
eventId: e.eventId,
|
|
70
|
+
hash: e.hash || null,
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
// Session history (access log)
|
|
74
|
+
const sessions = (brain.sessions?.history || []).map((s) => ({
|
|
75
|
+
tool: s.tool || "unknown",
|
|
76
|
+
startedAt: s.startedAt,
|
|
77
|
+
endedAt: s.endedAt || null,
|
|
78
|
+
summary: s.summary || "",
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
// Decision audit trail
|
|
82
|
+
const decisions = (brain.decisions || []).map((d) => ({
|
|
83
|
+
id: d.id,
|
|
84
|
+
text: d.text,
|
|
85
|
+
createdAt: d.createdAt,
|
|
86
|
+
source: d.source || "unknown",
|
|
87
|
+
tags: d.tags || [],
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
report: "SOC 2 Type II — SpecLock Compliance Export",
|
|
92
|
+
version: VERSION,
|
|
93
|
+
generatedAt: new Date().toISOString(),
|
|
94
|
+
project: {
|
|
95
|
+
name: brain.project?.name || "unknown",
|
|
96
|
+
id: brain.project?.id || "unknown",
|
|
97
|
+
createdAt: brain.project?.createdAt,
|
|
98
|
+
goal: brain.goal?.text || "",
|
|
99
|
+
},
|
|
100
|
+
auditChainIntegrity: {
|
|
101
|
+
valid: auditResult.valid,
|
|
102
|
+
totalEvents: auditResult.totalEvents,
|
|
103
|
+
hashedEvents: auditResult.hashedEvents,
|
|
104
|
+
unhashedEvents: auditResult.unhashedEvents,
|
|
105
|
+
brokenAt: auditResult.brokenAt,
|
|
106
|
+
verifiedAt: auditResult.verifiedAt,
|
|
107
|
+
},
|
|
108
|
+
constraintManagement: {
|
|
109
|
+
activeConstraints: activeLocks.length,
|
|
110
|
+
totalConstraints: allLocks.length,
|
|
111
|
+
removedConstraints: allLocks.filter((l) => !l.active).length,
|
|
112
|
+
changeHistory: constraintChanges,
|
|
113
|
+
},
|
|
114
|
+
accessLog: {
|
|
115
|
+
totalSessions: sessions.length,
|
|
116
|
+
sessions,
|
|
117
|
+
},
|
|
118
|
+
decisionAuditTrail: {
|
|
119
|
+
totalDecisions: decisions.length,
|
|
120
|
+
decisions,
|
|
121
|
+
},
|
|
122
|
+
changeManagement: {
|
|
123
|
+
totalEvents: events.length,
|
|
124
|
+
eventBreakdown: Object.fromEntries(
|
|
125
|
+
Object.entries(eventsByType).map(([type, evts]) => [type, evts.length])
|
|
126
|
+
),
|
|
127
|
+
recentChanges: (brain.state?.recentChanges || []).slice(0, 20),
|
|
128
|
+
reverts: brain.state?.reverts || [],
|
|
129
|
+
},
|
|
130
|
+
violations: {
|
|
131
|
+
total: (brain.state?.violations || []).length,
|
|
132
|
+
items: (brain.state?.violations || []).slice(0, 50),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generate HIPAA compliance report.
|
|
139
|
+
* Filtered for PHI-related constraints, access events, and encryption status.
|
|
140
|
+
*/
|
|
141
|
+
export function exportHIPAA(root) {
|
|
142
|
+
const brain = readBrain(root);
|
|
143
|
+
if (!brain) {
|
|
144
|
+
return { error: "SpecLock not initialized. Run speclock init first." };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const events = readEvents(root, { limit: 10000 });
|
|
148
|
+
const auditResult = verifyAuditChain(root);
|
|
149
|
+
const allLocks = brain.specLock?.items || [];
|
|
150
|
+
|
|
151
|
+
// Filter PHI-related locks
|
|
152
|
+
const phiLocks = allLocks.filter((l) => isPHIRelated(l.text));
|
|
153
|
+
const activePhiLocks = phiLocks.filter((l) => l.active);
|
|
154
|
+
|
|
155
|
+
// Filter PHI-related events
|
|
156
|
+
const phiEvents = events.filter(
|
|
157
|
+
(e) => isPHIRelated(e.summary || "") || isPHIRelated(e.lockText || "")
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// PHI-related decisions
|
|
161
|
+
const phiDecisions = (brain.decisions || []).filter((d) =>
|
|
162
|
+
isPHIRelated(d.text)
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// PHI-related violations
|
|
166
|
+
const phiViolations = (brain.state?.violations || []).filter(
|
|
167
|
+
(v) => isPHIRelated(v.lockText || "") || isPHIRelated(v.action || "")
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Check encryption status
|
|
171
|
+
const encryptionEnabled = !!process.env.SPECLOCK_ENCRYPTION_KEY;
|
|
172
|
+
|
|
173
|
+
// Access controls
|
|
174
|
+
const hasAuth = !!process.env.SPECLOCK_API_KEY;
|
|
175
|
+
const hasAuditChain = auditResult.hashedEvents > 0;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
report: "HIPAA Compliance — SpecLock PHI Protection Report",
|
|
179
|
+
version: VERSION,
|
|
180
|
+
generatedAt: new Date().toISOString(),
|
|
181
|
+
project: {
|
|
182
|
+
name: brain.project?.name || "unknown",
|
|
183
|
+
id: brain.project?.id || "unknown",
|
|
184
|
+
},
|
|
185
|
+
safeguards: {
|
|
186
|
+
technicalSafeguards: {
|
|
187
|
+
auditControls: {
|
|
188
|
+
enabled: hasAuditChain,
|
|
189
|
+
chainValid: auditResult.valid,
|
|
190
|
+
totalAuditedEvents: auditResult.hashedEvents,
|
|
191
|
+
status: hasAuditChain
|
|
192
|
+
? auditResult.valid
|
|
193
|
+
? "COMPLIANT"
|
|
194
|
+
: "NON-COMPLIANT — audit chain broken"
|
|
195
|
+
: "PARTIAL — enable HMAC audit chain for full compliance",
|
|
196
|
+
},
|
|
197
|
+
accessControl: {
|
|
198
|
+
authEnabled: hasAuth,
|
|
199
|
+
status: hasAuth
|
|
200
|
+
? "COMPLIANT"
|
|
201
|
+
: "NON-COMPLIANT — no API key authentication",
|
|
202
|
+
},
|
|
203
|
+
encryption: {
|
|
204
|
+
atRest: encryptionEnabled,
|
|
205
|
+
algorithm: encryptionEnabled ? "AES-256-GCM" : "none",
|
|
206
|
+
status: encryptionEnabled
|
|
207
|
+
? "COMPLIANT"
|
|
208
|
+
: "NON-COMPLIANT — enable SPECLOCK_ENCRYPTION_KEY",
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
administrativeSafeguards: {
|
|
212
|
+
constraintEnforcement: {
|
|
213
|
+
totalPhiConstraints: phiLocks.length,
|
|
214
|
+
activePhiConstraints: activePhiLocks.length,
|
|
215
|
+
constraints: activePhiLocks.map((l) => ({
|
|
216
|
+
id: l.id,
|
|
217
|
+
text: l.text,
|
|
218
|
+
createdAt: l.createdAt,
|
|
219
|
+
source: l.source,
|
|
220
|
+
})),
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
phiProtection: {
|
|
225
|
+
constraints: phiLocks.map((l) => ({
|
|
226
|
+
id: l.id,
|
|
227
|
+
text: l.text,
|
|
228
|
+
active: l.active,
|
|
229
|
+
createdAt: l.createdAt,
|
|
230
|
+
})),
|
|
231
|
+
decisions: phiDecisions.map((d) => ({
|
|
232
|
+
id: d.id,
|
|
233
|
+
text: d.text,
|
|
234
|
+
createdAt: d.createdAt,
|
|
235
|
+
})),
|
|
236
|
+
violations: phiViolations,
|
|
237
|
+
relatedEvents: phiEvents.length,
|
|
238
|
+
},
|
|
239
|
+
auditTrail: {
|
|
240
|
+
integrity: auditResult,
|
|
241
|
+
phiEventCount: phiEvents.length,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Generate CSV export of all events.
|
|
248
|
+
* Returns a CSV string suitable for auditor spreadsheets.
|
|
249
|
+
*/
|
|
250
|
+
export function exportCSV(root) {
|
|
251
|
+
const events = readEvents(root, { limit: 10000 });
|
|
252
|
+
|
|
253
|
+
if (!events.length) {
|
|
254
|
+
return "timestamp,event_id,type,summary,files,hash\n";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Reverse back to chronological order (readEvents returns newest first)
|
|
258
|
+
events.reverse();
|
|
259
|
+
|
|
260
|
+
const header = "timestamp,event_id,type,summary,files,hash";
|
|
261
|
+
const rows = events.map((e) => {
|
|
262
|
+
const timestamp = e.at || e.ts || "";
|
|
263
|
+
const eventId = e.eventId || "";
|
|
264
|
+
const type = e.type || "";
|
|
265
|
+
const summary = (e.summary || "").replace(/"/g, '""');
|
|
266
|
+
const files = (e.files || []).join("; ");
|
|
267
|
+
const hash = e.hash || "";
|
|
268
|
+
return `"${timestamp}","${eventId}","${type}","${summary}","${files}","${hash}"`;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return [header, ...rows].join("\n");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Main export function — dispatches by format.
|
|
276
|
+
*/
|
|
277
|
+
export function exportCompliance(root, format) {
|
|
278
|
+
switch (format) {
|
|
279
|
+
case "soc2":
|
|
280
|
+
return { format: "soc2", data: exportSOC2(root) };
|
|
281
|
+
case "hipaa":
|
|
282
|
+
return { format: "hipaa", data: exportHIPAA(root) };
|
|
283
|
+
case "csv":
|
|
284
|
+
return { format: "csv", data: exportCSV(root) };
|
|
285
|
+
default:
|
|
286
|
+
return {
|
|
287
|
+
error: `Unknown format: ${format}. Supported: soc2, hipaa, csv`,
|
|
288
|
+
supportedFormats: ["soc2", "hipaa", "csv"],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
package/src/core/engine.js
CHANGED
|
@@ -18,6 +18,10 @@ import {
|
|
|
18
18
|
import { hasGit, getHead, getDefaultBranch, captureDiff, getStagedFiles } from "./git.js";
|
|
19
19
|
import { getTemplateNames, getTemplate } from "./templates.js";
|
|
20
20
|
import { analyzeConflict } from "./semantics.js";
|
|
21
|
+
import { ensureAuditKeyGitignored } from "./audit.js";
|
|
22
|
+
import { verifyAuditChain } from "./audit.js";
|
|
23
|
+
import { exportCompliance } from "./compliance.js";
|
|
24
|
+
import { checkFeature, checkLimits, getLicenseInfo } from "./license.js";
|
|
21
25
|
|
|
22
26
|
// --- Internal helpers ---
|
|
23
27
|
|
|
@@ -41,6 +45,7 @@ function writePatch(root, eventId, content) {
|
|
|
41
45
|
|
|
42
46
|
export function ensureInit(root) {
|
|
43
47
|
ensureSpeclockDirs(root);
|
|
48
|
+
try { ensureAuditKeyGitignored(root); } catch { /* non-critical */ }
|
|
44
49
|
let brain = readBrain(root);
|
|
45
50
|
if (!brain) {
|
|
46
51
|
const gitExists = hasGit(root);
|
|
@@ -1215,3 +1220,9 @@ export function auditStagedFiles(root) {
|
|
|
1215
1220
|
message,
|
|
1216
1221
|
};
|
|
1217
1222
|
}
|
|
1223
|
+
|
|
1224
|
+
// --- Enterprise features (v2.1) ---
|
|
1225
|
+
|
|
1226
|
+
export { verifyAuditChain } from "./audit.js";
|
|
1227
|
+
export { exportCompliance } from "./compliance.js";
|
|
1228
|
+
export { checkFeature, checkLimits, getLicenseInfo } from "./license.js";
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecLock Freemium License System
|
|
3
|
+
* Three tiers: Free, Pro ($19/mo), Enterprise ($99/mo).
|
|
4
|
+
* Graceful degradation — free tier always works.
|
|
5
|
+
*
|
|
6
|
+
* Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import crypto from "crypto";
|
|
12
|
+
import { speclockDir, readBrain } from "./storage.js";
|
|
13
|
+
|
|
14
|
+
// Tier definitions
|
|
15
|
+
const TIERS = {
|
|
16
|
+
free: {
|
|
17
|
+
name: "Free",
|
|
18
|
+
maxLocks: 10,
|
|
19
|
+
maxDecisions: 5,
|
|
20
|
+
maxEvents: 500,
|
|
21
|
+
features: ["basic_conflict_detection", "context_pack", "session_tracking", "cli", "mcp"],
|
|
22
|
+
},
|
|
23
|
+
pro: {
|
|
24
|
+
name: "Pro",
|
|
25
|
+
maxLocks: Infinity,
|
|
26
|
+
maxDecisions: Infinity,
|
|
27
|
+
maxEvents: Infinity,
|
|
28
|
+
features: [
|
|
29
|
+
"basic_conflict_detection", "context_pack", "session_tracking", "cli", "mcp",
|
|
30
|
+
"llm_conflict_detection", "hmac_audit_chain", "compliance_exports",
|
|
31
|
+
"drift_detection", "templates", "github_actions",
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
enterprise: {
|
|
35
|
+
name: "Enterprise",
|
|
36
|
+
maxLocks: Infinity,
|
|
37
|
+
maxDecisions: Infinity,
|
|
38
|
+
maxEvents: Infinity,
|
|
39
|
+
features: [
|
|
40
|
+
"basic_conflict_detection", "context_pack", "session_tracking", "cli", "mcp",
|
|
41
|
+
"llm_conflict_detection", "hmac_audit_chain", "compliance_exports",
|
|
42
|
+
"drift_detection", "templates", "github_actions",
|
|
43
|
+
"rbac", "encrypted_storage", "sso", "hard_enforcement",
|
|
44
|
+
"semantic_precommit", "multi_project", "priority_support",
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const LICENSE_FILE = ".license";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the current license key from env or file.
|
|
53
|
+
*/
|
|
54
|
+
function getLicenseKey(root) {
|
|
55
|
+
// 1. Environment variable
|
|
56
|
+
if (process.env.SPECLOCK_LICENSE_KEY) {
|
|
57
|
+
return process.env.SPECLOCK_LICENSE_KEY;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. License file in .speclock/
|
|
61
|
+
if (root) {
|
|
62
|
+
const licensePath = path.join(speclockDir(root), LICENSE_FILE);
|
|
63
|
+
if (fs.existsSync(licensePath)) {
|
|
64
|
+
return fs.readFileSync(licensePath, "utf8").trim();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Decode a license key to extract tier and expiry.
|
|
73
|
+
* License format: base64(JSON({ tier, expiresAt, signature }))
|
|
74
|
+
* For now, a simple validation — full crypto verification in v3.0.
|
|
75
|
+
*/
|
|
76
|
+
function decodeLicense(key) {
|
|
77
|
+
if (!key) return null;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const decoded = JSON.parse(Buffer.from(key, "base64").toString("utf8"));
|
|
81
|
+
if (!decoded.tier || !decoded.expiresAt) return null;
|
|
82
|
+
|
|
83
|
+
// Check expiry
|
|
84
|
+
if (new Date(decoded.expiresAt) < new Date()) {
|
|
85
|
+
return { tier: "free", expired: true, originalTier: decoded.tier };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Validate tier name
|
|
89
|
+
if (!TIERS[decoded.tier]) return null;
|
|
90
|
+
|
|
91
|
+
return { tier: decoded.tier, expiresAt: decoded.expiresAt, expired: false };
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the current tier for this project.
|
|
99
|
+
*/
|
|
100
|
+
export function getTier(root) {
|
|
101
|
+
const key = getLicenseKey(root);
|
|
102
|
+
if (!key) return "free";
|
|
103
|
+
|
|
104
|
+
const license = decodeLicense(key);
|
|
105
|
+
if (!license || license.expired) return "free";
|
|
106
|
+
|
|
107
|
+
return license.tier;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get the limits for the current tier.
|
|
112
|
+
*/
|
|
113
|
+
export function getLimits(root) {
|
|
114
|
+
const tier = getTier(root);
|
|
115
|
+
return { tier, ...TIERS[tier] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if a specific feature is available in the current tier.
|
|
120
|
+
* Returns { allowed: bool, tier: string, requiredTier: string|null }
|
|
121
|
+
*/
|
|
122
|
+
export function checkFeature(root, featureName) {
|
|
123
|
+
const tier = getTier(root);
|
|
124
|
+
const tierConfig = TIERS[tier];
|
|
125
|
+
|
|
126
|
+
if (tierConfig.features.includes(featureName)) {
|
|
127
|
+
return { allowed: true, tier, requiredTier: null };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Find which tier has this feature
|
|
131
|
+
const requiredTier = Object.entries(TIERS).find(
|
|
132
|
+
([_, config]) => config.features.includes(featureName)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
allowed: false,
|
|
137
|
+
tier,
|
|
138
|
+
requiredTier: requiredTier ? requiredTier[0] : null,
|
|
139
|
+
message: requiredTier
|
|
140
|
+
? `Feature "${featureName}" requires ${requiredTier[1].name} tier. Current: ${tierConfig.name}. Upgrade at https://speclock.dev/pricing`
|
|
141
|
+
: `Unknown feature: ${featureName}`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if the current project is within its tier limits.
|
|
147
|
+
* Returns { withinLimits: bool, warnings: string[] }
|
|
148
|
+
*/
|
|
149
|
+
export function checkLimits(root) {
|
|
150
|
+
const tier = getTier(root);
|
|
151
|
+
const limits = TIERS[tier];
|
|
152
|
+
const brain = readBrain(root);
|
|
153
|
+
const warnings = [];
|
|
154
|
+
|
|
155
|
+
if (!brain) return { withinLimits: true, warnings: [], tier };
|
|
156
|
+
|
|
157
|
+
const activeLocks = (brain.specLock?.items || []).filter((l) => l.active).length;
|
|
158
|
+
const decisions = (brain.decisions || []).length;
|
|
159
|
+
const eventCount = brain.events?.count || 0;
|
|
160
|
+
|
|
161
|
+
if (activeLocks >= limits.maxLocks) {
|
|
162
|
+
warnings.push(
|
|
163
|
+
`Lock limit reached (${activeLocks}/${limits.maxLocks}). Upgrade to Pro for unlimited locks.`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (decisions >= limits.maxDecisions) {
|
|
168
|
+
warnings.push(
|
|
169
|
+
`Decision limit reached (${decisions}/${limits.maxDecisions}). Upgrade to Pro for unlimited decisions.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (eventCount >= limits.maxEvents) {
|
|
174
|
+
warnings.push(
|
|
175
|
+
`Event limit reached (${eventCount}/${limits.maxEvents}). Upgrade to Pro for unlimited events.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
withinLimits: warnings.length === 0,
|
|
181
|
+
warnings,
|
|
182
|
+
tier,
|
|
183
|
+
usage: {
|
|
184
|
+
locks: { current: activeLocks, max: limits.maxLocks },
|
|
185
|
+
decisions: { current: decisions, max: limits.maxDecisions },
|
|
186
|
+
events: { current: eventCount, max: limits.maxEvents },
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Generate a license key (for testing / admin use).
|
|
193
|
+
* In production, keys would be generated server-side.
|
|
194
|
+
*/
|
|
195
|
+
export function generateLicenseKey(tier, daysValid = 30) {
|
|
196
|
+
if (!TIERS[tier]) throw new Error(`Invalid tier: ${tier}`);
|
|
197
|
+
|
|
198
|
+
const expiresAt = new Date(Date.now() + daysValid * 24 * 60 * 60 * 1000).toISOString();
|
|
199
|
+
const payload = { tier, expiresAt, issuedAt: new Date().toISOString() };
|
|
200
|
+
|
|
201
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get license info for display.
|
|
206
|
+
*/
|
|
207
|
+
export function getLicenseInfo(root) {
|
|
208
|
+
const key = getLicenseKey(root);
|
|
209
|
+
const tier = getTier(root);
|
|
210
|
+
const limits = checkLimits(root);
|
|
211
|
+
const license = key ? decodeLicense(key) : null;
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
tier: TIERS[tier].name,
|
|
215
|
+
tierKey: tier,
|
|
216
|
+
expiresAt: license?.expiresAt || null,
|
|
217
|
+
expired: license?.expired || false,
|
|
218
|
+
...limits,
|
|
219
|
+
features: TIERS[tier].features,
|
|
220
|
+
};
|
|
221
|
+
}
|
package/src/core/storage.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import crypto from "crypto";
|
|
4
|
+
import { signEvent, isAuditEnabled } from "./audit.js";
|
|
4
5
|
|
|
5
6
|
export function nowIso() {
|
|
6
7
|
return new Date().toISOString();
|
|
@@ -140,6 +141,14 @@ export function writeBrain(root, brain) {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
export function appendEvent(root, event) {
|
|
144
|
+
// HMAC audit chain — sign event if audit is enabled
|
|
145
|
+
try {
|
|
146
|
+
if (isAuditEnabled(root)) {
|
|
147
|
+
signEvent(root, event);
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// Audit error — write event without hash (graceful degradation)
|
|
151
|
+
}
|
|
143
152
|
const line = JSON.stringify(event);
|
|
144
153
|
fs.appendFileSync(eventsPath(root), `${line}\n`);
|
|
145
154
|
}
|
package/src/mcp/http-server.js
CHANGED
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
applyTemplate,
|
|
29
29
|
generateReport,
|
|
30
30
|
auditStagedFiles,
|
|
31
|
+
verifyAuditChain,
|
|
32
|
+
exportCompliance,
|
|
31
33
|
} from "../core/engine.js";
|
|
32
34
|
import { generateContext, generateContextPack } from "../core/context.js";
|
|
33
35
|
import {
|
|
@@ -46,8 +48,51 @@ import {
|
|
|
46
48
|
} from "../core/git.js";
|
|
47
49
|
|
|
48
50
|
const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
49
|
-
const VERSION = "2.
|
|
51
|
+
const VERSION = "2.1.1";
|
|
50
52
|
const AUTHOR = "Sandeep Roy";
|
|
53
|
+
const START_TIME = Date.now();
|
|
54
|
+
|
|
55
|
+
// --- Rate Limiting ---
|
|
56
|
+
const RATE_LIMIT = parseInt(process.env.SPECLOCK_RATE_LIMIT || "100", 10);
|
|
57
|
+
const RATE_WINDOW_MS = 60_000;
|
|
58
|
+
const rateLimitMap = new Map();
|
|
59
|
+
|
|
60
|
+
function checkRateLimit(ip) {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
if (!rateLimitMap.has(ip)) {
|
|
63
|
+
rateLimitMap.set(ip, []);
|
|
64
|
+
}
|
|
65
|
+
const timestamps = rateLimitMap.get(ip).filter((t) => now - t < RATE_WINDOW_MS);
|
|
66
|
+
timestamps.push(now);
|
|
67
|
+
rateLimitMap.set(ip, timestamps);
|
|
68
|
+
return timestamps.length <= RATE_LIMIT;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Clean up stale entries every 5 minutes
|
|
72
|
+
setInterval(() => {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
for (const [ip, timestamps] of rateLimitMap) {
|
|
75
|
+
const active = timestamps.filter((t) => now - t < RATE_WINDOW_MS);
|
|
76
|
+
if (active.length === 0) rateLimitMap.delete(ip);
|
|
77
|
+
else rateLimitMap.set(ip, active);
|
|
78
|
+
}
|
|
79
|
+
}, 5 * 60_000);
|
|
80
|
+
|
|
81
|
+
// --- CORS Configuration ---
|
|
82
|
+
const ALLOWED_ORIGINS = process.env.SPECLOCK_CORS_ORIGINS
|
|
83
|
+
? process.env.SPECLOCK_CORS_ORIGINS.split(",").map((s) => s.trim())
|
|
84
|
+
: ["*"];
|
|
85
|
+
|
|
86
|
+
function setCorsHeaders(res) {
|
|
87
|
+
const origin = ALLOWED_ORIGINS.includes("*") ? "*" : ALLOWED_ORIGINS.join(", ");
|
|
88
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
89
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
90
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
91
|
+
res.setHeader("Access-Control-Max-Age", "86400");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- Request Size Limit ---
|
|
95
|
+
const MAX_BODY_SIZE = 1024 * 1024; // 1MB
|
|
51
96
|
|
|
52
97
|
function createSpecLockServer() {
|
|
53
98
|
const server = new McpServer(
|
|
@@ -293,13 +338,57 @@ function createSpecLockServer() {
|
|
|
293
338
|
return { content: [{ type: "text", text: `## Audit Failed\n\n${text}\n\n${result.message}` }] };
|
|
294
339
|
});
|
|
295
340
|
|
|
341
|
+
// Tool 23: speclock_verify_audit
|
|
342
|
+
server.tool("speclock_verify_audit", "Verify the integrity of the HMAC audit chain.", {}, async () => {
|
|
343
|
+
ensureInit(PROJECT_ROOT);
|
|
344
|
+
const result = verifyAuditChain(PROJECT_ROOT);
|
|
345
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Tool 24: speclock_export_compliance
|
|
349
|
+
server.tool("speclock_export_compliance", "Generate compliance reports (SOC 2, HIPAA, CSV).", { format: z.enum(["soc2", "hipaa", "csv"]).describe("Export format") }, async ({ format }) => {
|
|
350
|
+
ensureInit(PROJECT_ROOT);
|
|
351
|
+
const result = exportCompliance(PROJECT_ROOT, format);
|
|
352
|
+
if (result.error) return { content: [{ type: "text", text: result.error }], isError: true };
|
|
353
|
+
const output = format === "csv" ? result.data : JSON.stringify(result.data, null, 2);
|
|
354
|
+
return { content: [{ type: "text", text: output }] };
|
|
355
|
+
});
|
|
356
|
+
|
|
296
357
|
return server;
|
|
297
358
|
}
|
|
298
359
|
|
|
299
360
|
// --- HTTP Server ---
|
|
300
361
|
const app = createMcpExpressApp({ host: "0.0.0.0" });
|
|
301
362
|
|
|
363
|
+
// CORS preflight handler
|
|
364
|
+
app.options("*", (req, res) => {
|
|
365
|
+
setCorsHeaders(res);
|
|
366
|
+
res.writeHead(204).end();
|
|
367
|
+
});
|
|
368
|
+
|
|
302
369
|
app.post("/mcp", async (req, res) => {
|
|
370
|
+
setCorsHeaders(res);
|
|
371
|
+
|
|
372
|
+
// Rate limiting
|
|
373
|
+
const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
|
|
374
|
+
if (!checkRateLimit(clientIp)) {
|
|
375
|
+
return res.status(429).json({
|
|
376
|
+
jsonrpc: "2.0",
|
|
377
|
+
error: { code: -32000, message: `Rate limit exceeded (${RATE_LIMIT} req/min). Try again later.` },
|
|
378
|
+
id: null,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Request size check
|
|
383
|
+
const contentLength = parseInt(req.headers["content-length"] || "0", 10);
|
|
384
|
+
if (contentLength > MAX_BODY_SIZE) {
|
|
385
|
+
return res.status(413).json({
|
|
386
|
+
jsonrpc: "2.0",
|
|
387
|
+
error: { code: -32000, message: `Request too large (max ${MAX_BODY_SIZE / 1024}KB)` },
|
|
388
|
+
id: null,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
303
392
|
const server = createSpecLockServer();
|
|
304
393
|
try {
|
|
305
394
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
@@ -318,22 +407,47 @@ app.post("/mcp", async (req, res) => {
|
|
|
318
407
|
});
|
|
319
408
|
|
|
320
409
|
app.get("/mcp", async (req, res) => {
|
|
410
|
+
setCorsHeaders(res);
|
|
321
411
|
res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
|
|
322
412
|
});
|
|
323
413
|
|
|
324
414
|
app.delete("/mcp", async (req, res) => {
|
|
415
|
+
setCorsHeaders(res);
|
|
325
416
|
res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
|
|
326
417
|
});
|
|
327
418
|
|
|
328
|
-
// Health check endpoint
|
|
419
|
+
// Health check endpoint (enhanced for enterprise)
|
|
420
|
+
app.get("/health", (req, res) => {
|
|
421
|
+
setCorsHeaders(res);
|
|
422
|
+
let auditStatus = "unknown";
|
|
423
|
+
try {
|
|
424
|
+
const result = verifyAuditChain(PROJECT_ROOT);
|
|
425
|
+
auditStatus = result.valid ? "valid" : "broken";
|
|
426
|
+
} catch {
|
|
427
|
+
auditStatus = "unavailable";
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
res.json({
|
|
431
|
+
status: "healthy",
|
|
432
|
+
version: VERSION,
|
|
433
|
+
uptime: Math.floor((Date.now() - START_TIME) / 1000),
|
|
434
|
+
tools: 24,
|
|
435
|
+
auditChain: auditStatus,
|
|
436
|
+
rateLimit: { limit: RATE_LIMIT, windowMs: RATE_WINDOW_MS },
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Root info endpoint
|
|
329
441
|
app.get("/", (req, res) => {
|
|
442
|
+
setCorsHeaders(res);
|
|
330
443
|
res.json({
|
|
331
444
|
name: "speclock",
|
|
332
445
|
version: VERSION,
|
|
333
446
|
author: AUTHOR,
|
|
334
|
-
description: "AI Continuity Engine
|
|
335
|
-
tools:
|
|
447
|
+
description: "AI Continuity Engine with enterprise audit, compliance, and enforcement",
|
|
448
|
+
tools: 24,
|
|
336
449
|
mcp_endpoint: "/mcp",
|
|
450
|
+
health_endpoint: "/health",
|
|
337
451
|
npm: "https://www.npmjs.com/package/speclock",
|
|
338
452
|
github: "https://github.com/sgroy10/speclock",
|
|
339
453
|
});
|
package/src/mcp/server.js
CHANGED
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
applyTemplate,
|
|
24
24
|
generateReport,
|
|
25
25
|
auditStagedFiles,
|
|
26
|
+
verifyAuditChain,
|
|
27
|
+
exportCompliance,
|
|
28
|
+
checkLimits,
|
|
29
|
+
getLicenseInfo,
|
|
26
30
|
} from "../core/engine.js";
|
|
27
31
|
import { generateContext, generateContextPack } from "../core/context.js";
|
|
28
32
|
import {
|
|
@@ -57,7 +61,7 @@ const PROJECT_ROOT =
|
|
|
57
61
|
args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
58
62
|
|
|
59
63
|
// --- MCP Server ---
|
|
60
|
-
const VERSION = "2.
|
|
64
|
+
const VERSION = "2.1.1";
|
|
61
65
|
const AUTHOR = "Sandeep Roy";
|
|
62
66
|
|
|
63
67
|
const server = new McpServer(
|
|
@@ -883,6 +887,77 @@ server.tool(
|
|
|
883
887
|
}
|
|
884
888
|
);
|
|
885
889
|
|
|
890
|
+
// ========================================
|
|
891
|
+
// ENTERPRISE TOOLS (v2.1)
|
|
892
|
+
// ========================================
|
|
893
|
+
|
|
894
|
+
// Tool 23: speclock_verify_audit
|
|
895
|
+
server.tool(
|
|
896
|
+
"speclock_verify_audit",
|
|
897
|
+
"Verify the integrity of the HMAC audit chain. Detects tampering or corruption in the event log. Returns chain status, total events, and any broken links.",
|
|
898
|
+
{},
|
|
899
|
+
async () => {
|
|
900
|
+
ensureInit(PROJECT_ROOT);
|
|
901
|
+
const result = verifyAuditChain(PROJECT_ROOT);
|
|
902
|
+
|
|
903
|
+
const status = result.valid ? "VALID" : "BROKEN";
|
|
904
|
+
const parts = [
|
|
905
|
+
`## Audit Chain Verification`,
|
|
906
|
+
``,
|
|
907
|
+
`Status: **${status}**`,
|
|
908
|
+
`Total events: ${result.totalEvents}`,
|
|
909
|
+
`Hashed events: ${result.hashedEvents}`,
|
|
910
|
+
`Legacy events (pre-v2.1): ${result.unhashedEvents}`,
|
|
911
|
+
];
|
|
912
|
+
|
|
913
|
+
if (!result.valid && result.errors) {
|
|
914
|
+
parts.push(``, `### Errors`);
|
|
915
|
+
for (const err of result.errors) {
|
|
916
|
+
parts.push(`- Line ${err.line}: ${err.error}${err.eventId ? ` (${err.eventId})` : ""}`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
parts.push(``, result.message);
|
|
921
|
+
parts.push(``, `Verified at: ${result.verifiedAt}`);
|
|
922
|
+
|
|
923
|
+
return {
|
|
924
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
// Tool 24: speclock_export_compliance
|
|
930
|
+
server.tool(
|
|
931
|
+
"speclock_export_compliance",
|
|
932
|
+
"Generate compliance reports for enterprise auditing. Supports SOC 2 Type II, HIPAA, and CSV formats. Reports include constraint management, access logs, audit chain integrity, and violation history.",
|
|
933
|
+
{
|
|
934
|
+
format: z
|
|
935
|
+
.enum(["soc2", "hipaa", "csv"])
|
|
936
|
+
.describe("Export format: soc2 (JSON), hipaa (JSON), csv (spreadsheet)"),
|
|
937
|
+
},
|
|
938
|
+
async ({ format }) => {
|
|
939
|
+
ensureInit(PROJECT_ROOT);
|
|
940
|
+
const result = exportCompliance(PROJECT_ROOT, format);
|
|
941
|
+
|
|
942
|
+
if (result.error) {
|
|
943
|
+
return {
|
|
944
|
+
content: [{ type: "text", text: result.error }],
|
|
945
|
+
isError: true,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (format === "csv") {
|
|
950
|
+
return {
|
|
951
|
+
content: [{ type: "text", text: `## Compliance Export (CSV)\n\n\`\`\`csv\n${result.data}\n\`\`\`` }],
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return {
|
|
956
|
+
content: [{ type: "text", text: `## Compliance Export (${format.toUpperCase()})\n\n\`\`\`json\n${JSON.stringify(result.data, null, 2)}\n\`\`\`` }],
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
);
|
|
960
|
+
|
|
886
961
|
// --- Smithery sandbox export ---
|
|
887
962
|
export default function createSandboxServer() {
|
|
888
963
|
return server;
|