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:
7
.changeset/whatsapp-plugin-connects-again.md
Normal file
7
.changeset/whatsapp-plugin-connects-again.md
Normal 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.
|
||||
@@ -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"),
|
||||
/*
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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)),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user