#!/usr/bin/env node /** * check-preview-hosts — vite preview 서버의 host 허용 회귀 가드. * * 배포는 `npm run preview`를 cloudflared 터널로 공개 도메인에 연결한다. * vite 6 preview는 Host 헤더를 `preview.allowedHosts`로 검증하므로, 공개 호스트가 * 목록에 없으면 "Blocked request. This host is not allowed." 403만 반환되어 * 배포 도메인 전체가 블랭크가 된다(2026-06-30 회귀). 이 스크립트는 dist를 preview로 * 띄워, 공개 Host는 통과(200, app HTML)하고 임의 Host는 차단되는지 확인한다. * * Node의 fetch는 Host 헤더 오버라이드를 무시하므로 http.request로 직접 Host를 보낸다. * * 사용: node scripts/check-preview-hosts.mjs (dist 빌드가 선행되어야 한다) */ import { spawn } from "node:child_process"; import http from "node:http"; import { setTimeout as sleep } from "node:timers/promises"; const PORT = Number(process.env.PREVIEW_CHECK_PORT ?? 5191); const PUBLIC_HOSTS = ["vnet.18ka.net", "api-vnet.18ka.net", "vignette.chanpaca.net"]; const DISALLOWED_HOST = "evil.example.com"; const BLOCK_MARKER = "Blocked request"; const isWin = process.platform === "win32"; const viteBin = isWin ? "node_modules\\.bin\\vite.cmd" : "node_modules/.bin/vite"; function startPreview() { return spawn(viteBin, ["preview", "--host", "127.0.0.1", "--port", String(PORT)], { stdio: ["ignore", "pipe", "pipe"], shell: isWin, }); } /** http.request로 명시 Host 헤더를 보낸다(fetch는 Host 오버라이드를 무시함). */ function getWithHost(host) { return new Promise((resolve, reject) => { const req = http.request( { hostname: "127.0.0.1", port: PORT, path: "/", method: "GET", headers: { Host: host } }, (res) => { let body = ""; res.on("data", (c) => (body += c)); res.on("end", () => resolve({ status: res.statusCode, text: body })); }, ); req.on("error", reject); req.end(); }); } async function waitForUp(timeoutMs = 20000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { const { status } = await getWithHost("127.0.0.1"); if (status === 200) return true; } catch { // not up yet } await sleep(300); } return false; } function killTree(child) { if (!child || child.killed) return; if (isWin && child.pid) { spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", shell: true }); } else { child.kill("SIGTERM"); } } let preview; let failed = false; try { preview = startPreview(); const up = await waitForUp(); if (!up) { console.error("FAIL preview server did not come up; build dist first (npm run build)"); failed = true; } else { for (const host of PUBLIC_HOSTS) { const { status, text } = await getWithHost(host); const ok = status === 200 && !text.includes(BLOCK_MARKER) && text.includes("