diff --git a/extensions/bluebubbles/src/send.test.ts b/extensions/bluebubbles/src/send.test.ts index ff9935c84b3..bc3938b327c 100644 --- a/extensions/bluebubbles/src/send.test.ts +++ b/extensions/bluebubbles/src/send.test.ts @@ -9,7 +9,7 @@ import { installBlueBubblesFetchTestHooks, mockBlueBubblesPrivateApiStatusOnce, } from "./test-harness.js"; -import type { BlueBubblesSendTarget } from "./types.js"; +import { _setFetchGuardForTesting, type BlueBubblesSendTarget } from "./types.js"; const mockFetch = vi.fn(); const privateApiStatusMock = vi.mocked(getCachedBlueBubblesPrivateApiStatus); @@ -61,6 +61,33 @@ function mockNewChatSendResponse(guid: string) { }); } +function installSsrFPolicyCapture(policies: unknown[]) { + _setFetchGuardForTesting(async (params) => { + policies.push(params.policy); + const raw = await globalThis.fetch(params.url, params.init); + let body: ArrayBuffer; + if (typeof raw.arrayBuffer === "function") { + body = await raw.arrayBuffer(); + } else { + const text = + typeof (raw as { text?: () => Promise }).text === "function" + ? await (raw as { text: () => Promise }).text() + : typeof (raw as { json?: () => Promise }).json === "function" + ? JSON.stringify(await (raw as { json: () => Promise }).json()) + : ""; + body = new TextEncoder().encode(text).buffer; + } + return { + response: new Response(body, { + status: (raw as { status?: number }).status ?? 200, + headers: (raw as { headers?: HeadersInit }).headers, + }), + release: async () => {}, + finalUrl: params.url, + }; + }); +} + describe("send", () => { describe("resolveChatGuidForTarget", () => { const resolveHandleTargetGuid = async (data: Array>) => { @@ -448,6 +475,44 @@ describe("send", () => { expect(body.method).toBeUndefined(); }); + it("auto-enables private-network fetches for loopback serverUrl when allowPrivateNetwork is not set", async () => { + const policies: unknown[] = []; + installSsrFPolicyCapture(policies); + mockResolvedHandleTarget(); + mockSendResponse({ data: { guid: "msg-loopback" } }); + + try { + const result = await sendMessageBlueBubbles("+15551234567", "Hello world!", { + serverUrl: "http://localhost:1234", + password: "test", + }); + + expect(result.messageId).toBe("msg-loopback"); + expect(policies).toEqual([{ allowPrivateNetwork: true }, { allowPrivateNetwork: true }]); + } finally { + _setFetchGuardForTesting(null); + } + }); + + it("auto-enables private-network fetches for private IP serverUrl when allowPrivateNetwork is not set", async () => { + const policies: unknown[] = []; + installSsrFPolicyCapture(policies); + mockResolvedHandleTarget(); + mockSendResponse({ data: { guid: "msg-private-ip" } }); + + try { + const result = await sendMessageBlueBubbles("+15551234567", "Hello world!", { + serverUrl: "http://192.168.1.5:1234", + password: "test", + }); + + expect(result.messageId).toBe("msg-private-ip"); + expect(policies).toEqual([{ allowPrivateNetwork: true }, { allowPrivateNetwork: true }]); + } finally { + _setFetchGuardForTesting(null); + } + }); + it("strips markdown formatting from outbound messages", async () => { mockResolvedHandleTarget(); mockSendResponse({ data: { guid: "msg-uuid-stripped" } }); diff --git a/extensions/bluebubbles/src/send.ts b/extensions/bluebubbles/src/send.ts index e7aca022a26..70d0d1cab4d 100644 --- a/extensions/bluebubbles/src/send.ts +++ b/extensions/bluebubbles/src/send.ts @@ -1,5 +1,5 @@ import crypto from "node:crypto"; -import { resolveBlueBubblesAccount } from "./accounts.js"; +import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { getCachedBlueBubblesPrivateApiStatus, isBlueBubblesPrivateApiStatusEnabled, @@ -7,7 +7,6 @@ import { import type { OpenClawConfig } from "./runtime-api.js"; import { stripMarkdown } from "./runtime-api.js"; import { warnBlueBubbles } from "./runtime.js"; -import { normalizeSecretInputString } from "./secret-input.js"; import { extractBlueBubblesMessageId, resolveBlueBubblesSendTarget } from "./send-helpers.js"; import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js"; import { @@ -446,24 +445,13 @@ export async function sendMessageBlueBubbles( throw new Error("BlueBubbles send requires text (message was empty after markdown removal)"); } - const account = resolveBlueBubblesAccount({ + const { baseUrl, password, accountId, allowPrivateNetwork } = resolveBlueBubblesServerAccount({ cfg: opts.cfg ?? {}, accountId: opts.accountId, + serverUrl: opts.serverUrl, + password: opts.password, }); - const baseUrl = - normalizeSecretInputString(opts.serverUrl) || - normalizeSecretInputString(account.config.serverUrl); - const password = - normalizeSecretInputString(opts.password) || - normalizeSecretInputString(account.config.password); - if (!baseUrl) { - throw new Error("BlueBubbles serverUrl is required"); - } - if (!password) { - throw new Error("BlueBubbles password is required"); - } - const privateApiStatus = getCachedBlueBubblesPrivateApiStatus(account.accountId); - const allowPrivateNetwork = account.config.allowPrivateNetwork === true; + const privateApiStatus = getCachedBlueBubblesPrivateApiStatus(accountId); const target = resolveBlueBubblesSendTarget(to); const chatGuid = await resolveChatGuidForTarget({