import Fastify from 'fastify'; import { config } from './lib/config.js'; import { initializeContainers } from './lib/cosmos-init.js'; import { extractAuth, AuthError, requireAdmin } from './lib/auth.js'; import { generateCsrfToken, validateCsrfToken, getSessionId } from './lib/csrf.js'; import { serviceRoutes } from './modules/services/routes.js'; import { deploymentRoutes } from './modules/deployments/routes.js'; import { healthRoutes } from './modules/health/routes.js'; import { auditRoutes } from './modules/audit/routes.js'; import { backupRoutes } from './modules/backup/routes.js'; import { systemRoutes } from './modules/system/routes.js'; import { envRoutes } from './modules/env/routes.js'; import { azureConfigRoutes } from './modules/azure-config/routes.js'; import { codeQualityRoutes } from './modules/code-quality/routes.js'; import { cosmosConfigRoutes } from './modules/cosmos-config/routes.js'; import { hermesOpsRoutes } from './modules/hermes-ops/routes.js'; // import sse from 'fastify-sse-v2'; import rateLimit from '@fastify/rate-limit'; import swagger from '@fastify/swagger'; import swaggerUi from '@fastify/swagger-ui'; const fastify = Fastify({ logger: true, }); // Register SSE plugin // TODO: fastify-sse-v2 has compatibility issues with Fastify 5 // await fastify.register(sse); // Register rate limiting await fastify.register(rateLimit, { max: 100, // 100 requests per window timeWindow: '1 minute', errorResponseBuilder: (request, context) => ({ code: 429, error: 'Too many requests', retryAfter: context.ttl, }), skipOnError: false, }); // Register Swagger await fastify.register(swagger, { openapi: { openapi: '3.0.0', info: { title: 'ByteLyst DevOps API', description: 'API for deployment orchestration and service monitoring', version: '0.1.0', }, servers: [ { url: 'http://localhost:4004', description: 'Development server', }, ], components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }, }, }, }, }); await fastify.register(swaggerUi, { routePrefix: '/docs', uiConfig: { docExpansion: 'list', deepLinking: false, }, staticCSP: true, transformStaticCSP: (header) => header, transformSpecification: (swaggerObject, request, reply) => { return swaggerObject; }, transformSpecificationClone: true, }); // Auth hook - extract user info from JWT fastify.addHook('onRequest', async (request, reply) => { const auth = await extractAuth(request); if (auth) { (request as any).authUserId = auth.userId; (request as any).authRole = auth.role; (request as any).authEmail = auth.email; (request as any).authProductId = auth.productId; } }); // CSRF protection hook - validate CSRF tokens for state-changing requests fastify.addHook('onRequest', async (request, reply) => { const method = request.method; const stateChangingMethods = ['POST', 'PUT', 'DELETE', 'PATCH']; if (!stateChangingMethods.includes(method)) { return; } const sessionId = getSessionId(request); if (!sessionId) { return reply.code(401).send({ error: 'Unauthorized' }); } const csrfToken = request.headers['x-csrf-token'] as string; if (!csrfToken) { return reply.code(403).send({ error: 'CSRF token missing' }); } if (!validateCsrfToken(csrfToken, sessionId)) { return reply.code(403).send({ error: 'Invalid CSRF token' }); } }); // Performance monitoring hook fastify.addHook('onRequest', async (request, reply) => { (request as any).startTime = Date.now(); }); fastify.addHook('onResponse', async (request, reply) => { const startTime = (request as any).startTime; if (startTime) { const duration = Date.now() - startTime; const route = request.routeOptions.url || request.url; const method = request.method; fastify.log.info({ method, route, statusCode: reply.statusCode, duration, performance: 'slow', }, `${method} ${route} - ${reply.statusCode} (${duration}ms)`); // Alert on slow responses (> 1 second) if (duration > 1000) { fastify.log.warn({ method, route, statusCode: reply.statusCode, duration, alert: 'slow-response', }, `Slow response detected: ${method} ${route} took ${duration}ms`); } } }); // Error handler for AuthError fastify.setErrorHandler((error, request, reply) => { if (error instanceof AuthError) { return reply.code(error.statusCode).send({ error: error.message }); } // Default error handling reply.code(500).send({ error: 'Internal server error' }); }); // CORS - more secure configuration fastify.addHook('onSend', async (request, reply) => { const allowedOrigins = [ 'http://localhost:3000', 'http://localhost:3001', 'https://devops.bytelyst.com', ]; const origin = request.headers.origin; if (origin && allowedOrigins.includes(origin)) { reply.header('Access-Control-Allow-Origin', origin); } reply.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS'); reply.header('Access-Control-Allow-Headers', 'Content-Type,Authorization'); reply.header('Access-Control-Allow-Credentials', 'true'); reply.header('Access-Control-Max-Age', '86400'); // 24 hours // Security headers reply.header('X-Content-Type-Options', 'nosniff'); reply.header('X-Frame-Options', 'DENY'); reply.header('X-XSS-Protection', '1; mode=block'); reply.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); reply.header('Referrer-Policy', 'strict-origin-when-cross-origin'); }); // Handle OPTIONS preflight requests fastify.options('*', async (request, reply) => { reply.code(204).send(); }); // Health check fastify.get('/health', async () => ({ status: 'ok', service: 'devops-backend' })); // Register standalone routes with /api prefix await fastify.register(async function (fastify) { // Performance metrics endpoint (admin only) - DEPRECATED: Use /api/system/metrics instead fastify.get('/metrics', { preHandler: async (req) => requireAdmin(req), }, async (request, reply) => { try { const { getSystemMetrics } = await import('./modules/system/repository.js'); const metrics = await getSystemMetrics(); return reply.send(metrics); } catch (error) { fastify.log.error(error, 'Failed to get metrics'); return reply.code(500).send({ error: 'Failed to get metrics' }); } }); // CSRF token endpoint fastify.get('/csrf-token', async (request, reply) => { const sessionId = getSessionId(request); if (!sessionId) { return reply.code(401).send({ error: 'Unauthorized' }); } const token = generateCsrfToken(sessionId); return { csrfToken: token }; }); // Seed default services fastify.post('/seed', async (request, reply) => { const { createService } = await import('./modules/services/repository.js'); const defaultServices = [ { id: 'trading', name: 'Investment Trading', scriptPath: '../deploy-invttrdg.sh', healthUrl: 'https://api.bytelyst.com/invttrdg/health', repoPath: '../learning_ai_invt_trdg', }, { id: 'notes', name: 'Agentic Notes', scriptPath: '../deploy-notes.sh', healthUrl: 'https://api.notelett.app/health', repoPath: '../learning_ai_notes', }, { id: 'clock', name: 'AI Clock', scriptPath: '../deploy-clock.sh', healthUrl: 'https://api.clock.bytelyst.com/health', repoPath: '../learning_ai_clock', }, ]; for (const serviceData of defaultServices) { try { await createService(serviceData); } catch (error) { fastify.log.info({ serviceId: serviceData.id }, 'Service might already exist'); } } return reply.send({ message: 'Seeded default services' }); }); }, { prefix: '/api' }); // Register modular routes with /api prefix await fastify.register(serviceRoutes, { prefix: '/api' }); await fastify.register(deploymentRoutes, { prefix: '/api' }); await fastify.register(healthRoutes, { prefix: '/api' }); await fastify.register(auditRoutes, { prefix: '/api' }); await fastify.register(backupRoutes, { prefix: '/api' }); await fastify.register(systemRoutes, { prefix: '/api' }); await fastify.register(envRoutes, { prefix: '/api' }); await fastify.register(azureConfigRoutes, { prefix: '/api' }); await fastify.register(codeQualityRoutes, { prefix: '/api' }); await fastify.register(cosmosConfigRoutes, { prefix: '/api' }); await fastify.register(hermesOpsRoutes, { prefix: '/api' }); // Start server async function start() { try { // Try to initialize Cosmos containers, but allow server to start even if it fails try { await initializeContainers(); fastify.log.info('Cosmos containers initialized successfully'); } catch (err) { fastify.log.warn(err, 'Failed to initialize Cosmos containers (server will start anyway)'); } await fastify.listen({ port: parseInt(config.PORT), host: '0.0.0.0' }); fastify.log.info({ port: config.PORT }, 'DevOps backend listening'); } catch (err) { fastify.log.error(err); process.exit(1); } } start();