fase(7): api server refactor with composition root
This commit is contained in:
164
src/main.ts
Normal file
164
src/main.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
// API + Realtime
|
||||
import { createServer } from './api/server';
|
||||
import { SocketGateway } from './realtime/SocketGateway';
|
||||
|
||||
async function bootstrap(): Promise<void> {
|
||||
// 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 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. HTTP server
|
||||
const app = createServer({
|
||||
config,
|
||||
logger,
|
||||
db,
|
||||
crawlingDeps: { startCrawl, stopCrawl, getSession, listSessions },
|
||||
findingsDeps: { getFinding, listFindings, findingStats, resolveFinding, enrichFinding },
|
||||
fuzzingDeps: { runFuzz, repository: fuzzRepo },
|
||||
});
|
||||
|
||||
const httpServer = http.createServer(app);
|
||||
|
||||
// 11. Socket.io + gateway
|
||||
const io = new SocketIOServer(httpServer, {
|
||||
cors: { origin: config.cors.origin, credentials: true },
|
||||
});
|
||||
const gateway = new SocketGateway(io, eventBus, logger);
|
||||
gateway.start();
|
||||
|
||||
// 12. Start listening
|
||||
await new Promise<void>((resolve) => {
|
||||
httpServer.listen(config.port, config.host, resolve);
|
||||
});
|
||||
logger.info({ port: config.port, host: config.host }, 'ABE server ready');
|
||||
|
||||
// 13. Graceful shutdown
|
||||
let shuttingDown = false;
|
||||
|
||||
async function shutdown(signal: string): Promise<void> {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
|
||||
logger.info({ signal }, 'Shutting down...');
|
||||
|
||||
// Stop accepting new connections
|
||||
httpServer.close();
|
||||
|
||||
// Close socket.io
|
||||
io.close();
|
||||
|
||||
// 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);
|
||||
});
|
||||
Reference in New Issue
Block a user