@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,37 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ /**
3
+ * Reference {@link SubjectKeyStore}, backed by a `Map` in this process.
4
+ *
5
+ * NOT DURABLE, and not for production. The keys live only in this process's
6
+ * heap, so a restart erases every subject at once and a second proxy instance
7
+ * cannot decrypt what the first recorded. Both are silent failures: the chain
8
+ * still verifies, and the loss shows up only when someone tries to read a
9
+ * payload back.
10
+ *
11
+ * A real deployment needs a durable, access-controlled store — a KMS, an HSM,
12
+ * or an encrypted table whose read path is audited — because in erasable mode
13
+ * this store IS the confidentiality of every high-tier payload it holds a key
14
+ * for (ADR-0018).
15
+ */
16
+ export function createInMemorySubjectKeyStore() {
17
+ const keys = new Map();
18
+ return {
19
+ async getOrCreate(subjectId) {
20
+ const existing = keys.get(subjectId);
21
+ if (existing !== undefined)
22
+ return existing;
23
+ // 256-bit, random rather than derived: a key recomputable from the
24
+ // signing secret could not be destroyed, which is the whole point.
25
+ const created = randomBytes(32);
26
+ keys.set(subjectId, created);
27
+ return created;
28
+ },
29
+ async destroy(subjectId) {
30
+ keys.delete(subjectId);
31
+ // Idempotent: an unknown subject still gets a tombstone, so a repeated
32
+ // erasure request produces evidence rather than an error.
33
+ return { destroyedAt: new Date().toISOString() };
34
+ },
35
+ };
36
+ }
37
+ //# sourceMappingURL=subjectKeyStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subjectKeyStore.js","sourceRoot":"","sources":["../src/subjectKeyStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1C;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,6BAA6B;IAC3C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEvC,OAAO;QACL,KAAK,CAAC,WAAW,CAAC,SAAiB;YACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACrC,IAAI,QAAQ,KAAK,SAAS;gBAAE,OAAO,QAAQ,CAAC;YAC5C,mEAAmE;YACnE,mEAAmE;YACnE,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;YAChC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YAC7B,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,SAAiB;YAC7B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YACvB,uEAAuE;YACvE,0DAA0D;YAC1D,OAAO,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;QACnD,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,244 @@
1
+ import type { Identity, PromptMiddleware, ProxyIdentity, RejectionReason, ToolMiddleware } from 'mcpose';
2
+ export type SensitivityTier = 'low' | 'medium' | 'high';
3
+ export type SensitivityResolverFn = (tool: string, identity: Identity, args: Record<string, unknown>) => SensitivityTier;
4
+ /**
5
+ * Override passed to `createSensitivityResolver`. Receives the static map's
6
+ * resolution as `mapTier` (already defaulted to `'high'` for unknown tools)
7
+ * so it can fall back: `(tool, id, args, mapTier) => mapTier`.
8
+ */
9
+ export type SensitivityOverrideFn = (tool: string, identity: Identity, args: Record<string, unknown>, mapTier: SensitivityTier) => SensitivityTier;
10
+ export interface SigningKeyProvider {
11
+ sign(data: Buffer): Promise<Buffer>;
12
+ keyId: string;
13
+ algorithm: 'HMAC-SHA256';
14
+ }
15
+ /** Evidence that a subject's key was destroyed, returned by `destroy`. */
16
+ export interface SubjectKeyTombstone {
17
+ /** ISO timestamp of the destruction. */
18
+ destroyedAt: string;
19
+ }
20
+ /**
21
+ * Custody of the per-subject keys that make erasable mode erasable (ADR-0018).
22
+ *
23
+ * Supplying one to `AuditOptions.keyStore` switches the audit layer into
24
+ * erasable mode: per-event encryption keys and the keyed payload hashes then
25
+ * derive from the stored subject key rather than from the signing secret, so
26
+ * `destroy(subjectId)` makes that subject's recorded payloads permanently
27
+ * unreadable and unconfirmable. The chain and the manifest are untouched.
28
+ *
29
+ * These keys are private key material with the same handling rules as the
30
+ * signing secret: they must never be logged, exported, or written into an
31
+ * event, a manifest, or telemetry. An implementation that stores them in the
32
+ * clear voids the encryption guarantee for every subject in it.
33
+ */
34
+ export interface SubjectKeyStore {
35
+ /**
36
+ * The subject's 256-bit key, created at random on first use. Called before
37
+ * every audited call in erasable mode, so it must not be memoized in a way
38
+ * that outlives `destroy`: a subject that calls again after erasure gets a
39
+ * fresh key, and the old events stay dead.
40
+ *
41
+ * A rejection fails the audited call (the pre-call failure stage) rather
42
+ * than letting it run unaudited.
43
+ */
44
+ getOrCreate(subjectId: string): Promise<Buffer>;
45
+ /**
46
+ * Permanently removes the subject's key and returns the tombstone that
47
+ * evidences the erasure. Idempotent: destroying an unknown subject still
48
+ * returns a tombstone, because "this subject holds no key" is the state the
49
+ * caller asked for either way.
50
+ */
51
+ destroy(subjectId: string): Promise<SubjectKeyTombstone>;
52
+ }
53
+ export interface AuditEventBase {
54
+ /** Equals the ProxyContext requestId. */
55
+ id: string;
56
+ /** ISO timestamp captured before the upstream call started. */
57
+ startedAt: string;
58
+ /** ISO timestamp captured after the upstream call settled. */
59
+ endedAt: string;
60
+ sessionId?: string;
61
+ identity: Identity;
62
+ delegatedFrom?: Identity[];
63
+ /**
64
+ * The proxy instance that recorded this event, from `ProxyContext.proxy`.
65
+ * Provenance, not a principal — never part of `delegatedFrom` (ADR-0012).
66
+ *
67
+ * Required, and covered by the chain unconditionally (ADR-0019). While it
68
+ * was omitted when absent, an event with no `proxy` and an event whose
69
+ * `proxy` was stripped produced the same preimage, so deleting recorded
70
+ * provenance was not chain-detectable.
71
+ */
72
+ proxy: ProxyIdentity;
73
+ /**
74
+ * Present ONLY on events recorded for a prompt call (`prompts/get`), where
75
+ * `tool` holds the prompt name. An absent `kind` means a tool call, so
76
+ * every event recorded before prompts were audited keeps its meaning and
77
+ * its chain preimage (additive within v2, ADR-0012 and ADR-0014).
78
+ */
79
+ kind?: 'prompt';
80
+ /**
81
+ * Present ONLY on events recorded in erasable mode, where the per-event
82
+ * encryption key and the keyed `inputHash`/`outputHash` derive from a
83
+ * destroyable subject key instead of the signing secret (ADR-0018).
84
+ *
85
+ * Optional and covered by the chain under the ADR-0012 omission rule,
86
+ * exactly as `kind` is: an absent marker means default mode with plain
87
+ * `sha256` hashes, so events recorded before erasable mode existed keep
88
+ * their preimage, and a stored event cannot be silently reinterpreted
89
+ * under the wrong hash scheme.
90
+ */
91
+ erasable?: true;
92
+ tool: string;
93
+ duration_ms: number;
94
+ outcome: 'success' | 'rejected' | 'error';
95
+ /** Present when outcome is 'rejected' (from the MCP error's data field). */
96
+ rejectionReason?: RejectionReason;
97
+ /**
98
+ * Present when outcome is 'error': what the upstream call threw, or
99
+ * `{ name: 'ToolError' }` with a fixed message when a tool result carried
100
+ * `isError: true` in-band.
101
+ */
102
+ error?: {
103
+ name: string;
104
+ message: string;
105
+ };
106
+ inputHash: string;
107
+ outputHash: string;
108
+ chainHash: string;
109
+ replayManifestPosition: number;
110
+ }
111
+ export type LowAuditEvent = AuditEventBase & {
112
+ sensitivityTier: 'low';
113
+ inputRaw: Record<string, unknown>;
114
+ outputRaw: unknown;
115
+ };
116
+ export type MediumAuditEvent = AuditEventBase & {
117
+ sensitivityTier: 'medium';
118
+ inputRaw: Record<string, unknown>;
119
+ outputRaw: unknown;
120
+ };
121
+ export type HighAuditEvent = AuditEventBase & {
122
+ sensitivityTier: 'high';
123
+ inputEncrypted: string;
124
+ outputEncrypted: string;
125
+ };
126
+ export type AuditEvent = LowAuditEvent | MediumAuditEvent | HighAuditEvent;
127
+ export interface MerkleProof {
128
+ index: number;
129
+ siblings: string[];
130
+ directions: ('left' | 'right')[];
131
+ }
132
+ export interface ReplayManifest {
133
+ sessionId: string;
134
+ identity: Identity;
135
+ /**
136
+ * The proxy instance that produced this session's trail, captured when
137
+ * the session was first seen. Covered by the signature like every other
138
+ * field (ADR-0004, ADR-0012).
139
+ *
140
+ * Required, and included in the signing payload unconditionally
141
+ * (ADR-0019), so stripping it from a stored manifest fails the signature
142
+ * rather than rebuilding a payload that verifies.
143
+ */
144
+ proxy: ProxyIdentity;
145
+ startedAt: string;
146
+ closedAt: string;
147
+ eventCount: number;
148
+ merkleRoot: string;
149
+ merkleProofs: MerkleProof[];
150
+ signedBy: string;
151
+ /**
152
+ * HMAC over the canonical serialization of the ENTIRE manifest (every
153
+ * field above, domain-separated) — not just the Merkle root. Verify with
154
+ * `verifyManifestSignature`.
155
+ */
156
+ signature: string;
157
+ }
158
+ export interface AuditOptions {
159
+ signingKey: SigningKeyProvider;
160
+ sensitivityResolver: SensitivityResolverFn;
161
+ /**
162
+ * Supply a store to run in ERASABLE mode (ADR-0018). Omit it and the audit
163
+ * layer behaves exactly as it always has, byte for byte: same preimages,
164
+ * same ciphertexts, same plain `sha256` payload hashes, no `erasable`
165
+ * marker on any event.
166
+ *
167
+ * With a store, the erasure unit is the data subject — the resolved
168
+ * `identity.sub` of each event, with anonymous events sharing the
169
+ * `anonymousIdentity()` bucket — and `destroy(sub)` renders that subject's
170
+ * recorded payloads permanently undecryptable and unconfirmable. Chain and
171
+ * manifest verification are unaffected, because payloads are bound to the
172
+ * chain only through their hashes.
173
+ *
174
+ * Key custody becomes your responsibility: see the README's erasable-mode
175
+ * section before choosing a store.
176
+ */
177
+ keyStore?: SubjectKeyStore;
178
+ onEvent: (event: AuditEvent) => void | Promise<void>;
179
+ /**
180
+ * Called with the finished ReplayManifest when the host calls closeSession().
181
+ *
182
+ * Why this exists: ToolMiddleware is a pure per-request function with no
183
+ * lifecycle hooks. Sessions are owned by the HTTP transport, not by
184
+ * middleware. There is no in-band way for middleware to observe session close.
185
+ * This callback gives consumers a push-based way to receive the manifest
186
+ * exactly when the host signals the session has ended — mirroring how audit
187
+ * substrates in financial systems work: the host controls the flush boundary;
188
+ * the audit layer reacts.
189
+ */
190
+ onManifest?: (manifest: ReplayManifest) => void | Promise<void>;
191
+ /**
192
+ * Record events for rejected calls (hidden tools etc.).
193
+ * @default true
194
+ */
195
+ includeRejections?: boolean;
196
+ /**
197
+ * How long `closeSession` waits for calls already admitted to the session
198
+ * to append and persist their events before it seals the manifest. Past
199
+ * the deadline the manifest covers what has landed, and each straggler is
200
+ * reported through `onAuditError` with its tool and requestId. `Infinity`
201
+ * waits without bound.
202
+ * @default 30_000
203
+ */
204
+ closeDrainTimeoutMs?: number;
205
+ /**
206
+ * Called when the audit layer itself fails (event serialization, a
207
+ * throwing onEvent sink). The audit layer NEVER throws into the tool-call
208
+ * path; failures are reported here instead. If this hook itself throws,
209
+ * the exception is contained and never reaches the caller either.
210
+ * @default console.error
211
+ */
212
+ onAuditError?: (err: unknown, info: {
213
+ tool: string;
214
+ requestId: string;
215
+ sessionId?: string;
216
+ }) => void;
217
+ }
218
+ export interface AuditMiddlewareHandle {
219
+ middleware: ToolMiddleware;
220
+ /**
221
+ * Audits `prompts/get` calls. Wire it into
222
+ * `ProxyOptions.promptMiddleware`; it shares the session chain with
223
+ * `middleware`, so tool and prompt events interleave in one trail.
224
+ * Prompt events carry `kind: 'prompt'` (ADR-0014).
225
+ */
226
+ promptMiddleware: PromptMiddleware;
227
+ /**
228
+ * Signal that a session has ended. Waits for every call already admitted
229
+ * to the session to append and persist its event, then computes the
230
+ * Merkle tree over all audit events for the session, signs the full
231
+ * manifest, fires onManifest, and returns the ReplayManifest. A call still
232
+ * inside its handler at teardown therefore lands in the manifest rather
233
+ * than orphaned at position 0. Concurrent calls for the same session share
234
+ * one close. Returns undefined if the session had no events or is unknown.
235
+ *
236
+ * A rejection from the signing provider or from onManifest is retryable:
237
+ * the sealed session is retained, and calling closeSession again with the
238
+ * same id resumes where it stopped. A retry signs the same payload and
239
+ * delivers the same manifest, never a second signed artifact. The state
240
+ * is released only once onManifest has settled successfully.
241
+ */
242
+ closeSession(sessionId: string): Promise<ReplayManifest | undefined>;
243
+ }
244
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ import type { AuditEvent, ReplayManifest, SigningKeyProvider } from './types.js';
2
+ export type ChainVerification = {
3
+ valid: true;
4
+ } | {
5
+ valid: false;
6
+ index: number;
7
+ reason: string;
8
+ };
9
+ /**
10
+ * KEYED chain verification: recomputes every event's chainHash from its
11
+ * preimage fields with the chain key derived through the signing oracle.
12
+ * Unlike the keyless `assertAuditChainIntegrity` in @mcpose/testing, this
13
+ * detects key-inconsistent forgeries — a rewritten event, a renumbered
14
+ * suffix, or a chain built under a different secret.
15
+ *
16
+ * An empty event list is invalid: truncation-to-zero must not verify.
17
+ */
18
+ export declare function verifyAuditChain(events: readonly AuditEvent[], signingKey: SigningKeyProvider): Promise<ChainVerification>;
19
+ /**
20
+ * Recomputes the manifest's signature (which covers every manifest field,
21
+ * not just the Merkle root) and compares it in constant time.
22
+ */
23
+ export declare function verifyManifestSignature(manifest: ReplayManifest, signingKey: SigningKeyProvider): Promise<boolean>;
24
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAMpB,MAAM,MAAM,iBAAiB,GAC3B;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAQpE;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,UAAU,EAAE,kBAAkB,GAC7B,OAAO,CAAC,iBAAiB,CAAC,CA4B5B;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,cAAc,EACxB,UAAU,EAAE,kBAAkB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAMlB"}
package/dist/verify.js ADDED
@@ -0,0 +1,50 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import { chainPreimageFields, computeChainHash } from './chain.js';
3
+ import { manifestSigningPayload } from './middleware.js';
4
+ const DOMAIN_CHAIN = Buffer.from('mcpose/v2/chain');
5
+ function hexEqual(aHex, bHex) {
6
+ const a = Buffer.from(aHex, 'hex');
7
+ const b = Buffer.from(bHex, 'hex');
8
+ return a.length === b.length && a.length > 0 && timingSafeEqual(a, b);
9
+ }
10
+ /**
11
+ * KEYED chain verification: recomputes every event's chainHash from its
12
+ * preimage fields with the chain key derived through the signing oracle.
13
+ * Unlike the keyless `assertAuditChainIntegrity` in @mcpose/testing, this
14
+ * detects key-inconsistent forgeries — a rewritten event, a renumbered
15
+ * suffix, or a chain built under a different secret.
16
+ *
17
+ * An empty event list is invalid: truncation-to-zero must not verify.
18
+ */
19
+ export async function verifyAuditChain(events, signingKey) {
20
+ if (events.length === 0) {
21
+ return { valid: false, index: 0, reason: 'empty event list' };
22
+ }
23
+ const chainKey = await signingKey.sign(DOMAIN_CHAIN);
24
+ let prevChainHash = '';
25
+ for (const [i, event] of events.entries()) {
26
+ if (event.replayManifestPosition !== i) {
27
+ return {
28
+ valid: false,
29
+ index: i,
30
+ reason: `replayManifestPosition ${event.replayManifestPosition}, expected ${i}`,
31
+ };
32
+ }
33
+ const expected = computeChainHash(chainPreimageFields(event), prevChainHash, chainKey);
34
+ if (!hexEqual(event.chainHash, expected)) {
35
+ return { valid: false, index: i, reason: 'chainHash mismatch' };
36
+ }
37
+ prevChainHash = event.chainHash;
38
+ }
39
+ return { valid: true };
40
+ }
41
+ /**
42
+ * Recomputes the manifest's signature (which covers every manifest field,
43
+ * not just the Merkle root) and compares it in constant time.
44
+ */
45
+ export async function verifyManifestSignature(manifest, signingKey) {
46
+ const { signature, ...unsigned } = manifest;
47
+ const expected = await signingKey.sign(Buffer.from(manifestSigningPayload(unsigned)));
48
+ return hexEqual(signature, expected.toString('hex'));
49
+ }
50
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAM9C,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;AAKpD,SAAS,QAAQ,CAAC,IAAY,EAAE,IAAY;IAC1C,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACnC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACnC,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAA6B,EAC7B,UAA8B;IAE9B,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAChE,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACrD,IAAI,aAAa,GAAG,EAAE,CAAC;IAEvB,KAAK,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;QAC1C,IAAI,KAAK,CAAC,sBAAsB,KAAK,CAAC,EAAE,CAAC;YACvC,OAAO;gBACL,KAAK,EAAE,KAAK;gBACZ,KAAK,EAAE,CAAC;gBACR,MAAM,EAAE,0BAA0B,KAAK,CAAC,sBAAsB,cAAc,CAAC,EAAE;aAChF,CAAC;QACJ,CAAC;QACD,MAAM,QAAQ,GAAG,gBAAgB,CAC/B,mBAAmB,CAAC,KAAK,CAAC,EAC1B,aAAa,EACb,QAAQ,CACT,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,CAAC;YACzC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;QAClE,CAAC;QACD,aAAa,GAAG,KAAK,CAAC,SAAS,CAAC;IAClC,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACzB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,QAAwB,EACxB,UAA8B;IAE9B,MAAM,EAAE,SAAS,EAAE,GAAG,QAAQ,EAAE,GAAG,QAAQ,CAAC;IAC5C,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,IAAI,CACpC,MAAM,CAAC,IAAI,CAAC,sBAAsB,CAAC,QAAQ,CAAC,CAAC,CAC9C,CAAC;IACF,OAAO,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;AACvD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpose/audit",
3
- "version": "2.0.2",
3
+ "version": "3.0.0",
4
4
  "description": "Integrity-chained audit middleware for mcpose — HMAC chain, Merkle proofs, ReplayManifest",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,29 +9,58 @@
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
11
  "default": "./dist/index.js"
