@zanii/blackbox 0.3.0 → 0.4.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 (82) hide show
  1. package/README.md +26 -1
  2. package/dist/a2a/index.d.ts +27 -0
  3. package/dist/a2a/index.js +104 -1
  4. package/dist/agents/index.d.ts +8 -0
  5. package/dist/analysis/accuracy.d.ts +24 -0
  6. package/dist/analysis/accuracy.js +45 -0
  7. package/dist/analysis/credential.d.ts +101 -0
  8. package/dist/analysis/credential.js +142 -0
  9. package/dist/analysis/faults.js +115 -0
  10. package/dist/analysis/grounding.d.ts +122 -0
  11. package/dist/analysis/grounding.js +445 -0
  12. package/dist/analysis/hallucination.d.ts +32 -0
  13. package/dist/analysis/hallucination.js +357 -0
  14. package/dist/analysis/index.d.ts +23 -0
  15. package/dist/analysis/index.js +93 -0
  16. package/dist/analysis/memory.d.ts +8 -0
  17. package/dist/analysis/memory.js +35 -8
  18. package/dist/analysis/reference.d.ts +49 -0
  19. package/dist/analysis/reference.js +164 -0
  20. package/dist/analysis/taxonomy.js +1 -0
  21. package/dist/approvals/index.d.ts +23 -0
  22. package/dist/approvals/index.js +48 -0
  23. package/dist/archive/parquet.d.ts +2 -0
  24. package/dist/archive/parquet.js +185 -0
  25. package/dist/badge/index.d.ts +16 -0
  26. package/dist/badge/index.js +48 -0
  27. package/dist/bom/index.js +20 -0
  28. package/dist/cli.js +114 -10
  29. package/dist/compliance/art12.js +36 -9
  30. package/dist/compliance/index.d.ts +36 -2
  31. package/dist/compliance/index.js +78 -11
  32. package/dist/compliance/zanii.d.ts +29 -0
  33. package/dist/compliance/zanii.js +84 -0
  34. package/dist/constitution/index.d.ts +57 -0
  35. package/dist/constitution/index.js +131 -0
  36. package/dist/cv/index.d.ts +39 -0
  37. package/dist/cv/index.js +108 -0
  38. package/dist/disclosure/index.d.ts +31 -0
  39. package/dist/disclosure/index.js +113 -0
  40. package/dist/encryption/index.d.ts +9 -0
  41. package/dist/encryption/index.js +31 -0
  42. package/dist/evidence/index.d.ts +60 -0
  43. package/dist/evidence/index.js +151 -0
  44. package/dist/federation/index.d.ts +35 -0
  45. package/dist/federation/index.js +102 -0
  46. package/dist/finance/index.d.ts +126 -0
  47. package/dist/finance/index.js +320 -0
  48. package/dist/fleet/index.js +9 -0
  49. package/dist/gov/index.d.ts +108 -0
  50. package/dist/gov/index.js +225 -0
  51. package/dist/health/index.d.ts +120 -0
  52. package/dist/health/index.js +233 -0
  53. package/dist/index.d.ts +28 -6
  54. package/dist/index.js +28 -6
  55. package/dist/memory/index.d.ts +36 -0
  56. package/dist/memory/index.js +85 -0
  57. package/dist/occurrence/index.d.ts +11 -0
  58. package/dist/occurrence/index.js +18 -0
  59. package/dist/otlp/index.js +28 -1
  60. package/dist/packs/index.js +44 -4
  61. package/dist/policy/delta.js +7 -1
  62. package/dist/policy/index.d.ts +23 -6
  63. package/dist/policy/index.js +151 -8
  64. package/dist/policy/zanii.d.ts +31 -0
  65. package/dist/policy/zanii.js +87 -0
  66. package/dist/pq/index.d.ts +23 -0
  67. package/dist/pq/index.js +104 -0
  68. package/dist/search/index.d.ts +23 -0
  69. package/dist/search/index.js +69 -0
  70. package/dist/session/index.d.ts +89 -1
  71. package/dist/session/index.js +143 -11
  72. package/dist/sla/index.d.ts +61 -0
  73. package/dist/sla/index.js +197 -0
  74. package/dist/succession/index.d.ts +50 -0
  75. package/dist/succession/index.js +123 -0
  76. package/dist/tokens/index.d.ts +6 -0
  77. package/dist/tokens/index.js +46 -0
  78. package/dist/version.d.ts +1 -1
  79. package/dist/version.js +1 -1
  80. package/dist/walls/index.d.ts +31 -0
  81. package/dist/walls/index.js +119 -0
  82. package/package.json +1 -1
