import Fastify, { type FastifyInstance } from "fastify"; import { ZodError } from "zod"; import type { AppConfig } from "./config.js"; import { createPool, DatabaseNotConfiguredError } from "./db/pool.js"; import { ToolExecutionInputError } from "./mcp/tool-runtime.js"; import { AgentsRepository } from "./repositories/agents.js"; import { registerAgentRoutes } from "./routes/agents.js"; import { registerHealthRoutes } from "./routes/health.js"; import { registerMcpRoutes } from "./routes/mcp.js"; import { registerPublicRoutes } from "./routes/public.js"; import { registerToolRoutes } from "./routes/tools.js"; import { ForbiddenError } from "./security/authorization.js"; import { UnauthorizedError } from "./security/bearer.js"; import { InternalAuthNotConfiguredError } from "./security/internal.js"; import { TaskerAdapterError, TaskerAdapterNotConfiguredError, TaskerAdapterUnavailableError, TaskerClient } from "./tasker/client.js"; export async function buildApp(config: AppConfig): Promise { const pool = createPool(config); const agentsRepository = pool ? new AgentsRepository(pool) : null; const taskerClient = new TaskerClient({ baseUrl: config.NODEDC_TASKER_INTERNAL_URL, internalAccessToken: config.NODEDC_INTERNAL_ACCESS_TOKEN, }); const app = Fastify({ bodyLimit: 10 * 1024 * 1024, logger: { level: config.LOG_LEVEL, }, }); app.addHook("onClose", async () => { await pool?.end(); }); app.setErrorHandler((error, _request, reply) => { if (error instanceof ZodError) { void reply.status(400).send({ ok: false, error: "validation_error", details: error.issues, }); return; } if (error instanceof DatabaseNotConfiguredError) { void reply.status(503).send({ ok: false, error: "database_not_configured", message: "DATABASE_URL is required for Agent Gateway persistence endpoints.", }); return; } if (error instanceof UnauthorizedError) { void reply.status(401).send({ ok: false, error: "unauthorized", message: error.message, }); return; } if (error instanceof InternalAuthNotConfiguredError) { void reply.status(503).send({ ok: false, error: "internal_auth_not_configured", message: error.message, }); return; } if (error instanceof ForbiddenError) { void reply.status(403).send({ ok: false, error: "forbidden", message: error.message, }); return; } if (error instanceof ToolExecutionInputError) { void reply.status(error.httpStatus).send({ ok: false, error: error.code, message: error.message, details: error.details, }); return; } if (error instanceof TaskerAdapterNotConfiguredError) { void reply.status(503).send({ ok: false, error: "tasker_adapter_not_configured", message: error.message, }); return; } if (error instanceof TaskerAdapterError) { void reply.status(error.statusCode).send({ ok: false, error: "tasker_adapter_error", message: error.message, tasker_status: error.statusCode, tasker_payload: error.payload, }); return; } if (error instanceof TaskerAdapterUnavailableError) { void reply.status(502).send({ ok: false, error: "tasker_adapter_unavailable", message: error.message, }); return; } app.log.error(error); void reply.status(500).send({ ok: false, error: "internal_server_error", message: "Agent Gateway request failed.", }); }); await registerPublicRoutes(app); await registerHealthRoutes(app, config, pool); await registerAgentRoutes(app, { agentsRepository, publicUrl: config.NODEDC_AGENT_GATEWAY_PUBLIC_URL, internalAccessToken: config.NODEDC_AGENT_GATEWAY_INTERNAL_TOKEN, }); await registerToolRoutes(app, { agentsRepository, taskerClient }); await registerMcpRoutes(app, { agentsRepository, taskerClient }); return app; }