162 lines
6.0 KiB
JavaScript
162 lines
6.0 KiB
JavaScript
import { access, readFile } from "node:fs/promises";
|
|
import { createHash } from "node:crypto";
|
|
import { basename, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
|
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
|
const failures = [];
|
|
|
|
const args = process.argv.slice(2);
|
|
const optionValue = (name) => {
|
|
const index = args.indexOf(name);
|
|
return index >= 0 ? args[index + 1] : undefined;
|
|
};
|
|
|
|
const feedPath = resolve(
|
|
frontendDir,
|
|
optionValue("--feed") || process.env.DESKTOP_UPDATE_FEED || "src-tauri/target/release/bundle/latest.json",
|
|
);
|
|
const artifactDir = optionValue("--artifacts-dir") || process.env.DESKTOP_UPDATE_ARTIFACTS_DIR;
|
|
const expectedBaseUrl = optionValue("--base-url") || process.env.DESKTOP_UPDATE_BASE_URL;
|
|
const checksumManifestPath =
|
|
optionValue("--checksum-manifest") ||
|
|
process.env.DESKTOP_UPDATE_CHECKSUM_MANIFEST ||
|
|
(artifactDir ? resolve(artifactDir, "SHA256SUMS.txt") : undefined);
|
|
|
|
const fail = (message) => failures.push(message);
|
|
const assert = (condition, message) => {
|
|
if (!condition) fail(message);
|
|
};
|
|
|
|
const assertFileExists = async (path, description) => {
|
|
try {
|
|
await access(path);
|
|
} catch {
|
|
fail(`${description} does not exist: ${path}`);
|
|
}
|
|
};
|
|
|
|
const sha256 = async (path) => createHash("sha256").update(await readFile(path)).digest("hex");
|
|
|
|
const readChecksumManifest = async () => {
|
|
if (!checksumManifestPath) return undefined;
|
|
try {
|
|
const source = await readFile(checksumManifestPath, "utf8");
|
|
const checksums = new Map();
|
|
for (const line of source.split(/\r?\n/)) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
const match = trimmed.match(/^([a-f0-9]{64})\s+\*?(.+)$/i);
|
|
if (!match) {
|
|
fail(`Checksum manifest contains an invalid line: ${line}`);
|
|
continue;
|
|
}
|
|
checksums.set(basename(match[2]), match[1].toLowerCase());
|
|
}
|
|
return checksums;
|
|
} catch (error) {
|
|
fail(`Cannot read checksum manifest ${checksumManifestPath}: ${error.message}`);
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const assertChecksum = async (checksums, path, description) => {
|
|
if (!checksums) return;
|
|
const name = basename(path);
|
|
const expected = checksums.get(name);
|
|
assert(Boolean(expected), `Checksum manifest must include ${description}: ${name}`);
|
|
if (expected) {
|
|
const actual = await sha256(path);
|
|
assert(actual === expected, `${description} checksum mismatch for ${name}.`);
|
|
}
|
|
};
|
|
|
|
const assertManifestEntries = async (checksums) => {
|
|
if (!checksums || !artifactDir) return;
|
|
for (const [name, expected] of checksums.entries()) {
|
|
const path = resolve(artifactDir, name);
|
|
await assertFileExists(path, `checksum manifest entry ${name}`);
|
|
try {
|
|
const actual = await sha256(path);
|
|
assert(actual === expected, `Checksum manifest entry mismatch for ${name}.`);
|
|
} catch (error) {
|
|
fail(`Cannot verify checksum manifest entry ${name}: ${error.message}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
let feed;
|
|
try {
|
|
feed = JSON.parse(await readFile(feedPath, "utf8"));
|
|
} catch (error) {
|
|
fail(`Cannot read desktop update feed ${feedPath}: ${error.message}`);
|
|
}
|
|
|
|
if (feed) {
|
|
const checksums = await readChecksumManifest();
|
|
await assertManifestEntries(checksums);
|
|
const normalizedFeedVersion = String(feed.version || "").replace(/^v/, "");
|
|
const platforms = feed.platforms || {};
|
|
const darwinArm = platforms["darwin-aarch64"];
|
|
const darwinIntel = platforms["darwin-x86_64"];
|
|
|
|
assert(normalizedFeedVersion === packageInfo.version, `latest.json version must match package version ${packageInfo.version}.`);
|
|
assert(Boolean(feed.pub_date || feed.pubDate), "latest.json must include a publication date.");
|
|
assert(Boolean(darwinArm), "latest.json must include darwin-aarch64.");
|
|
assert(Boolean(darwinIntel), "latest.json must include darwin-x86_64.");
|
|
|
|
const entries = [
|
|
["darwin-aarch64", darwinArm],
|
|
["darwin-x86_64", darwinIntel],
|
|
];
|
|
|
|
for (const [platform, entry] of entries) {
|
|
const rawUrl = entry?.url;
|
|
const signature = entry?.signature;
|
|
assert(typeof signature === "string" && signature.length > 80, `${platform} must include an updater signature.`);
|
|
assert(typeof rawUrl === "string" && rawUrl.length > 0, `${platform} must include an artifact URL.`);
|
|
if (!rawUrl) continue;
|
|
|
|
let url;
|
|
try {
|
|
url = new URL(rawUrl);
|
|
} catch {
|
|
fail(`${platform} artifact URL is invalid: ${rawUrl}`);
|
|
continue;
|
|
}
|
|
|
|
assert(url.protocol === "https:", `${platform} artifact URL must use HTTPS.`);
|
|
assert(!/[?&]token=/i.test(url.search), `${platform} artifact URL must not contain token query parameters.`);
|
|
assert(!url.pathname.endsWith("/latest.json"), `${platform} artifact URL must point to an immutable artifact, not latest.json.`);
|
|
assert(url.pathname.includes(packageInfo.version), `${platform} artifact URL must include version ${packageInfo.version}.`);
|
|
if (expectedBaseUrl) {
|
|
assert(rawUrl.startsWith(expectedBaseUrl), `${platform} artifact URL must start with ${expectedBaseUrl}.`);
|
|
}
|
|
|
|
if (artifactDir) {
|
|
const artifactPath = resolve(artifactDir, basename(url.pathname));
|
|
const signaturePath = `${artifactPath}.sig`;
|
|
await assertFileExists(artifactPath, `${platform} updater artifact`);
|
|
await assertFileExists(signaturePath, `${platform} updater artifact signature`);
|
|
await assertChecksum(checksums, artifactPath, `${platform} updater artifact`);
|
|
await assertChecksum(checksums, signaturePath, `${platform} updater artifact signature`);
|
|
}
|
|
}
|
|
|
|
if (darwinArm?.url && darwinIntel?.url) {
|
|
assert(darwinArm.url === darwinIntel.url, "Universal macOS latest.json must point both darwin architectures at the same artifact.");
|
|
}
|
|
|
|
if (artifactDir) {
|
|
await assertChecksum(checksums, feedPath, "latest.json");
|
|
}
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error(`Desktop update feed check failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
|
process.exitCode = 1;
|
|
} else {
|
|
console.log("Desktop update feed check passed.");
|
|
}
|