-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaspl.mjs
More file actions
174 lines (152 loc) · 7.22 KB
/
Copy pathaspl.mjs
File metadata and controls
174 lines (152 loc) · 7.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
// ASPL JavaScript/TypeScript SDK — zero-dependency client for ASPL nodes.
//
// A second independent-language implementation of the ASPL client, to show the
// protocol is language-agnostic (Node 18+, uses built-in fetch + crypto).
//
// import { ASPLClient } from "./aspl.mjs";
// const c = new ASPLClient("http://localhost:5010");
// await c.register("my-js-agent");
// const matches = await c.need("translate text to french");
// const cap = await c.get("translate text to french"); // need+accept+deliver+verify
import crypto from "node:crypto";
export class DeliveryVerificationError extends Error {}
// sha256(prefix+nonce) leading-zero-bit count, matching the Python node.
function leadingZeroBits(buf) {
let bits = 0;
for (const b of buf) {
if (b === 0) { bits += 8; continue; }
for (let i = 7; i >= 0; i--) { if (b & (1 << i)) return bits; bits++; }
return bits;
}
return bits;
}
function solvePow(prefix, difficulty) {
let n = 0;
for (;;) {
const h = crypto.createHash("sha256").update(`${prefix}${n}`).digest();
if (leadingZeroBits(h) >= difficulty) return String(n);
n++;
}
}
// Canonical content hash — must byte-match the Python server (sorted keys,
// compact separators, sha256, first 32 hex chars).
function canonical(value) {
if (Array.isArray(value)) return "[" + value.map(canonical).join(",") + "]";
if (value && typeof value === "object") {
return "{" + Object.keys(value).sort().map(
k => JSON.stringify(k) + ":" + canonical(value[k])).join(",") + "}";
}
return JSON.stringify(value);
}
export function contentHash(content) {
const hex = crypto.createHash("sha256").update(canonical(content)).digest("hex");
return `sha256:${hex.slice(0, 32)}`;
}
// Verify an Ed25519 signature given a raw 32-byte hex public key.
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
export function verifySig(pubHex, message, sigHex) {
try {
const der = Buffer.concat([ED25519_SPKI_PREFIX, Buffer.from(pubHex, "hex")]);
const key = crypto.createPublicKey({ key: der, format: "der", type: "spki" });
return crypto.verify(null, Buffer.from(message), key, Buffer.from(sigHex, "hex"));
} catch {
return false;
}
}
export class ASPLClient {
constructor(baseUrl = "http://localhost:5010", apiKey = null) {
this.base = baseUrl.replace(/\/$/, "");
this.apiKey = apiKey;
this.agentId = null;
this.nodePublicKey = null;
}
async _req(method, path, body) {
const headers = { "Content-Type": "application/json" };
if (this.apiKey) headers["X-API-Key"] = this.apiKey;
const res = await fetch(`${this.base}${path}`,
{ method, headers, body: body ? JSON.stringify(body) : undefined });
if (!res.ok) throw new Error(`ASPL ${method} ${path} → ${res.status}: ${await res.text()}`);
return res.json();
}
async register(name, environment = null) {
const ch = await this._req("GET", "/v1/pow/challenge");
const nonce = solvePow(ch.prefix, ch.difficulty);
const body = { name, pow_challenge_id: ch.challenge_id, pow_nonce: nonce };
if (environment) body.environment = environment;
const r = await this._req("POST", "/v1/register", body);
this.apiKey = r.api_key;
this.agentId = r.agent_id;
this.nodePublicKey = (r.passport?.shop_public_key || "").replace("ed25519:", "");
return r;
}
async publish(cap) { return this._req("POST", "/v1/publish", cap); }
async need(intent, opts = {}) {
const r = await this._req("POST", "/v1/need",
{ intent, max_results: opts.maxResults ?? 5, min_trust: opts.minTrust ?? 0.0,
include_imported: opts.includeImported ?? true,
...(opts.typeFilter ? { type_filter: opts.typeFilter } : {}) });
return r.matches;
}
async accept(capabilityId) { return this._req("POST", "/v1/accept", { capability_id: capabilityId }); }
async deliver(transactionId, verify = true) {
const d = await this._req("GET", `/v1/deliver/${transactionId}`);
if (verify) {
const { ok, reason } = this.verifyDelivery(d);
if (!ok) throw new DeliveryVerificationError(`delivery ${transactionId}: ${reason}`);
}
return d;
}
verifyDelivery(delivery) {
const cap = delivery.capability || {};
const recomputed = contentHash(delivery.content || {});
if (cap.content_hash !== recomputed)
return { ok: false, reason: `hash mismatch (${cap.content_hash} vs ${recomputed})` };
let nodeKey = cap.shop_public_key || this.nodePublicKey || "";
nodeKey = nodeKey.replace("ed25519:", "");
const msg = `deliver:${delivery.transaction_id}:${cap.content_hash}`;
if (!cap.shop_signature || !nodeKey) return { ok: false, reason: "missing signature/key" };
if (!verifySig(nodeKey, msg, cap.shop_signature)) return { ok: false, reason: "signature invalid" };
return { ok: true, reason: "ok" };
}
async confirm(transactionId, success, feedback = null) {
const body = { transaction_id: transactionId, success };
if (feedback) body.feedback = feedback;
return this._req("POST", "/v1/confirm", body);
}
async get(intent, typeFilter = null) {
const matches = await this.need(intent, { maxResults: 1, typeFilter });
if (!matches.length) throw new Error(`no capability for: ${intent}`);
const txn = await this.accept(matches[0].id);
const d = await this.deliver(txn.transaction_id);
return { capability: d.capability, content: d.content, transactionId: txn.transaction_id };
}
async ingestMcp(serverUrl, tools) { return this._req("POST", "/v1/ingest/mcp", { server_url: serverUrl, tools }); }
async ingestMcpUrl(serverUrl) { return this._req("POST", "/v1/ingest/mcp/url", { server_url: serverUrl }); }
async revoke(capabilityId, reason) { return this._req("POST", "/v1/revoke", { capability_id: capabilityId, reason }); }
async revocations() { return this._req("GET", "/v1/revocations"); }
// Transparency log
async logSth() { return this._req("GET", "/v1/log/sth"); }
async logInclusion(idx) { return this._req("GET", `/v1/log/proof/inclusion?leaf_index=${idx}`); }
async logConsistency(first, second) { return this._req("GET", `/v1/log/proof/consistency?first=${first}&second=${second}`); }
async logLeaves(start = 0, end = null) {
const qs = end != null ? `?start=${start}&end=${end}` : `?start=${start}`;
return this._req("GET", `/v1/log/leaves${qs}`);
}
// Federation
async federate(peerUrl) { return this._req("POST", "/v1/federation/mirror", { peer_url: peerUrl }); }
async acquireFederated(capId) { return this._req("POST", "/v1/federation/acquire", { capability_id: capId }); }
// Discovery / catalog
async catalog(opts = {}) {
const params = new URLSearchParams();
if (opts.limit != null) params.set("limit", opts.limit);
if (opts.offset != null) params.set("offset", opts.offset);
if (opts.type) params.set("type", opts.type);
const qs = params.toString() ? `?${params}` : "";
return this._req("GET", `/v1/catalog${qs}`);
}
async stats() { return this._req("GET", "/v1/stats"); }
async capability(capId) { return this._req("GET", `/v1/capability/${encodeURIComponent(capId)}`); }
async passport(agentId) { return this._req("GET", `/v1/passport/${encodeURIComponent(agentId)}`); }
// Node discovery
async wellKnown() { return this._req("GET", "/.well-known/aspl"); }
}