#!/usr/bin/env node // Validates the npm tarball Docker E2E lanes install. // This is intentionally tarball-only: the check proves Docker lanes consume the // 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 { performance } from "node:perf_hooks"; import { LOCAL_BUILD_METADATA_DIST_PATHS } from "./lib/local-build-metadata-paths.mjs"; import { collectPackageDistImports, collectPackageDistImportErrors, expandPackageDistImportClosure, } from "./lib/package-dist-imports.mjs"; function usage() { return "Usage: node scripts/check-openclaw-package-tarball.mjs "; } function fail(message) { console.error(message); process.exit(1); } const tarball = process.argv[2]; if (!tarball || process.argv.length > 3) { fail(usage()); } if (!fs.existsSync(tarball)) { fail(`OpenClaw package tarball does not exist: ${tarball}`); } const phaseTimingsEnabled = process.env.OPENCLAW_PACKAGE_TARBALL_CHECK_TIMINGS !== "0"; function runPhase(label, action) { const startedAt = performance.now(); try { return action(); } finally { if (phaseTimingsEnabled) { const durationMs = Math.round(performance.now() - startedAt); console.error(`check-openclaw-package-tarball: ${label} completed in ${durationMs}ms`); } } } const list = runPhase("tar list", () => spawnSync("tar", ["-tf", tarball], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }), ); if (list.status !== 0) { fail(`tar -tf failed for ${tarball}: ${list.stderr || list.status}`); } const extractDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-package-tarball-")); try { const extract = runPhase("tar extract", () => spawnSync("tar", ["-xf", tarball, "-C", extractDir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }), ); if (extract.status !== 0) { fail(`tar -xf failed for ${tarball}: ${extract.stderr || extract.status}`); } } catch (error) { fs.rmSync(extractDir, { recursive: true, force: true }); throw error; } const entries = list.stdout .split(/\r?\n/u) .map((entry) => entry.trim()) .filter(Boolean); const normalized = entries.map((entry) => entry.replace(/^package\//u, "")); const entrySet = new Set(normalized); const errors = []; const warnings = []; const REQUIRED_TARBALL_ENTRIES = ["dist/control-ui/index.html"]; const REQUIRED_TARBALL_ENTRY_PREFIXES = ["dist/control-ui/assets/"]; 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); const LEGACY_OMITTED_PRIVATE_QA_INVENTORY_PREFIXES = [ "dist/extensions/qa-channel/", "dist/extensions/qa-lab/", "dist/extensions/qa-matrix/", "dist/plugin-sdk/extensions/qa-channel/", "dist/plugin-sdk/extensions/qa-lab/", ]; const LEGACY_OMITTED_PRIVATE_QA_INVENTORY_FILES = new Set([ "dist/plugin-sdk/qa-channel.d.ts", "dist/plugin-sdk/qa-channel.js", "dist/plugin-sdk/qa-channel-protocol.d.ts", "dist/plugin-sdk/qa-channel-protocol.js", "dist/plugin-sdk/qa-lab.d.ts", "dist/plugin-sdk/qa-lab.js", "dist/plugin-sdk/qa-runtime.d.ts", "dist/plugin-sdk/qa-runtime.js", "dist/plugin-sdk/src/plugin-sdk/qa-channel.d.ts", "dist/plugin-sdk/src/plugin-sdk/qa-channel-protocol.d.ts", "dist/plugin-sdk/src/plugin-sdk/qa-lab.d.ts", "dist/plugin-sdk/src/plugin-sdk/qa-runtime.d.ts", ]); function isLegacyOmittedPrivateQaInventoryEntry(relativePath) { return ( LEGACY_OMITTED_PRIVATE_QA_INVENTORY_FILES.has(relativePath) || LEGACY_OMITTED_PRIVATE_QA_INVENTORY_PREFIXES.some((prefix) => relativePath.startsWith(prefix)) ); } function parseCalver(version) { const match = /^(\d{4})\.(\d{1,2})\.(\d{1,2})(?:[-+].*)?$/u.exec(version); if (!match) { return null; } return { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]), }; } function compareCalver(left, right) { for (const key of ["year", "month", "day"]) { if (left[key] !== right[key]) { return left[key] - right[key]; } } return 0; } function isLegacyPackageAcceptanceCompatVersion(version) { const parsed = parseCalver(version); return parsed ? compareCalver(parsed, LEGACY_PACKAGE_ACCEPTANCE_COMPAT_MAX) <= 0 : false; } function isLegacyLocalBuildMetadataCompatVersion(version) { const parsed = parseCalver(version); return parsed ? compareCalver(parsed, LEGACY_LOCAL_BUILD_METADATA_COMPAT_MAX) <= 0 : false; } function readTarEntry(entryPath) { const candidates = [ path.join(extractDir, entryPath), path.join(extractDir, "package", entryPath), ]; for (const candidate of candidates) { if (fs.existsSync(candidate)) { return fs.readFileSync(candidate, "utf8"); } } return ""; } for (const entry of normalized) { if (entry.startsWith("/") || entry.split("/").includes("..")) { errors.push(`unsafe tar entry: ${entry}`); } } if (!entrySet.has("package.json")) { errors.push("missing package.json"); } if (!normalized.some((entry) => entry.startsWith("dist/"))) { errors.push("missing dist/ entries"); } for (const requiredEntry of REQUIRED_TARBALL_ENTRIES) { if (!entrySet.has(requiredEntry)) { errors.push(`missing required tar entry ${requiredEntry}`); } } for (const requiredPrefix of REQUIRED_TARBALL_ENTRY_PREFIXES) { if (!normalized.some((entry) => entry.startsWith(requiredPrefix))) { errors.push(`missing required tar entries under ${requiredPrefix}`); } } let packageVersion = ""; if (entrySet.has("package.json")) { try { const packageJson = JSON.parse(readTarEntry("package.json")); packageVersion = typeof packageJson.version === "string" ? packageJson.version : ""; } catch { packageVersion = ""; } } for (const forbiddenEntry of FORBIDDEN_LOCAL_BUILD_METADATA_FILES) { if (entrySet.has(forbiddenEntry)) { if (isLegacyLocalBuildMetadataCompatVersion(packageVersion)) { warnings.push(`legacy package includes local build metadata tar entry ${forbiddenEntry}`); continue; } errors.push(`forbidden local build metadata tar entry ${forbiddenEntry}`); } } if (!entrySet.has("dist/postinstall-inventory.json")) { errors.push("missing dist/postinstall-inventory.json"); } let packageDistImports = null; if (entrySet.has("dist/postinstall-inventory.json")) { try { const allowLegacyPrivateQaInventoryOmissions = isLegacyPackageAcceptanceCompatVersion(packageVersion); const inventory = JSON.parse(readTarEntry("dist/postinstall-inventory.json")); if (!Array.isArray(inventory) || inventory.some((entry) => typeof entry !== "string")) { errors.push("invalid dist/postinstall-inventory.json"); } else { const normalizedInventory = inventory.map((entry) => entry.replace(/\\/gu, "/")); const normalizedInventorySet = new Set(normalizedInventory); packageDistImports = runPhase("dist import graph", () => collectPackageDistImports({ files: normalized, readText: readTarEntry, }), ); for (const inventoryEntry of inventory) { const normalizedEntry = inventoryEntry.replace(/\\/gu, "/"); if (!entrySet.has(normalizedEntry)) { if ( allowLegacyPrivateQaInventoryOmissions && isLegacyOmittedPrivateQaInventoryEntry(normalizedEntry) ) { warnings.push( `legacy inventory references omitted private QA tar entry ${normalizedEntry}`, ); continue; } errors.push(`inventory references missing tar entry ${normalizedEntry}`); } } const expandedInventory = expandPackageDistImportClosure({ files: normalized, seedFiles: normalizedInventory, readText: readTarEntry, imports: packageDistImports, }); for (const importedEntry of expandedInventory) { if (!normalizedInventorySet.has(importedEntry)) { errors.push(`inventory omits imported dist file ${importedEntry}`); } } } } catch (error) { errors.push( `unreadable dist/postinstall-inventory.json: ${ error instanceof Error ? error.message : String(error) }`, ); } } errors.push( ...collectPackageDistImportErrors({ files: normalized, readText: readTarEntry, imports: packageDistImports ?? undefined, }), ); if (errors.length > 0) { fs.rmSync(extractDir, { recursive: true, force: true }); fail(`OpenClaw package tarball integrity failed:\n${errors.join("\n")}`); } for (const warning of warnings) { console.warn(`OpenClaw package tarball integrity warning: ${warning}`); } fs.rmSync(extractDir, { recursive: true, force: true }); console.log("OpenClaw package tarball integrity passed.");