chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,10 @@ import fs from "node:fs";
|
||||
import http from "node:http";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { handleServerOperatorJobRequest, serverOperatorJobOptions } from "./server-operator-job-server.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const authenticatedDeploymentServers = new WeakSet();
|
||||
|
||||
export function loadDeploymentContract(
|
||||
contractPath = path.join(repoRoot, "docs/web/deployment-contract.json"),
|
||||
@@ -78,12 +80,24 @@ function responseHeaders(contract, pathname, filePath, stat) {
|
||||
};
|
||||
}
|
||||
|
||||
export function createDeploymentHttpServer({ root, contract = loadDeploymentContract() }) {
|
||||
export function createDeploymentHttpServer({ root, contract = loadDeploymentContract(), serverOperator = serverOperatorJobOptions(), accessUsername = process.env.WEB_BLENDER_USERNAME || "owner", accessPassword = process.env.WEB_BLENDER_PASSWORD || "" }) {
|
||||
const staticRoot = path.resolve(root);
|
||||
return http.createServer((request, response) => {
|
||||
const expectedAuthorization = accessPassword ? crypto.createHash("sha256").update(`Basic ${Buffer.from(`${accessUsername}:${accessPassword}`).toString("base64")}`).digest() : null;
|
||||
const server = http.createServer((request, response) => {
|
||||
const requestUrl = new URL(request.url ?? "/", "http://deployment.local");
|
||||
const pathname = requestUrl.pathname;
|
||||
const commonHeaders = { ...contract.responseHeaders.allResponses, "Cache-Control": "no-cache" };
|
||||
if (expectedAuthorization && !crypto.timingSafeEqual(expectedAuthorization, crypto.createHash("sha256").update(request.headers.authorization ?? "").digest())) { response.writeHead(401, { ...commonHeaders, "WWW-Authenticate": 'Basic realm="Web Blender", charset="UTF-8"', "Cache-Control": "no-store", "Content-Length": "0" }); response.end(); return; }
|
||||
if (expectedAuthorization && !["GET", "HEAD"].includes(request.method) && request.headers.origin) { let allowedOrigin = false; try { const origin = new URL(request.headers.origin); allowedOrigin = ["http:", "https:"].includes(origin.protocol) && origin.host === request.headers.host; } catch {} if (!allowedOrigin) { response.writeHead(403, { ...commonHeaders, "Cache-Control": "no-store", "Content-Length": "0" }); response.end(); return; } }
|
||||
if (["/api/server-operator-job", "/api/blender-workflow", "/api/blender-session", "/api/blender-resources"].includes(pathname)) {
|
||||
for (const [name, value] of Object.entries(commonHeaders)) response.setHeader(name, value);
|
||||
response.setHeader("Cache-Control", "no-store");
|
||||
void handleServerOperatorJobRequest(request, response, { ...serverOperator, path: pathname }).catch(() => {
|
||||
if (!response.headersSent) response.writeHead(500, { "Content-Length": "0" });
|
||||
response.end();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!contract.methods.includes(request.method)) {
|
||||
response.writeHead(405, { ...commonHeaders, Allow: contract.methods.join(", "), "Content-Length": "0" });
|
||||
response.end();
|
||||
@@ -105,6 +119,7 @@ export function createDeploymentHttpServer({ root, contract = loadDeploymentCont
|
||||
}
|
||||
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
if (expectedAuthorization) { headers["Cache-Control"] = "private, no-store"; headers.Vary = "Authorization"; }
|
||||
const rangeEnabled = contract.rangeRequests.extensions.includes(extension);
|
||||
const etag = strongEtag(filePath);
|
||||
headers.ETag = etag;
|
||||
@@ -142,9 +157,11 @@ export function createDeploymentHttpServer({ root, contract = loadDeploymentCont
|
||||
if (request.method === "HEAD") response.end();
|
||||
else fs.createReadStream(filePath).pipe(response);
|
||||
});
|
||||
if (expectedAuthorization) authenticatedDeploymentServers.add(server); return server;
|
||||
}
|
||||
|
||||
export async function listenDeploymentHttpServer(server, { host = "127.0.0.1", port = 0 } = {}) {
|
||||
if (!["127.0.0.1", "::1"].includes(host) && !authenticatedDeploymentServers.has(server)) throw new Error("Private deployment requires WEB_BLENDER_PASSWORD before non-loopback binding");
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(port, host, () => {
|
||||
|
||||
Reference in New Issue
Block a user