| 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 |