@zanii/core 0.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 (48) hide show
  1. package/LICENSE +159 -0
  2. package/README.md +64 -0
  3. package/dist/a2a.d.ts +58 -0
  4. package/dist/a2a.d.ts.map +1 -0
  5. package/dist/a2a.js +102 -0
  6. package/dist/a2a.js.map +1 -0
  7. package/dist/bundle.d.ts +55 -0
  8. package/dist/bundle.d.ts.map +1 -0
  9. package/dist/bundle.js +92 -0
  10. package/dist/bundle.js.map +1 -0
  11. package/dist/delegation.d.ts +48 -0
  12. package/dist/delegation.d.ts.map +1 -0
  13. package/dist/delegation.js +110 -0
  14. package/dist/delegation.js.map +1 -0
  15. package/dist/encoding.d.ts +8 -0
  16. package/dist/encoding.d.ts.map +1 -0
  17. package/dist/encoding.js +29 -0
  18. package/dist/encoding.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +9 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/keys.d.ts +10 -0
  24. package/dist/keys.d.ts.map +1 -0
  25. package/dist/keys.js +33 -0
  26. package/dist/keys.js.map +1 -0
  27. package/dist/merkle.d.ts +12 -0
  28. package/dist/merkle.d.ts.map +1 -0
  29. package/dist/merkle.js +146 -0
  30. package/dist/merkle.js.map +1 -0
  31. package/dist/receipt.d.ts +49 -0
  32. package/dist/receipt.d.ts.map +1 -0
  33. package/dist/receipt.js +87 -0
  34. package/dist/receipt.js.map +1 -0
  35. package/dist/sth.d.ts +17 -0
  36. package/dist/sth.d.ts.map +1 -0
  37. package/dist/sth.js +24 -0
  38. package/dist/sth.js.map +1 -0
  39. package/package.json +43 -0
  40. package/src/a2a.ts +139 -0
  41. package/src/bundle.ts +176 -0
  42. package/src/delegation.ts +134 -0
  43. package/src/encoding.ts +30 -0
  44. package/src/index.ts +56 -0
  45. package/src/keys.ts +38 -0
  46. package/src/merkle.ts +154 -0
  47. package/src/receipt.ts +123 -0
  48. package/src/sth.ts +35 -0
