feat: unify group policy allowlists

This commit is contained in:
Peter Steinberger
2026-01-06 06:40:42 +00:00
parent 51e8bbd2a8
commit dbb51006cd
23 changed files with 729 additions and 88 deletions

View File

@@ -178,18 +178,30 @@ export async function monitorWebInbox(options: {
configuredAllowFrom && configuredAllowFrom.length > 0
? configuredAllowFrom
: defaultAllowFrom;
const groupAllowFrom =
cfg.whatsapp?.groupAllowFrom ??
(configuredAllowFrom && configuredAllowFrom.length > 0
? configuredAllowFrom
: undefined);
const isSamePhone = from === selfE164;
const isSelfChat = isSelfChatMode(selfE164, configuredAllowFrom);
// Pre-compute normalized allowlist for filtering (used by both group and DM checks)
const hasWildcard = allowFrom?.includes("*") ?? false;
// Pre-compute normalized allowlists for filtering
const dmHasWildcard = allowFrom?.includes("*") ?? false;
const normalizedAllowFrom =
allowFrom && allowFrom.length > 0 ? allowFrom.map(normalizeE164) : [];
allowFrom && allowFrom.length > 0
? allowFrom.filter((entry) => entry !== "*").map(normalizeE164)
: [];
const groupHasWildcard = groupAllowFrom?.includes("*") ?? false;
const normalizedGroupAllowFrom =
groupAllowFrom && groupAllowFrom.length > 0
? groupAllowFrom.filter((entry) => entry !== "*").map(normalizeE164)
: [];
// Group policy filtering: controls how group messages are handled
// - "open" (default): groups bypass allowFrom, only mention-gating applies
// - "disabled": block all group messages entirely
// - "allowlist": only allow group messages from senders in allowFrom
// - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom
const groupPolicy = cfg.whatsapp?.groupPolicy ?? "open";
if (group && groupPolicy === "disabled") {
logVerbose(`Blocked group message (groupPolicy: disabled)`);
@@ -198,9 +210,15 @@ export async function monitorWebInbox(options: {
if (group && groupPolicy === "allowlist") {
// For allowlist mode, the sender (participant) must be in allowFrom
// If we can't resolve the sender E164, block the message for safety
if (!groupAllowFrom || groupAllowFrom.length === 0) {
logVerbose(
"Blocked group message (groupPolicy: allowlist, no groupAllowFrom)",
);
continue;
}
const senderAllowed =
hasWildcard ||
(senderE164 != null && normalizedAllowFrom.includes(senderE164));
groupHasWildcard ||
(senderE164 != null && normalizedGroupAllowFrom.includes(senderE164));
if (!senderAllowed) {
logVerbose(
`Blocked group message from ${senderE164 ?? "unknown sender"} (groupPolicy: allowlist)`,
@@ -214,7 +232,7 @@ export async function monitorWebInbox(options: {
!group && Array.isArray(allowFrom) && allowFrom.length > 0;
if (!isSamePhone && allowlistEnabled) {
const candidate = from;
if (!hasWildcard && !normalizedAllowFrom.includes(candidate)) {
if (!dmHasWildcard && !normalizedAllowFrom.includes(candidate)) {
logVerbose(
`Blocked unauthorized sender ${candidate} (not in allowFrom list)`,
);

View File

@@ -711,10 +711,10 @@ describe("web monitor inbox", () => {
await listener.close();
});
it("blocks group messages from senders not in allowFrom when groupPolicy is 'allowlist'", async () => {
it("blocks group messages from senders not in groupAllowFrom when groupPolicy is 'allowlist'", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["+1234"], // Does not include +999
groupAllowFrom: ["+1234"], // Does not include +999
groupPolicy: "allowlist",
},
messages: {
@@ -746,16 +746,16 @@ describe("web monitor inbox", () => {
sock.ev.emit("messages.upsert", upsert);
await new Promise((resolve) => setImmediate(resolve));
// Should NOT call onMessage because sender +999 not in allowFrom
// Should NOT call onMessage because sender +999 not in groupAllowFrom
expect(onMessage).not.toHaveBeenCalled();
await listener.close();
});
it("allows group messages from senders in allowFrom when groupPolicy is 'allowlist'", async () => {
it("allows group messages from senders in groupAllowFrom when groupPolicy is 'allowlist'", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["+15551234567"], // Includes the sender
groupAllowFrom: ["+15551234567"], // Includes the sender
groupPolicy: "allowlist",
},
messages: {
@@ -787,7 +787,7 @@ describe("web monitor inbox", () => {
sock.ev.emit("messages.upsert", upsert);
await new Promise((resolve) => setImmediate(resolve));
// Should call onMessage because sender is in allowFrom
// Should call onMessage because sender is in groupAllowFrom
expect(onMessage).toHaveBeenCalledTimes(1);
const payload = onMessage.mock.calls[0][0];
expect(payload.chatType).toBe("group");
@@ -799,7 +799,7 @@ describe("web monitor inbox", () => {
it("allows all group senders with wildcard in groupPolicy allowlist", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
allowFrom: ["*"], // Wildcard allows everyone
groupAllowFrom: ["*"], // Wildcard allows everyone
groupPolicy: "allowlist",
},
messages: {
@@ -839,6 +839,45 @@ describe("web monitor inbox", () => {
await listener.close();
});
it("blocks group messages when groupPolicy allowlist has no groupAllowFrom", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {
groupPolicy: "allowlist",
},
messages: {
messagePrefix: undefined,
responsePrefix: undefined,
timestampPrefix: false,
},
});
const onMessage = vi.fn();
const listener = await monitorWebInbox({ verbose: false, onMessage });
const sock = await createWaSocket();
const upsert = {
type: "notify",
messages: [
{
key: {
id: "grp-allowlist-empty",
fromMe: false,
remoteJid: "11111@g.us",
participant: "999@s.whatsapp.net",
},
message: { conversation: "blocked by empty allowlist" },
},
],
};
sock.ev.emit("messages.upsert", upsert);
await new Promise((resolve) => setImmediate(resolve));
expect(onMessage).not.toHaveBeenCalled();
await listener.close();
});
it("allows messages from senders in allowFrom list", async () => {
mockLoadConfig.mockReturnValue({
whatsapp: {