import { NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { getKittyCadProviderConfig } from "@/lib/kittycad/server"; export const dynamic = "force-dynamic"; export const maxDuration = 60; export const runtime = "nodejs"; const HOP_BY_HOP_HEADERS = new Set([ "connection", "content-length", "host", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", ]); const BLOCKED_PATH_PREFIXES = [ "/admin", "/internal", "/logout", "/oauth2", "/org/api-tokens", "/user/api-tokens", "/users", ]; type RouteContext = { params: Promise<{ path: string[] }>; }; function isBlockedPath(pathname: string) { return BLOCKED_PATH_PREFIXES.some( (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`) ); } function copyRequestHeaders(request: Request, apiKey: string) { const headers = new Headers(); for (const [name, value] of request.headers.entries()) { const normalizedName = name.toLowerCase(); if ( HOP_BY_HOP_HEADERS.has(normalizedName) || normalizedName === "authorization" || normalizedName === "cookie" || normalizedName === "origin" || normalizedName === "referer" || normalizedName.startsWith("x-forwarded-") || normalizedName === "x-hicad-ws-proxy-secret" ) { continue; } headers.set(name, value); } headers.set("Authorization", `Bearer ${apiKey}`); headers.set("Accept", request.headers.get("accept") || "application/json"); return headers; } function copyResponseHeaders(upstream: Response) { const headers = new Headers(); for (const [name, value] of upstream.headers.entries()) { const normalizedName = name.toLowerCase(); if ( HOP_BY_HOP_HEADERS.has(normalizedName) || normalizedName === "content-encoding" || normalizedName === "set-cookie" ) { continue; } headers.set(name, value); } headers.set("Cache-Control", "private, no-store"); return headers; } async function proxyZooRequest(request: Request, context: RouteContext) { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "未登录" }, { status: 401 }); } const { path } = await context.params; const pathname = `/${path.map(encodeURIComponent).join("/")}`; if (isBlockedPath(pathname)) { return NextResponse.json( { error: "该 Zoo API 不允许通过 HiCAD 代理访问" }, { status: 403 } ); } try { const config = await getKittyCadProviderConfig(); const incomingUrl = new URL(request.url); const baseUrl = config.baseUrl.replace(/\/+$/, ""); const upstreamUrl = `${baseUrl}${pathname}${incomingUrl.search}`; const method = request.method.toUpperCase(); const hasBody = method !== "GET" && method !== "HEAD"; const init: RequestInit & { duplex?: "half" } = { body: hasBody ? request.body : undefined, cache: "no-store", duplex: hasBody ? "half" : undefined, headers: copyRequestHeaders(request, config.apiKey), method, redirect: "manual", signal: request.signal, }; const upstream = await fetch(upstreamUrl, init); return new Response(upstream.body, { headers: copyResponseHeaders(upstream), status: upstream.status, statusText: upstream.statusText, }); } catch (error) { return NextResponse.json( { error: error instanceof Error ? error.message : "Zoo API 代理请求失败", }, { status: 502 } ); } } export const DELETE = proxyZooRequest; export const GET = proxyZooRequest; export const HEAD = proxyZooRequest; export const OPTIONS = proxyZooRequest; export const PATCH = proxyZooRequest; export const POST = proxyZooRequest; export const PUT = proxyZooRequest;