evOo-local/apps/api/src/webhook.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

85 lines
2.5 KiB
TypeScript

import { hasValidBearerToken } from "@rakazo/core";
import type { Hono } from "hono";
import { mountGithubWebhookRoute } from "./github-webhook.js";
import { readBoundedBody } from "./http-body.js";
import {
deliverWebhookEvent,
formatWebhookPrompt,
loadWebhookTarget,
parseWebhookPayload,
WEBHOOK_MAX_BODY_BYTES,
type WebhookDeps,
} from "./webhook-inbound.js";
export {
formatGithubEventPrompt,
githubEventName,
githubWebhookPath,
hasValidGithubSignature,
} from "./github-webhook.js";
export {
formatUntrustedDeliveryPayload,
formatWebhookPrompt,
WEBHOOK_MAX_BODY_BYTES,
WEBHOOK_SECRET_KIND,
type WebhookDeps,
type WebhookEvents,
type WebhookTarget,
} from "./webhook-inbound.js";
export function webhookPath(botId: string): string {
return `/api/v1/bots/${botId}/webhook`;
}
export function mountWebhookHttpRoutes(app: Hono, deps: WebhookDeps) {
app.post("/api/v1/bots/:botId/webhook", async (c) => {
const unauthorized = () => c.json({ error: "Unauthorized" }, 401);
// Reject oversized bodies before target lookup so size limits do not reveal active bots.
const raw = await readBoundedBody(c.req.raw, WEBHOOK_MAX_BODY_BYTES);
if (raw === null) {
return c.json({ error: "Payload too large" }, 413);
}
const target = await loadWebhookTarget(deps, c.req.param("botId"));
// Same 401 for missing bot, missing secret, and bad bearer so bot ids are not enumerable.
if (!target || !hasValidBearerToken(c.req.header("authorization"), target.expected)) {
return unauthorized();
}
const payload = parseWebhookPayload(raw, c.req.header("content-type"));
const eventPrompt = formatWebhookPrompt(payload);
const webhookRoutines = await deps.prisma.routine.findMany({
where: {
botId: target.bot.id,
spaceId: target.bot.spaceId,
active: true,
webhookEnabled: true,
},
select: { id: true, name: true, prompt: true },
orderBy: { updatedAt: "desc" },
take: 5,
});
const idempotencyKey =
c.req.header("idempotency-key")?.trim() ||
c.req.header("x-idempotency-key")?.trim() ||
(typeof payload.id === "string" ? payload.id.trim() : "") ||
(typeof payload.event_id === "string" ? payload.event_id.trim() : "") ||
undefined;
return c.json(
await deliverWebhookEvent(deps, target, {
prompt: eventPrompt,
routines: webhookRoutines,
source: "webhook",
idempotencyKey,
}),
);
});
mountGithubWebhookRoute(app, deps);
}