TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import "server-only";2import { NextResponse } from "next/server";3import { z } from "zod";4import { getUserFromRequest } from "@/lib/session";5import type { AuthUser } from "@/lib/auth";6import { rateLimit, clientIp } from "@/lib/rate-limit";7import { ids } from "@/lib/ids";8import { log } from "@/lib/log";9import { PolyProviderError } from "@/lib/ai/core/types";10import { ERROR_MESSAGES } from "@/lib/ai/core/errors";1112export class ApiError extends Error {13 constructor(14 public status: number,15 message: string,16 public code = "BAD_REQUEST",17 public details?: unknown,18 ) {19 super(message);20 }21}2223export function json<T>(data: T, init?: ResponseInit) {24 return NextResponse.json(data, init);25}2627export function errorResponse(e: unknown, requestId?: string) {28 if (e instanceof ApiError) {29 return NextResponse.json({ error: { code: e.code, message: e.message, details: e.details, requestId } }, { status: e.status });30 }31 if (e instanceof z.ZodError) {32 return NextResponse.json({ error: { code: "VALIDATION_ERROR", message: "Invalid request", details: e.issues.slice(0, 10), requestId } }, { status: 400 });33 }34 if (e instanceof PolyProviderError) {35 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;36 return NextResponse.json({ error: { code: e.code, message: ERROR_MESSAGES[e.code], detail: e.message, provider: e.provider, requestId } }, { status });37 }38 log.error("unhandled api error", { requestId, error: (e as Error)?.message, stack: (e as Error)?.stack?.split("\n").slice(0, 4).join(" | ") });39 return NextResponse.json({ error: { code: "INTERNAL_ERROR", message: "Something went wrong.", requestId } }, { status: 500 });40}4142export interface Ctx {43 user: AuthUser;44 req: Request;45 requestId: string;46 ip: string;47}4849/**50 * Wraps a route handler with auth (verified user), request id, error mapping, and an51 * optional per-user rate limit.52 */53export function withUser<P = unknown>(54 handler: (ctx: Ctx, params: P) => Promise<Response>,55 opts: { limit?: { max: number; windowMs: number; key?: string }; allowUnverified?: boolean } = {},56) {57 return async (req: Request, routeCtx?: { params: Promise<P> }): Promise<Response> => {58 const requestId = ids.request();59 try {60 const user = await getUserFromRequest(req);61 if (!user) throw new ApiError(401, "Sign in required", "UNAUTHORIZED");62 if (opts.limit) {63 const rl = await rateLimit(`${opts.limit.key ?? new URL(req.url).pathname}:${user.id}`, opts.limit.max, opts.limit.windowMs);64 if (!rl.ok) {65 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)) } });66 }67 }68 const params = (routeCtx ? await routeCtx.params : undefined) as P;69 const res = await handler({ user, req, requestId, ip: clientIp(req) }, params);70 res.headers.set("X-Request-Id", requestId);71 return res;72 } catch (e) {73 return errorResponse(e, requestId);74 }75 };76}7778export async function parseBody<T extends z.ZodTypeAny>(req: Request, schema: T, maxBytes = 1_000_000): Promise<z.infer<T>> {79 const len = Number(req.headers.get("content-length") ?? 0);80 if (len > maxBytes) throw new ApiError(413, "Request body too large", "PAYLOAD_TOO_LARGE");81 let raw: unknown;82 try {83 raw = await req.json();84 } catch {85 throw new ApiError(400, "Invalid JSON body", "INVALID_JSON");86 }87 return schema.parse(raw);88}89