Files
Autonomous-Bug-Explorer/dist/server/index.js

200 lines
8.5 KiB
JavaScript

"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');
});
}