返回 CodeWhale
compiled-hosts.js
根目录 / npm / codewhale / scripts / compiled-hosts.js
1 // Optional release payloads. The Core remains the runtime/authority validator;
2 // this contract only prevents installers from mixing release bytes or silently
3 // substituting an unqualified runtime. No host is selected by default.
4 const crypto = require("node:crypto");
5 const fs = require("node:fs");
6 const path = require("node:path");
7
8 const HOST_CATALOG = "codewhale-extension-hosts.json";
9 const HOST_NAME = "codewhale-extension-host";
10 const TARGETS = Object.freeze({
11 "linux-x64": ["linux", "x64"],
12 "linux-arm64": ["linux", "arm64"],
13 "macos-x64": ["darwin", "x64"],
14 "macos-arm64": ["darwin", "arm64"],
15 "windows-x64": ["win32", "x64"],
16 "windows-arm64": ["win32", "arm64"],
17 });
18 const SHA = /^[a-f0-9]{64}$/;
19 const SOURCE = /^[a-f0-9]{40}$/;
20 const VERSION = /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/;
21 const fail = (message) => { throw new Error(`compiled host: ${message}`); };
22 const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
23
24 function names(target) {
25 if (!TARGETS[target]) fail(`unsupported target ${target}; Android is not a qualified Codewhale delivery target`);
26 const stem = `${HOST_NAME}-${target}`;
27 return {
28 binary: `${stem}${target.startsWith("windows-") ? ".exe" : ""}`,
29 notices: `${stem}-LICENSES.txt`,
30 source: `${stem}-relink-source.tar.gz`,
31 };
32 }
33
34 // Direct protocol cases and actual Rust kernel authority are separate proofs.
35 function compiledMinimum(target) { return target.startsWith("macos-") ? 6 : 5; }
36 function nativeMinimum(target) { return target.startsWith("windows-") ? 9 : 2; }
37
38 function parseCatalog(input, expectedVersion) {
39 if (Buffer.isBuffer(input)) input = input.toString("utf8");
40 if (Buffer.byteLength(typeof input === "string" ? input : JSON.stringify(input)) > 64 * 1024) fail("catalog exceeds 64 KiB");
41 const catalog = typeof input === "string" ? JSON.parse(input) : input;
42 if (!catalog || catalog.schema !== 1 || !VERSION.test(catalog.version) || !SOURCE.test(catalog.source_sha) || !SHA.test(catalog.bundle_sha256) || !Array.isArray(catalog.hosts) || catalog.hosts.length > 6) fail("malformed catalog identity");
43 if (expectedVersion && catalog.version !== expectedVersion) fail(`catalog version ${catalog.version} differs from requested ${expectedVersion}`);
44 const seen = new Set();
45 for (const host of catalog.hosts) {
46 if (!host || !TARGETS[host.target] || seen.has(host.target)) fail("duplicate or unsupported target");
47 seen.add(host.target);
48 const expected = names(host.target);
49 if (host.asset !== expected.binary || host.notices_asset !== expected.notices || host.source_asset !== expected.source) fail("noncanonical asset name");
50 for (const field of ["sha256", "notices_sha256", "source_sha256", "test_log_sha256", "runtime_sha256", "native_log_sha256"]) if (!SHA.test(host[field])) fail(`invalid ${field}`);
51 if (!VERSION.test(host.runtime_version) || !SOURCE.test(host.runtime_revision)) fail("invalid runtime identity");
52 if (!SOURCE.test(host.webkit_revision) || host.bundle_sha256 !== catalog.bundle_sha256 || host.source_commit !== catalog.source_sha) fail("host/source identity disagreement");
53 if (!Number.isSafeInteger(host.passed) || host.passed !== compiledMinimum(host.target) || host.failed !== 0 || host.skipped !== 0 || host.native_platform !== TARGETS[host.target][0] || host.native_arch !== TARGETS[host.target][1]) fail("no successful matching-native compiled-image qualification");
54 if (!Number.isSafeInteger(host.native_passed) || host.native_passed < nativeMinimum(host.target) || host.native_failed !== 0 || host.native_skipped !== 0) fail("no actual Native compiled-image containment and memory qualification");
55 if (host.target.startsWith("linux-") ? !["glibc", "musl"].includes(host.libc) : host.libc !== "none") fail("unknown runtime libc");
56 if (host.license_closure !== "complete" || host.relink_source !== "complete") fail("runtime notices and relinkable corresponding source are required");
57 }
58 return catalog;
59 }
60
61 function assets(catalog) {
62 return catalog ? [HOST_CATALOG, ...parseCatalog(catalog).hosts.flatMap((host) => [host.asset, host.notices_asset, host.source_asset])] : [];
63 }
64
65 function selectedHost(catalog, platform = process.platform, arch = process.arch) {
66 if (platform === "android") fail("Android is not a qualified Codewhale compiled-host delivery target; Node remains available");
67 const entry = parseCatalog(catalog).hosts.find((host) => TARGETS[host.target][0] === platform && TARGETS[host.target][1] === arch);
68 if (!entry) fail(`no qualified image for ${platform}/${arch}; use Node or explicitly qualify a matching local Bun`);
69 return entry;
70 }
71
72 function requested(env = process.env) {
73 return env.CODEWHALE_INSTALL_COMPILED_HOST === "1";
74 }
75
76 function verifyBytes(bytes, expected, name) {
77 if (!SHA.test(expected) || sha256(bytes) !== expected) fail(`SHA256 mismatch for ${name}`);
78 }
79
80 function verifyDirectory(directory, catalog) {
81 for (const host of parseCatalog(catalog).hosts) {
82 for (const [name, digest] of [[host.asset, host.sha256], [host.notices_asset, host.notices_sha256], [host.source_asset, host.source_sha256]]) {
83 const file = path.join(directory, name);
84 const stat = fs.lstatSync(file);
85 if (!stat.isFile() || stat.isSymbolicLink()) fail(`not a regular payload: ${name}`);
86 verifyBytes(fs.readFileSync(file), digest, name);
87 }
88 }
89 }
90
91 module.exports = { HOST_CATALOG, HOST_NAME, TARGETS, assets, names, compiledMinimum, nativeMinimum, parseCatalog, requested, selectedHost, sha256, verifyBytes, verifyDirectory };
92
92 lines JAVASCRIPT