@mcp-audit-gateway/core 0.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.
Files changed (145) hide show
  1. package/.github/workflows/npm-publish.yml +21 -0
  2. package/CONTRIBUTING.md +41 -0
  3. package/LICENSE +21 -0
  4. package/README.md +156 -0
  5. package/dist/attestation/audit-log.d.ts +25 -0
  6. package/dist/attestation/audit-log.d.ts.map +1 -0
  7. package/dist/attestation/audit-log.js +95 -0
  8. package/dist/attestation/audit-log.js.map +1 -0
  9. package/dist/attestation/bugfix.test.d.ts +2 -0
  10. package/dist/attestation/bugfix.test.d.ts.map +1 -0
  11. package/dist/attestation/bugfix.test.js +103 -0
  12. package/dist/attestation/bugfix.test.js.map +1 -0
  13. package/dist/attestation/chain.test.d.ts +2 -0
  14. package/dist/attestation/chain.test.d.ts.map +1 -0
  15. package/dist/attestation/chain.test.js +153 -0
  16. package/dist/attestation/chain.test.js.map +1 -0
  17. package/dist/attestation/keygen.d.ts +5 -0
  18. package/dist/attestation/keygen.d.ts.map +1 -0
  19. package/dist/attestation/keygen.js +12 -0
  20. package/dist/attestation/keygen.js.map +1 -0
  21. package/dist/attestation/signer.d.ts +25 -0
  22. package/dist/attestation/signer.d.ts.map +1 -0
  23. package/dist/attestation/signer.js +93 -0
  24. package/dist/attestation/signer.js.map +1 -0
  25. package/dist/attestation/signer.test.d.ts +2 -0
  26. package/dist/attestation/signer.test.d.ts.map +1 -0
  27. package/dist/attestation/signer.test.js +62 -0
  28. package/dist/attestation/signer.test.js.map +1 -0
  29. package/dist/attestation/verify.d.ts +26 -0
  30. package/dist/attestation/verify.d.ts.map +1 -0
  31. package/dist/attestation/verify.js +104 -0
  32. package/dist/attestation/verify.js.map +1 -0
  33. package/dist/attestation/verify.test.d.ts +2 -0
  34. package/dist/attestation/verify.test.d.ts.map +1 -0
  35. package/dist/attestation/verify.test.js +79 -0
  36. package/dist/attestation/verify.test.js.map +1 -0
  37. package/dist/cli.d.ts +3 -0
  38. package/dist/cli.d.ts.map +1 -0
  39. package/dist/cli.js +364 -0
  40. package/dist/cli.js.map +1 -0
  41. package/dist/index.d.ts +12 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +12 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/integration.test.d.ts +2 -0
  46. package/dist/integration.test.d.ts.map +1 -0
  47. package/dist/integration.test.js +88 -0
  48. package/dist/integration.test.js.map +1 -0
  49. package/dist/policy/engine.d.ts +24 -0
  50. package/dist/policy/engine.d.ts.map +1 -0
  51. package/dist/policy/engine.js +130 -0
  52. package/dist/policy/engine.js.map +1 -0
  53. package/dist/policy/engine.test.d.ts +2 -0
  54. package/dist/policy/engine.test.d.ts.map +1 -0
  55. package/dist/policy/engine.test.js +80 -0
  56. package/dist/policy/engine.test.js.map +1 -0
  57. package/dist/proxy/gateway.d.ts +54 -0
  58. package/dist/proxy/gateway.d.ts.map +1 -0
  59. package/dist/proxy/gateway.js +245 -0
  60. package/dist/proxy/gateway.js.map +1 -0
  61. package/dist/proxy/gateway.test.d.ts +2 -0
  62. package/dist/proxy/gateway.test.d.ts.map +1 -0
  63. package/dist/proxy/gateway.test.js +93 -0
  64. package/dist/proxy/gateway.test.js.map +1 -0
  65. package/dist/proxy/mcp-server-adapter.d.ts +21 -0
  66. package/dist/proxy/mcp-server-adapter.d.ts.map +1 -0
  67. package/dist/proxy/mcp-server-adapter.js +98 -0
  68. package/dist/proxy/mcp-server-adapter.js.map +1 -0
  69. package/dist/proxy/mcp-server-adapter.test.d.ts +2 -0
  70. package/dist/proxy/mcp-server-adapter.test.d.ts.map +1 -0
  71. package/dist/proxy/mcp-server-adapter.test.js +160 -0
  72. package/dist/proxy/mcp-server-adapter.test.js.map +1 -0
  73. package/dist/proxy/upstream-manager.d.ts +49 -0
  74. package/dist/proxy/upstream-manager.d.ts.map +1 -0
  75. package/dist/proxy/upstream-manager.js +246 -0
  76. package/dist/proxy/upstream-manager.js.map +1 -0
  77. package/dist/proxy/upstream-manager.test.d.ts +2 -0
  78. package/dist/proxy/upstream-manager.test.d.ts.map +1 -0
  79. package/dist/proxy/upstream-manager.test.js +413 -0
  80. package/dist/proxy/upstream-manager.test.js.map +1 -0
  81. package/dist/telemetry/logger.d.ts +26 -0
  82. package/dist/telemetry/logger.d.ts.map +1 -0
  83. package/dist/telemetry/logger.js +48 -0
  84. package/dist/telemetry/logger.js.map +1 -0
  85. package/dist/telemetry/logger.test.d.ts +2 -0
  86. package/dist/telemetry/logger.test.d.ts.map +1 -0
  87. package/dist/telemetry/logger.test.js +124 -0
  88. package/dist/telemetry/logger.test.js.map +1 -0
  89. package/dist/telemetry/metrics.d.ts +41 -0
  90. package/dist/telemetry/metrics.d.ts.map +1 -0
  91. package/dist/telemetry/metrics.js +64 -0
  92. package/dist/telemetry/metrics.js.map +1 -0
  93. package/dist/telemetry/metrics.test.d.ts +2 -0
  94. package/dist/telemetry/metrics.test.d.ts.map +1 -0
  95. package/dist/telemetry/metrics.test.js +93 -0
  96. package/dist/telemetry/metrics.test.js.map +1 -0
  97. package/dist/telemetry/tracer.d.ts +16 -0
  98. package/dist/telemetry/tracer.d.ts.map +1 -0
  99. package/dist/telemetry/tracer.js +71 -0
  100. package/dist/telemetry/tracer.js.map +1 -0
  101. package/dist/types.d.ts +477 -0
  102. package/dist/types.d.ts.map +1 -0
  103. package/dist/types.js +68 -0
  104. package/dist/types.js.map +1 -0
  105. package/dist/wrap/proxy.d.ts +2 -0
  106. package/dist/wrap/proxy.d.ts.map +1 -0
  107. package/dist/wrap/proxy.js +152 -0
  108. package/dist/wrap/proxy.js.map +1 -0
  109. package/dist/wrap/proxy.test.d.ts +2 -0
  110. package/dist/wrap/proxy.test.d.ts.map +1 -0
  111. package/dist/wrap/proxy.test.js +155 -0
  112. package/dist/wrap/proxy.test.js.map +1 -0
  113. package/examples/README.md +33 -0
  114. package/examples/claude-desktop.json +50 -0
  115. package/examples/multi-agent-production.json +72 -0
  116. package/gateway.config.example.json +61 -0
  117. package/package.json +49 -0
  118. package/src/attestation/audit-log.ts +126 -0
  119. package/src/attestation/bugfix.test.ts +113 -0
  120. package/src/attestation/chain.test.ts +188 -0
  121. package/src/attestation/keygen.ts +18 -0
  122. package/src/attestation/signer.test.ts +75 -0
  123. package/src/attestation/signer.ts +105 -0
  124. package/src/attestation/verify.test.ts +86 -0
  125. package/src/attestation/verify.ts +131 -0
  126. package/src/cli.ts +407 -0
  127. package/src/index.ts +21 -0
  128. package/src/integration.test.ts +108 -0
  129. package/src/policy/engine.test.ts +95 -0
  130. package/src/policy/engine.ts +162 -0
  131. package/src/proxy/gateway.test.ts +108 -0
  132. package/src/proxy/gateway.ts +328 -0
  133. package/src/proxy/mcp-server-adapter.test.ts +197 -0
  134. package/src/proxy/mcp-server-adapter.ts +140 -0
  135. package/src/proxy/upstream-manager.test.ts +534 -0
  136. package/src/proxy/upstream-manager.ts +326 -0
  137. package/src/telemetry/logger.test.ts +139 -0
  138. package/src/telemetry/logger.ts +72 -0
  139. package/src/telemetry/metrics.test.ts +125 -0
  140. package/src/telemetry/metrics.ts +83 -0
  141. package/src/telemetry/tracer.ts +90 -0
  142. package/src/types.ts +113 -0
  143. package/src/wrap/proxy.test.ts +175 -0
  144. package/src/wrap/proxy.ts +192 -0
  145. package/tsconfig.json +18 -0
