refactor(src): split oversized modules
This commit is contained in:
186
src/web/inbound/access-control.ts
Normal file
186
src/web/inbound/access-control.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
||||
import {
|
||||
readChannelAllowFromStore,
|
||||
upsertChannelPairingRequest,
|
||||
} from "../../pairing/pairing-store.js";
|
||||
import { isSelfChatMode, normalizeE164 } from "../../utils.js";
|
||||
import { resolveWhatsAppAccount } from "../accounts.js";
|
||||
|
||||
export type InboundAccessControlResult = {
|
||||
allowed: boolean;
|
||||
shouldMarkRead: boolean;
|
||||
isSelfChat: boolean;
|
||||
resolvedAccountId: string;
|
||||
};
|
||||
|
||||
export async function checkInboundAccessControl(params: {
|
||||
accountId: string;
|
||||
from: string;
|
||||
selfE164: string | null;
|
||||
senderE164: string | null;
|
||||
group: boolean;
|
||||
pushName?: string;
|
||||
isFromMe: boolean;
|
||||
sock: {
|
||||
sendMessage: (jid: string, content: { text: string }) => Promise<unknown>;
|
||||
};
|
||||
remoteJid: string;
|
||||
}): Promise<InboundAccessControlResult> {
|
||||
const cfg = loadConfig();
|
||||
const account = resolveWhatsAppAccount({
|
||||
cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
const dmPolicy = cfg.channels?.whatsapp?.dmPolicy ?? "pairing";
|
||||
const configuredAllowFrom = account.allowFrom;
|
||||
const storeAllowFrom = await readChannelAllowFromStore("whatsapp").catch(
|
||||
() => [],
|
||||
);
|
||||
// Without user config, default to self-only DM access so the owner can talk to themselves.
|
||||
const combinedAllowFrom = Array.from(
|
||||
new Set([...(configuredAllowFrom ?? []), ...storeAllowFrom]),
|
||||
);
|
||||
const defaultAllowFrom =
|
||||
combinedAllowFrom.length === 0 && params.selfE164
|
||||
? [params.selfE164]
|
||||
: undefined;
|
||||
const allowFrom =
|
||||
combinedAllowFrom.length > 0 ? combinedAllowFrom : defaultAllowFrom;
|
||||
const groupAllowFrom =
|
||||
account.groupAllowFrom ??
|
||||
(configuredAllowFrom && configuredAllowFrom.length > 0
|
||||
? configuredAllowFrom
|
||||
: undefined);
|
||||
const isSamePhone = params.from === params.selfE164;
|
||||
const isSelfChat = isSelfChatMode(params.selfE164, configuredAllowFrom);
|
||||
|
||||
// Pre-compute normalized allowlists for filtering.
|
||||
const dmHasWildcard = allowFrom?.includes("*") ?? false;
|
||||
const normalizedAllowFrom =
|
||||
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:
|
||||
// - "open": groups bypass allowFrom, only mention-gating applies
|
||||
// - "disabled": block all group messages entirely
|
||||
// - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom
|
||||
const groupPolicy = account.groupPolicy ?? "open";
|
||||
if (params.group && groupPolicy === "disabled") {
|
||||
logVerbose("Blocked group message (groupPolicy: disabled)");
|
||||
return {
|
||||
allowed: false,
|
||||
shouldMarkRead: false,
|
||||
isSelfChat,
|
||||
resolvedAccountId: account.accountId,
|
||||
};
|
||||
}
|
||||
if (params.group && groupPolicy === "allowlist") {
|
||||
if (!groupAllowFrom || groupAllowFrom.length === 0) {
|
||||
logVerbose(
|
||||
"Blocked group message (groupPolicy: allowlist, no groupAllowFrom)",
|
||||
);
|
||||
return {
|
||||
allowed: false,
|
||||
shouldMarkRead: false,
|
||||
isSelfChat,
|
||||
resolvedAccountId: account.accountId,
|
||||
};
|
||||
}
|
||||
const senderAllowed =
|
||||
groupHasWildcard ||
|
||||
(params.senderE164 != null &&
|
||||
normalizedGroupAllowFrom.includes(params.senderE164));
|
||||
if (!senderAllowed) {
|
||||
logVerbose(
|
||||
`Blocked group message from ${params.senderE164 ?? "unknown sender"} (groupPolicy: allowlist)`,
|
||||
);
|
||||
return {
|
||||
allowed: false,
|
||||
shouldMarkRead: false,
|
||||
isSelfChat,
|
||||
resolvedAccountId: account.accountId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// DM access control (secure defaults): "pairing" (default) / "allowlist" / "open" / "disabled".
|
||||
if (!params.group) {
|
||||
if (params.isFromMe && !isSamePhone) {
|
||||
logVerbose("Skipping outbound DM (fromMe); no pairing reply needed.");
|
||||
return {
|
||||
allowed: false,
|
||||
shouldMarkRead: false,
|
||||
isSelfChat,
|
||||
resolvedAccountId: account.accountId,
|
||||
};
|
||||
}
|
||||
if (dmPolicy === "disabled") {
|
||||
logVerbose("Blocked dm (dmPolicy: disabled)");
|
||||
return {
|
||||
allowed: false,
|
||||
shouldMarkRead: false,
|
||||
isSelfChat,
|
||||
resolvedAccountId: account.accountId,
|
||||
};
|
||||
}
|
||||
if (dmPolicy !== "open" && !isSamePhone) {
|
||||
const candidate = params.from;
|
||||
const allowed =
|
||||
dmHasWildcard ||
|
||||
(normalizedAllowFrom.length > 0 &&
|
||||
normalizedAllowFrom.includes(candidate));
|
||||
if (!allowed) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const { code, created } = await upsertChannelPairingRequest({
|
||||
channel: "whatsapp",
|
||||
id: candidate,
|
||||
meta: { name: (params.pushName ?? "").trim() || undefined },
|
||||
});
|
||||
if (created) {
|
||||
logVerbose(
|
||||
`whatsapp pairing request sender=${candidate} name=${params.pushName ?? "unknown"}`,
|
||||
);
|
||||
try {
|
||||
await params.sock.sendMessage(params.remoteJid, {
|
||||
text: buildPairingReply({
|
||||
channel: "whatsapp",
|
||||
idLine: `Your WhatsApp phone number: ${candidate}`,
|
||||
code,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
logVerbose(
|
||||
`whatsapp pairing reply failed for ${candidate}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logVerbose(
|
||||
`Blocked unauthorized sender ${candidate} (dmPolicy=${dmPolicy})`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
allowed: false,
|
||||
shouldMarkRead: false,
|
||||
isSelfChat,
|
||||
resolvedAccountId: account.accountId,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
shouldMarkRead: true,
|
||||
isSelfChat,
|
||||
resolvedAccountId: account.accountId,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user