"use strict"; /** * ABE API Server * Express + socket.io on port 3001. * Manages exploration sessions and serves REST + WebSocket API. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createApp = createApp; exports.createServer = createServer; const express_1 = __importDefault(require("express")); const cors_1 = __importDefault(require("cors")); const http_1 = __importDefault(require("http")); const express_rate_limit_1 = __importDefault(require("express-rate-limit")); const socket_io_1 = require("socket.io"); const sessions_1 = require("./routes/sessions"); const anomalies_1 = require("./routes/anomalies"); const config_1 = require("./routes/config"); const schedules_1 = require("./routes/schedules"); const visual_1 = require("./routes/visual"); const SessionStore_1 = require("./SessionStore"); const auth_1 = require("./middleware/auth"); const logger_1 = require("./logger"); const AIEnrichmentService_1 = require("./enrichment/AIEnrichmentService"); const PORT = process.env['ABE_PORT'] ? parseInt(process.env['ABE_PORT'], 10) : process.env['PORT'] ? parseInt(process.env['PORT'], 10) : 3001; function createApp(store, dbCheck, scheduleRepo, scheduler, visualRepo, enrichmentService) { const corsOrigin = process.env['ABE_CORS_ORIGIN'] ?? 'http://localhost:5173'; const app = (0, express_1.default)(); app.use((0, cors_1.default)({ origin: corsOrigin })); app.use(express_1.default.json()); // Health endpoints — no auth required app.get('/health', (_req, res) => { const uptime = Math.floor(process.uptime()); res.json({ status: 'ok', version: '0.1.0', uptime_seconds: uptime }); }); app.get('/ready', (_req, res) => { const stats = store.getStats(); if (dbCheck && !dbCheck()) { res.status(503).json({ status: 'not_ready', db: 'disconnected', active_sessions: stats.runningSessions }); return; } res.json({ status: 'ready', db: 'connected', active_sessions: stats.runningSessions }); }); // Apply API key auth to all /api/* routes app.use('/api', auth_1.apiKeyAuth); // Global rate limit: 200 req/min const globalLimiter = (0, express_rate_limit_1.default)({ windowMs: 60 * 1000, max: 200, standardHeaders: true, legacyHeaders: false, }); app.use('/api', globalLimiter); // POST /api/sessions rate limit: 20/hour const sessionCreateLimiter = (0, express_rate_limit_1.default)({ windowMs: 60 * 60 * 1000, max: 20, standardHeaders: true, legacyHeaders: false, }); app.post('/api/sessions', sessionCreateLimiter); app.get('/api/stats', (_req, res) => { res.json(store.getStats()); }); app.use('/api/sessions', (0, sessions_1.createSessionRouter)(store)); app.use('/api/anomalies', (0, anomalies_1.createAnomalyRouter)(store, enrichmentService)); app.use('/api/config', (0, config_1.createConfigRouter)()); if (scheduleRepo && scheduler) { app.use('/api/schedules', (0, schedules_1.createScheduleRouter)(scheduleRepo, scheduler)); } if (visualRepo) { app.use('/api/visual', (0, visual_1.createVisualRouter)(visualRepo)); } // Global error handler app.use((err, _req, res, _next) => { const isDev = process.env['NODE_ENV'] !== 'production'; const message = isDev && err instanceof Error ? err.message : 'Internal server error'; res.status(500).json({ error: message, code: 'INTERNAL_ERROR', timestamp: Date.now(), }); }); return app; } function createServer(store, dbCheck, scheduleRepo, scheduler, visualRepo) { const corsOrigin = process.env['ABE_CORS_ORIGIN'] ?? 'http://localhost:5173'; // Deferred emitter: AIEnrichmentService is created before io, using a closure let ioEmit = () => undefined; const enrichmentService = new AIEnrichmentService_1.AIEnrichmentService((event, payload) => ioEmit(event, payload)); const app = createApp(store, dbCheck, scheduleRepo, scheduler, visualRepo, enrichmentService); const httpServer = http_1.default.createServer(app); const io = new socket_io_1.Server(httpServer, { cors: { origin: corsOrigin }, }); // Now wire the real io emitter ioEmit = (event, payload) => io.emit(event, payload); io.on('connection', (socket) => { socket.on('session:stop', (data) => { store.stopSession(data.sessionId); }); }); store.setEmitter((event, payload) => { io.emit(event, payload); // Auto-enrich high/critical anomalies if (event === 'anomaly:detected') { const p = payload; if (p?.anomalyId) { const anomaly = store.getAnomaly(p.anomalyId); if (anomaly && enrichmentService.shouldAutoEnrich(anomaly)) { const context = { domSnapshot: '', httpLog: anomaly.evidence.httpLog ?? [], consoleErrors: anomaly.evidence.rawErrors ?? [], actionTrace: anomaly.actionTrace, pageTitle: '', url: anomaly.actionTrace[anomaly.actionTrace.length - 1]?.url ?? '', }; void enrichmentService.enrich(anomaly, context); } } } }); return httpServer; } if (require.main === module) { // eslint-disable-next-line @typescript-eslint/no-require-imports const { getDb } = require('../db/connection'); // eslint-disable-next-line @typescript-eslint/no-require-imports const { SessionRepository } = require('../db/SessionRepository'); // eslint-disable-next-line @typescript-eslint/no-require-imports const { AnomalyRepository } = require('../db/AnomalyRepository'); // eslint-disable-next-line @typescript-eslint/no-require-imports const { NotificationService } = require('./notifications/NotificationService'); const db = getDb(); const sessionRepo = new SessionRepository(db); const anomalyRepo = new AnomalyRepository(db); const notificationService = new NotificationService({ persister: (record) => { db.prepare(`INSERT OR REPLACE INTO notifications (id, anomaly_id, channel, status, sent_at, error) VALUES (?, ?, ?, ?, ?, ?)`).run(record.id, record.anomalyId, record.channel, record.status, record.sentAt ?? null, record.error ?? null); }, }); const outputDir = process.env['ABE_REPORTS_DIR'] ?? './reports'; const maxConcurrent = process.env['ABE_MAX_CONCURRENT_SESSIONS'] ? parseInt(process.env['ABE_MAX_CONCURRENT_SESSIONS'], 10) : 3; // eslint-disable-next-line @typescript-eslint/no-require-imports const { VisualBaselineRepository: VisualRepo } = require('../db/VisualBaselineRepository'); const visualRepo = new VisualRepo(db); const store = new SessionStore_1.SessionStore(outputDir, sessionRepo, anomalyRepo, maxConcurrent, notificationService, visualRepo); const dbCheck = () => { try { db.prepare('SELECT 1').run(); return true; } catch { return false; } }; // Scheduler // eslint-disable-next-line @typescript-eslint/no-require-imports const { ScheduleRepository: SchedRepo } = require('../db/ScheduleRepository'); // eslint-disable-next-line @typescript-eslint/no-require-imports const { SchedulerService: SchedSvc } = require('./scheduler/SchedulerService'); const scheduleRepo = new SchedRepo(db); const scheduler = new SchedSvc(scheduleRepo, store); scheduler.start(); const server = createServer(store, dbCheck, scheduleRepo, scheduler, visualRepo); // Graceful shutdown let shuttingDown = false; function shutdown(signal) { if (shuttingDown) return; shuttingDown = true; logger_1.log.info({ signal }, 'Graceful shutdown initiated'); scheduler.stop(); server.close(() => { try { db.close(); } catch { /* ignore */ } process.exit(0); }); setTimeout(() => { logger_1.log.error('Forced shutdown after 30s'); process.exit(1); }, 30000); } process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); server.listen(PORT, () => { logger_1.log.info({ port: PORT }, 'ABE API server listening'); }); }