import crypto from "node:crypto"; 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"; const toolsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url))); const projectRoot = path.resolve(toolsRoot, ".."); const docsRoot = path.join(projectRoot, "docs"); const packageRoot = path.join(projectRoot, "web", "offline-package"); const fcstdSource = path.join(projectRoot, "freecad-source", "build-web", "configure", "bin", "WebBox.FCStd"); const sourceRevision = JSON.parse(fs.readFileSync(path.join(packageRoot, "manifest.json"), "utf8")).sourceRevision; const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-web-upgrade-success-")); const previousRoot = path.join(tempRoot, "previous"); const candidateRoot = path.join(tempRoot, "candidate"); const activeRoot = path.join(tempRoot, "active"); const backupRoot = path.join(tempRoot, "backup"); const opfsRoot = path.join(tempRoot, "opfs"); function sha256(filePath) { return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex"); } function canonical(value) { return `${JSON.stringify(value, null, 2)}\n`; } function updateManifest(packageDir, packageVersion) { const versionPath = path.join(packageDir, "version.json"); const version = JSON.parse(fs.readFileSync(versionPath, "utf8")); version.packageVersion = packageVersion; version.generatedAt = "2026-08-19T00:00:00.000Z"; fs.writeFileSync(versionPath, canonical(version)); const manifestPath = path.join(packageDir, "manifest.json"); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); manifest.packageVersion = packageVersion; manifest.generatedAt = "2026-08-19T00:00:00.000Z"; const versionEntry = manifest.files.find((entry) => entry.path === "version.json"); versionEntry.size = fs.statSync(versionPath).size; versionEntry.sha256 = sha256(versionPath); manifest.manifestFile.selfSha256 = null; manifest.manifestFile.selfSha256 = crypto.createHash("sha256").update(canonical(manifest), "utf8").digest("hex"); fs.writeFileSync(manifestPath, canonical(manifest)); } function validatePackage(packageDir) { const manifest = JSON.parse(fs.readFileSync(path.join(packageDir, "manifest.json"), "utf8")); const canonicalManifest = { ...manifest, manifestFile: { ...manifest.manifestFile, selfSha256: null } }; const selfHash = crypto.createHash("sha256").update(canonical(canonicalManifest), "utf8").digest("hex"); if (selfHash !== manifest.manifestFile.selfSha256) throw new Error(`manifest self hash mismatch: ${packageDir}`); for (const entry of manifest.files) { const file = path.join(packageDir, entry.path); if (!fs.existsSync(file)) throw new Error(`manifest file missing: ${entry.path}`); if (fs.statSync(file).size !== entry.size || sha256(file) !== entry.sha256) throw new Error(`manifest hash mismatch: ${entry.path}`); } return manifest; } if (!fs.existsSync(fcstdSource)) throw new Error(`FCStd fixture missing: ${fcstdSource}`); fs.cpSync(packageRoot, previousRoot, { recursive: true }); fs.cpSync(packageRoot, candidateRoot, { recursive: true }); updateManifest(previousRoot, "0.0.9"); const previousManifest = validatePackage(previousRoot); const candidateManifest = validatePackage(candidateRoot); const candidateManifestSha256 = sha256(path.join(candidateRoot, "manifest.json")); fs.mkdirSync(activeRoot, { recursive: true }); fs.cpSync(previousRoot, activeRoot, { recursive: true }); fs.mkdirSync(opfsRoot, { recursive: true }); const documentPath = path.join(opfsRoot, "WebBox.FCStd"); fs.copyFileSync(fcstdSource, documentPath); const documentHashBefore = sha256(documentPath); const documentZipEntries = execFileSync("unzip", ["-Z1", documentPath], { encoding: "utf8" }).trim().split("\n").filter(Boolean); execFileSync("unzip", ["-t", documentPath], { stdio: "ignore" }); const metadataPath = path.join(opfsRoot, "metadata.json"); const metadataBefore = { activeVersion: previousManifest.packageVersion, activeManifestSha256: sha256(path.join(activeRoot, "manifest.json")), document: "WebBox.FCStd", documentSha256: documentHashBefore, }; fs.writeFileSync(metadataPath, canonical(metadataBefore)); fs.renameSync(activeRoot, backupRoot); fs.renameSync(candidateRoot, activeRoot); const activeManifest = validatePackage(activeRoot); const metadataAfter = { ...metadataBefore, activeVersion: activeManifest.packageVersion, activeManifestSha256: sha256(path.join(activeRoot, "manifest.json")), }; fs.writeFileSync(metadataPath, canonical(metadataAfter)); const documentHashAfter = sha256(documentPath); const backupStillValid = validatePackage(backupRoot).packageVersion === previousManifest.packageVersion; const result = { schema: "freecad-web-upgrade-success/v1", sourceRevision, browserPolicy: "Chrome only; no Firefox execution", previousPackage: { version: previousManifest.packageVersion, manifestSha256: metadataBefore.activeManifestSha256 }, candidatePackage: { version: candidateManifest.packageVersion, manifestSha256: candidateManifestSha256 }, activePackage: { version: activeManifest.packageVersion, manifestSha256: metadataAfter.activeManifestSha256, manifestValid: true }, opfsMetadata: { before: metadataBefore, after: metadataAfter }, fcstd: { path: "FreeCAD/freecad-source/build-web/configure/bin/WebBox.FCStd", sha256Before: documentHashBefore, sha256After: documentHashAfter, unchanged: documentHashBefore === documentHashAfter, zipIntegrity: true, entries: documentZipEntries, evidence: "FreeCAD/docs/freecad-fcstd-open-runtime-gate.md", }, rollbackSnapshot: { preserved: backupStillValid, path: "temporary backup/" }, summary: { versionCheck: previousManifest.packageVersion !== candidateManifest.packageVersion && activeManifest.packageVersion === candidateManifest.packageVersion, activePackageValid: true, fcstdReadable: true, fcstdUnchanged: documentHashBefore === documentHashAfter, oldSnapshotPreserved: backupStillValid, result: previousManifest.packageVersion !== candidateManifest.packageVersion && backupStillValid && documentHashBefore === documentHashAfter ? "pass_compatible_upgrade_and_document_preserved" : "fail_compatible_upgrade", }, }; fs.writeFileSync(path.join(docsRoot, "freecad-web-upgrade-success.json"), `${JSON.stringify(result, null, 2)}\n`); console.log(`upgrade ${previousManifest.packageVersion} -> ${activeManifest.packageVersion}; FCStd unchanged=${result.summary.fcstdUnchanged}`); console.log(`result=${result.summary.result}`); if (result.summary.result.startsWith("fail")) process.exitCode = 1;