import { Type } from '@sinclair/typebox'; import type { FastifyInstance } from 'fastify'; import { eq } from 'drizzle-orm'; import { nodes } from '@source/database'; import { AppError } from '../../lib/errors.js'; const HeartbeatSchema = { body: Type.Object({ active_servers: Type.Number({ minimum: 0 }), total_servers: Type.Number({ minimum: 0 }), version: Type.String(), }), }; function extractBearerToken(authHeader?: string): string | null { if (!authHeader) return null; const [scheme, token] = authHeader.split(' '); if (!scheme || !token || scheme.toLowerCase() !== 'bearer') return null; return token; } export default async function daemonNodeRoutes(app: FastifyInstance) { // POST /api/nodes/heartbeat app.post('/heartbeat', { schema: HeartbeatSchema }, async (request) => { const token = extractBearerToken( typeof request.headers.authorization === 'string' ? request.headers.authorization : undefined, ); if (!token) { throw AppError.unauthorized('Missing daemon bearer token', 'DAEMON_AUTH_MISSING'); } const node = await app.db.query.nodes.findFirst({ where: eq(nodes.daemonToken, token), columns: { id: true }, }); if (!node) { throw AppError.unauthorized('Invalid daemon token', 'DAEMON_AUTH_INVALID'); } const now = new Date(); await app.db .update(nodes) .set({ isOnline: true, lastHeartbeat: now, updatedAt: now, }) .where(eq(nodes.id, node.id)); const body = request.body as { active_servers: number; total_servers: number; version: string; }; return { success: true, nodeId: node.id, activeServers: body.active_servers, totalServers: body.total_servers, version: body.version, }; }); }