attenu-guard 0.5.0 → 0.6.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/CHANGELOG.md +35 -0
- package/README.md +11 -0
- package/dist/cjs/evidence.d.ts +34 -0
- package/dist/cjs/evidence.d.ts.map +1 -1
- package/dist/cjs/evidence.js +192 -60
- package/dist/cjs/evidence.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/evidence.d.ts +34 -0
- package/dist/esm/evidence.d.ts.map +1 -1
- package/dist/esm/evidence.js +192 -60
- package/dist/esm/evidence.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,41 @@ follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.0] - 2026-09-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **The bundle-level interop test vectors, and the structured failures they are scored
|
|
13
|
+
against.** The delegation-chain vectors under `test/fixtures/vectors/` have always let this
|
|
14
|
+
package score its own *token* verifier against the Python reference. There was no equivalent
|
|
15
|
+
for the *bundle* verifier, which is where the offline-verifiability claim actually lands: an
|
|
16
|
+
auditor checks a published ledger with no engine, no service and no vendor in the loop.
|
|
17
|
+
`test/fixtures/vectors/bundles/bundle_vectors_v1.json` is that file, copied byte for byte from
|
|
18
|
+
the Python repository (`tests/vectors/bundles/bundle_vectors_v1.json`, whose single writer is
|
|
19
|
+
`tests/vectors/generate_bundles.py`). Eight cases, every one derived from a single valid
|
|
20
|
+
schema-v2 bundle by exactly ONE change so each isolates one rule: `valid_bundle_v2` (accept),
|
|
21
|
+
`reject_params_mismatch`, `reject_outcome_without_allow`, `reject_outcome_before_allow`,
|
|
22
|
+
`reject_duplicate_outcome`, `reject_duplicate_call_id`, `reject_rehashed_chain` (one entry
|
|
23
|
+
edited and every later hash recomputed — the rewrite a hash chain alone cannot catch, which
|
|
24
|
+
only the signed anchor does) and `reject_tampered_entry` (the same edit with nothing
|
|
25
|
+
re-hashed, which fails AT that entry). All eight score here exactly as they score in Python,
|
|
26
|
+
with byte-identical `failures` strings. `tools/gen_fixtures.py` re-copies the file from the
|
|
27
|
+
installed Python package once a release ships it, so CI's existing fixture-drift check starts
|
|
28
|
+
guarding it with no workflow edit.
|
|
29
|
+
- **`verifyBundle()` reports `failure_details`, the structured twin of `failures`** (additive;
|
|
30
|
+
`failures` is byte-identical to before, because other implementations parse those strings).
|
|
31
|
+
One entry per string, same order, same count: `{reason, seq, node, call_id, detail}`, so a
|
|
32
|
+
conformance suite can assert WHICH check failed and WHERE instead of matching prose. `reason`
|
|
33
|
+
is the token before the colon in the message, except at the two historical sites whose message
|
|
34
|
+
names a node there (`unreadable_authority`, `unreadable_granted`), which state their reason
|
|
35
|
+
explicitly. Positions match the Python implementation's: the second sighting for a re-used
|
|
36
|
+
`call_id`, the `outcome` entry for every allow/outcome binding failure, and null for a
|
|
37
|
+
genuinely chain-level failure such as a signed anchor that no longer matches the ledger head.
|
|
38
|
+
Every failure in `src/evidence.ts` now goes through one collector, so a message cannot be
|
|
39
|
+
added without its twin — `test/bundle-vectors.test.ts` asserts the two lists stay in step at
|
|
40
|
+
every failure site in the module (including the ones no vector exercises) and traps a direct
|
|
41
|
+
append. The `execution_binding` sub-report's own shape is unchanged: the twins ride alongside
|
|
42
|
+
it, never inside it.
|
|
43
|
+
|
|
9
44
|
## [0.5.0] - 2026-08-31
|
|
10
45
|
|
|
11
46
|
### Fixed
|
package/README.md
CHANGED
|
@@ -178,6 +178,17 @@ dual-signing mode. The 19 committed interop vectors include the separating cases
|
|
|
178
178
|
for number spelling, raw Unicode, UTF-16 member ordering, large integers,
|
|
179
179
|
duplicates, non-finite values and an unmarked canonical header.
|
|
180
180
|
|
|
181
|
+
`test/fixtures/vectors/bundles/bundle_vectors_v1.json` is the second, bundle-level
|
|
182
|
+
suite: whole evidence bundles for `verifyBundle`, the check an auditor runs on a
|
|
183
|
+
published ledger with no engine and no vendor in the loop. The token vectors pin
|
|
184
|
+
what a delegation token means; these pin what the LEDGER of a run has to satisfy.
|
|
185
|
+
A bundle verifier reports a LIST of failures rather than one reject reason, so
|
|
186
|
+
each rejecting case declares the minimal set of `{reason, seq, node}` that MUST
|
|
187
|
+
appear, at that exact position. A conformant verifier may report more, never
|
|
188
|
+
fewer and never elsewhere. `verifyBundle` returns those positions as
|
|
189
|
+
`failure_details`, the structured twin of `failures`: one
|
|
190
|
+
`{reason, seq, node, call_id, detail}` entry per string, in the same order.
|
|
191
|
+
|
|
181
192
|
## What it does not do
|
|
182
193
|
|
|
183
194
|
- It does not decide what permissions a task needs. You state them; this library
|
package/dist/cjs/evidence.d.ts
CHANGED
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
* const bundle = exportBundle(guard.auditLog(), signer);
|
|
19
19
|
* const report = verifyBundle(bundle, signer);
|
|
20
20
|
*
|
|
21
|
+
* `report.failures` is the human-readable list — its strings are a published
|
|
22
|
+
* contract, other implementations parse them — and `report.failure_details` is
|
|
23
|
+
* its machine-readable twin: one entry per string, same order, same count,
|
|
24
|
+
* `{reason, seq, node, call_id, detail}`. It exists so a conformance suite can
|
|
25
|
+
* assert WHICH check failed and WHERE, not merely that something did. The
|
|
26
|
+
* bundle-level interop vectors under `test/fixtures/vectors/bundles/` are scored
|
|
27
|
+
* against exactly that shape.
|
|
28
|
+
*
|
|
21
29
|
* No engine state is consulted — the bundle is the whole input, which is the
|
|
22
30
|
* point. This is byte-compatible with the Python library's
|
|
23
31
|
* `attenu_guard.evidence`.
|
|
@@ -98,6 +106,23 @@ export interface ExportOptions {
|
|
|
98
106
|
* is thrown on any field outside it.
|
|
99
107
|
*/
|
|
100
108
|
export declare function exportBundle(auditLog: AuditLog | readonly LedgerEntry[], signer: Signer, options?: ExportOptions): Bundle;
|
|
109
|
+
/**
|
|
110
|
+
* One structured failure — the machine-readable twin of one `failures` string.
|
|
111
|
+
*
|
|
112
|
+
* `reason` is a stable token: the text before the first `:` in `detail`, with
|
|
113
|
+
* the two historical exceptions whose message names a NODE there
|
|
114
|
+
* (`unreadable_authority`, `unreadable_granted`) and so state their reason
|
|
115
|
+
* explicitly. `seq`/`node` are the offending entry's own fields, both `null`
|
|
116
|
+
* when the failure is chain-level with nothing single to point at. Same field
|
|
117
|
+
* names and same values as the Python implementation's `failure_details`.
|
|
118
|
+
*/
|
|
119
|
+
export interface FailureDetail {
|
|
120
|
+
reason: string;
|
|
121
|
+
seq: Json;
|
|
122
|
+
node: Json;
|
|
123
|
+
call_id: Json;
|
|
124
|
+
detail: string;
|
|
125
|
+
}
|
|
101
126
|
export interface GraphNode {
|
|
102
127
|
agent: Json;
|
|
103
128
|
task: Json;
|
|
@@ -165,6 +190,12 @@ export interface VerifyReport {
|
|
|
165
190
|
ok: boolean;
|
|
166
191
|
checks: VerifyChecks;
|
|
167
192
|
failures: string[];
|
|
193
|
+
/**
|
|
194
|
+
* The structured twin of `failures`: same order, same count, one
|
|
195
|
+
* `{reason, seq, node, call_id, detail}` per string, so a conformance suite can assert the
|
|
196
|
+
* reason AND the position of every failure instead of matching prose.
|
|
197
|
+
*/
|
|
198
|
+
failure_details: FailureDetail[];
|
|
168
199
|
nodes: number;
|
|
169
200
|
actions_checked: number;
|
|
170
201
|
chain_id: Json;
|
|
@@ -190,6 +221,9 @@ export interface VerifyReport {
|
|
|
190
221
|
* the bundle's actual `(seq, hash, chainId, v)` must equal it exactly, or `checks.expected_anchor`
|
|
191
222
|
* reports `"FAILED"` and the mismatch lands in `failures`. `report.verified_against` names which
|
|
192
223
|
* mode ran.
|
|
224
|
+
*
|
|
225
|
+
* `failure_details` is the structured twin of `failures`: same order, same count, one
|
|
226
|
+
* `{reason, seq, node, call_id, detail}` entry per string.
|
|
193
227
|
*/
|
|
194
228
|
export interface VerifyBundleOptions {
|
|
195
229
|
/** An independently retained anchor object to verify the bundle's actual head against. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../../src/evidence.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../../src/evidence.ts"],"names":[],"mappings":"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"}
|
package/dist/cjs/evidence.js
CHANGED
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
* const bundle = exportBundle(guard.auditLog(), signer);
|
|
20
20
|
* const report = verifyBundle(bundle, signer);
|
|
21
21
|
*
|
|
22
|
+
* `report.failures` is the human-readable list — its strings are a published
|
|
23
|
+
* contract, other implementations parse them — and `report.failure_details` is
|
|
24
|
+
* its machine-readable twin: one entry per string, same order, same count,
|
|
25
|
+
* `{reason, seq, node, call_id, detail}`. It exists so a conformance suite can
|
|
26
|
+
* assert WHICH check failed and WHERE, not merely that something did. The
|
|
27
|
+
* bundle-level interop vectors under `test/fixtures/vectors/bundles/` are scored
|
|
28
|
+
* against exactly that shape.
|
|
29
|
+
*
|
|
22
30
|
* No engine state is consulted — the bundle is the whole input, which is the
|
|
23
31
|
* point. This is byte-compatible with the Python library's
|
|
24
32
|
* `attenu_guard.evidence`.
|
|
@@ -228,6 +236,33 @@ function orNull(value) {
|
|
|
228
236
|
const plain = (0, canonical_js_1.toPlain)(value);
|
|
229
237
|
return plain === undefined ? null : plain;
|
|
230
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* The verifier's failure list, kept in two shapes that cannot drift apart.
|
|
241
|
+
*
|
|
242
|
+
* `messages` is the string list `verifyBundle` has always returned as
|
|
243
|
+
* `failures`; those exact strings are a published contract, so they are never
|
|
244
|
+
* reworded here. `details` is the structured twin of each one, appended in the
|
|
245
|
+
* same call. Every failure in this module goes through `add`, so a new check
|
|
246
|
+
* cannot add a message without its twin — `test/bundle-vectors.test.ts` greps
|
|
247
|
+
* this file for a direct append to a failure list and fails on one, and asserts
|
|
248
|
+
* the two lists stay in step at every site.
|
|
249
|
+
*/
|
|
250
|
+
class FailureLog {
|
|
251
|
+
messages = [];
|
|
252
|
+
details = [];
|
|
253
|
+
add(reason, detail, position = {}) {
|
|
254
|
+
const { seq = null, node = null, callId = null } = position;
|
|
255
|
+
this.messages.push(detail);
|
|
256
|
+
this.details.push({ reason, seq, node, call_id: callId, detail });
|
|
257
|
+
}
|
|
258
|
+
extend(other) {
|
|
259
|
+
this.messages.push(...other.messages);
|
|
260
|
+
this.details.push(...other.details);
|
|
261
|
+
}
|
|
262
|
+
get length() {
|
|
263
|
+
return this.messages.length;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
231
266
|
/**
|
|
232
267
|
* `node -> Authority` and `node -> parent`, reconstructed from `root` and
|
|
233
268
|
* `spawn` events alone. No engine state.
|
|
@@ -235,29 +270,40 @@ function orNull(value) {
|
|
|
235
270
|
function nodeAuthorities(entries) {
|
|
236
271
|
const auth = new Map();
|
|
237
272
|
const parent = new Map();
|
|
238
|
-
const failures =
|
|
273
|
+
const failures = new FailureLog();
|
|
274
|
+
const definedBy = new Map();
|
|
239
275
|
for (const e of entries) {
|
|
240
276
|
const ev = (0, canonical_js_1.toPlain)(e["event"]);
|
|
241
277
|
const node = (0, canonical_js_1.toPlain)(e["node"]);
|
|
242
278
|
if (ev === "root") {
|
|
279
|
+
definedBy.set(node, e);
|
|
243
280
|
try {
|
|
244
281
|
auth.set(node, authority_js_1.Authority.fromWire(e["authority"] ?? null));
|
|
245
282
|
}
|
|
246
283
|
catch (exc) {
|
|
247
|
-
|
|
284
|
+
// One of the two historical messages that name a node before their colon rather than a
|
|
285
|
+
// reason token, so the reason is stated here instead of parsed out of the string.
|
|
286
|
+
failures.add("unreadable_authority", `root ${node}: unreadable authority (${exc.message})`, {
|
|
287
|
+
seq: orNull(e["seq"]),
|
|
288
|
+
node: orNull(e["node"]),
|
|
289
|
+
});
|
|
248
290
|
}
|
|
249
291
|
}
|
|
250
292
|
else if (ev === "spawn") {
|
|
293
|
+
definedBy.set(node, e);
|
|
251
294
|
parent.set(node, (0, canonical_js_1.toPlain)(e["parent"]) ?? null);
|
|
252
295
|
try {
|
|
253
296
|
auth.set(node, authority_js_1.Authority.fromWire(e["granted"] ?? null));
|
|
254
297
|
}
|
|
255
298
|
catch (exc) {
|
|
256
|
-
failures.
|
|
299
|
+
failures.add("unreadable_granted", `spawn ${node}: unreadable granted (${exc.message})`, {
|
|
300
|
+
seq: orNull(e["seq"]),
|
|
301
|
+
node: orNull(e["node"]),
|
|
302
|
+
});
|
|
257
303
|
}
|
|
258
304
|
}
|
|
259
305
|
}
|
|
260
|
-
return { auth, parent, failures };
|
|
306
|
+
return { auth, parent, failures, definedBy };
|
|
261
307
|
}
|
|
262
308
|
/**
|
|
263
309
|
* A view of the chain from the bundle: each node with its agent, task,
|
|
@@ -596,24 +642,31 @@ const V2_ONLY_FIELDS = [
|
|
|
596
642
|
* invalid regardless of which field it is (merge-gate item 4/(c)).
|
|
597
643
|
*/
|
|
598
644
|
function v2FieldLeaksOnV1(entries) {
|
|
599
|
-
const failures =
|
|
645
|
+
const failures = new FailureLog();
|
|
600
646
|
for (const e of entries) {
|
|
601
647
|
const leaked = V2_ONLY_FIELDS.filter((f) => f in e).sort();
|
|
602
648
|
if (leaked.length > 0) {
|
|
603
|
-
failures.
|
|
604
|
-
`carries v2-only field(s) ${JSON.stringify(leaked)} on a schemaVersion: 1 entry
|
|
649
|
+
failures.add("v2_field_on_v1", `v2_field_on_v1: seq=${pyRepr((0, canonical_js_1.toPlain)(e["seq"]))} event=${pyRepr((0, canonical_js_1.toPlain)(e["event"]))} ` +
|
|
650
|
+
`carries v2-only field(s) ${JSON.stringify(leaked)} on a schemaVersion: 1 entry`, { seq: orNull(e["seq"]), node: orNull(e["node"]) });
|
|
605
651
|
}
|
|
606
652
|
}
|
|
607
653
|
return failures;
|
|
608
654
|
}
|
|
655
|
+
/**
|
|
656
|
+
* `[the execution_binding report, its failures]`. The report's own `failures` key keeps its
|
|
657
|
+
* historical list-of-strings shape — the structured twins ride alongside it rather than inside
|
|
658
|
+
* it, so this sub-report's published shape is unchanged.
|
|
659
|
+
*/
|
|
609
660
|
function executionBinding(entries, bundleV) {
|
|
610
661
|
if (bundleV === 1) {
|
|
611
662
|
const leaked = v2FieldLeaksOnV1(entries);
|
|
612
|
-
return leaked.length > 0
|
|
663
|
+
return leaked.length > 0
|
|
664
|
+
? [{ status: "not applicable", failures: leaked.messages }, leaked]
|
|
665
|
+
: [{ status: "not applicable" }, new FailureLog()];
|
|
613
666
|
}
|
|
614
667
|
if (bundleV !== 2)
|
|
615
|
-
return { status: "not applicable" };
|
|
616
|
-
const failures =
|
|
668
|
+
return [{ status: "not applicable" }, new FailureLog()];
|
|
669
|
+
const failures = new FailureLog();
|
|
617
670
|
const seenCallIds = new Map(); // callId -> [event, node, seq]
|
|
618
671
|
const allows = new Map();
|
|
619
672
|
const outcomes = new Map();
|
|
@@ -629,8 +682,12 @@ function executionBinding(entries, bundleV) {
|
|
|
629
682
|
if (node !== null)
|
|
630
683
|
nodes.add(node);
|
|
631
684
|
const err = validateRoot(e);
|
|
632
|
-
if (err)
|
|
633
|
-
failures.
|
|
685
|
+
if (err) {
|
|
686
|
+
failures.add("invalid_root", `invalid_root: ${err} (seq ${pyRepr(seqForEvent)})`, {
|
|
687
|
+
seq: orNull(e["seq"]),
|
|
688
|
+
node: orNull(e["node"]),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
634
691
|
}
|
|
635
692
|
else if (ev === "spawn") {
|
|
636
693
|
if (node !== null)
|
|
@@ -644,8 +701,12 @@ function executionBinding(entries, bundleV) {
|
|
|
644
701
|
for (const r of (0, canonical_js_1.toPlain)(e["revoked"]) ?? [])
|
|
645
702
|
revokedNodes.add(r);
|
|
646
703
|
const err = validateKill(e);
|
|
647
|
-
if (err)
|
|
648
|
-
failures.
|
|
704
|
+
if (err) {
|
|
705
|
+
failures.add("invalid_kill", `invalid_kill: ${err} (seq ${pyRepr(seqForEvent)})`, {
|
|
706
|
+
seq: orNull(e["seq"]),
|
|
707
|
+
node: orNull(e["node"]),
|
|
708
|
+
});
|
|
709
|
+
}
|
|
649
710
|
}
|
|
650
711
|
if (ev === "allow" || ev === "deny") {
|
|
651
712
|
const cid = (0, canonical_js_1.toPlain)(e["call_id"]);
|
|
@@ -653,8 +714,10 @@ function executionBinding(entries, bundleV) {
|
|
|
653
714
|
if (cid !== null && cid !== undefined) {
|
|
654
715
|
const prior = seenCallIds.get(cid);
|
|
655
716
|
if (prior !== undefined) {
|
|
656
|
-
|
|
657
|
-
|
|
717
|
+
// Positioned on the SECOND sighting: the entry that re-used a call_id is the offending
|
|
718
|
+
// record, the first one having been legitimate when it was written.
|
|
719
|
+
failures.add("duplicate_call_id", `duplicate_call_id: call_id ${cid} on seq ${pyRepr(seq)} (${ev}) already used at seq ` +
|
|
720
|
+
`${pyRepr(prior[2])} (${prior[0]})`, { seq: orNull(e["seq"]), node: orNull(e["node"]), callId: cid });
|
|
658
721
|
}
|
|
659
722
|
else {
|
|
660
723
|
seenCallIds.set(cid, [ev, node, seq]);
|
|
@@ -662,7 +725,11 @@ function executionBinding(entries, bundleV) {
|
|
|
662
725
|
}
|
|
663
726
|
const err = ev === "allow" ? validateAllow(e) : validateDeny(e);
|
|
664
727
|
if (err) {
|
|
665
|
-
failures.
|
|
728
|
+
failures.add(`invalid_${ev}`, `invalid_${ev}: ${err} (seq ${pyRepr(seq)})`, {
|
|
729
|
+
seq: orNull(e["seq"]),
|
|
730
|
+
node: orNull(e["node"]),
|
|
731
|
+
callId: cid ?? null,
|
|
732
|
+
});
|
|
666
733
|
if (ev === "allow" && cid !== null && cid !== undefined)
|
|
667
734
|
invalidAllowIds.add(cid);
|
|
668
735
|
continue;
|
|
@@ -675,12 +742,16 @@ function executionBinding(entries, bundleV) {
|
|
|
675
742
|
const seq = (0, canonical_js_1.toPlain)(e["seq"]);
|
|
676
743
|
const err = validateOutcome(e);
|
|
677
744
|
if (err) {
|
|
678
|
-
failures.
|
|
745
|
+
failures.add("invalid_outcome", `invalid_outcome: ${err} (seq ${pyRepr(seq)})`, {
|
|
746
|
+
seq: orNull(e["seq"]),
|
|
747
|
+
node: orNull(e["node"]),
|
|
748
|
+
callId: cid ?? null,
|
|
749
|
+
});
|
|
679
750
|
continue;
|
|
680
751
|
}
|
|
681
752
|
if (cid !== null && outcomes.has(cid)) {
|
|
682
|
-
failures.
|
|
683
|
-
`${pyRepr((0, canonical_js_1.toPlain)(outcomes.get(cid)["seq"]))})
|
|
753
|
+
failures.add("duplicate_outcome", `duplicate_outcome: call_id ${cid} at seq ${pyRepr(seq)} (first at seq ` +
|
|
754
|
+
`${pyRepr((0, canonical_js_1.toPlain)(outcomes.get(cid)["seq"]))})`, { seq: orNull(e["seq"]), node: orNull(e["node"]), callId: cid });
|
|
684
755
|
continue;
|
|
685
756
|
}
|
|
686
757
|
if (cid !== null)
|
|
@@ -694,29 +765,32 @@ function executionBinding(entries, bundleV) {
|
|
|
694
765
|
// its recorded content disagrees with what was authorized (spec: "parameter equality is
|
|
695
766
|
// established only for calls where both hashes are present; elsewhere only identity and order
|
|
696
767
|
// binding was checked" — params_mismatch is that separate concern).
|
|
768
|
+
// Every failure in this loop is about a PAIR, and is positioned on the `outcome` entry: the
|
|
769
|
+
// allow was a complete, valid record when it was written, and it is the outcome that fails to
|
|
770
|
+
// bind to it (or reports different arguments than were authorized).
|
|
697
771
|
const boundOk = new Set();
|
|
698
772
|
for (const [cid, oc] of outcomes) {
|
|
699
773
|
const allowE = allows.get(cid);
|
|
700
774
|
if (allowE === undefined) {
|
|
701
|
-
failures.
|
|
775
|
+
failures.add("outcome_without_allow", `outcome_without_allow: call_id ${cid} at seq ${pyRepr((0, canonical_js_1.toPlain)(oc["seq"]))} has no allow in this chain`, { seq: orNull(oc["seq"]), node: orNull(oc["node"]), callId: cid });
|
|
702
776
|
continue;
|
|
703
777
|
}
|
|
704
778
|
const nodeOk = (0, canonical_js_1.toPlain)(allowE["node"]) === (0, canonical_js_1.toPlain)(oc["node"]);
|
|
705
779
|
if (!nodeOk) {
|
|
706
|
-
failures.
|
|
707
|
-
`outcome on node ${pyRepr((0, canonical_js_1.toPlain)(oc["node"]))}
|
|
780
|
+
failures.add("cross_ref", `cross_ref: call_id ${cid} allow on node ${pyRepr((0, canonical_js_1.toPlain)(allowE["node"]))} but ` +
|
|
781
|
+
`outcome on node ${pyRepr((0, canonical_js_1.toPlain)(oc["node"]))}`, { seq: orNull(oc["seq"]), node: orNull(oc["node"]), callId: cid });
|
|
708
782
|
}
|
|
709
783
|
const ocSeq = (0, canonical_js_1.toPlain)(oc["seq"]);
|
|
710
784
|
const allowSeq = (0, canonical_js_1.toPlain)(allowE["seq"]);
|
|
711
785
|
const orderOk = typeof ocSeq === "number" && typeof allowSeq === "number" && ocSeq > allowSeq;
|
|
712
786
|
if (!orderOk) {
|
|
713
|
-
failures.
|
|
714
|
-
`after allow seq ${pyRepr(allowSeq ?? null)}
|
|
787
|
+
failures.add("outcome_before_allow", `outcome_before_allow: call_id ${cid} outcome seq ${pyRepr(ocSeq ?? null)} not ` +
|
|
788
|
+
`after allow seq ${pyRepr(allowSeq ?? null)}`, { seq: orNull(oc["seq"]), node: orNull(oc["node"]), callId: cid });
|
|
715
789
|
}
|
|
716
790
|
const ah = (0, canonical_js_1.toPlain)(allowE["authorized_params_hash"]);
|
|
717
791
|
const ih = (0, canonical_js_1.toPlain)(oc["invoked_params_hash"]);
|
|
718
792
|
if (ah !== null && ah !== undefined && ih !== null && ih !== undefined && ah !== ih) {
|
|
719
|
-
failures.
|
|
793
|
+
failures.add("params_mismatch", `params_mismatch: call_id ${cid} authorized_params_hash ${ah} != invoked_params_hash ${ih}`, { seq: orNull(oc["seq"]), node: orNull(oc["node"]), callId: cid });
|
|
720
794
|
}
|
|
721
795
|
if (nodeOk && orderOk)
|
|
722
796
|
boundOk.add(cid);
|
|
@@ -786,13 +860,52 @@ function executionBinding(entries, bundleV) {
|
|
|
786
860
|
}
|
|
787
861
|
if (Object.values(perCall).some((s) => s === "unobserved"))
|
|
788
862
|
escalate("incomplete");
|
|
789
|
-
return
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
863
|
+
return [
|
|
864
|
+
{
|
|
865
|
+
aggregate,
|
|
866
|
+
params_coverage: paramsCoverage(allows, outcomes, invalidAllowIds),
|
|
867
|
+
per_call: perCall,
|
|
868
|
+
per_node_lifecycle: lifecycle,
|
|
869
|
+
failures: failures.messages,
|
|
870
|
+
},
|
|
794
871
|
failures,
|
|
795
|
-
|
|
872
|
+
];
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* `[seq, node]` of the FIRST entry the hash chain does not reproduce at — position only.
|
|
876
|
+
*
|
|
877
|
+
* `AuditLog.verify` stays the authority on WHETHER the chain is broken and on the message this
|
|
878
|
+
* module reports; this walk exists so the structured twin of that message can say WHERE, which
|
|
879
|
+
* the message's own text does not expose in a parseable form. Mirrors `AuditLog.verify`'s walk
|
|
880
|
+
* exactly (same seq/prev_hash/hash order). `[null, null]` when nothing entry-local is wrong — a
|
|
881
|
+
* consistently re-hashed ledger fails against the signed anchor, not here, and that failure is
|
|
882
|
+
* chain-level.
|
|
883
|
+
*/
|
|
884
|
+
function integrityPosition(entries) {
|
|
885
|
+
let prev = audit_js_1.GENESIS;
|
|
886
|
+
for (let i = 0; i < entries.length; i++) {
|
|
887
|
+
const e = entries[i];
|
|
888
|
+
const payload = {};
|
|
889
|
+
for (const [k, v] of Object.entries(e)) {
|
|
890
|
+
if (k !== "hash")
|
|
891
|
+
payload[k] = v;
|
|
892
|
+
}
|
|
893
|
+
let broken;
|
|
894
|
+
try {
|
|
895
|
+
broken =
|
|
896
|
+
orNull(e["seq"]) !== i ||
|
|
897
|
+
orNull(payload["prev_hash"]) !== prev ||
|
|
898
|
+
(0, audit_js_1.hashEntry)(prev, payload) !== orNull(e["hash"]);
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
// An unhashable payload is itself the break, at this entry.
|
|
902
|
+
return [orNull(e["seq"]), orNull(e["node"])];
|
|
903
|
+
}
|
|
904
|
+
if (broken)
|
|
905
|
+
return [orNull(e["seq"]), orNull(e["node"])];
|
|
906
|
+
prev = orNull(e["hash"]);
|
|
907
|
+
}
|
|
908
|
+
return [null, null];
|
|
796
909
|
}
|
|
797
910
|
function verifyBundle(bundle, signer = null, options = {}) {
|
|
798
911
|
const entries = bundle.entries ?? [];
|
|
@@ -808,38 +921,41 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
808
921
|
root: false,
|
|
809
922
|
expected_anchor: "not checked",
|
|
810
923
|
};
|
|
811
|
-
const
|
|
924
|
+
const log = new FailureLog();
|
|
812
925
|
// (0) version: the bundle must declare a schema version this build understands, and — when
|
|
813
926
|
// an anchor is present — the anchor must be anchoring THAT version, not a different one.
|
|
814
927
|
const bundleV = (0, canonical_js_1.toPlain)(bundle.v);
|
|
815
928
|
let versionOk = typeof bundleV === "number" && exports.SUPPORTED_BUNDLE_VERSIONS.has(bundleV);
|
|
816
929
|
if (!versionOk) {
|
|
817
930
|
const supported = Array.from(exports.SUPPORTED_BUNDLE_VERSIONS).sort((a, b) => a - b);
|
|
818
|
-
|
|
931
|
+
log.add("unsupported_version", `unsupported_version: bundle v=${pyRepr(bundleV)} not in [${supported.join(", ")}]`);
|
|
819
932
|
}
|
|
820
933
|
const anchorV = (0, canonical_js_1.toPlain)(anchor["v"]);
|
|
821
934
|
if (anchorPresent && anchorV !== bundleV) {
|
|
822
935
|
versionOk = false;
|
|
823
|
-
|
|
936
|
+
log.add("anchor_version_mismatch", `anchor_version_mismatch: anchor v=${pyRepr(anchorV)} != bundle v=${pyRepr(bundleV)}`);
|
|
824
937
|
}
|
|
825
938
|
// (0a) exactly one root: a rootless bundle (or one splicing in a second root) would otherwise
|
|
826
939
|
// sail through monotonicity/containment trivially — there is nothing to anchor those checks to.
|
|
827
940
|
const rootEvents = entries.filter((e) => (0, canonical_js_1.toPlain)(e["event"]) === "root");
|
|
828
941
|
checks.root = rootEvents.length === 1;
|
|
829
942
|
if (!checks.root) {
|
|
830
|
-
|
|
943
|
+
log.add("missing_root", `missing_root: bundle has ${rootEvents.length} root event(s), expected exactly 1`);
|
|
831
944
|
}
|
|
832
945
|
const rootEntry = rootEvents.length === 1 ? rootEvents[0] : undefined;
|
|
833
946
|
// 0.9.0: a chain is created at ONE schema version and never mixes (spec section 9) — the root
|
|
834
947
|
// entry's v must equal the bundle's declared v, and no OTHER entry may carry a different v.
|
|
835
948
|
if (rootEntry !== undefined && (0, canonical_js_1.toPlain)(rootEntry["v"]) !== bundleV) {
|
|
836
949
|
versionOk = false;
|
|
837
|
-
|
|
950
|
+
log.add("root_version_mismatch", `root_version_mismatch: root v=${pyRepr((0, canonical_js_1.toPlain)(rootEntry["v"]))} != bundle v=${pyRepr(bundleV)}`, { seq: orNull(rootEntry["seq"]), node: orNull(rootEntry["node"]) });
|
|
838
951
|
}
|
|
839
|
-
const
|
|
952
|
+
const mixedEntries = entries.filter((e) => (0, canonical_js_1.toPlain)(e["v"]) !== bundleV);
|
|
953
|
+
const mixed = Array.from(new Set(mixedEntries.map((e) => (0, canonical_js_1.toPlain)(e["v"])))).sort((a, b) => (typeof a === "number" && typeof b === "number" ? a - b : String(a).localeCompare(String(b))));
|
|
840
954
|
if (mixed.length > 0) {
|
|
841
955
|
versionOk = false;
|
|
842
|
-
|
|
956
|
+
// One aggregate message over every offending entry (unchanged); the twin is positioned on
|
|
957
|
+
// the first of them, which is where a reader looks.
|
|
958
|
+
log.add("mixed_entry_versions", `mixed_entry_versions: entries declare v in [${mixed.map((v) => pyRepr(v)).join(", ")}], bundle v=${pyRepr(bundleV)}`, { seq: orNull(mixedEntries[0]["seq"]), node: orNull(mixedEntries[0]["node"]) });
|
|
843
959
|
}
|
|
844
960
|
checks.version = versionOk;
|
|
845
961
|
// (0c) independently retained expected anchor/head: verified against the BUNDLE's actual
|
|
@@ -853,7 +969,7 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
853
969
|
const [expSeq, expHash] = expectedHead;
|
|
854
970
|
if (actualSeq !== expSeq || actualHead !== expHash) {
|
|
855
971
|
expectedOk = false;
|
|
856
|
-
|
|
972
|
+
log.add("expected_head_mismatch", `expected_head_mismatch: bundle head is (seq=${actualSeq}, hash=${actualHead}) but the ` +
|
|
857
973
|
`independently retained expected head is (seq=${expSeq}, hash=${expHash})`);
|
|
858
974
|
}
|
|
859
975
|
}
|
|
@@ -864,7 +980,7 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
864
980
|
(0, canonical_js_1.toPlain)(ea["chain_id"]) !== (0, canonical_js_1.toPlain)(bundle.chain_id) ||
|
|
865
981
|
(0, canonical_js_1.toPlain)(ea["v"]) !== bundleV) {
|
|
866
982
|
expectedOk = false;
|
|
867
|
-
|
|
983
|
+
log.add("expected_anchor_mismatch", "expected_anchor_mismatch: the bundle's actual (seq, head, chainId, v) does not match " +
|
|
868
984
|
"the independently retained expected anchor");
|
|
869
985
|
}
|
|
870
986
|
}
|
|
@@ -874,32 +990,40 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
874
990
|
// must all name the SAME chain. Without this a correctly-signed, internally-consistent bundle
|
|
875
991
|
// for a DIFFERENT chain could be handed to a verifier who believes it is checking this one.
|
|
876
992
|
const bundleChainId = orNull(bundle.chain_id);
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
993
|
+
const foreign = entries.find((e) => orNull(e["chain_id"]) !== bundleChainId);
|
|
994
|
+
const entriesOk = foreign === undefined;
|
|
995
|
+
if (foreign !== undefined) {
|
|
996
|
+
log.add("chain_id_mismatch", `chain_id_mismatch: an entry does not carry chain_id=${pyRepr(bundleChainId)}`, {
|
|
997
|
+
seq: orNull(foreign["seq"]),
|
|
998
|
+
node: orNull(foreign["node"]),
|
|
999
|
+
});
|
|
880
1000
|
}
|
|
881
1001
|
const anchorChainId = orNull(anchor["chain_id"]);
|
|
882
1002
|
const anchorChainOk = !anchorPresent || anchorChainId === bundleChainId;
|
|
883
1003
|
if (!anchorChainOk) {
|
|
884
|
-
|
|
1004
|
+
log.add("chain_id_mismatch", `chain_id_mismatch: anchor chain_id=${pyRepr(anchorChainId)} != bundle chain_id=${pyRepr(bundleChainId)}`);
|
|
885
1005
|
}
|
|
886
1006
|
checks.chain_id = entriesOk && anchorChainOk;
|
|
887
1007
|
// (1) integrity: the hash chain, plus the signed anchor when a key is given.
|
|
888
1008
|
const [okChain, err] = audit_js_1.AuditLog.verify(entries);
|
|
889
|
-
if (!okChain)
|
|
890
|
-
|
|
1009
|
+
if (!okChain) {
|
|
1010
|
+
const [badSeq, badNode] = integrityPosition(entries);
|
|
1011
|
+
log.add("integrity", `integrity: ${err}`, { seq: badSeq, node: badNode });
|
|
1012
|
+
}
|
|
891
1013
|
if (signer !== null) {
|
|
892
1014
|
const [okAnchor, aerr] = audit_js_1.AuditLog.verifyAnchor(entries, anchor, signer);
|
|
893
1015
|
checks.anchor = okAnchor ? "verified" : "FAILED";
|
|
1016
|
+
// Chain-level by construction: the anchor commits to the head of the WHOLE ledger, so a
|
|
1017
|
+
// consistently re-hashed chain has no single offending entry to point at.
|
|
894
1018
|
if (!okAnchor)
|
|
895
|
-
|
|
1019
|
+
log.add("integrity(anchor)", `integrity(anchor): ${aerr}`);
|
|
896
1020
|
checks.integrity = okChain && okAnchor;
|
|
897
1021
|
}
|
|
898
1022
|
else {
|
|
899
1023
|
checks.integrity = okChain;
|
|
900
1024
|
}
|
|
901
|
-
const { auth, parent, failures: afail } = nodeAuthorities(entries);
|
|
902
|
-
|
|
1025
|
+
const { auth, parent, failures: afail, definedBy } = nodeAuthorities(entries);
|
|
1026
|
+
log.extend(afail);
|
|
903
1027
|
// (2) monotonicity: every child ⊆ its parent.
|
|
904
1028
|
let mono = true;
|
|
905
1029
|
for (const [node, pid] of parent) {
|
|
@@ -910,8 +1034,9 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
910
1034
|
const extra = Array.from(child.scopes).filter((s) => !p.scopes.has(s));
|
|
911
1035
|
if (!child.isNarrowerThan(p) && extra.length > 0) {
|
|
912
1036
|
mono = false;
|
|
913
|
-
|
|
914
|
-
|
|
1037
|
+
const spawnE = definedBy.get(node);
|
|
1038
|
+
log.add("monotonicity", `monotonicity: ${node} not ⊆ parent ${pid} (child scopes ` +
|
|
1039
|
+
`[${extra.sort(canonical_js_1.compareCodePoints).map((s) => `'${s}'`).join(", ")}] not held by parent)`, { seq: spawnE === undefined ? null : orNull(spawnE["seq"]), node });
|
|
915
1040
|
}
|
|
916
1041
|
}
|
|
917
1042
|
checks.monotonicity = mono && afail.length === 0;
|
|
@@ -928,19 +1053,25 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
928
1053
|
const a = auth.get(node);
|
|
929
1054
|
if (a === undefined) {
|
|
930
1055
|
contained = false;
|
|
931
|
-
|
|
1056
|
+
log.add("containment", `containment: allow on unknown node ${node}`, {
|
|
1057
|
+
seq: orNull(e["seq"]),
|
|
1058
|
+
node: orNull(e["node"]),
|
|
1059
|
+
callId: orNull(e["call_id"]),
|
|
1060
|
+
});
|
|
932
1061
|
continue;
|
|
933
1062
|
}
|
|
934
1063
|
if (!a.permits(scope, ctx).allowed) {
|
|
935
1064
|
contained = false;
|
|
936
|
-
|
|
937
|
-
`[${Array.from(a.scopes).sort(canonical_js_1.compareCodePoints).map((s) => `'${s}'`).join(", ")}]
|
|
1065
|
+
log.add("containment", `containment: allow of '${scope}' on ${node} outside its authority ` +
|
|
1066
|
+
`[${Array.from(a.scopes).sort(canonical_js_1.compareCodePoints).map((s) => `'${s}'`).join(", ")}]`, { seq: orNull(e["seq"]), node: orNull(e["node"]), callId: orNull(e["call_id"]) });
|
|
938
1067
|
}
|
|
939
1068
|
}
|
|
940
1069
|
checks.containment = contained;
|
|
941
|
-
const eb = versionOk
|
|
942
|
-
|
|
943
|
-
|
|
1070
|
+
const [eb, ebFailures] = versionOk
|
|
1071
|
+
? executionBinding(entries, bundleV)
|
|
1072
|
+
: [{ status: "not applicable" }, new FailureLog()];
|
|
1073
|
+
if (eb.failures !== undefined && eb.failures.length > 0)
|
|
1074
|
+
log.extend(ebFailures);
|
|
944
1075
|
// "anchor" and "expected_anchor" are excluded here — both carry a tri-state status string
|
|
945
1076
|
// ("not checked"/"verified"/"FAILED"), not a plain pass/fail boolean, and a failed check on
|
|
946
1077
|
// either already lands its own entry in `failures`, which the `ok` computation still gates on.
|
|
@@ -950,11 +1081,12 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
950
1081
|
checks.version &&
|
|
951
1082
|
checks.chain_id &&
|
|
952
1083
|
checks.root &&
|
|
953
|
-
|
|
1084
|
+
log.length === 0;
|
|
954
1085
|
return {
|
|
955
1086
|
ok,
|
|
956
1087
|
checks,
|
|
957
|
-
failures,
|
|
1088
|
+
failures: log.messages,
|
|
1089
|
+
failure_details: log.details,
|
|
958
1090
|
nodes: auth.size,
|
|
959
1091
|
actions_checked: actions,
|
|
960
1092
|
chain_id: orNull(bundle.chain_id),
|