@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/swarm.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* N-party (swarm) receipts as they sit in the log (SPEC §14): the verification every
|
|
3
|
+
* consumer of the log needs — the bundle verifier included — so it lives in core.
|
|
4
|
+
* `@zanii/swarm` re-exports these, and adds the client-side building helpers.
|
|
5
|
+
*/
|
|
6
|
+
import { ed25519 } from '@noble/curves/ed25519';
|
|
7
|
+
import { scopesAllow, verifyChain, type DelegationCert } from './delegation.js';
|
|
8
|
+
import { canonicalBytes, decodeSig } from './encoding.js';
|
|
9
|
+
import { publicKeyFromDid } from './keys.js';
|
|
10
|
+
import type { VerifyResult } from './receipt.js';
|
|
11
|
+
|
|
12
|
+
export interface SwarmBody {
|
|
13
|
+
v: 1;
|
|
14
|
+
type: 'swarm';
|
|
15
|
+
/** What the team did, e.g. `deploy.release`. */
|
|
16
|
+
task: string;
|
|
17
|
+
target: string;
|
|
18
|
+
/** Hash of the action payload (contents stay off-receipt). */
|
|
19
|
+
payload_hash?: string;
|
|
20
|
+
ts: string;
|
|
21
|
+
/** The team's `did:key`s — distinct, sorted (canonical). */
|
|
22
|
+
participants: string[];
|
|
23
|
+
/** M-of-N: how many of `participants` must sign for the receipt to be valid. */
|
|
24
|
+
threshold: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SwarmSignerEnvelope {
|
|
28
|
+
agent_id: string;
|
|
29
|
+
/** The signer's delegation chain proving its owner authorized it for the target scope. */
|
|
30
|
+
delegation: DelegationCert[];
|
|
31
|
+
/** The signer's previous log head (`null` = genesis). */
|
|
32
|
+
prev: string | null;
|
|
33
|
+
/** `ed25519:<hex>` over the canonical body — same as `signSwarm`. */
|
|
34
|
+
sig: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SwarmLedgerReceipt {
|
|
38
|
+
v: 1;
|
|
39
|
+
type: 'swarm';
|
|
40
|
+
body: SwarmBody;
|
|
41
|
+
signers: SwarmSignerEnvelope[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Authority-complete verification of an on-ledger swarm receipt (SPEC §14) — the exact
|
|
46
|
+
* mirror of the server's `verify_swarm_receipt`. Every listed signer must be a distinct
|
|
47
|
+
* declared participant with a valid signature, an owner-rooted unrevoked/unexpired
|
|
48
|
+
* delegation, the target in scope, and `prev` matching its chain head. Then valid signers
|
|
49
|
+
* ≥ threshold, and (default) ≥ threshold DISTINCT owners — real segregation of duties, so
|
|
50
|
+
* one owner's N agents cannot fake an M-of-N. Any failure hard-rejects.
|
|
51
|
+
*/
|
|
52
|
+
export function verifySwarmReceipt(
|
|
53
|
+
receipt: SwarmLedgerReceipt | undefined,
|
|
54
|
+
opts: { revoked?: Set<string>; expectedPrevFor?: Record<string, string | null>; requireDistinctOwners?: boolean } = {},
|
|
55
|
+
): VerifyResult {
|
|
56
|
+
const requireDistinctOwners = opts.requireDistinctOwners ?? true;
|
|
57
|
+
if (!receipt || receipt.v !== 1 || receipt.type !== 'swarm') return { ok: false, error: 'not a swarm receipt' };
|
|
58
|
+
const body = receipt.body;
|
|
59
|
+
if (!body || body.v !== 1 || body.type !== 'swarm') return { ok: false, error: 'invalid swarm body' };
|
|
60
|
+
const parts = body.participants;
|
|
61
|
+
const threshold = body.threshold;
|
|
62
|
+
const ts = new Date(body.ts);
|
|
63
|
+
if (!Array.isArray(parts) || parts.length < 3) return { ok: false, error: 'swarm needs at least 3 participants' };
|
|
64
|
+
if (new Set(parts).size !== parts.length) return { ok: false, error: 'participants must be distinct' };
|
|
65
|
+
if (!Number.isInteger(threshold) || threshold < 1 || threshold > parts.length) return { ok: false, error: 'invalid threshold' };
|
|
66
|
+
if (Number.isNaN(ts.getTime())) return { ok: false, error: 'invalid ts' };
|
|
67
|
+
|
|
68
|
+
const participantSet = new Set(parts);
|
|
69
|
+
const target = body.target ?? '';
|
|
70
|
+
const msg = canonicalBytes(body);
|
|
71
|
+
const signers = receipt.signers ?? [];
|
|
72
|
+
const seen = new Set<string>();
|
|
73
|
+
const owners: string[] = [];
|
|
74
|
+
for (const s of signers) {
|
|
75
|
+
const aid = s.agent_id;
|
|
76
|
+
if (!participantSet.has(aid)) return { ok: false, error: `signer ${aid} is not a declared participant` };
|
|
77
|
+
if (seen.has(aid)) return { ok: false, error: `signer ${aid} appears twice` };
|
|
78
|
+
seen.add(aid);
|
|
79
|
+
const pub = publicKeyFromDid(aid);
|
|
80
|
+
const sig = decodeSig(s.sig);
|
|
81
|
+
if (!pub || !sig) return { ok: false, error: `signer ${aid}: malformed key or signature` };
|
|
82
|
+
let sigOk = false;
|
|
83
|
+
try {
|
|
84
|
+
sigOk = ed25519.verify(sig, msg, pub);
|
|
85
|
+
} catch {
|
|
86
|
+
sigOk = false;
|
|
87
|
+
}
|
|
88
|
+
if (!sigOk) return { ok: false, error: `signer ${aid}: bad signature` };
|
|
89
|
+
const chain = verifyChain(s.delegation ?? [], { agentId: aid, revoked: opts.revoked, at: ts });
|
|
90
|
+
if (!chain.ok) return { ok: false, error: `signer ${aid} delegation: ${chain.error}` };
|
|
91
|
+
if (!scopesAllow(chain.effectiveScopes ?? [], target)) return { ok: false, error: `signer ${aid}: target ${target} outside effective scopes` };
|
|
92
|
+
const expected = opts.expectedPrevFor?.[aid];
|
|
93
|
+
if (expected !== undefined && s.prev !== expected) return { ok: false, error: `signer ${aid}: prev hash does not match chain head (chain broken)` };
|
|
94
|
+
const root = s.delegation?.[0]?.issuer;
|
|
95
|
+
if (root) owners.push(root);
|
|
96
|
+
}
|
|
97
|
+
if (seen.size < threshold) return { ok: false, error: `threshold not met: ${seen.size} valid of ${threshold} required` };
|
|
98
|
+
if (requireDistinctOwners && new Set(owners).size < threshold) {
|
|
99
|
+
return { ok: false, error: `distinct-owner threshold not met: ${new Set(owners).size} owners of ${threshold} required` };
|
|
100
|
+
}
|
|
101
|
+
return { ok: true };
|
|
102
|
+
}
|