fix(whatsapp): thread authDir through command authorization and owner bypass for LID JID resolution (#93379)

* fix(whatsapp): thread authDir through command authorization and owner bypass for LID JID resolution

WhatsApp group commands (/new, /stop) can be ignored when Baileys reports the sender as a LID JID (@lid) instead of a phone JID (@s.whatsapp.net). The resolveWhatsAppCommandAuthorized() and isOwnerSender() functions called getSelfIdentity/getSenderIdentity without passing authDir, so the LID-to-phone reverse mapping could not happen.

Fix: thread account.authDir through both command authorization and group owner-bypass identity resolution paths so that LID JIDs are properly resolved to phone E.164 identities before owner/allowlist checks.

* fix(whatsapp): replace deprecated top-level fields with admission overrides in LID JID test
This commit is contained in:
Peter Lee
2026-07-01 10:13:31 -05:00
committed by GitHub
parent 9fabdcf49d
commit 85f7834852
4 changed files with 63 additions and 6 deletions

View File

@@ -78,12 +78,16 @@ function shouldWarnForGroupDrop(warnKey: string): boolean {
return true;
}
function isOwnerSender(baseMentionConfig: MentionConfig, msg: AdmittedWebInboundMessage) {
const sender = normalizeE164(getSenderIdentity(msg).e164 ?? "");
function isOwnerSender(
baseMentionConfig: MentionConfig,
msg: AdmittedWebInboundMessage,
authDir?: string,
) {
const sender = normalizeE164(getSenderIdentity(msg, authDir).e164 ?? "");
if (!sender) {
return false;
}
const owners = resolveOwnerList(baseMentionConfig, getSelfIdentity(msg).e164 ?? undefined);
const owners = resolveOwnerList(baseMentionConfig, getSelfIdentity(msg, authDir).e164 ?? undefined);
return owners.includes(sender);
}
@@ -187,7 +191,7 @@ export async function applyGroupGating(params: ApplyGroupGatingParams) {
self.e164,
);
const activationCommand = parseActivationCommand(commandBody);
const owner = isOwnerSender(baseMentionConfig, params.msg);
const owner = isOwnerSender(baseMentionConfig, params.msg, params.authDir);
const shouldBypassMention = owner && hasControlCommand(commandBody, params.cfg);
if (activationCommand.hasCommand && !owner) {

View File

@@ -437,6 +437,7 @@ export async function processMessage(params: {
cfg: params.cfg,
msg: params.msg,
policy: inboundPolicy,
authDir: account.authDir,
})
: undefined;
const commandTurn: CommandTurnContext = isTextCommand

View File

@@ -173,13 +173,14 @@ export async function resolveWhatsAppCommandAuthorized(params: {
cfg: OpenClawConfig;
msg: AdmittedWebInboundMessage;
policy?: ResolvedWhatsAppInboundPolicy;
authDir?: string;
}): Promise<boolean> {
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
if (!useAccessGroups) {
return true;
}
const self = getSelfIdentity(params.msg);
const self = getSelfIdentity(params.msg, params.authDir);
const admission = requireWhatsAppInboundAdmission(params.msg);
const policy =
params.policy ??
@@ -189,7 +190,7 @@ export async function resolveWhatsAppCommandAuthorized(params: {
selfE164: self.e164 ?? null,
});
const isGroup = admission.conversation.kind === "group";
const sender = getSenderIdentity(params.msg);
const sender = getSenderIdentity(params.msg, params.authDir);
const dmSender = sender.e164 ?? admission.conversation.id;
const groupSender = sender.e164 ?? "";
if (!normalizeE164(isGroup ? groupSender : dmSender)) {

View File

@@ -1,4 +1,7 @@
// Whatsapp tests cover access control plugin behavior.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
import type {
AcceptedInboundAccessControlResult,
@@ -602,6 +605,54 @@ describe("WhatsApp dmPolicy precedence", () => {
expect(result.isSelfChat).toBe(false);
});
it("authorizes group commands when owner sender is a LID JID with authDir (issue #77755)", async () => {
const lidDigits = "9876543210";
const ownerE164 = "+15550001111";
const authDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-wa-lid-77755-"));
try {
// Write reverse LID mapping so the LID JID resolves to the owner's phone
fs.writeFileSync(
path.join(authDir, `lid-mapping-${lidDigits}_reverse.json`),
JSON.stringify(ownerE164),
);
const cfg = {
channels: {
whatsapp: {
dmPolicy: "allowlist",
allowFrom: [ownerE164],
},
},
};
setAccessControlTestConfig(cfg);
const result = await resolveWhatsAppCommandAuthorized({
cfg: cfg as never,
msg: createTestWebInboundMessage({
event: { id: "cmd-group-lid" },
payload: { body: "/status" },
platform: {
chatJid: "120363401234567890@g.us",
recipientJid: "+15550009999",
senderJid: `${lidDigits}@lid`,
selfE164: "+15550009999",
},
admission: {
conversation: {
id: "120363401234567890@g.us",
kind: "group",
},
},
}) as never,
authDir,
});
expect(result).toBe(true);
} finally {
fs.rmSync(authDir, { recursive: true, force: true });
}
});
it("treats same-phone DMs as self-chat only when explicitly configured", async () => {
const cfg = {
channels: {