fix: patch Baileys runtime hotfixes for npm installs

This commit is contained in:
Frank Yang
2026-04-14 14:25:03 +08:00
parent 229786e603
commit 74b49779ae
2 changed files with 168 additions and 59 deletions

View File

@@ -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}`,
);
}
}

View File

@@ -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,");
});
});