import type { FastifyPluginAsync } from 'fastify'; import { z } from 'zod'; import { recordAudit } from '../audit.js'; import { config } from '../config.js'; import { pool } from '../db/pool.js'; import { bunnyMediaStatus, isBunnyWebhookConfigured, verifyBunnyWebhookSignature } from '../providers/bunny.js'; const webhookSchema = z.object({ VideoGuid: z.string().uuid(), VideoLibraryId: z.coerce.number().int().positive(), Status: z.coerce.number().int().nonnegative(), Length: z.coerce.number().nonnegative().optional(), }); export const bunnyWebhookRoutes: FastifyPluginAsync = async (app) => { app.post('/bunny', async (request, reply) => { if (!isBunnyWebhookConfigured()) return reply.code(503).send({ error: 'Os webhooks do Bunny não estão configurados.' }); const rawBody = (request as typeof request & { rawBody?: Buffer }).rawBody; const signatureHeader = request.headers.signature || request.headers['x-bunny-signature']; const signature = Array.isArray(signatureHeader) ? signatureHeader[0] : signatureHeader; if (!rawBody || !verifyBunnyWebhookSignature(rawBody, signature)) { return reply.code(401).send({ error: 'Assinatura do webhook do Bunny inválida.' }); } const input = webhookSchema.parse(request.body); if (input.VideoLibraryId !== config.BUNNY_STREAM_LIBRARY_ID) { return reply.code(400).send({ error: 'Biblioteca de vídeos Bunny inesperada.' }); } const status = bunnyMediaStatus(input.Status); await pool.query( `update lesson_media set status = $2::media_status, duration_seconds = coalesce($3, duration_seconds), metadata = metadata || jsonb_build_object('bunnyStatus', $4, 'bunnyWebhookAt', now()) where provider = 'bunny' and external_id = $1`, [input.VideoGuid, status, input.Length ? Math.round(input.Length) : null, input.Status], ); await recordAudit({ action: 'media.bunny.webhook_received', subjectType: 'video', subjectId: input.VideoGuid, metadata: { status, providerStatus: input.Status }, ipAddress: request.ip }); return reply.code(204).send(); }); };