diff --git a/extensions/whatsapp/src/auto-reply/monitor/process-message.inbound-context.test.ts b/extensions/whatsapp/src/auto-reply/monitor/process-message.inbound-context.test.ts index 4b321ea8579..2798f017cbb 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/process-message.inbound-context.test.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/process-message.inbound-context.test.ts @@ -510,5 +510,10 @@ describe("web processMessage inbound context", () => { await processMessage(args); expect(updateLastRouteMock).toHaveBeenCalledTimes(1); + expect(updateLastRouteMock).toHaveBeenCalledWith( + expect.objectContaining({ + to: "+553598627740", + }), + ); }); }); diff --git a/extensions/whatsapp/src/auto-reply/monitor/process-message.ts b/extensions/whatsapp/src/auto-reply/monitor/process-message.ts index 069641b5a28..1694c0ea3cd 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/process-message.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/process-message.ts @@ -344,17 +344,18 @@ export async function processMessage(params: { cfg: params.cfg, msg: params.msg, }); - const shouldUpdateMainLastRoute = - !pinnedMainDmRecipient || - (dmRouteTarget - ? areEquivalentWhatsAppDirectTargets(pinnedMainDmRecipient, dmRouteTarget) - : false); + const canonicalMainLastRouteTarget = pinnedMainDmRecipient + ? dmRouteTarget && areEquivalentWhatsAppDirectTargets(pinnedMainDmRecipient, dmRouteTarget) + ? pinnedMainDmRecipient + : null + : dmRouteTarget; + const shouldUpdateMainLastRoute = Boolean(canonicalMainLastRouteTarget); const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({ route: params.route, sessionKey: params.route.sessionKey, }); if ( - dmRouteTarget && + canonicalMainLastRouteTarget && inboundLastRouteSessionKey === params.route.mainSessionKey && shouldUpdateMainLastRoute ) { @@ -364,7 +365,7 @@ export async function processMessage(params: { storeAgentId: params.route.agentId, sessionKey: params.route.mainSessionKey, channel: "whatsapp", - to: dmRouteTarget, + to: canonicalMainLastRouteTarget, accountId: params.route.accountId, ctx: ctxPayload, warn: params.replyLogger.warn.bind(params.replyLogger), diff --git a/extensions/whatsapp/src/inbound/access-control.test.ts b/extensions/whatsapp/src/inbound/access-control.test.ts index 0b05cf99267..0e966ab0b28 100644 --- a/extensions/whatsapp/src/inbound/access-control.test.ts +++ b/extensions/whatsapp/src/inbound/access-control.test.ts @@ -158,6 +158,33 @@ describe("WhatsApp dmPolicy precedence", () => { expect(sendMessageMock).not.toHaveBeenCalled(); }); + it("treats BR ninth-digit variants as the same self-chat phone", async () => { + setAccessControlTestConfig({ + channels: { + whatsapp: { + dmPolicy: "pairing", + allowFrom: ["+5511988887777"], + }, + }, + }); + + const result = await checkInboundAccessControl({ + accountId: "default", + from: "+5535998627740", + selfE164: "+553598627740", + senderE164: "+5535998627740", + group: false, + pushName: "Owner", + isFromMe: false, + sock: { sendMessage: sendMessageMock }, + remoteJid: "5535998627740@s.whatsapp.net", + }); + + expect(result.allowed).toBe(true); + expect(upsertPairingRequestMock).not.toHaveBeenCalled(); + expect(sendMessageMock).not.toHaveBeenCalled(); + }); + it("allows BR contacts when allowFrom and sender differ only by the ninth digit", async () => { setAccessControlTestConfig({ channels: { diff --git a/extensions/whatsapp/src/inbound/access-control.ts b/extensions/whatsapp/src/inbound/access-control.ts index 09d95b4a75e..9e697cfc5ab 100644 --- a/extensions/whatsapp/src/inbound/access-control.ts +++ b/extensions/whatsapp/src/inbound/access-control.ts @@ -73,7 +73,9 @@ export async function checkInboundAccessControl(params: { const dmAllowFrom = configuredAllowFrom.length > 0 ? configuredAllowFrom : defaultAllowFrom; const groupAllowFrom = account.groupAllowFrom ?? (configuredAllowFrom.length > 0 ? configuredAllowFrom : undefined); - const isSamePhone = params.from === params.selfE164; + const isSamePhone = params.selfE164 + ? areEquivalentWhatsAppDirectTargets(params.from, params.selfE164) + : false; const isSelfChat = account.selfChatMode ?? isSelfChatMode(params.selfE164, configuredAllowFrom); const pairingGraceMs = typeof params.pairingGraceMs === "number" && params.pairingGraceMs > 0 diff --git a/src/channels/plugins/whatsapp-heartbeat.test.ts b/src/channels/plugins/whatsapp-heartbeat.test.ts index 3cc6531eca1..835e7cd2d01 100644 --- a/src/channels/plugins/whatsapp-heartbeat.test.ts +++ b/src/channels/plugins/whatsapp-heartbeat.test.ts @@ -66,6 +66,17 @@ describe("resolveWhatsAppHeartbeatRecipients", () => { expect(result).toEqual({ recipients: ["+15550000001"], source: "session-single" }); }); + it("treats BR ninth-digit variants as authorized session recipients", () => { + setSessionStore({ + a: { lastChannel: "whatsapp", lastTo: "+5535998627740", updatedAt: 2, sessionId: "a" }, + }); + setAllowFromStore(["+553598627740"]); + + const result = resolveWith(); + + expect(result).toEqual({ recipients: ["+5535998627740"], source: "session-single" }); + }); + it("falls back to allowFrom when no session recipient is authorized", () => { setSingleUnauthorizedSessionWithAllowFrom(); diff --git a/src/channels/plugins/whatsapp-heartbeat.ts b/src/channels/plugins/whatsapp-heartbeat.ts index 35ec38d422a..c4d25f5a769 100644 --- a/src/channels/plugins/whatsapp-heartbeat.ts +++ b/src/channels/plugins/whatsapp-heartbeat.ts @@ -3,6 +3,7 @@ import { loadSessionStore, resolveStorePath } from "../../config/sessions.js"; import { readChannelAllowFromStoreSync } from "../../pairing/pairing-store.js"; import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; import { normalizeE164 } from "../../utils.js"; +import { areEquivalentWhatsAppDirectTargets } from "../../whatsapp/normalize.js"; import { normalizeChatChannelId } from "../registry.js"; type HeartbeatRecipientsResult = { recipients: string[]; source: string }; @@ -72,10 +73,13 @@ export function resolveWhatsAppHeartbeatRecipients( } if (allowFrom.length > 0) { - const allowSet = new Set(allowFrom); const authorizedSessionRecipients = sessionRecipients .map((entry) => entry.to) - .filter((recipient) => allowSet.has(recipient)); + .filter((recipient) => + allowFrom.some((allowedRecipient) => + areEquivalentWhatsAppDirectTargets(allowedRecipient, recipient), + ), + ); if (authorizedSessionRecipients.length === 1) { return { recipients: [authorizedSessionRecipients[0]], source: "session-single" }; }