export declare const playgroundRoutesTemplate = "import { NextResponse } from \"next/server\";\nimport { z } from \"zod\";\nimport { rateLimit } from \"@/utils/rateLimit\";\nimport { matchAllowlist } from \"@/utils/playgroundAllowlist\";\nimport { guardedFetch, BlockedError } from \"@/utils/ssrfGuard\";\nimport { isSiteRequestAuthorized } from \"@/lib/access\";\nimport { readJsonBody, RequestTooLargeError } from \"@/utils/requestBody\";\n\n// The playground proxy runs server-side so it can call APIs that block\n// cross-origin browser requests. It is NOT an open forward proxy: every target\n// is gated by the build-time allowlist (derived from the OpenAPI servers) and\n// then re-validated against private/metadata IP ranges before any socket opens.\nexport const runtime = \"nodejs\";\nexport const maxDuration = 40;\n\nconst MAX_ENVELOPE_BYTES = 1_500_000;\nconst MAX_REQUEST_BODY_BYTES = 1_000_000;\nconst MAX_RESPONSE_BYTES = 5_000_000;\nconst TIMEOUT_MS = 30_000;\n\nconst envelopeSchema = z.object({\n targetUrl: z.string().url().max(4096),\n method: z.enum([\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"HEAD\", \"OPTIONS\"]),\n headers: z.record(z.string().max(256), z.string().max(8192)).optional(),\n body: z.string().optional(),\n bodyEncoding: z.enum([\"utf8\", \"base64\"]).optional(),\n});\n\n// Headers we never forward upstream: hop-by-hop, or identity/cookie headers\n// that would either leak the docs origin or be set incorrectly.\nconst BLOCKED_REQUEST_HEADERS = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n \"host\",\n \"content-length\",\n \"cookie\",\n \"origin\",\n \"referer\",\n \"forwarded\",\n \"x-forwarded-for\",\n \"x-forwarded-host\",\n \"x-forwarded-proto\",\n]);\n\n// The only response headers echoed back to the client. Notably excludes\n// set-cookie, authorization, and www-authenticate.\nconst SAFE_RESPONSE_HEADERS = new Set([\n \"content-type\",\n \"content-length\",\n \"content-language\",\n \"content-disposition\",\n \"cache-control\",\n \"etag\",\n \"last-modified\",\n \"date\",\n \"location\",\n \"retry-after\",\n]);\n\nfunction hasControlChars(value: string): boolean {\n return (\n value.includes(String.fromCharCode(10)) ||\n value.includes(String.fromCharCode(13))\n );\n}\n\nfunction decodePlaygroundBody(\n body: string,\n encoding: \"utf8\" | \"base64\" | undefined,\n): Buffer {\n const decoded = Buffer.from(body, encoding === \"base64\" ? \"base64\" : \"utf8\");\n if (decoded.byteLength > MAX_REQUEST_BODY_BYTES) {\n throw new RequestTooLargeError();\n }\n return decoded;\n}\n\nfunction isTextContentType(contentType: string | null): boolean {\n if (!contentType) return false;\n const t = contentType.toLowerCase();\n return (\n t.startsWith(\"text/\") ||\n t.includes(\"json\") ||\n t.includes(\"xml\") ||\n t.includes(\"javascript\") ||\n t.includes(\"x-www-form-urlencoded\") ||\n t.includes(\"csv\")\n );\n}\n\nexport async function POST(req: Request) {\n if (!(await isSiteRequestAuthorized())) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n }\n\n const { allowed, retryAfter } = rateLimit(req);\n if (!allowed) {\n return NextResponse.json(\n { error: \"Too many requests\" },\n { status: 429, headers: { \"Retry-After\": String(retryAfter) } },\n );\n }\n\n let raw: unknown;\n try {\n raw = await readJsonBody(req, MAX_ENVELOPE_BYTES);\n } catch (error: unknown) {\n if (req.signal.aborted) throw error;\n if (error instanceof RequestTooLargeError) {\n return NextResponse.json(\n { error: \"Request body too large\" },\n { status: 413 },\n );\n }\n return NextResponse.json({ error: \"Invalid JSON body\" }, { status: 400 });\n }\n\n const parsed = envelopeSchema.safeParse(raw);\n if (!parsed.success) {\n return NextResponse.json(\n { error: \"Invalid request\", details: parsed.error.issues },\n { status: 400 },\n );\n }\n\n const { targetUrl, method, headers = {}, body, bodyEncoding } = parsed.data;\n\n const entry = matchAllowlist(targetUrl);\n if (!entry) {\n console.warn(\"[playground] blocked: non-allowlisted target\");\n return NextResponse.json(\n { error: \"This request target is not allowed by the API reference.\" },\n { status: 403 },\n );\n }\n\n if (Object.keys(headers).length > 50) {\n return NextResponse.json({ error: \"Too many headers\" }, { status: 400 });\n }\n\n const outboundHeaders: Record = {};\n for (const [key, value] of Object.entries(headers)) {\n const lower = key.toLowerCase();\n if (BLOCKED_REQUEST_HEADERS.has(lower)) continue;\n if (hasControlChars(key) || hasControlChars(value)) continue;\n outboundHeaders[key] = value;\n }\n outboundHeaders[\"user-agent\"] =\n outboundHeaders[\"user-agent\"] ?? \"Doccupine-API-Playground\";\n // Ask for an unencoded body so the response byte cap is measured accurately.\n outboundHeaders[\"accept-encoding\"] = \"identity\";\n\n let decodedBody: Buffer | undefined;\n try {\n if (body !== undefined) {\n decodedBody = decodePlaygroundBody(body, bodyEncoding);\n }\n } catch (error: unknown) {\n if (error instanceof RequestTooLargeError) {\n return NextResponse.json(\n { error: \"Request payload too large\" },\n { status: 413 },\n );\n }\n throw error;\n }\n const bodyBuffer =\n method !== \"GET\" && method !== \"HEAD\" ? decodedBody : undefined;\n\n try {\n const result = await guardedFetch(targetUrl, entry, {\n method,\n headers: outboundHeaders,\n body: bodyBuffer,\n timeoutMs: TIMEOUT_MS,\n maxBytes: MAX_RESPONSE_BYTES,\n });\n\n const safeHeaders: Record = {};\n let contentType: string | null = null;\n for (const [key, value] of Object.entries(result.headers)) {\n if (key === \"content-type\") contentType = value;\n if (SAFE_RESPONSE_HEADERS.has(key)) safeHeaders[key] = value;\n }\n\n const isText = isTextContentType(contentType);\n\n // Deliberately do NOT log the request/response here: the full URL (path and\n // query) and headers can carry reader secrets. Only blocked/failed attempts\n // are logged below via console.warn, and those record no sensitive values.\n return NextResponse.json({\n status: result.status,\n statusText: result.statusText,\n headers: safeHeaders,\n contentType,\n bodyEncoding: isText ? \"utf8\" : \"base64\",\n body: result.body.toString(isText ? \"utf8\" : \"base64\"),\n truncated: result.truncated,\n byteLength: result.body.length,\n durationMs: result.durationMs,\n });\n } catch (error) {\n if (error instanceof BlockedError) {\n console.warn(\"[playground] \" + error.reason);\n return NextResponse.json(\n { error: \"This request target is not allowed.\" },\n { status: 403 },\n );\n }\n const message = error instanceof Error ? error.message : \"\";\n if (message === \"timeout\") {\n console.warn(\"[playground] upstream timeout\");\n return NextResponse.json(\n { error: \"The upstream request timed out.\" },\n { status: 504 },\n );\n }\n console.warn(\"[playground] upstream request failed\");\n return NextResponse.json(\n { error: \"The upstream request failed.\" },\n { status: 502 },\n );\n }\n}\n";