diff --git a/CHANGELOG.md b/CHANGELOG.md index fb99dc42dea..26a3cda66bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -158,6 +158,7 @@ Docs: https://docs.openclaw.ai - OpenShell/sandbox: pin verified file reads to an already-opened descriptor, walk the ancestor chain for symlinked parents on platforms without fd-path readlink, and re-check file identity so parent symlink swaps cannot redirect in-sandbox reads to host files outside the allowed mount root. (#69798) Thanks @drobison00. - Gateway/Control UI: require authenticated Control UI read access before serving `/__openclaw/control-ui-config.json` when `gateway.auth` is enabled, so unauthenticated callers can no longer read bootstrap metadata. (#70247) Thanks @drobison00. - Gateway/restart: default session-scoped restart sentinels to a one-shot agent continuation, so chat-initiated Gateway restarts acknowledge successful boot automatically. (#70269) Thanks @obviyus. +- Build/npm publish: fail postpublish verification when root `dist/*` files import bundled plugin runtime dependencies without mirroring them in the root package manifest, so Slack-style plugin deps cannot silently ship on the wrong module-resolution path again. (#60112) thanks @medns. ## 2026.4.21 diff --git a/scripts/lib/bundled-plugin-root-runtime-mirrors.mjs b/scripts/lib/bundled-plugin-root-runtime-mirrors.mjs index 7ae46cbce66..907098609a4 100644 --- a/scripts/lib/bundled-plugin-root-runtime-mirrors.mjs +++ b/scripts/lib/bundled-plugin-root-runtime-mirrors.mjs @@ -151,6 +151,10 @@ function extractModuleSpecifiers(source) { return specifiers; } +function isPluginOwnedDistImporter(relativePath, pluginIds) { + return pluginIds.some((pluginId) => relativePath.startsWith(`extensions/${pluginId}/`)); +} + export function collectRootDistBundledRuntimeMirrors(params) { const distDir = params.distDir; const bundledSpecs = params.bundledRuntimeDependencySpecs; @@ -177,6 +181,9 @@ export function collectRootDistBundledRuntimeMirrors(params) { continue; } const bundledSpec = bundledSpecs.get(dependencyName); + if (isPluginOwnedDistImporter(relativePath, bundledSpec.pluginIds)) { + continue; + } const existing = mirrors.get(dependencyName); if (existing) { existing.importers.add(relativePath); @@ -195,6 +202,7 @@ export function collectRootDistBundledRuntimeMirrors(params) { export function collectBundledPluginRootRuntimeMirrorErrors(params) { const errors = []; + const declaredRootRuntimeDeps = collectRuntimeDependencySpecs(params.rootPackageJson); for (const [dependencyName, record] of params.bundledRuntimeDependencySpecs) { for (const conflict of record.conflicts) { @@ -204,5 +212,17 @@ export function collectBundledPluginRootRuntimeMirrorErrors(params) { } } - return errors; + for (const [dependencyName, record] of params.requiredRootMirrors) { + if (declaredRootRuntimeDeps.has(dependencyName)) { + continue; + } + const importerList = Array.from(record.importers) + .toSorted((left, right) => left.localeCompare(right)) + .join(", "); + errors.push( + `installed package root is missing mirrored bundled runtime dependency '${dependencyName}' for dist importers: ${importerList}. Add it to package.json dependencies/optionalDependencies or keep imports under dist/extensions/${record.pluginIds[0]}/.`, + ); + } + + return errors.toSorted((left, right) => left.localeCompare(right)); } diff --git a/test/openclaw-npm-postpublish-verify.test.ts b/test/openclaw-npm-postpublish-verify.test.ts index 0f6ae2183e8..75444904187 100644 --- a/test/openclaw-npm-postpublish-verify.test.ts +++ b/test/openclaw-npm-postpublish-verify.test.ts @@ -166,28 +166,49 @@ describe("collectInstalledMirroredRootDependencyManifestErrors", () => { writeFileSync(fullPath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); } - function writeSlackWebApiProbePackage( - root: string, - dependencies: Record = {}, - ): void { - writePackageFile(root, "package.json", { + function writeSlackWebApiProbePackage(params: { + root: string; + importerPath?: string; + rootDependencies?: Record; + rootOptionalDependencies?: Record; + }): void { + writePackageFile(params.root, "package.json", { version: "2026.4.10", - dependencies, + dependencies: params.rootDependencies, + optionalDependencies: params.rootOptionalDependencies, }); - writePackageFile(root, "dist/extensions/slack/package.json", { + writePackageFile(params.root, "dist/extensions/slack/package.json", { dependencies: { "@slack/web-api": "^7.15.0", }, }); - mkdirSync(join(root, "dist"), { recursive: true }); - writeFileSync(join(root, "dist", "probe-Cz2PiFtC.js"), 'import("@slack/web-api");\n', "utf8"); + const importerPath = params.importerPath ?? "dist/probe-Cz2PiFtC.js"; + mkdirSync(join(params.root, "dist"), { recursive: true }); + writeFileSync(join(params.root, importerPath), 'import("@slack/web-api");\n', "utf8"); } - it("does not require root mirrors for bundled plugin deps imported by root dist", () => { + it("flags bundled plugin deps imported by root dist when root mirrors are missing", () => { const packageRoot = makeInstalledPackageRoot(); try { - writeSlackWebApiProbePackage(packageRoot); + writeSlackWebApiProbePackage({ root: packageRoot }); + + expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([ + "installed package root is missing mirrored bundled runtime dependency '@slack/web-api' for dist importers: probe-Cz2PiFtC.js. Add it to package.json dependencies/optionalDependencies or keep imports under dist/extensions/slack/.", + ]); + } finally { + rmSync(packageRoot, { recursive: true, force: true }); + } + }); + + it("allows bundled plugin deps imported from their own extension dist without root mirrors", () => { + const packageRoot = makeInstalledPackageRoot(); + + try { + writeSlackWebApiProbePackage({ + root: packageRoot, + importerPath: "dist/extensions/slack/client-Cz2PiFtC.js", + }); expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([]); } finally { @@ -227,8 +248,11 @@ describe("collectInstalledMirroredRootDependencyManifestErrors", () => { const packageRoot = makeInstalledPackageRoot(); try { - writeSlackWebApiProbePackage(packageRoot, { - "@slack/web-api": "^7.16.0", + writeSlackWebApiProbePackage({ + root: packageRoot, + rootDependencies: { + "@slack/web-api": "^7.16.0", + }, }); expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([]);