a2a-exposed 0.8.20 → 0.8.21

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
@@ -11,6 +11,14 @@ import { readHidden } from "./pair.mjs";
11
11
  import * as WD from "./workersdev.mjs";
12
12
  import { printStar } from "./feedback.mjs";
13
13
 
14
+ export const VERSION = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
15
+
16
+ /** Arguments shared by deploy attempts, including workers.dev registration retries. */
17
+ export function deployArgs(version, isoDate) {
18
+ if (!/^[A-Za-z0-9._-]+$/.test(version)) throw new Error("invalid Worker version tag");
19
+ return ["deploy", "--message", `a2a-exposed ${version} ${isoDate}`, "--tag", `a2a-exposed-${version}`];
20
+ }
21
+
14
22
  const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
23
  /** Each Claude Code routine fire is a new cloud session: a 2xx only means a session started. */
16
24
  export const CLAUDE_CODE_NOTE = [
@@ -392,7 +400,7 @@ function runTee(cmd, args, { cwd, env }) {
392
400
  /** cf deploy (secrets via a temporary chmod-600 file). If the account has no workers.dev subdomain yet and
393
401
  * `register` names one, retry through cf's own registration prompt. Returns cf's output. */
394
402
  async function cfDeploy(dir, secrets, { register } = {}) {
395
- const base = ["deploy", "--message", `a2a-exposed ${new Date().toISOString()}`];
403
+ const base = deployArgs(VERSION, new Date().toISOString());
396
404
  const go = async (file) => {
397
405
  const args = cfArgs(file ? [...base, "--secrets-file", file] : base);
398
406
  let r = await runTee(cfBin(dir), args, { cwd: dir, env: deployEnv() });
package/lib/main.mjs CHANGED
@@ -88,7 +88,7 @@ https://github.com/telegraphic-dev/a2a-exposed/blob/main/docs/getting-started.md
88
88
  e.g. "node /path/to/repo/cli/bin/a2a-exposed.mjs")
89
89
  --worker-dir where the Worker project (template copy) lives (default
90
90
  <config dir>/worker). The config dir is --config-dir / A2A_CONFIG_DIR
91
- deploy [same flags] redeploy with saved settings (secrets persist)
91
+ deploy [same flags] redeploy with saved settings (secrets persist); tag the Worker version as a2a-exposed-<version>
92
92
  wake set [--preset P] [--agent-id A] [--key-header H] [--key-prefix X] [--body-template JSON]
93
93
  [--cli-command CMD] [--debounce S] [--max-per-hour N]
94
94
  save wake settings, upload wake secrets from env, and redeploy (one step)
@@ -145,11 +145,12 @@ Remove
145
145
  --dir and --worker-dir are the Worker project directory (default <config dir>/worker)
146
146
 
147
147
  Status
148
- status [--json] deployment, base URL, agent card check (fetched by the CLI: no curl needed), wake
148
+ status [--json] deployment, Worker version, base URL, agent card check (fetched by the CLI), wake
149
149
  mode (webhook / tunnel / none = polling), tunnel state, proxy mode (--upstream):
150
150
  upstream card, Access credential and upstream bearer configured (separately), the
151
151
  live upstream check, the last failed peer call, card-leak check; warnings for peer
152
152
  token variables shadowing saved tokens; and the next step to run.
153
+ Older Workers need deploy (exit 1); npm latest hints use the local update cache.
153
154
  When GET /owner/mcp reports enabled and the agent card is reachable, prints
154
155
  \`claude mcp add --transport http a2a-exposed <base URL>/mcp\`.
155
156
  A failed probe, an unreachable card, or a Worker that reports MCP off leaves that line out.
package/lib/status.mjs CHANGED
@@ -8,6 +8,17 @@ import { CLI, cardUrlProblem, cfErrorCode, describeHttp, die, fingerprint, httpJ
8
8
  import { baseUrl, owner, ownerTry } from "./commands.mjs";
9
9
  import * as D from "./deploy.mjs";
10
10
  import * as T from "./tunnel.mjs";
11
+ import { cacheFile, readCache, newer } from "./update.mjs";
12
+
13
+ /** Public health probe. Only a valid health answer establishes missing version reporting. */
14
+ export async function checkHealth(base) {
15
+ try {
16
+ const { status, data } = await httpJson(`${base}/health`, { timeout: 15000 });
17
+ if (status === 200 && data?.ok === true)
18
+ return { ok: true, version: typeof data.version === "string" && data.version ? data.version : null };
19
+ } catch { /* unreachable: version stays unknown */ }
20
+ return { ok: false, version: null };
21
+ }
11
22
 
12
23
  /** Presets whose webhook usually listens only locally: wakes need the tunnel (or another reverse proxy). */
13
24
  export const LOCAL_PRESETS = ["openclaw-wake", "openclaw-agent", "hermes"];
@@ -111,6 +122,9 @@ export function secretsPropagating(tunnel, now = Date.now()) {
111
122
  * as separate lines (so the next step stays one short instruction). */
112
123
  export function nextStep(s) {
113
124
  const n = nextStepCore(s);
125
+ const version = s.cliVersion || D.VERSION;
126
+ if (s.workerVersion && s.latestVersion && newer(s.latestVersion, s.workerVersion) && !newer(s.workerVersion, version))
127
+ n.also.push(`a2a-exposed ${s.latestVersion} is available: run npx -y a2a-exposed@latest deploy`);
114
128
  const sh = (s.peerTokenShadowed || []).map((k) => `${k} is set in the environment and overrides the different peer token saved in ${s.configFile} (e.g. a stale value from before \`connect\` stored a new one): send/poll use the environment value. Unset ${k} where it is set to use the saved token`);
115
129
  return sh.length ? { ...n, also: [...n.also, ...sh] } : n;
116
130
  }
@@ -133,6 +147,11 @@ function nextStepCore(s) {
133
147
  return fail(/HTTP 401/.test(s.ownerApi.error)
134
148
  ? `the owner token in ${s.configFile} does not match the Worker: run \`${CLI} init --rotate-owner-token\` from the machine that owns this deployment`
135
149
  : `the owner API failed (${s.ownerApi.error}): run \`${CLI} deploy\``);
150
+ const version = s.cliVersion || D.VERSION;
151
+ if (s.workerVersion && newer(version, s.workerVersion))
152
+ return fail(`the deployed Worker is ${s.workerVersion}, older than this CLI (${version}): run \`${CLI} deploy\``);
153
+ if (s.health?.ok && s.health.version === null)
154
+ return fail(`the deployed Worker predates version reporting (older than ${version}): run \`${CLI} deploy\``);
136
155
  // proxy / expose mode: the upstream answers peers, so wakes are optional (pairing notifications only)
137
156
  const fc = s.facade;
138
157
  if (fc) {
@@ -220,12 +239,14 @@ export async function collect() {
220
239
  upstream: C.get("A2A_UPSTREAM_URL") || null, facade: null, upstreamVerify: null, upstreamVerifyError: "", facadePeerAuth: null,
221
240
  upstreamNoToken: C.get("A2A_UPSTREAM_NO_TOKEN") === "1", localUpstreamToken: fingerprint(process.env.UPSTREAM_TOKEN),
222
241
  peerTokenShadowed: shadowedPeerTokens(),
223
- mcp: "unknown",
242
+ mcp: "unknown", health: null, workerVersion: null, cliVersion: D.VERSION, latestVersion: readCache(cacheFile())?.latest || null,
224
243
  };
225
244
  const envBase = (process.env.A2A_BASE_URL || "").replace(/\/$/, "");
226
245
  if (envBase && envBase !== s.baseUrlSaved) s.baseUrlEnv = envBase;
227
246
  if (!s.deployed || !s.baseUrl) return s;
228
247
  s.card = await checkCard(s.baseUrl);
248
+ s.health = await checkHealth(s.baseUrl);
249
+ s.workerVersion = s.health.version;
229
250
  if (s.hasOwnerToken) {
230
251
  try {
231
252
  const p = await owner("GET", "/owner/wake/preview");
@@ -335,6 +356,7 @@ export async function status(o) {
335
356
  if (s.deployed && s.baseUrl) {
336
357
  rows.push(["agent card", !s.card.ok ? `FAILED: ${s.card.error}` : s.card.urlProblem ? `WRONG URL: "${s.card.name}", but ${s.card.urlProblem}`
337
358
  : `OK: "${s.card.name}" (A2A ${s.card.versions.join(", ") || "?"})`]);
359
+ rows.push(["worker version", `${s.workerVersion || "unknown"} (CLI ${s.cliVersion})`]);
338
360
  rows.push(["owner API", s.ownerApi.ok ? "OK" : `FAILED: ${s.ownerApi.error}`]);
339
361
  if (s.upstream) {
340
362
  const f = s.facade || {};
package/lib/update.mjs CHANGED
@@ -36,7 +36,7 @@ export function notice(current, cache) {
36
36
 
37
37
  export const due = (cache, now = Date.now()) => !cache || !(now - (cache.checkedAt || 0) < CHECK_INTERVAL_MS);
38
38
 
39
- function readCache(file) {
39
+ export function readCache(file) {
40
40
  try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; }
41
41
  }
42
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2a-exposed",
3
- "version": "0.8.20",
3
+ "version": "0.8.21",
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": {
@@ -4,6 +4,7 @@
4
4
  // Worker serves a rewritten agent card and device-flow pairing, and forwards authenticated JSON-RPC to the upstream.
5
5
  // Handlers take a TenantContext (tenancy.ts) built from the Worker bindings. With the hosted gates unset, that
6
6
  // context is the single self-host tenant and behaviour matches a Worker that read env.* directly.
7
+ import { VERSION } from "./version.ts";
7
8
  import * as A from "./a2a.ts";
8
9
  import * as F from "./facade.ts";
9
10
  import { renderWake, redact, cloudflareErrorHint, wakeUrlIsTunnelHost, defaultDebounceSeconds, defaultMaxPerHour, type WakeConfig, type WakeEvent } from "./wake.ts";
@@ -3404,7 +3405,7 @@ async function handle(req: Request, ctx: TenantContext, ectx: ExecutionContext,
3404
3405
  // legacy discovery path (A2A 0.2): the 0.3-shaped card that clients of that era parse
3405
3406
  if (req.method === "GET" && path === "/.well-known/agent.json")
3406
3407
  return json(agentCard03(ctx, await agentCard(ctx)), 200, { "access-control-allow-origin": "*", "cache-control": cardCacheControl(ctx) });
3407
- if (req.method === "GET" && path === "/health") return json({ ok: true, publicUrl: ctx.PUBLIC_URL, mode: proxyMode(ctx) ? "proxy" : "inbox", mcp: mcpOn(ctx) });
3408
+ if (req.method === "GET" && path === "/health") return json({ ok: true, version: VERSION, publicUrl: ctx.PUBLIC_URL, mode: proxyMode(ctx) ? "proxy" : "inbox", mcp: mcpOn(ctx) });
3408
3409
  if (path === "/mcp") return mcpOn(ctx) ? await handleMcp(req, ctx, ectx, url) : json({ error: "not_found", error_description: "the MCP server is turned off on this A2A endpoint (MCP=off)" }, 404);
3409
3410
  if (mcpOn(ctx)) {
3410
3411
  if (req.method === "GET" && path === "/.well-known/oauth-protected-resource/mcp") return json(mcpResourceMetadata(ctx), 200, { "access-control-allow-origin": "*" });
@@ -1,2 +1,2 @@
1
1
  /** Inbox build. Keep this equal to `version` in cli/package.json. */
2
- export const VERSION = "0.8.20";
2
+ export const VERSION = "0.8.21";
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import { createRequire } from "node:module";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { Miniflare, convertV4MiniflareOptions } from "miniflare";
10
+ import { VERSION } from "../src/version.ts";
10
11
  import worker from "../src/index.ts";
11
12
  import { sha256 } from "../src/a2a.ts";
12
13
  import { sealWakeSecrets } from "../src/wake-secrets.ts";
@@ -81,6 +82,7 @@ test("gates unset: the hosted entry matches the self-host handler, including TEN
81
82
  const base = "https://agent.example.com";
82
83
  const nodeEnv = { DB: d1(MIGRATIONS), PUBLIC_URL: base, OWNER_TOKEN: "owner-secret" };
83
84
  const nodeHealth = await text(await worker.fetch(new Request(base + "/health"), nodeEnv as never, ectx as never));
85
+ assert.deepEqual(JSON.parse(nodeHealth), { ok: true, version: VERSION, publicUrl: base, mode: "inbox", mcp: true });
84
86
  const nodeDenied = await text(await worker.fetch(new Request(base + "/", {
85
87
  method: "POST", headers: { "content-type": "application/json" },
86
88
  body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "message/send", params: {} }),
@@ -180,6 +182,9 @@ test("per-tenant SQLite: directory, config push, and isolation", async (t) => {
180
182
  assert.equal(bobPush.result.applied, true, JSON.stringify(bobPush.result));
181
183
  await kv.put("tenant:bobby", JSON.stringify({ id: "id-bobby", status: "active", region: "", version: 1 }));
182
184
 
185
+ assert.deepEqual(await (await mf.dispatchFetch("https://alice.example.com/health")).json(),
186
+ { ok: true, version: VERSION, publicUrl: "https://alice.example.com", mode: "inbox", mcp: true });
187
+
183
188
  const card = async (name: string, headers?: HeadersInit) => (await mf.dispatchFetch(`https://${name}.example.com/.well-known/agent-card.json`, { headers })).json() as Promise<{ name: string }>;
184
189
  const aliceCard = await mf.dispatchFetch("https://alice.example.com/.well-known/agent-card.json");
185
190
  assert.equal(aliceCard.headers.get("cache-control"), "public, max-age=60");
@@ -1,5 +1,6 @@
1
1
  // Tenant context: gates default off, self-host owner auth and peer-token key stay compatible, hosted uses a hash
2
2
  // and TENANT_SECRETS_KEY. Run: npm test
3
+ import { VERSION } from "../src/version.ts";
3
4
  import { test } from "node:test";
4
5
  import assert from "node:assert/strict";
5
6
  import worker from "../src/index.ts";
@@ -258,7 +259,7 @@ test("fetch path: gates set or unset, the self-host Worker still answers from OW
258
259
  const plain = await call(base, "/health");
259
260
  const reserved = await call({ ...base, ...gates }, "/health");
260
261
  const empty = await call({ ...base, TENANCY: "", QUOTAS: "", DATA_REGION: "", SIGNUP_URL: "" }, "/health");
261
- assert.deepEqual(plain.data, { ok: true, publicUrl: BASE, mode: "inbox", mcp: true });
262
+ assert.deepEqual(plain.data, { ok: true, version: VERSION, publicUrl: BASE, mode: "inbox", mcp: true });
262
263
  assert.deepEqual(reserved.data, plain.data);
263
264
  assert.deepEqual(empty.data, plain.data);
264
265