fix: harden proxied client installation
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s

This commit is contained in:
2026-08-18 16:01:06 +00:00
parent 40c5021c85
commit a08a2c1a96
15 changed files with 513 additions and 87 deletions
+36 -2
View File
@@ -8,11 +8,17 @@ import { parseArgs } from "node:util";
const { values } = parseArgs({ options: {
"gateway-url": { type: "string" },
"download-base-url": { type: "string" },
"download-mirror-base-url": { type: "string", multiple: true, default: [] },
"created-at": { type: "string" },
"signing-key": { type: "string", default: ".secrets/release-signing-private.pem" },
out: { type: "string", default: "release" },
} });
const gatewayUrl = exactHttps(values["gateway-url"], "/mcp", "--gateway-url");
const downloadBaseUrl = httpsBase(values["download-base-url"], "--download-base-url");
const downloadMirrorBaseUrls = [...new Set(values["download-mirror-base-url"].map((value) =>
httpsBase(value, "--download-mirror-base-url")))].filter((value) => value !== downloadBaseUrl);
if (downloadMirrorBaseUrls.length > 4) throw new Error("--download-mirror-base-url 最多允许 4 个");
const createdAt = releaseCreatedAt(values["created-at"]);
const out = resolve(String(values.out));
const projectRoot = resolve(".");
const signingKeyPath = resolve(String(values["signing-key"]));
@@ -86,8 +92,14 @@ const baseArtifacts = {
const commonBootstrapReplacements = new Map([
["{{NODE_VERSION}}", nodeRuntime.version], ["{{NODE_MINIMUM_VERSION}}", nodeRuntime.minimumVersion],
["{{MANIFEST_URL}}", `${downloadBaseUrl}/manifest.json`],
["{{MANIFEST_MIRROR_URLS_POWERSHELL}}", downloadMirrorBaseUrls.map((value) => JSON.stringify(`${value}/manifest.json`)).join(", ")],
["{{MANIFEST_MIRROR_URLS_POSIX}}", downloadMirrorBaseUrls.map((value) => `${value}/manifest.json`).join(" ")],
["{{INSTALLER_URL}}", baseArtifacts.installer.url], ["{{INSTALLER_SHA256}}", baseArtifacts.installer.sha256],
["{{INSTALLER_URLS_POWERSHELL}}", artifactUrls(baseArtifacts.installer).map((value) => JSON.stringify(value)).join(", ")],
["{{INSTALLER_URLS_POSIX}}", artifactUrls(baseArtifacts.installer).join(" ")],
["{{PUBLIC_KEY_URL}}", baseArtifacts.publicKey.url], ["{{PUBLIC_KEY_SHA256}}", baseArtifacts.publicKey.sha256],
["{{PUBLIC_KEY_URLS_POWERSHELL}}", artifactUrls(baseArtifacts.publicKey).map((value) => JSON.stringify(value)).join(", ")],
["{{PUBLIC_KEY_URLS_POSIX}}", artifactUrls(baseArtifacts.publicKey).join(" ")],
["{{NODE_SOURCE_BASES_POWERSHELL}}", nodeRuntime.sourceBases.map((value) => JSON.stringify(value)).join(", ")],
["{{NODE_SOURCE_BASES_POSIX}}", nodeRuntime.sourceBases.join(" ")],
...Object.entries(nodeRuntime.platforms).flatMap(([platform, runtime]) => {
@@ -108,9 +120,12 @@ const artifacts = {
const manifest = {
schemaVersion: 3,
version,
createdAt: new Date().toISOString(),
createdAt,
gatewayUrl,
signing: { algorithm: "Ed25519", keyId },
...(downloadMirrorBaseUrls.length ? {
distribution: { manifestMirrors: downloadMirrorBaseUrls.map((value) => `${value}/manifest.json`) },
} : {}),
requirements: {
node: `>=${nodeRuntime.minimumVersion}`,
managedNode: { version: nodeRuntime.version, platforms: Object.keys(nodeRuntime.platforms).sort() },
@@ -128,10 +143,16 @@ const template = readFileSync(resolve("distribution/agent-install.template.md"),
const replacements = new Map([
["{{VERSION}}", version], ["{{MANIFEST_URL}}", `${downloadBaseUrl}/manifest.json`],
["{{MANIFEST_SIGNATURE_URL}}", `${downloadBaseUrl}/manifest.sig`], ["{{GATEWAY_URL}}", gatewayUrl],
["{{DOWNLOAD_MIRROR_BASE_URLS_MARKDOWN}}", downloadMirrorBaseUrls.length
? downloadMirrorBaseUrls.map((value) => ` - \`${value}\``).join("\n") : " - 未配置"],
["{{INSTALLER_URL}}", artifacts.installer.url], ["{{INSTALLER_SHA256}}", artifacts.installer.sha256],
["{{PUBLIC_KEY_URL}}", artifacts.publicKey.url], ["{{PUBLIC_KEY_SHA256}}", artifacts.publicKey.sha256],
["{{BOOTSTRAP_WINDOWS_URL}}", artifacts.bootstrapWindows.url], ["{{BOOTSTRAP_WINDOWS_SHA256}}", artifacts.bootstrapWindows.sha256],
["{{BOOTSTRAP_WINDOWS_URLS_POWERSHELL}}", artifactUrls(artifacts.bootstrapWindows).map((value) => JSON.stringify(value)).join(", ")],
["{{BOOTSTRAP_WINDOWS_MIRROR_URLS_MARKDOWN}}", (artifacts.bootstrapWindows.mirrors ?? []).map((value) => ` - \`${value}\``).join("\n") || " - 未配置"],
["{{BOOTSTRAP_POSIX_URL}}", artifacts.bootstrapPosix.url], ["{{BOOTSTRAP_POSIX_SHA256}}", artifacts.bootstrapPosix.sha256],
["{{BOOTSTRAP_POSIX_URLS_POSIX}}", artifactUrls(artifacts.bootstrapPosix).join(" ")],
["{{BOOTSTRAP_POSIX_MIRROR_URLS_MARKDOWN}}", (artifacts.bootstrapPosix.mirrors ?? []).map((value) => ` - \`${value}\``).join("\n") || " - 未配置"],
["{{NODE_VERSION}}", nodeRuntime.version], ["{{NODE_MINIMUM_VERSION}}", nodeRuntime.minimumVersion],
["{{RELEASE_KEY_ID}}", keyId],
]);
@@ -147,9 +168,15 @@ console.log(`签名密钥:${keyId}`);
console.log(`给用户的唯一链接:${downloadBaseUrl}/install.md`);
function artifact(name) {
return { url: `${downloadBaseUrl}/${basename(name)}`, sha256: sha256(join(out, name)) };
return {
url: `${downloadBaseUrl}/${basename(name)}`,
...(downloadMirrorBaseUrls.length ? { mirrors: downloadMirrorBaseUrls.map((value) => `${value}/${basename(name)}`) } : {}),
sha256: sha256(join(out, name)),
};
}
function artifactUrls(value) { return [value.url, ...(value.mirrors ?? [])]; }
function sha256(path) { return createHash("sha256").update(readFileSync(path)).digest("hex"); }
function releaseKeyId(value) {
const der = createPublicKey(value).export({ type: "spki", format: "der" });
@@ -217,3 +244,10 @@ function parseHttps(value, flag) {
if (url.protocol !== "https:" || url.username || url.password) throw new Error(`${flag} 必须使用无内嵌凭证的 HTTPS URL`);
return url;
}
function releaseCreatedAt(value) {
if (value === undefined) return new Date().toISOString();
const timestamp = Date.parse(String(value));
if (!Number.isFinite(timestamp)) throw new Error("--created-at 必须是有效 ISO-8601 时间");
return new Date(timestamp).toISOString();
}
+13
View File
@@ -24,8 +24,16 @@ for (const [kind, artifact] of Object.entries(manifest.artifacts ?? {})) {
const expected = String(artifact.sha256 ?? "");
const actual = sha256(join(releaseDir, name));
if (!/^[a-f0-9]{64}$/.test(expected) || actual !== expected) throw new Error(`${kind} SHA-256 不匹配`);
for (const mirror of artifact.mirrors ?? []) {
const url = verifiedHttpsUrl(mirror, `${kind} mirror`);
if (basename(url.pathname) !== name) throw new Error(`${kind} mirror 文件名不一致`);
}
}
if (manifest.artifacts.publicKey.keyId !== keyId) throw new Error("publicKey artifact keyId 不匹配");
for (const mirror of manifest.distribution?.manifestMirrors ?? []) {
const url = verifiedHttpsUrl(mirror, "manifest mirror");
if (!url.pathname.endsWith("/manifest.json")) throw new Error("manifest mirror 必须指向 manifest.json");
}
const root = mkdtempSync(join(tmpdir(), "dingmcp-release-verify-"));
try {
@@ -113,6 +121,11 @@ function artifactName(value, kind) {
if (typeof url !== "string") throw new Error(`manifest 缺少 ${kind} URL`);
return basename(new URL(url).pathname);
}
function verifiedHttpsUrl(value, label) {
const url = new URL(String(value));
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) throw new Error(`${label} 必须是 HTTPS URL`);
return url;
}
function sha256(path) { return createHash("sha256").update(readFileSync(path)).digest("hex"); }
function releaseKeyId(value) {
const der = createPublicKey(value).export({ type: "spki", format: "der" });