From eabcac5ca243228dc6e68bb86e84a6950c0eae2e Mon Sep 17 00:00:00 2001 From: Thiago Roque Ragazzo Date: Fri, 20 Mar 2026 03:06:20 -0300 Subject: [PATCH 1/3] whatsapp: normalize BR ninth-digit variants --- .../src/inbound/access-control.test.ts | 27 +++++++++++++ .../whatsapp/src/inbound/access-control.ts | 20 ++++++---- src/plugin-sdk/whatsapp-shared.ts | 7 +++- src/plugin-sdk/whatsapp.ts | 7 +++- src/whatsapp/normalize.test.ts | 38 ++++++++++++++++++- src/whatsapp/normalize.ts | 36 ++++++++++++++++++ src/whatsapp/resolve-outbound-target.test.ts | 17 +++++++++ src/whatsapp/resolve-outbound-target.ts | 16 ++++++-- 8 files changed, 155 insertions(+), 13 deletions(-) diff --git a/extensions/whatsapp/src/inbound/access-control.test.ts b/extensions/whatsapp/src/inbound/access-control.test.ts index 2d3e26650c7..0b05cf99267 100644 --- a/extensions/whatsapp/src/inbound/access-control.test.ts +++ b/extensions/whatsapp/src/inbound/access-control.test.ts @@ -157,4 +157,31 @@ describe("WhatsApp dmPolicy precedence", () => { 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: { + whatsapp: { + dmPolicy: "allowlist", + allowFrom: ["+553598627740"], + }, + }, + }); + + const result = await checkInboundAccessControl({ + accountId: "default", + from: "+5535998627740", + selfE164: "+5519999835286", + senderE164: "+5535998627740", + group: false, + pushName: "Jordan", + isFromMe: false, + sock: { sendMessage: sendMessageMock }, + remoteJid: "553598627740@s.whatsapp.net", + }); + + expect(result.allowed).toBe(true); + expect(upsertPairingRequestMock).not.toHaveBeenCalled(); + expect(sendMessageMock).not.toHaveBeenCalled(); + }); }); diff --git a/extensions/whatsapp/src/inbound/access-control.ts b/extensions/whatsapp/src/inbound/access-control.ts index 95fe6dd487a..09d95b4a75e 100644 --- a/extensions/whatsapp/src/inbound/access-control.ts +++ b/extensions/whatsapp/src/inbound/access-control.ts @@ -12,6 +12,7 @@ import { resolveDmGroupAccessWithLists, } from "openclaw/plugin-sdk/security-runtime"; import { isSelfChatMode, normalizeE164 } from "openclaw/plugin-sdk/text-runtime"; +import { areEquivalentWhatsAppDirectTargets } from "openclaw/plugin-sdk/whatsapp-shared"; import { resolveWhatsAppAccount } from "../accounts.js"; export type InboundAccessControlResult = { @@ -115,17 +116,22 @@ export async function checkInboundAccessControl(params: { if (hasWildcard) { return true; } - const normalizedEntrySet = new Set( - allowEntries - .map((entry) => normalizeE164(String(entry))) - .filter((entry): entry is string => Boolean(entry)), - ); + const normalizedEntries = allowEntries + .map((entry) => normalizeE164(String(entry))) + .filter((entry): entry is string => Boolean(entry)); if (!params.group && isSamePhone) { return true; } return params.group - ? Boolean(normalizedGroupSender && normalizedEntrySet.has(normalizedGroupSender)) - : normalizedEntrySet.has(normalizedDmSender); + ? Boolean( + normalizedGroupSender && + normalizedEntries.some((entry) => + areEquivalentWhatsAppDirectTargets(entry, normalizedGroupSender), + ), + ) + : normalizedEntries.some((entry) => + areEquivalentWhatsAppDirectTargets(entry, normalizedDmSender), + ); }, }); if (params.group && access.decision !== "allow") { diff --git a/src/plugin-sdk/whatsapp-shared.ts b/src/plugin-sdk/whatsapp-shared.ts index d1794898bc3..463af4d7360 100644 --- a/src/plugin-sdk/whatsapp-shared.ts +++ b/src/plugin-sdk/whatsapp-shared.ts @@ -6,4 +6,9 @@ export { resolveWhatsAppMentionStripRegexes, } from "../channels/plugins/whatsapp-shared.js"; export { resolveWhatsAppHeartbeatRecipients } from "../channels/plugins/whatsapp-heartbeat.js"; -export { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../whatsapp/normalize.js"; +export { + areEquivalentWhatsAppDirectTargets, + expandWhatsAppDirectTargetVariants, + isWhatsAppGroupJid, + normalizeWhatsAppTarget, +} from "../whatsapp/normalize.js"; diff --git a/src/plugin-sdk/whatsapp.ts b/src/plugin-sdk/whatsapp.ts index 0c4e0a5048b..1163faceb66 100644 --- a/src/plugin-sdk/whatsapp.ts +++ b/src/plugin-sdk/whatsapp.ts @@ -40,7 +40,12 @@ export { } from "../channels/plugins/group-policy-warnings.js"; export { buildAccountScopedDmSecurityPolicy } from "../channels/plugins/helpers.js"; export { resolveWhatsAppOutboundTarget } from "../whatsapp/resolve-outbound-target.js"; -export { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../whatsapp/normalize.js"; +export { + areEquivalentWhatsAppDirectTargets, + expandWhatsAppDirectTargetVariants, + isWhatsAppGroupJid, + normalizeWhatsAppTarget, +} from "../whatsapp/normalize.js"; export { resolveAllowlistProviderRuntimeGroupPolicy, diff --git a/src/whatsapp/normalize.test.ts b/src/whatsapp/normalize.test.ts index 330a1022588..7865bb70fd4 100644 --- a/src/whatsapp/normalize.test.ts +++ b/src/whatsapp/normalize.test.ts @@ -1,5 +1,11 @@ import { describe, expect, it } from "vitest"; -import { isWhatsAppGroupJid, isWhatsAppUserTarget, normalizeWhatsAppTarget } from "./normalize.js"; +import { + areEquivalentWhatsAppDirectTargets, + expandWhatsAppDirectTargetVariants, + isWhatsAppGroupJid, + isWhatsAppUserTarget, + normalizeWhatsAppTarget, +} from "./normalize.js"; describe("normalizeWhatsAppTarget", () => { it("preserves group JIDs", () => { @@ -58,6 +64,36 @@ describe("isWhatsAppUserTarget", () => { }); }); +describe("expandWhatsAppDirectTargetVariants", () => { + it("treats BR mobile numbers with and without the ninth digit as equivalent", () => { + expect(expandWhatsAppDirectTargetVariants("+5535998627740")).toEqual([ + "+5535998627740", + "+553598627740", + ]); + expect(expandWhatsAppDirectTargetVariants("+553598627740")).toEqual([ + "+553598627740", + "+5535998627740", + ]); + }); + + it("does not invent ninth-digit variants for non-BR numbers or groups", () => { + expect(expandWhatsAppDirectTargetVariants("+15551234567")).toEqual(["+15551234567"]); + expect(expandWhatsAppDirectTargetVariants("120363401234567890@g.us")).toEqual([ + "120363401234567890@g.us", + ]); + }); +}); + +describe("areEquivalentWhatsAppDirectTargets", () => { + it("matches BR ninth-digit variants", () => { + expect(areEquivalentWhatsAppDirectTargets("+5535998627740", "+553598627740")).toBe(true); + }); + + it("does not match unrelated numbers", () => { + expect(areEquivalentWhatsAppDirectTargets("+5535998627740", "+5535998627741")).toBe(false); + }); +}); + describe("isWhatsAppGroupJid", () => { it("detects group JIDs with or without prefixes", () => { expect(isWhatsAppGroupJid("120363401234567890@g.us")).toBe(true); diff --git a/src/whatsapp/normalize.ts b/src/whatsapp/normalize.ts index 1d661ddc75f..3661e41e834 100644 --- a/src/whatsapp/normalize.ts +++ b/src/whatsapp/normalize.ts @@ -78,3 +78,39 @@ export function normalizeWhatsAppTarget(value: string): string | null { const normalized = normalizeE164(candidate); return normalized.length > 1 ? normalized : null; } + +/** + * Expand direct-target variants that WhatsApp may surface differently from the human-facing phone. + * + * Brazil is the main footgun here: some chats/contacts resolve with the mobile ninth digit removed, + * while humans/config entries often keep the full E.164 with the extra 9. Treat both forms as + * equivalent for WhatsApp matching. + */ +export function expandWhatsAppDirectTargetVariants(value: string): string[] { + const normalized = normalizeWhatsAppTarget(value); + if (!normalized || isWhatsAppGroupJid(normalized)) { + return normalized ? [normalized] : []; + } + const variants = new Set([normalized]); + const digits = normalized.replace(/\D/g, ""); + + // BR mobile with ninth digit: +55 AA 9 XXXXXXXX -> +55 AA XXXXXXXX + const withNinthDigit = digits.match(/^55(\d{2})9([6-9]\d{7})$/); + if (withNinthDigit) { + variants.add(`+55${withNinthDigit[1]}${withNinthDigit[2]}`); + } + + // BR mobile without ninth digit: +55 AA XXXXXXXX -> +55 AA 9 XXXXXXXX + const withoutNinthDigit = digits.match(/^55(\d{2})([6-9]\d{7})$/); + if (withoutNinthDigit) { + variants.add(`+55${withoutNinthDigit[1]}9${withoutNinthDigit[2]}`); + } + + return [...variants]; +} + +export function areEquivalentWhatsAppDirectTargets(a: string, b: string): boolean { + const left = expandWhatsAppDirectTargetVariants(a); + const right = new Set(expandWhatsAppDirectTargetVariants(b)); + return left.some((candidate) => right.has(candidate)); +} diff --git a/src/whatsapp/resolve-outbound-target.test.ts b/src/whatsapp/resolve-outbound-target.test.ts index 4d7d16b4393..9a1009ed6db 100644 --- a/src/whatsapp/resolve-outbound-target.test.ts +++ b/src/whatsapp/resolve-outbound-target.test.ts @@ -137,6 +137,23 @@ describe("resolveWhatsAppOutboundTarget", () => { expectAllowedForTarget({ allowFrom: [PRIMARY_TARGET], mode: "implicit" }); }); + it("reuses the allowlist's canonical BR variant when ninth-digit forms differ", () => { + vi.mocked(normalize.normalizeWhatsAppTarget) + .mockReturnValueOnce("+553598627740") + .mockReturnValueOnce("+5535998627740"); + vi.mocked(normalize.isWhatsAppGroupJid).mockReturnValueOnce(false); + vi.mocked(normalize.areEquivalentWhatsAppDirectTargets).mockReturnValueOnce(true); + + expectResolutionOk( + { + to: "+5535998627740", + allowFrom: ["+553598627740"], + mode: "implicit", + }, + "+553598627740", + ); + }); + it("denies message when target is not in allowList", () => { mockNormalizedDirectMessage(PRIMARY_TARGET, SECONDARY_TARGET); expectDeniedForTarget({ allowFrom: [SECONDARY_TARGET], mode: "implicit" }); diff --git a/src/whatsapp/resolve-outbound-target.ts b/src/whatsapp/resolve-outbound-target.ts index 7ad7af1cd4c..cda64bc9729 100644 --- a/src/whatsapp/resolve-outbound-target.ts +++ b/src/whatsapp/resolve-outbound-target.ts @@ -1,5 +1,9 @@ import { missingTargetError } from "../infra/outbound/target-errors.js"; -import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "./normalize.js"; +import { + areEquivalentWhatsAppDirectTargets, + isWhatsAppGroupJid, + normalizeWhatsAppTarget, +} from "./normalize.js"; export type WhatsAppOutboundTargetResolution = | { ok: true; to: string } @@ -36,8 +40,14 @@ export function resolveWhatsAppOutboundTarget(params: { if (hasWildcard || allowList.length === 0) { return { ok: true, to: normalizedTo }; } - if (allowList.includes(normalizedTo)) { - return { ok: true, to: normalizedTo }; + const matchedAllowTarget = allowList.find( + (entry) => entry === normalizedTo || areEquivalentWhatsAppDirectTargets(entry, normalizedTo), + ); + if (matchedAllowTarget) { + // Prefer the allowlist's canonical entry so outbound sends reuse the exact JID/E.164 form + // that the operator previously authorized (important for BR contacts that surface without + // the mobile ninth digit on WhatsApp). + return { ok: true, to: matchedAllowTarget }; } return { ok: false, From 43ae3bc489b53474e5d959dda3099f092ff81bbb Mon Sep 17 00:00:00 2001 From: Thiago Roque Ragazzo Date: Fri, 20 Mar 2026 13:26:28 -0300 Subject: [PATCH 2/3] fix(whatsapp): cover BR variant checks in monitor routing --- .../process-message.inbound-context.test.ts | 71 +++++++++++++++++++ .../src/auto-reply/monitor/process-message.ts | 10 ++- 2 files changed, 79 insertions(+), 2 deletions(-) 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 c6db2affda3..4b321ea8579 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 @@ -350,6 +350,41 @@ describe("web processMessage inbound context", () => { expect(updateLastRouteMock).toHaveBeenCalledTimes(1); }); + it("authorizes control commands when BR ninth-digit variants differ", async () => { + capturedCtx = undefined; + + await processMessage( + makeProcessMessageArgs({ + routeSessionKey: "agent:main:whatsapp:direct:+5535998627740", + groupHistoryKey: "+5535998627740", + cfg: { + channels: { + whatsapp: { + dmPolicy: "allowlist", + allowFrom: ["+553598627740"], + }, + }, + commands: { useAccessGroups: true }, + messages: {}, + session: { store: sessionStorePath }, + } as unknown as ReturnType, + msg: { + id: "msg-command-br-variant", + from: "+5535998627740", + to: "+2000", + chatType: "direct", + body: "/status", + senderE164: "+5535998627740", + selfE164: "+5519999835286", + accountId: "default", + }, + }), + ); + + expect(capturedCtx).toBeTruthy(); + expect((capturedCtx as { CommandAuthorized?: boolean }).CommandAuthorized).toBe(true); + }); + it("does not update main last route for isolated DM scope sessions", async () => { const updateLastRouteMock = vi.mocked(updateLastRouteInBackground); updateLastRouteMock.mockClear(); @@ -440,4 +475,40 @@ describe("web processMessage inbound context", () => { expect(updateLastRouteMock).toHaveBeenCalledTimes(1); }); + + it("updates main last route when pinned owner and sender differ only by BR ninth digit", async () => { + const updateLastRouteMock = vi.mocked(updateLastRouteInBackground); + updateLastRouteMock.mockClear(); + + const args = makeProcessMessageArgs({ + routeSessionKey: "agent:main:main", + groupHistoryKey: "+5535998627740", + cfg: { + channels: { + whatsapp: { + allowFrom: ["+553598627740"], + }, + }, + messages: {}, + session: { store: sessionStorePath, dmScope: "main" }, + } as unknown as ReturnType, + msg: { + id: "msg-last-route-br-variant", + from: "+5535998627740", + to: "+2000", + chatType: "direct", + body: "hello", + senderE164: "+5535998627740", + }, + }); + args.route = { + ...args.route, + sessionKey: "agent:main:main", + mainSessionKey: "agent:main:main", + }; + + await processMessage(args); + + expect(updateLastRouteMock).toHaveBeenCalledTimes(1); + }); }); diff --git a/extensions/whatsapp/src/auto-reply/monitor/process-message.ts b/extensions/whatsapp/src/auto-reply/monitor/process-message.ts index 067087f87d3..069641b5a28 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/process-message.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/process-message.ts @@ -30,6 +30,7 @@ import { resolveDmGroupAccessWithCommandGate, } from "openclaw/plugin-sdk/security-runtime"; import { jidToE164, normalizeE164 } from "openclaw/plugin-sdk/text-runtime"; +import { areEquivalentWhatsAppDirectTargets } from "openclaw/plugin-sdk/whatsapp-shared"; import { resolveWhatsAppAccount } from "../../accounts.js"; import { newConnectionId } from "../../reconnect.js"; import { formatError } from "../../session.js"; @@ -101,7 +102,9 @@ async function resolveWhatsAppCommandAuthorized(params: { const normalizedEntries = allowEntries .map((entry) => normalizeE164(String(entry))) .filter((entry): entry is string => Boolean(entry)); - return normalizedEntries.includes(senderE164); + return normalizedEntries.some((entry) => + areEquivalentWhatsAppDirectTargets(entry, senderE164), + ); }, command: { useAccessGroups, @@ -342,7 +345,10 @@ export async function processMessage(params: { msg: params.msg, }); const shouldUpdateMainLastRoute = - !pinnedMainDmRecipient || pinnedMainDmRecipient === dmRouteTarget; + !pinnedMainDmRecipient || + (dmRouteTarget + ? areEquivalentWhatsAppDirectTargets(pinnedMainDmRecipient, dmRouteTarget) + : false); const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({ route: params.route, sessionKey: params.route.sessionKey, From 5247c293314e3d898cb28fbfe9e9c97c44f242ff Mon Sep 17 00:00:00 2001 From: Thiago Roque Ragazzo Date: Fri, 20 Mar 2026 13:54:23 -0300 Subject: [PATCH 3/3] fix(whatsapp): preserve BR variants across self-chat and heartbeat --- .../process-message.inbound-context.test.ts | 5 ++++ .../src/auto-reply/monitor/process-message.ts | 15 ++++++----- .../src/inbound/access-control.test.ts | 27 +++++++++++++++++++ .../whatsapp/src/inbound/access-control.ts | 4 ++- .../plugins/whatsapp-heartbeat.test.ts | 11 ++++++++ src/channels/plugins/whatsapp-heartbeat.ts | 8 ++++-- 6 files changed, 60 insertions(+), 10 deletions(-) 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" }; }