learning_ai_clock/backend/src/modules/webhooks/routes.ts
saravanakumardb1 9897d2cd09 docs(todos): standardize all TODOs with running numbers and delegatable instructions
Replace ad-hoc AGENTIC-N comments with standardized TODO-NNN format across
the entire codebase. Each TODO has:
  - Running number (TODO-001 through TODO-011)
  - Priority level (high/medium/low)
  - Phase reference (0, A.1, A.4, B, cleanup)
  - Clear step-by-step instructions an AI agent can follow

TODO index:
  TODO-001: Kill switch maintenance banner (providers.tsx)
  TODO-002: Feedback button in settings page
  TODO-003: Accessibility focus trap for modals
  TODO-004: Clone routine template instead of mutating in-place
  TODO-005: Wire real LLM enrichment for context messages
  TODO-006: Centralize backend URL configuration
  TODO-007: MCP tool integration tests (common-plat)
  TODO-008: Wire trackEvent() calls into routes + components
  TODO-009: Unit tests for AI context generation
  TODO-010: Import PRODUCT_ID from product-config (5 route files)
  TODO-011: Wire error boundary to telemetry

Added consolidated TODO Index table at top of AGENTIC_AI_ROADMAP.md
for agent scanning. 219 backend tests pass, no code changes.
2026-04-01 01:05:25 -07:00

115 lines
3.7 KiB
TypeScript

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';
// TODO-010: Import PRODUCT_ID from product-config instead of hardcoding
// Priority: low | Phase: cleanup
// Replace this line with: import { PRODUCT_ID } from '../../lib/product-config.js';
const PRODUCT_ID = 'chronomind';
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<string, string>).limit || '50', 10);
return repo.listEvents(id, Math.min(limit, 100));
});
}