import express from "express"; import { randomUUID } from "crypto"; import compression from "compression"; import logger from "./services/logger.js"; import helmet from "helmet"; import path from "path"; import { fileURLToPath } from "url"; import rateLimit from "express-rate-limit"; import { convertRouter } from "./routes/convert.js"; import { templatesRouter } from "./routes/templates.js"; import { healthRouter } from "./routes/health.js"; import { signupRouter } from "./routes/signup.js"; import { recoverRouter } from "./routes/recover.js"; import { billingRouter } from "./routes/billing.js"; import { emailChangeRouter } from "./routes/email-change.js"; import { authMiddleware } from "./middleware/auth.js"; import { usageMiddleware, loadUsageData } from "./middleware/usage.js"; import { getUsageStats } from "./middleware/usage.js"; import { pdfRateLimitMiddleware, getConcurrencyStats } from "./middleware/pdfRateLimit.js"; import { initBrowser, closeBrowser } from "./services/browser.js"; import { loadKeys, getAllKeys } from "./services/keys.js"; import { verifyToken, loadVerifications } from "./services/verification.js"; import { initDatabase, pool } from "./services/db.js"; const app = express(); const PORT = parseInt(process.env.PORT || "3100", 10); app.use(helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } })); // Request ID + request logging middleware app.use((req, res, next) => { const requestId = (req.headers["x-request-id"] as string) || randomUUID(); (req as any).requestId = requestId; res.setHeader("X-Request-Id", requestId); const start = Date.now(); res.on("finish", () => { const ms = Date.now() - start; if (req.path !== "/health") { logger.info({ method: req.method, path: req.path, status: res.statusCode, ms, requestId }, "request"); } }); next(); }); // Permissions-Policy header app.use((_req, res, next) => { res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(self)"); next(); }); // Compression app.use(compression()); // Differentiated CORS middleware app.use((req, res, next) => { const isAuthBillingRoute = req.path.startsWith('/v1/signup') || req.path.startsWith('/v1/recover') || req.path.startsWith('/v1/billing') || req.path.startsWith('/v1/email-change'); if (isAuthBillingRoute) { res.setHeader("Access-Control-Allow-Origin", "https://docfast.dev"); } else { res.setHeader("Access-Control-Allow-Origin", "*"); } res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key"); res.setHeader("Access-Control-Max-Age", "86400"); if (req.method === "OPTIONS") { res.status(204).end(); return; } next(); }); // Raw body for Stripe webhook signature verification app.use("/v1/billing/webhook", express.raw({ type: "application/json" })); app.use(express.json({ limit: "2mb" })); app.use(express.text({ limit: "2mb", type: "text/*" })); // Trust nginx proxy app.set("trust proxy", 1); // Global rate limiting - reduced from 10,000 to reasonable limit const limiter = rateLimit({ windowMs: 60_000, max: 100, standardHeaders: true, legacyHeaders: false, }); app.use(limiter); // Public routes app.use("/health", healthRouter); app.use("/v1/signup", signupRouter); app.use("/v1/recover", recoverRouter); app.use("/v1/billing", billingRouter); app.use("/v1/email-change", emailChangeRouter); // Authenticated routes app.use("/v1/convert", authMiddleware, usageMiddleware, pdfRateLimitMiddleware, convertRouter); app.use("/v1/templates", authMiddleware, usageMiddleware, templatesRouter); // Admin: usage stats app.get("/v1/usage", authMiddleware, (req: any, res) => { res.json(getUsageStats(req.apiKeyInfo?.key)); }); // Admin: concurrency stats app.get("/v1/concurrency", authMiddleware, (_req, res) => { res.json(getConcurrencyStats()); }); // Email verification endpoint app.get("/verify", (req, res) => { const token = req.query.token as string; if (!token) { res.status(400).send(verifyPage("Invalid Link", "No verification token provided.", null)); return; } const result = verifyToken(token); switch (result.status) { case "ok": res.send(verifyPage("Email Verified! ๐", "Your DocFast API key is ready:", result.verification!.apiKey)); break; case "already_verified": res.send(verifyPage("Already Verified", "This email was already verified. Here's your API key:", result.verification!.apiKey)); break; case "expired": res.status(410).send(verifyPage("Link Expired", "This verification link has expired (24h). Please sign up again.", null)); break; case "invalid": res.status(404).send(verifyPage("Invalid Link", "This verification link is not valid.", null)); break; } }); function verifyPage(title: string, message: string, apiKey: string | null): string { return `
${message}
${apiKey ? `The page you're looking for doesn't exist or has been moved.