From 85f7834852d331c4147ef7eef4ecc87c13f54d16 Mon Sep 17 00:00:00 2001
From: Peter Lee
Date: Wed, 1 Jul 2026 10:13:31 -0500
Subject: [PATCH] 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
---
.../src/auto-reply/monitor/group-gating.ts | 12 +++--
.../src/auto-reply/monitor/process-message.ts | 1 +
extensions/whatsapp/src/inbound-policy.ts | 5 +-
.../src/inbound/access-control.test.ts | 51 +++++++++++++++++++
4 files changed, 63 insertions(+), 6 deletions(-)
diff --git a/extensions/whatsapp/src/auto-reply/monitor/group-gating.ts b/extensions/whatsapp/src/auto-reply/monitor/group-gating.ts
index 14c07985fe64..db0ec8231a41 100644
--- a/extensions/whatsapp/src/auto-reply/monitor/group-gating.ts
+++ b/extensions/whatsapp/src/auto-reply/monitor/group-gating.ts
@@ -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) {
diff --git a/extensions/whatsapp/src/auto-reply/monitor/process-message.ts b/extensions/whatsapp/src/auto-reply/monitor/process-message.ts
index 0105c8060d83..572f92ba6edf 100644
--- a/extensions/whatsapp/src/auto-reply/monitor/process-message.ts
+++ b/extensions/whatsapp/src/auto-reply/monitor/process-message.ts
@@ -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
diff --git a/extensions/whatsapp/src/inbound-policy.ts b/extensions/whatsapp/src/inbound-policy.ts
index bdb5bd976989..94b865e3fd97 100644
--- a/extensions/whatsapp/src/inbound-policy.ts
+++ b/extensions/whatsapp/src/inbound-policy.ts
@@ -173,13 +173,14 @@ export async function resolveWhatsAppCommandAuthorized(params: {
cfg: OpenClawConfig;
msg: AdmittedWebInboundMessage;
policy?: ResolvedWhatsAppInboundPolicy;
+ authDir?: string;
}): Promise {
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)) {
diff --git a/extensions/whatsapp/src/inbound/access-control.test.ts b/extensions/whatsapp/src/inbound/access-control.test.ts
index 233c9d035369..4b0ac0710955 100644
--- a/extensions/whatsapp/src/inbound/access-control.test.ts
+++ b/extensions/whatsapp/src/inbound/access-control.test.ts
@@ -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: {