diff --git a/env.ts b/env.ts index 2f4a8c0..3b58417 100644 --- a/env.ts +++ b/env.ts @@ -106,11 +106,6 @@ export const env = createEnv({ // Jira Webhook JIRA_WEBHOOK_SECRET: z.string().min(1).optional(), - // Forge app — shared secret required when using the Jira Forge app - // instead of (or alongside) the classic webhook. Both /jira/dispatch - // and /runs/:key are gated by an X-Forge-Secret header matching this. - FORGE_SHARED_SECRET: z.string().min(1).optional(), - // Redis (run registry) AI_WORKFLOW_KV_REST_API_URL: z.string().url(), AI_WORKFLOW_KV_REST_API_TOKEN: z.string().min(1), diff --git a/src/routes/jira/dispatch.post.ts b/src/routes/jira/dispatch.post.ts deleted file mode 100644 index f357f2a..0000000 --- a/src/routes/jira/dispatch.post.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { timingSafeEqual } from "node:crypto"; -import { defineEventHandler, readBody, getHeader, createError } from "h3"; -import { env } from "../../../env.js"; -import { createAdapters } from "../../lib/adapters.js"; -import { dispatchTicket } from "../../lib/dispatch.js"; -import { logger } from "../../lib/logger.js"; - -/** - * Forge app dispatch endpoint — invoked by the Jira Forge app's - * `avi:jira:updated:issue` trigger when a ticket enters the AI column. - * - * Auth: X-Forge-Secret header matching FORGE_SHARED_SECRET. The Forge runtime - * adds the header in `src/index.js` of ai-workflow-jira-app; the same secret - * value is stored in Forge Storage and in this backend's env. - * - * Differs from /webhooks/jira: the Forge trigger has already filtered by - * status, so we skip the column / live-state checks and dispatch directly. - */ -export default defineEventHandler(async (event) => { - if (!env.FORGE_SHARED_SECRET) { - throw createError({ statusCode: 503, statusMessage: "FORGE_SHARED_SECRET not configured" }); - } - - verifyForgeSecret(getHeader(event, "x-forge-secret")); - - const body = (await readBody(event)) as { - issueKey?: string; - cloudId?: string; - source?: string; - }; - - if (!body?.issueKey) { - throw createError({ statusCode: 400, statusMessage: "Missing issueKey" }); - } - - logger.info( - { ticketKey: body.issueKey, cloudId: body.cloudId, source: body.source ?? "forge" }, - "forge_dispatch_received", - ); - - const adapters = createAdapters(); - const result = await dispatchTicket(body.issueKey, adapters, env.MAX_CONCURRENT_AGENTS); - - logger.info( - { ticketKey: body.issueKey, started: result.started, reason: result.reason, runId: result.runId }, - "forge_dispatch_result", - ); - - return { - status: result.started ? "dispatched" : "skipped", - ticketKey: body.issueKey, - reason: result.reason, - runId: result.runId, - }; -}); - -function verifyForgeSecret(received: string | undefined): void { - if (!received) { - throw createError({ statusCode: 401, statusMessage: "Missing X-Forge-Secret header" }); - } - const expected = env.FORGE_SHARED_SECRET!; - const a = Buffer.from(received); - const b = Buffer.from(expected); - if (a.length !== b.length || !timingSafeEqual(a, b)) { - throw createError({ statusCode: 401, statusMessage: "Invalid forge secret" }); - } -} diff --git a/src/routes/runs/[key].get.ts b/src/routes/runs/[key].get.ts deleted file mode 100644 index 8d0bccb..0000000 --- a/src/routes/runs/[key].get.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { timingSafeEqual } from "node:crypto"; -import { defineEventHandler, getHeader, getRouterParam, createError } from "h3"; -import { env } from "../../../env.js"; -import { createAdapters } from "../../lib/adapters.js"; -import { logger } from "../../lib/logger.js"; - -/** - * Returns the current run state for a ticket, used by the Forge issue panel. - * - * Shape is intentionally narrow — only what the panel renders. Phase / PR URL - * are best-effort: the run registry tracks runId + sandboxId only, so richer - * state would require either expanding the registry or reading from VCS. - */ -export default defineEventHandler(async (event) => { - if (!env.FORGE_SHARED_SECRET) { - throw createError({ statusCode: 503, statusMessage: "FORGE_SHARED_SECRET not configured" }); - } - - verifyForgeSecret(getHeader(event, "x-forge-secret")); - - const issueKey = getRouterParam(event, "key"); - if (!issueKey) { - throw createError({ statusCode: 400, statusMessage: "Missing issueKey" }); - } - - const adapters = createAdapters(); - const runId = await adapters.runRegistry.getRunId(issueKey); - - if (!runId) { - const failed = await adapters.runRegistry.isTicketFailed(issueKey).catch(() => false); - if (failed) { - return { status: "failed", issueKey }; - } - setResponseStatus(event, 404); - return { status: "idle", issueKey }; - } - - const sandboxId = await adapters.runRegistry.getSandboxId(issueKey).catch(() => null); - - // Branch convention is set in agent.ts: `blazebot/{ticketKey-lowercase}`. - // Mirror it here so the panel can show a PR link without expanding the - // run registry schema. - let prUrl: string | null = null; - try { - const branchName = `blazebot/${issueKey.toLowerCase()}`; - const pr = await adapters.vcs.findPR(branchName); - prUrl = pr?.url ?? null; - } catch (err) { - logger.debug({ issueKey, error: (err as Error).message }, "forge_runs_pr_lookup_failed"); - } - - return { - status: "active", - issueKey, - runId, - sandboxId, - prUrl, - }; -}); - -function verifyForgeSecret(received: string | undefined): void { - if (!received) { - throw createError({ statusCode: 401, statusMessage: "Missing X-Forge-Secret header" }); - } - const expected = env.FORGE_SHARED_SECRET!; - const a = Buffer.from(received); - const b = Buffer.from(expected); - if (a.length !== b.length || !timingSafeEqual(a, b)) { - throw createError({ statusCode: 401, statusMessage: "Invalid forge secret" }); - } -} - -function setResponseStatus(event: Parameters[0], status: number): void { - // h3 has setResponseStatus but importing it here to keep the file's imports - // explicit and self-contained. - (event.node?.res ?? (event as any).res).statusCode = status; -}