@mcpose/audit 2.0.3 → 3.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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +440 -28
  3. package/dist/chain.d.ts +49 -6
  4. package/dist/chain.d.ts.map +1 -1
  5. package/dist/chain.js +227 -28
  6. package/dist/chain.js.map +1 -1
  7. package/dist/index.d.ts +5 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/middleware.d.ts +3 -1
  12. package/dist/middleware.d.ts.map +1 -1
  13. package/dist/middleware.js +499 -85
  14. package/dist/middleware.js.map +1 -1
  15. package/dist/sensitivity.d.ts +11 -2
  16. package/dist/sensitivity.d.ts.map +1 -1
  17. package/dist/sensitivity.js +18 -2
  18. package/dist/sensitivity.js.map +1 -1
  19. package/dist/signingKey.d.ts +8 -0
  20. package/dist/signingKey.d.ts.map +1 -1
  21. package/dist/signingKey.js +21 -4
  22. package/dist/signingKey.js.map +1 -1
  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 +170 -18
  28. package/dist/types.d.ts.map +1 -1
  29. package/dist/verify.d.ts +24 -0
  30. package/dist/verify.d.ts.map +1 -0
  31. package/dist/verify.js +66 -0
  32. package/dist/verify.js.map +1 -0
  33. package/package.json +43 -15
  34. package/src/__tests__/chain.batch.test.ts +30 -0
  35. package/src/__tests__/chain.test.ts +433 -0
  36. package/src/__tests__/erasable.test.ts +499 -0
  37. package/src/__tests__/merkle-batch.middleware.test.ts +96 -0
  38. package/src/__tests__/middleware.http.test.ts +172 -0
  39. package/src/__tests__/middleware.rejection.test.ts +183 -0
  40. package/src/__tests__/middleware.robustness.test.ts +809 -0
  41. package/src/__tests__/middleware.test.ts +1121 -0
  42. package/src/__tests__/middleware.unusableResult.test.ts +141 -0
  43. package/src/__tests__/properties.test.ts +764 -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 +781 -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 +287 -0
  53. package/src/verify.ts +92 -0
