export interface GraphqlRequest { query: string; variables?: Record; operationName?: string; } export interface GraphqlExecutionResult { data?: unknown; errors?: Array<{ message: string; [key: string]: unknown }>; } export interface GraphqlOptions { execute( request: GraphqlRequest, context: { request: Request; signal: AbortSignal }, ): GraphqlExecutionResult | Promise; maxQueryBytes?: number; maxDepth?: number; maxAliases?: number; allowIntrospection?: boolean; } function queryMetrics(query: string) { let depth = 0, maxDepth = 0, aliases = 0; let string = false, escaped = false; for (let index = 0; index < query.length; index++) { const char = query[index]!; if (string) { if (escaped) escaped = false; else if (char === "\\") escaped = true; else if (char === '"') string = false; continue; } if (char === '"') string = true; else if (char === "{") maxDepth = Math.max(maxDepth, ++depth); else if (char === "}") depth--; else if (char === ":" && /[A-Za-z0-9_]\s*$/.test(query.slice(Math.max(0, index - 40), index))) aliases++; } return { depth: maxDepth, aliases }; } export function createGraphqlHandler(options: GraphqlOptions) { return async (request: Request): Promise => { if (request.method !== "POST") return new Response("Method Not Allowed", { status: 405, headers: { allow: "POST" } }); const maxBytes = options.maxQueryBytes ?? 100_000; const length = Number(request.headers.get("content-length") ?? 0); if (length > maxBytes) return Response.json( { errors: [{ message: "GraphQL query is too large" }] }, { status: 413 }, ); const body = (await request.json().catch(() => null)) as GraphqlRequest | null; if ( !body || typeof body.query !== "string" || new TextEncoder().encode(body.query).length > maxBytes ) return Response.json( { errors: [{ message: "A bounded GraphQL query string is required" }] }, { status: 400 }, ); if (options.allowIntrospection === false && /\b__(?:schema|type)\b/.test(body.query)) return Response.json( { errors: [{ message: "GraphQL introspection is disabled" }] }, { status: 403 }, ); const metrics = queryMetrics(body.query); if (metrics.depth > (options.maxDepth ?? 12) || metrics.aliases > (options.maxAliases ?? 50)) return Response.json( { errors: [{ message: "GraphQL query complexity limit exceeded" }] }, { status: 400 }, ); try { const result = await options.execute(body, { request, signal: request.signal }); return Response.json(result, { status: result.errors?.length && result.data === undefined ? 400 : 200, headers: { "cache-control": "no-store" }, }); } catch { return Response.json({ errors: [{ message: "GraphQL execution failed" }] }, { status: 500 }); } }; } export { graphqlPlugin } from "./plugin.ts";