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
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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
|
||||
Reference in New Issue
Block a user