From f1ff24d634daf89036784306f53086974c305d96 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 12 Dec 2025 00:50:40 +0000 Subject: [PATCH] web: default to self-only without config --- src/auto-reply/reply.ts | 11 ++++++- src/web/auto-reply.test.ts | 38 +++++++++++++++++++++-- src/web/auto-reply.ts | 2 +- src/web/inbound.ts | 11 ++++++- src/web/monitor-inbox.test.ts | 58 ++++++++++++++++++++++++++++++++++- src/web/test-helpers.ts | 9 +++--- 6 files changed, 119 insertions(+), 10 deletions(-) diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index 4ab3dc4d0..e0828b82a 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -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; diff --git a/src/web/auto-reply.test.ts b/src/web/auto-reply.test.ts index fb60b3ed5..1e7cd5386 100644 --- a/src/web/auto-reply.test.ts +++ b/src/web/auto-reply.test.ts @@ -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"), ); }); diff --git a/src/web/auto-reply.ts b/src/web/auto-reply.ts index b3bd8f146..c12ffdb88 100644 --- a/src/web/auto-reply.ts +++ b/src/web/auto-reply.ts @@ -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); diff --git a/src/web/inbound.ts b/src/web/inbound.ts index a20d4b8a9..deff1e81f 100644 --- a/src/web/inbound.ts +++ b/src/web/inbound.ts @@ -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 = diff --git a/src/web/monitor-inbox.test.ts b/src/web/monitor-inbox.test.ts index be5b31d0e..467615211 100644 --- a/src/web/monitor-inbox.test.ts +++ b/src/web/monitor-inbox.test.ts @@ -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(); + }); diff --git a/src/web/test-helpers.ts b/src/web/test-helpers.ts index b4aa076c6..d20a2cb3d 100644 --- a/src/web/test-helpers.ts +++ b/src/web/test-helpers.ts @@ -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, }, };