import type { FastifyInstance } from 'fastify'; import { CreateSubscriptionSchema, UpdateSubscriptionSchema, WEBHOOK_EVENT_TYPES, } from './types.js'; import * as repo from './repository.js'; import { dispatchEvent } from './dispatcher.js'; import { extractAuth } from '../../lib/auth.js'; import { BadRequestError } from '@bytelyst/errors'; import { PRODUCT_ID } from '../../lib/product-config.js'; export async function webhookRoutes(app: FastifyInstance) { // Event types — must be before :id param route app.get('/webhooks/event-types', async (_req, reply) => { return reply.send({ eventTypes: WEBHOOK_EVENT_TYPES.map(type => ({ type, category: type.split('.')[0], action: type.split('.')[1], })), }); }); // Test — must be before :id param route app.post('/webhooks/test', async (req, reply) => { const auth = await extractAuth(req); const body = req.body as { subscriptionId?: string; eventType?: string }; if (!body.subscriptionId) { throw new BadRequestError('subscriptionId is required'); } await repo.getSubscription(body.subscriptionId, auth.sub); const eventType = (body.eventType || 'timer.fired') as (typeof WEBHOOK_EVENT_TYPES)[number]; if (!WEBHOOK_EVENT_TYPES.includes(eventType)) { throw new BadRequestError(`Invalid event type: ${eventType}`); } const results = await dispatchEvent( auth.sub, PRODUCT_ID, eventType, { test: true, message: 'This is a test webhook event from ChronoMind', timestamp: new Date().toISOString(), }, req.log ); return reply.send({ results }); }); // List subscriptions app.get('/webhooks', async req => { const auth = await extractAuth(req); return repo.listSubscriptions(auth.sub, PRODUCT_ID); }); // Get subscription app.get('/webhooks/:id', async req => { const auth = await extractAuth(req); const { id } = req.params as { id: string }; return repo.getSubscription(id, auth.sub); }); // Create subscription app.post('/webhooks', async (req, reply) => { const auth = await extractAuth(req); const parsed = CreateSubscriptionSchema.safeParse(req.body); if (!parsed.success) { throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); } const id = crypto.randomUUID(); const sub = await repo.createSubscription(id, auth.sub, PRODUCT_ID, parsed.data); return reply.status(201).send(sub); }); // Update subscription app.put('/webhooks/:id', async req => { const auth = await extractAuth(req); const { id } = req.params as { id: string }; const parsed = UpdateSubscriptionSchema.safeParse(req.body); if (!parsed.success) { throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); } return repo.updateSubscription(id, auth.sub, parsed.data); }); // Delete subscription app.delete('/webhooks/:id', async (req, reply) => { const auth = await extractAuth(req); const { id } = req.params as { id: string }; await repo.deleteSubscription(id, auth.sub); return reply.status(204).send(); }); // List events for subscription app.get('/webhooks/:id/events', async req => { const auth = await extractAuth(req); const { id } = req.params as { id: string }; // Verify ownership await repo.getSubscription(id, auth.sub); const limit = parseInt((req.query as Record).limit || '50', 10); return repo.listEvents(id, Math.min(limit, 100)); }); }