docs: enterprise refactor plan with ralph specs
This commit is contained in:
199
dist/server/index.js
vendored
Normal file
199
dist/server/index.js
vendored
Normal file
@@ -0,0 +1,199 @@
|
||||
"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');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user