/** * Shared entry gate for every /api/gallery/ai/* route: authenticate, then * throttle. Lives in a `_`-prefixed file so the App Router never treats it as a * route (only `route.ts` defines an endpoint). * * Order matters: we authenticate FIRST so the rate limit can be keyed by * principal rather than by a spoofable `x-forwarded-for` hop wherever possible. * The session check is one cheap BFF round trip; the work it guards is a GPU * call, so paying it before throttling is the right trade. * * SERVER-ONLY. */ import { NextResponse } from "next/server"; import { limit } from "@/lib/server/rate-limit"; import { rateLimitKey, requireGallerySession } from "@/lib/server/session"; /** Per-minute budgets, per the Smart Gallery route contract. */ export const RATE_LIMITS = { classify: 30, edit: 12, tilt: 30, transcribe: 20, denoise: 20, } as const; const WINDOW_MS = 60_000; export type GuardResult = | { ok: true; principalId?: string } /** Ready-to-return error response — the route should return it unchanged. */ | { ok: false; response: NextResponse }; /** * @param route Which budget to apply (also namespaces the limiter key so a * caller's `edit` spend does not consume their `classify` budget). */ export async function guard(req: Request, route: keyof typeof RATE_LIMITS): Promise { const session = await requireGallerySession(req); if (!session.ok) { return { ok: false, response: NextResponse.json({ error: session.error }, { status: session.status }), }; } const key = `${route}:${rateLimitKey(req, session.principalId)}`; const { ok, retryAfter } = limit(key, RATE_LIMITS[route], WINDOW_MS); if (!ok) { return { ok: false, response: NextResponse.json( { error: "Too many requests — slow down." }, { status: 429, headers: { "Retry-After": String(retryAfter) } }, ), }; } return { ok: true, principalId: session.principalId }; }