mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(release): validate package artifact boundaries
This commit is contained in:
@@ -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}"',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user