Files
WorkRoot-WRNexusJSComponent/scripts/verify-deployment.ts
T

135 lines
4.5 KiB
TypeScript

import { CORE_DESIGN_SYSTEMS } from "@wrnexus/design-system";
// Exercise the compiled entry point, including package-relative Studio assets.
const reservation = Bun.serve({
port: 0,
hostname: "127.0.0.1",
fetch: () => new Response(),
});
const port = reservation.port!;
reservation.stop(true);
const server = Bun.spawn([process.execPath, "dist/server.js"], {
env: { ...process.env, PORT: String(port) },
stdout: "pipe",
stderr: "pipe",
});
const origin = `http://127.0.0.1:${port}`;
try {
let ready = false;
for (let attempt = 0; attempt < 60; attempt++) {
try {
ready = (await fetch(origin)).ok;
} catch {
/* Wait for the server to listen. */
}
if (ready) break;
await Bun.sleep(100);
}
if (!ready) throw Error("Compiled server did not become ready.");
for (const path of [
"/",
"/components/button",
"/design-studio/",
"/design-studio/studio.js",
"/design-studio/studio.css",
"/design-studio/site-help.js",
]) {
const response = await fetch(origin + path);
if (!response.ok) throw Error(`${path} returned ${response.status}`);
if (
path === "/design-studio/" &&
!(await response.clone().text()).includes("How to use Design Studio")
)
throw Error("Studio's usage guide was not included in the compiled app.");
const policy = response.headers.get("content-security-policy") ?? "";
if (path === "/" && !policy.includes("require-trusted-types-for"))
throw Error(
"Normal component pages lost the production security policy.",
);
if (
path === "/design-studio/" &&
(!policy.includes("frame-ancestors 'none'") ||
policy.includes("require-trusted-types-for"))
)
throw Error("The app replaced Studio's isolated editor security policy.");
}
const application = await (
await fetch(origin + "/design-studio/api/application")
).json();
// Browser module requests carry Origin; the VM forwards them over plain HTTP.
const publicOrigin =
process.env.STUDIO_PUBLIC_ORIGIN ?? "https://component.wrnexusjs.dev";
const proxiedHeaders = {
origin: publicOrigin,
host: new URL(publicOrigin).host,
};
for (const path of [
"/design-studio/studio.js",
"/design-studio/api/application",
]) {
const proxied = await fetch(origin + path, { headers: proxiedHeaders });
if (!proxied.ok)
throw Error(
`HTTPS-origin request over HTTP failed: ${path} (${proxied.status})`,
);
}
const rejected = await fetch(origin + "/design-studio/studio.js", {
headers: { ...proxiedHeaders, origin: "https://attacker.example" },
});
if (rejected.status !== 403)
throw Error("Cross-origin Studio requests were not rejected.");
const generation = await (
await fetch(origin + "/design-studio/api/generation")
).json();
if (
process.env.APIMART_API_KEY?.trim() &&
(generation.provider !== "APIMart" || generation.model !== "gpt-6-luna")
)
throw Error(
"The compiled app did not pick up its server-side AI configuration.",
);
if (application.enabled !== false)
throw Error("Hosted Studio must remain export-only.");
const preview = await fetch(origin + "/design-studio/api/preview", {
method: "POST",
headers: { "content-type": "application/json", origin },
body: JSON.stringify({ system: CORE_DESIGN_SYSTEMS.soft }),
});
if (!preview.ok)
throw Error(
`Studio preview returned ${preview.status}: ${await preview.text()}`,
);
const { url } = await preview.json();
if (
!url.startsWith("/design-studio/preview/") ||
!(await fetch(origin + url)).ok
)
throw Error("Studio preview was not mounted under the app path.");
for (let page = 0; page < 7; page++) {
const gallery = await fetch(origin + "/design-studio/api/preview", {
method: "POST",
headers: { "content-type": "application/json", origin },
body: JSON.stringify({
system: CORE_DESIGN_SYSTEMS.soft,
content: "catalog",
page,
}),
});
if (!gallery.ok)
throw Error(
`Component gallery page ${page + 1} failed: ${await gallery.text()}`,
);
const artifact = await gallery.json();
if (!(await fetch(origin + artifact.url)).ok)
throw Error(`Gallery page ${page + 1} did not render.`);
}
console.log(
"Compiled server serves component pages, Studio assets, export-only APIs and previews on one port.",
);
} finally {
server.kill("SIGTERM");
await server.exited;
const errors = await new Response(server.stderr).text();
if (errors.trim()) console.error(errors);
}