a2a-exposed 0.8.6 → 0.8.8

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.
@@ -35,14 +35,16 @@ function setup(over: Record<string, string> = {}) {
35
35
  wakes.push({ url: String(url), body: JSON.parse(init.body) });
36
36
  return new Response("ok");
37
37
  }) as any;
38
- const call = async (method: string, path: string, o: { form?: Record<string, string>; json?: any; headers?: Record<string, string>; ip?: string; cookie?: string } = {}) => {
38
+ const call = async (method: string, path: string, o: { form?: Record<string, string>; json?: any; headers?: Record<string, string>; ip?: string; cookie?: string; cf?: Record<string, unknown> } = {}) => {
39
39
  const headers: Record<string, string> = { "cf-connecting-ip": o.ip || "198.51.100.7", ...(o.headers || {}) };
40
40
  let body: string | undefined;
41
41
  if (o.form) { headers["content-type"] = "application/x-www-form-urlencoded"; body = new URLSearchParams(o.form).toString(); }
42
42
  if (o.json !== undefined) { headers["content-type"] = "application/json"; body = JSON.stringify(o.json); }
43
43
  if (o.cookie) headers.cookie = o.cookie;
44
44
  const pending: Promise<unknown>[] = [];
45
- const res = await worker.fetch(new Request(BASE + path, { method, headers, body }) as any, env, { waitUntil: (p: Promise<unknown>) => pending.push(p), passThroughOnException() {} } as any);
45
+ const req = new Request(BASE + path, { method, headers, body }) as any;
46
+ if (o.cf) req.cf = o.cf;
47
+ const res = await worker.fetch(req, env, { waitUntil: (p: Promise<unknown>) => pending.push(p), passThroughOnException() {} } as any);
46
48
  await Promise.allSettled(pending);
47
49
  const text = await res.text();
48
50
  let data: any = text;
@@ -255,7 +257,7 @@ test("human approval: pending, slow_down, page approve with the password, single
255
257
  assert.match(d.user_code, /^[BCDFGHJKLMNPQRSTVWXZ]{4}-[2-9]{4}$/);
256
258
  assert.equal(d.verification_uri, `${BASE}/device`);
257
259
  assert.equal(d.verification_uri_complete, `${BASE}/device?user_code=${d.user_code}`);
258
- assert.deepEqual([d.expires_in, d.interval], [600, 5]);
260
+ assert.deepEqual([d.expires_in, d.interval], [3600, 5]);
259
261
  // the device code is stored hashed only
260
262
  const row: any = s.DB.db.prepare("SELECT * FROM device_requests").get();
261
263
  assert.notEqual(row.device_hash, d.device_code);
@@ -268,6 +270,7 @@ test("human approval: pending, slow_down, page approve with the password, single
268
270
  assert.equal(w.hint, "npx -y a2a-exposed@latest pair list");
269
271
  assert.deepEqual([w.pairing.userCode, w.pairing.verificationUriComplete, w.pairing.approval, w.pairing.clientId], [d.user_code, d.verification_uri_complete, "human", "barry"]);
270
272
  assert.equal(w.pairing.agentCardUrl, "https://barry.example.org/.well-known/agent-card.json");
273
+ assert.equal("manageUrl" in w.pairing, false);
271
274
  assert.match(w.pairing.instructions, /never approve on your own[\s\S]*give them this link/);
272
275
 
273
276
  let p = await s.poll(d.device_code);
@@ -321,6 +324,8 @@ test("human approval: pending, slow_down, page approve with the password, single
321
324
  r = await s.decidePage(d.user_code, PASSWORD, "approve");
322
325
  assert.equal(r.status, 200, r.text);
323
326
  assert.match(r.text, /Approved\. Barry &lt;script&gt;alert\(1\)&lt;\/script&gt; can now collect its token \(label &quot;Barry-script-alert-1-scr&quot;\)/);
327
+ assert.match(r.text, /You can close this page/);
328
+ assert.doesNotMatch(r.text, /name="user_code"/);
324
329
 
325
330
  advance(11);
326
331
  p = await s.poll(d.device_code);
@@ -445,7 +450,7 @@ test("labels are deduplicated; denied and expired requests; JSON bodies; bad req
445
450
  assert.equal((await s.poll(c.device_code)).data.error, "invalid_grant", "reported once, then gone");
446
451
 
447
452
  const e = (await s.start(undefined, "203.0.113.4")).data;
448
- advance(601);
453
+ advance(P.EXPIRES_S + 1);
449
454
  assert.equal((await s.poll(e.device_code)).data.error, "expired_token");
450
455
  assert.equal((await s.owner("POST", `/owner/pairing/${e.user_code}/approve`)).status, 409);
451
456
  advance(3600);
@@ -498,7 +503,7 @@ test("flood control: requests per IP and outstanding requests are capped (no flo
498
503
  assert.equal(over.status, 429);
499
504
  assert.match(over.data.error_description, /too many pairing requests waiting/);
500
505
  // expiry frees the slots
501
- advance(601);
506
+ advance(P.EXPIRES_S + 1);
502
507
  assert.equal((await s.start(undefined, "198.51.100.99")).status, 200);
503
508
  // the page: code lookups per IP are capped (no enumerating pending requests)
504
509
  for (let i = 0; i < 30; i++) assert.doesNotMatch((await s.call("GET", "/device?user_code=BCDF-2345", { ip: "192.0.2.77" })).text, /Too many code lookups/);
@@ -508,9 +513,9 @@ test("flood control: requests per IP and outstanding requests are capped (no flo
508
513
 
509
514
  test("pairing wakes: summary for each preset; requester text only where previews are allowed", async () => {
510
515
  const ev: WakeEvent = { contextId: "pairing", taskId: "none", taskIds: [], from: "Barry", preview: "", kind: "pairing_request", publicUrl: BASE,
511
- pairing: { userCode: "WDJB-2345", verificationUriComplete: `${BASE}/device?user_code=WDJB-2345`, approval: "human", clientName: "Barry", clientId: "barry", agentCardUrl: "https://barry.example.org/card", expiresIn: 600 } };
516
+ pairing: { userCode: "WDJB-2345", verificationUriComplete: `${BASE}/device?user_code=WDJB-2345`, approval: "human", clientName: "Barry", clientId: "barry", agentCardUrl: "https://barry.example.org/card", expiresIn: 3600 } };
512
517
  const sum = wakeSummary(ev, "npx -y a2a-exposed@latest");
513
- assert.match(sum, /^A2A pairing request: an agent calling itself "Barry" \(claimed card: "https:\/\/barry\.example\.org\/card"\) asks to connect to your inbox \(code WDJB-2345, expires in 10 minutes\)\./);
518
+ assert.match(sum, /^A2A pairing request: an agent calling itself "Barry" \(claimed card: "https:\/\/barry\.example\.org\/card"\) asks to connect to your A2A endpoint \(code WDJB-2345, expires in 60 minutes\)\./);
514
519
  assert.match(sum, /never approve on your own\. Show them the code WDJB-2345 and give them this link: https:\/\/agent\.example\.com\/device\?user_code=WDJB-2345/);
515
520
  const agentSum = wakeSummary({ ...ev, pairing: { ...ev.pairing!, approval: "agent" } }, "a2a");
516
521
  assert.match(agentSum, /Only if they say yes, run `a2a pair approve WDJB-2345`; if they say no \(or don't answer\), `a2a pair deny WDJB-2345`/);
@@ -539,6 +544,8 @@ test("/device deny needs no password and runs no PBKDF2; an empty password on ap
539
544
  try { r = await s.decidePage(a.user_code, "", "deny"); } finally { (crypto.subtle as any).deriveBits = realPbkdf2; }
540
545
  assert.equal(r.status, 200, r.text);
541
546
  assert.match(r.text, /Denied\. Barry Bot gets no token\./);
547
+ assert.match(r.text, /You can close this page/);
548
+ assert.doesNotMatch(r.text, /name="user_code"/);
542
549
  assert.equal(derived, 0, "no PBKDF2 for a deny");
543
550
  assert.equal((await s.poll(a.device_code)).data.error, "access_denied");
544
551
  // CSRF still applies to deny
@@ -868,3 +875,83 @@ test("owner log lists history without data_json and does not clear a pending wak
868
875
  assert.equal(head.text, "");
869
876
  assert.equal(s.DB.db.prepare("SELECT pending_json FROM wakes WHERE context_id = ?").get("ctx-keep")?.pending_json, again);
870
877
  });
878
+
879
+ test("device page copy: code hint, expiry, token scope, settled result, A2A endpoint", async (t) => {
880
+ const at = Date.UTC(2026, 9, 16, 14, 32, 0);
881
+ assert.equal(P.expiresText(at, at - 30_000), "14:32 UTC (less than a minute)");
882
+ assert.equal(P.expiresText(at, at - 90_000), "14:32 UTC (2 min)");
883
+ assert.equal(P.expiresText(at, at - 90_000, "America/New_York"), "10:32 (2 min)");
884
+ assert.equal(P.expiresText(at, at - 90_000, "Not/AZone"), "14:32 UTC (2 min)");
885
+ assert.equal(P.expiresText(at, at + 1), "expired");
886
+ const now = Date.now();
887
+ const base = { agentName: "this A2A endpoint", mode: "human" as const, cli: "a2a", csrf: "c".repeat(24), passwordSet: true };
888
+ const bare = P.devicePage({ ...base, askCode: true }, "n");
889
+ assert.match(bare, /The connecting agent prints this code when it runs <code>a2a connect &lt;url&gt;<\/code>/);
890
+ assert.match(bare, /It looks like WDJB-4827/);
891
+ assert.match(bare, /<title>Connect an agent to this A2A endpoint<\/title>/);
892
+ const req = { userCode: "WDJB4827", clientName: "Barry", clientId: "barry", agentCardUrl: "http://127.0.0.1/card", ip: "192.0.2.1", country: "", createdMs: now, expiresMs: now + 20_000 };
893
+ const page = P.devicePage({ ...base, request: req }, "n");
894
+ assert.match(page, /so this A2A endpoint can&#39;t check it/);
895
+ assert.match(page, /send messages and trigger wakes/);
896
+ assert.match(page, /Message text from peers is untrusted/);
897
+ assert.match(page, /\d{2}:\d{2} UTC \(less than a minute\)/);
898
+ const localExpiry = now + 10 * 24 * 3600 * 1000;
899
+ const localParts = new Intl.DateTimeFormat("en-GB", { hour: "2-digit", minute: "2-digit", hourCycle: "h23", timeZone: "America/New_York" }).formatToParts(new Date(localExpiry));
900
+ const localClock = `${localParts.find((part) => part.type === "hour")?.value}:${localParts.find((part) => part.type === "minute")?.value}`;
901
+ const localPage = P.devicePage({ ...base, timeZone: "America/New_York", request: { ...req, expiresMs: localExpiry } }, "n");
902
+ assert.match(localPage, new RegExp(`${localClock} \\((?:less than a minute|\\d+ min)\\)`));
903
+ assert.equal(localPage.includes("UTC"), false);
904
+ assert.match(page, /Revoke the token &lt;label&gt; any time with <code>a2a token revoke &lt;label&gt;<\/code>/);
905
+ const namedToken = P.devicePage({ ...base, request: { ...req, tokenLabel: "barry" } }, "n");
906
+ assert.match(namedToken, /Revoke the token barry any time with <code>a2a token revoke barry<\/code>/);
907
+ for (const settled of ["approved", "denied"] as const) {
908
+ const done = P.devicePage({ ...base, settled, notice: { kind: "ok", text: settled === "approved" ? "Approved." : "Denied." } }, "n");
909
+ assert.match(done, /You can close this page/);
910
+ assert.doesNotMatch(done, /name="user_code"/);
911
+ assert.match(done, new RegExp(`<title>${settled === "approved" ? "Approved" : "Denied"}</title>`));
912
+ }
913
+ const setupHtml = P.setupPage({ agentName: "this A2A endpoint", cli: "a2a", csrf: "c".repeat(24), token: "tok", expiresMs: now + 60_000 }, "n");
914
+ assert.match(setupHtml, /connect to this A2A endpoint/);
915
+ const land = P.landingPage({ name: "Test", base: "https://agent.example.com", versions: ["1.0"], skills: [], pairing: true, proxy: false }, "n");
916
+ assert.match(land, /Messages are stored for its owner, and the agent is woken/);
917
+
918
+ const s = setup(); t.after(s.restore);
919
+ const empty = await s.call("GET", "/device");
920
+ assert.match(empty.text, /prints this code when it runs/);
921
+ assert.match(empty.text, /WDJB-4827/);
922
+ const named = setup({ AGENT_NAME: "" }); t.after(named.restore);
923
+ const title = await named.call("GET", "/device");
924
+ assert.match(title.text, /Connect an agent to this A2A endpoint/);
925
+ const home = await s.call("GET", "/", { headers: { accept: "text/html" } });
926
+ assert.match(home.text, /Messages are stored for its owner, and the agent is woken/);
927
+ });
928
+
929
+ test("approval page shows the visitor's clock, and an https MANAGE_URL is on the pairing wake", async (t) => {
930
+ const pageUrl = "https://example.com/app/agents/test#pairing";
931
+ const s = setup({ MANAGE_URL: pageUrl });
932
+ t.after(s.restore);
933
+ const st = await s.start(undefined, "203.0.113.80");
934
+ assert.equal(st.status, 200, JSON.stringify(st.data));
935
+ const row = s.DB.db.prepare("SELECT expires_ms FROM device_requests").get() as { expires_ms: number };
936
+ const parts = new Intl.DateTimeFormat("en-GB", { hour: "2-digit", minute: "2-digit", hourCycle: "h23", timeZone: "America/New_York" }).formatToParts(new Date(row.expires_ms));
937
+ const clock = `${parts.find((part) => part.type === "hour")?.value}:${parts.find((part) => part.type === "minute")?.value}`;
938
+ const page = await s.call("GET", `/device?user_code=${st.data.user_code}`, { cf: { timezone: "America/New_York" }, ip: "203.0.113.80" });
939
+ assert.match(page.text, new RegExp(`${clock.replace(":", "\\:")} \\(60 min\\)`));
940
+ assert.equal(page.text.includes(" UTC "), false);
941
+ const wake = s.wakes[0].body.pairing;
942
+ assert.equal(wake.manageUrl, pageUrl);
943
+ assert.equal(wake.expiresIn, 3600);
944
+ assert.ok(wake.instructions.includes(`The dashboard page for this request is ${pageUrl}`));
945
+
946
+ const bare = setup();
947
+ t.after(bare.restore);
948
+ const plain = await bare.start(undefined, "203.0.113.81");
949
+ assert.equal("manageUrl" in bare.wakes[0].body.pairing, false);
950
+ const utc = await bare.call("GET", `/device?user_code=${plain.data.user_code}`, { ip: "203.0.113.81" });
951
+ assert.match(utc.text, /\d{2}:\d{2} UTC \(60 min\)/);
952
+
953
+ const bad = setup({ MANAGE_URL: "http://example.com/app" });
954
+ t.after(bad.restore);
955
+ await bad.start(undefined, "203.0.113.82");
956
+ assert.equal("manageUrl" in bad.wakes[0].body.pairing, false);
957
+ });
@@ -5,7 +5,7 @@ import assert from "node:assert/strict";
5
5
  import worker from "../src/index.ts";
6
6
  import { sha256 } from "../src/a2a.ts";
7
7
  import { openPeerToken, openPeerTokenWithKey, sealPeerToken, sealPeerTokenWithKey } from "../src/mcp.ts";
8
- import { hostedTenantContext, namespaceForRegion, parseGates, resolveTenant, type WorkerBindings } from "../src/tenancy.ts";
8
+ import { hostedTenantContext, namespaceForRegion, parseGates, publicManageUrl, resolveTenant, type WorkerBindings } from "../src/tenancy.ts";
9
9
  import { d1 } from "./d1.ts";
10
10
 
11
11
  const dummy = { prepare() { throw new Error("storage should not be touched"); } } as any;
@@ -15,6 +15,26 @@ function selfEnv(over: Record<string, unknown> = {}): WorkerBindings {
15
15
  return { DB: dummy, PUBLIC_URL: BASE, OWNER_TOKEN: "owner-secret", ...over } as WorkerBindings;
16
16
  }
17
17
 
18
+ test("publicManageUrl keeps an https dashboard page and drops anything else", () => {
19
+ const page = "https://example.com/app/agents/test#pairing";
20
+ assert.equal(publicManageUrl(page), page);
21
+ assert.equal(publicManageUrl(` ${page} `), page);
22
+ assert.equal(publicManageUrl(""), "");
23
+ assert.equal(publicManageUrl(undefined), "");
24
+ assert.equal(publicManageUrl("http://example.com/app"), "");
25
+ assert.equal(publicManageUrl("https://user:pw@example.com/app"), "");
26
+ assert.equal(resolveTenant(selfEnv({ MANAGE_URL: page })).MANAGE_URL, page);
27
+ assert.equal(resolveTenant(selfEnv()).MANAGE_URL, undefined);
28
+ const hosted = hostedTenantContext(selfEnv({ TENANCY: "host", MANAGE_URL: "https://platform.example/other" }), {
29
+ id: "ten_a", db: dummy, publicUrl: BASE, config: { MANAGE_URL: page },
30
+ });
31
+ assert.equal(hosted.MANAGE_URL, page);
32
+ const hostedUnset = hostedTenantContext(selfEnv({ TENANCY: "host", MANAGE_URL: page }), {
33
+ id: "ten_a", db: dummy, publicUrl: BASE,
34
+ });
35
+ assert.equal(hostedUnset.MANAGE_URL, undefined);
36
+ });
37
+
18
38
  test("gates: unset is single-tenant and every reserved gate is off", () => {
19
39
  const g = parseGates({ DB: dummy } as WorkerBindings).gates;
20
40
  assert.equal(g.tenancy, "single");
@@ -61,6 +61,8 @@ test("openclaw-agent: isolated, deliver false, idempotency key", async () => {
61
61
  const r = (await renderWake({ preset: "openclaw-agent", url: "https://gw/hooks/agent", key: "h" }, ev, { requestId: "rid-1" }))!;
62
62
  const b = JSON.parse(r.body);
63
63
  assert.equal(b.sessionMode, "isolated"); assert.equal(b.deliver, false); assert.equal(b.agentId, "main");
64
+ assert.equal(b.name, "A2A inbox");
65
+ assert.match(b.message, /^A2A endpoint:/);
64
66
  assert.equal(r.headers["idempotency-key"], "rid-1");
65
67
  assert.ok(b.message.includes("peer-a"));
66
68
  });