@@ -0,0 +1,131 @@
1
+ // The signed rulebook (spec/governance.md), in Zanii's constitution v1 format, byte for byte with
2
+ // @zanii/constitution / zanii.constitution, and a session's governance against it. Pure; mirrors
3
+ // sdks/python/src/zanii_blackbox/constitution.py; pinned by spec/vectors/governance.json.
4
+ import { canonicalBytes, jcsHash, publicKeyFromDid, scopeCovers } from "@zanii/core";
5
+ import { ed25519Sign, ed25519Verify } from "../transparency/index.js";
6
+ const EFFECTS = new Set(["forbid", "require_approval", "allow"]);
7
+ /** Builds and signs the rulebook; rules are evaluated first match wins, top to bottom. */
8
+ export function createConstitution(input) {
9
+ const { issuer, name, rules, defaultEffect = "allow", createdAt } = input;
10
+ if (!issuer)
11
+ throw new Error("issuer is required");
12
+ if (!name)
13
+ throw new Error("name is required");
14
+ if (!rules.length)
15
+ throw new Error("a constitution with no rules constrains nothing");
16
+ const ids = new Set();
17
+ for (const r of rules) {
18
+ if (!r.id)
19
+ throw new Error("every rule needs an id");
20
+ if (ids.has(r.id))
21
+ throw new Error(`duplicate rule id '${r.id}'`);
22
+ ids.add(r.id);
23
+ if (!EFFECTS.has(r.effect))
24
+ throw new Error(`rule '${r.id}': invalid effect '${r.effect}'`);
25
+ if (!r.targets?.length)
26
+ throw new Error(`rule '${r.id}' has no targets`);
27
+ }
28
+ if (defaultEffect !== "allow" && defaultEffect !== "forbid")
29
+ throw new Error("default_effect must be 'allow' or 'forbid'");
30
+ if (Number.isNaN(Date.parse(createdAt)))
31
+ throw new Error("created_at must be an ISO timestamp");
32
+ const unsigned = {
33
+ v: 1,
34
+ type: "constitution",
35
+ issuer,
36
+ name,
37
+ rules,
38
+ default: defaultEffect,
39
+ created_at: createdAt,
40
+ };
41
+ const sig = Buffer.from(ed25519Sign(input.issuerPrivateKey, canonicalBytes(unsigned))).toString("hex");
42
+ return { ...unsigned, sig: `ed25519:${sig}` };
43
+ }
44
+ /** Structure and the issuer's signature. */
45
+ export function verifyConstitution(c) {
46
+ const p = (c ?? {});
47
+ if (p.v !== 1 || p.type !== "constitution" || !p.issuer || !p.name)
48
+ return false;
49
+ if (!Array.isArray(p.rules) || p.rules.length === 0)
50
+ return false;
51
+ const ids = p.rules.map((r) => (r && typeof r === "object" ? r.id : undefined));
52
+ if (new Set(ids).size !== ids.length)
53
+ return false;
54
+ for (const r of p.rules)
55
+ if (!r || typeof r !== "object" || !EFFECTS.has(r.effect) || !r.targets?.length)
56
+ return false;
57
+ if (p.default !== "allow" && p.default !== "forbid")
58
+ return false;
59
+ const pub = publicKeyFromDid(p.issuer);
60
+ const m = typeof p.sig === "string" ? /^ed25519:([0-9a-f]{128})$/.exec(p.sig) : null;
61
+ if (!pub || !m)
62
+ return false;
63
+ const { sig: _sig, ...unsigned } = p;
64
+ return ed25519Verify(pub, canonicalBytes(unsigned), Buffer.from(m[1], "hex"));
65
+ }
66
+ /** The `manifest_hash`: RFC 8785 JSON, SHA-256. */
67
+ export const constitutionHash = (c) => jcsHash(c);
68
+ /** First match wins, then the default. */
69
+ export function evaluateConstitution(c, target) {
70
+ for (const r of c.rules)
71
+ if (r.targets.some((t) => scopeCovers(t, target)))
72
+ return { effect: r.effect, rule: r };
73
+ return { effect: c.default, rule: null };
74
+ }
75
+ /** spec/governance.md §3: the action's target, or null when the event isn't an action. */
76
+ function targetOf(e) {
77
+ const m = e.meta;
78
+ const policy = m.policy;
79
+ if (policy?.action === "deny")
80
+ return null; // refused: it never ran
81
+ if (e.kind === "tool.call" && typeof m.tool === "string")
82
+ return m.server !== undefined
83
+ ? `mcp.${String(m.server)}.${m.tool}`
84
+ : `tool.${String(m.via)}.${m.tool}`;
85
+ if (e.kind === "llm.request")
86
+ return `model.${String(m.provider ?? "unknown")}`;
87
+ if (e.kind === "sdk.event" && m.type === "tool.call")
88
+ return `sdk.${String(m.name ?? "unknown")}`;
89
+ return null;
90
+ }
91
+ /** spec/governance.md §3: a session's record against the rulebook. */
92
+ export function governanceOf(lines, c) {
93
+ const manifest = constitutionHash(c);
94
+ const events = lines.map((l) => JSON.parse(l));
95
+ const open = events.find((e) => e.kind === "session.open");
96
+ const governed = open?.meta.manifest_hash === manifest;
97
+ const approved = new Set(events
98
+ .filter((e) => e.kind === "control" && e.meta.action === "approval" && e.meta.decision === "approve")
99
+ .map((e) => e.meta.call_seq));
100
+ const violations = [];
101
+ const approvals = [];
102
+ let actions = 0;
103
+ for (const e of events) {
104
+ const target = targetOf(e);
105
+ if (target === null)
106
+ continue;
107
+ actions++;
108
+ if (!governed) {
109
+ violations.push({ seq: e.seq, target, kind: "ungoverned" });
110
+ continue;
111
+ }
112
+ const ev = evaluateConstitution(c, target);
113
+ if (ev.effect === "forbid")
114
+ violations.push({
115
+ seq: e.seq,
116
+ target,
117
+ kind: "forbidden",
118
+ ...(ev.rule ? { rule_id: ev.rule.id } : {}),
119
+ });
120
+ else if (ev.effect === "require_approval" && ev.rule)
121
+ approvals.push({ seq: e.seq, target, rule_id: ev.rule.id, approved: approved.has(e.seq) });
122
+ }
123
+ return {
124
+ manifest_hash: manifest,
125
+ governed,
126
+ ok: violations.length === 0,
127
+ actions,
128
+ violations,
129
+ approvals_required: approvals,
130
+ };
131
+ }
@@ -0,0 +1,39 @@
1
+ type Obj = Record<string, unknown>;
2
+ /** Zanii's agentcv from its parts. */
3
+ export declare function buildCv(o: {
4
+ did: string;
5
+ summary: Obj;
6
+ generatedAt: string;
7
+ owner?: string | null;
8
+ work?: Obj[];
9
+ authority?: Obj[];
10
+ benchmarks?: Obj[];
11
+ skills?: string[];
12
+ }): Obj;
13
+ export declare const cvHash: (cv: Obj) => string;
14
+ /** Zanii's offline check: structure, the subject's signature when there is one, and warnings. */
15
+ export declare function verifyCv(cv: unknown): {
16
+ ok: boolean;
17
+ reasons: string[];
18
+ signed: boolean | null;
19
+ warnings: string[];
20
+ };
21
+ type Summary = {
22
+ session_id: string;
23
+ label: string | null;
24
+ started_at: string;
25
+ last_event_at: string;
26
+ closed: boolean;
27
+ outcome: string | null;
28
+ tools: Record<string, number>;
29
+ };
30
+ /**
31
+ * The agent behind a label, as Zanii's agentcv, from session summaries (spec/fleet.md §1) and the
32
+ * anchor receipt of each anchored session: summary.receipts counts the anchored sessions, and each
33
+ * work entry (the newest 20) cites its anchor receipt.
34
+ */
35
+ export declare function agentCv(label: string, summaries: readonly Summary[], o: {
36
+ anchored: Readonly<Record<string, string>>;
37
+ generatedAt: string;
38
+ }): Obj;
39
+ export {};
@@ -0,0 +1,108 @@
1
+ // Agent CVs (spec/fleet.md §7): Zanii's agentcv, a portable view over what the record already
2
+ // holds, every work entry backed by a ledger ref (@zanii/cv), built here from the gateway's own
3
+ // logbook. Pure; mirrors sdks/python/src/zanii_blackbox/cv.py; pinned by spec/vectors/cv.json.
4
+ import { canonicalBytes, jcsHash, publicKeyFromDid } from "@zanii/core";
5
+ import { ed25519Verify } from "../transparency/index.js";
6
+ const WORK_SHOWN = 20;
7
+ const validTs = (ts) => typeof ts === "string" && !Number.isNaN(Date.parse(ts));
8
+ /** Zanii's agentcv from its parts. */
9
+ export function buildCv(o) {
10
+ if (!o.did)
11
+ throw new Error("did is required");
12
+ if (!o.summary || Object.keys(o.summary).length === 0)
13
+ throw new Error("summary is required (the ledger aggregate — the un-inflatable part)");
14
+ if (!validTs(o.generatedAt))
15
+ throw new Error("generated_at must be an ISO timestamp");
16
+ return {
17
+ v: 1,
18
+ type: "agentcv",
19
+ did: o.did,
20
+ owner: o.owner ?? null,
21
+ summary: o.summary,
22
+ work: o.work ?? [],
23
+ authority: o.authority ?? [],
24
+ benchmarks: o.benchmarks ?? [],
25
+ skills: o.skills ?? [],
26
+ generated_at: o.generatedAt,
27
+ };
28
+ }
29
+ export const cvHash = (cv) => jcsHash(cv);
30
+ const skillBacked = (skill, scopes) => [...scopes].some((sc) => typeof sc === "string" &&
31
+ (sc === skill || sc === "*" || (sc.endsWith(".*") && skill.startsWith(sc.slice(0, -1)))));
32
+ /** Zanii's offline check: structure, the subject's signature when there is one, and warnings. */
33
+ export function verifyCv(cv) {
34
+ const c = (cv ?? {});
35
+ const reasons = [];
36
+ const warnings = [];
37
+ if (c.v !== 1 || c.type !== "agentcv")
38
+ reasons.push("not an agentcv (v1)");
39
+ if (!c.did)
40
+ reasons.push("missing did");
41
+ const summary = c.summary;
42
+ if (!summary || typeof summary !== "object" || !Number.isSafeInteger(summary.receipts))
43
+ reasons.push("missing/invalid summary (the ledger aggregate)");
44
+ if (!validTs(c.generated_at))
45
+ reasons.push("missing/invalid generated_at");
46
+ let signed = null;
47
+ if (typeof c.sig === "string") {
48
+ const pub = publicKeyFromDid(String(c.did));
49
+ const m = /^ed25519:([0-9a-f]{128})$/.exec(c.sig);
50
+ if (!pub || !m) {
51
+ signed = false;
52
+ reasons.push("malformed signature or subject key");
53
+ }
54
+ else {
55
+ const { sig: _sig, ...unsigned } = c;
56
+ signed = ed25519Verify(pub, canonicalBytes(unsigned), Buffer.from(m[1], "hex"));
57
+ if (!signed)
58
+ reasons.push("signature does not verify against the subject did (CV was altered or signed by someone else)");
59
+ }
60
+ }
61
+ if (reasons.length === 0 && summary) {
62
+ const scopes = new Set((c.authority ?? []).map((a) => a.scope));
63
+ for (const s of c.skills ?? [])
64
+ if (!skillBacked(s, scopes))
65
+ warnings.push(`skill "${s}" is claimed but not backed by any delegated authority scope`);
66
+ const work = c.work ?? [];
67
+ if (work.length > summary.receipts)
68
+ warnings.push(`work lists ${work.length} entries but the ledger only counts ${summary.receipts} receipts — the curated list cannot exceed the real total`);
69
+ if (summary.revoked)
70
+ warnings.push("the subject's authority is REVOKED on the ledger");
71
+ }
72
+ return { ok: reasons.length === 0, reasons, signed, warnings };
73
+ }
74
+ /**
75
+ * The agent behind a label, as Zanii's agentcv, from session summaries (spec/fleet.md §1) and the
76
+ * anchor receipt of each anchored session: summary.receipts counts the anchored sessions, and each
77
+ * work entry (the newest 20) cites its anchor receipt.
78
+ */
79
+ export function agentCv(label, summaries, o) {
80
+ const mine = summaries.filter((s) => s.label === label);
81
+ if (mine.length === 0)
82
+ throw new Error(`no sessions for ${label}`);
83
+ const tools = new Set(mine.flatMap((s) => Object.keys(s.tools ?? {})));
84
+ const closed = mine.filter((s) => s.closed);
85
+ const onLedger = mine.filter((s) => s.session_id in o.anchored);
86
+ const newest = [...onLedger].sort((a, b) => a.last_event_at < b.last_event_at ? 1 : a.last_event_at > b.last_event_at ? -1 : 0);
87
+ const max = (xs) => xs.reduce((a, b) => (b > a ? b : a));
88
+ const min = (xs) => xs.reduce((a, b) => (b < a ? b : a));
89
+ return buildCv({
90
+ did: `urn:blackbox:agent:${label}`,
91
+ summary: {
92
+ receipts: onLedger.length,
93
+ distinct_counterparties: tools.size,
94
+ first_seen: min(mine.map((s) => s.started_at)),
95
+ last_seen: max(mine.map((s) => s.last_event_at)),
96
+ revoked: false,
97
+ fully_anchored: closed.length > 0 && closed.every((s) => s.session_id in o.anchored),
98
+ audit_bundle: null,
99
+ },
100
+ generatedAt: o.generatedAt,
101
+ work: newest.slice(0, WORK_SHOWN).map((s) => ({
102
+ session_id: s.session_id,
103
+ outcome: s.outcome,
104
+ at: s.last_event_at,
105
+ ref: o.anchored[s.session_id],
106
+ })),
107
+ });
108
+ }
@@ -0,0 +1,31 @@
1
+ export interface DisclosureEnvelope {
2
+ _zr: "sd-v1";
3
+ root: string;
4
+ keys: string[];
5
+ }
6
+ export interface Disclosure {
7
+ _zr: "sd-disclosure-v1";
8
+ key: string;
9
+ value: unknown;
10
+ salt: string;
11
+ index: number;
12
+ size: number;
13
+ proof: string[];
14
+ root: string;
15
+ }
16
+ /**
17
+ * The scalar leaves of `args` (an empty object or array is a leaf too), as [JSON Pointer, value],
18
+ * sorted by pointer; null when `args` isn't an object, or is too big or too deep to commit to.
19
+ */
20
+ export declare function argumentLeaves(args: unknown): Array<[string, unknown]> | null;
21
+ /** `meta.disclosure` for a call with these arguments and body hash; null when there's none. */
22
+ export declare function disclosureEnvelope(args: unknown, bodyHash: string, key: Uint8Array): DisclosureEnvelope | null;
23
+ /** One disclosure per pointer. Throws on a pointer that isn't a leaf of the arguments. */
24
+ export declare function discloseFields(args: unknown, bodyHash: string, key: Uint8Array, pointers: readonly string[]): Disclosure[];
25
+ /** Zanii's check: the root and size match, keys[index] is the key, and the proof verifies. */
26
+ export declare function verifyDisclosure(d: unknown, envelope: unknown): boolean;
27
+ /** Checks disclosures against the envelope in a stored event line's `meta.disclosure`. */
28
+ export declare function verifyEventDisclosure(line: string, disclosures: readonly unknown[]): {
29
+ ok: boolean;
30
+ fields: Record<string, unknown>;
31
+ };
@@ -0,0 +1,113 @@
1
+ // Selective disclosure of a tool call's arguments (spec/disclosure.md), in Zanii's Selective
2
+ // Disclosure v1 format (@zanii/redact / zanii.redact, byte for byte). Pure; mirrors
3
+ // sdks/python/src/zanii_blackbox/disclosure.py; pinned by spec/vectors/disclosure.json.
4
+ import { createHmac } from "node:crypto";
5
+ import { canonicalBytes, inclusionProof, leafHash, merkleRoot, verifyInclusion } from "@zanii/core";
6
+ const MAX_LEAVES = 256;
7
+ const MAX_DEPTH = 8;
8
+ const hex = (b) => Buffer.from(b).toString("hex");
9
+ const escapeKey = (k) => k.replaceAll("~", "~0").replaceAll("/", "~1");
10
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
11
+ /**
12
+ * The scalar leaves of `args` (an empty object or array is a leaf too), as [JSON Pointer, value],
13
+ * sorted by pointer; null when `args` isn't an object, or is too big or too deep to commit to.
14
+ */
15
+ export function argumentLeaves(args) {
16
+ if (!isObj(args))
17
+ return null;
18
+ const out = [];
19
+ let tooDeep = false;
20
+ const walk = (v, ptr, depth) => {
21
+ if (depth > MAX_DEPTH)
22
+ tooDeep = true;
23
+ if (tooDeep || out.length > MAX_LEAVES)
24
+ return;
25
+ if (isObj(v) && Object.keys(v).length)
26
+ for (const [k, x] of Object.entries(v))
27
+ walk(x, `${ptr}/${escapeKey(k)}`, depth + 1);
28
+ else if (Array.isArray(v) && v.length)
29
+ for (const [i, x] of v.entries())
30
+ walk(x, `${ptr}/${i}`, depth + 1);
31
+ else
32
+ out.push([ptr, v]);
33
+ };
34
+ walk(args, "", 0);
35
+ if (tooDeep || out.length > MAX_LEAVES || out.length === 0)
36
+ return null;
37
+ return out.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
38
+ }
39
+ const salt = (key, bodyHash, pointer) => createHmac("sha256", key).update(`${bodyHash}\n${pointer}`).digest("hex").slice(0, 32);
40
+ const leaf = (pointer, value, s) => leafHash(canonicalBytes(["zr-field-v1", pointer, value, s]));
41
+ function tree(args, bodyHash, key) {
42
+ const leaves = argumentLeaves(args);
43
+ if (!leaves)
44
+ return null;
45
+ const salts = leaves.map(([p]) => salt(key, bodyHash, p));
46
+ const hashes = leaves.map(([p, v], i) => leaf(p, v, salts[i]));
47
+ return { leaves, salts, hashes, root: `sha256:${hex(merkleRoot(hashes))}` };
48
+ }
49
+ /** `meta.disclosure` for a call with these arguments and body hash; null when there's none. */
50
+ export function disclosureEnvelope(args, bodyHash, key) {
51
+ const t = tree(args, bodyHash, key);
52
+ return t ? { _zr: "sd-v1", root: t.root, keys: t.leaves.map(([p]) => p) } : null;
53
+ }
54
+ /** One disclosure per pointer. Throws on a pointer that isn't a leaf of the arguments. */
55
+ export function discloseFields(args, bodyHash, key, pointers) {
56
+ const t = tree(args, bodyHash, key);
57
+ if (!t)
58
+ throw new Error("these arguments have no commitment");
59
+ return pointers.map((p) => {
60
+ const index = t.leaves.findIndex(([k]) => k === p);
61
+ if (index < 0)
62
+ throw new Error(`not a field of this call: ${p}`);
63
+ return {
64
+ _zr: "sd-disclosure-v1",
65
+ key: p,
66
+ value: t.leaves[index][1],
67
+ salt: t.salts[index],
68
+ index,
69
+ size: t.leaves.length,
70
+ proof: inclusionProof(index, t.hashes).map(hex),
71
+ root: t.root,
72
+ };
73
+ });
74
+ }
75
+ /** Zanii's check: the root and size match, keys[index] is the key, and the proof verifies. */
76
+ export function verifyDisclosure(d, envelope) {
77
+ const x = d;
78
+ const e = envelope;
79
+ if (!x || !e || x.root !== e.root || !Array.isArray(e.keys) || typeof e.root !== "string")
80
+ return false;
81
+ const { index, size, key, salt: s, proof } = x;
82
+ if (size !== e.keys.length || typeof index !== "number" || !Number.isInteger(index))
83
+ return false;
84
+ if (index < 0 || index >= e.keys.length || e.keys[index] !== key)
85
+ return false;
86
+ if (typeof key !== "string" || typeof s !== "string" || !Array.isArray(proof))
87
+ return false;
88
+ try {
89
+ return verifyInclusion(leaf(key, x.value, s), index, size, proof.map((p) => new Uint8Array(Buffer.from(String(p), "hex"))), new Uint8Array(Buffer.from(e.root.replace(/^sha256:/, ""), "hex")));
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ /** Checks disclosures against the envelope in a stored event line's `meta.disclosure`. */
96
+ export function verifyEventDisclosure(line, disclosures) {
97
+ let envelope;
98
+ try {
99
+ envelope = JSON.parse(line).meta?.disclosure;
100
+ }
101
+ catch {
102
+ return { ok: false, fields: {} };
103
+ }
104
+ const fields = {};
105
+ let ok = envelope !== undefined && disclosures.length > 0;
106
+ for (const d of disclosures) {
107
+ if (verifyDisclosure(d, envelope))
108
+ fields[d.key] = d.value;
109
+ else
110
+ ok = false;
111
+ }
112
+ return { ok, fields };
113
+ }
@@ -0,0 +1,9 @@
1
+ export interface SealedSegment {
2
+ hash: string;
3
+ /** base64: the stored bytes (nonce ‖ ciphertext ‖ tag), or the plain bytes when `key` is null. */
4
+ sealed: string;
5
+ /** base64: the segment's own key, or null for a segment stored before encryption. */
6
+ key: string | null;
7
+ }
8
+ /** The body, when every segment opens, hashes to its own hash, and they join to `bodyHash`; else null. */
9
+ export declare function openSegments(segments: readonly SealedSegment[], bodyHash: string): Uint8Array | null;
@@ -0,0 +1,31 @@
1
+ // The auditor's side of spec/encryption.md §4: open one body from its sealed segments and their
2
+ // keys, and check it against the hash in the chain. Pure; mirrors
3
+ // sdks/python/src/zanii_blackbox/encryption.py; pinned by spec/vectors/body-encryption.json.
4
+ import { createDecipheriv, createHash } from "node:crypto";
5
+ const sha256 = (b) => `sha256:${createHash("sha256").update(b).digest("hex")}`;
6
+ function open(key, hash, sealed) {
7
+ if (key.length !== 32 || sealed.length < 28)
8
+ return null;
9
+ try {
10
+ const d = createDecipheriv("aes-256-gcm", key, sealed.subarray(0, 12));
11
+ d.setAAD(Buffer.from(hash, "utf8"));
12
+ d.setAuthTag(sealed.subarray(sealed.length - 16));
13
+ return Buffer.concat([d.update(sealed.subarray(12, sealed.length - 16)), d.final()]);
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ /** The body, when every segment opens, hashes to its own hash, and they join to `bodyHash`; else null. */
20
+ export function openSegments(segments, bodyHash) {
21
+ const parts = [];
22
+ for (const s of segments) {
23
+ const bytes = Buffer.from(s.sealed, "base64");
24
+ const plain = s.key === null ? bytes : open(Buffer.from(s.key, "base64"), s.hash, bytes);
25
+ if (!plain || sha256(plain) !== s.hash)
26
+ return null;
27
+ parts.push(plain);
28
+ }
29
+ const body = Buffer.concat(parts);
30
+ return sha256(body) === bodyHash ? new Uint8Array(body) : null;
31
+ }
@@ -0,0 +1,60 @@
1
+ import type { Bundle, BundleReport } from "../verify/index.ts";
2
+ type Text = {
3
+ en: string;
4
+ ar: string;
5
+ };
6
+ export interface EvidencePackOptions {
7
+ generated_at: string;
8
+ bundle_sha256: string;
9
+ proofs: "checked" | "not_checked";
10
+ }
11
+ /** spec/investigations.md §7. */
12
+ export declare function evidencePack(bundle: Bundle, report: BundleReport, opts: EvidencePackOptions): {
13
+ v: 1;
14
+ kind: "blackbox-evidence-pack";
15
+ session_id: string;
16
+ label: string | null;
17
+ agent: {} | null;
18
+ anchored_by: string | null;
19
+ ledger_url: string | null;
20
+ record: {
21
+ events: number;
22
+ head: string | null;
23
+ first_at: string | null;
24
+ last_at: string | null;
25
+ closed: boolean;
26
+ };
27
+ anchors: {
28
+ count: number;
29
+ last: {
30
+ count: number;
31
+ receipt_hash: string;
32
+ anchored_at: string;
33
+ } | null;
34
+ };
35
+ transparency: {
36
+ statement: boolean;
37
+ receipt: boolean;
38
+ checkpoint: boolean;
39
+ timestamp: boolean;
40
+ } | null;
41
+ verified: boolean;
42
+ verification: {
43
+ chain: string;
44
+ identity: string;
45
+ proofs: string;
46
+ };
47
+ legal_basis: Text;
48
+ steps: {
49
+ en: string;
50
+ ar: string;
51
+ n: number;
52
+ }[];
53
+ reproduce: string[];
54
+ generated_at: string;
55
+ bundle_sha256: string;
56
+ };
57
+ export type EvidencePack = ReturnType<typeof evidencePack>;
58
+ /** The Markdown copy (spec/investigations.md §7): `en`, `ar` or `both`. */
59
+ export declare function renderEvidencePack(pack: EvidencePack, lang?: "en" | "ar" | "both"): string;
60
+ export {};