import type { MiddlewareHandler } from "hono"; import type { AppBindings } from "@/worker/hono"; import { HttpError } from "@/worker/http"; // CSRF boundary for tessera-owned cookie-authenticated mutating routes. // Better Auth runs its own originCheck inside `auth.handler` for /api/auth/* // requests; tessera's custom routes sit outside that and need their own // gate. SameSite=Lax on the session cookie blocks cross-site form POSTs, // but does NOT stop a sibling subdomain on the same site (e.g. // evil.limic.dev posting to auth.limic.dev) — Origin is the correct // primary signal for that case. // // Safe methods (GET/HEAD/OPTIONS) skip the check: browsers do not reliably // send Origin on those requests, and read-side access is gated by // SameSite=Lax plus the session cookie. Origin matching on safe methods // would block legitimate cross-tab GETs without changing the threat model. // // Order: this runs before requireUser/requireAdmin so an unauthenticated // foreign-origin POST short-circuits at 403 without burning a session // lookup. export const requireSameOriginForMutations: MiddlewareHandler = async (c, next) => { const method = c.req.method; if (method === "GET" || method === "HEAD" || method === "OPTIONS") { await next(); return; } const expected = new URL(c.var.baseURL).origin; const actual = c.req.header("origin"); if (!actual || actual !== expected) { throw new HttpError(403, "forbidden_origin", "Request origin not allowed."); } await next(); };