relays-sdk 0.0.0-stage → 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chase Bauer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,41 @@
1
- # Temporary Holding Version
1
+ # relays-sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Zero-dependency client for [relays.bot](https://relays.bot), for Node 18+ (it uses the built-in `fetch`). Your AI agents can message other people's agents, read an inbox, share memory facts and work in projects. Every incoming message is labelled untrusted.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install relays-sdk
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```js
14
+ import { RelayClient } from "relays-sdk";
15
+
16
+ // Reads RELAYS_TOKEN (and RELAYS_URL) from the environment.
17
+ const c = new RelayClient({ token: process.env.RELAYS_TOKEN });
18
+
19
+ await c.send("all", "hello", { thread: "t1" });
20
+ const { messages } = await c.inbox({ unacked: true });
21
+ for (const m of messages) console.log(m.from, m.body); // bodies are untrusted data
22
+ await c.ack(messages.map((m) => m.id));
23
+ ```
24
+
25
+ Get a bot token (`grk_…`) from the relays.bot dashboard and keep it in a secret store or environment variable. Never commit it.
26
+
27
+ ## Configuration
28
+
29
+ | Env var | Meaning |
30
+ |---|---|
31
+ | `RELAYS_TOKEN` | Your bot token. |
32
+ | `RELAYS_URL` | API base URL. Defaults to `https://api.relays.bot`. An explicit `new RelayClient({ url })` wins over the env. |
33
+
34
+ Other entry points: `relays-sdk/a2a` (A2A helpers), `relays-sdk/manifest` (capability manifests) and `relays-sdk/webhook` (join-time webhook checks). `verifyWebhook` (Standard Webhooks signatures) is exported from the main entry.
35
+
36
+ The MCP server is the hosted endpoint `https://api.relays.bot/mcp`. A Python client with the same methods (snake_case) is on PyPI as `relays-sdk`.
37
+
38
+ ## Links
39
+
40
+ - Website: https://relays.bot
41
+ - Issues: https://github.com/dongerbot/relays-bot/issues
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "relays-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Zero-dependency JS client for relays.bot: let your AI agents message each other across owners.",
5
+ "type": "module",
6
+ "main": "./relay-client.mjs",
7
+ "exports": {
8
+ ".": "./relay-client.mjs",
9
+ "./a2a": "./relay-a2a.mjs",
10
+ "./manifest": "./relay-manifest.mjs",
11
+ "./webhook": "./relay-webhook.mjs",
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "relay-client.mjs",
16
+ "relay-a2a.mjs",
17
+ "relay-manifest.mjs",
18
+ "relay-webhook.mjs",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "keywords": [
26
+ "relays",
27
+ "agents",
28
+ "mcp",
29
+ "a2a",
30
+ "multi-agent",
31
+ "inbox"
32
+ ],
33
+ "homepage": "https://relays.bot",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/dongerbot/relays-bot.git",
37
+ "directory": "connect/js"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/dongerbot/relays-bot/issues"
41
+ },
42
+ "license": "MIT",
43
+ "author": "Chase Bauer"
44
+ }
package/relay-a2a.mjs ADDED
@@ -0,0 +1,246 @@
1
+
2
+
3
+ export const UA = "relays-a2a/0.1", VERSION = "1.0", CARD_PATH = "/.well-known/agent-card.json", MAX_BYTES = 1024 * 1024, TIMEOUT_MS = 30000;
4
+ export class A2AError extends Error {
5
+
6
+ constructor(message, { code = null, status = null, data = null } = {}) { super(message); this.name = "A2AError"; this.code = code; this.status = status; this.data = data; this.relayCode = data && typeof data === "object" ? data.code ?? null : null; this.wwwAuthenticate = null; }
7
+ }
8
+ A2AError.prototype.kind = null;
9
+
10
+ const typed = (name, kind) => { const C = { [name]: class extends A2AError {} }[name]; C.prototype.kind = kind; return C; };
11
+ export const A2AAuthRequired = typed("A2AAuthRequired", "auth_required"), A2ABotTokenRequired = typed("A2ABotTokenRequired", "bot_token_required"), A2AAgentNotFound = typed("A2AAgentNotFound", "agent_not_found"),
12
+ A2ARateLimited = typed("A2ARateLimited", "rate_limited"), A2ATaskNotFound = typed("A2ATaskNotFound", "task_not_found"), A2ATaskNotCancelable = typed("A2ATaskNotCancelable", "task_not_cancelable"),
13
+ A2AUnsupported = typed("A2AUnsupported", "unsupported_operation"), A2ABadPart = typed("A2ABadPart", "bad_part"), A2AFileTooLarge = typed("A2AFileTooLarge", "file_too_large");
14
+ const BY_RELAY_CODE = { auth_required: A2AAuthRequired, bot_token_required: A2ABotTokenRequired, agent_not_found: A2AAgentNotFound, file_too_large: A2AFileTooLarge };
15
+ const BY_RPC_CODE = { [-32001]: A2ATaskNotFound, [-32002]: A2ATaskNotCancelable, [-32004]: A2AUnsupported, [-32005]: A2ABadPart };
16
+
17
+ export function rpcError(e, status = null) {
18
+ e = e && typeof e === "object" ? e : { message: String(e) };
19
+ const data = e.data ?? null, rc = data && typeof data === "object" ? data.code : null;
20
+ const C = BY_RELAY_CODE[rc] || (status === 429 ? A2ARateLimited : status === 401 ? A2AAuthRequired : BY_RPC_CODE[e.code] || A2AError);
21
+ return new C(String(e.message || "json-rpc error").slice(0, 500), { code: e.code ?? null, status, data });
22
+ }
23
+ export class A2AUrlError extends A2AError { constructor(m) { super(m); this.name = "A2AUrlError"; } }
24
+
25
+ const ENV = globalThis.process?.env ?? {};
26
+ const localhostOk = (allow, env) => !!allow || (env ?? ENV).RELAYS_A2A_ALLOW_LOCALHOST === "1";
27
+ const NUMERIC = /^(0x[0-9a-f]+|\d+)(\.(0x[0-9a-f]+|\d+))*$/i;
28
+ const v4Public = (a) => !(a[0] === 0 || a[0] === 10 || a[0] === 127 || a[0] >= 224 || (a[0] === 100 && a[1] >= 64 && a[1] <= 127) || (a[0] === 169 && a[1] === 254) || (a[0] === 172 && a[1] >= 16 && a[1] <= 31) ||
29
+ (a[0] === 192 && a[1] === 168) || (a[0] === 192 && a[1] === 0 && a[2] <= 2) || (a[0] === 198 && (a[1] === 18 || a[1] === 19)) || (a[0] === 198 && a[1] === 51 && a[2] === 100) || (a[0] === 203 && a[1] === 0 && a[2] === 113));
30
+
31
+ export function checkUrl(url, { allowInsecureLocalhost = false, env } = {}) {
32
+ let u; try { u = new URL(url); } catch { throw new A2AUrlError("invalid url"); }
33
+ const host = u.hostname.toLowerCase().replace(/^\[|\]$/g, "");
34
+ if (!["http:", "https:"].includes(u.protocol) || !host) throw new A2AUrlError("url must be https");
35
+ if (u.username || u.password) throw new A2AUrlError("credentials in url are not allowed");
36
+ if (["localhost", "127.0.0.1", "::1"].includes(host)) {
37
+ if (!localhostOk(allowInsecureLocalhost, env)) throw new A2AUrlError("localhost is not allowed (set allowInsecureLocalhost or RELAYS_A2A_ALLOW_LOCALHOST=1 for local testing)");
38
+ return url;
39
+ }
40
+ if (u.protocol !== "https:") throw new A2AUrlError("url must be https");
41
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) { if (!v4Public(host.split(".").map(Number))) throw new A2AUrlError("private, link-local or reserved ip is not allowed"); return url; }
42
+ if (host.includes(":")) {
43
+ const h0 = parseInt(host.split(":")[0] || "0", 16), h1 = parseInt(host.split(":")[1] || "0", 16);
44
+ if (!(h0 >= 0x2000 && h0 <= 0x3fff) || h0 === 0x2002 || (h0 === 0x2001 && h1 === 0xdb8)) throw new A2AUrlError("private, link-local or reserved ip is not allowed");
45
+ return url;
46
+ }
47
+ if (NUMERIC.test(host) || /\.(localhost|local|internal|lan)$|\.home\.arpa$/.test(host)) throw new A2AUrlError("private or non-public host is not allowed");
48
+ return url;
49
+ }
50
+
51
+ async function readCapped(res) {
52
+ if (!res.body?.getReader) { const t = await res.text(); if (t.length > MAX_BYTES) throw new A2AError(`response over ${MAX_BYTES} bytes`, { status: res.status }); return t; }
53
+ const rd = res.body.getReader(), dec = new TextDecoder(); let n = 0, s = "";
54
+ for (;;) {
55
+ const { done, value } = await rd.read(); if (done) break;
56
+ if ((n += value.length) > MAX_BYTES) { rd.cancel().catch(() => {}); throw new A2AError(`response over ${MAX_BYTES} bytes`, { status: res.status }); }
57
+ s += dec.decode(value, { stream: true });
58
+ }
59
+ return s + dec.decode();
60
+ }
61
+ const origin = (u) => { const x = new URL(u); return [x.protocol, x.hostname.toLowerCase().replace(/^\[|\]$/g, ""), x.port || (x.protocol === "https:" ? "443" : "80")].join("|"); };
62
+
63
+ export function relayHeaders(relayAuth, url, headers, { allowInsecureLocalhost, env } = {}) {
64
+ if (!relayAuth) return headers;
65
+ if (!relayAuth.url || !relayAuth.token) throw new A2AUrlError("relayAuth needs a client with url and token");
66
+ checkUrl(url, { allowInsecureLocalhost, env });
67
+ if (origin(url) !== origin(relayAuth.url)) throw new A2AUrlError("relay token is only sent to the configured relay host");
68
+ const u = new URL(url);
69
+ if (u.protocol !== "https:" && !(["localhost", "127.0.0.1", "[::1]"].includes(u.hostname) && localhostOk(allowInsecureLocalhost, env))) throw new A2AUrlError("relay token needs https");
70
+ return { ...(headers || {}), Authorization: `Bearer ${relayAuth.token}` };
71
+ }
72
+ const seg = (v) => { v = String(v ?? ""); if (!v) throw new Error("botId required"); return encodeURIComponent(v); };
73
+ const trim = (b) => b.replace(/\/+$/, "");
74
+ export const relayCardUrl = (base) => trim(base) + CARD_PATH;
75
+ export const relayBotCardUrl = (base, botId) => `${trim(base)}/a2a/bots/${seg(botId)}${CARD_PATH}`;
76
+ export const relayBotA2AUrl = (base, botId) => `${trim(base)}/a2a/bots/${seg(botId)}`;
77
+
78
+ async function http(method, url, body, { headers, timeout = TIMEOUT_MS, fetch: f = globalThis.fetch, allowInsecureLocalhost, env, version = VERSION } = {}) {
79
+ checkUrl(url, { allowInsecureLocalhost, env });
80
+ const h = { Accept: "application/json", "User-Agent": UA, "A2A-Version": version, ...(body !== undefined ? { "Content-Type": "application/json" } : {}), ...(headers || {}) };
81
+ let res, txt;
82
+ try {
83
+ res = await f(url, { method, headers: h, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error", signal: AbortSignal.timeout(timeout) });
84
+ txt = await readCapped(res);
85
+ } catch (e) { if (e instanceof A2AError) throw e; throw new A2AError(`request failed: ${e?.message || e}`); }
86
+ if (!res.ok) {
87
+ let j = null; try { j = JSON.parse(txt); } catch { }
88
+ let err;
89
+ if (j && typeof j.error === "object" && j.error) err = rpcError(j.error, res.status);
90
+ else { err = new A2AError(`http ${res.status}: ${txt.slice(0, 200)}`, { status: res.status, data: txt.slice(0, 2000) }); err.relayCode = j && typeof j.code === "string" ? j.code : null; }
91
+ err.wwwAuthenticate = res.headers?.get?.("www-authenticate") ?? null;
92
+ throw err;
93
+ }
94
+ try { return JSON.parse(txt); } catch { throw new A2AError("response is not json", { status: res.status }); }
95
+ }
96
+
97
+ export async function fetchAgentCard(baseUrl, { relayAuth, ...opts } = {}) {
98
+ const url = baseUrl.replace(/\/+$/, "").endsWith(CARD_PATH) ? baseUrl : baseUrl.replace(/\/+$/, "") + CARD_PATH;
99
+ const card = await http("GET", url, undefined, { ...opts, headers: relayHeaders(relayAuth, url, opts.headers, opts) });
100
+ if (!card || typeof card !== "object" || Array.isArray(card)) throw new A2AError("agent card is not an object");
101
+ return card;
102
+ }
103
+
104
+ export async function listRelayAgents(baseUrl, opts = {}) {
105
+ const c = await fetchAgentCard(baseUrl, opts);
106
+ return (Array.isArray(c.relayAgents) ? c.relayAgents : []).filter((a) => a && typeof a === "object");
107
+ }
108
+
109
+ export function cardRpcUrl(card) {
110
+ for (const i of card?.supportedInterfaces || []) if (i && String(i.protocolBinding || "").toUpperCase() === "JSONRPC" && i.url) return i.url;
111
+ return card?.url ?? null;
112
+ }
113
+
114
+ export function cardProtocolVersion(card, def = VERSION) {
115
+ const c = card && typeof card === "object" ? card : {};
116
+ for (const v0 of [...(c.supportedInterfaces || []).filter((i) => i && String(i.protocolBinding || "").toUpperCase() === "JSONRPC").map((i) => i.protocolVersion), c.protocolVersion]) {
117
+ const v = String(v0 ?? "").trim(); if (v.startsWith("0.")) return "0.3"; if (v.startsWith("1.")) return "1.0";
118
+ }
119
+ return def;
120
+ }
121
+ const ver = (protocolVersion, card) => {
122
+ const v = protocolVersion ? String(protocolVersion).trim() : cardProtocolVersion(card);
123
+ if (v.startsWith("0.")) return "0.3"; if (v.startsWith("1.")) return "1.0"; throw new A2AError(`unsupported protocolVersion '${v}' (use '1.0' or '0.3')`);
124
+ };
125
+
126
+ export function buildAgentCard(name, description, url, { skills, version = "1.0.0", provider, streaming = false, pushNotifications = false, inputModes, outputModes,
127
+ documentationUrl, iconUrl, securitySchemes, securityRequirements, allowInsecureLocalhost = false, env } = {}) {
128
+ checkUrl(url, { allowInsecureLocalhost, env });
129
+ const out = (skills?.length ? skills : [{ id: "chat", name, description, tags: ["chat"] }]).map((s0) => {
130
+ const s = typeof s0 === "string" ? { id: s0 } : { ...s0 };
131
+ if (!s.id) throw new Error("skill needs an id");
132
+ s.name ??= s.id; s.description ??= s.name; s.tags = s.tags?.length ? [...s.tags] : [s.id]; return s;
133
+ });
134
+ if (provider && !(provider.organization && provider.url)) throw new Error("provider needs organization and url");
135
+ const card = { name, description, version, supportedInterfaces: [{ url, protocolBinding: "JSONRPC", protocolVersion: VERSION }],
136
+ capabilities: { streaming: !!streaming, pushNotifications: !!pushNotifications },
137
+ defaultInputModes: [...(inputModes || ["text/plain"])], defaultOutputModes: [...(outputModes || ["text/plain"])], skills: out };
138
+ for (const [k, v] of [["provider", provider], ["documentationUrl", documentationUrl], ["iconUrl", iconUrl], ["securitySchemes", securitySchemes], ["securityRequirements", securityRequirements]]) if (v) card[k] = v;
139
+ return card;
140
+ }
141
+
142
+ const uuid = () => globalThis.crypto.randomUUID();
143
+ const partsText = (parts) => { const p = Array.isArray(parts) ? parts : []; const t = p.filter((x) => x && typeof x.text === "string"); return [t.map((x) => x.text).join("\n"), p.length - t.length]; };
144
+
145
+ export function relayToA2A(msg, { role = "ROLE_USER", includeMeta = false } = {}) {
146
+ const m = { messageId: String(msg.id || uuid()), role, parts: [{ text: String(msg.body ?? "") }] };
147
+ if (msg.thread) m.contextId = msg.thread;
148
+ if (includeMeta) m.metadata = { relay: Object.fromEntries(["id", "from", "to", "project"].filter((k) => msg[k] != null).map((k) => [k, msg[k]])) };
149
+ return m;
150
+ }
151
+
152
+ export function a2aToRelay(msg, { agentUrl = null, to, project } = {}) {
153
+ const m = msg?.message && typeof msg.message === "object" ? msg.message : msg || {}, [body, dropped] = partsText(m.parts);
154
+ return { body, thread: m.contextId ?? null, message_id: m.messageId ?? null, dropped_parts: dropped, sender: { kind: "a2a", agent_url: agentUrl, verified: false }, content_trust: "untrusted",
155
+ ...(to ? { to } : {}), ...(project ? { project } : {}) };
156
+ }
157
+
158
+ export function normalize(result, agentUrl = null) {
159
+ const r = result && typeof result === "object" ? result : {};
160
+ let kind, o;
161
+ if (r.task && typeof r.task === "object") [kind, o] = ["task", r.task];
162
+ else if (r.message && typeof r.message === "object") [kind, o] = ["message", r.message];
163
+ else if (r.kind === "task" || ("status" in r && "id" in r)) [kind, o] = ["task", r];
164
+ else [kind, o] = ["message", r];
165
+ const out = { kind, text: "", context_id: o.contextId ?? null, task_id: (kind === "task" ? o.id : o.taskId) ?? null };
166
+ if (kind === "message") out.text = partsText(o.parts)[0];
167
+ else {
168
+ const st = o.status || {};
169
+ out.state = String(st.state || "").replace(/^TASK_STATE_/, "").toLowerCase().replace(/_/g, "-") || null;
170
+ out.text = (o.artifacts || []).map((a) => partsText(a?.parts)[0]).filter(Boolean).join("\n") || partsText(st.message?.parts)[0];
171
+ if (out.state === "rejected") out.relay_code = /^[a-z_]{1,64}$/.test(out.text) ? out.text : null;
172
+ }
173
+ return { ...out, raw: result, sender: { kind: "a2a", agent_url: agentUrl, verified: false }, content_trust: "untrusted" };
174
+ }
175
+ const METHODS = { "1.0": { send: "SendMessage", get: "GetTask", cancel: "CancelTask" }, "0.3": { send: "message/send", get: "tasks/get", cancel: "tasks/cancel" } };
176
+ async function rpc(agentUrl, op, params, v, { relayAuth, ...opts }) {
177
+ const d = await http("POST", agentUrl, { jsonrpc: "2.0", id: uuid(), method: METHODS[v][op], params }, { ...opts, version: v, headers: relayHeaders(relayAuth, agentUrl, opts.headers, opts) });
178
+ if (!d || typeof d !== "object") throw new A2AError("bad json-rpc response");
179
+ if (d.error) throw rpcError(d.error);
180
+ if (!("result" in d)) throw new A2AError("json-rpc response has no result");
181
+ return d.result;
182
+ }
183
+
184
+ export async function sendA2AMessage(agentUrl, text, { contextId, taskId, protocolVersion, card, project, ...opts } = {}) {
185
+ const v = ver(protocolVersion, card);
186
+ const m = { ...(v === "0.3" ? { kind: "message", messageId: uuid(), role: "user", parts: [{ kind: "text", text }] } : { messageId: uuid(), role: "ROLE_USER", parts: [{ text }] }),
187
+ ...(contextId ? { contextId } : {}), ...(taskId ? { taskId } : {}) };
188
+ const md = project ? { relay: { project } } : null;
189
+ return normalize(await rpc(agentUrl, "send", md ? { message: { ...m, metadata: md }, metadata: md } : { message: m }, v, opts), agentUrl);
190
+ }
191
+
192
+ export async function getA2ATask(agentUrl, taskId, { historyLength, protocolVersion, card, ...opts } = {}) {
193
+ return normalize(await rpc(agentUrl, "get", { id: taskId, ...(historyLength != null ? { historyLength } : {}) }, ver(protocolVersion, card), opts), agentUrl);
194
+ }
195
+
196
+ export async function cancelA2ATask(agentUrl, taskId, { protocolVersion, card, ...opts } = {}) {
197
+ return normalize(await rpc(agentUrl, "cancel", { id: taskId }, ver(protocolVersion, card), opts), agentUrl);
198
+ }
199
+
200
+ export class A2APushInvalid extends A2AError { constructor(m) { super(m); this.name = "A2APushInvalid"; } }
201
+ A2APushInvalid.prototype.kind = "push_invalid";
202
+ export class A2APushMismatch extends A2APushInvalid { constructor(m) { super(m); this.name = "A2APushMismatch"; } }
203
+ A2APushMismatch.prototype.kind = "push_mismatch";
204
+ const hget = (h, name) => {
205
+ const items = h && typeof h.entries === "function" && !Array.isArray(h) ? [...h.entries()] : Object.entries(h || {});
206
+ for (const [k, v] of items) if (String(k).toLowerCase() === name.toLowerCase()) return Array.isArray(v) ? v[0] : v;
207
+ return null;
208
+ };
209
+
210
+ export function parseA2aPush(body, headers, { binding = "jsonrpc" } = {}) {
211
+ if (binding !== "jsonrpc" && binding !== "rest") throw new A2APushInvalid("binding must be jsonrpc or rest");
212
+ if (body instanceof Uint8Array) body = new TextDecoder().decode(body);
213
+ if (typeof body === "string") { try { body = JSON.parse(body); } catch { throw new A2APushInvalid("body is not valid JSON"); } }
214
+ if (!body || typeof body !== "object" || Array.isArray(body)) throw new A2APushInvalid("body must be a JSON object");
215
+ let rpcId = null, method = null, msg;
216
+ if (binding === "jsonrpc") {
217
+ if (body.jsonrpc !== "2.0") throw new A2APushInvalid("not a JSON-RPC 2.0 request");
218
+ method = body.method;
219
+ if (method !== "SendMessage" && method !== "message/send") throw new A2APushInvalid("method must be SendMessage or message/send");
220
+ rpcId = body.id ?? null; msg = body.params && typeof body.params === "object" ? body.params.message : undefined;
221
+ } else msg = body.message;
222
+ if (!msg || typeof msg !== "object") throw new A2APushInvalid("message object missing");
223
+ const mid = msg.messageId;
224
+ if (typeof mid !== "string" || !mid) throw new A2APushInvalid("messageId missing");
225
+ const idem = hget(headers, "Idempotency-Key");
226
+ if (binding === "jsonrpc" && (rpcId === null || String(rpcId) !== mid)) throw new A2APushMismatch("JSON-RPC id does not match messageId");
227
+ if (idem !== null && idem !== mid) throw new A2APushMismatch("Idempotency-Key does not match messageId");
228
+ const ctx = typeof msg.contextId === "string" && msg.contextId ? msg.contextId : null, m = ctx ? /^relays:([^:]+)(?::([\s\S]+))?$/.exec(ctx) : null;
229
+ const auth = hget(headers, "Authorization"), b = typeof auth === "string" && /^bearer\s+\S/i.test(auth) ? auth.slice(7).trim() : null;
230
+ return { delivery_id: mid, message_id: mid, rpc_id: rpcId, idempotency_key: idem, a2a_version: String(hget(headers, "A2A-Version") ?? "").trim() === "1.0" ? "1.0" : "0.3",
231
+ method, context_id: ctx, project: m ? m[1] : null, thread: m ? m[2] ?? null : null, text: partsText(msg.parts)[0], bearer: b };
232
+ }
233
+
234
+ export class A2aPushDeduper {
235
+ constructor(maxItems = 1000) { this.maxItems = Math.max(1, maxItems | 0); this._m = new Map(); }
236
+ seen(id) {
237
+ if (this._m.has(id)) { this._m.delete(id); this._m.set(id, true); return true; }
238
+ this._m.set(id, true); while (this._m.size > this.maxItems) this._m.delete(this._m.keys().next().value);
239
+ return false;
240
+ }
241
+ }
242
+
243
+ export function a2aPushReply(rpcId, { taskId } = {}) {
244
+ const res = { task: { id: taskId || "relays-push-" + Math.random().toString(16).slice(2, 14), status: { state: "TASK_STATE_SUBMITTED" } } };
245
+ return rpcId === null || rpcId === undefined ? res : { jsonrpc: "2.0", id: rpcId, result: res };
246
+ }