import type { Express, Request, Response } from "express"; import { createServer } from "node:http"; import type { Server } from "node:http"; // eslint-disable-next-line @typescript-eslint/no-var-requires import * as session from "express-session"; // eslint-disable-next-line @typescript-eslint/no-var-requires import MemoryStoreModule from "memorystore"; const MemoryStore = MemoryStoreModule(session.default || session); import { SYMBOLS, computeExpirations as mockExpirations, computeGexProfile as mockGexProfile, computeScreener as mockScreener, computeSummary as mockSummary, } from "./marketData"; import { oratsClient } from "./oratsClient"; import { registerAuthRoutes } from "./authRoutes"; import { initDb } from "./db"; import { rateLimit, securityHeaders } from "./middleware"; const SessionMemoryStore = new MemoryStore({ checkPeriod: 86400000 }); const KNOWN_TICKERS = new Set(SYMBOLS.map((s) => s.ticker)); function withTicker(req: Request, res: Response, fn: (ticker: string) => unknown) { const raw = String(req.params.symbol || "").toUpperCase(); if (!KNOWN_TICKERS.has(raw)) { res.status(404).json({ error: `Unknown symbol: ${raw}` }); return; } try { res.json(fn(raw)); } catch (err) { res.status(500).json({ error: "Internal server error" }); } } export async function registerRoutes(httpServer: Server, app: Express): Promise { await initDb(); // ── Security Headers ────────────────────────────────────────────── app.use(securityHeaders); // ── Rate Limiting ───────────────────────────────────────────────── // General rate limit: 500 requests per 15 minutes (relaxed for dev) // Home subnet (192.168.x.x) is whitelisted in middleware.ts app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 500 })); // Auth-specific rate limit: 50 requests per 15 minutes (relaxed for dev) app.use("/api/auth/", rateLimit({ windowMs: 15 * 60 * 1000, max: 50 })); // ── Session Configuration ───────────────────────────────────────── app.use( (session.default || session)({ store: new MemoryStore({ checkPeriod: 86400000 }), secret: process.env.SESSION_SECRET || "gammadesk-dev-secret-change-me", resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === "production", httpOnly: true, // Prevent XSS from accessing session cookie sameSite: "strict", // Prevent CSRF maxAge: 24 * 60 * 60 * 1000, // 24 hours }, }), ); // ── Body Parsing ────────────────────────────────────────────────── app.use(express.json({ limit: "1mb" })); app.use(express.urlencoded({ extended: false, limit: "1mb" })); // Auth routes registerAuthRoutes(app); // Public API routes app.get("/api/symbols", (_req, res) => { res.json(SYMBOLS); }); // ORATS status - hide sensitive info app.get("/api/orats/status", (_req, res) => { res.json({ configured: oratsClient.isConfigured(), baseUrl: oratsClient.isConfigured() ? "https://api.orats.io/datav2" : "not-configured", }); }); // Save ORATS API key + base URL to session app.post("/api/orats/config", (req, res) => { const { apiKey, baseUrl } = req.body as Record; const session = req.session as Record; if (!apiKey || typeof apiKey !== "string") { return res.status(400).json({ error: "API key is required" }); } const sanitizedApiKey = apiKey.trim().replace(/[<>]/g, ""); session.oratsApiKey = sanitizedApiKey; // Update the orats client with the new key oratsClient.setApiKey(sanitizedApiKey); if (baseUrl && typeof baseUrl === "string") { oratsClient.setBaseUrl(baseUrl); } res.json({ ok: true, configured: oratsClient.isConfigured() }); }); // Clear ORATS API key from session app.delete("/api/orats/config", (req, res) => { const session = req.session as Record; delete session.oratsApiKey; oratsClient.setApiKey(""); res.json({ ok: true, configured: oratsClient.isConfigured() }); }); // Market data routes app.get("/api/market/:symbol/summary", async (req, res) => { const ticker = String(req.params.symbol || "").toUpperCase(); if (!KNOWN_TICKERS.has(ticker)) { return res.status(404).json({ error: `Unknown symbol: ${ticker}` }); } try { const summary = oratsClient.isConfigured() ? await oratsClient.computeSummary(ticker) : mockSummary(ticker); res.json(summary); } catch (err) { console.error(`ORATS summary error for ${ticker}:`, err); res.json(mockSummary(ticker)); } }); app.get("/api/market/:symbol/gex", async (req, res) => { const ticker = String(req.params.symbol || "").toUpperCase(); if (!KNOWN_TICKERS.has(ticker)) { return res.status(404).json({ error: `Unknown symbol: ${ticker}` }); } try { const gex = oratsClient.isConfigured() ? await oratsClient.computeGexProfile(ticker) : mockGexProfile(ticker); res.json(gex); } catch (err) { console.error(`ORATS GEX error for ${ticker}:`, err); res.json(mockGexProfile(ticker)); } }); app.get("/api/market/:symbol/expirations", async (req, res) => { const ticker = String(req.params.symbol || "").toUpperCase(); if (!KNOWN_TICKERS.has(ticker)) { return res.status(404).json({ error: `Unknown symbol: ${ticker}` }); } try { const exp = oratsClient.isConfigured() ? await oratsClient.computeExpirations(ticker) : mockExpirations(ticker); res.json(exp); } catch (err) { console.error(`ORATS expirations error for ${ticker}:`, err); res.json(mockExpirations(ticker)); } }); app.get("/api/screener", async (_req, res) => { try { if (oratsClient.isConfigured()) { const summaries = await Promise.all( SYMBOLS.map((s) => oratsClient.computeSummary(s.ticker)), ); const screenerData = summaries.map((summary, i) => { const symbol = SYMBOLS[i]; return { ticker: symbol.ticker, name: symbol.name, spot: summary.spot, spotChangePct: summary.spotChangePct, netGex: summary.netGex, gammaRegime: summary.gammaRegime, ivRank: summary.ivRank, ivx: summary.ivx, callWall: summary.callWall, putWall: summary.putWall, distanceToCallWall: ((summary.callWall - summary.spot) / summary.spot) * 100, distanceToPutWall: ((summary.putWall - summary.spot) / summary.spot) * 100, expectedMovePct: summary.expectedMovePct, skew: summary.skew, zeroDteNetGex: summary.netGex, }; }); res.json(screenerData); } else { res.json(mockScreener()); } } catch (err) { console.error("ORATS screener error:", err); res.json(mockScreener()); } }); return httpServer; } // Import express for body parsing import express from "express";