@zanii/core 0.5.1 → 0.5.3

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/src/bundle.ts CHANGED
@@ -1,176 +1,214 @@
1
- /**
2
- * Audit bundles — SPEC §8. A self-contained, offline-verifiable export of one
3
- * agent's complete action history: receipts, proofs, revocations, anchors.
4
- * verifyAuditBundle() is pure: no network, no trusted party.
5
- */
6
- import { hexToBytes } from '@noble/hashes/utils';
7
- import { canonicalBytes, jcsHash } from './encoding.js';
8
- import { certHash, verifyRevocation, type RevocationRecord } from './delegation.js';
9
- import { leafHash, verifyConsistency, verifyInclusion } from './merkle.js';
10
- import { verifySTH, type SignedTreeHead } from './sth.js';
11
- import { verifyReceipt, type Receipt } from './receipt.js';
12
- import { participantsOf, prevFor, verifyA2AReceipt, type A2AReceipt } from './a2a.js';
13
-
14
- export interface BundledReceipt {
15
- receipt: Receipt | A2AReceipt;
16
- hash: string;
17
- index: number;
18
- proof: string[];
19
- }
20
-
21
- export interface BundledRevocation {
22
- revocation: RevocationRecord;
23
- cert_hash: string;
24
- index: number;
25
- proof: string[];
26
- }
27
-
28
- export interface BundledAnchor {
29
- sth: SignedTreeHead;
30
- backend: string;
31
- ref: string;
32
- block?: number;
33
- anchored_at: string;
34
- /** Consistency proof from the anchored tree to this bundle's STH. */
35
- consistency_proof: string[];
36
- }
37
-
38
- export interface AuditBundle {
39
- v: 1;
40
- bundle_type: 'zanii-audit-bundle';
41
- generated_at: string;
42
- log_id: string;
43
- agent_id: string;
44
- sth: SignedTreeHead;
45
- receipts: BundledReceipt[];
46
- revocations: BundledRevocation[];
47
- anchors: BundledAnchor[];
48
- }
49
-
50
- export interface BundleCheck {
51
- label: string;
52
- ok: boolean;
53
- detail?: string;
54
- }
55
-
56
- export interface BundleReport {
57
- ok: boolean;
58
- checks: BundleCheck[];
59
- stats: {
60
- receipts: number;
61
- first_ts: string | null;
62
- last_ts: string | null;
63
- revoked_certs: number;
64
- anchored_through: number | null;
65
- };
66
- }
67
-
68
- const root = (sth: SignedTreeHead) => hexToBytes(sth.root.replace('sha256:', ''));
69
-
70
- /** Fully verify an audit bundle offline. Every failure is reported, none throws. */
71
- export function verifyAuditBundle(bundle: AuditBundle): BundleReport {
72
- const checks: BundleCheck[] = [];
73
- const push = (label: string, ok: boolean, detail?: string) => checks.push({ label, ok, detail });
74
-
75
- try {
76
- push('bundle format', bundle.v === 1 && bundle.bundle_type === 'zanii-audit-bundle');
77
- push(
78
- 'signed tree head signature and log identity',
79
- verifySTH(bundle.sth) && bundle.sth.log_id === bundle.log_id,
80
- bundle.log_id,
81
- );
82
-
83
- // Receipts: signature/delegation/scope, identity, hash, inclusion, prev-chain continuity.
84
- let receiptsOk = true;
85
- let chainOk = true;
86
- let expectedPrev: string | null = null;
87
- const sorted = [...bundle.receipts].sort((a, b) => a.index - b.index);
88
- for (const [i, br] of sorted.entries()) {
89
- // revocation reported separately below
90
- const check =
91
- (br.receipt as A2AReceipt).type === 'a2a'
92
- ? verifyA2AReceipt(br.receipt as A2AReceipt)
93
- : verifyReceipt(br.receipt as Receipt);
94
- const identityOk = participantsOf(br.receipt).includes(bundle.agent_id);
95
- const hashOk = jcsHash(br.receipt) === br.hash;
96
- const inclusionOk = verifyInclusion(
97
- leafHash(canonicalBytes(br.receipt)),
98
- br.index,
99
- bundle.sth.size,
100
- br.proof.map(hexToBytes),
101
- root(bundle.sth),
102
- );
103
- if (!check.ok || !identityOk || !hashOk || !inclusionOk) {
104
- receiptsOk = false;
105
- push(`receipt #${i} (leaf ${br.index})`, false, check.error ?? 'hash/identity/inclusion mismatch');
106
- }
107
- if (prevFor(br.receipt, bundle.agent_id) !== expectedPrev) chainOk = false;
108
- expectedPrev = br.hash;
109
- }
110
- push(`all ${sorted.length} receipts: signature, delegation, scope, hash, Merkle inclusion`, receiptsOk);
111
- push('per-agent hash chain is complete and unbroken (full history, no gaps)', chainOk);
112
-
113
- // Revocations: issuer-signed and present in the tree.
114
- let revocationsOk = true;
115
- for (const bv of bundle.revocations) {
116
- const sigOk = verifyRevocation(bv.revocation) && certHash(bv.revocation.cert) === bv.cert_hash;
117
- const inclusionOk = verifyInclusion(
118
- leafHash(canonicalBytes(bv.revocation)),
119
- bv.index,
120
- bundle.sth.size,
121
- bv.proof.map(hexToBytes),
122
- root(bundle.sth),
123
- );
124
- if (!sigOk || !inclusionOk) {
125
- revocationsOk = false;
126
- push(`revocation of ${bv.cert_hash.slice(0, 24)}…`, false);
127
- }
128
- }
129
- push(`all ${bundle.revocations.length} revocation records: issuer signature and Merkle inclusion`, revocationsOk);
130
-
131
- // Anchors: each anchored STH verifies and the bundle tree is an append-only extension of it.
132
- let anchorsOk = true;
133
- let anchoredThrough: number | null = null;
134
- for (const a of bundle.anchors) {
135
- const sthOk = verifySTH(a.sth) && a.sth.log_id === bundle.log_id;
136
- const consistent = verifyConsistency(
137
- a.sth.size,
138
- bundle.sth.size,
139
- root(a.sth),
140
- root(bundle.sth),
141
- a.consistency_proof.map(hexToBytes),
142
- );
143
- if (!sthOk || !consistent) {
144
- anchorsOk = false;
145
- push(`anchor ${a.ref}`, false, 'STH signature or consistency proof failed');
146
- } else {
147
- anchoredThrough = Math.max(anchoredThrough ?? 0, a.sth.size);
148
- }
149
- }
150
- push(
151
- `all ${bundle.anchors.length} anchors: anchored STH valid, bundle tree is append-only extension`,
152
- anchorsOk,
153
- anchoredThrough !== null ? `history immutable through leaf ${anchoredThrough - 1}` : undefined,
154
- );
155
-
156
- const revokedCertHashes = new Set(bundle.revocations.map((r) => r.cert_hash));
157
- return {
158
- ok: checks.every((c) => c.ok),
159
- checks,
160
- stats: {
161
- receipts: sorted.length,
162
- first_ts: sorted[0]?.receipt.ts ?? null,
163
- last_ts: sorted[sorted.length - 1]?.receipt.ts ?? null,
164
- revoked_certs: revokedCertHashes.size,
165
- anchored_through: anchoredThrough,
166
- },
167
- };
168
- } catch (err) {
169
- push('bundle parses and verifies without errors', false, String(err));
170
- return {
171
- ok: false,
172
- checks,
173
- stats: { receipts: 0, first_ts: null, last_ts: null, revoked_certs: 0, anchored_through: null },
174
- };
175
- }
176
- }
1
+ /**
2
+ * Audit bundles — SPEC §8. A self-contained, offline-verifiable export of one
3
+ * agent's complete action history: receipts, proofs, revocations, anchors.
4
+ * verifyAuditBundle() is pure: no network, no trusted party.
5
+ */
6
+ import { hexToBytes } from '@noble/hashes/utils';
7
+ import { canonicalBytes, jcsHash } from './encoding.js';
8
+ import { certHash, verifyRevocation, type RevocationRecord } from './delegation.js';
9
+ import { leafHash, verifyConsistency, verifyInclusion } from './merkle.js';
10
+ import { verifySTH, type SignedTreeHead } from './sth.js';
11
+ import { isSessionClose, verifyReceipt, type Receipt } from './receipt.js';
12
+ import { participantsOf, prevFor, verifyA2AReceipt, type A2AReceipt } from './a2a.js';
13
+ import { verifySwarmReceipt, type SwarmLedgerReceipt } from './swarm.js';
14
+
15
+ /** A receipt's timestamp: top-level on solo and a2a receipts, inside `body` on swarm ones. */
16
+ function receiptTs(r: unknown): string | null {
17
+ const o = r as { ts?: unknown; body?: { ts?: unknown } };
18
+ if (typeof o?.ts === 'string') return o.ts;
19
+ return typeof o?.body?.ts === 'string' ? o.body.ts : null;
20
+ }
21
+
22
+ export interface BundledReceipt {
23
+ receipt: Receipt | A2AReceipt;
24
+ hash: string;
25
+ index: number;
26
+ proof: string[];
27
+ }
28
+
29
+ export interface BundledRevocation {
30
+ revocation: RevocationRecord;
31
+ cert_hash: string;
32
+ index: number;
33
+ proof: string[];
34
+ }
35
+
36
+ export interface BundledAnchor {
37
+ sth: SignedTreeHead;
38
+ backend: string;
39
+ ref: string;
40
+ block?: number;
41
+ anchored_at: string;
42
+ /** Consistency proof from the anchored tree to this bundle's STH. */
43
+ consistency_proof: string[];
44
+ }
45
+
46
+ export interface AuditBundle {
47
+ v: 1;
48
+ bundle_type: 'zanii-audit-bundle';
49
+ generated_at: string;
50
+ log_id: string;
51
+ agent_id: string;
52
+ sth: SignedTreeHead;
53
+ receipts: BundledReceipt[];
54
+ revocations: BundledRevocation[];
55
+ anchors: BundledAnchor[];
56
+ }
57
+
58
+ export interface BundleCheck {
59
+ label: string;
60
+ ok: boolean;
61
+ detail?: string;
62
+ }
63
+
64
+ export interface BundleReport {
65
+ ok: boolean;
66
+ checks: BundleCheck[];
67
+ stats: {
68
+ receipts: number;
69
+ first_ts: string | null;
70
+ last_ts: string | null;
71
+ revoked_certs: number;
72
+ anchored_through: number | null;
73
+ /** The newest receipt closes a session (SPEC §15). */
74
+ session_closed: boolean;
75
+ /** When the newest receipt closed its session, else null. */
76
+ closed_at: string | null;
77
+ };
78
+ }
79
+
80
+ const root = (sth: SignedTreeHead) => hexToBytes(sth.root.replace('sha256:', ''));
81
+
82
+ /** Fully verify an audit bundle offline. Every failure is reported, none throws. */
83
+ export function verifyAuditBundle(bundle: AuditBundle): BundleReport {
84
+ const checks: BundleCheck[] = [];
85
+ const push = (label: string, ok: boolean, detail?: string) => checks.push({ label, ok, detail });
86
+
87
+ try {
88
+ push('bundle format', bundle.v === 1 && bundle.bundle_type === 'zanii-audit-bundle');
89
+ push(
90
+ 'signed tree head signature and log identity',
91
+ verifySTH(bundle.sth) && bundle.sth.log_id === bundle.log_id,
92
+ bundle.log_id,
93
+ );
94
+
95
+ // Receipts: signature/delegation/scope, identity, hash, inclusion, prev-chain continuity.
96
+ let receiptsOk = true;
97
+ let chainOk = true;
98
+ let expectedPrev: string | null = null;
99
+ const sorted = [...bundle.receipts].sort((a, b) => a.index - b.index);
100
+ for (const [i, br] of sorted.entries()) {
101
+ // revocation reported separately below
102
+ // Each receipt is checked by the rules of its own type. A swarm receipt checked as a
103
+ // solo one failed signature and chain checks, so an honest swarm participant's
104
+ // history was reported as broken.
105
+ const kind = (br.receipt as { type?: string }).type;
106
+ const check =
107
+ kind === 'a2a'
108
+ ? verifyA2AReceipt(br.receipt as A2AReceipt)
109
+ : kind === 'swarm'
110
+ ? verifySwarmReceipt(br.receipt as unknown as SwarmLedgerReceipt)
111
+ : verifyReceipt(br.receipt as Receipt);
112
+ const identityOk = participantsOf(br.receipt).includes(bundle.agent_id);
113
+ const hashOk = jcsHash(br.receipt) === br.hash;
114
+ const inclusionOk = verifyInclusion(
115
+ leafHash(canonicalBytes(br.receipt)),
116
+ br.index,
117
+ bundle.sth.size,
118
+ br.proof.map(hexToBytes),
119
+ root(bundle.sth),
120
+ );
121
+ if (!check.ok || !identityOk || !hashOk || !inclusionOk) {
122
+ receiptsOk = false;
123
+ push(`receipt #${i} (leaf ${br.index})`, false, check.error ?? 'hash/identity/inclusion mismatch');
124
+ }
125
+ if (prevFor(br.receipt, bundle.agent_id) !== expectedPrev) chainOk = false;
126
+ expectedPrev = br.hash;
127
+ }
128
+ push(`all ${sorted.length} receipts: signature, delegation, scope, hash, Merkle inclusion`, receiptsOk);
129
+ push('per-agent hash chain is complete and unbroken (full history, no gaps)', chainOk);
130
+
131
+ // Revocations: issuer-signed and present in the tree.
132
+ let revocationsOk = true;
133
+ for (const bv of bundle.revocations) {
134
+ const sigOk = verifyRevocation(bv.revocation) && certHash(bv.revocation.cert) === bv.cert_hash;
135
+ const inclusionOk = verifyInclusion(
136
+ leafHash(canonicalBytes(bv.revocation)),
137
+ bv.index,
138
+ bundle.sth.size,
139
+ bv.proof.map(hexToBytes),
140
+ root(bundle.sth),
141
+ );
142
+ if (!sigOk || !inclusionOk) {
143
+ revocationsOk = false;
144
+ push(`revocation of ${bv.cert_hash.slice(0, 24)}…`, false);
145
+ }
146
+ }
147
+ push(`all ${bundle.revocations.length} revocation records: issuer signature and Merkle inclusion`, revocationsOk);
148
+
149
+ // Anchors: each anchored STH verifies and the bundle tree is an append-only extension of it.
150
+ let anchorsOk = true;
151
+ let anchoredThrough: number | null = null;
152
+ for (const a of bundle.anchors) {
153
+ const sthOk = verifySTH(a.sth) && a.sth.log_id === bundle.log_id;
154
+ const consistent = verifyConsistency(
155
+ a.sth.size,
156
+ bundle.sth.size,
157
+ root(a.sth),
158
+ root(bundle.sth),
159
+ a.consistency_proof.map(hexToBytes),
160
+ );
161
+ if (!sthOk || !consistent) {
162
+ anchorsOk = false;
163
+ push(`anchor ${a.ref}`, false, 'STH signature or consistency proof failed');
164
+ } else {
165
+ anchoredThrough = Math.max(anchoredThrough ?? 0, a.sth.size);
166
+ }
167
+ }
168
+ push(
169
+ `all ${bundle.anchors.length} anchors: anchored STH valid, bundle tree is append-only extension`,
170
+ anchorsOk,
171
+ anchoredThrough !== null ? `history immutable through leaf ${anchoredThrough - 1}` : undefined,
172
+ );
173
+
174
+ // Session close (SPEC §15) is reported, never failed: an agent mid-session is normal.
175
+ // It is mentioned only for agents that use sessions, so a bundle from an agent that
176
+ // never closes one reads exactly as it always has.
177
+ const newest = sorted[sorted.length - 1]?.receipt;
178
+ const sessionClosed = newest !== undefined && isSessionClose(newest);
179
+ const closedAt = sessionClosed ? receiptTs(newest) : null;
180
+ if (sorted.some((br) => isSessionClose(br.receipt))) {
181
+ push(
182
+ sessionClosed
183
+ ? `the newest receipt closes a session (${closedAt})`
184
+ : 'the newest receipt is not a session close: the agent may still be running, or receipts after its last one were withheld',
185
+ true,
186
+ );
187
+ }
188
+
189
+ const revokedCertHashes = new Set(bundle.revocations.map((r) => r.cert_hash));
190
+ return {
191
+ ok: checks.every((c) => c.ok),
192
+ checks,
193
+ stats: {
194
+ receipts: sorted.length,
195
+ first_ts: sorted.length ? receiptTs(sorted[0]!.receipt) : null,
196
+ last_ts: sorted.length ? receiptTs(sorted[sorted.length - 1]!.receipt) : null,
197
+ revoked_certs: revokedCertHashes.size,
198
+ anchored_through: anchoredThrough,
199
+ session_closed: sessionClosed,
200
+ closed_at: closedAt,
201
+ },
202
+ };
203
+ } catch (err) {
204
+ push('bundle parses and verifies without errors', false, String(err));
205
+ return {
206
+ ok: false,
207
+ checks,
208
+ stats: {
209
+ receipts: 0, first_ts: null, last_ts: null, revoked_certs: 0, anchored_through: null,
210
+ session_closed: false, closed_at: null,
211
+ },
212
+ };
213
+ }
214
+ }
package/src/index.ts CHANGED
@@ -52,6 +52,11 @@ export {
52
52
  saltedPayloadHash,
53
53
  verifyPayload,
54
54
  verifyPayloadLegacy,
55
+ RISK_LEVELS,
56
+ SESSION_CLOSE_TARGET,
57
+ isSessionClose,
58
+ isRiskLevel,
59
+ type RiskLevel,
55
60
  type PayloadDialect,
56
61
  verifyReceipt,
57
62
  verifyReceiptSignature,
@@ -69,3 +74,4 @@ export {
69
74
  verifyCoSignature,
70
75
  type ReceiptCoSignature,
71
76
  } from './cosign.js';
77
+ export { verifySwarmReceipt, type SwarmBody, type SwarmLedgerReceipt, type SwarmSignerEnvelope } from './swarm.js';