@mcpose/audit 2.0.2 → 3.0.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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +437 -28
  3. package/dist/chain.d.ts +55 -0
  4. package/dist/chain.d.ts.map +1 -0
  5. package/dist/chain.js +265 -0
  6. package/dist/chain.js.map +1 -0
  7. package/dist/index.d.ts +9 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +7 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/middleware.d.ts +5 -0
  12. package/dist/middleware.d.ts.map +1 -0
  13. package/dist/middleware.js +488 -0
  14. package/dist/middleware.js.map +1 -0
  15. package/dist/sensitivity.d.ts +12 -0
  16. package/dist/sensitivity.d.ts.map +1 -0
  17. package/dist/sensitivity.js +24 -0
  18. package/dist/sensitivity.js.map +1 -0
  19. package/dist/signingKey.d.ts +11 -0
  20. package/dist/signingKey.d.ts.map +1 -0
  21. package/dist/signingKey.js +30 -0
  22. package/dist/signingKey.js.map +1 -0
  23. package/dist/subjectKeyStore.d.ts +17 -0
  24. package/dist/subjectKeyStore.d.ts.map +1 -0
  25. package/dist/subjectKeyStore.js +37 -0
  26. package/dist/subjectKeyStore.js.map +1 -0
  27. package/dist/types.d.ts +244 -0
  28. package/dist/types.d.ts.map +1 -0
  29. package/dist/types.js +2 -0
  30. package/dist/types.js.map +1 -0
  31. package/dist/verify.d.ts +24 -0
  32. package/dist/verify.d.ts.map +1 -0
  33. package/dist/verify.js +50 -0
  34. package/dist/verify.js.map +1 -0
  35. package/package.json +43 -14
  36. package/src/__tests__/chain.batch.test.ts +30 -0
  37. package/src/__tests__/chain.test.ts +399 -0
  38. package/src/__tests__/erasable.test.ts +499 -0
  39. package/src/__tests__/merkle-batch.middleware.test.ts +96 -0
  40. package/src/__tests__/middleware.http.test.ts +172 -0
  41. package/src/__tests__/middleware.robustness.test.ts +737 -0
  42. package/src/__tests__/middleware.test.ts +1121 -0
  43. package/src/__tests__/properties.test.ts +737 -0
  44. package/src/__tests__/sensitivity.test.ts +72 -0
  45. package/src/__tests__/signingKey.test.ts +76 -0
  46. package/src/chain.ts +317 -0
  47. package/src/index.ts +31 -0
  48. package/src/middleware.ts +690 -0
  49. package/src/sensitivity.ts +33 -0
  50. package/src/signingKey.ts +35 -0
  51. package/src/subjectKeyStore.ts +38 -0
  52. package/src/types.ts +281 -0
  53. package/src/verify.ts +76 -0
