import "server-only"; import { NextResponse } from "next/server"; import { z } from "zod"; import { getUserFromRequest } from "@/lib/session"; import type { AuthUser } from "@/lib/auth"; import { rateLimit, clientIp } from "@/lib/rate-limit"; import { ids } from "@/lib/ids"; import { log } from "@/lib/log"; import { PolyProviderError } from "@/lib/ai/core/types"; import { ERROR_MESSAGES } from "@/lib/ai/core/errors"; export class ApiError extends Error { constructor( public status: number, message: string, public code = "BAD_REQUEST", public details?: unknown, ) { super(message); } } export function json(data: T, init?: ResponseInit) { return NextResponse.json(data, init); } export function errorResponse(e: unknown, requestId?: string) { if (e instanceof ApiError) { return NextResponse.json({ error: { code: e.code, message: e.message, details: e.details, requestId } }, { status: e.status }); } if (e instanceof z.ZodError) { return NextResponse.json({ error: { code: "VALIDATION_ERROR", message: "Invalid request", details: e.issues.slice(0, 10), requestId } }, { status: 400 }); } if (e instanceof PolyProviderError) { const status = e.code === "INVALID_API_KEY" ? 401 : e.code === "RATE_LIMITED" ? 429 : e.code === "MODEL_NOT_FOUND" ? 404 : e.code === "INVALID_PARAMETER" || e.code === "CONTEXT_TOO_LONG" ? 400 : 502; return NextResponse.json({ error: { code: e.code, message: ERROR_MESSAGES[e.code], detail: e.message, provider: e.provider, requestId } }, { status }); } log.error("unhandled api error", { requestId, error: (e as Error)?.message, stack: (e as Error)?.stack?.split("\n").slice(0, 4).join(" | ") }); return NextResponse.json({ error: { code: "INTERNAL_ERROR", message: "Something went wrong.", requestId } }, { status: 500 }); } export interface Ctx { user: AuthUser; req: Request; requestId: string; ip: string; } /** * Wraps a route handler with auth (verified user), request id, error mapping, and an * optional per-user rate limit. */ export function withUser

( handler: (ctx: Ctx, params: P) => Promise, opts: { limit?: { max: number; windowMs: number; key?: string }; allowUnverified?: boolean } = {}, ) { return async (req: Request, routeCtx?: { params: Promise

}): Promise => { const requestId = ids.request(); try { const user = await getUserFromRequest(req); if (!user) throw new ApiError(401, "Sign in required", "UNAUTHORIZED"); if (opts.limit) { const rl = await rateLimit(`${opts.limit.key ?? new URL(req.url).pathname}:${user.id}`, opts.limit.max, opts.limit.windowMs); if (!rl.ok) { return NextResponse.json({ error: { code: "RATE_LIMITED", message: "Too many requests. Slow down a little.", requestId } }, { status: 429, headers: { "Retry-After": String(Math.ceil(rl.retryAfterMs / 1000)) } }); } } const params = (routeCtx ? await routeCtx.params : undefined) as P; const res = await handler({ user, req, requestId, ip: clientIp(req) }, params); res.headers.set("X-Request-Id", requestId); return res; } catch (e) { return errorResponse(e, requestId); } }; } export async function parseBody(req: Request, schema: T, maxBytes = 1_000_000): Promise> { const len = Number(req.headers.get("content-length") ?? 0); if (len > maxBytes) throw new ApiError(413, "Request body too large", "PAYLOAD_TOO_LARGE"); let raw: unknown; try { raw = await req.json(); } catch { throw new ApiError(400, "Invalid JSON body", "INVALID_JSON"); } return schema.parse(raw); }