From 74b49779aea8a772900bcad96d381c47ac7a7a91 Mon Sep 17 00:00:00 2001 From: Frank Yang Date: Tue, 14 Apr 2026 14:25:03 +0800 Subject: [PATCH] fix: patch Baileys runtime hotfixes for npm installs --- scripts/postinstall-bundled-plugins.mjs | 80 ++++++++-- .../stage-bundled-plugin-runtime-deps.test.ts | 147 +++++++++++++----- 2 files changed, 168 insertions(+), 59 deletions(-) diff --git a/scripts/postinstall-bundled-plugins.mjs b/scripts/postinstall-bundled-plugins.mjs index 0c2e002d72b..ca6d9e4e5d3 100644 --- a/scripts/postinstall-bundled-plugins.mjs +++ b/scripts/postinstall-bundled-plugins.mjs @@ -55,6 +55,30 @@ const BAILEYS_MEDIA_HOTFIX_REPLACEMENT = [ " await Promise.all([encFinishPromise, originalFinishPromise]);", " logger?.debug('encrypted data successfully');", ].join("\n"); +const BAILEYS_MEDIA_DISPATCHER_NEEDLE = [ + " const response = await fetch(url, {", + " dispatcher: fetchAgent,", + " method: 'POST',", +].join("\n"); +const BAILEYS_MEDIA_DISPATCHER_REPLACEMENT = [ + " const response = await fetch(url, {", + " method: 'POST',", +].join("\n"); +const BAILEYS_MEDIA_DISPATCHER_HEADER_NEEDLE = [ + " 'Content-Type': 'application/octet-stream',", + " Origin: DEFAULT_ORIGIN", + " },", +].join("\n"); +const BAILEYS_MEDIA_DISPATCHER_HEADER_REPLACEMENT = [ + " 'Content-Type': 'application/octet-stream',", + " Origin: DEFAULT_ORIGIN", + " },", + " // Baileys passes a generic agent here in some runtimes. Undici's", + " // `dispatcher` only works with Dispatcher-compatible implementations,", + " // so only wire it through when the object actually implements", + " // `dispatch`.", + " ...(fetchAgent?.dispatch ? { dispatcher: fetchAgent } : {}),", +].join("\n"); const BAILEYS_MEDIA_ONCE_IMPORT_RE = /import\s+\{\s*once\s*\}\s+from\s+['"]events['"]/u; const BAILEYS_MEDIA_ASYNC_CONTEXT_RE = /async\s+function\s+encryptedStream|encryptedStream\s*=\s*async/u; @@ -243,23 +267,45 @@ export function applyBaileysEncryptedStreamFinishHotfix(params = {}) { } const currentText = readFile(targetPath, "utf8"); - if (currentText.includes(BAILEYS_MEDIA_HOTFIX_REPLACEMENT)) { - return { applied: false, reason: "already_patched" }; - } - if (!currentText.includes(BAILEYS_MEDIA_HOTFIX_NEEDLE)) { - return { applied: false, reason: "unexpected_content" }; - } - if (!BAILEYS_MEDIA_ONCE_IMPORT_RE.test(currentText)) { - return { applied: false, reason: "missing_once_import", targetPath }; - } - if (!BAILEYS_MEDIA_ASYNC_CONTEXT_RE.test(currentText)) { - return { applied: false, reason: "not_async_context", targetPath }; + let patchedText = currentText; + let applied = false; + + if (!patchedText.includes(BAILEYS_MEDIA_HOTFIX_REPLACEMENT)) { + if (!patchedText.includes(BAILEYS_MEDIA_HOTFIX_NEEDLE)) { + return { applied: false, reason: "unexpected_content" }; + } + if (!BAILEYS_MEDIA_ONCE_IMPORT_RE.test(patchedText)) { + return { applied: false, reason: "missing_once_import", targetPath }; + } + if (!BAILEYS_MEDIA_ASYNC_CONTEXT_RE.test(patchedText)) { + return { applied: false, reason: "not_async_context", targetPath }; + } + patchedText = patchedText.replace( + BAILEYS_MEDIA_HOTFIX_NEEDLE, + BAILEYS_MEDIA_HOTFIX_REPLACEMENT, + ); + applied = true; } - const patchedText = currentText.replace( - BAILEYS_MEDIA_HOTFIX_NEEDLE, - BAILEYS_MEDIA_HOTFIX_REPLACEMENT, - ); + if (!patchedText.includes("...(fetchAgent?.dispatch ? { 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" }; + } + patchedText = patchedText + .replace(BAILEYS_MEDIA_DISPATCHER_NEEDLE, BAILEYS_MEDIA_DISPATCHER_REPLACEMENT) + .replace( + BAILEYS_MEDIA_DISPATCHER_HEADER_NEEDLE, + BAILEYS_MEDIA_DISPATCHER_HEADER_REPLACEMENT, + ); + applied = true; + } + + if (!applied) { + return { applied: false, reason: "already_patched" }; + } const tempPath = createTempPath(targetPath); const tempFd = openFile(tempPath, "wx", initialTargetValidation.mode); let tempFdClosed = false; @@ -298,12 +344,12 @@ function applyBundledPluginRuntimeHotfixes(params = {}) { const log = params.log ?? console; const baileysResult = applyBaileysEncryptedStreamFinishHotfix(params); if (baileysResult.applied) { - log.log("[postinstall] patched @whiskeysockets/baileys encryptedStream flush ordering"); + log.log("[postinstall] patched @whiskeysockets/baileys runtime hotfixes"); return; } if (baileysResult.reason !== "missing" && baileysResult.reason !== "already_patched") { log.warn( - `[postinstall] could not patch @whiskeysockets/baileys encryptedStream: ${baileysResult.reason}`, + `[postinstall] could not patch @whiskeysockets/baileys runtime hotfixes: ${baileysResult.reason}`, ); } } diff --git a/src/plugins/stage-bundled-plugin-runtime-deps.test.ts b/src/plugins/stage-bundled-plugin-runtime-deps.test.ts index d056bdc15cc..5c3a44ce6a1 100644 --- a/src/plugins/stage-bundled-plugin-runtime-deps.test.ts +++ b/src/plugins/stage-bundled-plugin-runtime-deps.test.ts @@ -63,6 +63,68 @@ function writeRepoFile(repoRoot: string, relativePath: string, value: string) { fs.writeFileSync(fullPath, value, "utf8"); } +function createBaileysMessagesMediaSource(params?: { + dispatcherPatched?: boolean; + encryptedStreamPatched?: boolean; +}) { + const encryptedLines = 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, {", + " method: 'POST',", + " body: stream,", + " headers: {", + " 'Content-Type': 'application/octet-stream',", + " Origin: DEFAULT_ORIGIN", + " },", + " // Baileys passes a generic agent here in some runtimes. Undici's", + " // `dispatcher` only works with Dispatcher-compatible implementations,", + " // so only wire it through when the object actually implements", + " // `dispatch`.", + " ...(fetchAgent?.dispatch ? { dispatcher: fetchAgent } : {}),", + " duplex: 'half',", + " });", + ] + : [ + " const response = await fetch(url, {", + " dispatcher: fetchAgent,", + " method: 'POST',", + " body: stream,", + " headers: {", + " 'Content-Type': 'application/octet-stream',", + " Origin: DEFAULT_ORIGIN", + " },", + " duplex: 'half',", + " });", + ]; + return [ + "import { once } from 'events';", + "const encryptedStream = async () => {", + ...encryptedLines, + "};", + "const upload = async () => {", + ...dispatcherLines, + "};", + ].join("\n"); +} + afterEach(() => { cleanupTrackedTempDirs(tempDirs); }); @@ -208,16 +270,7 @@ describe("stageBundledPluginRuntimeDeps", () => { writeRepoFile( repoRoot, "node_modules/@whiskeysockets/baileys/lib/Utils/messages-media.js", - [ - "import { once } from 'events';", - "const encryptedStream = async () => {", - " encFileWriteStream.write(mac);", - " encFileWriteStream.end();", - " originalFileStream?.end?.();", - " stream.destroy();", - " logger?.debug('encrypted data successfully');", - "};", - ].join("\n"), + createBaileysMessagesMediaSource(), ); const { applyBaileysEncryptedStreamFinishHotfix } = await loadPostinstallBundledPluginsModule(); @@ -234,6 +287,43 @@ describe("stageBundledPluginRuntimeDeps", () => { expect(fs.readFileSync(targetPath, "utf8")).toContain( "await Promise.all([encFinishPromise, originalFinishPromise]);", ); + expect(fs.readFileSync(targetPath, "utf8")).toContain( + "...(fetchAgent?.dispatch ? { dispatcher: fetchAgent } : {}),", + ); + expect(fs.readFileSync(targetPath, "utf8")).not.toContain("dispatcher: fetchAgent,"); + }); + + it("patches the Baileys dispatcher guard when the flush hotfix is already present", async () => { + const repoRoot = makeRepoRoot("openclaw-stage-bundled-runtime-hotfix-dispatcher-"); + 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({ encryptedStreamPatched: true }), + ); + + const { applyBaileysEncryptedStreamFinishHotfix } = await loadPostinstallBundledPluginsModule(); + const result = applyBaileysEncryptedStreamFinishHotfix({ packageRoot: repoRoot }); + + expect(result).toEqual({ + applied: true, + reason: "patched", + targetPath, + }); + expect(fs.readFileSync(targetPath, "utf8")).toContain( + "await Promise.all([encFinishPromise, originalFinishPromise]);", + ); + expect(fs.readFileSync(targetPath, "utf8")).toContain( + "...(fetchAgent?.dispatch ? { dispatcher: fetchAgent } : {}),", + ); }); it("preserves the original module read mode when replacing Baileys", async () => { @@ -250,16 +340,7 @@ describe("stageBundledPluginRuntimeDeps", () => { writeRepoFile( repoRoot, "node_modules/@whiskeysockets/baileys/lib/Utils/messages-media.js", - [ - "import { once } from 'events';", - "const encryptedStream = async () => {", - " encFileWriteStream.write(mac);", - " encFileWriteStream.end();", - " originalFileStream?.end?.();", - " stream.destroy();", - " logger?.debug('encrypted data successfully');", - "};", - ].join("\n"), + createBaileysMessagesMediaSource(), ); fs.chmodSync(targetPath, 0o644); @@ -315,16 +396,7 @@ describe("stageBundledPluginRuntimeDeps", () => { writeRepoFile( repoRoot, "node_modules/@whiskeysockets/baileys/lib/Utils/messages-media.js", - [ - "import { once } from 'events';", - "const encryptedStream = async () => {", - " encFileWriteStream.write(mac);", - " encFileWriteStream.end();", - " originalFileStream?.end?.();", - " stream.destroy();", - " logger?.debug('encrypted data successfully');", - "};", - ].join("\n"), + createBaileysMessagesMediaSource(), ); const { applyBaileysEncryptedStreamFinishHotfix } = await loadPostinstallBundledPluginsModule(); @@ -341,7 +413,7 @@ describe("stageBundledPluginRuntimeDeps", () => { targetPath, error: "read-only filesystem", }); - expect(fs.readFileSync(targetPath, "utf8")).toContain("encFileWriteStream.end();"); + expect(fs.readFileSync(targetPath, "utf8")).toContain("dispatcher: fetchAgent,"); }); it("refuses pre-created symlink temp paths instead of following them", async () => { @@ -363,16 +435,7 @@ describe("stageBundledPluginRuntimeDeps", () => { writeRepoFile( repoRoot, "node_modules/@whiskeysockets/baileys/lib/Utils/messages-media.js", - [ - "import { once } from 'events';", - "const encryptedStream = async () => {", - " encFileWriteStream.write(mac);", - " encFileWriteStream.end();", - " originalFileStream?.end?.();", - " stream.destroy();", - " logger?.debug('encrypted data successfully');", - "};", - ].join("\n"), + createBaileysMessagesMediaSource(), ); writeRepoFile(repoRoot, "redirected-temp-target.js", "const untouched = true;\n"); fs.symlinkSync(redirectedTarget, attackerTempPath); @@ -389,6 +452,6 @@ describe("stageBundledPluginRuntimeDeps", () => { expect(result.reason).toBe("error"); expect(result.error).toContain("EEXIST"); expect(fs.readFileSync(redirectedTarget, "utf8")).toBe("const untouched = true;\n"); - expect(fs.readFileSync(targetPath, "utf8")).toContain("encFileWriteStream.end();"); + expect(fs.readFileSync(targetPath, "utf8")).toContain("dispatcher: fetchAgent,"); }); });