import express from "express"; import { randomUUID } from "crypto"; import "./types.js"; // Augments Express.Request with requestId, acquirePdfSlot, releasePdfSlot import { compressionMiddleware } from "./middleware/compression.js"; 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 { demoRouter } from "./routes/demo.js"; import { recoverRouter } from "./routes/recover.js"; import { emailChangeRouter } from "./routes/email-change.js"; import { billingRouter } from "./routes/billing.js"; import { authMiddleware } from "./middleware/auth.js"; import { usageMiddleware, loadUsageData, flushDirtyEntries } from "./middleware/usage.js"; import { pdfRateLimitMiddleware } from "./middleware/pdfRateLimit.js"; import { adminRouter } from "./routes/admin.js"; import { initBrowser, closeBrowser } from "./services/browser.js"; import { loadKeys, getAllKeys } from "./services/keys.js"; import { pagesRouter } from "./routes/pages.js"; import { initDatabase, pool, cleanupStaleData } from "./services/db.js"; import { startPeriodicCleanup, stopPeriodicCleanup } from "./utils/periodic-cleanup.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"] || randomUUID(); req.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(compressionMiddleware); // Block search engine indexing on staging app.use((req, res, next) => { if (req.hostname.includes("staging")) { res.setHeader("X-Robots-Tag", "noindex, nofollow"); } next(); }); // Differentiated CORS middleware const ALLOWED_ORIGINS = new Set(["https://docfast.dev", "https://staging.docfast.dev"]); 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/demo') || req.path.startsWith('/v1/email-change'); if (isAuthBillingRoute) { const origin = req.headers.origin; if (origin && ALLOWED_ORIGINS.has(origin)) { res.setHeader("Access-Control-Allow-Origin", origin); res.setHeader("Vary", "Origin"); } else { 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" })); // NOTE: No global express.json() here — route-specific parsers are applied // per-route below to enforce correct body size limits (BUG-101 fix). 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/demo", express.json({ limit: "50kb" }), pdfRateLimitMiddleware, demoRouter); /** * @openapi * /v1/signup/free: * post: * tags: [Account] * deprecated: true * summary: Request a free API key (discontinued) * description: Free accounts have been discontinued. Use the demo endpoints or upgrade to Pro. * responses: * 410: * description: Feature discontinued * content: * application/json: * schema: * type: object * properties: * error: * type: string * demo_endpoint: * type: string * pro_url: * type: string */ app.use("/v1/signup", (_req, res) => { res.status(410).json({ error: "Free accounts have been discontinued. Try our demo at POST /v1/demo/html or upgrade to Pro at https://docfast.dev", demo_endpoint: "/v1/demo/html", pro_url: "https://docfast.dev/#pricing" }); }); // Default 2MB JSON parser for standard routes const defaultJsonParser = express.json({ limit: "2mb" }); app.use("/v1/recover", defaultJsonParser, recoverRouter); app.use("/v1/email-change", defaultJsonParser, emailChangeRouter); app.use("/v1/billing", defaultJsonParser, billingRouter); // Authenticated routes — conversion routes get tighter body limits (500KB) const convertBodyLimit = express.json({ limit: "500kb" }); app.use("/v1/convert", convertBodyLimit, authMiddleware, usageMiddleware, pdfRateLimitMiddleware, convertRouter); app.use("/v1/templates", defaultJsonParser, authMiddleware, usageMiddleware, templatesRouter); // Admin + usage routes (extracted to routes/admin.ts) app.use(adminRouter); // Pages, favicon, docs, openapi.json, /api (extracted to routes/pages.ts) const __dirname = path.dirname(fileURLToPath(import.meta.url)); app.use(pagesRouter); // Static asset cache headers middleware app.use((req, res, next) => { if (/\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/.test(req.path)) { res.setHeader('Cache-Control', 'public, max-age=604800, immutable'); } next(); }); app.use(express.static(path.join(__dirname, "../public"), { etag: true, cacheControl: false, })); // 404 handler - must be after all routes app.use((req, res) => { // Check if it's an API request const isApiRequest = req.path.startsWith('/v1/') || req.path.startsWith('/api') || req.path.startsWith('/health'); if (isApiRequest) { // JSON 404 for API paths res.status(404).json({ error: `Not Found: ${req.method} ${req.path}` }); } else { // HTML 404 for browser paths res.status(404).send(`
The page you're looking for doesn't exist or has been moved.