@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/README.md +2 -0
- package/dist/a2a.d.ts +7 -0
- package/dist/a2a.d.ts.map +1 -1
- package/dist/a2a.js +8 -1
- package/dist/a2a.js.map +1 -1
- package/dist/bundle.d.ts +4 -0
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +36 -6
- package/dist/bundle.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/receipt.d.ts +17 -0
- package/dist/receipt.d.ts.map +1 -1
- package/dist/receipt.js +27 -3
- package/dist/receipt.js.map +1 -1
- package/dist/swarm.d.ts +45 -0
- package/dist/swarm.d.ts.map +1 -0
- package/dist/swarm.js +81 -0
- package/dist/swarm.js.map +1 -0
- package/package.json +1 -1
- package/src/a2a.ts +148 -139
- package/src/bundle.ts +214 -176
- package/src/index.ts +6 -0
- package/src/receipt.ts +340 -308
- package/src/swarm.ts +102 -0
package/src/a2a.ts
CHANGED
|
@@ -1,139 +1,148 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cross-org co-signed receipts — SPEC §9. One interaction, two signers, one
|
|
3
|
-
* neutral proof that enters both agents' hash chains.
|
|
4
|
-
*/
|
|
5
|
-
import { ed25519 } from '@noble/curves/ed25519';
|
|
6
|
-
import { canonicalBytes, decodeSig, encodeSig, jcsHash } from './encoding.js';
|
|
7
|
-
import { publicKeyFromDid } from './keys.js';
|
|
8
|
-
import { scopesAllow, verifyChain, type DelegationCert } from './delegation.js';
|
|
9
|
-
import type { VerifyResult } from './receipt.js';
|
|
10
|
-
|
|
11
|
-
export interface A2AParty {
|
|
12
|
-
agent_id: string;
|
|
13
|
-
delegation: DelegationCert[];
|
|
14
|
-
prev: string | null;
|
|
15
|
-
sig: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface A2AReceipt {
|
|
19
|
-
v: 1;
|
|
20
|
-
type: 'a2a';
|
|
21
|
-
action: string;
|
|
22
|
-
target: string;
|
|
23
|
-
payload_hash: string;
|
|
24
|
-
ts: string;
|
|
25
|
-
initiator: A2AParty;
|
|
26
|
-
responder: A2AParty;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** The body both parties sign: the receipt with both sig fields removed. */
|
|
30
|
-
export type A2ABody = Omit<A2AReceipt, 'initiator' | 'responder'> & {
|
|
31
|
-
initiator: Omit<A2AParty, 'sig'>;
|
|
32
|
-
responder: Omit<A2AParty, 'sig'>;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export function buildA2ABody(fields: {
|
|
36
|
-
action: string;
|
|
37
|
-
target: string;
|
|
38
|
-
payloadHash: string;
|
|
39
|
-
ts: string;
|
|
40
|
-
initiator: Omit<A2AParty, 'sig'>;
|
|
41
|
-
responder: Omit<A2AParty, 'sig'>;
|
|
42
|
-
}): A2ABody {
|
|
43
|
-
if (Number.isNaN(Date.parse(fields.ts))) throw new Error(`invalid ts timestamp: ${fields.ts}`);
|
|
44
|
-
if (fields.initiator.agent_id === fields.responder.agent_id) {
|
|
45
|
-
throw new Error('initiator and responder must be distinct agents');
|
|
46
|
-
}
|
|
47
|
-
return {
|
|
48
|
-
v: 1,
|
|
49
|
-
type: 'a2a',
|
|
50
|
-
action: fields.action,
|
|
51
|
-
target: fields.target,
|
|
52
|
-
payload_hash: fields.payloadHash,
|
|
53
|
-
ts: fields.ts,
|
|
54
|
-
initiator: fields.initiator,
|
|
55
|
-
responder: fields.responder,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Both parties sign the identical body — order-independent. */
|
|
60
|
-
export function signA2ABody(body: A2ABody, privateKey: Uint8Array): string {
|
|
61
|
-
return encodeSig(ed25519.sign(canonicalBytes(body), privateKey));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function assembleA2AReceipt(body: A2ABody, initiatorSig: string, responderSig: string): A2AReceipt {
|
|
65
|
-
return {
|
|
66
|
-
...body,
|
|
67
|
-
initiator: { ...body.initiator, sig: initiatorSig },
|
|
68
|
-
responder: { ...body.responder, sig: responderSig },
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Hash of the full receipt (both sigs) — what both agents' next `prev` points to. */
|
|
73
|
-
export function a2aHash(receipt: A2AReceipt): string {
|
|
74
|
-
return jcsHash(receipt);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function bodyOf(receipt: A2AReceipt): A2ABody {
|
|
78
|
-
const { sig: _i, ...initiator } = receipt.initiator;
|
|
79
|
-
const { sig: _r, ...responder } = receipt.responder;
|
|
80
|
-
return { ...receipt, initiator, responder };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function partySigOk(body: A2ABody, party: A2AParty): boolean {
|
|
84
|
-
const pub = publicKeyFromDid(party.agent_id);
|
|
85
|
-
const sig = decodeSig(party.sig);
|
|
86
|
-
if (!pub || !sig) return false;
|
|
87
|
-
try {
|
|
88
|
-
return ed25519.verify(sig, canonicalBytes(body), pub);
|
|
89
|
-
} catch {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Full A2A verification per SPEC §9. `expectedPrevFor` maps agent DID → required prev. */
|
|
95
|
-
export function verifyA2AReceipt(
|
|
96
|
-
receipt: A2AReceipt,
|
|
97
|
-
opts: { revoked?: Set<string>; expectedPrevFor?: Record<string, string | null> } = {},
|
|
98
|
-
): VerifyResult {
|
|
99
|
-
if (receipt.v !== 1 || receipt.type !== 'a2a') return { ok: false, error: 'not an a2a receipt' };
|
|
100
|
-
if (receipt.initiator.agent_id === receipt.responder.agent_id) {
|
|
101
|
-
return { ok: false, error: 'initiator and responder must be distinct' };
|
|
102
|
-
}
|
|
103
|
-
const ts = new Date(receipt.ts);
|
|
104
|
-
if (Number.isNaN(ts.getTime())) return { ok: false, error: 'invalid ts' };
|
|
105
|
-
|
|
106
|
-
const body = bodyOf(receipt);
|
|
107
|
-
for (const [role, party] of [['initiator', receipt.initiator], ['responder', receipt.responder]] as const) {
|
|
108
|
-
if (!partySigOk(body, party)) return { ok: false, error: `${role}: bad signature` };
|
|
109
|
-
const chain = verifyChain(party.delegation, { agentId: party.agent_id, revoked: opts.revoked, at: ts });
|
|
110
|
-
if (!chain.ok) return { ok: false, error: `${role} delegation: ${chain.error}` };
|
|
111
|
-
if (!scopesAllow(chain.effectiveScopes!, receipt.target)) {
|
|
112
|
-
return { ok: false, error: `${role}: target ${receipt.target} outside effective scopes` };
|
|
113
|
-
}
|
|
114
|
-
const expected = opts.expectedPrevFor?.[party.agent_id];
|
|
115
|
-
if (expected !== undefined && party.prev !== expected) {
|
|
116
|
-
return { ok: false, error: `${role}: prev hash does not match chain head (chain broken)` };
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return { ok: true };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** The DIDs participating in any log receipt (solo or a2a). */
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
entry
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Cross-org co-signed receipts — SPEC §9. One interaction, two signers, one
|
|
3
|
+
* neutral proof that enters both agents' hash chains.
|
|
4
|
+
*/
|
|
5
|
+
import { ed25519 } from '@noble/curves/ed25519';
|
|
6
|
+
import { canonicalBytes, decodeSig, encodeSig, jcsHash } from './encoding.js';
|
|
7
|
+
import { publicKeyFromDid } from './keys.js';
|
|
8
|
+
import { scopesAllow, verifyChain, type DelegationCert } from './delegation.js';
|
|
9
|
+
import type { VerifyResult } from './receipt.js';
|
|
10
|
+
|
|
11
|
+
export interface A2AParty {
|
|
12
|
+
agent_id: string;
|
|
13
|
+
delegation: DelegationCert[];
|
|
14
|
+
prev: string | null;
|
|
15
|
+
sig: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface A2AReceipt {
|
|
19
|
+
v: 1;
|
|
20
|
+
type: 'a2a';
|
|
21
|
+
action: string;
|
|
22
|
+
target: string;
|
|
23
|
+
payload_hash: string;
|
|
24
|
+
ts: string;
|
|
25
|
+
initiator: A2AParty;
|
|
26
|
+
responder: A2AParty;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The body both parties sign: the receipt with both sig fields removed. */
|
|
30
|
+
export type A2ABody = Omit<A2AReceipt, 'initiator' | 'responder'> & {
|
|
31
|
+
initiator: Omit<A2AParty, 'sig'>;
|
|
32
|
+
responder: Omit<A2AParty, 'sig'>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function buildA2ABody(fields: {
|
|
36
|
+
action: string;
|
|
37
|
+
target: string;
|
|
38
|
+
payloadHash: string;
|
|
39
|
+
ts: string;
|
|
40
|
+
initiator: Omit<A2AParty, 'sig'>;
|
|
41
|
+
responder: Omit<A2AParty, 'sig'>;
|
|
42
|
+
}): A2ABody {
|
|
43
|
+
if (Number.isNaN(Date.parse(fields.ts))) throw new Error(`invalid ts timestamp: ${fields.ts}`);
|
|
44
|
+
if (fields.initiator.agent_id === fields.responder.agent_id) {
|
|
45
|
+
throw new Error('initiator and responder must be distinct agents');
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
v: 1,
|
|
49
|
+
type: 'a2a',
|
|
50
|
+
action: fields.action,
|
|
51
|
+
target: fields.target,
|
|
52
|
+
payload_hash: fields.payloadHash,
|
|
53
|
+
ts: fields.ts,
|
|
54
|
+
initiator: fields.initiator,
|
|
55
|
+
responder: fields.responder,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Both parties sign the identical body — order-independent. */
|
|
60
|
+
export function signA2ABody(body: A2ABody, privateKey: Uint8Array): string {
|
|
61
|
+
return encodeSig(ed25519.sign(canonicalBytes(body), privateKey));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function assembleA2AReceipt(body: A2ABody, initiatorSig: string, responderSig: string): A2AReceipt {
|
|
65
|
+
return {
|
|
66
|
+
...body,
|
|
67
|
+
initiator: { ...body.initiator, sig: initiatorSig },
|
|
68
|
+
responder: { ...body.responder, sig: responderSig },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Hash of the full receipt (both sigs) — what both agents' next `prev` points to. */
|
|
73
|
+
export function a2aHash(receipt: A2AReceipt): string {
|
|
74
|
+
return jcsHash(receipt);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function bodyOf(receipt: A2AReceipt): A2ABody {
|
|
78
|
+
const { sig: _i, ...initiator } = receipt.initiator;
|
|
79
|
+
const { sig: _r, ...responder } = receipt.responder;
|
|
80
|
+
return { ...receipt, initiator, responder };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function partySigOk(body: A2ABody, party: A2AParty): boolean {
|
|
84
|
+
const pub = publicKeyFromDid(party.agent_id);
|
|
85
|
+
const sig = decodeSig(party.sig);
|
|
86
|
+
if (!pub || !sig) return false;
|
|
87
|
+
try {
|
|
88
|
+
return ed25519.verify(sig, canonicalBytes(body), pub);
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Full A2A verification per SPEC §9. `expectedPrevFor` maps agent DID → required prev. */
|
|
95
|
+
export function verifyA2AReceipt(
|
|
96
|
+
receipt: A2AReceipt,
|
|
97
|
+
opts: { revoked?: Set<string>; expectedPrevFor?: Record<string, string | null> } = {},
|
|
98
|
+
): VerifyResult {
|
|
99
|
+
if (receipt.v !== 1 || receipt.type !== 'a2a') return { ok: false, error: 'not an a2a receipt' };
|
|
100
|
+
if (receipt.initiator.agent_id === receipt.responder.agent_id) {
|
|
101
|
+
return { ok: false, error: 'initiator and responder must be distinct' };
|
|
102
|
+
}
|
|
103
|
+
const ts = new Date(receipt.ts);
|
|
104
|
+
if (Number.isNaN(ts.getTime())) return { ok: false, error: 'invalid ts' };
|
|
105
|
+
|
|
106
|
+
const body = bodyOf(receipt);
|
|
107
|
+
for (const [role, party] of [['initiator', receipt.initiator], ['responder', receipt.responder]] as const) {
|
|
108
|
+
if (!partySigOk(body, party)) return { ok: false, error: `${role}: bad signature` };
|
|
109
|
+
const chain = verifyChain(party.delegation, { agentId: party.agent_id, revoked: opts.revoked, at: ts });
|
|
110
|
+
if (!chain.ok) return { ok: false, error: `${role} delegation: ${chain.error}` };
|
|
111
|
+
if (!scopesAllow(chain.effectiveScopes!, receipt.target)) {
|
|
112
|
+
return { ok: false, error: `${role}: target ${receipt.target} outside effective scopes` };
|
|
113
|
+
}
|
|
114
|
+
const expected = opts.expectedPrevFor?.[party.agent_id];
|
|
115
|
+
if (expected !== undefined && party.prev !== expected) {
|
|
116
|
+
return { ok: false, error: `${role}: prev hash does not match chain head (chain broken)` };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { ok: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The DIDs participating in any log receipt (solo or a2a). */
|
|
123
|
+
type SignerLike = { agent_id?: string; prev?: string | null };
|
|
124
|
+
|
|
125
|
+
export function participantsOf(entry: { agent_id?: string; type?: string; initiator?: A2AParty; responder?: A2AParty; signers?: SignerLike[] }): string[] {
|
|
126
|
+
if (entry.type === 'a2a') return [entry.initiator!.agent_id, entry.responder!.agent_id];
|
|
127
|
+
// Attribute a swarm receipt to its actual co-signers, never to declared participants
|
|
128
|
+
// who did not sign (SPEC §14) — the same rule the log uses when it indexes one.
|
|
129
|
+
if (entry.type === 'swarm') return (entry.signers ?? []).map((s) => s.agent_id).filter((a): a is string => !!a);
|
|
130
|
+
return entry.agent_id ? [entry.agent_id] : [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The `prev` a given participant declared in a log receipt, or undefined if not a party. */
|
|
134
|
+
export function prevFor(
|
|
135
|
+
entry: { agent_id?: string; prev?: string | null; type?: string; initiator?: A2AParty; responder?: A2AParty; signers?: SignerLike[] },
|
|
136
|
+
agentId: string,
|
|
137
|
+
): string | null | undefined {
|
|
138
|
+
if (entry.type === 'a2a') {
|
|
139
|
+
if (entry.initiator?.agent_id === agentId) return entry.initiator.prev;
|
|
140
|
+
if (entry.responder?.agent_id === agentId) return entry.responder.prev;
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
if (entry.type === 'swarm') {
|
|
144
|
+
const signer = (entry.signers ?? []).find((s) => s.agent_id === agentId);
|
|
145
|
+
return signer ? (signer.prev ?? null) : undefined;
|
|
146
|
+
}
|
|
147
|
+
return entry.agent_id === agentId ? (entry.prev as string | null) : undefined;
|
|
148
|
+
}
|