@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,72 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createSensitivityResolver } from '../sensitivity.js';
3
+ import type { Identity } from 'mcpose';
4
+
5
+ const identity: Identity = {
6
+ sub: 'u1',
7
+ type: 'human',
8
+ roles: [],
9
+ claims: {},
10
+ resolvedAt: new Date().toISOString(),
11
+ source: 'jwt',
12
+ };
13
+
14
+ describe('createSensitivityResolver', () => {
15
+ it('returns the mapped tier for a known tool', () => {
16
+ const resolve = createSensitivityResolver({
17
+ transfer_funds: 'high',
18
+ get_balance: 'low',
19
+ });
20
+ expect(resolve('transfer_funds', identity, {})).toBe('high');
21
+ expect(resolve('get_balance', identity, {})).toBe('low');
22
+ });
23
+
24
+ it('returns high for an unknown tool', () => {
25
+ const resolve = createSensitivityResolver({ get_balance: 'low' });
26
+ expect(resolve('unknown_tool', identity, {})).toBe('high');
27
+ });
28
+
29
+ it('override fn takes precedence over the static map', () => {
30
+ const resolve = createSensitivityResolver(
31
+ { get_balance: 'low' },
32
+ (_tool, _identity, args) => (args.pii ? 'high' : 'low'),
33
+ );
34
+ expect(resolve('get_balance', identity, { pii: true })).toBe('high');
35
+ expect(resolve('get_balance', identity, {})).toBe('low');
36
+ });
37
+
38
+ it('override receives the map tier and can fall back to it', () => {
39
+ const resolve = createSensitivityResolver(
40
+ { get_balance: 'medium' },
41
+ (_tool, _identity, _args, mapTier) => mapTier,
42
+ );
43
+ expect(resolve('get_balance', identity, {})).toBe('medium');
44
+ // Unknown tool: mapTier is already defaulted to high.
45
+ expect(resolve('unknown_tool', identity, {})).toBe('high');
46
+ });
47
+
48
+ it('resolves prototype-inherited tool names to high (attacker-controlled names)', () => {
49
+ const resolve = createSensitivityResolver({ get_balance: 'low' });
50
+ // These exist on Object.prototype — a naive map lookup returns a
51
+ // truthy Function, bypassing the unknown⇒high default.
52
+ for (const tool of [
53
+ 'toString',
54
+ 'constructor',
55
+ 'valueOf',
56
+ 'hasOwnProperty',
57
+ '__proto__',
58
+ ]) {
59
+ expect(resolve(tool, identity, {})).toBe('high');
60
+ }
61
+ });
62
+
63
+ it('treats a garbage tier from the map or override as high (fail closed)', () => {
64
+ const badMap = createSensitivityResolver({
65
+ typo_tool: 'pubic' as never,
66
+ });
67
+ expect(badMap('typo_tool', identity, {})).toBe('high');
68
+
69
+ const badOverride = createSensitivityResolver({}, () => undefined as never);
70
+ expect(badOverride('anything', identity, {})).toBe('high');
71
+ });
72
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createHash, createHmac } from 'node:crypto';
3
+ import { createDefaultSigningKeyProvider } from '../signingKey.js';
4
+
5
+ describe('createDefaultSigningKeyProvider', () => {
6
+ it('has correct shape', () => {
7
+ const provider = createDefaultSigningKeyProvider('secret');
8
+ expect(provider.algorithm).toBe('HMAC-SHA256');
9
+ expect(typeof provider.keyId).toBe('string');
10
+ expect(provider.keyId.length).toBeGreaterThan(0);
11
+ expect(typeof provider.sign).toBe('function');
12
+ });
13
+
14
+ it('derives keyId as HMAC(secret, mcpose/v2/keyid), never bare SHA256(secret)', () => {
15
+ // v1 published SHA256(secret) as keyId, enabling offline brute-force of
16
+ // low-entropy secrets from any manifest. Pin the v2 formula and the label.
17
+ const provider = createDefaultSigningKeyProvider('secret');
18
+ expect(provider.keyId).toBe(
19
+ createHmac('sha256', Buffer.from('secret'))
20
+ .update('mcpose/v2/keyid')
21
+ .digest('hex'),
22
+ );
23
+ expect(provider.keyId).not.toBe(
24
+ createHash('sha256').update('secret').digest('hex'),
25
+ );
26
+ });
27
+
28
+ it('sign returns a non-empty buffer', async () => {
29
+ const provider = createDefaultSigningKeyProvider('secret');
30
+ const sig = await provider.sign(Buffer.from('hello'));
31
+ expect(sig).toBeInstanceOf(Buffer);
32
+ expect(sig.length).toBeGreaterThan(0);
33
+ });
34
+
35
+ it('same secret + same data → same signature (deterministic)', async () => {
36
+ const p1 = createDefaultSigningKeyProvider('secret');
37
+ const p2 = createDefaultSigningKeyProvider('secret');
38
+ const data = Buffer.from('test-data');
39
+ const [s1, s2] = await Promise.all([p1.sign(data), p2.sign(data)]);
40
+ expect(s1.toString('hex')).toBe(s2.toString('hex'));
41
+ });
42
+
43
+ it('different secrets → different signatures', async () => {
44
+ const p1 = createDefaultSigningKeyProvider('secret-a');
45
+ const p2 = createDefaultSigningKeyProvider('secret-b');
46
+ const data = Buffer.from('test-data');
47
+ const [s1, s2] = await Promise.all([p1.sign(data), p2.sign(data)]);
48
+ expect(s1.toString('hex')).not.toBe(s2.toString('hex'));
49
+ });
50
+
51
+ it('accepts a Buffer secret and keys it identically to its utf8 string form', async () => {
52
+ // Pins why the provider hands the secret to createHmac as given rather
53
+ // than normalizing a string to a Buffer first: createHmac encodes a
54
+ // string key as utf8 itself, so the conversion was unobservable and its
55
+ // mutants unkillable (#110). A non-ASCII secret makes the encoding
56
+ // load-bearing rather than incidental.
57
+ const text = 'sécret';
58
+ const fromString = createDefaultSigningKeyProvider(text);
59
+ const fromBuffer = createDefaultSigningKeyProvider(Buffer.from(text));
60
+
61
+ expect(fromBuffer.keyId).toBe(fromString.keyId);
62
+ expect(fromBuffer.keyId).toBe(
63
+ createHmac('sha256', Buffer.from(text, 'utf8'))
64
+ .update('mcpose/v2/keyid')
65
+ .digest('hex'),
66
+ );
67
+
68
+ const data = Buffer.from('data');
69
+ const [fromBufferSig, fromStringSig] = await Promise.all([
70
+ fromBuffer.sign(data),
71
+ fromString.sign(data),
72
+ ]);
73
+ expect(fromBufferSig).toBeInstanceOf(Buffer);
74
+ expect(fromBufferSig.toString('hex')).toBe(fromStringSig.toString('hex'));
75
+ });
76
+ });
package/src/chain.ts ADDED
@@ -0,0 +1,317 @@
1
+ import { createHash, createHmac } from 'node:crypto';
2
+ import type { AuditEvent, MerkleProof } from './types.js';
3
+
4
+ // ── Canonical serialization ────────────────────────────────────────────────────
5
+ //
6
+ // v1 hashed `JSON.stringify` output, which made object-key insertion order
7
+ // load-bearing: an independently written verifier that reconstructed the
8
+ // fields in a different order failed on every chainHash. v2 hashes a
9
+ // canonical form instead — lexicographically sorted keys at every depth —
10
+ // so only the field SET matters. See ADR-0004.
11
+
12
+ /**
13
+ * Canonical JSON: object keys sorted lexicographically at every depth,
14
+ * `undefined`-valued keys skipped. Throws on values with no canonical JSON
15
+ * form (BigInt, function, symbol, circular references).
16
+ *
17
+ * Use for data the audit layer controls (chain preimages, manifest
18
+ * payloads). For arbitrary caller payloads use {@link stableStringify}.
19
+ */
20
+ export function canonicalJson(value: unknown): string {
21
+ return serialize(value, new Set(), { strict: true });
22
+ }
23
+
24
+ /**
25
+ * Total variant of {@link canonicalJson} for arbitrary caller payloads:
26
+ * never throws. Circular references become `"[Circular]"`, BigInt becomes
27
+ * its decimal string. Used for `inputHash`/`outputHash`, so two
28
+ * semantically identical payloads hash identically regardless of the
29
+ * client's JSON key order.
30
+ */
31
+ export function stableStringify(value: unknown): string {
32
+ return serialize(value, new Set(), { strict: false });
33
+ }
34
+
35
+ function serialize(
36
+ value: unknown,
37
+ seen: Set<object>,
38
+ opts: { strict: boolean },
39
+ ): string {
40
+ if (value === null || value === undefined) return 'null';
41
+
42
+ switch (typeof value) {
43
+ case 'string':
44
+ case 'number':
45
+ case 'boolean':
46
+ return JSON.stringify(value);
47
+ case 'bigint':
48
+ if (opts.strict) {
49
+ throw new TypeError('canonicalJson: BigInt has no canonical JSON form');
50
+ }
51
+ return JSON.stringify(value.toString());
52
+ case 'function':
53
+ case 'symbol':
54
+ if (opts.strict) {
55
+ throw new TypeError(
56
+ `canonicalJson: ${typeof value} has no canonical JSON form`,
57
+ );
58
+ }
59
+ return 'null';
60
+ }
61
+
62
+ const obj = value;
63
+ if (seen.has(obj)) {
64
+ if (opts.strict) {
65
+ throw new TypeError('canonicalJson: circular reference');
66
+ }
67
+ return '"[Circular]"';
68
+ }
69
+ seen.add(obj);
70
+ try {
71
+ if (Array.isArray(obj)) {
72
+ return `[${obj.map((item) => serialize(item, seen, opts)).join(',')}]`;
73
+ }
74
+ if (typeof (obj as { toJSON?: unknown }).toJSON === 'function') {
75
+ return serialize((obj as { toJSON: () => unknown }).toJSON(), seen, opts);
76
+ }
77
+ const entries = Object.entries(obj)
78
+ .filter(([, v]) => v !== undefined)
79
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
80
+ .map(([k, v]) => `${JSON.stringify(k)}:${serialize(v, seen, opts)}`);
81
+ return `{${entries.join(',')}}`;
82
+ } finally {
83
+ seen.delete(obj);
84
+ }
85
+ }
86
+
87
+ // ── Hash primitives ────────────────────────────────────────────────────────────
88
+
89
+ export function sha256hex(data: string | Buffer): string {
90
+ return createHash('sha256').update(data).digest('hex');
91
+ }
92
+
93
+ function hmacSha256hex(data: string | Buffer, key: Buffer): string {
94
+ return createHmac('sha256', key).update(data).digest('hex');
95
+ }
96
+
97
+ // ── HMAC chain ─────────────────────────────────────────────────────────────────
98
+
99
+ const DOMAIN_CHAIN_HASH = 'mcpose/v2/chain';
100
+
101
+ /**
102
+ * The exact fields of an audit event that the chain covers, extracted from
103
+ * the event. Used by both the producer (middleware) and any verifier
104
+ * ({@link verifyAuditChain}) so the two cannot drift.
105
+ *
106
+ * NOT covered (bound only via inputHash/outputHash): the raw/encrypted
107
+ * payloads.
108
+ */
109
+ export function chainPreimageFields(
110
+ event: Omit<AuditEvent, 'chainHash'> | AuditEvent,
111
+ ): Record<string, unknown> {
112
+ return {
113
+ id: event.id,
114
+ startedAt: event.startedAt,
115
+ endedAt: event.endedAt,
116
+ ...(event.sessionId === undefined ? {} : { sessionId: event.sessionId }),
117
+ ...(event.delegatedFrom === undefined
118
+ ? {}
119
+ : { delegatedFrom: event.delegatedFrom }),
120
+ // Required on every event, so it is a plain always-present field rather
121
+ // than an omission-guarded optional one (ADR-0019). Under the omission
122
+ // pattern an event with no proxy and one whose proxy was stripped hashed
123
+ // identically, so removing recorded provenance was undetectable.
124
+ proxy: event.proxy,
125
+ ...(event.kind === undefined ? {} : { kind: event.kind }),
126
+ // Optional, omitted when absent (ADR-0012), so a default-mode event keeps
127
+ // the preimage it had before erasable mode existed. Covering it is what
128
+ // stops a stored event being reinterpreted under the other hash scheme:
129
+ // adding or stripping the marker breaks verification at that index
130
+ // (ADR-0018).
131
+ ...(event.erasable === undefined ? {} : { erasable: event.erasable }),
132
+ identity: event.identity,
133
+ tool: event.tool,
134
+ duration_ms: event.duration_ms,
135
+ outcome: event.outcome,
136
+ // Required on every event, so it is a plain always-present field rather
137
+ // than an omission-guarded optional one (ADR-0015).
138
+ sensitivityTier: event.sensitivityTier,
139
+ ...(event.rejectionReason === undefined
140
+ ? {}
141
+ : { rejectionReason: event.rejectionReason }),
142
+ ...(event.error === undefined ? {} : { error: event.error }),
143
+ inputHash: event.inputHash,
144
+ outputHash: event.outputHash,
145
+ replayManifestPosition: event.replayManifestPosition,
146
+ };
147
+ }
148
+
149
+ /**
150
+ * `HMAC-SHA256(chainKey, canonicalJson({domain, prevChainHash, event}))`.
151
+ * Domain-separated and canonically framed: neither field order nor
152
+ * string-concatenation ambiguity can affect the hash. The entry is
153
+ * serialized without its own chainHash. First entry uses `prevChainHash: ''`.
154
+ */
155
+ export function computeChainHash(
156
+ entryWithoutChainHash: Record<string, unknown>,
157
+ prevChainHash: string,
158
+ chainKey: Buffer,
159
+ ): string {
160
+ const preimage = canonicalJson({
161
+ domain: DOMAIN_CHAIN_HASH,
162
+ prevChainHash,
163
+ event: entryWithoutChainHash,
164
+ });
165
+ return hmacSha256hex(preimage, chainKey);
166
+ }
167
+
168
+ // ── Merkle tree ────────────────────────────────────────────────────────────────
169
+ //
170
+ // Leaves and internal nodes are domain-separated so an internal node can
171
+ // never be replayed as a leaf (second-preimage hardening). Odd layers
172
+ // duplicate the last node; ambiguity from that padding is neutralized by
173
+ // the signed `eventCount` in the manifest payload.
174
+
175
+ const LEAF_TAG = 'mcpose/v2/leaf\0';
176
+ const NODE_TAG = 'mcpose/v2/node\0';
177
+
178
+ function leafHash(chainHash: string): string {
179
+ return sha256hex(LEAF_TAG + chainHash);
180
+ }
181
+
182
+ function nodeHash(left: string, right: string): string {
183
+ return sha256hex(NODE_TAG + left + right);
184
+ }
185
+
186
+ function nextLayer(layer: string[]): string[] {
187
+ const next: string[] = [];
188
+ for (let i = 0; i < layer.length; i += 2) {
189
+ // `i < layer.length` guarantees `left`; a missing right sibling means an
190
+ // odd layer, whose last node is duplicated (ADR-0004) — same padding as
191
+ // `computeMerkleProof` and `computeMerkleRootAndProofs`.
192
+ const left = layer[i]!;
193
+ const right = layer[i + 1] ?? left;
194
+ next.push(nodeHash(left, right));
195
+ }
196
+ return next;
197
+ }
198
+
199
+ /** Merkle root over the events' `chainHash` values. Empty → `sha256('')`. */
200
+ export function computeMerkleRoot(hashes: string[]): string {
201
+ if (hashes.length === 0) return sha256hex('');
202
+ let layer = hashes.map(leafHash);
203
+ while (layer.length > 1) {
204
+ layer = nextLayer(layer);
205
+ }
206
+ // `hashes` is non-empty here and the loop exits only at length 1.
207
+ return layer[0]!;
208
+ }
209
+
210
+ export function computeMerkleProof(
211
+ hashes: string[],
212
+ index: number,
213
+ ): MerkleProof {
214
+ if (!Number.isInteger(index) || index < 0 || index >= hashes.length) {
215
+ throw new RangeError(
216
+ `computeMerkleProof: index ${index} out of range for ${hashes.length} leaves`,
217
+ );
218
+ }
219
+
220
+ const siblings: string[] = [];
221
+ const directions: ('left' | 'right')[] = [];
222
+
223
+ let layer = hashes.map(leafHash);
224
+ let idx = index;
225
+
226
+ while (layer.length > 1) {
227
+ const siblingIdx = idx % 2 === 0 ? idx + 1 : idx - 1;
228
+ // A sibling past the end means an odd layer whose last node is
229
+ // duplicated, and `idx` is then that last node — same padding as
230
+ // `nextLayer`, which must stay identical. `idx` is always in range.
231
+ siblings.push(layer[siblingIdx] ?? layer[idx]!);
232
+ directions.push(idx % 2 === 0 ? 'right' : 'left');
233
+ layer = nextLayer(layer);
234
+ idx = Math.floor(idx / 2);
235
+ }
236
+
237
+ return { index, siblings, directions };
238
+ }
239
+
240
+ function proofFromLayers(index: number, layers: string[][]): MerkleProof {
241
+ const siblings: string[] = [];
242
+ const directions: ('left' | 'right')[] = [];
243
+ let idx = index;
244
+
245
+ for (let level = 0; level < layers.length - 1; level++) {
246
+ const layer = layers[level]!;
247
+ const siblingIdx = idx % 2 === 0 ? idx + 1 : idx - 1;
248
+ siblings.push(layer[siblingIdx] ?? layer[idx]!);
249
+ directions.push(idx % 2 === 0 ? 'right' : 'left');
250
+ idx = Math.floor(idx / 2);
251
+ }
252
+
253
+ return { index, siblings, directions };
254
+ }
255
+
256
+ /** Build a manifest root and every proof from the same set of tree layers. */
257
+ export function computeMerkleRootAndProofs(hashes: string[]): {
258
+ merkleRoot: string;
259
+ merkleProofs: MerkleProof[];
260
+ } {
261
+ if (hashes.length === 0) {
262
+ return { merkleRoot: sha256hex(''), merkleProofs: [] };
263
+ }
264
+
265
+ const layers = [hashes.map(leafHash)];
266
+ while (layers[layers.length - 1]!.length > 1) {
267
+ layers.push(nextLayer(layers[layers.length - 1]!));
268
+ }
269
+
270
+ return {
271
+ merkleRoot: layers[layers.length - 1]![0]!,
272
+ merkleProofs: hashes.map((_, index) => proofFromLayers(index, layers)),
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Verifies a Merkle proof for an event's `chainHash` (the raw chain hash —
278
+ * the leaf tag is applied internally).
279
+ *
280
+ * Rejects malformed proofs: `siblings`/`directions` length mismatch,
281
+ * directions inconsistent with `proof.index` (each level's direction is
282
+ * fully determined by the index), or an index that the path cannot
283
+ * represent (`index >= 2 ** siblings.length`, which leaves high bits
284
+ * unconsumed after traversal).
285
+ */
286
+ export function verifyMerkleProof(
287
+ leafChainHash: string,
288
+ proof: MerkleProof,
289
+ root: string,
290
+ ): boolean {
291
+ if (
292
+ !Number.isSafeInteger(proof.index) ||
293
+ proof.index < 0 ||
294
+ proof.siblings.length !== proof.directions.length
295
+ ) {
296
+ return false;
297
+ }
298
+
299
+ let current = leafHash(leafChainHash);
300
+ let idx = proof.index;
301
+ for (let i = 0; i < proof.siblings.length; i++) {
302
+ const expectedDir = idx % 2 === 0 ? 'right' : 'left';
303
+ if (proof.directions[i] !== expectedDir) return false;
304
+ const sibling = proof.siblings[i];
305
+ // A hole in a caller-supplied `siblings` array is a malformed proof.
306
+ if (sibling === undefined) return false;
307
+ current =
308
+ expectedDir === 'right'
309
+ ? nodeHash(current, sibling)
310
+ : nodeHash(sibling, current);
311
+ idx = Math.floor(idx / 2);
312
+ }
313
+ // Bits left over mean the claimed position lies outside a tree of this
314
+ // depth: a two-leaf proof for position 0 must not also verify at 2 or 4.
315
+ if (idx !== 0) return false;
316
+ return current === root;
317
+ }
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ export type {
2
+ SensitivityTier,
3
+ SensitivityResolverFn,
4
+ SensitivityOverrideFn,
5
+ SigningKeyProvider,
6
+ SubjectKeyStore,
7
+ SubjectKeyTombstone,
8
+ AuditEventBase,
9
+ LowAuditEvent,
10
+ MediumAuditEvent,
11
+ HighAuditEvent,
12
+ AuditEvent,
13
+ MerkleProof,
14
+ ReplayManifest,
15
+ AuditOptions,
16
+ AuditMiddlewareHandle,
17
+ } from './types.js';
18
+
19
+ export { createSensitivityResolver } from './sensitivity.js';
20
+ export { createDefaultSigningKeyProvider } from './signingKey.js';
21
+ export { createInMemorySubjectKeyStore } from './subjectKeyStore.js';
22
+ export { createAuditMiddleware } from './middleware.js';
23
+ export {
24
+ computeMerkleRoot,
25
+ computeMerkleProof,
26
+ verifyMerkleProof,
27
+ canonicalJson,
28
+ stableStringify,
29
+ } from './chain.js';
30
+ export type { ChainVerification } from './verify.js';
31
+ export { verifyAuditChain, verifyManifestSignature } from './verify.js';