syntaxai/tdd.md · main · src / d21_handlers_api_agents.ts
// c21 — handlers: agent-facing JSON API. Manual judge trigger
// (admin-token-gated) and the self-service visibility toggle (agent
// pushes their own Forgejo token to flip public|limited|private).
// Extracted from c21_app.ts per the SAMA Atomic rule. The push-driven
// judge entry point lives in c21_handlers_webhook — different auth
// model (HMAC), different concept.
import { judge } from "./c14_judge.ts";
import { timingSafeEqual } from "./b32_session.ts";
import {
FORGEJO_URL,
adminApiHeaders,
} from "./c14_forgejo.ts";
export const judgeApiHandler = async (
req: Request & { params: { owner: string; repo: string } },
): Promise<Response> => {
if (req.method !== "POST") {
return new Response("method not allowed; POST to trigger a judge run", { status: 405 });
}
// Manual triggers require the admin token. Push-driven runs come
// through /api/forgejo/webhook with HMAC signature verification.
const adminToken = process.env.FORGEJO_ADMIN_TOKEN;
const provided = req.headers.get("authorization")?.replace(/^[Bb]earer\s+/, "") ?? "";
if (!adminToken || !timingSafeEqual(provided, adminToken)) {
return new Response(
"unauthorized — POST with `Authorization: Bearer <admin-token>`",
{ status: 401 },
);
}
try {
const verdict = await judge(req.params.owner, req.params.repo);
return Response.json(verdict);
} catch (err) {
return Response.json({ error: (err as Error).message }, { status: 500 });
}
};
// Self-service visibility toggle. Agent posts their push token in
// Authorization, picks "public" | "limited" | "private". We verify
// the token actually belongs to :name by hitting Forgejo's /user
// endpoint with it, then PATCH the user via the admin token.
export const agentVisibilityHandler = async (
req: Request & { params: { name: string } },
): Promise<Response> => {
if (req.method !== "POST") return new Response("POST only", { status: 405 });
const name = req.params.name;
const provided = req.headers.get("authorization")?.replace(/^[Bb]earer\s+/, "") ?? "";
if (!provided) return Response.json({ error: "missing bearer token" }, { status: 401 });
// Verify the token belongs to :name (or is the admin token).
const adminToken = process.env.FORGEJO_ADMIN_TOKEN ?? "";
let allowed = !!adminToken && timingSafeEqual(provided, adminToken);
if (!allowed) {
const meRes = await fetch(`${FORGEJO_URL}/api/v1/user`, {
headers: { Authorization: `token ${provided}` },
});
if (meRes.ok) {
const me = (await meRes.json()) as { login?: string };
allowed = me.login === name;
}
}
if (!allowed) return Response.json({ error: "token does not match agent" }, { status: 403 });
let body: { visibility?: string };
try {
body = (await req.json()) as { visibility?: string };
} catch {
return Response.json({ error: "invalid json" }, { status: 400 });
}
const visibility = body.visibility;
if (visibility !== "public" && visibility !== "limited" && visibility !== "private") {
return Response.json(
{ error: "visibility must be one of public|limited|private" },
{ status: 400 },
);
}
const patchRes = await fetch(
`${FORGEJO_URL}/api/v1/admin/users/${encodeURIComponent(name)}`,
{
method: "PATCH",
headers: { ...adminApiHeaders(), "Content-Type": "application/json" },
body: JSON.stringify({ visibility, source_id: 0, login_name: name }),
},
);
if (!patchRes.ok) {
const text = await patchRes.text();
return Response.json(
{ error: `forgejo PATCH failed: ${patchRes.status} ${text}` },
{ status: 502 },
);
}
return Response.json({ name, visibility });
};