diff --git a/scripts/whatsapp-bridge/allowlist.js b/scripts/whatsapp-bridge/allowlist.js index ffc8949a7b..2cb5c634a8 100644 --- a/scripts/whatsapp-bridge/allowlist.js +++ b/scripts/whatsapp-bridge/allowlist.js @@ -63,6 +63,15 @@ export function expandWhatsAppIdentifiers(identifier, sessionDir) { return resolved; } +export function matchesAllowedSender(senderId, senderPn, allowedUsers, sessionDir) { + // WhatsApp Multi-Device can expose a first-contact sender as an opaque LID + // before it persists LID mapping files. Baileys supplies the same sender's + // authenticated phone JID separately in msg.key.senderPn, so consult it + // as an additional alias without changing the allowlist itself. + return matchesAllowedUser(senderId, allowedUsers, sessionDir) + || matchesAllowedUser(senderPn, allowedUsers, sessionDir); +} + export function matchesAllowedUser(senderId, allowedUsers, sessionDir) { // Empty allowlist = NO ONE allowed (secure default, #8389). Operators // who want an open bot must set ``WHATSAPP_ALLOWED_USERS=*`` explicitly. diff --git a/scripts/whatsapp-bridge/allowlist.test.mjs b/scripts/whatsapp-bridge/allowlist.test.mjs index c6ca1cb3c4..42fde36804 100644 --- a/scripts/whatsapp-bridge/allowlist.test.mjs +++ b/scripts/whatsapp-bridge/allowlist.test.mjs @@ -6,6 +6,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { expandWhatsAppIdentifiers, + matchesAllowedSender, matchesAllowedUser, normalizeWhatsAppIdentifier, parseAllowedUsers, @@ -46,6 +47,27 @@ test('matchesAllowedUser accepts mapped lid sender when allowlist only contains } }); +test('matchesAllowedSender accepts Baileys senderPn when a first-contact LID has no mapping yet', () => { + const sessionDir = mkdtempSync(path.join(os.tmpdir(), 'hermes-wa-allowlist-')); + + try { + const allowedUsers = parseAllowedUsers('+19175395595'); + // On a first message from a WhatsApp Multi-Device contact, Baileys can + // emit a LID as senderId before it has written LID mapping files, while + // senderPn carries WhatsApp's authenticated phone JID. + assert.equal( + matchesAllowedSender('267383306489914@lid', '19175395595@s.whatsapp.net', allowedUsers, sessionDir), + true, + ); + assert.equal( + matchesAllowedSender('267383306489914@lid', '18881234567@s.whatsapp.net', allowedUsers, sessionDir), + false, + ); + } finally { + rmSync(sessionDir, { recursive: true, force: true }); + } +}); + test('matchesAllowedUser treats * as allow-all wildcard', () => { const sessionDir = mkdtempSync(path.join(os.tmpdir(), 'hermes-wa-allowlist-')); diff --git a/scripts/whatsapp-bridge/bridge.js b/scripts/whatsapp-bridge/bridge.js index b00a1fec02..830247f10d 100644 --- a/scripts/whatsapp-bridge/bridge.js +++ b/scripts/whatsapp-bridge/bridge.js @@ -30,7 +30,7 @@ import { randomBytes, createHash } from 'crypto'; import { execFileSync } from 'child_process'; import { tmpdir } from 'os'; import qrcode from 'qrcode-terminal'; -import { matchesAllowedUser, parseAllowedUsers } from './allowlist.js'; +import { matchesAllowedSender, matchesAllowedUser, parseAllowedUsers } from './allowlist.js'; import { createOutboundIdTracker } from './outbound_ids.js'; import { classifyOwnerMessageGate } from './owner_message_gate.js'; import { @@ -525,8 +525,10 @@ async function startSocket() { const chatId = msg.key.remoteJid; const senderId = msg.key.participant || chatId; + const senderPn = msg.key.senderPn || ''; + const resolvedSenderId = senderPn || senderId; const isGroup = chatId.endsWith('@g.us'); - const senderNumber = senderId.replace(/@.*/, ''); + const senderNumber = resolvedSenderId.replace(/@.*/, ''); emitDebugEvent({ stage: 'upsert', type, @@ -627,7 +629,7 @@ async function startSocket() { } catch {} continue; } - if (WHATSAPP_DM_POLICY !== 'pairing' && !matchesAllowedUser(senderId, ALLOWED_USERS, SESSION_DIR)) { + if (WHATSAPP_DM_POLICY !== 'pairing' && !matchesAllowedSender(senderId, senderPn, ALLOWED_USERS, SESSION_DIR)) { try { console.log(JSON.stringify({ event: 'ignored', @@ -703,7 +705,7 @@ async function startSocket() { const event = await extractBridgeEvent({ msg, chatId, - senderId, + senderId: resolvedSenderId, senderNumber, botIds, isGroup,