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.
@@ -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
  }
@@ -19,6 +19,7 @@ import {
19
19
  updateDeployFacts,
20
20
  logChange,
21
21
  checkConflict,
22
+ checkConflictAsync,
22
23
  getSessionBriefing,
23
24
  endSession,
24
25
  suggestLocks,
@@ -27,6 +28,8 @@ import {
27
28
  applyTemplate,
28
29
  generateReport,
29
30
  auditStagedFiles,
31
+ verifyAuditChain,
32
+ exportCompliance,
30
33
  } from "../core/engine.js";
31
34
  import { generateContext, generateContextPack } from "../core/context.js";
32
35
  import {
@@ -45,8 +48,51 @@ import {
45
48
  } from "../core/git.js";
46
49
 
47
50
  const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
48
- const VERSION = "1.7.0";
51
+ const VERSION = "2.1.0";
49
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
50
96
 
51
97
  function createSpecLockServer() {
52
98
  const server = new McpServer(
@@ -176,7 +222,7 @@ function createSpecLockServer() {
176
222
  // Tool 12: speclock_check_conflict
177
223
  server.tool("speclock_check_conflict", "Check if a proposed action conflicts with any active SpecLock.", { proposedAction: z.string().min(1).describe("Description of the action") }, async ({ proposedAction }) => {
178
224
  ensureInit(PROJECT_ROOT);
179
- const result = checkConflict(PROJECT_ROOT, proposedAction);
225
+ const result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
180
226
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
181
227
  });
182
228
 
@@ -292,13 +338,57 @@ function createSpecLockServer() {
292
338
  return { content: [{ type: "text", text: `## Audit Failed\n\n${text}\n\n${result.message}` }] };
293
339
  });
294
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
+
295
357
  return server;
296
358
  }
297
359
 
298
360
  // --- HTTP Server ---
299
361
  const app = createMcpExpressApp({ host: "0.0.0.0" });
300
362
 
363
+ // CORS preflight handler
364
+ app.options("*", (req, res) => {
365
+ setCorsHeaders(res);
366
+ res.writeHead(204).end();
367
+ });
368
+
301
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
+
302
392
  const server = createSpecLockServer();
303
393
  try {
304
394
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
@@ -317,22 +407,47 @@ app.post("/mcp", async (req, res) => {
317
407
  });
318
408
 
319
409
  app.get("/mcp", async (req, res) => {
410
+ setCorsHeaders(res);
320
411
  res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
321
412
  });
322
413
 
323
414
  app.delete("/mcp", async (req, res) => {
415
+ setCorsHeaders(res);
324
416
  res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
325
417
  });
326
418
 
327
- // 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
328
441
  app.get("/", (req, res) => {
442
+ setCorsHeaders(res);
329
443
  res.json({
330
444
  name: "speclock",
331
445
  version: VERSION,
332
446
  author: AUTHOR,
333
- description: "AI Continuity Engine Kill AI amnesia",
334
- tools: 22,
447
+ description: "AI Continuity Engine with enterprise audit, compliance, and enforcement",
448
+ tools: 24,
335
449
  mcp_endpoint: "/mcp",
450
+ health_endpoint: "/health",
336
451
  npm: "https://www.npmjs.com/package/speclock",
337
452
  github: "https://github.com/sgroy10/speclock",
338
453
  });
package/src/mcp/server.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  updateDeployFacts,
13
13
  logChange,
14
14
  checkConflict,
15
+ checkConflictAsync,
15
16
  getSessionBriefing,
16
17
  endSession,
17
18
  suggestLocks,
@@ -22,6 +23,10 @@ import {
22
23
  applyTemplate,
23
24
  generateReport,
24
25
  auditStagedFiles,
26
+ verifyAuditChain,
27
+ exportCompliance,
28
+ checkLimits,
29
+ getLicenseInfo,
25
30
  } from "../core/engine.js";
26
31
  import { generateContext, generateContextPack } from "../core/context.js";
27
32
  import {
@@ -56,7 +61,7 @@ const PROJECT_ROOT =
56
61
  args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
57
62
 
58
63
  // --- MCP Server ---
59
- const VERSION = "1.7.0";
64
+ const VERSION = "2.1.0";
60
65
  const AUTHOR = "Sandeep Roy";
61
66
 
62
67
  const server = new McpServer(
@@ -427,7 +432,7 @@ server.tool(
427
432
  .describe("Description of the action you plan to take"),
428
433
  },
429
434
  async ({ proposedAction }) => {
430
- const result = checkConflict(PROJECT_ROOT, proposedAction);
435
+ const result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
431
436
  return {
432
437
  content: [{ type: "text", text: result.analysis }],
433
438
  };
@@ -882,6 +887,77 @@ server.tool(
882
887
  }
883
888
  );
884
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
+
885
961
  // --- Smithery sandbox export ---
886
962
  export default function createSandboxServer() {
887
963
  return server;