fix(whatsapp-plugin): connect with current WA Web version and load as bundled plugin

WhatsApp rejects handshakes advertising Baileys' baked-in stale protocol
version with a 405 close, so the plugin cycled starting->disconnected and
never issued a QR. connect() now fetches the current WA Web version per
socket build. /status also exposes lastError so this failure mode is
diagnosable from the documented troubleshooting surface.

The published bundled.js also failed to load entirely ("Dynamic require
of 'crypto' is not supported"): plugin bundles are ESM but Baileys is
CJS. bundlePluginEntry now injects the same createRequire banner as
dist/bin.js.

Verified end-to-end against an isolated fn serve: bundled.js loads,
status reaches awaiting-qr, /qr serves a scannable QR data URL,
/pair-code validates input, /logout clears auth state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-17 09:13:55 -07:00
parent e445b3e367
commit c0d610d9d7
5 changed files with 30 additions and 3 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Fix WhatsApp Chat plugin failing to connect (405 rejection) and its bundled build failing to load.
category: fix
dev: connect() now passes fetchLatestBaileysVersion() to makeWASocket so WhatsApp accepts the handshake; /status exposes lastError; plugin bundled.js builds get the createRequire ESM banner so CJS deps (Baileys) can require node builtins.

View File

@@ -203,6 +203,13 @@ async function bundlePluginEntry({ pluginId, srcDir, destDir, withMcpAsset = fal
target: "node22",
outfile: join(destDir, "bundled.js"),
external: ["@fusion/engine", ...external],
/*
* FNXC:BundledPlugins 2026-07-17-09:20:
* CJS dependencies bundled into ESM output (e.g. Baileys in the WhatsApp plugin) compile to esbuild's __require helper, which throws "Dynamic require of \"crypto\" is not supported" at load time in an ESM module. Inject the same createRequire shim dist/bin.js uses so every bundled.js can require node builtins.
*/
banner: {
js: 'import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
},
alias: {
"@fusion/plugin-sdk": join(__dirname, "..", "plugin-sdk", "src", "index.ts"),
/*

View File

@@ -24,6 +24,7 @@ const mockState = vi.hoisted(() => {
vi.mock("@whiskeysockets/baileys", () => ({
default: mockState.makeWASocket,
makeWASocket: mockState.makeWASocket,
fetchLatestBaileysVersion: async () => ({ version: [2, 3000, 0], isLatest: true }),
DisconnectReason: { loggedOut: 401 },
BufferJSON: { reviver: undefined, replacer: undefined },
initAuthCreds: () => ({}),
@@ -140,11 +141,12 @@ describe("WhatsAppConnection", () => {
const connection = new WhatsAppConnection(makeCtx(), "0.1.0", vi.fn().mockResolvedValue("reply"), createInMemoryPersistence());
await connection.start();
await mockState.handlers.get("connection.update")?.({ connection: "close", lastDisconnect: { error: new Error("boom") } });
vi.advanceTimersByTime(1000);
// connect() awaits fetchLatestBaileysVersion before building the socket, so flush microtasks after the timer fires.
await vi.advanceTimersByTimeAsync(1000);
expect(mockState.makeWASocket).toHaveBeenCalledTimes(2);
await mockState.handlers.get("connection.update")?.({ connection: "close", lastDisconnect: { error: { output: { statusCode: 401 } } } });
vi.advanceTimersByTime(1000);
await vi.advanceTimersByTimeAsync(1000);
expect(mockState.makeWASocket).toHaveBeenCalledTimes(2);
vi.useRealTimers();
});

View File

@@ -1,4 +1,4 @@
import { DisconnectReason, makeWASocket, type ConnectionState, type WAMessage, type WAMessageContent, type WASocket } from "@whiskeysockets/baileys";
import { DisconnectReason, fetchLatestBaileysVersion, makeWASocket, type ConnectionState, type WAMessage, type WAMessageContent, type WASocket } from "@whiskeysockets/baileys";
import type { PluginContext } from "@fusion/plugin-sdk";
import pino from "pino";
import qrcode from "qrcode";
@@ -156,7 +156,13 @@ export class WhatsAppConnection {
if (!this.authState) this.authState = await createPersistenceAuthState(this.persistence);
this.status = { state: "starting" };
/**
* FNXC:WhatsAppProtocolVersion 2026-07-17-09:15:
* WhatsApp rejects handshakes that advertise a stale WA Web protocol version with a 405 "Connection Failure" close, which left the plugin stuck cycling starting→disconnected and no QR was ever issued. Fetch the current version before each socket build; fetchLatestBaileysVersion falls back to the library's baked-in version on network failure, so this never blocks connecting.
*/
const { version } = await fetchLatestBaileysVersion();
const socket = makeWASocket({
version,
auth: this.authState.state,
printQRInTerminal: false,
browser: ["Fusion", "Chrome", this.fusionVersion],

View File

@@ -95,11 +95,16 @@ const routes: PluginRouteDefinition[] = [
const { connection, error } = getConnectionOrResponse(ctx);
if (!connection) return error as PluginRouteResponse;
const status = connection.getStatus();
/**
* FNXC:WhatsAppStatusVisibility 2026-07-17-09:15:
* /status must expose lastError: a stale-protocol 405 rejection previously showed only a bare "disconnected" with no way to diagnose it from the API surface the README points troubleshooters at.
*/
return {
status: 200,
body: {
status: status.state,
jid: status.jid,
lastError: status.lastError,
allowedSenders: Array.from(getAllowedSenders(ctx.settings)),
},
};