# Phase 2: Shared Infrastructure ## Config.ts Usa Zod para validar TODAS las env vars al arranque. Si falla → crash inmediato con mensaje claro. ```typescript import { z } from 'zod'; import dotenv from 'dotenv'; dotenv.config(); const configSchema = z.object({ port: z.coerce.number().default(3001), host: z.string().default('0.0.0.0'), nodeEnv: z.enum(['development', 'production', 'test']).default('development'), db: z.object({ driver: z.enum(['sqlite', 'postgres']).default('sqlite'), path: z.string().default('./data/abe.db'), url: z.string().optional(), }), auth: z.object({ secret: z.string().min(16).default('abe-dev-secret-change-in-prod'), sessionMaxAge: z.coerce.number().default(86400), }), storage: z.object({ driver: z.enum(['local', 's3']).default('local'), path: z.string().default('./data/storage'), }), cors: z.object({ origin: z.string().default('http://localhost:5173') }), log: z.object({ level: z.enum(['debug','info','warn','error']).default('info') }), api: z.object({ key: z.string().default('abe-dev-key-123'), rateLimitWindowMs: z.coerce.number().default(900000), rateLimitMax: z.coerce.number().default(100), }), ai: z.object({ provider: z.enum(['claude','openai','ollama','none']).default('none'), apiKey: z.string().default(''), autoEnrich: z.coerce.boolean().default(false), minSeverity: z.enum(['low','medium','high','critical']).default('high'), }), jobs: z.object({ maxConcurrentSessions: z.coerce.number().default(3), pollIntervalMs: z.coerce.number().default(1000), }), license: z.object({ key: z.string().default('') }), }); export type AppConfig = z.infer; export function loadConfig(): AppConfig { // Map env vars to schema shape, parse } ``` ## Logger.ts ```typescript import pino from 'pino'; export function createLogger(config: { level: string; nodeEnv: string }): pino.Logger { return pino({ level: config.level, transport: config.nodeEnv === 'development' ? { target: 'pino-pretty', options: { colorize: true, translateTime: 'HH:MM:ss' } } : undefined, }); } export type Logger = pino.Logger; ``` ## DatabaseConnection.ts ```typescript import { Kysely, SqliteDialect } from 'kysely'; import SQLite from 'better-sqlite3'; // Define Database interface con todas las tablas export interface Database { sessions: SessionTable; states: StateTable; actions: ActionTable; anomalies: AnomalyTable; // ... más tablas se añaden en fases posteriores } export function createDatabase(config: { driver: string; path: string; url?: string }): Kysely { if (config.driver === 'postgres') { // Import dinámico de pg para no requerir en SQLite const { Pool } = require('pg'); const { PostgresDialect } = require('kysely'); return new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: config.url }) }), }); } // Crear directorio data/ si no existe const path = require('path'); const fs = require('fs'); fs.mkdirSync(path.dirname(config.path), { recursive: true }); return new Kysely({ dialect: new SqliteDialect({ database: new SQLite(config.path) }), }); } ``` ## InProcessEventBus.ts ```typescript import { EventEmitter } from 'events'; // Implements EventBus interface from shared/application // Logging de cada evento publicado // Catch errors en handlers (log pero no crash) // setMaxListeners(50) ``` ## StorageProvider.ts ```typescript export interface IStorageProvider { save(relativePath: string, data: Buffer): Promise; get(relativePath: string): Promise; delete(relativePath: string): Promise; exists(relativePath: string): Promise; } // LocalStorageProvider: usa fs.promises, base path = config.storage.path // Crea directorios automáticamente con mkdir recursive ``` ## Migración 001 Crea las tablas que ya existen en el schema actual (sessions, states, actions, anomalies, notifications). Usar `CREATE TABLE IF NOT EXISTS` para idempotencia. Los tipos de columna deben coincidir con lo que ya tiene better-sqlite3. ## IMPORTANTE - Config DEBE fallar rápido si hay env vars inválidas - Logger NUNCA debe usar console.log - Database factory NUNCA importa pg a menos que driver sea postgres - EventBus handlers que fallan se loguean pero NO crashean el bus