a2a-exposed 0.8.2 → 0.8.3
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/package.json
CHANGED
package/worker/src/index.ts
CHANGED
|
@@ -1312,7 +1312,7 @@ async function tokenEndpoint(req: Request, ctx: TenantContext): Promise<Response
|
|
|
1312
1312
|
if (now >= r.expires_ms) return oauthError("expired_token", "the device_code expired; start again");
|
|
1313
1313
|
if (r.status === "denied") {
|
|
1314
1314
|
await ctx.DB.prepare("DELETE FROM device_requests WHERE device_hash = ?").bind(hash).run();
|
|
1315
|
-
return oauthError("access_denied", "the
|
|
1315
|
+
return oauthError("access_denied", "the agent owner denied this request");
|
|
1316
1316
|
}
|
|
1317
1317
|
if (r.status === "pending") {
|
|
1318
1318
|
// RFC 8628 §3.5: polling faster than the interval -> slow_down, and the interval grows by 5 s for good
|
|
@@ -1321,7 +1321,7 @@ async function tokenEndpoint(req: Request, ctx: TenantContext): Promise<Response
|
|
|
1321
1321
|
return oauthError("slow_down", `polling too fast; wait at least ${r.interval_s + 5} seconds between requests`);
|
|
1322
1322
|
}
|
|
1323
1323
|
await ctx.DB.prepare("UPDATE device_requests SET last_poll_ms = ? WHERE device_hash = ?").bind(now, hash).run();
|
|
1324
|
-
return oauthError("authorization_pending", "waiting for the
|
|
1324
|
+
return oauthError("authorization_pending", "waiting for the agent owner to approve");
|
|
1325
1325
|
}
|
|
1326
1326
|
// approved: single use. Only the request that flips the row gets the token; the row is then deleted.
|
|
1327
1327
|
const claimed = await ctx.DB.prepare("UPDATE device_requests SET status = 'redeemed' WHERE device_hash = ? AND status = 'approved'").bind(hash).run();
|
|
@@ -2029,7 +2029,7 @@ async function authorizePage(req: Request, ctx: TenantContext, url: URL, head =
|
|
|
2029
2029
|
return show(403, { kind: "error", text: "This form expired or came from another site. Start connecting again from your MCP client." });
|
|
2030
2030
|
if (form.decision === "deny") {
|
|
2031
2031
|
log("mcp_authorization_denied", { ip, clientName: c.client!.client_name });
|
|
2032
|
-
return redirectWith(ctx, c.redirect!, { error: "access_denied", error_description: "the
|
|
2032
|
+
return redirectWith(ctx, c.redirect!, { error: "access_denied", error_description: "the agent owner denied access", state: c.params.state });
|
|
2033
2033
|
}
|
|
2034
2034
|
if (form.decision === "oidc") {
|
|
2035
2035
|
const started = await O.startOidc(ctx, {
|
package/worker/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Inbox build. Keep this equal to `version` in cli/package.json. */
|
|
2
|
-
export const VERSION = "0.8.
|
|
2
|
+
export const VERSION = "0.8.3";
|
package/worker/test/mcp.test.ts
CHANGED
|
@@ -231,7 +231,7 @@ test("authorization: consent page, CSP allows the redirect origin, password requ
|
|
|
231
231
|
c = await s.consent(q, "", "deny");
|
|
232
232
|
assert.equal(c.post.status, 302);
|
|
233
233
|
const den = new URL(c.post.headers.get("location")!);
|
|
234
|
-
assert.deepEqual([den.origin + den.pathname, den.searchParams.get("error"), den.searchParams.get("state"), den.searchParams.get("iss")], [CLAUDE_CB, "access_denied", "xyz", BASE]);
|
|
234
|
+
assert.deepEqual([den.origin + den.pathname, den.searchParams.get("error"), den.searchParams.get("error_description"), den.searchParams.get("state"), den.searchParams.get("iss")], [CLAUDE_CB, "access_denied", "the agent owner denied access", "xyz", BASE]);
|
|
235
235
|
// CSRF: a POST without the cookie is refused
|
|
236
236
|
const forged = await s.call("POST", "/oauth/authorize", { form: { ...q, csrf: "x".repeat(24), password: PASSWORD, decision: "approve" }, headers: { origin: BASE } });
|
|
237
237
|
assert.equal(forged.status, 403);
|
|
@@ -271,7 +271,7 @@ test("human approval: pending, slow_down, page approve with the password, single
|
|
|
271
271
|
assert.match(w.pairing.instructions, /never approve on your own[\s\S]*give them this link/);
|
|
272
272
|
|
|
273
273
|
let p = await s.poll(d.device_code);
|
|
274
|
-
assert.deepEqual([p.status, p.data.error], [400, "authorization_pending"]);
|
|
274
|
+
assert.deepEqual([p.status, p.data.error, p.data.error_description], [400, "authorization_pending", "waiting for the agent owner to approve"]);
|
|
275
275
|
p = await s.poll(d.device_code);
|
|
276
276
|
assert.deepEqual([p.status, p.data.error], [400, "slow_down"], "polled again at once");
|
|
277
277
|
assert.match(p.data.error_description, /at least 10 seconds/);
|
|
@@ -390,7 +390,8 @@ test("human mode: the owner token approves and denies; the device poll returns t
|
|
|
390
390
|
assert.equal(denied.data.decision, "denied");
|
|
391
391
|
const denyRow = s.DB.db.prepare("SELECT decided_by FROM device_requests WHERE user_code = ?").get(deny.user_code.replace("-", "")) as { decided_by: string };
|
|
392
392
|
assert.equal(denyRow.decided_by, "owner");
|
|
393
|
-
|
|
393
|
+
const deniedPoll = await s.poll(deny.device_code);
|
|
394
|
+
assert.deepEqual([deniedPoll.data.error, deniedPoll.data.error_description], ["access_denied", "the agent owner denied this request"]);
|
|
394
395
|
});
|
|
395
396
|
|
|
396
397
|
test("a pairing label comes from the client name, a second one is suffixed, and inbox from uses it", async (t) => {
|