feat: establish NekoNest Cloud control and relay
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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 -`;
|
||||
}
|
||||
Reference in New Issue
Block a user