/** * ABE — composition root. * Wires all modules together and starts the HTTP + WebSocket server. */ import http from 'http'; import { Server as SocketIOServer } from 'socket.io'; import { loadConfig } from './shared/infrastructure/Config'; import { createLogger } from './shared/infrastructure/Logger'; import { createDatabase } from './shared/infrastructure/DatabaseConnection'; import { InProcessEventBus } from './shared/infrastructure/InProcessEventBus'; import { runMigrations } from './db/migrator'; // Crawling module import { KyselyCrawlSessionRepository } from './modules/crawling/infrastructure/repositories/KyselyCrawlSessionRepository'; import { KyselyStateRepository } from './modules/crawling/infrastructure/repositories/KyselyStateRepository'; import { StartCrawlCommand } from './modules/crawling/application/commands/StartCrawlCommand'; import { StopCrawlCommand } from './modules/crawling/application/commands/StopCrawlCommand'; import { GetSessionQuery } from './modules/crawling/application/queries/GetSessionQuery'; import { ListSessionsQuery } from './modules/crawling/application/queries/ListSessionsQuery'; // Findings module import { KyselyFindingRepository } from './modules/findings/infrastructure/repositories/KyselyFindingRepository'; import { CreateFindingCommand } from './modules/findings/application/commands/CreateFindingCommand'; import { EnrichFindingCommand } from './modules/findings/application/commands/EnrichFindingCommand'; import { ResolveFindingCommand } from './modules/findings/application/commands/ResolveFindingCommand'; import { GetFindingQuery } from './modules/findings/application/queries/GetFindingQuery'; import { ListFindingsQuery } from './modules/findings/application/queries/ListFindingsQuery'; import { FindingStatsQuery } from './modules/findings/application/queries/FindingStatsQuery'; import { OnAnomalyDetected } from './modules/findings/application/event-handlers/OnAnomalyDetected'; import { NullAIEnricher } from './modules/findings/infrastructure/NullAIEnricher'; // Fuzzing module import { FuzzingEngineAdapter } from './modules/fuzzing/infrastructure/adapters/FuzzingEngineAdapter'; import { RunFuzzCommand } from './modules/fuzzing/application/commands/RunFuzzCommand'; import { OnActionExecuted } from './modules/fuzzing/application/event-handlers/OnActionExecuted'; import { InMemoryFuzzSessionRepository } from './modules/fuzzing/infrastructure/repositories/InMemoryFuzzSessionRepository'; // Auth module import { KyselyUserRepository } from './modules/auth/infrastructure/repositories/KyselyUserRepository'; import { KyselyOrganizationRepository } from './modules/auth/infrastructure/repositories/KyselyOrganizationRepository'; import { KyselyApiKeyRepository } from './modules/auth/infrastructure/repositories/KyselyApiKeyRepository'; import { KyselySessionRepository } from './modules/auth/infrastructure/repositories/KyselySessionRepository'; import { RegisterCommand } from './modules/auth/application/commands/RegisterCommand'; import { LoginCommand } from './modules/auth/application/commands/LoginCommand'; import { CreateOrganizationCommand } from './modules/auth/application/commands/CreateOrganizationCommand'; import { InviteMemberCommand } from './modules/auth/application/commands/InviteMemberCommand'; import { CreateApiKeyCommand } from './modules/auth/application/commands/CreateApiKeyCommand'; import { GetUserQuery } from './modules/auth/application/queries/GetUserQuery'; import { ListOrgMembersQuery } from './modules/auth/application/queries/ListOrgMembersQuery'; import { hashPassword, verifyPassword } from './modules/auth/infrastructure/auth/PasswordService'; // Reporting module import { KyselyReportRepository } from './modules/reporting/infrastructure/repositories/KyselyReportRepository'; import { GenerateReportCommand } from './modules/reporting/application/commands/GenerateReportCommand'; // Job queue import { SQLiteJobQueue } from './jobs/SQLiteJobQueue'; import { createExplorationJobHandler, EXPLORATION_JOB_TYPE } from './jobs/workers/ExplorationWorker'; import { createReportJobHandler, REPORT_JOB_TYPE } from './jobs/workers/ReportWorker'; // API + Realtime import { createServer } from './api/server'; import { SocketGateway } from './realtime/SocketGateway'; async function bootstrap(): Promise { // 1. Config const config = loadConfig(); // 2. Logger const logger = createLogger({ level: config.log.level, nodeEnv: config.nodeEnv }); logger.info({ port: config.port, env: config.nodeEnv }, 'Starting ABE...'); // 3. Database + migrations const db = createDatabase(config.db); await runMigrations(db); logger.info('Database migrations applied'); // 4. Event bus const eventBus = new InProcessEventBus(logger); // 5. Repositories const sessionRepo = new KyselyCrawlSessionRepository(db); const stateRepo = new KyselyStateRepository(db); const findingRepo = new KyselyFindingRepository(db); const reportRepo = new KyselyReportRepository(db); const fuzzRepo = new InMemoryFuzzSessionRepository(); // Suppress unused warning for stateRepo — used by crawling infrastructure void stateRepo; // 6. Crawling use cases const startCrawl = new StartCrawlCommand(sessionRepo, eventBus); const stopCrawl = new StopCrawlCommand(sessionRepo, eventBus); const getSession = new GetSessionQuery(sessionRepo); const listSessions = new ListSessionsQuery(sessionRepo); // 7. Findings use cases const createFinding = new CreateFindingCommand(findingRepo, eventBus); const enricher = new NullAIEnricher(); const enrichFinding = new EnrichFindingCommand(findingRepo, enricher, eventBus); const resolveFinding = new ResolveFindingCommand(findingRepo, eventBus); const getFinding = new GetFindingQuery(findingRepo); const listFindings = new ListFindingsQuery(findingRepo); const findingStats = new FindingStatsQuery(findingRepo); // 8. Fuzzing use cases const fuzzerEngine = new FuzzingEngineAdapter({ intensity: 'low', seed: 42 }); const runFuzz = new RunFuzzCommand(fuzzerEngine, fuzzRepo, eventBus); // 9. Event handlers — subscribe to EventBus const onAnomalyDetected = new OnAnomalyDetected(createFinding); eventBus.subscribe('crawling.anomaly_detected', onAnomalyDetected); const onActionExecuted = new OnActionExecuted(runFuzz); eventBus.subscribe('crawling.action_executed', onActionExecuted); // 10. Auth module const userRepo = new KyselyUserRepository(db); const orgRepo = new KyselyOrganizationRepository(db); const apiKeyRepo = new KyselyApiKeyRepository(db); const authSessionRepo = new KyselySessionRepository(db); const registerCommand = new RegisterCommand(userRepo, eventBus, hashPassword); const loginCommand = new LoginCommand(userRepo, authSessionRepo, eventBus, verifyPassword); const createOrgCommand = new CreateOrganizationCommand(orgRepo, userRepo, eventBus); const inviteMemberCommand = new InviteMemberCommand(orgRepo, userRepo, eventBus); const createApiKeyCommand = new CreateApiKeyCommand(apiKeyRepo, userRepo); const getUserQuery = new GetUserQuery(userRepo); const listOrgMembersQuery = new ListOrgMembersQuery(orgRepo, userRepo); // 11. Reporting use cases const generateReport = new GenerateReportCommand(reportRepo, eventBus); // 12. Job queue (created before HTTP server so it can be injected) const jobQueue = new SQLiteJobQueue(db, logger, config.jobs.pollIntervalMs); jobQueue.registerHandler( EXPLORATION_JOB_TYPE, createExplorationJobHandler({ sessionRepo, eventBus, logger }), ); jobQueue.registerHandler(REPORT_JOB_TYPE, createReportJobHandler({ logger, reportRepository: reportRepo, findingRepository: findingRepo })); jobQueue.start(); // 13. HTTP server const app = createServer({ config, logger, db, crawlingDeps: { startCrawl, stopCrawl, getSession, listSessions }, findingsDeps: { getFinding, listFindings, findingStats, resolveFinding, enrichFinding }, fuzzingDeps: { runFuzz, repository: fuzzRepo }, reportingDeps: { generateReport, reportRepository: reportRepo, jobQueue }, authDeps: { registerCommand, loginCommand, createOrgCommand, inviteMemberCommand, createApiKeyCommand, getUserQuery, listOrgMembersQuery, sessionRepository: authSessionRepo, apiKeyRepository: apiKeyRepo, userRepository: userRepo, }, }); const httpServer = http.createServer(app); // 12. Socket.io + gateway const io = new SocketIOServer(httpServer, { cors: { origin: config.cors.origin, credentials: true }, }); const gateway = new SocketGateway(io, eventBus, logger); gateway.start(); // 13. Start listening await new Promise((resolve) => { httpServer.listen(config.port, config.host, resolve); }); logger.info({ port: config.port, host: config.host }, 'ABE server ready'); // 14. Graceful shutdown let shuttingDown = false; async function shutdown(signal: string): Promise { if (shuttingDown) return; shuttingDown = true; logger.info({ signal }, 'Shutting down...'); // Stop accepting new connections httpServer.close(); // Close socket.io io.close(); // Stop job queue and wait for active jobs jobQueue.pause(); await jobQueue.waitForActive(30_000); // Close database try { await db.destroy(); } catch (err) { logger.warn({ err }, 'Error closing database'); } logger.info('Shutdown complete'); process.exit(0); } // Force-exit if graceful shutdown takes too long function forceExit(signal: string): void { void shutdown(signal).catch(() => { process.exit(1); }); setTimeout(() => { logger.error('Forced shutdown after 30s'); process.exit(1); }, 30_000).unref(); } process.on('SIGTERM', () => forceExit('SIGTERM')); process.on('SIGINT', () => forceExit('SIGINT')); } bootstrap().catch((err: unknown) => { console.error('Fatal: failed to start ABE', err); process.exit(1); });