@@ -0,0 +1,21 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ release:
5
+ types: [created]
6
+
7
+ jobs:
8
+ build-and-publish:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
+ with:
14
+ node-version: 20
15
+ registry-url: https://registry.npmjs.org/
16
+ - run: npm ci
17
+ - run: npm run build
18
+ - run: npm test
19
+ - run: npm publish --access public
20
+ env:
21
+ NODE_AUTH_TOKEN: ${{secrets.npm_token}}
@@ -0,0 +1,41 @@
1
+ # Contributing
2
+
3
+ Contributions welcome. Here's how to get going.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ git clone <repo-url> && cd mcp-audit-gateway
9
+ npm install
10
+ npm run build
11
+ npm test
12
+ ```
13
+
14
+ Requires Node.js 20+.
15
+
16
+ ## Running tests
17
+
18
+ ```bash
19
+ npm test # all tests
20
+ npx vitest run src/attestation/ # just attestation tests
21
+ ```
22
+
23
+ Tests use Vitest and run against real filesystem paths (no mocks for the audit log). Temp files go in `/tmp` and are cleaned up in `afterEach`.
24
+
25
+ ## Code style
26
+
27
+ TypeScript strict mode. No `any` unless interfacing with the MCP SDK's dynamic types. Keep functions short and avoid class inheritance.
28
+
29
+ ## Pull requests
30
+
31
+ One feature or fix per PR. Include a test that fails without the fix and passes with it.
32
+
33
+ If you're changing the attestation or signing logic, run the full verification test suite and include `verify` CLI output in the PR description showing the audit log validates end-to-end.
34
+
35
+ ## Architecture decisions
36
+
37
+ The gateway is a transparent proxy. It should never modify tool call arguments or results. Its job is to observe, sign, and enforce policy. If a feature requires mutating upstream data, it belongs in a different layer.
38
+
39
+ ## Security
40
+
41
+ If you find a vulnerability in the signing, chain verification, or policy enforcement, please report it privately before opening a public issue.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Elankumaran Srinivasan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # mcp-audit
2
+
3
+ Your AI agent made 847 tool calls yesterday. Can you verify what it did?
4
+
5
+ Tamper-evident audit trail for AI agent tool calls.
6
+
7
+ ## Setup (10 seconds)
8
+
9
+ Before:
10
+ ```json
11
+ {
12
+ "command": "npx",
13
+ "args": ["@modelcontextprotocol/server-github"]
14
+ }
15
+ ```
16
+
17
+ After:
18
+ ```json
19
+ {
20
+ "command": "npx",
21
+ "args": ["mcp-audit", "wrap", "--", "npx", "@modelcontextprotocol/server-github"]
22
+ }
23
+ ```
24
+
25
+ Every tool call is now cryptographically signed and hash-chained. Nothing else changes. The MCP server works exactly as before.
26
+
27
+ ## What it does
28
+
29
+ ```
30
+ $ mcp-audit tail
31
+
32
+ ✓ 14:32:01 github/create_pr 234ms bf7a2f62
33
+ ✓ 14:32:03 github/list_issues 89ms a1c4e890
34
+ ✗ 14:32:05 fs/delete_file 12ms c3d9f012
35
+ ✓ 14:32:08 github/merge_pr 456ms e5f6a7b8
36
+ ```
37
+
38
+ Every entry is signed with HMAC-SHA256 and chained to the previous record. Tamper with any entry and verification fails. Delete an entry and the chain breaks.
39
+
40
+ ## Verify integrity
41
+
42
+ ```bash
43
+ $ mcp-audit verify ~/.mcp-audit/audit.jsonl
44
+
45
+ Results:
46
+ Total records: 847
47
+ Valid: 847
48
+ Invalid: 0
49
+
50
+ All records verified successfully.
51
+ ```
52
+
53
+ ## How it works
54
+
55
+ ```
56
+ ┌────────────┐ ┌───────────┐ ┌────────────┐
57
+ │ MCP Client │──────▶│ mcp-audit │──────▶│ MCP Server │
58
+ │ (Claude, │◀──────│ wrap │◀──────│ (any) │
59
+ │ Cursor) │ └─────┬─────┘ └────────────┘
60
+ └────────────┘ │
61
+
62
+ ~/.mcp-audit/
63
+ audit.jsonl
64
+ ```
65
+
66
+ The wrap command spawns your MCP server as a child process and sits between the client and server on stdio. It forwards ALL messages transparently. Only `tools/call` responses get signed and logged. Everything else passes through untouched.
67
+
68
+ On first run, a signing key is auto-generated in `~/.mcp-audit/key.hex`. No configuration needed.
69
+
70
+ ## Audit record format
71
+
72
+ ```json
73
+ {
74
+ "id": "bf7a2f62-4d0f-4cce-afd2-cbfbf7bca2a5",
75
+ "timestamp": "2026-08-16T14:32:01.000Z",
76
+ "method": "tools/call",
77
+ "toolName": "github/create_pr",
78
+ "args": {"title": "Fix bug", "body": "..."},
79
+ "durationMs": 234,
80
+ "success": true,
81
+ "previousHash": "8a3f2b...",
82
+ "attestation": "7c4d9e..."
83
+ }
84
+ ```
85
+
86
+ The `attestation` is an HMAC-SHA256 signature over the record's canonical fields. The `previousHash` is SHA-256 of the preceding record. Together they detect tampering, ordering, and completeness.
87
+
88
+ ## Use with Claude Desktop
89
+
90
+ `claude_desktop_config.json`:
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "github": {
95
+ "command": "npx",
96
+ "args": ["mcp-audit", "wrap", "--", "npx", "@modelcontextprotocol/server-github"]
97
+ },
98
+ "filesystem": {
99
+ "command": "npx",
100
+ "args": ["mcp-audit", "wrap", "--", "npx", "@modelcontextprotocol/server-filesystem", "/tmp"]
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ## Use with Claude Code
107
+
108
+ `.claude/hooks/mcp-servers.json` or directly in your MCP server command — prefix with `mcp-audit wrap --`.
109
+
110
+ ## CLI
111
+
112
+ ```bash
113
+ mcp-audit wrap -- <cmd> [args] # Wrap any MCP server
114
+ mcp-audit tail # Live stream of tool calls
115
+ mcp-audit verify <log> # Verify chain integrity
116
+ mcp-audit serve [config] # Full gateway (policy + OTel)
117
+ mcp-audit keygen [dir] # Generate Ed25519 key pair
118
+ ```
119
+
120
+ ## Full gateway mode
121
+
122
+ For teams that also need access control, rate limiting, and multi-server routing:
123
+
124
+ ```bash
125
+ mcp-audit serve gateway.config.json
126
+ ```
127
+
128
+ The full gateway adds:
129
+ - Policy engine (glob-based ACLs, per-principal rate limits)
130
+ - Tool namespacing across multiple upstream MCP servers
131
+ - OpenTelemetry traces and metrics export
132
+ - Upstream health management with automatic reconnection
133
+ - Ed25519 signatures (stronger than HMAC, portable verification)
134
+
135
+ See [gateway configuration](gateway.config.example.json) for the full schema.
136
+
137
+ ## Install
138
+
139
+ ```bash
140
+ npm install -g @mcp-gateway/audit
141
+ ```
142
+
143
+ This installs the `mcp-audit` CLI globally. Or use without installing:
144
+ ```bash
145
+ npx @mcp-gateway/audit wrap -- <your mcp server command>
146
+ ```
147
+
148
+ ## Testing
149
+
150
+ ```bash
151
+ npm test # 96 tests across 12 test files
152
+ ```
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,25 @@
1
+ import type { AuditRecord } from "../types.js";
2
+ import type { Signer } from "./signer.js";
3
+ export declare function hashRecord(record: AuditRecord): string;
4
+ export declare class AuditLog {
5
+ private path;
6
+ private signer;
7
+ private rotateAfterBytes;
8
+ private currentSize;
9
+ private lastHash;
10
+ private writeQueue;
11
+ constructor(path: string, signer: Signer, rotateAfterBytes: number);
12
+ record(method: string, opts: {
13
+ toolName?: string;
14
+ namespace?: string;
15
+ upstream?: string;
16
+ principal?: string;
17
+ durationMs: number;
18
+ success: boolean;
19
+ errorCode?: number;
20
+ }): Promise<AuditRecord>;
21
+ private writeRecord;
22
+ private rotate;
23
+ init(): Promise<void>;
24
+ }
25
+ //# sourceMappingURL=audit-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-log.d.ts","sourceRoot":"","sources":["../../src/attestation/audit-log.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAGtD;AAED,qBAAa,QAAQ;IAMjB,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,gBAAgB;IAP1B,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,UAAU,CAAoC;gBAG5C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM;IAG5B,MAAM,CACV,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;QACJ,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,OAAO,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GACA,OAAO,CAAC,WAAW,CAAC;YAaT,WAAW;YAmCX,MAAM;IAUd,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAmC5B"}
@@ -0,0 +1,95 @@
1
+ import { appendFile, stat, rename, open } from "node:fs/promises";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ export function hashRecord(record) {
4
+ const json = JSON.stringify(record);
5
+ return createHash("sha256").update(json).digest("hex");
6
+ }
7
+ export class AuditLog {
8
+ path;
9
+ signer;
10
+ rotateAfterBytes;
11
+ currentSize = 0;
12
+ lastHash = "genesis";
13
+ writeQueue = Promise.resolve();
14
+ constructor(path, signer, rotateAfterBytes) {
15
+ this.path = path;
16
+ this.signer = signer;
17
+ this.rotateAfterBytes = rotateAfterBytes;
18
+ }
19
+ async record(method, opts) {
20
+ return new Promise((resolve, reject) => {
21
+ this.writeQueue = this.writeQueue.then(async () => {
22
+ try {
23
+ const result = await this.writeRecord(method, opts);
24
+ resolve(result);
25
+ }
26
+ catch (err) {
27
+ reject(err);
28
+ }
29
+ });
30
+ });
31
+ }
32
+ async writeRecord(method, opts) {
33
+ const record = {
34
+ id: randomUUID(),
35
+ timestamp: new Date().toISOString(),
36
+ method,
37
+ ...opts,
38
+ previousHash: this.lastHash,
39
+ };
40
+ record.attestation = await this.signer.sign(record);
41
+ const line = JSON.stringify(record) + "\n";
42
+ await appendFile(this.path, line);
43
+ this.currentSize += Buffer.byteLength(line);
44
+ this.lastHash = hashRecord(record);
45
+ if (this.currentSize >= this.rotateAfterBytes) {
46
+ await this.rotate();
47
+ }
48
+ return record;
49
+ }
50
+ async rotate() {
51
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
52
+ const rotatedPath = this.path.replace(/\.jsonl$/, `-${ts}.jsonl`);
53
+ try {
54
+ await rename(this.path, rotatedPath);
55
+ this.currentSize = 0;
56
+ this.lastHash = "genesis";
57
+ }
58
+ catch { }
59
+ }
60
+ async init() {
61
+ let s;
62
+ try {
63
+ s = await stat(this.path);
64
+ }
65
+ catch {
66
+ this.currentSize = 0;
67
+ return;
68
+ }
69
+ this.currentSize = s.size;
70
+ if (s.size > 0) {
71
+ const tailSize = Math.min(s.size, 8192);
72
+ const fh = await open(this.path, "r");
73
+ try {
74
+ const buf = Buffer.alloc(tailSize);
75
+ await fh.read(buf, 0, tailSize, s.size - tailSize);
76
+ const tail = buf.toString("utf-8");
77
+ const lines = tail.trimEnd().split("\n");
78
+ for (let i = lines.length - 1; i >= 0; i--) {
79
+ try {
80
+ const record = JSON.parse(lines[i]);
81
+ this.lastHash = hashRecord(record);
82
+ return;
83
+ }
84
+ catch {
85
+ process.stderr.write(`[audit-log] warning: corrupt record in tail, skipping\n`);
86
+ }
87
+ }
88
+ }
89
+ finally {
90
+ await fh.close();
91
+ }
92
+ }
93
+ }
94
+ }
95
+ //# sourceMappingURL=audit-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-log.js","sourceRoot":"","sources":["../../src/attestation/audit-log.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=bugfix.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bugfix.test.d.ts","sourceRoot":"","sources":["../../src/attestation/bugfix.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { unlink, readFile } from "node:fs/promises";
3
+ import { HmacSigner } from "./signer.js";
4
+ import { AuditLog, hashRecord } from "./audit-log.js";
5
+ import { verifyAuditLog } from "./verify.js";
6
+ const TEST_LOG = "/tmp/bugfix-audit.jsonl";
7
+ const SECRET = "d".repeat(64);
8
+ describe("Bug fix: pipe injection in canonicalization", () => {
9
+ const signer = new HmacSigner(SECRET);
10
+ it("different records with pipe chars produce different signatures", async () => {
11
+ const record1 = {
12
+ id: "uuid-1",
13
+ timestamp: "2026-08-16T10:00:00Z",
14
+ method: "tools/call",
15
+ toolName: "db/read|admin",
16
+ namespace: "db",
17
+ upstream: "evil",
18
+ principal: "alice",
19
+ durationMs: 50,
20
+ success: true,
21
+ };
22
+ const record2 = {
23
+ id: "uuid-1",
24
+ timestamp: "2026-08-16T10:00:00Z",
25
+ method: "tools/call",
26
+ toolName: "db/read",
27
+ namespace: "admin",
28
+ upstream: "db",
29
+ principal: "evil|alice",
30
+ durationMs: 50,
31
+ success: true,
32
+ };
33
+ const sig1 = await signer.sign(record1);
34
+ const sig2 = await signer.sign(record2);
35
+ expect(sig1).not.toBe(sig2);
36
+ });
37
+ });
38
+ describe("Bug fix: hash chain survives restart", () => {
39
+ const signer = new HmacSigner(SECRET);
40
+ beforeEach(async () => { try {
41
+ await unlink(TEST_LOG);
42
+ }
43
+ catch { } });
44
+ afterEach(async () => { try {
45
+ await unlink(TEST_LOG);
46
+ }
47
+ catch { } });
48
+ it("restores lastHash from existing log on init", async () => {
49
+ const log1 = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
50
+ await log1.init();
51
+ await log1.record("tools/call", { toolName: "test/a", durationMs: 10, success: true });
52
+ await log1.record("tools/call", { toolName: "test/b", durationMs: 10, success: true });
53
+ const log2 = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
54
+ await log2.init();
55
+ await log2.record("tools/call", { toolName: "test/c", durationMs: 10, success: true });
56
+ const result = await verifyAuditLog(TEST_LOG, signer, { verifyChain: true });
57
+ expect(result.total).toBe(3);
58
+ expect(result.valid).toBe(3);
59
+ expect(result.invalid).toBe(0);
60
+ });
61
+ });
62
+ describe("Bug fix: concurrent writes maintain chain integrity", () => {
63
+ const signer = new HmacSigner(SECRET);
64
+ beforeEach(async () => { try {
65
+ await unlink(TEST_LOG);
66
+ }
67
+ catch { } });
68
+ afterEach(async () => { try {
69
+ await unlink(TEST_LOG);
70
+ }
71
+ catch { } });
72
+ it("serializes concurrent writes correctly", async () => {
73
+ const log = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
74
+ await log.init();
75
+ await log.record("tools/call", { toolName: "test/setup", durationMs: 10, success: true });
76
+ const concurrent = Array.from({ length: 10 }, (_, i) => log.record("tools/call", { toolName: `test/tool_${i}`, durationMs: i, success: true }));
77
+ await Promise.all(concurrent);
78
+ const content = await readFile(TEST_LOG, "utf-8");
79
+ const records = content.trimEnd().split("\n").map((l) => JSON.parse(l));
80
+ expect(records).toHaveLength(11);
81
+ const previousHashes = records.map((r) => r.previousHash);
82
+ const unique = new Set(previousHashes);
83
+ expect(unique.size).toBe(11);
84
+ for (let i = 1; i < records.length; i++) {
85
+ const expected = hashRecord(records[i - 1]);
86
+ expect(records[i].previousHash).toBe(expected);
87
+ }
88
+ });
89
+ });
90
+ describe("Bug fix: glob matcher prefix/suffix overlap", () => {
91
+ it("does not match when prefix and suffix overlap", async () => {
92
+ const { PolicyEngine } = await import("../policy/engine.js");
93
+ const engine = new PolicyEngine("allow", []);
94
+ const match = engine.globMatch.bind(engine);
95
+ expect(match("ab*ab", "ab")).toBe(false);
96
+ expect(match("admin*admin", "admin")).toBe(false);
97
+ expect(match("ab*ab", "abab")).toBe(true);
98
+ expect(match("ab*ab", "abXab")).toBe(true);
99
+ expect(match("*/write_*", "fs/write_file")).toBe(true);
100
+ expect(match("agent:readonly-*", "agent:readonly-bot")).toBe(true);
101
+ });
102
+ });
103
+ //# sourceMappingURL=bugfix.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bugfix.test.js","sourceRoot":"","sources":["../../src/attestation/bugfix.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=chain.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain.test.d.ts","sourceRoot":"","sources":["../../src/attestation/chain.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { unlink, readFile } from "node:fs/promises";
3
+ import { HmacSigner } from "./signer.js";
4
+ import { AuditLog, hashRecord } from "./audit-log.js";
5
+ import { verifyChain } from "./verify.js";
6
+ const TEST_LOG = "/tmp/test-chain-audit.jsonl";
7
+ const SECRET = "c".repeat(64);
8
+ async function readRecords(path) {
9
+ const content = await readFile(path, "utf-8");
10
+ return content
11
+ .split("\n")
12
+ .filter((line) => line.trim().length > 0)
13
+ .map((line) => JSON.parse(line));
14
+ }
15
+ describe("hash chain integrity", () => {
16
+ const signer = new HmacSigner(SECRET);
17
+ beforeEach(async () => {
18
+ try {
19
+ await unlink(TEST_LOG);
20
+ }
21
+ catch { }
22
+ });
23
+ afterEach(async () => {
24
+ try {
25
+ await unlink(TEST_LOG);
26
+ }
27
+ catch { }
28
+ });
29
+ it("valid chain passes verification", async () => {
30
+ const log = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
31
+ await log.init();
32
+ await log.record("tools/call", {
33
+ toolName: "test/tool_a",
34
+ namespace: "test",
35
+ upstream: "test-server",
36
+ principal: "agent:bot",
37
+ durationMs: 100,
38
+ success: true,
39
+ });
40
+ await log.record("tools/call", {
41
+ toolName: "test/tool_b",
42
+ namespace: "test",
43
+ upstream: "test-server",
44
+ principal: "agent:bot",
45
+ durationMs: 50,
46
+ success: true,
47
+ });
48
+ await log.record("tools/call", {
49
+ toolName: "test/tool_c",
50
+ namespace: "test",
51
+ upstream: "test-server",
52
+ principal: "agent:bot",
53
+ durationMs: 75,
54
+ success: true,
55
+ });
56
+ const records = await readRecords(TEST_LOG);
57
+ const result = await verifyChain(records);
58
+ expect(result.valid).toBe(true);
59
+ expect(result.total).toBe(3);
60
+ expect(result.errors).toHaveLength(0);
61
+ // Verify first record has genesis
62
+ expect(records[0].previousHash).toBe("genesis");
63
+ // Verify second record references hash of first
64
+ expect(records[1].previousHash).toBe(hashRecord(records[0]));
65
+ // Verify third record references hash of second
66
+ expect(records[2].previousHash).toBe(hashRecord(records[1]));
67
+ });
68
+ it("detects deleted record", async () => {
69
+ const log = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
70
+ await log.init();
71
+ await log.record("tools/call", {
72
+ toolName: "test/tool_a",
73
+ durationMs: 100,
74
+ success: true,
75
+ });
76
+ await log.record("tools/call", {
77
+ toolName: "test/tool_b",
78
+ durationMs: 50,
79
+ success: true,
80
+ });
81
+ await log.record("tools/call", {
82
+ toolName: "test/tool_c",
83
+ durationMs: 75,
84
+ success: true,
85
+ });
86
+ const records = await readRecords(TEST_LOG);
87
+ // Remove the middle record
88
+ const tampered = [records[0], records[2]];
89
+ const result = await verifyChain(tampered);
90
+ expect(result.valid).toBe(false);
91
+ expect(result.errors.length).toBeGreaterThan(0);
92
+ expect(result.errors[0].reason).toBe("previousHash does not match hash of prior record");
93
+ });
94
+ it("detects reordered records", async () => {
95
+ const log = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
96
+ await log.init();
97
+ await log.record("tools/call", {
98
+ toolName: "test/tool_a",
99
+ durationMs: 100,
100
+ success: true,
101
+ });
102
+ await log.record("tools/call", {
103
+ toolName: "test/tool_b",
104
+ durationMs: 50,
105
+ success: true,
106
+ });
107
+ await log.record("tools/call", {
108
+ toolName: "test/tool_c",
109
+ durationMs: 75,
110
+ success: true,
111
+ });
112
+ const records = await readRecords(TEST_LOG);
113
+ // Swap the second and third records
114
+ const reordered = [records[0], records[2], records[1]];
115
+ const result = await verifyChain(reordered);
116
+ expect(result.valid).toBe(false);
117
+ expect(result.errors.length).toBeGreaterThan(0);
118
+ expect(result.errors[0].reason).toBe("previousHash does not match hash of prior record");
119
+ });
120
+ it("detects inserted record (previousHash won't match)", async () => {
121
+ const log = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
122
+ await log.init();
123
+ await log.record("tools/call", {
124
+ toolName: "test/tool_a",
125
+ durationMs: 100,
126
+ success: true,
127
+ });
128
+ await log.record("tools/call", {
129
+ toolName: "test/tool_b",
130
+ durationMs: 50,
131
+ success: true,
132
+ });
133
+ const records = await readRecords(TEST_LOG);
134
+ // Insert a fake record between the two
135
+ const inserted = {
136
+ id: "fake-inserted-id",
137
+ timestamp: new Date().toISOString(),
138
+ method: "tools/call",
139
+ toolName: "test/malicious_tool",
140
+ durationMs: 1,
141
+ success: true,
142
+ previousHash: hashRecord(records[0]),
143
+ };
144
+ const tampered = [records[0], inserted, records[1]];
145
+ const result = await verifyChain(tampered);
146
+ expect(result.valid).toBe(false);
147
+ // The record after the inserted one will fail because its previousHash
148
+ // was computed against the original record[0], not the inserted record
149
+ expect(result.errors.length).toBeGreaterThan(0);
150
+ expect(result.errors.some((e) => e.reason === "previousHash does not match hash of prior record")).toBe(true);
151
+ });
152
+ });
153
+ //# sourceMappingURL=chain.test.js.map