export declare const ssrfGuardTemplate = "// SERVER-ONLY. Imported solely by app/api/playground/route.ts (a route handler,\n// which never ships to the client). Performs SSRF-safe outbound requests for\n// the API playground proxy: it resolves the target host, rejects any address in\n// a private/loopback/link-local/metadata range, and pins the connection to the\n// pre-validated IPs so DNS rebinding between check and connect cannot redirect\n// the socket to an internal address.\n\nimport dns from \"node:dns/promises\";\nimport net from \"node:net\";\nimport http from \"node:http\";\nimport https from \"node:https\";\nimport type { LookupAddress } from \"node:dns\";\nimport type { LookupFunction } from \"node:net\";\nimport type { AllowlistEntry } from \"@/utils/playgroundAllowlist\";\n\nexport class BlockedError extends Error {\n constructor(public reason: string) {\n super(reason);\n this.name = \"BlockedError\";\n }\n}\n\nexport interface GuardedInit {\n method: string;\n headers: Record;\n body?: Buffer;\n timeoutMs: number;\n maxBytes: number;\n}\n\nexport interface GuardedResult {\n status: number;\n statusText: string;\n headers: Record;\n body: Buffer;\n truncated: boolean;\n durationMs: number;\n}\n\nfunction parseIpv4(ip: string): number[] | null {\n const parts = ip.split(\".\");\n if (parts.length !== 4) return null;\n const nums = parts.map((p) => Number(p));\n if (nums.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;\n return nums;\n}\n\nfunction isBlockedIpv4(ip: string): boolean {\n const nums = parseIpv4(ip);\n if (!nums) return true;\n const a = nums[0];\n const b = nums[1];\n const c = nums[2];\n if (a === 0 || a === 10 || a === 127) return true;\n if (a === 169 && b === 254) return true; // link-local incl. metadata\n if (a === 172 && b >= 16 && b <= 31) return true;\n if (a === 192 && b === 168) return true;\n if (a === 192 && b === 0 && c === 0) return true;\n if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT\n if (a === 198 && (b === 18 || b === 19)) return true; // benchmarking\n if (a >= 224) return true; // multicast + reserved + broadcast\n return false;\n}\n\nfunction parseIpv6Words(ip: string): number[] | null {\n let clean = ip.toLowerCase().split(\"%\")[0];\n if (clean.includes(\".\")) {\n const separator = clean.lastIndexOf(\":\");\n const ipv4 = parseIpv4(clean.slice(separator + 1));\n if (separator < 0 || !ipv4) return null;\n clean =\n clean.slice(0, separator) +\n \":\" +\n ((ipv4[0] << 8) | ipv4[1]).toString(16) +\n \":\" +\n ((ipv4[2] << 8) | ipv4[3]).toString(16);\n }\n\n const halves = clean.split(\"::\");\n if (halves.length > 2) return null;\n const parseHalf = (half: string): number[] | null => {\n if (!half) return [];\n const segments = half.split(\":\");\n if (segments.some((segment) => !/^[0-9a-f]{1,4}$/.test(segment))) {\n return null;\n }\n return segments.map((segment) => parseInt(segment, 16));\n };\n const left = parseHalf(halves[0]);\n const right = parseHalf(halves[1] ?? \"\");\n if (!left || !right) return null;\n\n if (halves.length === 1) return left.length === 8 ? left : null;\n const omitted = 8 - left.length - right.length;\n if (omitted < 1) return null;\n return [...left, ...Array(omitted).fill(0), ...right];\n}\n\nfunction ipv4FromWords(words: number[]): string {\n return [words[6] >> 8, words[6] & 255, words[7] >> 8, words[7] & 255].join(\n \".\",\n );\n}\n\nfunction translatedIpv4(words: number[]): string | null {\n const firstFourZero = words.slice(0, 4).every((word) => word === 0);\n const firstFiveZero = firstFourZero && words[4] === 0;\n\n // IPv4-compatible (::/96), mapped (::ffff:0:0/96), and translated\n // (::ffff:0:0:0/96) forms all carry the IPv4 address in the final 32 bits.\n if (\n (firstFiveZero && (words[5] === 0 || words[5] === 0xffff)) ||\n (firstFourZero && words[4] === 0xffff && words[5] === 0)\n ) {\n return ipv4FromWords(words);\n }\n\n // RFC 6052 well-known NAT64 prefix 64:ff9b::/96.\n if (\n words[0] === 0x64 &&\n words[1] === 0xff9b &&\n words.slice(2, 6).every((word) => word === 0)\n ) {\n return ipv4FromWords(words);\n }\n\n return null;\n}\n\nfunction isNat64LocalUse(words: number[]): boolean {\n // RFC 8215 reserves the entire 64:ff9b:1::/48 prefix for local translation.\n return words[0] === 0x64 && words[1] === 0xff9b && words[2] === 1;\n}\n\nfunction isBlockedIpv6(ip: string): boolean {\n const words = parseIpv6Words(ip);\n if (!words) return true;\n if (isNat64LocalUse(words)) return true;\n\n const translated = translatedIpv4(words);\n if (translated) return isBlockedIpv4(translated);\n\n const allZero = words.every((word) => word === 0);\n const loopback =\n words.slice(0, 7).every((word) => word === 0) && words[7] === 1;\n if (allZero || loopback) return true;\n\n const head = words[0];\n if ((head & 0xffc0) === 0xfe80 || (head & 0xffc0) === 0xfec0) {\n return true; // link-local fe80::/10 + deprecated site-local fec0::/10\n }\n if ((head & 0xfe00) === 0xfc00) return true; // unique-local fc00::/7\n if ((head & 0xff00) === 0xff00) return true; // multicast ff00::/8\n return false;\n}\n\nexport function isBlockedIp(ip: string): boolean {\n const type = net.isIP(ip);\n if (type === 4) return isBlockedIpv4(ip);\n if (type === 6) return isBlockedIpv6(ip);\n return true; // not a parseable IP -> fail closed\n}\n\n/**\n * Ranges that remain blocked even for an explicitly loopback-enabled entry.\n * In particular, link-local addresses include cloud instance metadata services\n * and must never become reachable through an imported OpenAPI document.\n */\nfunction isAlwaysBlockedIpv4(ip: string): boolean {\n const nums = parseIpv4(ip);\n if (!nums) return true;\n const [a, b] = nums;\n return a === 0 || (a === 169 && b === 254) || a >= 224;\n}\n\nexport function isAlwaysBlockedIp(ip: string): boolean {\n const type = net.isIP(ip);\n if (type === 4) return isAlwaysBlockedIpv4(ip);\n if (type === 6) {\n const words = parseIpv6Words(ip);\n if (!words || isNat64LocalUse(words)) return true;\n const translated = translatedIpv4(words);\n if (translated) return isAlwaysBlockedIpv4(translated);\n\n const head = words[0];\n return (\n words.every((word) => word === 0) ||\n (head & 0xffc0) === 0xfe80 ||\n (head & 0xffc0) === 0xfec0 ||\n (head & 0xff00) === 0xff00\n );\n }\n return true;\n}\n\nfunction flattenHeaders(\n headers: http.IncomingHttpHeaders,\n): Record {\n const out: Record = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value === undefined) continue;\n out[key.toLowerCase()] = Array.isArray(value) ? value.join(\", \") : value;\n }\n return out;\n}\n\nexport async function guardedFetch(\n targetUrl: string,\n entry: AllowlistEntry,\n init: GuardedInit,\n): Promise {\n const url = new URL(targetUrl);\n let host = url.hostname.toLowerCase();\n if (host.endsWith(\".\")) host = host.slice(0, -1);\n host = host.replace(/^\\[|\\]$/g, \"\");\n\n const literalType = net.isIP(host);\n let validated: LookupAddress[];\n // Loopback opt-in exists only for local development. Production always\n // applies the complete private-address block regardless of generated data.\n const allowPrivate =\n process.env.NODE_ENV !== \"production\" && entry.allowPrivate === true;\n\n if (literalType !== 0) {\n // Raw-IP target: only allowed if the spec itself declared this host, and\n // only into a private range when it opted in via allowPrivate.\n if (isAlwaysBlockedIp(host) || (isBlockedIp(host) && !allowPrivate)) {\n throw new BlockedError(\"blocked: private IP-literal target\");\n }\n validated = [{ address: host, family: literalType }];\n } else {\n let records: LookupAddress[];\n try {\n records = await dns.lookup(host, { all: true, verbatim: true });\n } catch {\n throw new BlockedError(\"blocked: DNS resolution failed\");\n }\n if (records.length === 0) throw new BlockedError(\"blocked: no DNS records\");\n for (const record of records) {\n if (\n isAlwaysBlockedIp(record.address) ||\n (isBlockedIp(record.address) && !allowPrivate)\n ) {\n throw new BlockedError(\"blocked: private IP\");\n }\n }\n validated = records;\n }\n\n const isHttps = url.protocol === \"https:\";\n const transport = isHttps ? https : http;\n\n // Pin resolution to the addresses we just validated so a re-resolution\n // between check and connect (DNS rebinding) cannot swap in a private IP.\n const lookup: LookupFunction = (_hostname, options, callback) => {\n if (options && options.all) {\n callback(null, validated);\n } else {\n callback(null, validated[0].address, validated[0].family);\n }\n };\n\n const requestOptions: https.RequestOptions = {\n method: init.method,\n headers: init.headers,\n lookup,\n timeout: init.timeoutMs,\n };\n\n return await new Promise((resolve, reject) => {\n const start = Date.now();\n let settled = false;\n const done = (fn: () => void) => {\n if (settled) return;\n settled = true;\n fn();\n };\n\n const req = transport.request(url, requestOptions, (res) => {\n const chunks: Buffer[] = [];\n let received = 0;\n let truncated = false;\n\n res.on(\"data\", (chunk: Buffer) => {\n received += chunk.length;\n if (received > init.maxBytes) {\n truncated = true;\n res.destroy();\n return;\n }\n chunks.push(chunk);\n });\n\n const finish = () =>\n done(() =>\n resolve({\n status: res.statusCode ?? 0,\n statusText: res.statusMessage ?? \"\",\n headers: flattenHeaders(res.headers),\n body: Buffer.concat(chunks),\n truncated,\n durationMs: Date.now() - start,\n }),\n );\n\n res.on(\"end\", finish);\n res.on(\"close\", finish);\n res.on(\"error\", () => done(() => reject(new Error(\"stream error\"))));\n });\n\n req.on(\"timeout\", () => {\n req.destroy(new Error(\"timeout\"));\n });\n req.on(\"error\", (err) => done(() => reject(err)));\n\n if (init.body && init.method !== \"GET\" && init.method !== \"HEAD\") {\n req.write(init.body);\n }\n req.end();\n });\n}\n";