import { type NextRequest, NextResponse } from "next/server"; import { resolveImagerBase } from "@/lib/imager"; const FIGURE_RE = /^[a-z]{2}-\d+(-\d+)?(\.[a-z]{2}-\d+(-\d+)?)*$/i; const FIGURE_MAX_LEN = 512; const FIGURE_MAX_PARTS = 24; const UPSTREAM_TIMEOUT_MS = 10_000; export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const figure = ( searchParams.get("figure") ?? searchParams.get("look") ?? "" ).trim(); if (!figure) { return NextResponse.json( { error: "Missing figure parameter" }, { status: 400 }, ); } if (figure.length > FIGURE_MAX_LEN || !FIGURE_RE.test(figure)) { return NextResponse.json( { error: "Invalid figure format" }, { status: 400 }, ); } if (figure.split(".").length > FIGURE_MAX_PARTS) { return NextResponse.json( { error: "Figure has too many parts" }, { status: 400 }, ); } const sizeParam = searchParams.get("size"); const size: "s" | "m" | "l" = sizeParam === "s" || sizeParam === "l" ? sizeParam : "m"; const direction = clampInt(searchParams.get("direction"), 0, 7, 2); const headDirection = clampInt( searchParams.get("head_direction") ?? searchParams.get("headDirection"), 0, 7, 3, ); const headOnly = searchParams.get("headonly") === "1" || searchParams.get("headOnly") === "1"; const gesture = searchParams.get("gesture") ?? undefined; const action = searchParams.get("action") ?? undefined; const effect = searchParams.get("effect") ?? undefined; const params = new URLSearchParams({ figure, direction: String(direction), head_direction: String(headDirection), size, img_format: "png", }); if (headOnly) params.set("headonly", "1"); if (gesture) params.set("gesture", gesture); if (action) params.set("action", action); if (effect) params.set("effect", effect); const imgFormat = searchParams.get("img_format"); if (imgFormat) params.set("img_format", imgFormat); const upstream = resolveImagerBase(); const upstreamUrl = `${upstream}?${params.toString()}`; try { const res = await fetch(upstreamUrl, { signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS), headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", }, }); if (!res.ok) { return NextResponse.json( { error: "Upstream imager returned an error" }, { status: res.status }, ); } const buffer = await res.arrayBuffer(); const contentType = res.headers.get("content-type") || "image/png"; return new NextResponse(buffer, { status: 200, headers: { "Content-Type": contentType, "Cache-Control": "public, max-age=3600", "Access-Control-Allow-Origin": "*", }, }); } catch { return NextResponse.json( { error: "Failed to fetch avatar from upstream" }, { status: 502 }, ); } } export async function OPTIONS() { return new NextResponse(null, { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, If-None-Match", }, }); } function clampInt( raw: string | null, min: number, max: number, fallback: number, ): number { if (raw == null) return fallback; const n = Number.parseInt(raw, 10); if (Number.isNaN(n) || n < min || n > max) return fallback; return n; }