@@ -0,0 +1,172 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type * as http from 'node:http';
3
+ import { startHttpProxy } from 'mcpose';
4
+ import type { LocalTool } from 'mcpose';
5
+ import { createMockBackendClient } from 'mcpose/testing';
6
+ import { createAuditMiddleware } from '../middleware.js';
7
+ import { createDefaultSigningKeyProvider } from '../signingKey.js';
8
+ import { verifyAuditChain } from '../verify.js';
9
+ import type { AuditEvent, ReplayManifest } from '../types.js';
10
+
11
+ /**
12
+ * End-to-end regression for #168 over real loopback HTTP: a session torn
13
+ * down (client DELETE or TTL expiry) while a tool call is still inside its
14
+ * handler must still seal that call into the manifest, after any events
15
+ * the session already recorded.
16
+ */
17
+
18
+ const MCP_HEADERS = {
19
+ 'Content-Type': 'application/json',
20
+ Accept: 'application/json, text/event-stream',
21
+ };
22
+
23
+ function deferred<T = void>() {
24
+ let resolve!: (v: T) => void;
25
+ const promise = new Promise<T>((r) => (resolve = r));
26
+ return { promise, resolve };
27
+ }
28
+
29
+ function rpc(id: number, method: string, params: unknown): string {
30
+ return JSON.stringify({ jsonrpc: '2.0', id, method, params });
31
+ }
32
+
33
+ function port(server: http.Server): number {
34
+ const addr = server.address();
35
+ if (!addr || typeof addr === 'string') throw new Error('no port');
36
+ return addr.port;
37
+ }
38
+
39
+ async function harness(sessionTtlMs: number) {
40
+ const events: AuditEvent[] = [];
41
+ const manifests: Promise<ReplayManifest | undefined>[] = [];
42
+ const closed = deferred<string>();
43
+ const entered = deferred();
44
+ const release = deferred();
45
+ const audit = createAuditMiddleware({
46
+ signingKey: createDefaultSigningKeyProvider('test-secret'),
47
+ sensitivityResolver: () => 'low',
48
+ onEvent: (e) => void events.push(e),
49
+ });
50
+ const slow: LocalTool = {
51
+ tool: { name: 'slow', inputSchema: { type: 'object' } },
52
+ handler: async (params) => {
53
+ if (params.arguments?.['block'] === true) {
54
+ entered.resolve();
55
+ await release.promise;
56
+ }
57
+ return { content: [] };
58
+ },
59
+ };
60
+ const server = await startHttpProxy(
61
+ createMockBackendClient(),
62
+ {
63
+ name: 'audit-http-test',
64
+ localTools: [slow],
65
+ toolMiddleware: [audit.middleware],
66
+ },
67
+ {
68
+ port: 0,
69
+ path: '/mcp',
70
+ sessionTtlMs,
71
+ onSessionClosed: (id) => {
72
+ manifests.push(audit.closeSession(id));
73
+ closed.resolve(id);
74
+ },
75
+ },
76
+ );
77
+ const baseUrl = `http://localhost:${port(server)}/mcp`;
78
+ const init = await fetch(baseUrl, {
79
+ method: 'POST',
80
+ headers: MCP_HEADERS,
81
+ body: rpc(1, 'initialize', {
82
+ protocolVersion: '2024-11-05',
83
+ capabilities: {},
84
+ clientInfo: { name: 'test', version: '0.0.1' },
85
+ }),
86
+ });
87
+ await init.text();
88
+ const sessionId = init.headers.get('mcp-session-id')!;
89
+ expect(sessionId).toBeTruthy();
90
+ const headers = { ...MCP_HEADERS, 'mcp-session-id': sessionId };
91
+ const call = (id: number, block: boolean) =>
92
+ fetch(baseUrl, {
93
+ method: 'POST',
94
+ headers,
95
+ body: rpc(id, 'tools/call', { name: 'slow', arguments: { block } }),
96
+ })
97
+ .then((r) => r.text())
98
+ .catch(() => undefined);
99
+
100
+ return {
101
+ events,
102
+ manifests,
103
+ closed,
104
+ entered,
105
+ release,
106
+ sessionId,
107
+ headers,
108
+ baseUrl,
109
+ call,
110
+ close: () => new Promise<void>((r) => server.close(() => r())),
111
+ };
112
+ }
113
+
114
+ async function expectSealed(h: Awaited<ReturnType<typeof harness>>) {
115
+ expect(h.manifests).toHaveLength(1);
116
+ const manifest = await h.manifests[0];
117
+ expect(manifest?.sessionId).toBe(h.sessionId);
118
+ expect(manifest?.eventCount).toBe(2);
119
+ expect(h.events.map((e) => e.replayManifestPosition)).toEqual([0, 1]);
120
+ expect(h.events.every((e) => e.sessionId === h.sessionId)).toBe(true);
121
+ expect(
122
+ await verifyAuditChain(
123
+ h.events,
124
+ createDefaultSigningKeyProvider('test-secret'),
125
+ ),
126
+ ).toEqual({ valid: true });
127
+ }
128
+
129
+ describe('audit middleware over HTTP — teardown during a blocked call (#168)', () => {
130
+ it('client DELETE during a blocked call still seals it into the manifest', async () => {
131
+ const h = await harness(60_000);
132
+ try {
133
+ await h.call(2, false);
134
+ expect(h.events).toHaveLength(1);
135
+
136
+ const blocked = h.call(3, true);
137
+ await h.entered.promise;
138
+
139
+ const del = await fetch(h.baseUrl, {
140
+ method: 'DELETE',
141
+ headers: h.headers,
142
+ });
143
+ expect(del.status).toBeLessThan(500);
144
+ expect(await h.closed.promise).toBe(h.sessionId);
145
+
146
+ h.release.resolve();
147
+ await blocked;
148
+ await expectSealed(h);
149
+ } finally {
150
+ h.release.resolve();
151
+ await h.close();
152
+ }
153
+ });
154
+
155
+ it('TTL expiry during a blocked call still seals it into the manifest', async () => {
156
+ const h = await harness(150);
157
+ try {
158
+ await h.call(2, false);
159
+ const blocked = h.call(3, true);
160
+ await h.entered.promise;
161
+
162
+ expect(await h.closed.promise).toBe(h.sessionId);
163
+
164
+ h.release.resolve();
165
+ await blocked;
166
+ await expectSealed(h);
167
+ } finally {
168
+ h.release.resolve();
169
+ await h.close();
170
+ }
171
+ });
172
+ });