import Fastify, { type FastifyRequest, type FastifyReply } from 'fastify'; import cors from '@fastify/cors'; import rateLimit from '@fastify/rate-limit'; import { ScreenerController } from './controllers/screener.controller'; import { FinanceController } from './controllers/finance.controller'; import { CallsController } from './controllers/calls.controller'; import { AnalyzeController } from './controllers/analyze.controller'; import { ScreenerEngine } from './services/ScreenerEngine'; import { BenchmarkProvider } from './services/BenchmarkProvider'; import { PortfolioAdvisor } from './services/PortfolioAdvisor'; import { CalendarService } from './services/CalendarService'; import { LLMAnalyst } from './services/LLMAnalyst'; import { CatalystAnalyst } from './services/CatalystAnalyst'; import { YahooFinanceClient } from './clients/YahooFinanceClient'; import { MarketCallRepository } from './repositories/MarketCallRepository'; import { PortfolioRepository } from './repositories/PortfolioRepository'; import { createDb } from './db/index'; import { noopLogger } from './utils/logger'; interface BuildAppOptions { logger?: boolean; } // ── Adding a new domain ─────────────────────────────────────────────────── // 1. server/types/.model.ts — define request/response shapes // 2. server/services/.ts — business logic // 3. server/controllers/.controller.ts — HTTP wiring (class + register) // 4. Register: new Controller(...).register(app) ← add below // ─────────────────────────────────────────────────────────────────────────── export async function buildApp({ logger = true }: BuildAppOptions = {}) { const app = Fastify({ logger }); await app.register(cors, { origin: process.env.CLIENT_ORIGIN ?? 'http://localhost:5173', }); // ── Rate limiting — applied globally, tightest on expensive routes ─────── await app.register(rateLimit, { global: false, // opt-in per route via config.rateLimit max: 60, timeWindow: '1 minute', }); // ── API key auth — only enforced when API_KEY env var is set ───────────── const API_KEY = process.env.API_KEY; if (API_KEY) { app.addHook('onRequest', async (req: FastifyRequest, reply: FastifyReply) => { // Skip auth for health check and OPTIONS preflight if (req.url === '/health' || req.method === 'OPTIONS') return; const header = req.headers['authorization'] ?? ''; if (header !== `Bearer ${API_KEY}`) { return reply.code(401).send({ error: 'Unauthorized' }); } }); } const db = createDb(); const yahoo = new YahooFinanceClient(); const benchmark = new BenchmarkProvider(yahoo, { logger: noopLogger }); const engine = new ScreenerEngine(yahoo, benchmark, { logger: noopLogger }); const advisor = new PortfolioAdvisor(yahoo); const calSvc = new CalendarService(yahoo); const llm = new LLMAnalyst({ logger: noopLogger }); const catalyst = new CatalystAnalyst({ logger: noopLogger }); new ScreenerController(engine).register(app); new FinanceController(engine, new PortfolioRepository(db), advisor).register(app); new CallsController(new MarketCallRepository(db), engine, calSvc).register(app); new AnalyzeController(catalyst, llm).register(app); app.get('/health', async () => ({ status: 'ok' })); return app; }