diff --git a/extensions/whatsapp/src/auto-reply/monitor/on-message.audio-preflight.test.ts b/extensions/whatsapp/src/auto-reply/monitor/on-message.audio-preflight.test.ts index 5d8c33c2b9e..93791fe5b9b 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/on-message.audio-preflight.test.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/on-message.audio-preflight.test.ts @@ -74,6 +74,7 @@ function makeAudioMsg(): WebInboundMsg { id: "msg-1", from: "+15550000002", to: "+15550000001", + accessControlPassed: true, body: "", chatType: "direct", mediaType: "audio/ogg; codecs=opus", @@ -146,4 +147,45 @@ describe("createWebOnMessageHandler audio preflight", () => { }), ); }); + + it("skips early DM ack/preflight when access-control was not explicitly passed through", async () => { + const handler = createWebOnMessageHandler({ + cfg: { + channels: { + whatsapp: { + ackReaction: { enabled: true }, + }, + }, + } as never, + verbose: false, + connectionId: "conn-1", + maxMediaBytes: 1024 * 1024, + groupHistoryLimit: 20, + groupHistories: new Map(), + groupMemberNames: new Map(), + echoTracker: makeEchoTracker() as never, + backgroundTasks: new Set(), + replyResolver: vi.fn() as never, + replyLogger: { + info: () => {}, + warn: () => {}, + debug: () => {}, + error: () => {}, + } as never, + baseMentionConfig: {} as never, + account: { authDir: "/tmp/auth", accountId: "default" }, + }); + + await handler({ ...makeAudioMsg(), accessControlPassed: undefined }); + + expect(events).toEqual([]); + expect(transcribeFirstAudioMock).not.toHaveBeenCalled(); + expect(maybeSendAckReactionMock).not.toHaveBeenCalled(); + expect(processMessageMock).toHaveBeenCalledWith( + expect.not.objectContaining({ + preflightAudioTranscript: expect.anything(), + ackAlreadySent: true, + }), + ); + }); }); diff --git a/extensions/whatsapp/src/auto-reply/monitor/on-message.ts b/extensions/whatsapp/src/auto-reply/monitor/on-message.ts index 0a84f9cd2e3..d79372600fa 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/on-message.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/on-message.ts @@ -176,13 +176,17 @@ export function createWebOnMessageHandler(params: { // Preflight audio transcription: run once here, before broadcast fan-out, so // all agents share the same transcript instead of each making a separate STT call. + // For DMs, only do this on the real inbound path after access-control/pairing + // checks have already passed in inbound/monitor.ts. That keeps external STT and + // early ack feedback behind the same auth-first gate as the rest of DM handling. // null = preflight was attempted but produced no transcript (failed / disabled / no audio); // undefined = preflight was not attempted (non-audio message). let preflightAudioTranscript: string | null | undefined; const hasAudioBody = msg.mediaType?.startsWith("audio/") === true && msg.body === ""; + const canRunEarlyDmPreflight = msg.chatType === "group" || msg.accessControlPassed === true; let ackAlreadySent = false; - if (hasAudioBody && msg.mediaPath) { + if (canRunEarlyDmPreflight && hasAudioBody && msg.mediaPath) { await maybeSendAckReaction({ cfg: params.cfg, msg, diff --git a/extensions/whatsapp/src/inbound/monitor.ts b/extensions/whatsapp/src/inbound/monitor.ts index ef73d214d70..1926904e19d 100644 --- a/extensions/whatsapp/src/inbound/monitor.ts +++ b/extensions/whatsapp/src/inbound/monitor.ts @@ -567,6 +567,7 @@ export async function attachWebInboxToSocket( conversationId: inbound.from, to: self.e164 ?? "me", accountId: inbound.access.resolvedAccountId, + accessControlPassed: true, body: enriched.body, pushName: senderName, timestamp, diff --git a/extensions/whatsapp/src/inbound/types.ts b/extensions/whatsapp/src/inbound/types.ts index 0890dc65231..f30c333b9f4 100644 --- a/extensions/whatsapp/src/inbound/types.ts +++ b/extensions/whatsapp/src/inbound/types.ts @@ -57,6 +57,8 @@ export type WebInboundMessage = { conversationId: string; // alias for clarity (same as from) to: string; accountId: string; + /** Set by the real inbound monitor after access-control / pairing checks pass. */ + accessControlPassed?: boolean; body: string; pushName?: string; timestamp?: number; diff --git a/extensions/whatsapp/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts b/extensions/whatsapp/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts index 677993d7bc6..16cf52798f3 100644 --- a/extensions/whatsapp/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +++ b/extensions/whatsapp/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts @@ -150,7 +150,12 @@ describe("web monitor inbox", () => { expect(onMessage).toHaveBeenCalledTimes(1); expect(onMessage).toHaveBeenCalledWith( - expect.objectContaining({ from: "+123", to: "+123", body: "self ping" }), + expect.objectContaining({ + from: "+123", + to: "+123", + body: "self ping", + accessControlPassed: true, + }), ); expect(sock.readMessages).not.toHaveBeenCalled();