diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index da332abb9..8f0c97b53 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -398,7 +398,17 @@ export const googlechatPlugin: ChannelPlugin = { ), }; } - return { ok: true, to: normalized }; + const hasWildcard = allowListRaw.includes("*"); + if (hasWildcard || allowList.length === 0 || allowList.includes(normalized)) { + return { ok: true, to: normalized }; + } + if (mode === "implicit" || mode === "heartbeat") { + return { ok: true, to: allowList[0] }; + } + return { + ok: false, + error: new Error(`Google Chat target ${trimmed} is not in the allowFrom list.`), + }; } if (allowList.length > 0) { diff --git a/extensions/twitch/src/outbound.ts b/extensions/twitch/src/outbound.ts index ad67d8d32..d3707e62e 100644 --- a/extensions/twitch/src/outbound.ts +++ b/extensions/twitch/src/outbound.ts @@ -51,25 +51,19 @@ export const twitchOutbound: ChannelOutboundAdapter = { .map((entry: string) => normalizeTwitchChannel(entry)) .filter((entry): entry is string => entry.length > 0); - // If target is provided, normalize and validate it if (trimmed) { const normalizedTo = normalizeTwitchChannel(trimmed); - - // For implicit/heartbeat modes with allowList, check against allowlist + if (hasWildcard || allowList.length === 0 || allowList.includes(normalizedTo)) { + return { ok: true, to: normalizedTo }; + } if (mode === "implicit" || mode === "heartbeat") { - if (hasWildcard || allowList.length === 0) { - return { ok: true, to: normalizedTo }; - } - if (allowList.includes(normalizedTo)) { - return { ok: true, to: normalizedTo }; - } - // Fallback to first allowFrom entry // biome-ignore lint/style/noNonNullAssertion: length > 0 check ensures element exists return { ok: true, to: allowList[0]! }; } - - // For explicit mode, accept any valid channel name - return { ok: true, to: normalizedTo }; + return { + ok: false, + error: new Error(`Twitch target ${trimmed} is not in the allowFrom list.`), + }; } // No target provided, use allowFrom fallback diff --git a/extensions/whatsapp/src/channel.ts b/extensions/whatsapp/src/channel.ts index ee7951cd7..78022b5fa 100644 --- a/extensions/whatsapp/src/channel.ts +++ b/extensions/whatsapp/src/channel.ts @@ -303,18 +303,24 @@ export const whatsappPlugin: ChannelPlugin = { }; } if (isWhatsAppGroupJid(normalizedTo)) { + if (hasWildcard || allowList.length === 0 || allowList.includes(normalizedTo)) { + return { ok: true, to: normalizedTo }; + } + return { + ok: false, + error: new Error(`WhatsApp group ${trimmed} is not in the allowFrom list.`), + }; + } + if (hasWildcard || allowList.length === 0 || allowList.includes(normalizedTo)) { return { ok: true, to: normalizedTo }; } if (mode === "implicit" || mode === "heartbeat") { - if (hasWildcard || allowList.length === 0) { - return { ok: true, to: normalizedTo }; - } - if (allowList.includes(normalizedTo)) { - return { ok: true, to: normalizedTo }; - } return { ok: true, to: allowList[0] }; } - return { ok: true, to: normalizedTo }; + return { + ok: false, + error: new Error(`WhatsApp target ${trimmed} is not in the allowFrom list.`), + }; } if (allowList.length > 0) { diff --git a/src/channels/plugins/outbound/whatsapp.ts b/src/channels/plugins/outbound/whatsapp.ts index 303a015da..92c3acd4e 100644 --- a/src/channels/plugins/outbound/whatsapp.ts +++ b/src/channels/plugins/outbound/whatsapp.ts @@ -35,18 +35,31 @@ export const whatsappOutbound: ChannelOutboundAdapter = { }; } if (isWhatsAppGroupJid(normalizedTo)) { + // Groups also restricted to allowlist when configured + if (hasWildcard || allowList.length === 0 || allowList.includes(normalizedTo)) { + return { ok: true, to: normalizedTo }; + } + return { + ok: false, + error: new Error(`WhatsApp group ${trimmed} is not in the allowFrom list.`), + }; + } + if (hasWildcard || allowList.length === 0) { return { ok: true, to: normalizedTo }; } + if (allowList.includes(normalizedTo)) { + return { ok: true, to: normalizedTo }; + } + // Target not in allowlist — fallback for implicit, reject for explicit if (mode === "implicit" || mode === "heartbeat") { - if (hasWildcard || allowList.length === 0) { - return { ok: true, to: normalizedTo }; - } - if (allowList.includes(normalizedTo)) { - return { ok: true, to: normalizedTo }; - } return { ok: true, to: allowList[0] }; } - return { ok: true, to: normalizedTo }; + return { + ok: false, + error: new Error( + `WhatsApp target ${trimmed} is not in the allowFrom list. Allowed: ${allowListRaw.join(", ")}`, + ), + }; } if (allowList.length > 0) { diff --git a/src/infra/outbound/targets.ts b/src/infra/outbound/targets.ts index 0660b31c9..a92e7ce66 100644 --- a/src/infra/outbound/targets.ts +++ b/src/infra/outbound/targets.ts @@ -162,6 +162,16 @@ export function resolveOutboundTarget(params: { const trimmed = params.to?.trim(); if (trimmed) { + const mode = params.mode ?? "explicit"; + if (allowFrom && allowFrom.length > 0 && !allowFrom.includes("*")) { + const list = allowFrom.map((e) => String(e).trim()).filter(Boolean); + if (list.includes(trimmed)) return { ok: true, to: trimmed }; + if (mode === "implicit" || mode === "heartbeat") { + const fallback = list[0]; + if (fallback) return { ok: true, to: fallback }; + } + return { ok: false, error: new Error(`Target ${trimmed} is not in the allowFrom list.`) }; + } return { ok: true, to: trimmed }; } const hint = plugin.messaging?.targetResolver?.hint;