| import { NextResponse } from "next/server"; |
| import { NextRequest } from "next/server"; |
| import { getCustomModelList, multiApiKeyPolling } from "@/utils/model"; |
| import { verifySignature } from "@/utils/signature"; |
|
|
| const NODE_ENV = process.env.NODE_ENV; |
| const accessPassword = process.env.ACCESS_PASSWORD || ""; |
| |
| const GOOGLE_GENERATIVE_AI_API_KEY = |
| process.env.GOOGLE_GENERATIVE_AI_API_KEY || ""; |
| const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || ""; |
| const OPENAI_API_KEY = process.env.OPENAI_API_KEY || ""; |
| const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || ""; |
| const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY || ""; |
| const XAI_API_KEY = process.env.XAI_API_KEY || ""; |
| const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY || ""; |
| const AZURE_API_KEY = process.env.AZURE_API_KEY || ""; |
| const OPENAI_COMPATIBLE_API_KEY = process.env.OPENAI_COMPATIBLE_API_KEY || ""; |
| |
| const TAVILY_API_KEY = process.env.TAVILY_API_KEY || ""; |
| const FIRECRAWL_API_KEY = process.env.FIRECRAWL_API_KEY || ""; |
| const EXA_API_KEY = process.env.EXA_API_KEY || ""; |
| const BOCHA_API_KEY = process.env.BOCHA_API_KEY || ""; |
| |
| const DISABLED_AI_PROVIDER = process.env.NEXT_PUBLIC_DISABLED_AI_PROVIDER || ""; |
| const DISABLED_SEARCH_PROVIDER = |
| process.env.NEXT_PUBLIC_DISABLED_SEARCH_PROVIDER || ""; |
| const MODEL_LIST = process.env.NEXT_PUBLIC_MODEL_LIST || ""; |
|
|
| |
| export const config = { |
| matcher: "/api/:path*", |
| }; |
|
|
| const ERRORS = { |
| NO_PERMISSIONS: { |
| code: 403, |
| message: "No permissions", |
| status: "FORBIDDEN", |
| }, |
| NO_API_KEY: { |
| code: 500, |
| message: "The server does not have an API key.", |
| status: "Internal Server Error", |
| }, |
| }; |
|
|
| export async function middleware(request: NextRequest) { |
| if (NODE_ENV === "production") console.debug(request); |
|
|
| const disabledAIProviders = |
| DISABLED_AI_PROVIDER.length > 0 ? DISABLED_AI_PROVIDER.split(",") : []; |
| const disabledSearchProviders = |
| DISABLED_SEARCH_PROVIDER.length > 0 |
| ? DISABLED_SEARCH_PROVIDER.split(",") |
| : []; |
|
|
| const hasDisabledGeminiModel = () => { |
| if (request.method.toUpperCase() === "GET") return false; |
| const { availableModelList, disabledModelList } = getCustomModelList( |
| MODEL_LIST.length > 0 ? MODEL_LIST.split(",") : [] |
| ); |
| const isAvailableModel = availableModelList.some((availableModel) => |
| request.nextUrl.pathname.includes(`models/${availableModel}:`) |
| ); |
| if (isAvailableModel) return false; |
| if (disabledModelList.includes("all")) return true; |
| return disabledModelList.some((disabledModel) => |
| request.nextUrl.pathname.includes(`models/${disabledModel}:`) |
| ); |
| }; |
| const hasDisabledAIModel = async () => { |
| if (request.method.toUpperCase() === "GET") return false; |
| const { model = "" } = await request.json(); |
| const { availableModelList, disabledModelList } = getCustomModelList( |
| MODEL_LIST.length > 0 ? MODEL_LIST.split(",") : [] |
| ); |
| const isAvailableModel = availableModelList.some( |
| (availableModel) => availableModel === model |
| ); |
| if (isAvailableModel) return false; |
| if (disabledModelList.includes("all")) return true; |
| return disabledModelList.some((disabledModel) => disabledModel === model); |
| }; |
|
|
| if (request.nextUrl.pathname.startsWith("/api/ai/google")) { |
| const authorization = request.headers.get("x-goog-api-key") || ""; |
| const isDisabledGeminiModel = hasDisabledGeminiModel(); |
| if ( |
| !verifySignature(authorization, accessPassword, Date.now()) || |
| disabledAIProviders.includes("google") || |
| isDisabledGeminiModel |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const apiKey = multiApiKeyPolling(GOOGLE_GENERATIVE_AI_API_KEY); |
| if (apiKey) { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.set( |
| "x-goog-api-client", |
| request.headers.get("x-goog-api-client") || "genai-js/0.24.0" |
| ); |
| requestHeaders.set("x-goog-api-key", apiKey); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } else { |
| return NextResponse.json( |
| { |
| error: ERRORS.NO_API_KEY, |
| }, |
| { status: 500 } |
| ); |
| } |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/ai/openrouter")) { |
| const authorization = request.headers.get("authorization") || ""; |
| const isDisabledModel = await hasDisabledAIModel(); |
| if ( |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledAIProviders.includes("openrouter") || |
| isDisabledModel |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const apiKey = multiApiKeyPolling(OPENROUTER_API_KEY); |
| if (apiKey) { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.set("Authorization", `Bearer ${apiKey}`); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } else { |
| return NextResponse.json( |
| { |
| error: ERRORS.NO_API_KEY, |
| }, |
| { status: 500 } |
| ); |
| } |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/ai/openaicompatible")) { |
| const authorization = request.headers.get("authorization") || ""; |
| const isDisabledModel = await hasDisabledAIModel(); |
| if ( |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledAIProviders.includes("openaicompatible") || |
| isDisabledModel |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const apiKey = multiApiKeyPolling(OPENAI_COMPATIBLE_API_KEY); |
| if (apiKey) { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.set("Authorization", `Bearer ${apiKey}`); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } else { |
| return NextResponse.json( |
| { |
| error: ERRORS.NO_API_KEY, |
| }, |
| { status: 500 } |
| ); |
| } |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/ai/openai")) { |
| const authorization = request.headers.get("authorization") || ""; |
| const isDisabledModel = await hasDisabledAIModel(); |
| if ( |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledAIProviders.includes("openai") || |
| isDisabledModel |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const apiKey = multiApiKeyPolling(OPENAI_API_KEY); |
| if (apiKey) { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.set("Authorization", `Bearer ${apiKey}`); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } else { |
| return NextResponse.json( |
| { |
| error: ERRORS.NO_API_KEY, |
| }, |
| { status: 500 } |
| ); |
| } |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/ai/anthropic")) { |
| const authorization = request.headers.get("x-api-key") || ""; |
| const isDisabledModel = await hasDisabledAIModel(); |
| if ( |
| !verifySignature(authorization, accessPassword, Date.now()) || |
| disabledAIProviders.includes("anthropic") || |
| isDisabledModel |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const apiKey = multiApiKeyPolling(ANTHROPIC_API_KEY); |
| if (apiKey) { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.set("x-api-key", apiKey); |
| requestHeaders.set( |
| "anthropic-version", |
| request.headers.get("anthropic-version") || "2023-06-01" |
| ); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } else { |
| return NextResponse.json( |
| { |
| error: ERRORS.NO_API_KEY, |
| }, |
| { status: 500 } |
| ); |
| } |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/ai/deepseek")) { |
| const authorization = request.headers.get("authorization") || ""; |
| const isDisabledModel = await hasDisabledAIModel(); |
| if ( |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledAIProviders.includes("deepseek") || |
| isDisabledModel |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const apiKey = multiApiKeyPolling(DEEPSEEK_API_KEY); |
| if (apiKey) { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.set("Authorization", `Bearer ${apiKey}`); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } else { |
| return NextResponse.json( |
| { |
| error: ERRORS.NO_API_KEY, |
| }, |
| { status: 500 } |
| ); |
| } |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/ai/xai")) { |
| const authorization = request.headers.get("authorization") || ""; |
| const isDisabledModel = await hasDisabledAIModel(); |
| if ( |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledAIProviders.includes("xai") || |
| isDisabledModel |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const apiKey = multiApiKeyPolling(XAI_API_KEY); |
| if (apiKey) { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.set("Authorization", `Bearer ${apiKey}`); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } else { |
| return NextResponse.json( |
| { |
| error: ERRORS.NO_API_KEY, |
| }, |
| { status: 500 } |
| ); |
| } |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/ai/mistral")) { |
| const authorization = request.headers.get("authorization") || ""; |
| const isDisabledModel = await hasDisabledAIModel(); |
| if ( |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledAIProviders.includes("mistral") || |
| isDisabledModel |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const apiKey = multiApiKeyPolling(MISTRAL_API_KEY); |
| if (apiKey) { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.set("Authorization", `Bearer ${apiKey}`); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } else { |
| return NextResponse.json( |
| { |
| error: ERRORS.NO_API_KEY, |
| }, |
| { status: 500 } |
| ); |
| } |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/ai/azure")) { |
| const authorization = request.headers.get("authorization") || ""; |
| const isDisabledModel = await hasDisabledAIModel(); |
| if ( |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledAIProviders.includes("azure") || |
| isDisabledModel |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const apiKey = multiApiKeyPolling(AZURE_API_KEY); |
| if (apiKey) { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.set("Authorization", `Bearer ${apiKey}`); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } else { |
| return NextResponse.json( |
| { |
| error: ERRORS.NO_API_KEY, |
| }, |
| { status: 500 } |
| ); |
| } |
| } |
| } |
| |
| if (request.nextUrl.pathname.startsWith("/api/ai/pollinations")) { |
| const authorization = request.headers.get("authorization") || ""; |
| const isDisabledModel = await hasDisabledAIModel(); |
| if ( |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledAIProviders.includes("pollinations") || |
| isDisabledModel |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } |
| } |
| |
| if (request.nextUrl.pathname.startsWith("/api/ai/ollama")) { |
| const authorization = request.headers.get("authorization") || ""; |
| const isDisabledModel = await hasDisabledAIModel(); |
| if ( |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledAIProviders.includes("ollama") || |
| isDisabledModel |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/search/tavily")) { |
| const authorization = request.headers.get("authorization") || ""; |
| if ( |
| request.method.toUpperCase() !== "POST" || |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledSearchProviders.includes("tavily") |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const apiKey = multiApiKeyPolling(TAVILY_API_KEY); |
| if (apiKey) { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.set("Authorization", `Bearer ${apiKey}`); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } else { |
| return NextResponse.json( |
| { |
| error: ERRORS.NO_API_KEY, |
| }, |
| { status: 500 } |
| ); |
| } |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/search/firecrawl")) { |
| const authorization = request.headers.get("authorization") || ""; |
| if ( |
| request.method.toUpperCase() !== "POST" || |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledSearchProviders.includes("firecrawl") |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const apiKey = multiApiKeyPolling(FIRECRAWL_API_KEY); |
| if (apiKey) { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.set("Authorization", `Bearer ${apiKey}`); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } else { |
| return NextResponse.json( |
| { |
| error: ERRORS.NO_API_KEY, |
| }, |
| { status: 500 } |
| ); |
| } |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/search/exa")) { |
| const authorization = request.headers.get("authorization") || ""; |
| if ( |
| request.method.toUpperCase() !== "POST" || |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledSearchProviders.includes("exa") |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const apiKey = multiApiKeyPolling(EXA_API_KEY); |
| if (apiKey) { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.set("Authorization", `Bearer ${apiKey}`); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } else { |
| return NextResponse.json( |
| { |
| error: ERRORS.NO_API_KEY, |
| }, |
| { status: 500 } |
| ); |
| } |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/search/bocha")) { |
| const authorization = request.headers.get("authorization") || ""; |
| if ( |
| request.method.toUpperCase() !== "POST" || |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledSearchProviders.includes("bocha") |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const apiKey = multiApiKeyPolling(BOCHA_API_KEY); |
| if (apiKey) { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.set("Authorization", `Bearer ${apiKey}`); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } else { |
| return NextResponse.json( |
| { |
| error: ERRORS.NO_API_KEY, |
| }, |
| { status: 500 } |
| ); |
| } |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/search/searxng")) { |
| const authorization = request.headers.get("authorization") || ""; |
| if ( |
| request.method.toUpperCase() !== "POST" || |
| !verifySignature( |
| authorization.substring(7), |
| accessPassword, |
| Date.now() |
| ) || |
| disabledSearchProviders.includes("searxng") |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.delete("Authorization"); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/crawler")) { |
| const authorization = request.headers.get("authorization") || ""; |
| if ( |
| request.method.toUpperCase() !== "POST" || |
| !verifySignature(authorization.substring(7), accessPassword, Date.now()) |
| ) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.delete("Authorization"); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/sse")) { |
| let authorization = request.headers.get("authorization") || ""; |
| if (authorization !== "") { |
| authorization = authorization.substring(7); |
| } else if (request.method.toUpperCase() === "GET") { |
| authorization = request.nextUrl.searchParams.get("password") || ""; |
| } |
| if (authorization !== accessPassword) { |
| return NextResponse.json( |
| { error: ERRORS.NO_PERMISSIONS }, |
| { status: 403 } |
| ); |
| } else { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.delete("Authorization"); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } |
| } |
| if (request.nextUrl.pathname.startsWith("/api/mcp")) { |
| const authorization = request.headers.get("authorization") || ""; |
| if (authorization.substring(7) !== accessPassword) { |
| const responseHeaders = new Headers(); |
| responseHeaders.set("WWW-Authenticate", ERRORS.NO_PERMISSIONS.message); |
| return NextResponse.json( |
| { |
| error: 401, |
| error_description: ERRORS.NO_PERMISSIONS.message, |
| error_uri: request.nextUrl, |
| }, |
| { headers: responseHeaders, status: 401 } |
| ); |
| } else { |
| const requestHeaders = new Headers(); |
| requestHeaders.set( |
| "Content-Type", |
| request.headers.get("Content-Type") || "application/json" |
| ); |
| requestHeaders.delete("Authorization"); |
| return NextResponse.next({ |
| request: { |
| headers: requestHeaders, |
| }, |
| }); |
| } |
| } |
| return NextResponse.next(); |
| } |
|
|