fix(release): verify package entrypoint imports

This commit is contained in:
Peter Steinberger
2026-04-28 14:06:46 +01:00
parent 727bff8133
commit 2e8f91c36e
5 changed files with 203 additions and 4 deletions

View File

@@ -4,6 +4,8 @@
// prebuilt package artifact with dist inventory, not a source checkout.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { LOCAL_BUILD_METADATA_DIST_PATHS } from "./lib/local-build-metadata-paths.mjs";
function usage() {
@@ -39,6 +41,17 @@ const normalized = entries.map((entry) => entry.replace(/^package\//u, ""));
const entrySet = new Set(normalized);
const errors = [];
const warnings = [];
const unsafeEntries = normalized.filter(
(entry) => entry.startsWith("/") || entry.split("/").includes(".."),
);
const DIST_JS_IMPORT_SPECIFIER_PATTERN =
/\b(?:import|export)\s+(?:(?:[^'"()]*?\s+from\s+)|)["'](?<staticSpecifier>[^"']+)["']|\bimport\s*\(\s*["'](?<dynamicSpecifier>[^"']+)["']\s*\)/gu;
const DIST_IMPORT_REFERENCE_ENTRYPOINTS = [
"dist/entry.js",
"dist/cli/run-main.js",
"dist/index.js",
"dist/index.mjs",
];
const LEGACY_PACKAGE_ACCEPTANCE_COMPAT_MAX = { year: 2026, month: 4, day: 25 };
const LEGACY_LOCAL_BUILD_METADATA_COMPAT_MAX = { year: 2026, month: 4, day: 26 };
const FORBIDDEN_LOCAL_BUILD_METADATA_FILES = new Set(LOCAL_BUILD_METADATA_DIST_PATHS);
@@ -117,10 +130,73 @@ function readTarEntry(entryPath) {
return "";
}
for (const entry of normalized) {
if (entry.startsWith("/") || entry.split("/").includes("..")) {
errors.push(`unsafe tar entry: ${entry}`);
function isRelativeModuleSpecifier(value) {
return value.startsWith("./") || value.startsWith("../");
}
function normalizeModuleSpecifierTarget(value) {
return value.split(/[?#]/u, 1)[0] ?? value;
}
function normalizeTarPath(value) {
return value.replace(/\\/gu, "/");
}
function resolveTarImportTarget(importer, specifier) {
const normalizedSpecifier = normalizeModuleSpecifierTarget(specifier);
const base = normalizeTarPath(
new URL(normalizedSpecifier, `file:///${importer}`).pathname.replace(/^\//u, ""),
);
const candidates = [
base,
`${base}.js`,
`${base}.mjs`,
`${base}.cjs`,
`${base}/index.js`,
`${base}/index.mjs`,
`${base}/index.cjs`,
];
return candidates.find((candidate) => entrySet.has(candidate)) ?? null;
}
function collectTarImportReferenceErrors() {
if (unsafeEntries.length > 0) {
return [];
}
const extractRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-package-check-"));
const importErrors = [];
try {
const extract = spawnSync("tar", ["-xzf", tarball, "-C", extractRoot], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
if (extract.status !== 0) {
return [
`tar extraction failed for import reference check: ${extract.stderr || extract.status}`,
];
}
for (const entry of DIST_IMPORT_REFERENCE_ENTRYPOINTS.filter((entry) => entrySet.has(entry))) {
const source = fs.readFileSync(path.join(extractRoot, "package", entry), "utf8");
for (const match of source.matchAll(DIST_JS_IMPORT_SPECIFIER_PATTERN)) {
const specifier = match.groups?.staticSpecifier ?? match.groups?.dynamicSpecifier ?? "";
if (!isRelativeModuleSpecifier(specifier)) {
continue;
}
if (resolveTarImportTarget(entry, specifier)) {
continue;
}
importErrors.push(`missing packaged dist import target ${specifier} from ${entry}`);
}
}
} finally {
fs.rmSync(extractRoot, { recursive: true, force: true });
}
return importErrors.toSorted((left, right) => left.localeCompare(right));
}
for (const entry of unsafeEntries) {
errors.push(`unsafe tar entry: ${entry}`);
}
if (!entrySet.has("package.json")) {
@@ -182,6 +258,7 @@ if (entrySet.has("dist/postinstall-inventory.json")) {
);
}
}
errors.push(...collectTarImportReferenceErrors());
if (errors.length > 0) {
fail(`OpenClaw package tarball integrity failed:\n${errors.join("\n")}`);