SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
3.6 KB · 89 lines typescript
Raw Blame History
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