Files
nekonest-cloud/app/daemon-release.ts
T

139 lines
4.3 KiB
TypeScript

import { DAEMON_RELEASE_POLICY } from "../release/daemon-policy.mjs";
import {
compareStableVersions,
MINIMUM_CLOUD_DAEMON_VERSION,
parseStableVersion,
} from "../release/daemon-version.ts";
export { MINIMUM_CLOUD_DAEMON_VERSION };
type ReleaseEnvironment = Partial<{
NEKONEST_CLOUD_DAEMON_RELEASE_VERSION: string;
NEKONEST_CLOUD_DAEMON_RELEASE_BASE_URL: string;
NEKONEST_CLOUD_DAEMON_WINDOWS_AMD64_SHA256: string;
NEKONEST_CLOUD_DAEMON_LINUX_AMD64_SHA256: string;
NEKONEST_CLOUD_DAEMON_LINUX_ARM64_SHA256: string;
}>;
export type DaemonReleaseAsset = {
platform: "windows" | "linux";
architecture: "amd64" | "arm64";
label: string;
filename: string;
downloadUrl: string;
sha256: string;
};
export type DaemonReleaseState =
| {
available: false;
reason: "not_configured" | "invalid_config" | "incompatible_version";
minimumVersion: string;
}
| {
available: true;
version: string;
minimumVersion: string;
releasePageUrl: string;
checksumsUrl: string;
assets: DaemonReleaseAsset[];
};
const ASSETS = DAEMON_RELEASE_POLICY.assets;
function safeReleaseBaseUrl(raw: string | undefined, version: string): string {
const fallback = `https://github.com/${DAEMON_RELEASE_POLICY.repository}/releases/download/v${version}`;
const parsed = new URL(raw?.trim() || fallback);
if (
parsed.protocol !== "https:" ||
parsed.username ||
parsed.password ||
parsed.search ||
parsed.hash
) {
throw new Error("unsafe_release_base_url");
}
parsed.pathname = parsed.pathname.replace(/\/+$/, "");
return parsed.toString().replace(/\/$/, "");
}
function normalizedChecksum(value: string | undefined): string {
const checksum = value?.trim().toLowerCase() ?? "";
if (!/^[0-9a-f]{64}$/.test(checksum)) throw new Error("invalid_release_checksum");
return checksum;
}
export function parseDaemonReleaseEnvironment(config: ReleaseEnvironment): DaemonReleaseState {
const version = config.NEKONEST_CLOUD_DAEMON_RELEASE_VERSION?.trim() ?? "";
if (!version) {
return {
available: false,
reason: "not_configured",
minimumVersion: MINIMUM_CLOUD_DAEMON_VERSION,
};
}
const parsedVersion = parseStableVersion(version);
const minimumVersion = parseStableVersion(MINIMUM_CLOUD_DAEMON_VERSION);
if (!parsedVersion || !minimumVersion) {
return {
available: false,
reason: "invalid_config",
minimumVersion: MINIMUM_CLOUD_DAEMON_VERSION,
};
}
if (compareStableVersions(parsedVersion, minimumVersion) < 0) {
return {
available: false,
reason: "incompatible_version",
minimumVersion: MINIMUM_CLOUD_DAEMON_VERSION,
};
}
try {
const baseUrl = safeReleaseBaseUrl(
config.NEKONEST_CLOUD_DAEMON_RELEASE_BASE_URL,
version,
);
const assets = ASSETS.map((asset) => ({
platform: asset.platform as DaemonReleaseAsset["platform"],
architecture: asset.architecture as DaemonReleaseAsset["architecture"],
label: asset.label,
filename: asset.filename,
downloadUrl: `${baseUrl}/${asset.filename}`,
sha256: normalizedChecksum(config[asset.checksumEnv as keyof ReleaseEnvironment]),
}));
return {
available: true,
version,
minimumVersion: MINIMUM_CLOUD_DAEMON_VERSION,
releasePageUrl: `https://github.com/${DAEMON_RELEASE_POLICY.repository}/releases/tag/v${version}`,
checksumsUrl: `${baseUrl}/checksums.txt`,
assets,
};
} catch {
return {
available: false,
reason: "invalid_config",
minimumVersion: MINIMUM_CLOUD_DAEMON_VERSION,
};
}
}
export async function getDaemonReleaseState(): Promise<DaemonReleaseState> {
const { env } = await import("cloudflare:workers");
return parseDaemonReleaseEnvironment(env);
}
export function checksumVerificationCommand(asset: DaemonReleaseAsset): string {
if (asset.platform === "windows") {
return [
`$expected = '${asset.sha256}'`,
`$actual = (Get-FileHash -LiteralPath '.\\${asset.filename}' -Algorithm SHA256).Hash.ToLowerInvariant()`,
`if ($actual -ne $expected) { throw 'SHA-256 不匹配,请删除文件' }`,
`Write-Host 'SHA-256 校验通过'`,
].join("\n");
}
return `printf '%s %s\\n' '${asset.sha256}' '${asset.filename}' | sha256sum -c -`;
}