a2a-exposed 0.8.36 → 0.8.37

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/lib/hosted.mjs CHANGED
@@ -5,6 +5,7 @@ import { controlOrigin, deviceLogin } from "./account.mjs";
5
5
  import { CliError, upstreamUrlProblem } from "./a2a.mjs";
6
6
 
7
7
  export const SETUP_SCOPES = ["agents:read", "agents:create", "agents:owner-token", "wake:write", "wake:test"];
8
+ const SETUP_UNSUPPORTED = "This service does not expose scoped hosted setup capabilities. Check the --control-url service origin. Use the service dashboard to create an agent and configure Wake settings, or hosted MCP create_agent/set_wake with human confirmation and secret entry. Do not run init or deploy to repair a hosted address; no setup approval was requested.";
8
9
  const NEXT = "Run hosted capabilities --json to list scopes and presets; login --machine NAME --scopes SCOPES to approve setup.";
9
10
  const fail = (code, next) => { const error = new CliError(next); error.result = { ok: false, code, next }; throw error; };
10
11
  const printable = value => typeof value === "string" && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value);
@@ -34,14 +35,16 @@ function originFor(o = {}) {
34
35
 
35
36
  async function request(origin, path, { token = "", method = "GET", body, fetchImpl = fetch } = {}) {
36
37
  let response, data;
38
+ const discovery = method === "GET" && path === "/api/v1/setup/capabilities";
37
39
  try {
38
40
  response = await fetchImpl(`${origin}${path}`, {
39
41
  method, redirect: "error", signal: AbortSignal.timeout(30_000),
40
42
  headers: { accept: "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}), ...(body === undefined ? {} : { "content-type": "application/json" }) },
41
43
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
42
44
  });
43
- data = await response.json();
45
+ if (!(discovery && response.status === 404)) data = await response.json();
44
46
  } catch { fail("unavailable", "Hosted setup did not answer with JSON. Check the service and try again. Do not repeat a create request until hosted list confirms its result."); }
47
+ if (discovery && response.status === 404) fail("unsupported", SETUP_UNSUPPORTED);
45
48
  if (!data || typeof data !== "object" || Array.isArray(data)) fail("invalid_response", "The hosted service returned an invalid setup response.");
46
49
  if (!response.ok) {
47
50
  if (response.status === 429 && data.error === "too_many_references") {
@@ -70,7 +73,7 @@ async function request(origin, path, { token = "", method = "GET", body, fetchIm
70
73
 
71
74
  export async function capabilities(o = {}) {
72
75
  const data = await request(originFor(o), "/api/v1/setup/capabilities");
73
- if (!Array.isArray(data.scopes) || !Array.isArray(data.wake?.presets)) fail("unsupported", "This service does not expose hosted setup capabilities yet.");
76
+ if (!Array.isArray(data.scopes) || !Array.isArray(data.wake?.presets)) fail("unsupported", SETUP_UNSUPPORTED);
74
77
  return { ok: true, code: "capabilities", next: "Select explicit scopes and a wake preset from this listing.", scopes: data.scopes.filter(s => SETUP_SCOPES.includes(s)), presets: data.wake.presets.filter(s => /^[a-z-]+$/.test(s)), preflight: data.preflight === true, secret_reference: data.wake.secret_reference === true, wake_removal: data.wake.removal === true, secure_link_create: data.wake.secure_link_create === true };
75
78
  }
76
79
 
package/lib/main.mjs CHANGED
@@ -28,6 +28,7 @@ Setup options: --skip-install skips dependency installation when dependencies al
28
28
 
29
29
  Hosted setup:
30
30
  hosted capabilities --json list setup scopes, wake presets and service capabilities
31
+ unsupported/404: check --control-url; use dashboard or hosted MCP with human confirmation
31
32
  login --machine NAME --scopes SCOPES [--control-url URL] --json
32
33
  approve the explicit scopes using the printed website link once;
33
34
  save the scoped grant locally (mode 600), never print it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2a-exposed",
3
- "version": "0.8.36",
3
+ "version": "0.8.37",
4
4
  "description": "Give any AI agent a public A2A (Agent2Agent) endpoint: a Cloudflare Worker, a wake webhook, or scheduled checks",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1147,7 +1147,7 @@ async function checkedPeerFetch(ctx: TenantContext, ectx: ExecutionContext, base
1147
1147
  try { baseUrl = new URL(base); target = new URL(url); } catch { throw new ToolError("peer endpoint is not a URL"); }
1148
1148
  if (target.origin !== baseUrl.origin) throw new ToolError("peer card endpoint refused: it is on a different origin");
1149
1149
  const why = peerUrlProblem(target.href);
1150
- if (why) throw new ToolError(`peer endpoint ${target.href} refused: ${why}`);
1150
+ if (why) throw new ToolError(`peer endpoint ${target.host} refused: ${why}`);
1151
1151
  const internal = await fetchTenant(ctx, target, { ...init, redirect: "manual" });
1152
1152
  if (internal) {
1153
1153
  if (internal.status >= 300 && internal.status < 400) throw new ToolError("peer endpoint refused: redirects are not allowed");
@@ -1563,10 +1563,12 @@ async function deliverReplyMessage(ctx: TenantContext, ectx: ExecutionContext, t
1563
1563
  const pushed = await notifyPeer(ctx, ectx, task, signal);
1564
1564
  const failed = pushed.find((x: Json) => !x.ok);
1565
1565
  const target = safeTarget(undefined, String((failed || pushed[0])?.url || ""));
1566
- const http = /HTTP (\d{3})/.exec(String((failed || pushed[0])?.info || ""));
1566
+ const info = String((failed || pushed[0])?.info || "");
1567
+ const http = /^HTTP (\d{3})$/.exec(info);
1568
+ const httpStatus = http && http[0] === info ? Number(http[1]) : undefined;
1567
1569
  return {
1568
1570
  status: failed ? "delivery-failed" : "delivered", method: "push", taskId: task.id, state,
1569
- ...(target ? { target } : {}), ...(http ? { httpStatus: Number(http[1]) } : {}),
1571
+ ...(target ? { target } : {}), ...(httpStatus !== undefined && httpStatus >= 100 && httpStatus <= 599 ? { httpStatus } : {}),
1570
1572
  detail: failed ? "A task push failed; the reply remains stored on the task." : "Delivered as the task's existing push update.",
1571
1573
  };
1572
1574
  }
@@ -1,2 +1,2 @@
1
1
  /** Inbox build. Keep this equal to `version` in cli/package.json. */
2
- export const VERSION = "0.8.36";
2
+ export const VERSION = "0.8.37";
@@ -259,3 +259,36 @@ for (const version of ["0.3", "1.0"]) {
259
259
  });
260
260
  }
261
261
  }
262
+
263
+
264
+ test("push status only accepts an exact HTTP result in the output schema range", async (t) => {
265
+ const s = await fixture(t as never);
266
+ const originalFetch = globalThis.fetch;
267
+ t.after(() => { globalThis.fetch = originalFetch; });
268
+ for (const value of [200, 307, 401, 599, 99, 600, 999, "HTTP 999", "prefix HTTP 401 suffix", "HTTP 401\n"]) {
269
+ globalThis.fetch = (async () => {
270
+ if (typeof value === "string") throw value;
271
+ const response = new Response(null);
272
+ Object.defineProperty(response, "status", { value });
273
+ return response;
274
+ }) as typeof fetch;
275
+ const taskId = await s.sendToBob("status " + value, crypto.randomUUID(), { id: "cfg", url: "https://push.example.net/update" });
276
+ const result = await s.owner(s.b, "acct-bob", "POST", "/owner/mcp/reply", { taskId, text: "stored answer" });
277
+ const delivery = (result.body as any).structuredContent as Delivery;
278
+ assert.equal(delivery.httpStatus, typeof value === "number" && value >= 100 && value <= 599 ? value : undefined, String(value));
279
+ assert.equal(delivery.status, value === 200 ? "delivered" : "delivery-failed");
280
+ const stored = await s.dbB.prepare("SELECT state FROM tasks WHERE id = ?").bind(taskId).first() as { state: string };
281
+ assert.equal(stored.state, "completed");
282
+ }
283
+ });
284
+
285
+ test("direct reply refusal reports only the endpoint host", async (t) => {
286
+ const s = await fixture(t as never);
287
+ await s.dbB.prepare("UPDATE outbound_peers SET url = 'http://alice.example.com/private-path?private-query=yes' WHERE alias = 'alice'").run();
288
+ const taskId = await s.sendToBob("refused endpoint", "ctx-refused-url");
289
+ const result = await s.owner(s.b, "acct-bob", "POST", "/owner/mcp/reply", { taskId, text: "stored answer" });
290
+ const delivery = (result.body as any).structuredContent;
291
+ assert.equal(delivery.status, "delivery-failed");
292
+ assert.match(delivery.detail, /peer endpoint alice\.example\.com refused/);
293
+ assert.doesNotMatch(JSON.stringify(result.body), /private-path|private-query|http:\/\//);
294
+ });