From 7a48e4e5e176a94c84c1a64c302ea56d3f54a02e Mon Sep 17 00:00:00 2001 From: Frank Yang Date: Tue, 14 Apr 2026 16:31:24 +0800 Subject: [PATCH] fix: harden Baileys postinstall and dep root dedupe --- scripts/postinstall-bundled-plugins.mjs | 31 +++---- scripts/stage-bundled-plugin-runtime-deps.mjs | 8 +- .../stage-bundled-plugin-runtime-deps.test.ts | 83 ++++++++++++++----- .../stage-bundled-plugin-runtime-deps.test.ts | 46 ++++++++++ 4 files changed, 128 insertions(+), 40 deletions(-) diff --git a/scripts/postinstall-bundled-plugins.mjs b/scripts/postinstall-bundled-plugins.mjs index ded24ba067d..8c2c1e2d771 100644 --- a/scripts/postinstall-bundled-plugins.mjs +++ b/scripts/postinstall-bundled-plugins.mjs @@ -293,11 +293,9 @@ export function applyBaileysEncryptedStreamFinishHotfix(params = {}) { (BAILEYS_MEDIA_HOTFIX_FINISH_PROMISES_RE.test(patchedText) && (BAILEYS_MEDIA_HOTFIX_PROMISE_ALL_RE.test(patchedText) || BAILEYS_MEDIA_HOTFIX_SEQUENTIAL_AWAITS_RE.test(patchedText))); + const encryptedStreamPatchable = patchedText.includes(BAILEYS_MEDIA_HOTFIX_NEEDLE); - if (!encryptedStreamAlreadyPatched) { - if (!patchedText.includes(BAILEYS_MEDIA_HOTFIX_NEEDLE)) { - return { applied: false, reason: "unexpected_content" }; - } + if (!encryptedStreamAlreadyPatched && encryptedStreamPatchable) { if (!BAILEYS_MEDIA_ONCE_IMPORT_RE.test(patchedText)) { return { applied: false, reason: "missing_once_import", targetPath }; } @@ -311,17 +309,14 @@ export function applyBaileysEncryptedStreamFinishHotfix(params = {}) { applied = true; } - if ( - !patchedText.includes( - "...(typeof fetchAgent?.dispatch === 'function' ? { dispatcher: fetchAgent } : {}),", - ) - ) { - if (!patchedText.includes(BAILEYS_MEDIA_DISPATCHER_NEEDLE)) { - return { applied: false, reason: "unexpected_content" }; - } - if (!patchedText.includes(BAILEYS_MEDIA_DISPATCHER_HEADER_NEEDLE)) { - return { applied: false, reason: "unexpected_content" }; - } + const dispatcherAlreadyPatched = patchedText.includes( + "...(typeof fetchAgent?.dispatch === 'function' ? { dispatcher: fetchAgent } : {}),", + ); + const dispatcherPatchable = + patchedText.includes(BAILEYS_MEDIA_DISPATCHER_NEEDLE) && + patchedText.includes(BAILEYS_MEDIA_DISPATCHER_HEADER_NEEDLE); + + if (!dispatcherAlreadyPatched && dispatcherPatchable) { patchedText = patchedText .replace(BAILEYS_MEDIA_DISPATCHER_NEEDLE, BAILEYS_MEDIA_DISPATCHER_REPLACEMENT) .replace( @@ -332,6 +327,12 @@ export function applyBaileysEncryptedStreamFinishHotfix(params = {}) { } if (!applied) { + if ( + (!encryptedStreamAlreadyPatched && !encryptedStreamPatchable) || + (!dispatcherAlreadyPatched && !dispatcherPatchable) + ) { + return { applied: false, reason: "unexpected_content" }; + } return { applied: false, reason: "already_patched" }; } const tempPath = createTempPath(targetPath); diff --git a/scripts/stage-bundled-plugin-runtime-deps.mjs b/scripts/stage-bundled-plugin-runtime-deps.mjs index be0640ce3fc..34b58c2a1fb 100644 --- a/scripts/stage-bundled-plugin-runtime-deps.mjs +++ b/scripts/stage-bundled-plugin-runtime-deps.mjs @@ -171,8 +171,9 @@ function collectInstalledRuntimeDependencyRoots(rootNodeModulesDir, dependencySp if (depRoot === null) { return null; } + const canonicalDepRoot = fs.realpathSync(depRoot); - const seenKey = `${current.depName}\0${depRoot}`; + const seenKey = `${current.depName}\0${canonicalDepRoot}`; if (seen.has(seenKey)) { continue; } @@ -184,8 +185,9 @@ function collectInstalledRuntimeDependencyRoots(rootNodeModulesDir, dependencySp directRoots.push(record); } - const packageJson = packageCache.get(depRoot) ?? readJson(path.join(depRoot, "package.json")); - packageCache.set(depRoot, packageJson); + const packageJson = + packageCache.get(canonicalDepRoot) ?? readJson(path.join(depRoot, "package.json")); + packageCache.set(canonicalDepRoot, packageJson); for (const [childName, childSpec] of Object.entries(packageJson.dependencies ?? {})) { queue.push({ depName: childName, diff --git a/src/plugins/stage-bundled-plugin-runtime-deps.test.ts b/src/plugins/stage-bundled-plugin-runtime-deps.test.ts index 35d4e2f2c0c..e928d9855c8 100644 --- a/src/plugins/stage-bundled-plugin-runtime-deps.test.ts +++ b/src/plugins/stage-bundled-plugin-runtime-deps.test.ts @@ -68,23 +68,14 @@ function createBaileysMessagesMediaSource(params?: { encryptedStreamPatched?: boolean; encryptedStreamPatchedSequentially?: boolean; encryptedStreamPatchedSequentiallyWithComments?: boolean; + encryptedStreamUnrecognized?: boolean; }) { - const encryptedLines = params?.encryptedStreamPatchedSequentiallyWithComments + const encryptedLines = params?.encryptedStreamUnrecognized ? [ " encFileWriteStream.write(mac);", - " const encFinishPromise = once(encFileWriteStream, 'finish');", - " const originalFinishPromise = originalFileStream ? once(originalFileStream, 'finish') : Promise.resolve();", - " encFileWriteStream.end();", - " originalFileStream?.end?.();", - " stream.destroy();", - " // Wait for write streams to fully flush to disk before returning encFilePath.", - " // Without this await, the caller may open a read stream on the file before", - " // the OS has created it, causing a race-condition ENOENT crash.", - " await encFinishPromise;", - " await originalFinishPromise;", - " logger?.debug('encrypted data successfully');", + " logger?.debug('encrypted data changed upstream');", ] - : params?.encryptedStreamPatchedSequentially + : params?.encryptedStreamPatchedSequentiallyWithComments ? [ " encFileWriteStream.write(mac);", " const encFinishPromise = once(encFileWriteStream, 'finish');", @@ -92,11 +83,14 @@ function createBaileysMessagesMediaSource(params?: { " encFileWriteStream.end();", " originalFileStream?.end?.();", " stream.destroy();", + " // Wait for write streams to fully flush to disk before returning encFilePath.", + " // Without this await, the caller may open a read stream on the file before", + " // the OS has created it, causing a race-condition ENOENT crash.", " await encFinishPromise;", " await originalFinishPromise;", " logger?.debug('encrypted data successfully');", ] - : params?.encryptedStreamPatched + : params?.encryptedStreamPatchedSequentially ? [ " encFileWriteStream.write(mac);", " const encFinishPromise = once(encFileWriteStream, 'finish');", @@ -104,16 +98,28 @@ function createBaileysMessagesMediaSource(params?: { " encFileWriteStream.end();", " originalFileStream?.end?.();", " stream.destroy();", - " await Promise.all([encFinishPromise, originalFinishPromise]);", + " await encFinishPromise;", + " await originalFinishPromise;", " logger?.debug('encrypted data successfully');", ] - : [ - " encFileWriteStream.write(mac);", - " encFileWriteStream.end();", - " originalFileStream?.end?.();", - " stream.destroy();", - " logger?.debug('encrypted data successfully');", - ]; + : params?.encryptedStreamPatched + ? [ + " encFileWriteStream.write(mac);", + " const encFinishPromise = once(encFileWriteStream, 'finish');", + " const originalFinishPromise = originalFileStream ? once(originalFileStream, 'finish') : Promise.resolve();", + " encFileWriteStream.end();", + " originalFileStream?.end?.();", + " stream.destroy();", + " await Promise.all([encFinishPromise, originalFinishPromise]);", + " logger?.debug('encrypted data successfully');", + ] + : [ + " encFileWriteStream.write(mac);", + " encFileWriteStream.end();", + " originalFileStream?.end?.();", + " stream.destroy();", + " logger?.debug('encrypted data successfully');", + ]; const dispatcherLines = params?.dispatcherPatched ? [ " const response = await fetch(url, {", @@ -355,6 +361,39 @@ describe("stageBundledPluginRuntimeDeps", () => { ); }); + it("patches the Baileys dispatcher guard even when the encryptedStream block changed", async () => { + const repoRoot = makeRepoRoot("openclaw-stage-bundled-runtime-hotfix-dispatcher-only-"); + const targetPath = path.join( + repoRoot, + "node_modules", + "@whiskeysockets", + "baileys", + "lib", + "Utils", + "messages-media.js", + ); + writeRepoFile( + repoRoot, + "node_modules/@whiskeysockets/baileys/lib/Utils/messages-media.js", + createBaileysMessagesMediaSource({ encryptedStreamUnrecognized: true }), + ); + + const { applyBaileysEncryptedStreamFinishHotfix } = await loadPostinstallBundledPluginsModule(); + const result = applyBaileysEncryptedStreamFinishHotfix({ packageRoot: repoRoot }); + + expect(result).toEqual({ + applied: true, + reason: "patched", + targetPath, + }); + expect(fs.readFileSync(targetPath, "utf8")).toContain( + "logger?.debug('encrypted data changed upstream');", + ); + expect(fs.readFileSync(targetPath, "utf8")).toContain( + "...(typeof fetchAgent?.dispatch === 'function' ? { dispatcher: fetchAgent } : {}),", + ); + }); + it("patches the Baileys dispatcher guard when sequential awaits include comments", async () => { const repoRoot = makeRepoRoot( "openclaw-stage-bundled-runtime-hotfix-dispatcher-sequential-comments-", diff --git a/test/scripts/stage-bundled-plugin-runtime-deps.test.ts b/test/scripts/stage-bundled-plugin-runtime-deps.test.ts index 48b18ad7661..8c2bec7a7d4 100644 --- a/test/scripts/stage-bundled-plugin-runtime-deps.test.ts +++ b/test/scripts/stage-bundled-plugin-runtime-deps.test.ts @@ -331,6 +331,52 @@ describe("stageBundledPluginRuntimeDeps", () => { expect(fs.readFileSync(stampPath, "utf8")).toBe(firstStamp); }); + it("dedupes cyclic dependency aliases by canonical root", () => { + const { pluginDir, repoRoot } = createBundledPluginFixture({ + packageJson: { + name: "@openclaw/fixture-plugin", + version: "1.0.0", + dependencies: { a: "1.0.0" }, + openclaw: { bundle: { stageRuntimeDependencies: true } }, + }, + }); + const rootNodeModulesDir = path.join(repoRoot, "node_modules"); + const storeDir = path.join(repoRoot, ".store"); + const aStoreDir = path.join(storeDir, "a"); + const bStoreDir = path.join(storeDir, "b"); + fs.mkdirSync(path.join(aStoreDir, "node_modules"), { recursive: true }); + fs.mkdirSync(path.join(bStoreDir, "node_modules"), { recursive: true }); + fs.writeFileSync( + path.join(aStoreDir, "package.json"), + '{ "name": "a", "version": "1.0.0", "dependencies": { "b": "1.0.0" } }\n', + "utf8", + ); + fs.writeFileSync(path.join(aStoreDir, "index.js"), "module.exports = 'a';\n", "utf8"); + fs.writeFileSync( + path.join(bStoreDir, "package.json"), + '{ "name": "b", "version": "1.0.0", "dependencies": { "a": "1.0.0" } }\n', + "utf8", + ); + fs.writeFileSync(path.join(bStoreDir, "index.js"), "module.exports = 'b';\n", "utf8"); + fs.mkdirSync(rootNodeModulesDir, { recursive: true }); + fs.symlinkSync(aStoreDir, path.join(rootNodeModulesDir, "a")); + fs.symlinkSync(bStoreDir, path.join(rootNodeModulesDir, "b")); + fs.symlinkSync(bStoreDir, path.join(aStoreDir, "node_modules", "b")); + fs.symlinkSync(aStoreDir, path.join(bStoreDir, "node_modules", "a")); + + stageBundledPluginRuntimeDeps({ cwd: repoRoot }); + + expect(fs.readFileSync(path.join(pluginDir, "node_modules", "a", "index.js"), "utf8")).toBe( + "module.exports = 'a';\n", + ); + expect( + fs.readFileSync( + path.join(pluginDir, "node_modules", "a", "node_modules", "b", "index.js"), + "utf8", + ), + ).toBe("module.exports = 'b';\n"); + }); + it("falls back to install when the root transitive closure is incomplete", () => { const { pluginDir, repoRoot } = createBundledPluginFixture({ packageJson: {