feat: establish NekoNest Cloud control and relay
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import test from "node:test";
|
||||
import { DAEMON_RELEASE_POLICY } from "../release/daemon-policy.mjs";
|
||||
import {
|
||||
ReleaseVerificationError,
|
||||
formatCloudReleaseEnvironment,
|
||||
parseReleaseTag,
|
||||
verifyDaemonRelease,
|
||||
} from "../release/verify-daemon-release-core.mjs";
|
||||
|
||||
const TAG = "v0.2.6";
|
||||
const API_URL = `https://api.github.com/repos/${DAEMON_RELEASE_POLICY.repository}/releases/tags/${TAG}`;
|
||||
const RELEASE_BASE = `https://github.com/${DAEMON_RELEASE_POLICY.repository}/releases/download/${TAG}`;
|
||||
|
||||
function hash(bytes) {
|
||||
return createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
const bodies = new Map(
|
||||
DAEMON_RELEASE_POLICY.assets.map((asset, index) => [
|
||||
asset.filename,
|
||||
Buffer.from(`verified-daemon-asset-${index + 1}`),
|
||||
]),
|
||||
);
|
||||
const checksumsText = [
|
||||
...DAEMON_RELEASE_POLICY.assets.map(
|
||||
(asset) => `${hash(bodies.get(asset.filename))} ${asset.filename}`,
|
||||
),
|
||||
`${"9".repeat(64)} nekonest-server-linux-amd64.tar.gz`,
|
||||
].join("\n") + "\n";
|
||||
const checksumsBytes = Buffer.from(checksumsText);
|
||||
const release = {
|
||||
tag_name: TAG,
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
html_url: `https://github.com/${DAEMON_RELEASE_POLICY.repository}/releases/tag/${TAG}`,
|
||||
published_at: "2026-08-12T00:00:00Z",
|
||||
assets: [
|
||||
...DAEMON_RELEASE_POLICY.assets.map((asset) => ({
|
||||
name: asset.filename,
|
||||
browser_download_url: `${RELEASE_BASE}/${asset.filename}`,
|
||||
size: bodies.get(asset.filename).byteLength,
|
||||
state: "uploaded",
|
||||
digest: `sha256:${hash(bodies.get(asset.filename))}`,
|
||||
})),
|
||||
{
|
||||
name: "checksums.txt",
|
||||
browser_download_url: `${RELEASE_BASE}/checksums.txt`,
|
||||
size: checksumsBytes.byteLength,
|
||||
state: "uploaded",
|
||||
digest: `sha256:${hash(checksumsBytes)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
const requests = [];
|
||||
const fetchImpl = async (url, init) => {
|
||||
requests.push({ url: String(url), init });
|
||||
let bytes;
|
||||
if (String(url) === API_URL) bytes = Buffer.from(JSON.stringify(release));
|
||||
else if (String(url) === `${RELEASE_BASE}/checksums.txt`) bytes = checksumsBytes;
|
||||
else {
|
||||
const filename = String(url).slice(RELEASE_BASE.length + 1);
|
||||
bytes = bodies.get(filename);
|
||||
}
|
||||
if (!bytes) return new Response("missing", { status: 404 });
|
||||
return new Response(bytes, {
|
||||
status: 200,
|
||||
headers: { "content-length": String(bytes.byteLength) },
|
||||
});
|
||||
};
|
||||
return { bodies, checksumsBytes, fetchImpl, release, requests };
|
||||
}
|
||||
|
||||
async function expectCode(promise, code) {
|
||||
await assert.rejects(promise, (error) => {
|
||||
assert.ok(error instanceof ReleaseVerificationError);
|
||||
assert.equal(error.code, code);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
test("rejects old or prerelease tags before making a network request", async () => {
|
||||
let calls = 0;
|
||||
const fetchImpl = async () => {
|
||||
calls += 1;
|
||||
throw new Error("should not fetch");
|
||||
};
|
||||
assert.throws(() => parseReleaseTag("v0.2.5"), /不得低于 v0\.2\.6/);
|
||||
assert.throws(() => parseReleaseTag("v0.2.6-beta.1"), /稳定版本/);
|
||||
await expectCode(
|
||||
verifyDaemonRelease({ tag: "v0.2.5", fetchImpl }),
|
||||
"incompatible_release_version",
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
|
||||
test("verifies API identity, checksums digest, asset digests, sizes, and downloaded bytes", async () => {
|
||||
const data = fixture();
|
||||
const token = "github-test-token-that-must-not-be-returned";
|
||||
const verified = await verifyDaemonRelease({ tag: TAG, token, fetchImpl: data.fetchImpl });
|
||||
assert.equal(verified.version, "0.2.6");
|
||||
assert.equal(verified.assets.length, 3);
|
||||
assert.equal(verified.assets[0].sha256, hash(data.bodies.get(verified.assets[0].filename)));
|
||||
assert.equal(data.requests.length, 5);
|
||||
assert.equal(data.requests[0].init.headers.authorization, `Bearer ${token}`);
|
||||
for (const request of data.requests.slice(1)) {
|
||||
assert.equal(request.init.headers.authorization, undefined);
|
||||
}
|
||||
const serialized = JSON.stringify(verified);
|
||||
assert.doesNotMatch(serialized, /github-test-token/);
|
||||
const environment = formatCloudReleaseEnvironment(verified);
|
||||
assert.match(environment, /NEKONEST_CLOUD_DAEMON_RELEASE_VERSION=0\.2\.6/);
|
||||
assert.match(environment, /NEKONEST_CLOUD_DAEMON_LINUX_ARM64_SHA256=[0-9a-f]{64}/);
|
||||
assert.doesNotMatch(environment, /GITHUB_TOKEN|github-test-token/);
|
||||
});
|
||||
|
||||
test("fails closed on duplicate assets, URL substitution, prerelease metadata, or digest drift", async () => {
|
||||
{
|
||||
const data = fixture();
|
||||
data.release.assets.push({ ...data.release.assets[0] });
|
||||
await expectCode(
|
||||
verifyDaemonRelease({ tag: TAG, fetchImpl: data.fetchImpl }),
|
||||
"release_asset_duplicate",
|
||||
);
|
||||
}
|
||||
{
|
||||
const data = fixture();
|
||||
data.release.assets[0].browser_download_url = "https://download.example.test/substituted.zip";
|
||||
await expectCode(
|
||||
verifyDaemonRelease({ tag: TAG, fetchImpl: data.fetchImpl }),
|
||||
"release_asset_url_mismatch",
|
||||
);
|
||||
}
|
||||
{
|
||||
const data = fixture();
|
||||
data.release.prerelease = true;
|
||||
await expectCode(
|
||||
verifyDaemonRelease({ tag: TAG, fetchImpl: data.fetchImpl }),
|
||||
"release_identity_invalid",
|
||||
);
|
||||
}
|
||||
{
|
||||
const data = fixture();
|
||||
data.release.assets[0].digest = `sha256:${"f".repeat(64)}`;
|
||||
await expectCode(
|
||||
verifyDaemonRelease({ tag: TAG, fetchImpl: data.fetchImpl }),
|
||||
"release_digest_mismatch",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("detects same-sized downloaded byte substitution even when metadata still looks valid", async () => {
|
||||
const data = fixture();
|
||||
const filename = DAEMON_RELEASE_POLICY.assets[0].filename;
|
||||
const original = data.bodies.get(filename);
|
||||
data.bodies.set(filename, Buffer.from(original.toString().replace("verified", "VERIFIED")));
|
||||
assert.equal(data.bodies.get(filename).byteLength, original.byteLength);
|
||||
await expectCode(
|
||||
verifyDaemonRelease({ tag: TAG, fetchImpl: data.fetchImpl }),
|
||||
"release_asset_hash_mismatch",
|
||||
);
|
||||
});
|
||||
|
||||
test("CLI has a no-network usage failure when the exact tag is missing", () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
["tools/verify-daemon-release.mjs"],
|
||||
{ cwd: new URL("..", import.meta.url), encoding: "utf8" },
|
||||
);
|
||||
assert.equal(result.status, 2);
|
||||
assert.match(result.stderr, /vX\.Y\.Z/);
|
||||
assert.equal(result.stdout, "");
|
||||
});
|
||||
Reference in New Issue
Block a user