@@ -0,0 +1,141 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createAuditMiddleware } from '../middleware.js';
3
+ import { createDefaultSigningKeyProvider } from '../signingKey.js';
4
+ import { createSensitivityResolver } from '../sensitivity.js';
5
+ import { verifyAuditChain, verifyManifestSignature } from '../verify.js';
6
+ import type { AuditEvent, AuditOptions } from '../types.js';
7
+ import { createProxyContext } from 'mcpose';
8
+ import type { Identity } from 'mcpose';
9
+
10
+ /**
11
+ * A call that neither throws nor produces a tool result is a failed call, and
12
+ * the trail has to say so.
13
+ *
14
+ * `hasToolContent` narrows the pipeline's result and reads `.content` on the
15
+ * way, so a nullish one used to throw inside the post-call section — whose
16
+ * only handler reports and moves on. `buildEvent` never ran, so a governed
17
+ * call that failed reached the client and never reached the trail, while the
18
+ * manifest that counted it still verified: a valid chain under a valid
19
+ * signature, short one event of the calls it claims to cover.
20
+ */
21
+
22
+ const identity: Identity = {
23
+ sub: 'user-1',
24
+ type: 'human',
25
+ roles: ['analyst'],
26
+ claims: {},
27
+ resolvedAt: '2026-06-01T00:00:00.000Z',
28
+ source: 'jwt',
29
+ };
30
+
31
+ const signingKey = createDefaultSigningKeyProvider('test-secret');
32
+
33
+ function makeCtx(sessionId: string) {
34
+ return createProxyContext({
35
+ transport: 'http',
36
+ identity,
37
+ sessionId,
38
+ proxy: { name: 'test-proxy', version: '0.0.0' },
39
+ });
40
+ }
41
+
42
+ function makeReq(tool: string) {
43
+ return {
44
+ method: 'tools/call' as const,
45
+ params: { name: tool, arguments: {} },
46
+ };
47
+ }
48
+
49
+ function makeOptions(events: AuditEvent[]): AuditOptions {
50
+ return {
51
+ signingKey,
52
+ sensitivityResolver: createSensitivityResolver({ search: 'low' }),
53
+ onEvent: (e) => void events.push(e),
54
+ };
55
+ }
56
+
57
+ describe('a pipeline that resolves without a tool result', () => {
58
+ for (const [label, value] of [
59
+ ['undefined', undefined],
60
+ ['null', null],
61
+ ['a string', 'all good'],
62
+ ['the number 0', 0],
63
+ ] as const) {
64
+ it(`records the call as an error when the pipeline resolves to ${label}`, async () => {
65
+ const events: AuditEvent[] = [];
66
+ const handle = createAuditMiddleware(makeOptions(events));
67
+
68
+ await handle.middleware(
69
+ makeReq('search'),
70
+ async () => value as never,
71
+ makeCtx(`sess-${label}`),
72
+ );
73
+
74
+ expect(events).toHaveLength(1);
75
+ const event = events[0]!;
76
+ expect(event.outcome).toBe('error');
77
+ expect(event.error).toEqual({
78
+ name: 'InvalidToolResult',
79
+ message: 'Tool call resolved to a value that is not a CallToolResult',
80
+ });
81
+ expect(event.replayManifestPosition).toBe(0);
82
+ });
83
+ }
84
+
85
+ it('names the shape a prompt fetch was expected to return', async () => {
86
+ const events: AuditEvent[] = [];
87
+ const { promptMiddleware } = createAuditMiddleware(makeOptions(events));
88
+
89
+ await promptMiddleware(
90
+ { method: 'prompts/get' as const, params: { name: 'greet' } },
91
+ async () => undefined as never,
92
+ makeCtx('sess-prompt'),
93
+ );
94
+
95
+ expect(events).toHaveLength(1);
96
+ const event = events[0]!;
97
+ expect(event.kind).toBe('prompt');
98
+ expect(event.outcome).toBe('error');
99
+ expect(event.error).toEqual({
100
+ name: 'InvalidToolResult',
101
+ message: 'Prompt fetch resolved to a value that is not a GetPromptResult',
102
+ });
103
+ });
104
+
105
+ it('keeps the chain and the manifest honest across a dropped result', async () => {
106
+ const events: AuditEvent[] = [];
107
+ const onAuditError = vi.fn();
108
+ const handle = createAuditMiddleware({
109
+ ...makeOptions(events),
110
+ onAuditError,
111
+ });
112
+ const ctx = makeCtx('sess-mixed');
113
+
114
+ // First call fails the way a local tool handler with a missing `return`
115
+ // does; the second is an ordinary success.
116
+ await handle.middleware(
117
+ makeReq('search'),
118
+ async () => undefined as never,
119
+ ctx,
120
+ );
121
+ await handle.middleware(
122
+ makeReq('search'),
123
+ async () => ({ content: [{ type: 'text', text: 'ok' }] }),
124
+ ctx,
125
+ );
126
+
127
+ const manifest = await handle.closeSession('sess-mixed');
128
+
129
+ // Two governed calls, two records, in order.
130
+ expect(events).toHaveLength(2);
131
+ expect(events.map((e) => e.replayManifestPosition)).toEqual([0, 1]);
132
+ expect(events.map((e) => e.outcome)).toEqual(['error', 'success']);
133
+ // Nothing needed the containment report any more: the unusable result is
134
+ // in the trail, not in the operator's log.
135
+ expect(onAuditError).not.toHaveBeenCalled();
136
+
137
+ expect(await verifyAuditChain(events, signingKey)).toEqual({ valid: true });
138
+ expect(manifest?.eventCount).toBe(2);
139
+ expect(await verifyManifestSignature(manifest!, signingKey)).toBe(true);
140
+ });
141
+ });