fix(release): validate package artifact boundaries

This commit is contained in:
Vincent Koc
2026-07-09 04:30:50 -07:00
parent d44fb23a03
commit 69bdd92a61
4 changed files with 95 additions and 4 deletions
@@ -267,6 +267,8 @@ jobs:
},
...(Array.isArray(manifest.dependencyTarballs) ? manifest.dependencyTarballs : []),
];
const packageNames = new Set();
const tarballNames = new Set();
for (const entry of entries) {
if (
!entry.packageName ||
@@ -277,6 +279,11 @@ jobs:
) {
throw new Error("package artifact manifest contains invalid tarball metadata");
}
if (packageNames.has(entry.packageName) || tarballNames.has(entry.tarballName)) {
throw new Error("package artifact manifest contains duplicate package metadata");
}
packageNames.add(entry.packageName);
tarballNames.add(entry.tarballName);
const tarballPath = path.join(packageDir, entry.tarballName);
if (!fs.existsSync(tarballPath)) {
throw new Error(`package artifact is missing ${entry.tarballName}`);
@@ -294,6 +301,16 @@ jobs:
throw new Error(`package artifact metadata mismatch for ${entry.packageName}`);
}
}
const artifactTarballs = fs
.readdirSync(packageDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith(".tgz"))
.map((entry) => entry.name);
if (
artifactTarballs.length !== tarballNames.size ||
artifactTarballs.some((name) => !tarballNames.has(name))
) {
throw new Error("package artifact tarball set does not match preflight manifest");
}
process.stdout.write(path.join(packageDir, manifest.tarballName));
NODE
)"
@@ -114,8 +114,10 @@ async function proxyUpstream(url, response) {
const upstreamUrl = new URL(`${url.pathname}${url.search}`, `${upstreamRegistry}/`);
const upstreamResponse = await fetch(upstreamUrl, { redirect: "manual" });
const body = Buffer.from(await upstreamResponse.arrayBuffer());
const headers = {};
for (const name of ["content-length", "content-type", "location"]) {
// Fetch decodes compressed bodies but preserves upstream length metadata.
// Emit the decoded size so npm clients do not truncate proxied responses.
const headers = { "content-length": String(body.length) };
for (const name of ["content-type", "location"]) {
const value = upstreamResponse.headers.get(name);
if (value) {
headers[name] = value;
@@ -1785,6 +1785,8 @@ describe("package artifact reuse", () => {
'manifest="${package_dir}/preflight-manifest.json"',
'candidate_manifest="${package_dir}/package-candidate.json"',
'find "${package_dir}" -type f -name "*.tgz"',
"package artifact manifest contains duplicate package metadata",
"package artifact tarball set does not match preflight manifest",
"package candidate manifest does not match the OpenClaw tarball",
"package candidate digest mismatch",
'export OPENCLAW_NPM_TELEGRAM_PACKAGE_DIR="${package_dir}"',
+72 -2
View File
@@ -12,6 +12,7 @@ import {
import { createServer, request as httpRequest } from "node:http";
import { tmpdir } from "node:os";
import path from "node:path";
import { gzipSync } from "node:zlib";
import { describe, expect, it } from "vitest";
import { createBoundedChildOutput } from "../helpers/bounded-child-output.js";
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
@@ -160,7 +161,7 @@ function requestFixtureRegistry(
port: number,
requestPath: string,
headers: Record<string, string> = {},
): Promise<{ body: string; statusCode: number | undefined }> {
): Promise<{ body: string; contentLength: string | undefined; statusCode: number | undefined }> {
return new Promise((resolve, reject) => {
const request = httpRequest(
{ headers, host: "127.0.0.1", method: "GET", path: requestPath, port },
@@ -171,7 +172,11 @@ function requestFixtureRegistry(
body += chunk;
});
response.on("end", () => {
resolve({ body, statusCode: response.statusCode });
resolve({
body,
contentLength: response.headers["content-length"],
statusCode: response.statusCode,
});
});
},
);
@@ -670,6 +675,71 @@ test -d "$OPENCLAW_PLUGINS_TMP_DIR"
}
});
it("recomputes proxied content length after fetch decodes the response", async () => {
const tempDirs: string[] = [];
const root = makeTempDir(tempDirs, "openclaw-plugin-npm-fixture-proxy-");
const portFile = path.join(root, "port");
const tarballPath = path.join(root, "demo-plugin.tgz");
const upstreamBody = JSON.stringify({ payload: "x".repeat(1_000) });
const compressedBody = gzipSync(upstreamBody);
writeFileSync(tarballPath, "fixture package archive", "utf8");
const upstream = createServer((_request, response) => {
response.writeHead(200, {
"content-encoding": "gzip",
"content-length": String(compressedBody.length),
"content-type": "application/json",
});
response.end(compressedBody);
});
await new Promise<void>((resolve) => {
upstream.listen(0, "127.0.0.1", resolve);
});
const upstreamAddress = upstream.address();
if (!upstreamAddress || typeof upstreamAddress === "string") {
throw new Error("expected upstream registry address");
}
const child = spawn(
process.execPath,
[
"scripts/e2e/lib/plugins/npm-registry-server.mjs",
portFile,
"@openclaw/demo-plugin-npm",
"1.0.0",
tarballPath,
],
{
cwd: process.cwd(),
env: {
...process.env,
OPENCLAW_NPM_REGISTRY_UPSTREAM: `http://127.0.0.1:${upstreamAddress.port}`,
},
stdio: ["ignore", "pipe", "pipe"],
},
);
try {
const port = await waitForPortFile(portFile);
const response = await requestFixtureRegistry(port, "/upstream-package");
expect(response.statusCode).toBe(200);
expect(response.body).toBe(upstreamBody);
expect(response.contentLength).toBe(String(Buffer.byteLength(upstreamBody)));
} finally {
if (child.exitCode === null) {
child.kill();
await new Promise((resolve) => {
child.once("close", resolve);
});
}
await new Promise<void>((resolve) => {
upstream.close(() => resolve());
});
cleanupTempDirs(tempDirs);
}
});
it("rejects invalid plugin fixture log byte limits before npm fixture setup", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-npm-fixture-log-invalid-"));
try {