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",
|
target: "node22",
|
||||||
outfile: join(destDir, "bundled.js"),
|
outfile: join(destDir, "bundled.js"),
|
||||||
external: ["@fusion/engine", ...external],
|
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: {
|
alias: {
|
||||||
"@fusion/plugin-sdk": join(__dirname, "..", "plugin-sdk", "src", "index.ts"),
|
"@fusion/plugin-sdk": join(__dirname, "..", "plugin-sdk", "src", "index.ts"),
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const mockState = vi.hoisted(() => {
|
|||||||
vi.mock("@whiskeysockets/baileys", () => ({
|
vi.mock("@whiskeysockets/baileys", () => ({
|
||||||
default: mockState.makeWASocket,
|
default: mockState.makeWASocket,
|
||||||
makeWASocket: mockState.makeWASocket,
|
makeWASocket: mockState.makeWASocket,
|
||||||
|
fetchLatestBaileysVersion: async () => ({ version: [2, 3000, 0], isLatest: true }),
|
||||||
DisconnectReason: { loggedOut: 401 },
|
DisconnectReason: { loggedOut: 401 },
|
||||||
BufferJSON: { reviver: undefined, replacer: undefined },
|
BufferJSON: { reviver: undefined, replacer: undefined },
|
||||||
initAuthCreds: () => ({}),
|
initAuthCreds: () => ({}),
|
||||||
@@ -140,11 +141,12 @@ describe("WhatsAppConnection", () => {
|
|||||||
const connection = new WhatsAppConnection(makeCtx(), "0.1.0", vi.fn().mockResolvedValue("reply"), createInMemoryPersistence());
|
const connection = new WhatsAppConnection(makeCtx(), "0.1.0", vi.fn().mockResolvedValue("reply"), createInMemoryPersistence());
|
||||||
await connection.start();
|
await connection.start();
|
||||||
await mockState.handlers.get("connection.update")?.({ connection: "close", lastDisconnect: { error: new Error("boom") } });
|
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);
|
expect(mockState.makeWASocket).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
await mockState.handlers.get("connection.update")?.({ connection: "close", lastDisconnect: { error: { output: { statusCode: 401 } } } });
|
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);
|
expect(mockState.makeWASocket).toHaveBeenCalledTimes(2);
|
||||||
vi.useRealTimers();
|
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 type { PluginContext } from "@fusion/plugin-sdk";
|
||||||
import pino from "pino";
|
import pino from "pino";
|
||||||
import qrcode from "qrcode";
|
import qrcode from "qrcode";
|
||||||
@@ -156,7 +156,13 @@ export class WhatsAppConnection {
|
|||||||
if (!this.authState) this.authState = await createPersistenceAuthState(this.persistence);
|
if (!this.authState) this.authState = await createPersistenceAuthState(this.persistence);
|
||||||
|
|
||||||
this.status = { state: "starting" };
|
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({
|
const socket = makeWASocket({
|
||||||
|
version,
|
||||||
auth: this.authState.state,
|
auth: this.authState.state,
|
||||||
printQRInTerminal: false,
|
printQRInTerminal: false,
|
||||||
browser: ["Fusion", "Chrome", this.fusionVersion],
|
browser: ["Fusion", "Chrome", this.fusionVersion],
|
||||||
|
|||||||
@@ -95,11 +95,16 @@ const routes: PluginRouteDefinition[] = [
|
|||||||
const { connection, error } = getConnectionOrResponse(ctx);
|
const { connection, error } = getConnectionOrResponse(ctx);
|
||||||
if (!connection) return error as PluginRouteResponse;
|
if (!connection) return error as PluginRouteResponse;
|
||||||
const status = connection.getStatus();
|
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 {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
status: status.state,
|
status: status.state,
|
||||||
jid: status.jid,
|
jid: status.jid,
|
||||||
|
lastError: status.lastError,
|
||||||
allowedSenders: Array.from(getAllowedSenders(ctx.settings)),
|
allowedSenders: Array.from(getAllowedSenders(ctx.settings)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user