web: default to self-only without config

This commit is contained in:
Peter Steinberger
2025-12-12 00:50:40 +00:00
parent 0242383ec3
commit f1ff24d634
6 changed files with 119 additions and 10 deletions

View File

@@ -473,10 +473,19 @@ export async function getReplyFromConfig(
}
// Optional allowlist by origin number (E.164 without whatsapp: prefix)
const allowFrom = cfg.inbound?.allowFrom;
const configuredAllowFrom = cfg.inbound?.allowFrom;
const from = (ctx.From ?? "").replace(/^whatsapp:/, "");
const to = (ctx.To ?? "").replace(/^whatsapp:/, "");
const isSamePhone = from && to && from === to;
// If no config is present, default to self-only DM access.
const defaultAllowFrom =
(!configuredAllowFrom || configuredAllowFrom.length === 0) && to
? [to]
: undefined;
const allowFrom =
configuredAllowFrom && configuredAllowFrom.length > 0
? configuredAllowFrom
: defaultAllowFrom;
const abortKey = sessionKey ?? (from || undefined) ?? (to || undefined);
const rawBodyNormalized = triggerBodyNormalized;

View File

@@ -18,6 +18,7 @@ import {
runWebHeartbeatOnce,
stripHeartbeatToken,
} from "./auto-reply.js";
import { getReplyFromConfig } from "../auto-reply/reply.js";
import type { sendMessageWhatsApp } from "./outbound.js";
import {
resetBaileysMocks,
@@ -201,6 +202,39 @@ describe("partial reply gating", () => {
expect(reply).toHaveBeenCalledTimes(1);
expect(reply).toHaveBeenCalledWith("final reply");
});
it("defaults to self-only when no config is present", async () => {
const cfg: ClawdisConfig = {
inbound: {
// No allowFrom provided; this simulates zero config file while keeping reply simple
reply: { mode: "text", text: "ok" },
},
};
// Not self: should be blocked
const blocked = await getReplyFromConfig(
{
Body: "hi",
From: "whatsapp:+999",
To: "whatsapp:+123",
},
undefined,
cfg,
);
expect(blocked).toBeUndefined();
// Self: should be allowed
const allowed = await getReplyFromConfig(
{
Body: "hi",
From: "whatsapp:+123",
To: "whatsapp:+123",
},
undefined,
cfg,
);
expect(allowed).toEqual({ text: "ok" });
});
});
describe("runWebHeartbeatOnce", () => {
@@ -531,7 +565,7 @@ describe("web auto-reply", () => {
await run;
});
it("stops after hitting max reconnect attempts", async () => {
it("stops after hitting max reconnect attempts", { timeout: 20000 }, async () => {
const closeResolvers: Array<() => void> = [];
const sleep = vi.fn(async () => {});
const listenerFactory = vi.fn(async () => {
@@ -570,7 +604,7 @@ describe("web auto-reply", () => {
await run;
expect(runtime.error).toHaveBeenCalledWith(
expect.stringContaining("Reached max retries"),
expect.stringContaining("max attempts reached"),
);
});

View File

@@ -1383,7 +1383,7 @@ export async function monitorWebProvider(
},
"web reconnect: max attempts reached; continuing in degraded mode",
);
reconnectAttempts = 0;
break;
}
const delay = computeBackoff(reconnectPolicy, reconnectAttempts);

View File

@@ -149,7 +149,16 @@ export async function monitorWebInbox(options: {
// Filter unauthorized senders early to prevent wasted processing
// and potential session corruption from Bad MAC errors
const cfg = loadConfig();
const allowFrom = cfg.inbound?.allowFrom;
const configuredAllowFrom = cfg.inbound?.allowFrom;
// Without user config, default to self-only DM access so the owner can talk to themselves
const defaultAllowFrom =
(!configuredAllowFrom || configuredAllowFrom.length === 0) && selfE164
? [selfE164]
: undefined;
const allowFrom =
configuredAllowFrom && configuredAllowFrom.length > 0
? configuredAllowFrom
: defaultAllowFrom;
const isSamePhone = from === selfE164;
const allowlistEnabled =

View File

@@ -11,7 +11,7 @@ vi.mock("../media/store.js", () => ({
const mockLoadConfig = vi.fn().mockReturnValue({
inbound: {
allowFrom: ["*"], // Allow all in tests
allowFrom: ["*"], // Allow all in tests by default
messagePrefix: undefined,
responsePrefix: undefined,
timestampPrefix: false,
@@ -553,3 +553,59 @@ describe("web monitor inbox", () => {
await listener.close();
});
});
it("defaults to self-only when no config is present", async () => {
// No config file => allowFrom should be derived from selfE164
mockLoadConfig.mockReturnValue({});
const onMessage = vi.fn();
const listener = await monitorWebInbox({ verbose: false, onMessage });
const sock = await createWaSocket();
// Message from someone else should be blocked
const upsertBlocked = {
type: "notify",
messages: [
{
key: { id: "no-config-1", fromMe: false, remoteJid: "999@s.whatsapp.net" },
message: { conversation: "ping" },
messageTimestamp: 1_700_000_000,
},
],
};
sock.ev.emit("messages.upsert", upsertBlocked);
await new Promise((resolve) => setImmediate(resolve));
expect(onMessage).not.toHaveBeenCalled();
// Message from self should be allowed
const upsertSelf = {
type: "notify",
messages: [
{
key: { id: "no-config-2", fromMe: false, remoteJid: "123@s.whatsapp.net" },
message: { conversation: "self ping" },
messageTimestamp: 1_700_000_001,
},
],
};
sock.ev.emit("messages.upsert", upsertSelf);
await new Promise((resolve) => setImmediate(resolve));
expect(onMessage).toHaveBeenCalledTimes(1);
expect(onMessage).toHaveBeenCalledWith(
expect.objectContaining({ body: "self ping", from: "+123", to: "+123" }),
);
// Reset mock for other tests
mockLoadConfig.mockReturnValue({
inbound: {
allowFrom: ["*"],
messagePrefix: undefined,
responsePrefix: undefined,
timestampPrefix: false,
},
});
await listener.close();
});

View File

@@ -7,10 +7,11 @@ import { createMockBaileys } from "../../test/mocks/baileys.js";
const CONFIG_KEY = Symbol.for("clawdis:testConfigMock");
const DEFAULT_CONFIG = {
inbound: {
allowFrom: ["*"], // Allow all in tests by default
messagePrefix: undefined, // No message prefix in tests
responsePrefix: undefined, // No response prefix in tests
timestampPrefix: false, // No timestamp in tests
// Tests can override; default remains open to avoid surprising fixtures
allowFrom: ["*"],
messagePrefix: undefined,
responsePrefix: undefined,
timestampPrefix: false,
},
};