import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { createDeploymentHttpServer, listenDeploymentHttpServer, loadDeploymentContract, } from "./deployment-http-server.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); function option(name, fallback) { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : fallback; } function assertSafeArchiveEntries(archive) { const entries = execFileSync("tar", ["-tzf", archive], { encoding: "utf8" }) .split("\n") .filter(Boolean); assert.ok(entries.length > 0, "binary archive is empty"); for (const entry of entries) { assert.ok(!entry.startsWith("/"), `binary archive contains an absolute path: ${entry}`); assert.ok(!entry.split("/").includes(".."), `binary archive contains path traversal: ${entry}`); assert.ok(entry === "blender-web-offline/" || entry.startsWith("blender-web-offline/"), `unexpected binary archive root: ${entry}`); } } const archive = path.resolve(option("--archive", path.join(repoRoot, "release/blender-web-offline.tar.gz"))); const port = Number.parseInt(option("--port", process.env.WEB_TEST_PORT ?? "5189"), 10); assert.ok(fs.statSync(archive).isFile(), `binary archive is missing: ${archive}`); assert.ok(Number.isSafeInteger(port) && port > 0 && port < 65536, `invalid deployment port: ${port}`); assertSafeArchiveEntries(archive); const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "blender-archive-server-")); execFileSync("tar", ["--no-same-owner", "--no-same-permissions", "-xzf", archive, "-C", workspace]); const bundleRoot = path.join(workspace, "blender-web-offline"); const staticRoot = path.join(bundleRoot, "app"); assert.ok(fs.statSync(path.join(staticRoot, "index.html")).isFile(), "extracted binary archive has no app/index.html"); assert.ok(!staticRoot.startsWith(`${repoRoot}${path.sep}`), "archive server must not serve a workspace directory"); const contract = loadDeploymentContract(path.join(bundleRoot, "deployment-contract.json")); const server = createDeploymentHttpServer({ root: staticRoot, contract }); let closing = false; async function close(exitCode = 0) { if (closing) return; closing = true; await new Promise((resolve) => server.close(() => resolve())); fs.rmSync(workspace, { recursive: true, force: true }); process.exit(exitCode); } process.once("SIGINT", () => void close(0)); process.once("SIGTERM", () => void close(0)); process.once("uncaughtException", (error) => { process.stderr.write(`${error.stack ?? error}\n`); void close(1); }); process.once("unhandledRejection", (error) => { process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`); void close(1); }); const origin = await listenDeploymentHttpServer(server, { port }); process.stdout.write(`archive-deployment-server ${origin} root=${staticRoot} archive=${archive}\n`);