import { createHash } from "node:crypto"; import { readFileSync, readdirSync } from "node:fs"; import { basename, join, resolve } from "node:path"; import { APP_PACKAGE_TARGETS } from "./app-package-targets.mjs"; import { SERVER_PACKAGE_TARGETS } from "./server-package-targets.mjs"; export function readReleaseArtifacts(directory, version, revision) { if (!directory || !/^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version ?? "") || !/^[0-9a-f]{40}$/.test(revision ?? "")) { throw new Error("An artifact directory, stable version and full source SHA are required"); } const root = resolve(directory); const files = new Map(); const packages = []; const servers = []; const targets = new Set(); function asset(name) { if (typeof name !== "string" || basename(name) !== name || !/^[a-zA-Z0-9._-]+$/.test(name)) { throw new Error("Release asset name is invalid"); } const path = join(root, name); if (!files.has(name)) { const bytes = readFileSync(path); files.set(name, { name, path, size: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") }); } return files.get(name); } for (const name of readdirSync(root).filter((name) => name.endsWith(".release.json"))) { const descriptor = JSON.parse(readFileSync(asset(name).path, "utf8")); if (descriptor.schema !== 2 || descriptor.version !== version || descriptor.revision !== revision) { throw new Error(`Release identity mismatch: ${name}`); } const target = descriptor.target ?? "server"; if (targets.has(target)) throw new Error(`Duplicate release target: ${target}`); targets.add(target); const archive = asset(descriptor.artifact); if (archive.sha256 !== descriptor.artifactSha256) { throw new Error(`Release artifact checksum mismatch: ${name}`); } packages.push(archive); const serverTarget = SERVER_PACKAGE_TARGETS.find(candidate => candidate.id === target); if (serverTarget) { if (asset(descriptor.manifest).sha256 !== descriptor.manifestSha256) { throw new Error("Server manifest checksum mismatch"); } servers.push({ ...archive, arch: serverTarget.goarch }); } else if (readFileSync(asset(`${descriptor.artifact}.sha256`).path, "utf8").trim() !== `${descriptor.artifactSha256} ${descriptor.artifact}`) { throw new Error(`App checksum file mismatch: ${target}`); } } const expected = [...SERVER_PACKAGE_TARGETS, ...APP_PACKAGE_TARGETS].map(target => target.id); if (targets.size !== expected.length || expected.some((target) => !targets.has(target))) { throw new Error("Publication requires every registered Server and App target"); } // Sidecars prove the local build; only installable packages are public assets. return { files: packages, servers, targets: [...targets] }; }