import { ProxyError, readBareHeaders } from "./policy"; export type RenderConfig = { RENDER_RELAY_URL?: string; RELAY_SHARED_SECRET?: string; }; const RENDER_HOSTS = [ "discord.com", "discordapp.com", "discordapp.net", "discord.gg", "discord.media", "discordcdn.com", ]; export function renderRelayEndpoint(env: RenderConfig): URL | null { if (!env.RELAY_SHARED_SECRET || env.RELAY_SHARED_SECRET.length < 32 || !env.RENDER_RELAY_URL) return null; let url: URL; try { url = new URL(env.RENDER_RELAY_URL); } catch { throw new ProxyError(503, "RENDER_CONFIG", "Invalid Render relay URL."); } if (url.protocol !== "https:" || url.username || url.password || url.port || url.search || url.hash || (url.pathname !== "/" && url.pathname !== "")) { throw new ProxyError(503, "RENDER_CONFIG", "The Render relay must use a plain HTTPS origin."); } return new URL("/bare/v3/", url); } export function isDiscordRelayHost(target: URL): boolean { const host = target.hostname.toLowerCase(); return RENDER_HOSTS.some(domain => host === domain || host.endsWith("." + domain)); } export function useRenderRelay(target: URL, env: RenderConfig): boolean { return isDiscordRelayHost(target) && renderRelayEndpoint(env) !== null; } // Forward Bare metadata and payloads without ever returning the backend token. // An HTTP 200 with x-bare-status=4xx/5xx is a *valid* upstream response, not // a relay failure; routing must not fail over on those status codes. export async function forwardBareHttp( request: Request, target: URL, url: URL, token: string, timeoutMs = 60_000, unavailableCode = "RENDER_UNAVAILABLE", ): Promise { const safeHeaders = readBareHeaders(request.headers); for (const name of ["accept-language", "user-agent"]) { if (!safeHeaders.has(name) && request.headers.has(name)) safeHeaders.set(name, request.headers.get(name)!); } const serialized = JSON.stringify(Object.fromEntries(safeHeaders)); if (serialized.length > 12_000) throw new ProxyError(431, "INVALID_BARE_HEADER", "Request metadata is too large for the remote relay."); const headers = new Headers({ "x-bare-url": target.href, "x-bare-headers": serialized, "x-relay-token": token, }); // Timeout only the connection / response headers. AbortSignal.timeout() // stays active after fetch() resolves and can truncate streaming JS bundles // long after a healthy relay has sent its headers. const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); let response: Response; try { response = await fetch(url, { method: request.method, headers, body: request.method === "GET" || request.method === "HEAD" ? null : request.body, redirect: "manual", signal: controller.signal, }); } finally { clearTimeout(timeout); } if (!response.ok || !response.headers.has("x-bare-status") || (!response.headers.has("x-bare-headers") && !response.headers.has("x-bare-headers-0"))) { await response.body?.cancel(); throw new ProxyError(502, unavailableCode, "The remote relay is unavailable or rejected the request."); } const output = new Headers({ "content-type": "application/octet-stream", "cache-control": "no-store", "x-content-type-options": "nosniff", }); for (const [name, value] of response.headers) { if (/^x-bare-status(?:-text)?$|^x-bare-headers(?:-\d+)?$/.test(name)) output.set(name, value); } return new Response(request.method === "HEAD" ? null : response.body, { status: 200, headers: output }); } // Keep the private Render token entirely in the Worker. The browser keeps using // the same-origin /bare/v3/ endpoint, so it never sees Render's credentials. export async function renderRelayHttp(request: Request, target: URL, env: RenderConfig): Promise { const url = renderRelayEndpoint(env); if (!url) throw new ProxyError(503, "RENDER_CONFIG", "Render relay is not configured."); return forwardBareHttp(request, target, url, env.RELAY_SHARED_SECRET!); }