fix: guard provider-prefixed delivery targets

This commit is contained in:
Peter Steinberger
2026-05-02 05:29:55 +01:00
parent 2218ce46fe
commit 43121fb096
44 changed files with 753 additions and 25 deletions

View File

@@ -129,6 +129,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
},
},
messaging: {
targetPrefixes: ["bluebubbles"],
normalizeTarget: normalizeBlueBubblesMessagingTarget,
inferTargetChatType: ({ to }) => inferBlueBubblesTargetChatType(to),
resolveOutboundSessionRoute: (params) => resolveBlueBubblesOutboundSessionRoute(params),

View File

@@ -218,6 +218,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
],
},
messaging: {
targetPrefixes: ["discord"],
normalizeTarget: normalizeDiscordMessagingTarget,
resolveInboundConversation: ({ from, to, conversationId, isGroup }) =>
resolveDiscordInboundConversation({ from, to, conversationId, isGroup }),

View File

@@ -1154,6 +1154,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
setup: feishuSetupAdapter,
setupWizard: feishuSetupWizard,
messaging: {
targetPrefixes: ["feishu", "lark"],
normalizeTarget: (raw) => normalizeFeishuTarget(raw) ?? undefined,
resolveDeliveryTarget: ({ conversationId, parentConversationId }) => {
const directId = parseFeishuDirectConversationId(conversationId);

View File

@@ -144,6 +144,7 @@ export const googlechatPlugin = createChatChannelPlugin({
},
groups: googlechatGroupsAdapter,
messaging: {
targetPrefixes: ["googlechat", "google-chat", "gchat"],
normalizeTarget: normalizeGoogleChatTarget,
targetResolver: {
looksLikeId: (raw, normalized) => {

View File

@@ -233,6 +233,7 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = createChat
},
},
messaging: {
targetPrefixes: ["irc"],
normalizeTarget: normalizeIrcMessagingTarget,
targetResolver: {
looksLikeId: looksLikeIrcTargetId,

View File

@@ -42,6 +42,7 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = createChatChannelP
resolveRequireMention: resolveLineGroupRequireMention,
},
messaging: {
targetPrefixes: ["line"],
normalizeTarget: (target) => {
const trimmed = target.trim();
if (!trimmed) {

View File

@@ -376,6 +376,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount, MatrixProbe> =
}).map(projectMatrixConversationBinding),
},
messaging: {
targetPrefixes: ["matrix"],
normalizeTarget: normalizeMatrixMessagingTarget,
resolveInboundConversation: ({ to, conversationId, threadId }) =>
resolveMatrixInboundConversation({ to, conversationId, threadId }),

View File

@@ -306,6 +306,7 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
(await loadMattermostChannelRuntime()).listMattermostDirectoryPeers(params),
}),
messaging: {
targetPrefixes: ["mattermost"],
defaultMarkdownTableMode: "off",
normalizeTarget: normalizeMattermostMessagingTarget,
resolveDeliveryTarget: ({ conversationId, parentConversationId }) => {

View File

@@ -450,6 +450,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount, ProbeMSTeamsRe
},
setup: msteamsSetupAdapter,
messaging: {
targetPrefixes: ["msteams", "teams"],
normalizeTarget: normalizeMSTeamsMessagingTarget,
resolveOutboundSessionRoute: (params) => resolveMSTeamsOutboundSessionRoute(params),
targetResolver: {

View File

@@ -119,6 +119,7 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
resolveToolPolicy: resolveNextcloudTalkGroupToolPolicy,
},
messaging: {
targetPrefixes: ["nextcloud-talk", "nc-talk", "nc"],
normalizeTarget: normalizeNextcloudTalkMessagingTarget,
resolveOutboundSessionRoute: (params) => resolveNextcloudTalkOutboundSessionRoute(params),
targetResolver: {

View File

@@ -118,6 +118,7 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = createChatChanne
}),
},
messaging: {
targetPrefixes: ["nostr"],
normalizeTarget: (target) => {
// Strip nostr: prefix if present
const cleaned = target.trim().replace(/^nostr:/i, "");

View File

@@ -99,6 +99,7 @@ export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
},
approvalCapability: getQQBotApprovalCapability(),
messaging: {
targetPrefixes: ["qqbot"],
/** Normalize common QQ Bot target formats into the canonical qqbot:... form. */
normalizeTarget: coreNormalizeTarget,
targetResolver: {

View File

@@ -270,6 +270,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount, SignalProbe> =
},
},
messaging: {
targetPrefixes: ["signal"],
normalizeTarget: normalizeSignalMessagingTarget,
parseExplicitTarget: ({ raw }) => parseSignalExplicitTarget(raw),
inferTargetChatType: ({ to }) => inferSignalTargetChatType(to),

View File

@@ -385,6 +385,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
resolveToolPolicy: resolveSlackGroupToolPolicy,
},
messaging: {
targetPrefixes: ["slack"],
normalizeTarget: normalizeSlackMessagingTarget,
resolveDeliveryTarget: ({ conversationId, parentConversationId }) => {
const parent = parentConversationId?.trim();

View File

@@ -345,6 +345,8 @@ describe("createSynologyChatPlugin", () => {
it("normalizeTarget strips prefix and trims", () => {
const plugin = createSynologyChatPlugin();
expect(plugin.messaging.normalizeTarget("synology-chat:123")).toBe("123");
expect(plugin.messaging.normalizeTarget("synology_chat:123")).toBe("123");
expect(plugin.messaging.normalizeTarget("synology:123")).toBe("123");
expect(plugin.messaging.normalizeTarget(" 456 ")).toBe("456");
expect(plugin.messaging.normalizeTarget("")).toBeUndefined();
});
@@ -353,6 +355,8 @@ describe("createSynologyChatPlugin", () => {
const plugin = createSynologyChatPlugin();
expect(plugin.messaging.targetResolver.looksLikeId("12345")).toBe(true);
expect(plugin.messaging.targetResolver.looksLikeId("synology-chat:99")).toBe(true);
expect(plugin.messaging.targetResolver.looksLikeId("synology_chat:99")).toBe(true);
expect(plugin.messaging.targetResolver.looksLikeId("synology:99")).toBe(true);
expect(plugin.messaging.targetResolver.looksLikeId("notanumber")).toBe(false);
expect(plugin.messaging.targetResolver.looksLikeId("")).toBe(false);
});

View File

@@ -155,6 +155,7 @@ type SynologyChatPlugin = Omit<
}) => string[];
};
messaging: {
targetPrefixes?: readonly string[];
normalizeTarget: (target: string) => string | undefined;
targetResolver: {
looksLikeId: (id: string) => boolean;
@@ -237,13 +238,14 @@ export function createSynologyChatPlugin(): SynologyChatPlugin {
},
approvalCapability: synologyChatApprovalAuth,
messaging: {
targetPrefixes: ["synology-chat", "synology_chat", "synology"],
normalizeTarget: (target: string) => {
const trimmed = target.trim();
if (!trimmed) {
return undefined;
}
// Strip common prefixes
return trimmed.replace(/^synology[-_]?chat:/i, "").trim();
return trimmed.replace(/^synology(?:[-_]?chat)?:/i, "").trim();
},
targetResolver: {
looksLikeId: (id: string) => {
@@ -252,7 +254,7 @@ export function createSynologyChatPlugin(): SynologyChatPlugin {
return false;
}
// Synology Chat user IDs are numeric
return /^\d+$/.test(trimmed) || /^synology[-_]?chat:/i.test(trimmed);
return /^\d+$/.test(trimmed) || /^synology(?:[-_]?chat)?:/i.test(trimmed);
},
hint: "<userId>",
},

View File

@@ -130,7 +130,7 @@ function validateWebhookPath(value: string): string | undefined {
}
function parseSynologyUserId(value: string): string | null {
const cleaned = value.replace(/^synology-chat:/i, "").trim();
const cleaned = value.replace(/^synology(?:[-_]?chat)?:/i, "").trim();
return /^\d+$/.test(cleaned) ? cleaned : null;
}

View File

@@ -691,6 +691,7 @@ export const telegramPlugin = createChatChannelPlugin({
},
},
messaging: {
targetPrefixes: ["telegram", "tg"],
normalizeTarget: normalizeTelegramMessagingTarget,
resolveInboundConversation: ({ to, conversationId, threadId }) =>
resolveTelegramInboundConversation({ to, conversationId, threadId }),

View File

@@ -96,6 +96,7 @@ export const tlonPlugin = createChatChannelPlugin({
},
doctor: tlonDoctor,
messaging: {
targetPrefixes: ["tlon"],
normalizeTarget: (target) => {
const parsed = parseTlonTarget(target);
if (!parsed) {

View File

@@ -101,6 +101,17 @@ describe("whatsappChannelOutbound", () => {
});
});
it("rejects non-WhatsApp provider-prefixed outbound targets", () => {
const result = whatsappChannelOutbound.resolveTarget?.({
to: "telegram:1234567890",
allowFrom: [],
mode: undefined,
});
expect(result?.ok).toBe(false);
expect(hoisted.sendMessageWhatsApp).not.toHaveBeenCalled();
});
it("preserves indentation for payload delivery", async () => {
await whatsappChannelOutbound.sendPayload!({
cfg: {},

View File

@@ -111,6 +111,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> =
},
},
messaging: {
targetPrefixes: ["whatsapp"],
normalizeTarget: normalizeWhatsAppMessagingTarget,
resolveOutboundSessionRoute: (params) => resolveWhatsAppOutboundSessionRoute(params),
parseExplicitTarget: ({ raw }) => parseWhatsAppExplicitTarget(raw),

View File

@@ -4,6 +4,7 @@ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtim
const WHATSAPP_USER_JID_RE = /^(\d+)(?::\d+)?@s\.whatsapp\.net$/i;
const WHATSAPP_LEGACY_USER_JID_RE = /^(\d+)@c\.us$/i;
const WHATSAPP_LID_RE = /^(\d+)@lid$/i;
const NON_WHATSAPP_PROVIDER_PREFIX_RE = /^[a-z][a-z0-9-]*:/i;
function stripWhatsAppTargetPrefixes(value: string): string {
let candidate = value.trim();
@@ -74,6 +75,9 @@ export function normalizeWhatsAppTarget(value: string): string | null {
if (candidate.includes("@")) {
return null;
}
if (NON_WHATSAPP_PROVIDER_PREFIX_RE.test(candidate)) {
return null;
}
const normalized = normalizeE164(candidate);
return normalized.length > 1 ? normalized : null;
}

View File

@@ -42,6 +42,13 @@ describe("normalizeWhatsAppTarget", () => {
expect(normalizeWhatsAppTarget("abc@s.whatsapp.net")).toBeNull();
});
it("rejects non-WhatsApp provider-prefixed phone-like targets", () => {
expect(normalizeWhatsAppTarget("telegram:1234567890")).toBeNull();
expect(normalizeWhatsAppTarget("tg:1234567890")).toBeNull();
expect(normalizeWhatsAppTarget("sms:+15551234567")).toBeNull();
expect(looksLikeWhatsAppTargetId("telegram:1234567890")).toBe(false);
});
it("handles repeated prefixes", () => {
expect(normalizeWhatsAppTarget("whatsapp:whatsapp:+1555")).toBe("+1555");
expect(normalizeWhatsAppTarget("group:group:120@g.us")).toBeNull();

View File

@@ -196,6 +196,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount, ZaloProbeResult> =
},
actions: zaloMessageActions,
messaging: {
targetPrefixes: ["zalo", "zl"],
normalizeTarget: normalizeZaloMessagingTarget,
resolveOutboundSessionRoute: (params) => resolveZaloOutboundSessionRoute(params),
targetResolver: {

View File

@@ -370,6 +370,7 @@ export const zalouserOutboundAdapter = {
};
export const zalouserMessagingAdapter = {
targetPrefixes: ["zalouser", "zlu"],
normalizeTarget: (raw: string) => normalizeZalouserTarget(raw),
resolveOutboundSessionRoute: (
params: Parameters<typeof resolveZalouserOutboundSessionRoute>[0],