import { trpcServer } from "@hono/trpc-server"; import { createContext } from "@pram/api/context"; import { appRouter } from "@pram/api/routers/index"; import { checkRateLimitAllowed } from "@pram/api/lib/rate-limit"; import { auth } from "@pram/auth"; import { env } from "@pram/env/server"; import { withRequestId } from "@pram/api/logger"; import { Hono } from "hono"; import { cors } from "hono/cors"; import "@pram/api/queue"; const app = new Hono(); app.use("/*", async (c, next) => { const reqId = c.req.header("x-request-id") ?? crypto.randomUUID(); const reqLogger = withRequestId(reqId); const start = Date.now(); c.res.headers.set("x-request-id", reqId); reqLogger.info(`${c.req.method} ${c.req.path}`); await next(); const duration = Date.now() - start; reqLogger.info(`${c.req.method} ${c.req.path} ${c.res.status}`, { durationMs: duration }); }); app.use("/*", async (c, next) => { await next(); c.res.headers.set("X-Content-Type-Options", "nosniff"); c.res.headers.set("X-Frame-Options", "DENY"); c.res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); c.res.headers.set("X-XSS-Protection", "0"); c.res.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()"); }); app.use( "/*", cors({ origin: env.CORS_ORIGIN, allowMethods: ["GET", "POST", "OPTIONS"], allowHeaders: ["Content-Type", "Authorization"], credentials: true, }), ); // Rate limit sign-up attempts per IP to mitigate mass account creation app.use("/api/auth/sign-up/email", async (c, next) => { if (c.req.method !== "POST") return next(); const cfIp = c.req.header("cf-connecting-ip"); const forwarded = c.req.header("x-forwarded-for"); const realIp = c.req.header("x-real-ip"); const firstForwardedIp = forwarded ? forwarded.split(",")[0]?.trim() : undefined; const ip = cfIp ?? firstForwardedIp ?? realIp ?? "unknown"; const allowed = await checkRateLimitAllowed({ key: `signup:ip:${ip}`, limit: 5, windowMs: 3_600_000 }); if (!allowed) { return c.json({ error: "Too many sign-up attempts. Please try again later." }, 429); } return next(); }); app.on(["POST", "GET"], "/api/auth/*", (c) => auth.handler(c.req.raw)); app.use( "/trpc/*", trpcServer({ router: appRouter, createContext: (_opts, context) => { return createContext({ context }); }, }), ); app.get("/sitemap.xml", (c) => { const baseUrl = "https://pram-app.vercel.app"; const urls = [ { loc: `${baseUrl}/`, changefreq: "weekly", priority: "1.0" }, ]; const sitemap = ` ${urls .map( (url) => ` ${url.loc} ${url.changefreq} ${url.priority} `, ) .join("\n")} `; return c.text(sitemap, 200, { "Content-Type": "application/xml" }); }); app.get("/", (c) => { return c.text("OK"); }); export default { port: process.env.PORT || 3000, hostname: "0.0.0.0", fetch: app.fetch, };