package/src/merkle.ts ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * RFC 6962 / RFC 9162 Merkle tree: root, inclusion proofs, consistency proofs.
3
+ * All functions operate on arrays of *leaf hashes* (already 0x00-prefixed-hashed).
4
+ */
5
+ import { sha256 } from '@noble/hashes/sha256';
6
+
7
+ const LEAF_PREFIX = 0x00;
8
+ const NODE_PREFIX = 0x01;
9
+
10
+ /** SHA-256(0x00 || data) */
11
+ export function leafHash(data: Uint8Array): Uint8Array {
12
+ const buf = new Uint8Array(1 + data.length);
13
+ buf[0] = LEAF_PREFIX;
14
+ buf.set(data, 1);
15
+ return sha256(buf);
16
+ }
17
+
18
+ function nodeHash(left: Uint8Array, right: Uint8Array): Uint8Array {
19
+ const buf = new Uint8Array(1 + 64);
20
+ buf[0] = NODE_PREFIX;
21
+ buf.set(left, 1);
22
+ buf.set(right, 33);
23
+ return sha256(buf);
24
+ }
25
+
26
+ /** Largest power of two strictly less than n (n >= 2). */
27
+ function split(n: number): number {
28
+ let k = 1;
29
+ while (k * 2 < n) k *= 2;
30
+ return k;
31
+ }
32
+
33
+ // ponytail: recursive slices, O(n log n) per call — swap for an incremental level-cached tree past ~100k leaves
34
+
35
+ export function merkleRoot(leafHashes: Uint8Array[]): Uint8Array {
36
+ const n = leafHashes.length;
37
+ if (n === 0) return sha256(new Uint8Array(0));
38
+ if (n === 1) return leafHashes[0]!;
39
+ const k = split(n);
40
+ return nodeHash(merkleRoot(leafHashes.slice(0, k)), merkleRoot(leafHashes.slice(k)));
41
+ }
42
+
43
+ /** Audit path for leaf `index` in a tree of the given leaf hashes (RFC 6962 §2.1.1). */
44
+ export function inclusionProof(index: number, leafHashes: Uint8Array[]): Uint8Array[] {
45
+ const n = leafHashes.length;
46
+ if (!Number.isInteger(index) || index < 0 || index >= n) {
47
+ throw new Error(`leaf index ${index} out of range for tree size ${n}`);
48
+ }
49
+ if (n === 1) return [];
50
+ const k = split(n);
51
+ return index < k
52
+ ? [...inclusionProof(index, leafHashes.slice(0, k)), merkleRoot(leafHashes.slice(k))]
53
+ : [...inclusionProof(index - k, leafHashes.slice(k)), merkleRoot(leafHashes.slice(0, k))];
54
+ }
55
+
56
+ const eq = (a: Uint8Array, b: Uint8Array) => a.length === b.length && a.every((x, i) => x === b[i]);
57
+
58
+ /** RFC 9162 §2.1.3.2. Pure; returns false on any malformed input. */
59
+ export function verifyInclusion(
60
+ leaf: Uint8Array,
61
+ index: number,
62
+ size: number,
63
+ proof: Uint8Array[],
64
+ root: Uint8Array,
65
+ ): boolean {
66
+ if (!Number.isInteger(index) || !Number.isInteger(size) || index < 0 || index >= size) return false;
67
+ let fn = index;
68
+ let sn = size - 1;
69
+ let r = leaf;
70
+ for (const p of proof) {
71
+ if (p.length !== 32) return false;
72
+ if (sn === 0) return false;
73
+ if (fn % 2 === 1 || fn === sn) {
74
+ r = nodeHash(p, r);
75
+ if (fn % 2 === 0) {
76
+ while (fn % 2 === 0 && fn !== 0) {
77
+ fn = Math.floor(fn / 2);
78
+ sn = Math.floor(sn / 2);
79
+ }
80
+ }
81
+ } else {
82
+ r = nodeHash(r, p);
83
+ }
84
+ fn = Math.floor(fn / 2);
85
+ sn = Math.floor(sn / 2);
86
+ }
87
+ return sn === 0 && eq(r, root);
88
+ }
89
+
90
+ /** Consistency proof from tree size m to the current tree (RFC 6962 §2.1.2). */
91
+ export function consistencyProof(m: number, leafHashes: Uint8Array[]): Uint8Array[] {
92
+ const n = leafHashes.length;
93
+ if (!Number.isInteger(m) || m < 1 || m > n) {
94
+ throw new Error(`old size ${m} out of range for tree size ${n}`);
95
+ }
96
+ if (m === n) return [];
97
+ return subproof(m, leafHashes, true);
98
+ }
99
+
100
+ function subproof(m: number, leafHashes: Uint8Array[], complete: boolean): Uint8Array[] {
101
+ const n = leafHashes.length;
102
+ if (m === n) return complete ? [] : [merkleRoot(leafHashes)];
103
+ const k = split(n);
104
+ return m <= k
105
+ ? [...subproof(m, leafHashes.slice(0, k), complete), merkleRoot(leafHashes.slice(k))]
106
+ : [...subproof(m - k, leafHashes.slice(k), false), merkleRoot(leafHashes.slice(0, k))];
107
+ }
108
+
109
+ /** RFC 9162 §2.1.4.2. Pure; returns false on any malformed input. */
110
+ export function verifyConsistency(
111
+ size1: number,
112
+ size2: number,
113
+ root1: Uint8Array,
114
+ root2: Uint8Array,
115
+ proof: Uint8Array[],
116
+ ): boolean {
117
+ if (!Number.isInteger(size1) || !Number.isInteger(size2) || size1 < 0 || size1 > size2) return false;
118
+ if (size1 === size2) return proof.length === 0 && eq(root1, root2);
119
+ if (size1 === 0) return proof.length === 0;
120
+ if (proof.length === 0) return false;
121
+
122
+ const path = [...proof];
123
+ // If size1 is an exact power of two, root1 is itself the first component.
124
+ if ((size1 & (size1 - 1)) === 0) path.unshift(root1);
125
+
126
+ let fn = size1 - 1;
127
+ let sn = size2 - 1;
128
+ while (fn % 2 === 1) {
129
+ fn = Math.floor(fn / 2);
130
+ sn = Math.floor(sn / 2);
131
+ }
132
+
133
+ let fr = path[0]!;
134
+ let sr = path[0]!;
135
+ for (const c of path.slice(1)) {
136
+ if (c.length !== 32) return false;
137
+ if (sn === 0) return false;
138
+ if (fn % 2 === 1 || fn === sn) {
139
+ fr = nodeHash(c, fr);
140
+ sr = nodeHash(c, sr);
141
+ if (fn % 2 === 0) {
142
+ while (fn % 2 === 0 && fn !== 0) {
143
+ fn = Math.floor(fn / 2);
144
+ sn = Math.floor(sn / 2);
145
+ }
146
+ }
147
+ } else {
148
+ sr = nodeHash(sr, c);
149
+ }
150
+ fn = Math.floor(fn / 2);
151
+ sn = Math.floor(sn / 2);
152
+ }
153
+ return sn === 0 && eq(fr, root1) && eq(sr, root2);
154
+ }
package/src/receipt.ts ADDED
@@ -0,0 +1,123 @@
1
+ import { ed25519 } from '@noble/curves/ed25519';
2
+ import { sha256 } from '@noble/hashes/sha256';
3
+ import { bytesToHex } from '@noble/hashes/utils';
4
+ import { canonicalBytes, decodeSig, encodeSig, jcsHash } from './encoding.js';
5
+ import { publicKeyFromDid } from './keys.js';
6
+ import { scopesAllow, verifyChain, type DelegationCert } from './delegation.js';
7
+
8
+ export interface Receipt {
9
+ v: 1;
10
+ agent_id: string;
11
+ delegation: DelegationCert[];
12
+ action: string;
13
+ target: string;
14
+ payload_hash: string;
15
+ ts: string;
16
+ prev: string | null;
17
+ sig: string;
18
+ }
19
+
20
+ export interface VerifyResult {
21
+ ok: boolean;
22
+ error?: string;
23
+ }
24
+
25
+ export function hashPayload(payload: Uint8Array | string): string {
26
+ const bytes = typeof payload === 'string' ? new TextEncoder().encode(payload) : payload;
27
+ return `sha256:${bytesToHex(sha256(bytes))}`;
28
+ }
29
+
30
+ export function createReceipt(
31
+ fields: {
32
+ agentId: string;
33
+ delegation: DelegationCert[];
34
+ action: string;
35
+ target: string;
36
+ payloadHash: string;
37
+ ts: string;
38
+ prev: string | null;
39
+ },
40
+ agentPrivateKey: Uint8Array,
41
+ ): Receipt {
42
+ if (Number.isNaN(Date.parse(fields.ts))) throw new Error(`invalid ts timestamp: ${fields.ts}`);
43
+ const unsigned = {
44
+ v: 1 as const,
45
+ agent_id: fields.agentId,
46
+ delegation: fields.delegation,
47
+ action: fields.action,
48
+ target: fields.target,
49
+ payload_hash: fields.payloadHash,
50
+ ts: fields.ts,
51
+ prev: fields.prev,
52
+ };
53
+ const sig = ed25519.sign(canonicalBytes(unsigned), agentPrivateKey);
54
+ return { ...unsigned, sig: encodeSig(sig) };
55
+ }
56
+
57
+ /** Hash of the full receipt (incl. sig) — what the next receipt's `prev` points to. */
58
+ export function receiptHash(receipt: Receipt): string {
59
+ return jcsHash(receipt);
60
+ }
61
+
62
+ export function verifyReceiptSignature(receipt: Receipt): boolean {
63
+ const pub = publicKeyFromDid(receipt.agent_id);
64
+ const sig = decodeSig(receipt.sig);
65
+ if (!pub || !sig) return false;
66
+ const { sig: _omitted, ...unsigned } = receipt;
67
+ try {
68
+ return ed25519.verify(sig, canonicalBytes(unsigned), pub);
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Full single-receipt verification per SPEC §4:
76
+ * signature → delegation chain (at receipt time) → scope covers target → prev linkage.
77
+ */
78
+ export function verifyReceipt(
79
+ receipt: Receipt,
80
+ opts: { expectedOwner?: string; revoked?: Set<string>; expectedPrev?: string | null } = {},
81
+ ): VerifyResult {
82
+ if (receipt.v !== 1) return { ok: false, error: 'unsupported receipt version' };
83
+ if (!verifyReceiptSignature(receipt)) return { ok: false, error: 'bad receipt signature' };
84
+
85
+ const ts = new Date(receipt.ts);
86
+ if (Number.isNaN(ts.getTime())) return { ok: false, error: 'invalid ts' };
87
+
88
+ const chain = verifyChain(receipt.delegation, {
89
+ agentId: receipt.agent_id,
90
+ expectedRoot: opts.expectedOwner,
91
+ revoked: opts.revoked,
92
+ at: ts,
93
+ });
94
+ if (!chain.ok) return { ok: false, error: `delegation: ${chain.error}` };
95
+
96
+ if (!scopesAllow(chain.effectiveScopes!, receipt.target)) {
97
+ return { ok: false, error: `target ${receipt.target} outside effective scopes` };
98
+ }
99
+
100
+ if (opts.expectedPrev !== undefined && receipt.prev !== opts.expectedPrev) {
101
+ return { ok: false, error: 'prev hash does not match preceding receipt (chain broken)' };
102
+ }
103
+
104
+ return { ok: true };
105
+ }
106
+
107
+ /**
108
+ * Verify an agent's full receipt sequence: every receipt valid and hash-linked
109
+ * to the one before it. Returns the index of the first bad receipt on failure.
110
+ */
111
+ export function verifyReceiptChain(
112
+ receipts: Receipt[],
113
+ opts: { expectedOwner?: string; revoked?: Set<string> } = {},
114
+ ): VerifyResult & { failedIndex?: number } {
115
+ let expectedPrev: string | null = null;
116
+ for (let i = 0; i < receipts.length; i++) {
117
+ const r = receipts[i]!;
118
+ const result = verifyReceipt(r, { ...opts, expectedPrev });
119
+ if (!result.ok) return { ok: false, error: `receipt ${i}: ${result.error}`, failedIndex: i };
120
+ expectedPrev = receiptHash(r);
121
+ }
122
+ return { ok: true };
123
+ }
package/src/sth.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { ed25519 } from '@noble/curves/ed25519';
2
+ import { canonicalBytes, decodeSig, encodeSig } from './encoding.js';
3
+ import { publicKeyFromDid } from './keys.js';
4
+
5
+ /** Signed Tree Head — the log's periodic, signed commitment to its state (SPEC §5). */
6
+ export interface SignedTreeHead {
7
+ v: 1;
8
+ log_id: string;
9
+ size: number;
10
+ root: string;
11
+ ts: string;
12
+ sig: string;
13
+ }
14
+
15
+ export function createSTH(
16
+ fields: { logId: string; size: number; root: string; ts: string },
17
+ logPrivateKey: Uint8Array,
18
+ ): SignedTreeHead {
19
+ const unsigned = { v: 1 as const, log_id: fields.logId, size: fields.size, root: fields.root, ts: fields.ts };
20
+ const sig = ed25519.sign(canonicalBytes(unsigned), logPrivateKey);
21
+ return { ...unsigned, sig: encodeSig(sig) };
22
+ }
23
+
24
+ export function verifySTH(sth: SignedTreeHead): boolean {
25
+ if (sth.v !== 1 || !Number.isInteger(sth.size) || sth.size < 0) return false;
26
+ const pub = publicKeyFromDid(sth.log_id);
27
+ const sig = decodeSig(sth.sig);
28
+ if (!pub || !sig) return false;
29
+ const { sig: _omitted, ...unsigned } = sth;
30
+ try {
31
+ return ed25519.verify(sig, canonicalBytes(unsigned), pub);
32
+ } catch {
33
+ return false;
34
+ }
35
+ }