@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.
- package/LICENSE +21 -0
- package/README.md +440 -28
- package/dist/chain.d.ts +49 -6
- package/dist/chain.d.ts.map +1 -1
- package/dist/chain.js +227 -28
- package/dist/chain.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +3 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +499 -85
- package/dist/middleware.js.map +1 -1
- package/dist/sensitivity.d.ts +11 -2
- package/dist/sensitivity.d.ts.map +1 -1
- package/dist/sensitivity.js +18 -2
- package/dist/sensitivity.js.map +1 -1
- package/dist/signingKey.d.ts +8 -0
- package/dist/signingKey.d.ts.map +1 -1
- package/dist/signingKey.js +21 -4
- package/dist/signingKey.js.map +1 -1
- package/dist/subjectKeyStore.d.ts +17 -0
- package/dist/subjectKeyStore.d.ts.map +1 -0
- package/dist/subjectKeyStore.js +37 -0
- package/dist/subjectKeyStore.js.map +1 -0
- package/dist/types.d.ts +170 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/verify.d.ts +24 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +66 -0
- package/dist/verify.js.map +1 -0
- package/package.json +43 -15
- package/src/__tests__/chain.batch.test.ts +30 -0
- package/src/__tests__/chain.test.ts +433 -0
- package/src/__tests__/erasable.test.ts +499 -0
- package/src/__tests__/merkle-batch.middleware.test.ts +96 -0
- package/src/__tests__/middleware.http.test.ts +172 -0
- package/src/__tests__/middleware.rejection.test.ts +183 -0
- package/src/__tests__/middleware.robustness.test.ts +809 -0
- package/src/__tests__/middleware.test.ts +1121 -0
- package/src/__tests__/middleware.unusableResult.test.ts +141 -0
- package/src/__tests__/properties.test.ts +764 -0
- package/src/__tests__/sensitivity.test.ts +72 -0
- package/src/__tests__/signingKey.test.ts +76 -0
- package/src/chain.ts +317 -0
- package/src/index.ts +31 -0
- package/src/middleware.ts +781 -0
- package/src/sensitivity.ts +33 -0
- package/src/signingKey.ts +35 -0
- package/src/subjectKeyStore.ts +38 -0
- package/src/types.ts +287 -0
- package/src/verify.ts +92 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amir Gorji
|
|
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
CHANGED
|
@@ -2,14 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@mcpose/audit)
|
|
4
4
|
[](https://github.com/amir-gorji/mcpose/blob/main/LICENSE)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://github.com/amir-gorji/mcpose/actions/workflows/ci.yml)
|
|
5
8
|
|
|
6
9
|
**Tamper-evident audit middleware for [mcpose](https://www.npmjs.com/package/mcpose).**
|
|
7
10
|
|
|
8
|
-
`@mcpose/audit` turns every tool call flowing through an mcpose proxy into a tamper-evident **audit event**: HMAC-chained to its predecessor, hashed, and
|
|
11
|
+
`@mcpose/audit` turns every tool call flowing through an mcpose proxy into a tamper-evident **audit event**: HMAC-chained to its predecessor, hashed, and, for high-sensitivity calls, encrypted at rest.
|
|
12
|
+
When a **session** closes it emits a signed **replay manifest** with a Merkle root and per-event proofs, so a third party can verify that a single event happened without access to the full log.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [When to reach for it](#when-to-reach-for-it)
|
|
17
|
+
- [Features](#features)
|
|
18
|
+
- [Install](#install)
|
|
19
|
+
- [Quick start](#quick-start)
|
|
20
|
+
- [How it works](#how-it-works)
|
|
21
|
+
- [Security model](#security-model)
|
|
22
|
+
- [What the chain covers](#what-the-chain-covers)
|
|
23
|
+
- [What it does not cover](#what-it-does-not-cover)
|
|
24
|
+
- [Key hierarchy](#key-hierarchy)
|
|
25
|
+
- [Erasable mode: cryptographic erasure](#erasable-mode-cryptographic-erasure)
|
|
26
|
+
- [API surface](#api-surface)
|
|
27
|
+
- [`createAuditMiddleware(options)`](#createauditmiddlewareoptions)
|
|
28
|
+
- [`createSensitivityResolver(map, override?)`](#createsensitivityresolvermap-override)
|
|
29
|
+
- [`createDefaultSigningKeyProvider(secret)`](#createdefaultsigningkeyprovidersecret)
|
|
30
|
+
- [`SubjectKeyStore`](#subjectkeystore)
|
|
31
|
+
- [`AuditEvent`](#auditevent)
|
|
32
|
+
- [`ReplayManifest`](#replaymanifest)
|
|
33
|
+
- [Verification](#verification)
|
|
34
|
+
- [Testing](#testing)
|
|
35
|
+
- [Documentation](#documentation)
|
|
36
|
+
- [License](#license)
|
|
9
37
|
|
|
10
38
|
## When to reach for it
|
|
11
39
|
|
|
12
|
-
You operate an MCP server in a regulated environment
|
|
40
|
+
You operate an MCP server in a regulated environment and need to prove, after the fact, exactly which tool calls happened, by whom, and in what order, with cryptographic evidence that the record has not been altered, inserted into, or truncated.
|
|
41
|
+
|
|
42
|
+
If what you actually need is observability (latency, error rates, call volume), reach for mcpose core's `onTelemetry` hook and your existing log pipeline instead.
|
|
43
|
+
This package exists for the harder problem: a record that stays credible when someone disputes it.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **HMAC-chained audit events.** Every event is cryptographically linked to its predecessor over a canonical serialization, so a secret-holder can detect insertion, deletion, or reordering with `verifyAuditChain`.
|
|
48
|
+
- **Signed ReplayManifest.** The signature covers the *entire* manifest (session, identity, timestamps, event count, Merkle root, and every proof), not just the root, so no field is silently swappable.
|
|
49
|
+
- **Full coverage.** Rejected calls (hidden tools) and `passThroughTools` are audited too, because the middleware is registered as a pass-through observer.
|
|
50
|
+
- **Sensitivity-tiered storage.** Classify tools as low, medium, or high; high-tier payloads are AES-256-GCM encrypted at rest with per-event keys. Unknown or invalid tiers fail closed to `high`.
|
|
51
|
+
- **Subkeys derived through the signing oracle.** Chain and encryption keys derive from the signing secret with domain separation, never from the public key id ([ADR-0003](https://github.com/amir-gorji/mcpose/blob/main/docs/adr/0003-audit-subkeys-derived-from-signing-oracle.md)).
|
|
52
|
+
- **Optional erasable mode.** Supply a `SubjectKeyStore` and a subject's payloads become destroyable: `destroy(sub)` kills both decryptability and hash-confirmability for that subject, and the chain and manifest keep verifying ([ADR-0018](https://github.com/amir-gorji/mcpose/blob/main/docs/adr/0018-cryptographic-erasure-and-the-chain.md)). Omit it and nothing changes.
|
|
53
|
+
- **Never blocks the call path.** Audit failures (a throwing sink, unserializable payloads) are routed to `onAuditError`; the tool call always completes with its real result or error.
|
|
54
|
+
A throwing `onAuditError`, or one that returns a rejected promise, is contained too, so a broken reporter cannot replace a tool result or mask an upstream error.
|
|
55
|
+
Three configuration errors are the deliberate exception, and all fail before the upstream is reached rather than after: an unavailable signing provider, a `ProxyContext` with no `proxy` identity, and an unavailable `SubjectKeyStore` in erasable mode.
|
|
56
|
+
Each would otherwise produce a record that cannot be verified or cannot be attributed, so the call fails instead of running unaudited.
|
|
57
|
+
A proxy built by `createProxyServer` always stamps `ctx.proxy`, so only a host that invokes the middleware with its own hand-built context needs to supply it.
|
|
58
|
+
- **No storage lock-in.** Events and manifests are pushed to your own sinks through `onEvent` and `onManifest`.
|
|
13
59
|
|
|
14
60
|
## Install
|
|
15
61
|
|
|
@@ -17,7 +63,13 @@ You operate an MCP server in a regulated environment (e.g. financial services) a
|
|
|
17
63
|
npm install @mcpose/audit mcpose
|
|
18
64
|
```
|
|
19
65
|
|
|
20
|
-
|
|
66
|
+
Requires Node.js 20+ (uses `node:crypto`).
|
|
67
|
+
`mcpose` (`>=3.0.0 <4`) is a peer dependency: the audit event and handle types reference `ProxyIdentity` and `PromptMiddleware`, which core exports from 3.0.0 on.
|
|
68
|
+
|
|
69
|
+
> **Format note:** version 3.0.0 writes the **v2 audit format** (`mcpose/v2/*` domain labels).
|
|
70
|
+
> Chains and manifests written by a 2.x release do not verify under 3.x.
|
|
71
|
+
> Keep a pinned 2.x to verify old archives.
|
|
72
|
+
> See [ADR-0004](https://github.com/amir-gorji/mcpose/blob/main/docs/adr/0004-audit-format-v2-canonical-serialization.md).
|
|
21
73
|
|
|
22
74
|
## Quick start
|
|
23
75
|
|
|
@@ -30,13 +82,13 @@ import {
|
|
|
30
82
|
import { startHttpProxy } from 'mcpose';
|
|
31
83
|
|
|
32
84
|
// Supplied by your application:
|
|
33
|
-
// backend
|
|
34
|
-
// auditLog
|
|
35
|
-
// manifestStore
|
|
36
|
-
// piiMW
|
|
37
|
-
// extractJwt
|
|
85
|
+
// backend: an mcpose BackendClient (see the `mcpose` docs)
|
|
86
|
+
// auditLog: your durable sink for audit events
|
|
87
|
+
// manifestStore: your durable sink for replay manifests
|
|
88
|
+
// piiMW: an upstream redaction middleware
|
|
89
|
+
// extractJwt: your resolveIdentity function
|
|
38
90
|
|
|
39
|
-
// The signing secret never leaves the process;
|
|
91
|
+
// The signing secret never leaves the process; every subkey derives from it.
|
|
40
92
|
const signingKey = createDefaultSigningKeyProvider(process.env.AUDIT_SECRET!);
|
|
41
93
|
|
|
42
94
|
// Map tools to a sensitivity tier. Unknown tools resolve to 'high'.
|
|
@@ -55,7 +107,8 @@ const auditHandle = createAuditMiddleware({
|
|
|
55
107
|
|
|
56
108
|
await startHttpProxy(
|
|
57
109
|
backend,
|
|
58
|
-
|
|
110
|
+
// Redaction first, so audit only ever records clean data.
|
|
111
|
+
{ name: 'payments-proxy', toolMiddleware: [piiMW, auditHandle.middleware] },
|
|
59
112
|
{
|
|
60
113
|
resolveIdentity: extractJwt,
|
|
61
114
|
// Flush the replay manifest when the session ends.
|
|
@@ -64,56 +117,415 @@ await startHttpProxy(
|
|
|
64
117
|
);
|
|
65
118
|
```
|
|
66
119
|
|
|
120
|
+
Two wiring details decide whether this actually works:
|
|
121
|
+
|
|
122
|
+
- **Middleware order.** `ProxyOptions` arrays are in response-processing order, so `[piiMW, auditHandle.middleware]` redacts *before* it audits. Reversing it logs raw PII.
|
|
123
|
+
- **`onSessionClosed`.** Without it, no manifest is ever produced, because nothing tells the audit layer a session ended.
|
|
124
|
+
|
|
125
|
+
**The host owns the close call, and the proxy closes sessions on its own.**
|
|
126
|
+
`createAuditMiddleware` holds an in-memory event array per session and releases it only when `closeSession` runs.
|
|
127
|
+
`startHttpProxy` fires `onSessionClosed` on every way a session can end: a client DELETE, `sessionTtlMs` expiry, and server shutdown.
|
|
128
|
+
Wiring `onSessionClosed` to `closeSession`, as above, is therefore the whole pattern: an abandoned session still expires on the TTL, and its manifest still gets signed and handed to `onManifest`.
|
|
129
|
+
`closeSession` waits for every call the session has already admitted to append and persist its event before it seals, so a tool call still running when the client sends DELETE or the TTL fires ends up in the manifest at its proper position rather than as an orphan.
|
|
130
|
+
Concurrent `closeSession` calls for one session share a single drain and a single signed manifest.
|
|
131
|
+
The wait is bounded by `closeDrainTimeoutMs` (default 30 seconds, `Infinity` to wait without bound): past it the manifest seals what has landed, and each call still in flight is reported through `onAuditError` with its tool and requestId, so a handler that never settles cannot hold the session's recorded events unsigned forever.
|
|
132
|
+
A close that fails after the drain, because the signing provider or `onManifest` rejected, is retryable: the sealed session is kept, and calling `closeSession` again with the same id resumes from the failed step.
|
|
133
|
+
The retry signs the same payload and delivers the same manifest, so a transient KMS or manifest-store outage costs a retry rather than a second signed artifact or a lost session.
|
|
134
|
+
The state is released only once `onManifest` has settled successfully, so a host that never retries keeps that sealed state in memory.
|
|
135
|
+
Skip the wiring and an abandoned session leaves its events unsigned in memory for the life of the process, which is both a leak and a silent hole in the audit record.
|
|
136
|
+
|
|
137
|
+
`sessionTtlMs` defaults to 30 minutes and `maxSessions` to 1000, so that expiry happens whether or not a host configures either.
|
|
138
|
+
A deployment that opts out with `sessionTtlMs: Infinity` takes back the job of ending abandoned sessions itself.
|
|
139
|
+
|
|
67
140
|
## How it works
|
|
68
141
|
|
|
69
|
-
- **Audit event
|
|
70
|
-
|
|
71
|
-
- **
|
|
142
|
+
- **Audit event**: the record of one tool call or prompt call: `identity`, `tool`, `outcome`, input and output hashes, and a `chainHash` linking it to its predecessor.
|
|
143
|
+
`AuditEvent` is a discriminated union on `sensitivityTier`; prompt events additionally carry `kind: 'prompt'`.
|
|
144
|
+
- **Sensitivity tier** (`low` | `medium` | `high`): decides whether the event stores plaintext (`inputRaw` / `outputRaw`) or AES-256-GCM ciphertext (`inputEncrypted` / `outputEncrypted`).
|
|
145
|
+
- **Replay manifest**: produced at session close. A Merkle root over every event's `chainHash`, one proof per event, and a signature over the whole document.
|
|
146
|
+
It proves *what happened*; it does not re-execute anything.
|
|
147
|
+
|
|
148
|
+
**Sessions are required for chaining.**
|
|
149
|
+
Chaining needs `ProxyContext.sessionId`, which only HTTP transport provides.
|
|
150
|
+
Over stdio there is no session, so every event carries position 0 with an empty previous hash, and no manifest is produced.
|
|
151
|
+
This is intentional, not a gap to work around.
|
|
72
152
|
|
|
73
153
|
## Security model
|
|
74
154
|
|
|
75
|
-
The append-only HMAC chain makes insertion, deletion,
|
|
155
|
+
The append-only HMAC chain makes insertion, deletion, and reordering detectable **by a holder of the signing secret**.
|
|
156
|
+
The signed manifest anchors the whole session, and high-tier payloads are encrypted at rest with AAD binding each ciphertext to its own event and direction.
|
|
157
|
+
|
|
158
|
+
### What the chain covers
|
|
159
|
+
|
|
160
|
+
Each link is `HMAC(chainKey, canonicalJson({ domain, prevChainHash, event }))` over this field set:
|
|
161
|
+
|
|
162
|
+
`id`, `startedAt`, `endedAt`, `sessionId?`, `delegatedFrom?`, `proxy`, `kind?`, `erasable?`, `identity`, `tool`, `duration_ms`, `outcome`, `sensitivityTier`, `rejectionReason?`, `error?`, `inputHash`, `outputHash`, `replayManifestPosition`.
|
|
163
|
+
|
|
164
|
+
Optional fields are omitted from the preimage when absent, so events recorded before an optional field existed keep verifying unchanged (ADR-0012).
|
|
165
|
+
`sensitivityTier` and `proxy` are required on every event and are therefore always present, which is why covering each of them amended format v2 in place rather than extending it (ADR-0015 and ADR-0019).
|
|
166
|
+
`proxy` was optional at first, and that made removing recorded provenance undetectable: an event that never carried one and an event whose one was stripped produced the same preimage.
|
|
167
|
+
Covering it unconditionally closes that gap, at the cost of requiring `ctx.proxy` on every audited request.
|
|
168
|
+
|
|
169
|
+
Serialization is canonical (keys sorted at every depth), so key insertion order is not load-bearing and an independently written verifier reproduces the same hash.
|
|
170
|
+
The **field set** is what matters, and it is defined once in the source and shared by producer and verifier.
|
|
171
|
+
An independently written verifier must track the current field set: one pinned to an older set mis-hashes events that carry a newer optional field.
|
|
172
|
+
|
|
173
|
+
### What it does not cover
|
|
174
|
+
|
|
175
|
+
Stating the boundaries precisely is the point of an audit trail, so:
|
|
176
|
+
|
|
177
|
+
- **Payloads are bound only by hash.** `inputHash` / `outputHash` commit to the payload; the raw and encrypted bodies themselves are not in the preimage.
|
|
178
|
+
- **The keyless assertions in `@mcpose/testing` do not prove authenticity.** They prove internal consistency, catching reordering, renumbering, duplication, head or middle deletion, and a swapped Merkle root. A forger who rewrites every event and regenerates the root and proofs produces a document they accept, and does not need the signing secret to do it, because the forger supplies the hashes. See [that package's README](https://www.npmjs.com/package/@mcpose/testing) for the per-assertion limits.
|
|
179
|
+
- **Tail truncation leaves a valid prefix.** Head and middle deletion renumber everything after them and are caught; truncating the tail is not, so `assertAuditChainIntegrity` accepts it on its own. The manifest's `eventCount` is what catches it.
|
|
180
|
+
|
|
181
|
+
### Key hierarchy
|
|
182
|
+
|
|
183
|
+
> **The signing secret is the root of all of it.**
|
|
184
|
+
> The per-entry **chain key** and the per-event AES **encryption root** derive from the secret *through* the `SigningKeyProvider.sign()` oracle with domain separation, never from the public **key id**.
|
|
185
|
+
> `ReplayManifest.signedBy` is a public identifier only.
|
|
186
|
+
> **Never use it as key material**, and never hand-roll the chain or encryption keys.
|
|
187
|
+
> See **[ADR-0003](https://github.com/amir-gorji/mcpose/blob/main/docs/adr/0003-audit-subkeys-derived-from-signing-oracle.md)** for the attack this closes.
|
|
188
|
+
|
|
189
|
+
The secret must be high-entropy (32+ random bytes).
|
|
190
|
+
`keyId` is published in every manifest, so a guessable passphrase is offline-attackable.
|
|
191
|
+
|
|
192
|
+
For production, implement `SigningKeyProvider` against your KMS rather than holding the secret in process.
|
|
193
|
+
`createDefaultSigningKeyProvider` is in-process HMAC-SHA256 signing, suitable for development and single-trust deployments.
|
|
194
|
+
|
|
195
|
+
## Erasable mode: cryptographic erasure
|
|
196
|
+
|
|
197
|
+
A GDPR or CCPA erasure request and a tamper-evident trail pull in opposite directions: one demands a subject's data become unrecoverable, the other exists to make recorded history immutable.
|
|
198
|
+
Erasable mode reconciles them, on the observation that payloads are bound to the chain only through their hashes.
|
|
199
|
+
Destroying the ability to *read* a payload touches no preimage, so the trail stays exactly as verifiable as it was.
|
|
200
|
+
|
|
201
|
+
[ADR-0018](https://github.com/amir-gorji/mcpose/blob/main/docs/adr/0018-cryptographic-erasure-and-the-chain.md) is the binding design.
|
|
202
|
+
|
|
203
|
+
### Activation
|
|
204
|
+
|
|
205
|
+
Pass a `SubjectKeyStore` as `keyStore`.
|
|
206
|
+
That is the whole switch:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { createAuditMiddleware, createInMemorySubjectKeyStore } from '@mcpose/audit';
|
|
210
|
+
|
|
211
|
+
const keyStore = createInMemorySubjectKeyStore(); // dev only, see the warning below
|
|
76
212
|
|
|
77
|
-
|
|
213
|
+
const audit = createAuditMiddleware({
|
|
214
|
+
signingKey,
|
|
215
|
+
sensitivityResolver,
|
|
216
|
+
keyStore,
|
|
217
|
+
onEvent: (event) => sink.write(event),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// An erasure request, in full:
|
|
221
|
+
const tombstone = await keyStore.destroy('user-42');
|
|
222
|
+
// → { destroyedAt: '2026-08-29T09:12:04.118Z' }
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Omit `keyStore` and nothing changes.**
|
|
226
|
+
Default mode is byte-identical to what it has always been: the same preimages, the same ciphertexts, the same plain `sha256` payload hashes, and no `erasable` marker on any event.
|
|
227
|
+
Erasable mode is opt-in per middleware instance, and the two modes never interfere.
|
|
228
|
+
|
|
229
|
+
### The unit of erasure is the data subject
|
|
230
|
+
|
|
231
|
+
One random 256-bit key per subject, created on first use, where the subject is the event's resolved `identity.sub`.
|
|
232
|
+
Events with no resolved identity share the `anonymous` bucket, so one `destroy('anonymous')` erases all of them together.
|
|
233
|
+
Erasure requests arrive per data subject, so that is the unit; anything finer multiplies stored keys without matching a regulatory unit.
|
|
234
|
+
|
|
235
|
+
The key is fetched before every audited call, at the same pre-call stage as subkey derivation, and for the same reason: with no key there is no way to record the event, so the call fails fast rather than running unaudited.
|
|
236
|
+
A store outage therefore refuses calls and recovers on its own once the store returns; nothing is cached, so `destroy` takes effect on the very next call.
|
|
237
|
+
|
|
238
|
+
### What erasure destroys, and what survives
|
|
239
|
+
|
|
240
|
+
`destroy(subjectId)` removes the key. That destroys two things at once:
|
|
241
|
+
|
|
242
|
+
- **Decryptability.** High-tier `inputEncrypted` / `outputEncrypted` were sealed under a key derived from the subject key, so they become permanently unreadable. Nobody can recover them, the operator included.
|
|
243
|
+
- **Hash-confirmability.** This is the part a naive design misses. Default `inputHash` / `outputHash` are deterministic and unkeyed, so an adversary holding a *candidate* payload can confirm after erasure that a subject's event contained it. Salting does not help, because a stored salt reproduces the same confirmation. In erasable mode the hashes are `HMAC-SHA256` under a hash subkey of the subject key, so destroying the key destroys the ability to confirm a guess.
|
|
244
|
+
|
|
245
|
+
What survives is everything the trail is for:
|
|
246
|
+
|
|
247
|
+
- The **chain** verifies, at every index, before and after erasure. `verifyAuditChain` is unaffected.
|
|
248
|
+
- The **`ReplayManifest`** verifies, and its format is unchanged. Erasure is deliberately not recorded in the chain or the manifest: doing so would make the trail assert facts about key custody it cannot verify.
|
|
249
|
+
- Every covered field stays readable. Who called what, when, with what outcome, at which tier, under which proxy, is all still there. Only the payload bodies go dark.
|
|
250
|
+
|
|
251
|
+
Two consequences to be explicit about:
|
|
78
252
|
|
|
79
|
-
|
|
253
|
+
- **Erasure trades replay completeness for compliance.** An erased event's payload cannot be replayed or re-verified against a copy. That is the point of the feature, not a limitation of it.
|
|
254
|
+
- **Low- and medium-tier payloads are stored as plaintext** and are not protected by any key, so erasing a subject key does not remove them. Classify anything an erasure request must reach as `high`.
|
|
255
|
+
|
|
256
|
+
An erased event is indistinguishable, at the format level, from an event whose key custodian simply lost the key.
|
|
257
|
+
Accountability for the erasure itself lives in the tombstone the store returns, outside the chain.
|
|
258
|
+
|
|
259
|
+
### Custody is yours
|
|
260
|
+
|
|
261
|
+
> **In erasable mode the `SubjectKeyStore` *is* the confidentiality of every high-tier payload it holds a key for.**
|
|
262
|
+
> A store that keeps keys in the clear, or that anyone with read access to the event sink can also read, voids the encryption guarantee for every subject in it.
|
|
263
|
+
> A store that silently loses keys performs an erasure nobody asked for, and the loss is invisible until someone tries to read a payload back.
|
|
264
|
+
|
|
265
|
+
`createInMemorySubjectKeyStore()` is the reference implementation and is **not durable**: keys live in one process's heap, so a restart erases every subject and a second proxy instance cannot read what the first recorded.
|
|
266
|
+
Use it in development and tests.
|
|
267
|
+
A real deployment implements the two-method interface against a KMS, an HSM, or an encrypted, access-controlled table whose read path is itself audited, with the same handling rules as the signing secret.
|
|
268
|
+
|
|
269
|
+
### What a decryptor has to know
|
|
270
|
+
|
|
271
|
+
Erasable mode is visible on the record: an event produced under it carries `erasable: true`.
|
|
272
|
+
That marker is covered by the chain as an optional field (ADR-0012), so a stored event cannot be silently reinterpreted under the wrong hash scheme, and adding or stripping it fails verification at that index.
|
|
273
|
+
|
|
274
|
+
A tool that reads payloads back therefore branches on it:
|
|
275
|
+
|
|
276
|
+
| | `erasable` absent | `erasable: true` |
|
|
277
|
+
|---|---|---|
|
|
278
|
+
| Event key root | `encRoot = sign('mcpose/v2/enc')` | the subject's stored key |
|
|
279
|
+
| `inputHash` / `outputHash` | `sha256(stableStringify(payload))` | `HMAC(hashSubkey, stableStringify(payload))`, where `hashSubkey = HMAC(subjectKey, 'mcpose/v2/hashkey')` |
|
|
280
|
+
| Needs the signing secret | yes | yes, for the chain |
|
|
281
|
+
| Needs the subject key | no | yes, for the payloads |
|
|
282
|
+
|
|
283
|
+
The event-key derivation, the AES-256-GCM layout, and the AAD are identical in both modes.
|
|
284
|
+
Only the root the key comes from changes.
|
|
80
285
|
|
|
81
286
|
## API surface
|
|
82
287
|
|
|
83
288
|
| Export | Purpose |
|
|
84
289
|
|---|---|
|
|
85
|
-
| `createAuditMiddleware(options)` | Returns `{ middleware, closeSession }`. Add `middleware` to
|
|
86
|
-
| `createSensitivityResolver(map, override?)` | Build a `SensitivityResolverFn
|
|
290
|
+
| `createAuditMiddleware(options)` | Returns `{ middleware, promptMiddleware, closeSession }`. Add `middleware` to `toolMiddleware` and `promptMiddleware` to `promptMiddleware`; call `closeSession(sessionId)` to emit the manifest. |
|
|
291
|
+
| `createSensitivityResolver(map, override?)` | Build a `SensitivityResolverFn`. Unknown or invalid tiers resolve to `high`. |
|
|
87
292
|
| `createDefaultSigningKeyProvider(secret)` | In-process HMAC-SHA256 `SigningKeyProvider`. |
|
|
293
|
+
| `createInMemorySubjectKeyStore()` | Reference `SubjectKeyStore` for [erasable mode](#erasable-mode-cryptographic-erasure). **Not durable**: development and tests only. |
|
|
294
|
+
| `verifyAuditChain(events, signingKey)` | **Keyed** chain verification: recomputes every `chainHash` and reports the first tampered index. An empty event list is invalid. A stored `chainHash` must be the exact lowercase hex string; a respelling is a mismatch. |
|
|
295
|
+
| `verifyManifestSignature(manifest, signingKey)` | **Keyed** check of the full-manifest signature, in constant time. The stored `signature` must be the exact lowercase hex string; a respelling (uppercase, an appended character) does not verify, so an independent verifier that compares strings agrees with this one. |
|
|
88
296
|
| `computeMerkleRoot` · `computeMerkleProof` · `verifyMerkleProof` | Low-level Merkle helpers for independent verification. |
|
|
297
|
+
| `canonicalJson` · `stableStringify` | The canonical serializations the format is defined over, exported so third parties can write their own verifier. |
|
|
89
298
|
|
|
90
|
-
**Key types:** `AuditEvent` (`LowAuditEvent` \| `MediumAuditEvent` \| `HighAuditEvent`), `AuditEventBase`, `SensitivityTier`, `SensitivityResolverFn`, `SigningKeyProvider`, `AuditOptions`, `AuditMiddlewareHandle`, `ReplayManifest`, `MerkleProof`, `
|
|
299
|
+
**Key types:** `AuditEvent` (`LowAuditEvent` \| `MediumAuditEvent` \| `HighAuditEvent`), `AuditEventBase`, `SensitivityTier`, `SensitivityResolverFn`, `SensitivityOverrideFn`, `SigningKeyProvider`, `SubjectKeyStore`, `SubjectKeyTombstone`, `AuditOptions`, `AuditMiddlewareHandle`, `ReplayManifest`, `MerkleProof`, `ChainVerification`.
|
|
91
300
|
|
|
92
|
-
### `
|
|
301
|
+
### `createAuditMiddleware(options)`
|
|
302
|
+
|
|
303
|
+
<details>
|
|
304
|
+
<summary>Show <code>AuditOptions</code> and <code>AuditMiddlewareHandle</code></summary>
|
|
93
305
|
|
|
94
306
|
```ts
|
|
95
307
|
interface AuditOptions {
|
|
96
308
|
signingKey: SigningKeyProvider;
|
|
97
309
|
sensitivityResolver: SensitivityResolverFn;
|
|
98
310
|
onEvent: (event: AuditEvent) => void | Promise<void>;
|
|
311
|
+
/**
|
|
312
|
+
* Supply a store to run in ERASABLE mode; omit it and behaviour is
|
|
313
|
+
* byte-identical to default mode. See "Erasable mode" above.
|
|
314
|
+
*/
|
|
315
|
+
keyStore?: SubjectKeyStore;
|
|
316
|
+
/**
|
|
317
|
+
* Called with the finished ReplayManifest when closeSession() is invoked.
|
|
318
|
+
*
|
|
319
|
+
* Why this exists: ToolMiddleware is a pure per-request function with no
|
|
320
|
+
* lifecycle hooks, and sessions are owned by the HTTP transport rather
|
|
321
|
+
* than by middleware. The host signals session end via closeSession();
|
|
322
|
+
* onManifest is the push-based delivery mechanism for the result.
|
|
323
|
+
*/
|
|
99
324
|
onManifest?: (manifest: ReplayManifest) => void | Promise<void>;
|
|
100
|
-
|
|
101
|
-
includeRejections?: boolean;
|
|
102
|
-
|
|
325
|
+
/** Record events for rejected calls (hidden tools etc.). Default: true. */
|
|
326
|
+
includeRejections?: boolean;
|
|
327
|
+
/**
|
|
328
|
+
* Called when the audit layer itself fails (event serialization, a
|
|
329
|
+
* throwing onEvent sink). The audit layer NEVER throws into the
|
|
330
|
+
* tool-call path. Default: console.error.
|
|
331
|
+
*/
|
|
332
|
+
onAuditError?: (
|
|
333
|
+
err: unknown,
|
|
334
|
+
info: { tool: string; requestId: string; sessionId?: string },
|
|
335
|
+
) => unknown;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
interface AuditMiddlewareHandle {
|
|
339
|
+
middleware: ToolMiddleware;
|
|
340
|
+
/** Audits prompts/get. Wire into ProxyOptions.promptMiddleware. */
|
|
341
|
+
promptMiddleware: PromptMiddleware;
|
|
342
|
+
closeSession(sessionId: string): Promise<ReplayManifest | undefined>;
|
|
103
343
|
}
|
|
104
344
|
```
|
|
105
345
|
|
|
106
|
-
|
|
346
|
+
</details>
|
|
347
|
+
|
|
348
|
+
`closeSession` returns `undefined` if the session had no events or is unknown; wire it to `HttpProxyOptions.onSessionClosed`.
|
|
349
|
+
The proxy awaits the returned promise, reports a rejection through `HttpProxyOptions.onError`, and holds `server.close()` open until every session's manifest has settled.
|
|
350
|
+
Await the close before exiting the process, or the final manifest can be lost.
|
|
351
|
+
The returned `middleware` is already marked as a pass-through observer, so tools in `passThroughTools` stay audited with no extra setup.
|
|
352
|
+
|
|
353
|
+
`promptMiddleware` audits `prompts/get` calls and shares the session chain with `middleware`, so tool and prompt events interleave in one trail and one manifest.
|
|
354
|
+
Wire it alongside the tool middleware:
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
{
|
|
358
|
+
toolMiddleware: [piiMW, auditHandle.middleware],
|
|
359
|
+
promptMiddleware: [auditHandle.promptMiddleware],
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Prompt events carry `kind: 'prompt'`, with the prompt name in `tool`; an event with no `kind` is a tool call (ADR-0014).
|
|
364
|
+
In mesh mode the `BACKEND_UNROUTABLE` rejection for an unroutable prompt name is thrown inside the pipeline, so it is audited like any other rejection.
|
|
365
|
+
The sensitivity resolver receives the prompt name as its first argument, so a name-keyed map that does not list a prompt resolves it to `high` and encrypts it.
|
|
366
|
+
|
|
367
|
+
The handle is a triple rather than a bare middleware for a structural reason: middleware is per-request and has no lifecycle, so session end has to be signalled from outside, and the tool and prompt surfaces are separate pipelines in core.
|
|
368
|
+
|
|
369
|
+
### `createSensitivityResolver(map, override?)`
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
const resolver = createSensitivityResolver(
|
|
373
|
+
{ get_balance: 'low', search: 'medium' },
|
|
374
|
+
// Optional override, which takes precedence over the static map.
|
|
375
|
+
// The 4th argument is the map's own resolution (already defaulted to
|
|
376
|
+
// 'high' for unknown tools), so the override can fall back to it.
|
|
377
|
+
(tool, identity, args, mapTier) =>
|
|
378
|
+
identity.roles.includes('admin') ? 'low' : mapTier,
|
|
379
|
+
);
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The override function type is exported as `SensitivityOverrideFn`.
|
|
383
|
+
|
|
384
|
+
**This resolver fails closed on purpose.**
|
|
385
|
+
Tool names are attacker-controlled, so lookup uses `Object.hasOwn` (a tool named `toString` must not inherit a prototype value and bypass the default), tier values are validated, and anything unknown or malformed resolves to `'high'`.
|
|
386
|
+
Only an explicit `'low'` or `'medium'` results in plaintext storage.
|
|
387
|
+
|
|
388
|
+
### `createDefaultSigningKeyProvider(secret)`
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
const signingKey = createDefaultSigningKeyProvider(process.env.AUDIT_SECRET!);
|
|
392
|
+
// → { algorithm: 'HMAC-SHA256', keyId, sign(data) }
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
`keyId` is derived as a domain-separated HMAC of the secret, not a bare hash of it.
|
|
396
|
+
That distinction matters: `keyId` is published in every manifest, and a bare digest would let anyone holding a manifest brute-force a low-entropy secret offline.
|
|
397
|
+
It remains **public** either way, so it is never key material.
|
|
398
|
+
|
|
399
|
+
### `SubjectKeyStore`
|
|
400
|
+
|
|
401
|
+
Custody of the destroyable per-subject keys that make [erasable mode](#erasable-mode-cryptographic-erasure) erasable.
|
|
402
|
+
Two methods, both async so a real store can do I/O:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
interface SubjectKeyStore {
|
|
406
|
+
/** The subject's 256-bit key, created at random on first use. */
|
|
407
|
+
getOrCreate(subjectId: string): Promise<Buffer>;
|
|
408
|
+
/** Permanently removes it, and returns the evidence that it happened. */
|
|
409
|
+
destroy(subjectId: string): Promise<{ destroyedAt: string }>;
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
- `getOrCreate` is called before every audited call, so it must not memoize past a `destroy`: a subject that calls again after erasure gets a **fresh** key, and the old events stay dead. A rejection fails the audited call rather than letting it run unaudited.
|
|
414
|
+
- `destroy` is **idempotent**. Destroying a subject that holds no key still returns a tombstone, because "this subject holds no key" is the state the caller asked for either way, and a repeated erasure request should produce evidence rather than an error.
|
|
415
|
+
- The key must be **random**, never derived from the signing secret or from the subject id. A recomputable key cannot be destroyed, so destroying it would be theatre.
|
|
416
|
+
|
|
417
|
+
`createInMemorySubjectKeyStore()` implements this over a `Map` for development and tests.
|
|
418
|
+
It is not durable, and it is not access-controlled; see [Custody is yours](#custody-is-yours).
|
|
419
|
+
|
|
420
|
+
### `AuditEvent`
|
|
421
|
+
|
|
422
|
+
A discriminated union on `sensitivityTier`, sharing one base record.
|
|
423
|
+
|
|
424
|
+
| Tier | Stored fields |
|
|
425
|
+
|---|---|
|
|
426
|
+
| `'low'` | `inputRaw`, `outputRaw` (plaintext) |
|
|
427
|
+
| `'medium'` | `inputRaw`, `outputRaw` (plaintext; PII already redacted upstream) |
|
|
428
|
+
| `'high'` | `inputEncrypted`, `outputEncrypted` (AES-256-GCM, per-event key) |
|
|
429
|
+
|
|
430
|
+
<details>
|
|
431
|
+
<summary>Show the <code>AuditEventBase</code> schema</summary>
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
type AuditEvent = LowAuditEvent | MediumAuditEvent | HighAuditEvent;
|
|
435
|
+
|
|
436
|
+
interface AuditEventBase {
|
|
437
|
+
id: string; // = ProxyContext.requestId
|
|
438
|
+
startedAt: string; // ISO timestamp, captured before the upstream call
|
|
439
|
+
endedAt: string; // ISO timestamp, captured after it settled
|
|
440
|
+
sessionId?: string;
|
|
441
|
+
identity: Identity;
|
|
442
|
+
delegatedFrom?: Identity[];
|
|
443
|
+
proxy: ProxyIdentity; // { name, version } of the recording proxy instance
|
|
444
|
+
kind?: 'prompt'; // present only on prompt events; absent means a tool call
|
|
445
|
+
erasable?: true; // present only on events recorded in erasable mode
|
|
446
|
+
tool: string; // the tool name, or the prompt name when kind is 'prompt'
|
|
447
|
+
duration_ms: number;
|
|
448
|
+
outcome: 'success' | 'rejected' | 'error';
|
|
449
|
+
/**
|
|
450
|
+
* Present when outcome is 'rejected': a member of the `RejectionReason` union read from the
|
|
451
|
+
* MCP error's data field. An unrecognized value is recorded as outcome 'error' instead.
|
|
452
|
+
*/
|
|
453
|
+
rejectionReason?: RejectionReason;
|
|
454
|
+
/** Present when outcome is 'error': what the upstream call threw. */
|
|
455
|
+
error?: { name: string; message: string };
|
|
456
|
+
inputHash: string; // SHA-256 over a stable serialization, or a keyed HMAC when erasable
|
|
457
|
+
outputHash: string;
|
|
458
|
+
chainHash: string; // HMAC over the canonical preimage
|
|
459
|
+
replayManifestPosition: number;
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
</details>
|
|
464
|
+
|
|
465
|
+
A missing `ctx.identity` degrades to an anonymous identity rather than failing the call.
|
|
466
|
+
|
|
467
|
+
### `ReplayManifest`
|
|
468
|
+
|
|
469
|
+
Produced at session close, covering every audit event with a Merkle root and individual proofs, signed by the `SigningKeyProvider`.
|
|
470
|
+
Any third party can verify a single event without access to the full log.
|
|
471
|
+
|
|
472
|
+
<details>
|
|
473
|
+
<summary>Show the <code>ReplayManifest</code> interface</summary>
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
interface ReplayManifest {
|
|
477
|
+
sessionId: string;
|
|
478
|
+
identity: Identity;
|
|
479
|
+
proxy: ProxyIdentity; // { name, version } of the producing proxy instance
|
|
480
|
+
startedAt: string;
|
|
481
|
+
closedAt: string;
|
|
482
|
+
eventCount: number;
|
|
483
|
+
merkleRoot: string;
|
|
484
|
+
merkleProofs: MerkleProof[];
|
|
485
|
+
signedBy: string; // public keyId
|
|
486
|
+
signature: string; // HMAC over the canonical serialization of the ENTIRE manifest
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
</details>
|
|
491
|
+
|
|
492
|
+
Merkle leaves and internal nodes are hashed under **different** domain labels, so an internal node can never be presented as a leaf.
|
|
493
|
+
Odd layers duplicate the last node, and `closeSession` never signs an empty manifest.
|
|
494
|
+
|
|
495
|
+
### Verification
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
import { verifyAuditChain, verifyManifestSignature } from '@mcpose/audit';
|
|
499
|
+
|
|
500
|
+
const result = await verifyAuditChain(events, signingKey);
|
|
501
|
+
if (!result.valid) throw new Error(`Chain broken at index ${result.firstInvalidIndex}`);
|
|
502
|
+
|
|
503
|
+
if (!(await verifyManifestSignature(manifest, signingKey))) {
|
|
504
|
+
throw new Error('Manifest signature does not verify');
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
These are the **keyed** verifiers and the only ones that prove authenticity.
|
|
509
|
+
Use them wherever the signing secret is available: a verification job, a compliance export, an incident investigation.
|
|
510
|
+
In test suites, where the secret usually is not available, use [`@mcpose/testing`](#testing) and read its stated limits.
|
|
107
511
|
|
|
108
512
|
## Testing
|
|
109
513
|
|
|
110
|
-
Verify chains and manifests in your test suite with [`@mcpose/testing`](https://www.npmjs.com/package/@mcpose/testing)
|
|
514
|
+
Verify chains and manifests in your test suite with [`@mcpose/testing`](https://www.npmjs.com/package/@mcpose/testing): `assertAuditChainIntegrity`, `assertReplayManifestValid`, `assertPiiRedacted`, and `assertDelegationHonored`.
|
|
515
|
+
|
|
516
|
+
Those assertions are deliberately keyless and prove internal consistency, not authenticity.
|
|
517
|
+
Pair them with `verifyAuditChain` and `verifyManifestSignature` anywhere the secret *is* available.
|
|
518
|
+
|
|
519
|
+
For a reproducible Merkle scaling comparison after building from the repository root, run `pnpm --filter @mcpose/audit benchmark:merkle`.
|
|
520
|
+
The benchmark compares the legacy root-plus-per-proof path with the one-tree manifest path at 500, 1000, and 2000 events, and checks root and proof equality before reporting.
|
|
521
|
+
Timings vary by runtime and hardware; use them to compare scaling, not to predict request latency.
|
|
111
522
|
|
|
112
523
|
## Documentation
|
|
113
524
|
|
|
114
|
-
- [
|
|
115
|
-
- [ADR-0003
|
|
116
|
-
- [
|
|
525
|
+
- [Project README](https://github.com/amir-gorji/mcpose#readme): concepts, comparison, and guides
|
|
526
|
+
- [ADR-0003: audit subkeys derived from the signing oracle](https://github.com/amir-gorji/mcpose/blob/main/docs/adr/0003-audit-subkeys-derived-from-signing-oracle.md)
|
|
527
|
+
- [ADR-0004: audit format v2 and canonical serialization](https://github.com/amir-gorji/mcpose/blob/main/docs/adr/0004-audit-format-v2-canonical-serialization.md)
|
|
528
|
+
- [`CONTEXT.md`](https://github.com/amir-gorji/mcpose/blob/main/CONTEXT.md): the canonical domain glossary
|
|
117
529
|
|
|
118
530
|
## License
|
|
119
531
|
|
package/dist/chain.d.ts
CHANGED
|
@@ -1,12 +1,55 @@
|
|
|
1
|
-
import type { MerkleProof } from './types.js';
|
|
1
|
+
import type { AuditEvent, MerkleProof } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Canonical JSON: object keys sorted lexicographically at every depth,
|
|
4
|
+
* `undefined`-valued keys skipped. Throws on values with no canonical JSON
|
|
5
|
+
* form (BigInt, function, symbol, circular references).
|
|
6
|
+
*
|
|
7
|
+
* Use for data the audit layer controls (chain preimages, manifest
|
|
8
|
+
* payloads). For arbitrary caller payloads use {@link stableStringify}.
|
|
9
|
+
*/
|
|
10
|
+
export declare function canonicalJson(value: unknown): string;
|
|
11
|
+
/**
|
|
12
|
+
* Total variant of {@link canonicalJson} for arbitrary caller payloads:
|
|
13
|
+
* never throws. Circular references become `"[Circular]"`, BigInt becomes
|
|
14
|
+
* its decimal string. Used for `inputHash`/`outputHash`, so two
|
|
15
|
+
* semantically identical payloads hash identically regardless of the
|
|
16
|
+
* client's JSON key order.
|
|
17
|
+
*/
|
|
18
|
+
export declare function stableStringify(value: unknown): string;
|
|
2
19
|
export declare function sha256hex(data: string | Buffer): string;
|
|
3
|
-
export declare function hmacSha256hex(data: string | Buffer, key: Buffer): string;
|
|
4
20
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
21
|
+
* The exact fields of an audit event that the chain covers, extracted from
|
|
22
|
+
* the event. Used by both the producer (middleware) and any verifier
|
|
23
|
+
* ({@link verifyAuditChain}) so the two cannot drift.
|
|
24
|
+
*
|
|
25
|
+
* NOT covered (bound only via inputHash/outputHash): the raw/encrypted
|
|
26
|
+
* payloads.
|
|
7
27
|
*/
|
|
8
|
-
export declare function
|
|
28
|
+
export declare function chainPreimageFields(event: Omit<AuditEvent, 'chainHash'> | AuditEvent): Record<string, unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* `HMAC-SHA256(chainKey, canonicalJson({domain, prevChainHash, event}))`.
|
|
31
|
+
* Domain-separated and canonically framed: neither field order nor
|
|
32
|
+
* string-concatenation ambiguity can affect the hash. The entry is
|
|
33
|
+
* serialized without its own chainHash. First entry uses `prevChainHash: ''`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function computeChainHash(entryWithoutChainHash: Record<string, unknown>, prevChainHash: string, chainKey: Buffer): string;
|
|
36
|
+
/** Merkle root over the events' `chainHash` values. Empty → `sha256('')`. */
|
|
9
37
|
export declare function computeMerkleRoot(hashes: string[]): string;
|
|
10
38
|
export declare function computeMerkleProof(hashes: string[], index: number): MerkleProof;
|
|
11
|
-
|
|
39
|
+
/** Build a manifest root and every proof from the same set of tree layers. */
|
|
40
|
+
export declare function computeMerkleRootAndProofs(hashes: string[]): {
|
|
41
|
+
merkleRoot: string;
|
|
42
|
+
merkleProofs: MerkleProof[];
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Verifies a Merkle proof for an event's `chainHash` (the raw chain hash —
|
|
46
|
+
* the leaf tag is applied internally).
|
|
47
|
+
*
|
|
48
|
+
* Rejects malformed proofs: `siblings`/`directions` length mismatch,
|
|
49
|
+
* directions inconsistent with `proof.index` (each level's direction is
|
|
50
|
+
* fully determined by the index), or an index that the path cannot
|
|
51
|
+
* represent (`index >= 2 ** siblings.length`, which leaves high bits
|
|
52
|
+
* unconsumed after traversal).
|
|
53
|
+
*/
|
|
54
|
+
export declare function verifyMerkleProof(leafChainHash: string, proof: MerkleProof, root: string): boolean;
|
|
12
55
|
//# sourceMappingURL=chain.d.ts.map
|
package/dist/chain.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chain.d.ts","sourceRoot":"","sources":["../src/chain.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"chain.d.ts","sourceRoot":"","sources":["../src/chain.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAU1D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtD;AAwDD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAEvD;AAUD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,GAAG,UAAU,GAChD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoCzB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9C,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GACf,MAAM,CAOR;AAiCD,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ1D;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EAAE,EAChB,KAAK,EAAE,MAAM,GACZ,WAAW,CAyBb;AAkBD,8EAA8E;AAC9E,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B,CAcA;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,MAAM,GACX,OAAO,CA2BT"}
|