Files
workinf_Blender_Wasm/tools/web/server-job-result-binding.mjs
mes123456 380cbed4ff
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Checkpoint web parity through Chromium input tasks
2026-08-19 10:39:03 -04:00

78 lines
4.9 KiB
JavaScript

import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
export const SERVER_JOB_RESULT_SCHEMA = 1;
const DIGEST = /^[a-f0-9]{64}$/;
function invalid(message) { throw new Error(`SERVER_JOB_RESULT_INVALID: ${message}`); }
function digest(value, field) {
if (typeof value !== "string" || !DIGEST.test(value)) invalid(`${field} hash is invalid`);
return value;
}
function revision(value, field) {
if (!Number.isSafeInteger(value) || value < 0) invalid(`${field} is invalid`);
return value;
}
function hashBytes(bytes) { return crypto.createHash("sha256").update(bytes).digest("hex"); }
async function hashFile(file) { return hashBytes(await fs.readFile(file)); }
export function createServerJobResultIdentity(value) {
if (!value || typeof value !== "object") invalid("identity is invalid");
const requestId = typeof value.requestId === "string" && /^[A-Za-z0-9:_-]{1,128}$/.test(value.requestId) ? value.requestId : invalid("requestId is invalid");
const projectId = typeof value.projectId === "string" && /^[A-Za-z0-9:_-]{1,128}$/.test(value.projectId) ? value.projectId : invalid("projectId is invalid");
return Object.freeze({
schemaVersion: SERVER_JOB_RESULT_SCHEMA,
requestId,
projectId,
baseRevision: revision(value.baseRevision, "baseRevision"),
sourceSha256: digest(value.sourceSha256, "source"),
settingsSha256: digest(value.settingsSha256, "settings"),
buildSha256: digest(value.buildSha256, "build"),
});
}
export async function commitServerJobResult(identityValue, outputBytes, options = {}) {
const identity = createServerJobResultIdentity(identityValue);
if (options.expectedIdentity !== undefined) {
const expected = createServerJobResultIdentity(options.expectedIdentity);
for (const field of ["requestId", "projectId", "baseRevision", "sourceSha256", "settingsSha256", "buildSha256"]) {
if (identity[field] !== expected[field]) throw new Error(`SERVER_JOB_RESULT_IDENTITY_MISMATCH: ${field} does not match the request`);
}
}
if (!(outputBytes instanceof Uint8Array) || outputBytes.byteLength < 1) invalid("output bytes are empty");
const outputSha256 = hashBytes(outputBytes);
const outputByteLength = outputBytes.byteLength;
const outputDirectory = options.outputDirectory;
if (typeof outputDirectory !== "string" || !path.isAbsolute(outputDirectory)) invalid("output directory is invalid");
await fs.mkdir(outputDirectory, { recursive: true, mode: 0o700 });
const fileName = `${identity.requestId}.result`;
const target = path.join(outputDirectory, fileName);
const temporary = path.join(outputDirectory, `.${fileName}.${crypto.randomUUID()}.tmp`);
try {
await fs.writeFile(temporary, outputBytes, { flag: "wx", mode: 0o600 });
const staged = await fs.readFile(temporary);
if (staged.byteLength !== outputByteLength || hashBytes(staged) !== outputSha256) invalid("staged output readback mismatch");
if (options.faultAt === "AFTER_STAGE") throw new Error("SERVER_JOB_RESULT_STORAGE: injected failure after stage");
if (options.faultAt === "QUOTA") throw new Error("SERVER_JOB_RESULT_STORAGE_QUOTA: output quota exceeded");
await fs.rename(temporary, target);
const persisted = await fs.readFile(target);
if (persisted.byteLength !== outputByteLength || hashBytes(persisted) !== outputSha256) invalid("committed output readback mismatch");
return Object.freeze({ schemaVersion: SERVER_JOB_RESULT_SCHEMA, status: "COMMITTED", requestId: identity.requestId, projectId: identity.projectId, baseRevision: identity.baseRevision, sourceSha256: identity.sourceSha256, settingsSha256: identity.settingsSha256, buildSha256: identity.buildSha256, outputSha256, outputByteLength, outputPath: target, publish: true, execution: "DISABLED" });
} catch (error) {
await fs.rm(temporary, { force: true });
if (error instanceof Error && (error.message.startsWith("SERVER_JOB_RESULT_") || error.message.startsWith("SERVER_JOB_RESULT_STORAGE"))) throw error;
throw new Error(`SERVER_JOB_RESULT_STORAGE: ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function verifyServerJobResultReceipt(receipt, expected, outputDirectory) {
if (!receipt || receipt.schemaVersion !== SERVER_JOB_RESULT_SCHEMA || receipt.status !== "COMMITTED" || receipt.publish !== true) invalid("receipt is not committed");
const identity = createServerJobResultIdentity(expected);
for (const field of ["requestId", "projectId", "baseRevision", "sourceSha256", "settingsSha256", "buildSha256"]) if (receipt[field] !== identity[field]) invalid(`${field} identity mismatch`);
const target = path.join(outputDirectory, `${identity.requestId}.result`);
const bytes = await fs.readFile(target);
if (bytes.byteLength !== receipt.outputByteLength || hashBytes(bytes) !== receipt.outputSha256) throw new Error("SERVER_JOB_RESULT_HASH_MISMATCH: committed output changed");
return Object.freeze({ ...receipt, verified: true });
}