import { type NextApiRequest, type NextApiResponse } from "next"; import { type ZodType, type z } from "zod/v4"; import { ApiAuthService } from "@/src/features/public-api/server/apiAuth"; import { prisma } from "@langfuse/shared/src/db"; import { redis, type AuthHeaderValidVerificationResult, traceException, logger, } from "@langfuse/shared/src/server"; import { type RateLimitResource } from "@langfuse/shared"; import { RateLimitService } from "@/src/features/public-api/server/RateLimitService"; import { contextWithLangfuseProps } from "@langfuse/shared/src/server"; import * as opentelemetry from "@opentelemetry/api"; import { env } from "@/src/env.mjs"; type RouteConfig< TQuery extends ZodType, TBody extends ZodType, TResponse extends ZodType, > = { name: string; querySchema?: TQuery; bodySchema?: TBody; responseSchema: TResponse; successStatusCode?: number; rateLimitResource?: z.infer; // defaults to public-api fn: (params: { query: z.infer; body: z.infer; req: NextApiRequest; res: NextApiResponse; auth: AuthHeaderValidVerificationResult & { scope: { projectId: string; accessLevel: "project" }; }; }) => Promise>; }; export const createAuthedProjectAPIRoute = < TQuery extends ZodType, TBody extends ZodType, TResponse extends ZodType, >( routeConfig: RouteConfig, ): ((req: NextApiRequest, res: NextApiResponse) => Promise) => { return async (req: NextApiRequest, res: NextApiResponse) => { const auth = await new ApiAuthService( prisma, redis, ).verifyAuthHeaderAndReturnScope(req.headers.authorization); if (!auth.validKey) { res.status(401).json({ message: auth.error }); return; } if (auth.scope.accessLevel !== "project") { res.status(401).json({ message: "Access denied - need to use basic auth with secret key", }); return; } if (!auth.scope.projectId) { res.status(401).json({ message: "Project ID not found for API token. Are you using an organization key?", }); return; } const rateLimitResponse = await RateLimitService.getInstance().rateLimitRequest( auth.scope, routeConfig.rateLimitResource || "public-api", ); if (rateLimitResponse?.isRateLimited()) { return rateLimitResponse.sendRestResponseIfLimited(res); } logger.debug( `Request to route ${routeConfig.name} projectId ${auth.scope.projectId}`, { query: req.query, body: req.body, }, ); const query = routeConfig.querySchema ? routeConfig.querySchema.parse(req.query) : ({} as z.infer); const body = routeConfig.bodySchema ? routeConfig.bodySchema.parse(req.body) : ({} as z.infer); const ctx = contextWithLangfuseProps({ headers: req.headers, projectId: auth.scope.projectId, }); return opentelemetry.context.with(ctx, async () => { const response = await routeConfig.fn({ query, body, req, res, auth: auth as AuthHeaderValidVerificationResult & { scope: { projectId: string; accessLevel: "project" }; }, }); if (env.NODE_ENV === "development" && routeConfig.responseSchema) { const parsingResult = routeConfig.responseSchema.safeParse(response); if (!parsingResult.success) { logger.error("Response validation failed:", parsingResult.error); traceException(parsingResult.error); } } res .status( // Check whether status code was already set inside handler to non default value res.statusCode !== 200 ? res.statusCode : routeConfig.successStatusCode || 200, ) .json(response || { message: "OK" }); }); }; };