Files
elizaos--eliza/packages/scripts/audit-bin-export-subpaths.mjs
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

236 lines
7.5 KiB
JavaScript

#!/usr/bin/env node
// Static guard for the elizaOS/eliza#8000 bug class: a `bin`-listed entry
// imports a deep subpath (e.g. `@elizaos/cli/dist/index.js`) that the target
// package's `exports` map does not whitelist. Under a global install Node uses
// the strict ESM resolver and throws ERR_PACKAGE_PATH_NOT_EXPORTED, so the CLI
// dies before doing anything. Local installs and bundlers often hide it.
//
// This walks every `packages/*/package.json`, follows each `bin` target plus
// the files it transitively imports inside the same package, collects every
// bare import specifier of the form `pkg/subpath` or `@scope/pkg/subpath`, and
// asserts the resolved package's `exports` map permits that subpath. Anything
// it cannot resolve to a workspace package is skipped (external deps own their
// own contracts).
//
// Usage:
// node packages/scripts/audit-bin-export-subpaths.mjs # report
// node packages/scripts/audit-bin-export-subpaths.mjs --check # exit 1 on violation
// node packages/scripts/audit-bin-export-subpaths.mjs --json # machine-readable
import fs from "node:fs";
import path from "node:path";
const args = new Set(process.argv.slice(2));
const asJson = args.has("--json");
const check = args.has("--check");
const repoRoot = process.cwd();
const packagesDir = path.join(repoRoot, "packages");
function readJson(file) {
return JSON.parse(fs.readFileSync(file, "utf8"));
}
function listWorkspacePackages() {
const result = new Map(); // name -> { dir, pkg }
for (const entry of fs.readdirSync(packagesDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const pkgJsonPath = path.join(packagesDir, entry.name, "package.json");
if (!fs.existsSync(pkgJsonPath)) continue;
const pkg = readJson(pkgJsonPath);
if (!pkg.name) continue;
result.set(pkg.name, {
name: pkg.name,
dir: path.join(packagesDir, entry.name),
pkg,
});
}
return result;
}
// Parse a bare import specifier into { pkgName, subpath } where subpath
// includes the leading "/" (or "" for the package root). Returns null for
// relative/builtin/absolute specifiers.
function splitSpecifier(specifier) {
if (
specifier.startsWith(".") ||
specifier.startsWith("/") ||
specifier.startsWith("node:") ||
specifier.startsWith("bun:")
) {
return null;
}
const parts = specifier.split("/");
let pkgName;
let rest;
if (specifier.startsWith("@")) {
if (parts.length < 2) return null;
pkgName = `${parts[0]}/${parts[1]}`;
rest = parts.slice(2);
} else {
pkgName = parts[0];
rest = parts.slice(1);
}
const subpath = rest.length ? `./${rest.join("/")}` : ".";
return { pkgName, subpath };
}
// Collect every static/dynamic import specifier from a source file.
function extractSpecifiers(source) {
const specifiers = new Set();
// import ... from 'x' | export ... from 'x' | import 'x'
const staticRe =
/(?:import|export)(?:[\s\S]*?\sfrom)?\s*["']([^"']+)["']|import\s*["']([^"']+)["']/g;
// import('x') | require('x')
const dynamicRe = /(?:import|require)\s*\(\s*["']([^"']+)["']\s*\)/g;
for (const re of [staticRe, dynamicRe]) {
let match;
while ((match = re.exec(source)) !== null) {
const spec = match[1] ?? match[2];
if (spec) specifiers.add(spec);
}
}
return specifiers;
}
// Walk the bin file and the same-package files it transitively imports,
// returning the set of cross-package specifiers encountered.
function collectCrossPackageSpecifiers(binFile, packageDir) {
const crossPackage = new Set();
const visited = new Set();
const queue = [binFile];
while (queue.length) {
const file = queue.shift();
const resolved = resolveLocalFile(file);
if (!resolved || visited.has(resolved)) continue;
visited.add(resolved);
if (!resolved.startsWith(packageDir)) continue; // stay inside this package
let source;
try {
source = fs.readFileSync(resolved, "utf8");
} catch {
continue;
}
for (const spec of extractSpecifiers(source)) {
if (spec.startsWith(".")) {
queue.push(path.resolve(path.dirname(resolved), spec));
} else {
crossPackage.add(spec);
}
}
}
return crossPackage;
}
const FILE_EXTS = [
".js",
".mjs",
".cjs",
".ts",
".mts",
".cts",
".jsx",
".tsx",
];
function resolveLocalFile(file) {
if (fs.existsSync(file) && fs.statSync(file).isFile()) return file;
for (const ext of FILE_EXTS) {
if (fs.existsSync(file + ext)) return file + ext;
}
for (const ext of FILE_EXTS) {
const indexFile = path.join(file, `index${ext}`);
if (fs.existsSync(indexFile)) return indexFile;
}
return null;
}
// Does the target package's exports map permit `subpath`? Mirrors Node's
// coarse subpath-matching: exact key, or a "./*" pattern whose prefix/suffix
// bracket the subpath. No `exports` means compatibility resolution allows any file.
function exportsPermit(targetPkg, subpath) {
const exportsField = targetPkg.exports;
if (exportsField === undefined) return true; // legacy: any internal file allowed
if (typeof exportsField === "string") {
return subpath === ".";
}
const keys = Object.keys(exportsField);
// If no keys look like subpaths (all are conditions like "import"/"types"),
// the map only describes the "." entry.
const subpathKeys = keys.filter((k) => k === "." || k.startsWith("./"));
if (subpathKeys.length === 0) {
return subpath === ".";
}
for (const key of subpathKeys) {
if (key === subpath) return true;
if (key.includes("*")) {
const [prefix, suffix] = key.split("*");
if (
subpath.startsWith(prefix) &&
subpath.endsWith(suffix) &&
subpath.length >= prefix.length + suffix.length
) {
return true;
}
}
}
return false;
}
function main() {
const workspace = listWorkspacePackages();
const violations = [];
const checked = [];
for (const { name, dir, pkg } of workspace.values()) {
if (!pkg.bin) continue;
const binTargets =
typeof pkg.bin === "string" ? [pkg.bin] : Object.values(pkg.bin);
for (const binRel of binTargets) {
const binFile = path.resolve(dir, binRel);
const specifiers = collectCrossPackageSpecifiers(binFile, dir);
for (const spec of specifiers) {
const split = splitSpecifier(spec);
if (!split) continue;
if (split.pkgName === name) continue; // self
const target = workspace.get(split.pkgName);
if (!target) continue; // external package owns its own contract
if (split.subpath === ".") continue; // bare import always fine
checked.push({ owner: name, bin: binRel, specifier: spec });
if (!exportsPermit(target.pkg, split.subpath)) {
violations.push({
owner: name,
bin: binRel,
specifier: spec,
targetPackage: split.pkgName,
subpath: split.subpath,
});
}
}
}
}
if (asJson) {
console.log(JSON.stringify({ checked, violations }, null, 2));
} else if (violations.length === 0) {
console.log(
`bin-export-subpaths: ${checked.length} cross-package bin import(s) checked, no violations.`,
);
} else {
console.error(
`bin-export-subpaths: ${violations.length} disallowed subpath import(s) (ERR_PACKAGE_PATH_NOT_EXPORTED risk):`,
);
for (const v of violations) {
console.error(
` - ${v.owner} bin ${v.bin} imports "${v.specifier}" but ${v.targetPackage} exports does not permit "${v.subpath}"`,
);
}
}
if (check && violations.length > 0) {
process.exit(1);
}
}
main();