feat(whatsapp): support Baileys WebSocket URL override (#97155)

* Allow WhatsApp monitor socket substitution

* Refine WhatsApp socket substitution surface

* Keep WhatsApp socket seam on existing API

* Tighten WhatsApp socket seam type exports

* Simplify WhatsApp monitor runtime option state

* Use WhatsApp WebSocket URL override
This commit is contained in:
Dallin Romney
2026-06-29 10:28:34 -07:00
committed by GitHub
parent 6de357ad47
commit 0deddfa204
3 changed files with 52 additions and 0 deletions

View File

@@ -41,6 +41,7 @@ const useMultiFileAuthStateMock = vi.mocked(baileys.useMultiFileAuthState);
let createWaSocket: typeof import("./session.js").createWaSocket;
let formatError: typeof import("./session.js").formatError;
let logWebSelfId: typeof import("./session.js").logWebSelfId;
let OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV: typeof import("./session.js").OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV;
let renderQrTerminalMock: ReturnType<typeof vi.fn>;
let waitForWaConnection: typeof import("./session.js").waitForWaConnection;
let waitForCredsSaveQueue: typeof import("./session.js").waitForCredsSaveQueue;
@@ -153,6 +154,7 @@ function readLastSocketOptions(): {
fetchAgent?: unknown;
keepAliveIntervalMs?: number;
printQRInTerminal?: boolean;
waWebSocketUrl?: string | URL;
logger?: { level?: string; trace?: unknown };
} {
const [options] = firstMockCall(
@@ -169,6 +171,7 @@ function readLastSocketOptions(): {
fetchAgent?: unknown;
keepAliveIntervalMs?: number;
printQRInTerminal?: boolean;
waWebSocketUrl?: string | URL;
logger?: { level?: string; trace?: unknown };
};
}
@@ -224,6 +227,7 @@ describe("web session", () => {
createWaSocket,
formatError,
logWebSelfId,
OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV,
waitForWaConnection,
waitForCredsSaveQueue,
writeCredsJsonAtomically,
@@ -411,6 +415,40 @@ describe("web session", () => {
expect(passed.defaultQueryTimeoutMs).toBe(120_000);
});
it("passes explicit Baileys WebSocket URL overrides", async () => {
await createWaSocket(false, false, {
waWebSocketUrl: " ws://127.0.0.1:49152/ws/chat ",
});
expect(readLastSocketOptions().waWebSocketUrl).toBe("ws://127.0.0.1:49152/ws/chat");
});
it("uses OPENCLAW_WHATSAPP_WEB_SOCKET_URL as the default Baileys WebSocket URL", async () => {
vi.stubEnv(OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV, " ws://127.0.0.1:49153/ws/chat ");
await createWaSocket(false, false);
expect(readLastSocketOptions().waWebSocketUrl).toBe("ws://127.0.0.1:49153/ws/chat");
});
it("preserves explicit Baileys WebSocket URL options over environment", async () => {
vi.stubEnv(OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV, "ws://127.0.0.1:49153/ws/chat");
await createWaSocket(false, false, {
waWebSocketUrl: "ws://127.0.0.1:49154/ws/chat",
});
expect(readLastSocketOptions().waWebSocketUrl).toBe("ws://127.0.0.1:49154/ws/chat");
});
it("ignores blank Baileys WebSocket URL environment overrides", async () => {
vi.stubEnv(OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV, " ");
await createWaSocket(false, false);
expect(readLastSocketOptions().waWebSocketUrl).toBeUndefined();
});
it("uses ambient env proxy agent when HTTPS_PROXY is configured", async () => {
vi.stubEnv("HTTPS_PROXY", "http://proxy.test:8080");

View File

@@ -68,6 +68,7 @@ const LOGGED_OUT_STATUS = 401;
const WHATSAPP_WEBSOCKET_PROXY_TARGET = "https://mmg.whatsapp.net/";
const CREDS_FLUSH_TIMEOUT_MESSAGE =
"Queued WhatsApp creds save did not finish before auth bootstrap; skipping repair and continuing with primary creds.";
export const OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV = "OPENCLAW_WHATSAPP_WEB_SOCKET_URL";
async function rejectUnsafeWebCredsPath(authDir: string): Promise<void> {
await assertWebCredsPathRegularFileOrMissing(resolveWebCredsPath(authDir));
@@ -125,6 +126,13 @@ async function printTerminalQr(qr: string): Promise<void> {
process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
}
function resolveWaWebSocketUrl(value: string | URL | undefined): string | URL | undefined {
if (typeof value !== "string") {
return value;
}
return value.trim() || undefined;
}
/**
* Create a Baileys socket backed by the multi-file auth store we keep on disk.
* Consumers can opt into QR printing for interactive login flows.
@@ -137,6 +145,7 @@ export async function createWaSocket(
onQr?: (qr: string) => void;
getMessage?: (key: WAMessageKey) => Promise<proto.IMessage | undefined>;
cachedGroupMetadata?: (jid: string) => Promise<GroupMetadata | undefined>;
waWebSocketUrl?: string | URL;
} & WhatsAppSocketTimingOptions = {},
): Promise<ReturnType<typeof makeWASocket>> {
const baseLogger = getChildLogger(
@@ -172,6 +181,9 @@ export async function createWaSocket(
defaultQueryTimeoutMs:
opts.defaultQueryTimeoutMs ?? DEFAULT_WHATSAPP_SOCKET_TIMING.defaultQueryTimeoutMs,
};
const waWebSocketUrl =
resolveWaWebSocketUrl(opts.waWebSocketUrl) ??
resolveWaWebSocketUrl(process.env[OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV]);
const sock = makeWASocket({
auth: {
creds: state.creds,
@@ -188,6 +200,7 @@ export async function createWaSocket(
// Baileys types still model `fetchAgent` as a Node agent even though the
// runtime path accepts an undici dispatcher for upload fetches.
fetchAgent: fetchAgent as Agent | undefined,
...(waWebSocketUrl ? { waWebSocketUrl } : {}),
...(opts.getMessage ? { getMessage: opts.getMessage } : {}),
...(opts.cachedGroupMetadata ? { cachedGroupMetadata: opts.cachedGroupMetadata } : {}),
});

View File

@@ -921,6 +921,7 @@ describe("workspace .env blocklist completeness", () => {
"OPENCLAW_DISABLE_BUNDLED_PLUGINS",
"OPENCLAW_ALLOW_INSECURE_PRIVATE_WS",
"OPENCLAW_BROWSER_EXECUTABLE_PATH",
"OPENCLAW_WHATSAPP_WEB_SOCKET_URL",
"EXAMPLE_API_HOST",
"HOMEBREW_BREW_FILE",
"HOMEBREW_PREFIX",