docs: enterprise refactor plan with ralph specs

This commit is contained in:
debian
2026-03-04 16:17:03 -05:00
parent 4c92712d20
commit f8191133c8
204 changed files with 32722 additions and 422 deletions

View File

@@ -0,0 +1,216 @@
# Phase 7: API Server Refactor + Composition Root
## Middleware stack (ORDEN IMPORTA)
```typescript
// server.ts
export function createServer(deps: ServerDependencies): Express {
const app = express();
// 1. Request ID (PRIMERO — todo log necesita esto)
app.use(requestIdMiddleware);
// 2. Security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
connectSrc: ["'self'", "ws:", "wss:"],
scriptSrc: ["'self'", "'unsafe-inline'"], // para Scalar docs
},
},
}));
// 3. CORS
app.use(cors({
origin: deps.config.cors.origin,
credentials: true,
}));
// 4. Rate limiting global
app.use(rateLimit({
windowMs: deps.config.api.rateLimitWindowMs,
max: deps.config.api.rateLimitMax,
standardHeaders: true,
legacyHeaders: false,
}));
// 5. Body parsing
app.use(express.json({ limit: '10mb' }));
// 6. Health endpoints (SIN auth)
app.get('/health/live', (_, res) => res.json({ status: 'ok' }));
app.get('/health/ready', async (_, res) => { /* check DB */ });
// 7. Auth routes (SIN auth middleware general)
app.use('/api/auth', deps.authController.router);
// 8. Auth middleware (TODOS los /api/ a partir de aquí)
app.use('/api', deps.authMiddleware);
// 9. Module routes
app.use('/api', deps.crawlingController.router);
app.use('/api', deps.findingsController.router);
app.use('/api', deps.fuzzingController.router);
// ... más módulos
// 10. 404 handler
app.use(notFoundMiddleware);
// 11. Error handler (SIEMPRE último)
app.use(globalErrorHandler);
return app;
}
```
## Error hierarchy
```typescript
export class AppError extends Error {
constructor(
message: string,
public readonly statusCode: number,
public readonly code: string,
public readonly isOperational = true,
) { super(message); }
}
export class ValidationError extends AppError {
constructor(message: string, public readonly details?: unknown) {
super(message, 400, 'VALIDATION_ERROR');
}
}
export class AuthenticationError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401, 'AUTHENTICATION_ERROR');
}
}
export class ForbiddenError extends AppError {
constructor(message = 'Forbidden') {
super(message, 403, 'FORBIDDEN');
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 404, 'NOT_FOUND');
}
}
export class ConflictError extends AppError {
constructor(message: string) {
super(message, 409, 'CONFLICT');
}
}
export class RateLimitError extends AppError {
constructor() {
super('Too many requests', 429, 'RATE_LIMIT');
}
}
```
## Global error handler
```typescript
export function globalErrorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
const logger = req.log || console; // pino child logger
if (err instanceof AppError && err.isOperational) {
logger.warn({ err, statusCode: err.statusCode }, err.message);
return res.status(err.statusCode).json({
error: err.message,
code: err.code,
...(err instanceof ValidationError && err.details ? { details: err.details } : {}),
});
}
// Programmer error — log full stack, return generic message
logger.error({ err }, 'Unhandled error');
return res.status(500).json({
error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message,
code: 'INTERNAL_ERROR',
});
}
```
## Composition root (main.ts)
```typescript
async function bootstrap() {
// 1. Config
const config = loadConfig();
// 2. Logger
const logger = createLogger(config);
logger.info({ port: config.port }, 'Starting ABE...');
// 3. Database + migrations
const db = createDatabase(config.db);
await runMigrations(db, logger);
// 4. Event bus
const eventBus = new InProcessEventBus(logger);
// 5. Storage
const storage = new LocalStorageProvider(config.storage.path);
// 6. Repositories
const sessionRepo = new KyselyCrawlSessionRepository(db);
const findingRepo = new KyselyFindingRepository(db);
// ... etc
// 7. Use cases
const startCrawl = new StartCrawlCommand(sessionRepo, eventBus);
const listFindings = new ListFindingsQuery(findingRepo);
// ... etc
// 8. Event handlers — subscribe to event bus
const onAnomalyDetected = new OnAnomalyDetected(findingRepo, eventBus);
eventBus.subscribe('crawling.anomaly_detected', onAnomalyDetected);
// ... etc
// 9. Controllers
const crawlingController = new CrawlingController(startCrawl, ...);
const findingsController = new FindingsController(listFindings, ...);
// ... etc
// 10. HTTP server
const app = createServer({ config, authMiddleware, crawlingController, findingsController, ... });
const httpServer = createServer(app);
// 11. Socket.io
const io = new Server(httpServer, { cors: { origin: config.cors.origin } });
const gateway = new SocketGateway(io, eventBus);
// 12. Job queue
const jobQueue = new SQLiteJobQueue(db, logger);
jobQueue.start();
// 13. Listen
httpServer.listen(config.port, config.host, () => {
logger.info({ port: config.port }, 'ABE server ready');
});
// 14. Graceful shutdown
async function shutdown(signal: string) {
logger.info({ signal }, 'Shutting down...');
httpServer.close();
io.close();
jobQueue.pause();
await jobQueue.waitForActive(30000);
await db.destroy();
logger.info('Shutdown complete');
process.exit(0);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
}
bootstrap().catch((err) => {
console.error('Fatal: failed to start ABE', err);
process.exit(1);
});
```
## IMPORTANTE
- El código existente en src/server/ debe DEJAR DE USARSE gradualmente
- Mantener los endpoints viejos funcionando durante la migración
- Cada controller es una clase con un `.router` getter que retorna Express.Router
- NUNCA meter lógica de negocio en controllers — solo parse request → call use case → format response