a2a-exposed 0.8.3 → 0.8.4

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/deploy.mjs CHANGED
@@ -103,10 +103,13 @@ function syncTemplate(dir) {
103
103
  fs.cpSync(src, dir, { recursive: true, force: true, filter: (p) => !skip.has(path.basename(p)) });
104
104
  }
105
105
 
106
- function run(cmd, args, { cwd, env, capture = false, allowFail = false } = {}) {
107
- const r = spawnSync(cmd, args, { cwd, env: env || process.env, stdio: ["ignore", capture ? "pipe" : "inherit", "inherit"], encoding: "utf8" });
106
+ function run(cmd, args, { cwd, env, capture = false, captureStderr = false, allowFail = false } = {}) {
107
+ const r = spawnSync(cmd, args, { cwd, env: env || process.env, stdio: ["ignore", capture ? "pipe" : "inherit", capture && captureStderr ? "pipe" : "inherit"], encoding: "utf8" });
108
108
  if (r.error) die(`${cmd}: ${r.error.message}`);
109
- if (r.status !== 0 && !allowFail) die(`${cmd} ${args[0] || ""} ${args[1] || ""} failed (exit ${r.status})`);
109
+ if (r.status !== 0 && !allowFail) {
110
+ if (captureStderr && r.stderr) process.stderr.write(r.stderr.endsWith("\n") ? r.stderr : r.stderr + "\n");
111
+ die(`${cmd} ${args[0] || ""} ${args[1] || ""} failed (exit ${r.status})`);
112
+ }
110
113
  return r;
111
114
  }
112
115
 
@@ -298,26 +301,61 @@ async function verifyDeployedCard(base) {
298
301
  console.error(`warning: agent card not reachable yet at ${base} (${r.lastError || "no answer"}); ${C.get("A2A_HOSTNAME") ? "a new custom domain" : "a new workers.dev deployment or subdomain"} can take a few minutes. Check again with: ${CLI} status`);
299
302
  }
300
303
 
304
+ /** Drop cf's non-interactive confirm line. The migrations still apply; the question is answered yes. */
305
+ export function migrationStderr(text) {
306
+ return String(text || "").split(/\r?\n/).filter((line) => {
307
+ const t = line.trim();
308
+ if (!t) return false;
309
+ if (/Using fallback value in non-interactive context/i.test(t)) return false;
310
+ if (/^\? About to apply \d+ migration/.test(t)) return false;
311
+ if (/may not be available to serve requests during the migration, continue\?/.test(t)) return false;
312
+ return true;
313
+ }).join("\n");
314
+ }
315
+
301
316
  /** Apply D1 migrations quietly: cf prints a bare JSON array (often `[]`) when stdout is not a TTY. */
302
317
  function applyMigrations(dir, env) {
303
318
  step("applying D1 migrations");
304
- const r = runCf(dir, ["d1", "migrations", "apply", C.get("A2A_D1_ID"), "--dir", "migrations"], { env, capture: true });
319
+ const r = runCf(dir, ["d1", "migrations", "apply", C.get("A2A_D1_ID"), "--dir", "migrations"], { env, capture: true, captureStderr: true, allowFail: true });
320
+ const err = migrationStderr(r.stderr);
321
+ if (err) console.error(err);
322
+ if (r.status !== 0) die(`cf d1 migrations failed (exit ${r.status})`);
305
323
  let applied = null;
306
324
  try { applied = JSON.parse(r.stdout); } catch { /* not JSON: show as is */ }
307
325
  if (Array.isArray(applied)) console.error(applied.length ? ` applied: ${applied.map((m) => m.name || m.id || JSON.stringify(m)).join(", ")}` : " (none pending)");
308
326
  else if (r.stdout.trim()) console.error(r.stdout.trim());
309
327
  }
310
328
 
329
+ /** cf deploy sometimes names a Wrangler file. This project configures the Worker in cloudflare.config.ts. */
330
+ export function withoutWranglerConfigMention(text) {
331
+ return String(text || "").split(/\n/).filter((line) => !/Wrangler config file|Wrangler file/i.test(line)).join("\n");
332
+ }
333
+
311
334
  /** Run a command, streaming its output to stderr while collecting it. */
312
335
  function runTee(cmd, args, { cwd, env }) {
313
336
  return new Promise((resolve, reject) => {
314
337
  const ch = spawn(cmd, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
315
- let out = "";
316
- const on = (d) => { process.stderr.write(d); out += d; };
317
- ch.stdout.on("data", on);
318
- ch.stderr.on("data", on);
338
+ let out = "", pending = "";
339
+ const take = (d) => {
340
+ pending += d;
341
+ const lines = pending.split("\n");
342
+ pending = lines.pop();
343
+ for (const line of lines) {
344
+ if (withoutWranglerConfigMention(line) !== line) continue;
345
+ process.stderr.write(line + "\n");
346
+ out += line + "\n";
347
+ }
348
+ };
349
+ ch.stdout.on("data", take);
350
+ ch.stderr.on("data", take);
319
351
  ch.on("error", reject);
320
- ch.on("close", (code) => resolve({ status: code ?? 1, out }));
352
+ ch.on("close", (code) => {
353
+ if (pending && withoutWranglerConfigMention(pending) === pending) {
354
+ process.stderr.write(pending);
355
+ out += pending;
356
+ }
357
+ resolve({ status: code ?? 1, out });
358
+ });
321
359
  });
322
360
  }
323
361
 
@@ -421,6 +459,7 @@ export async function init(o) {
421
459
  applyMigrations(dir, env);
422
460
 
423
461
  let ownerToken = C.fileConfig().A2A_OWNER_TOKEN;
462
+ const rotated = !!(ownerToken && o["rotate-owner-token"]);
424
463
  if (!ownerToken || o["rotate-owner-token"]) ownerToken = randomToken(32);
425
464
  C.saveConfig({ A2A_OWNER_TOKEN: ownerToken, A2A_BASE_URL: publicBase() || undefined });
426
465
 
@@ -432,6 +471,7 @@ export async function init(o) {
432
471
  console.log(base);
433
472
  reportSwitch(switched);
434
473
  proxyNote(secrets);
474
+ if (rotated) console.error(`owner token replaced. Update A2A_OWNER_TOKEN wherever this inbox is called, then run \`${CLI} peers sync\` so uploaded peers stay readable.`);
435
475
  console.error(`config saved to ${C.CONFIG_FILE} (chmod 600). Next: ${CLI} status (shows the next setup step)`);
436
476
  printStar();
437
477
  }
@@ -594,7 +634,16 @@ export async function wake(sub, o) {
594
634
  if (sub === "unset") {
595
635
  const dir = workerDir(o), name = workerName();
596
636
  const env = cfEnv();
597
- for (const k of WAKE_SECRETS) runCf(dir, ["workers", "secrets", "delete", k, "--worker", name, "--force"], { env, allowFail: true });
637
+ const problems = [];
638
+ for (const k of WAKE_SECRETS) {
639
+ const r = runCf(dir, ["workers", "secrets", "delete", k, "--worker", name, "--force"], { env, allowFail: true, capture: true, captureStderr: true });
640
+ if (r.status === 0) continue;
641
+ const err = `${r.stderr || ""}\n${r.stdout || ""}`;
642
+ if (/404|not found|does not exist|could not be found/i.test(err)) continue; // already unset
643
+ const line = (err.match(/\[\d+\][^\n]*/) || [err.trim().split("\n").filter(Boolean).pop() || "cf failed"])[0];
644
+ problems.push(`${k}: ${line.slice(0, 300)}`);
645
+ }
646
+ if (problems.length) die(`could not remove wake secrets:\n ${problems.join("\n ")}`);
598
647
  return console.error("wake secrets removed (wake is now a no-op; use polling)");
599
648
  }
600
649
  die("usage: wake set|unset|test|preview|fingerprint");
package/lib/main.mjs CHANGED
@@ -53,7 +53,8 @@ https://github.com/telegraphic-dev/a2a-exposed/blob/main/docs/getting-started.md
53
53
  Plugin manifests leave MCP unset: the URL differs per deploy.
54
54
  When GET /owner/mcp reports enabled and the agent card is reachable,
55
55
  \`status\` prints \`claude mcp add --transport http a2a-exposed <url>/mcp\`
56
- --rotate-owner-token on init, replace the owner token; other settings come from config.env
56
+ --rotate-owner-token on init, replace the owner token; other settings come from config.env.
57
+ Then update A2A_OWNER_TOKEN wherever this inbox is called, and run \`peers sync\`
57
58
  --upstream URL proxy / expose mode: a public façade for an agent that already
58
59
  speaks A2A (JSON-RPC) on a private network. URL is its endpoint on
59
60
  a Cloudflare Tunnel hostname behind Access (never a Tailnet/LAN
package/lib/tunnel.mjs CHANGED
@@ -428,8 +428,11 @@ export async function remove(o, { quiet = false } = {}) {
428
428
  // 2. DNS before Access, so the hostname never resolves without its Access app
429
429
  await attempt("DNS record", "A2A_TUNNEL_DNS_ID", () => cfCall(dir, ["dns", "records", "delete", C.get("A2A_TUNNEL_DNS_ID"), "-z", C.get("A2A_TUNNEL_ZONE_ID"), "--force"], { allowNotFound: true }));
430
430
  await attempt("tunnel", "A2A_TUNNEL_ID", () => {
431
- cfCall(dir, ["tunnels", "connections", "cleanup", C.get("A2A_TUNNEL_ID")], { allowNotFound: true });
432
- cfCall(dir, ["tunnels", "delete", C.get("A2A_TUNNEL_ID"), "--force"], { allowNotFound: true });
431
+ const id = C.get("A2A_TUNNEL_ID");
432
+ // Already deleted (a previous rm stopped on the confirm prompt, or it was removed by hand): clear the saved id.
433
+ if (!cfCall(dir, ["tunnels", "get", id], { allowNotFound: true })) return;
434
+ cfCall(dir, ["tunnels", "connections", "cleanup", id, "--force"], { allowNotFound: true });
435
+ cfCall(dir, ["tunnels", "delete", id, "--force"], { allowNotFound: true });
433
436
  });
434
437
  await attempt("Access application (and its policy)", "A2A_TUNNEL_ACCESS_APP_ID", () => cfCall(dir, ["zero-trust", "access", "applications", "delete", C.get("A2A_TUNNEL_ACCESS_APP_ID"), "--force"], { allowNotFound: true }));
435
438
  await attempt("Access service token", "A2A_TUNNEL_ACCESS_TOKEN_ID", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2a-exposed",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "Give any AI agent a public A2A (Agent2Agent) endpoint: Cloudflare Worker inbox + wake webhook or scheduled inbox checks",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@ const v = (k: string) => (e[k] ?? "").trim();
11
11
  const name = v("A2A_WORKER_NAME") || "a2a-exposed";
12
12
  const hostname = v("A2A_HOSTNAME"); // e.g. agent.example.com (on a zone in your account)
13
13
  // No hostname: serve on <name>.<account subdomain>.workers.dev instead of a custom domain (and the other way round:
14
- // with a hostname, the workers.dev route is switched off).
14
+ // with a hostname, the workers.dev route is switched off). Preview URLs follow that same switch.
15
15
  const workersDev = !hostname;
16
16
  const wdSubdomain = v("A2A_WORKERS_DEV_SUBDOMAIN"); // the account's workers.dev subdomain, once known
17
17
  // Public base URL for the agent card, derived only from where the Worker is served (no override: a local webhook,
@@ -112,6 +112,7 @@ export default defineConfig((ctx) => ({
112
112
  ...(hosted ? { exports: { TenantStore: exports.durableObject({ storage: "sqlite" }) } } : {}),
113
113
  ...(hostname ? { domains: [hostname] } : {}),
114
114
  workersDev,
115
+ previewUrls: workersDev,
115
116
  // Optional persisted Workers Logs (A2A_WORKERS_LOGS=1, `init|deploy --workers-logs`): the JSON event lines plus
116
117
  // invocation logs with CPU time. Query strings are redacted, so one-time setup links never reach the logs.
117
118
  ...(v("A2A_WORKERS_LOGS") === "1" ? { observability: { enabled: true, redactQueryString: true, logs: { enabled: true, invocationLogs: true } } } : {}),
@@ -1,2 +1,2 @@
1
1
  /** Inbox build. Keep this equal to `version` in cli/package.json. */
2
- export const VERSION = "0.8.3";
2
+ export const VERSION = "0.8.4";