12
- }
12
+ },
13
+ "./package.json": "./package.json"
13
14
  },
14
- "files": ["dist", "README.md"],
15
- "keywords": ["mcp", "audit", "compliance", "fintech", "hmac", "merkle"],
15
+ "sideEffects": false,
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "src",
22
+ "README.md"
23
+ ],
24
+ "keywords": [
25
+ "mcp",
26
+ "audit",
27
+ "compliance",
28
+ "fintech",
29
+ "hmac",
30
+ "merkle"
31
+ ],
16
32
  "author": "Amir Gorji",
17
33
  "license": "MIT",
34
+ "homepage": "https://github.com/amir-gorji/mcpose#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/amir-gorji/mcpose/issues"
37
+ },
18
38
  "repository": {
19
39
  "type": "git",
20
40
  "url": "git+https://github.com/amir-gorji/mcpose.git",
21
41
  "directory": "packages/audit"
22
42
  },
23
- "scripts": {
24
- "build": "tsc -p tsconfig.build.json",
25
- "ts:ci": "tsc --noEmit",
26
- "test": "vitest run --passWithNoTests"
43
+ "engines": {
44
+ "node": ">=20"
27
45
  },
28
46
  "peerDependencies": {
29
- "mcpose": ">=2.0.0"
47
+ "mcpose": ">=3.0.0 <4"
30
48
  },
31
49
  "devDependencies": {
32
- "@types/node": "^22.0.0",
33
- "typescript": "^5.0.0",
34
- "vitest": "^3.0.0",
35
- "mcpose": "workspace:*"
50
+ "@stryker-mutator/core": "10.0.0",
51
+ "@stryker-mutator/vitest-runner": "10.0.0",
52
+ "@types/node": "^26.5.1",
53
+ "@vitest/coverage-v8": "5.0.1",
54
+ "fast-check": "4.10.2",
55
+ "typescript": "6.0.3",
56
+ "vitest": "5.0.1",
57
+ "mcpose": "3.0.0"
58
+ },
59
+ "scripts": {
60
+ "build": "tsc -p tsconfig.build.json",
61
+ "benchmark:merkle": "node ./bench/merkle-proofs.mjs",
62
+ "ts:ci": "tsc --noEmit",
63
+ "test": "vitest run --coverage",
64
+ "mutation": "stryker run"
36
65
  }
37
- }
66
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ computeMerkleProof,
4
+ computeMerkleRoot,
5
+ computeMerkleRootAndProofs,
6
+ verifyMerkleProof,
7
+ } from '../chain.js';
8
+
9
+ describe('batched Merkle root and proofs', () => {
10
+ it('matches the existing root and individual proofs for empty, odd, and even trees', () => {
11
+ for (let size = 0; size <= 17; size++) {
12
+ const hashes = Array.from(
13
+ { length: size },
14
+ (_, index) => `hash-${index}`,
15
+ );
16
+ const { merkleRoot, merkleProofs } = computeMerkleRootAndProofs(hashes);
17
+
18
+ expect(merkleRoot).toBe(computeMerkleRoot(hashes));
19
+ expect(merkleProofs).toEqual(
20
+ hashes.map((_, index) => computeMerkleProof(hashes, index)),
21
+ );
22
+
23
+ for (let index = 0; index < hashes.length; index++) {
24
+ expect(
25
+ verifyMerkleProof(hashes[index]!, merkleProofs[index]!, merkleRoot),
26
+ ).toBe(true);
27
+ }
28
+ }
29
+ });
30
+ });