From b4566499749449c7c25f14fb41530036fcea7338 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 7 Mar 2026 22:37:59 +0000 Subject: [PATCH] refactor: unify account-scoped dm security policy resolver --- extensions/bluebubbles/src/channel.ts | 25 +++---- extensions/discord/src/channel.ts | 20 +++-- extensions/googlechat/src/channel.ts | 24 +++--- extensions/imessage/src/channel.ts | 25 +++---- extensions/irc/src/channel.ts | 25 +++---- extensions/line/src/channel.ts | 25 +++---- extensions/matrix/src/channel.ts | 27 ++++--- extensions/mattermost/src/channel.ts | 25 +++---- extensions/nextcloud-talk/src/channel.ts | 23 +++--- extensions/signal/src/channel.ts | 25 +++---- extensions/slack/src/channel.ts | 20 +++-- extensions/telegram/src/channel.ts | 21 +++--- extensions/whatsapp/src/channel.ts | 21 +++--- extensions/zalo/src/channel.ts | 19 ++--- extensions/zalouser/src/channel.ts | 19 ++--- src/channels/plugins/helpers.test.ts | 95 ++++++++++++++++++++++++ src/channels/plugins/helpers.ts | 38 ++++++++++ src/plugin-sdk/index.ts | 5 +- 18 files changed, 293 insertions(+), 189 deletions(-) create mode 100644 src/channels/plugins/helpers.test.ts diff --git a/extensions/bluebubbles/src/channel.ts b/extensions/bluebubbles/src/channel.ts index e0260c5c716..99930658aa1 100644 --- a/extensions/bluebubbles/src/channel.ts +++ b/extensions/bluebubbles/src/channel.ts @@ -1,4 +1,7 @@ -import { buildOpenGroupPolicyRestrictSendersWarning } from "openclaw/plugin-sdk"; +import { + buildAccountScopedDmSecurityPolicy, + buildOpenGroupPolicyRestrictSendersWarning, +} from "openclaw/plugin-sdk"; import type { ChannelAccountSnapshot, ChannelPlugin, @@ -12,7 +15,6 @@ import { collectBlueBubblesStatusIssues, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, - formatPairingApproveHint, migrateBaseNameToDefaultAccount, normalizeAccountId, PAIRING_APPROVED_MESSAGE, @@ -125,19 +127,16 @@ export const bluebubblesPlugin: ChannelPlugin = { actions: bluebubblesMessageActions, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean(cfg.channels?.bluebubbles?.accounts?.[resolvedAccountId]); - const basePath = useAccountPath - ? `channels.bluebubbles.accounts.${resolvedAccountId}.` - : "channels.bluebubbles."; - return { - policy: account.config.dmPolicy ?? "pairing", + return buildAccountScopedDmSecurityPolicy({ + cfg, + channelKey: "bluebubbles", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.config.dmPolicy, allowFrom: account.config.allowFrom ?? [], - policyPath: `${basePath}dmPolicy`, - allowFromPath: basePath, - approveHint: formatPairingApproveHint("bluebubbles"), + policyPathSuffix: "dmPolicy", normalizeEntry: (raw) => normalizeBlueBubblesHandle(raw.replace(/^bluebubbles:/i, "")), - }; + }); }, collectWarnings: ({ account }) => { const groupPolicy = account.config.groupPolicy ?? "allowlist"; diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 33fe3bebcaf..767666cb568 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -1,4 +1,5 @@ import { + buildAccountScopedDmSecurityPolicy, buildOpenGroupPolicyConfigureRouteAllowlistWarning, buildOpenGroupPolicyWarning, } from "openclaw/plugin-sdk"; @@ -13,7 +14,6 @@ import { deleteAccountFromConfigSection, discordOnboardingAdapter, DiscordConfigSchema, - formatPairingApproveHint, getChatChannelMeta, inspectDiscordAccount, listDiscordAccountIds, @@ -127,18 +127,16 @@ export const discordPlugin: ChannelPlugin = { }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean(cfg.channels?.discord?.accounts?.[resolvedAccountId]); - const allowFromPath = useAccountPath - ? `channels.discord.accounts.${resolvedAccountId}.dm.` - : "channels.discord.dm."; - return { - policy: account.config.dm?.policy ?? "pairing", + return buildAccountScopedDmSecurityPolicy({ + cfg, + channelKey: "discord", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.config.dm?.policy, allowFrom: account.config.dm?.allowFrom ?? [], - allowFromPath, - approveHint: formatPairingApproveHint("discord"), + allowFromPathSuffix: "dm.", normalizeEntry: (raw) => raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"), - }; + }); }, collectWarnings: ({ account, cfg }) => { const warnings: string[] = []; diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index f7635b9bf11..021cdfe9cca 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -1,4 +1,7 @@ -import { buildOpenGroupPolicyConfigureRouteAllowlistWarning } from "openclaw/plugin-sdk"; +import { + buildAccountScopedDmSecurityPolicy, + buildOpenGroupPolicyConfigureRouteAllowlistWarning, +} from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, applySetupAccountConfigPatch, @@ -6,7 +9,6 @@ import { buildChannelConfigSchema, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, - formatPairingApproveHint, getChatChannelMeta, listDirectoryGroupEntriesFromMapKeys, listDirectoryUserEntriesFromAllowFrom, @@ -190,18 +192,16 @@ export const googlechatPlugin: ChannelPlugin = { }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean(cfg.channels?.["googlechat"]?.accounts?.[resolvedAccountId]); - const allowFromPath = useAccountPath - ? `channels.googlechat.accounts.${resolvedAccountId}.dm.` - : "channels.googlechat.dm."; - return { - policy: account.config.dm?.policy ?? "pairing", + return buildAccountScopedDmSecurityPolicy({ + cfg, + channelKey: "googlechat", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.config.dm?.policy, allowFrom: account.config.dm?.allowFrom ?? [], - allowFromPath, - approveHint: formatPairingApproveHint("googlechat"), + allowFromPathSuffix: "dm.", normalizeEntry: (raw) => formatAllowFromEntry(raw), - }; + }); }, collectWarnings: ({ account, cfg }) => { const warnings: string[] = []; diff --git a/extensions/imessage/src/channel.ts b/extensions/imessage/src/channel.ts index c4d21f2f913..4e2e23be129 100644 --- a/extensions/imessage/src/channel.ts +++ b/extensions/imessage/src/channel.ts @@ -1,11 +1,13 @@ -import { buildOpenGroupPolicyRestrictSendersWarning } from "openclaw/plugin-sdk"; +import { + buildAccountScopedDmSecurityPolicy, + buildOpenGroupPolicyRestrictSendersWarning, +} from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, buildChannelConfigSchema, collectStatusIssuesFromLastError, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, - formatPairingApproveHint, formatTrimmedAllowFromEntries, getChatChannelMeta, imessageOnboardingAdapter, @@ -132,18 +134,15 @@ export const imessagePlugin: ChannelPlugin = { }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean(cfg.channels?.imessage?.accounts?.[resolvedAccountId]); - const basePath = useAccountPath - ? `channels.imessage.accounts.${resolvedAccountId}.` - : "channels.imessage."; - return { - policy: account.config.dmPolicy ?? "pairing", + return buildAccountScopedDmSecurityPolicy({ + cfg, + channelKey: "imessage", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.config.dmPolicy, allowFrom: account.config.allowFrom ?? [], - policyPath: `${basePath}dmPolicy`, - allowFromPath: basePath, - approveHint: formatPairingApproveHint("imessage"), - }; + policyPathSuffix: "dmPolicy", + }); }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); diff --git a/extensions/irc/src/channel.ts b/extensions/irc/src/channel.ts index ec8997bcf9a..bb3dae50266 100644 --- a/extensions/irc/src/channel.ts +++ b/extensions/irc/src/channel.ts @@ -1,11 +1,13 @@ -import { buildOpenGroupPolicyWarning } from "openclaw/plugin-sdk"; +import { + buildAccountScopedDmSecurityPolicy, + buildOpenGroupPolicyWarning, +} from "openclaw/plugin-sdk"; import { buildBaseAccountStatusSnapshot, buildBaseChannelStatusSummary, buildChannelConfigSchema, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, - formatPairingApproveHint, getChatChannelMeta, PAIRING_APPROVED_MESSAGE, resolveAllowlistProviderRuntimeGroupPolicy, @@ -123,19 +125,16 @@ export const ircPlugin: ChannelPlugin = { }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean(cfg.channels?.irc?.accounts?.[resolvedAccountId]); - const basePath = useAccountPath - ? `channels.irc.accounts.${resolvedAccountId}.` - : "channels.irc."; - return { - policy: account.config.dmPolicy ?? "pairing", + return buildAccountScopedDmSecurityPolicy({ + cfg, + channelKey: "irc", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.config.dmPolicy, allowFrom: account.config.allowFrom ?? [], - policyPath: `${basePath}dmPolicy`, - allowFromPath: `${basePath}allowFrom`, - approveHint: formatPairingApproveHint("irc"), + policyPathSuffix: "dmPolicy", normalizeEntry: (raw) => normalizeIrcAllowEntry(raw), - }; + }); }, collectWarnings: ({ account, cfg }) => { const warnings: string[] = []; diff --git a/extensions/line/src/channel.ts b/extensions/line/src/channel.ts index 1107a0c1b48..d2883ab52fd 100644 --- a/extensions/line/src/channel.ts +++ b/extensions/line/src/channel.ts @@ -1,4 +1,7 @@ -import { buildOpenGroupPolicyRestrictSendersWarning } from "openclaw/plugin-sdk"; +import { + buildAccountScopedDmSecurityPolicy, + buildOpenGroupPolicyRestrictSendersWarning, +} from "openclaw/plugin-sdk"; import { buildChannelConfigSchema, buildComputedAccountStatusSnapshot, @@ -159,21 +162,17 @@ export const linePlugin: ChannelPlugin = { }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean( - (cfg.channels?.line as LineConfig | undefined)?.accounts?.[resolvedAccountId], - ); - const basePath = useAccountPath - ? `channels.line.accounts.${resolvedAccountId}.` - : "channels.line."; - return { - policy: account.config.dmPolicy ?? "pairing", + return buildAccountScopedDmSecurityPolicy({ + cfg, + channelKey: "line", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.config.dmPolicy, allowFrom: account.config.allowFrom ?? [], - policyPath: `${basePath}dmPolicy`, - allowFromPath: basePath, + policyPathSuffix: "dmPolicy", approveHint: "openclaw pairing approve line ", normalizeEntry: (raw) => raw.replace(/^line:(?:user:)?/i, ""), - }; + }); }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); diff --git a/extensions/matrix/src/channel.ts b/extensions/matrix/src/channel.ts index 80b0987e3df..d2b81606152 100644 --- a/extensions/matrix/src/channel.ts +++ b/extensions/matrix/src/channel.ts @@ -1,4 +1,7 @@ -import { buildOpenGroupPolicyWarning } from "openclaw/plugin-sdk"; +import { + buildAccountScopedDmSecurityPolicy, + buildOpenGroupPolicyWarning, +} from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, buildChannelConfigSchema, @@ -6,7 +9,6 @@ import { collectStatusIssuesFromLastError, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, - formatPairingApproveHint, normalizeAccountId, PAIRING_APPROVED_MESSAGE, resolveAllowlistProviderRuntimeGroupPolicy, @@ -159,20 +161,17 @@ export const matrixPlugin: ChannelPlugin = { formatAllowFrom: ({ allowFrom }) => normalizeMatrixAllowList(allowFrom), }, security: { - resolveDmPolicy: ({ account }) => { - const accountId = account.accountId; - const prefix = - accountId && accountId !== "default" - ? `channels.matrix.accounts.${accountId}.dm` - : "channels.matrix.dm"; - return { - policy: account.config.dm?.policy ?? "pairing", + resolveDmPolicy: ({ cfg, accountId, account }) => { + return buildAccountScopedDmSecurityPolicy({ + cfg: cfg as CoreConfig, + channelKey: "matrix", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.config.dm?.policy, allowFrom: account.config.dm?.allowFrom ?? [], - policyPath: `${prefix}.policy`, - allowFromPath: `${prefix}.allowFrom`, - approveHint: formatPairingApproveHint("matrix"), + allowFromPathSuffix: "dm.", normalizeEntry: (raw) => normalizeMatrixUserId(raw), - }; + }); }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg as CoreConfig); diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index c325d588dbb..bb0a95cb15b 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -1,4 +1,7 @@ -import { buildOpenGroupPolicyRestrictSendersWarning } from "openclaw/plugin-sdk"; +import { + buildAccountScopedDmSecurityPolicy, + buildOpenGroupPolicyRestrictSendersWarning, +} from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, applySetupAccountConfigPatch, @@ -6,7 +9,6 @@ import { buildChannelConfigSchema, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, - formatPairingApproveHint, migrateBaseNameToDefaultAccount, normalizeAccountId, resolveAllowlistProviderRuntimeGroupPolicy, @@ -281,19 +283,16 @@ export const mattermostPlugin: ChannelPlugin = { }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean(cfg.channels?.mattermost?.accounts?.[resolvedAccountId]); - const basePath = useAccountPath - ? `channels.mattermost.accounts.${resolvedAccountId}.` - : "channels.mattermost."; - return { - policy: account.config.dmPolicy ?? "pairing", + return buildAccountScopedDmSecurityPolicy({ + cfg, + channelKey: "mattermost", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.config.dmPolicy, allowFrom: account.config.allowFrom ?? [], - policyPath: `${basePath}dmPolicy`, - allowFromPath: basePath, - approveHint: formatPairingApproveHint("mattermost"), + policyPathSuffix: "dmPolicy", normalizeEntry: (raw) => normalizeAllowEntry(raw), - }; + }); }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); diff --git a/extensions/nextcloud-talk/src/channel.ts b/extensions/nextcloud-talk/src/channel.ts index 567e89c8d5b..fcb020595a3 100644 --- a/extensions/nextcloud-talk/src/channel.ts +++ b/extensions/nextcloud-talk/src/channel.ts @@ -1,4 +1,5 @@ import { + buildAccountScopedDmSecurityPolicy, buildOpenGroupPolicyNoRouteAllowlistWarning, buildOpenGroupPolicyRestrictSendersWarning, } from "openclaw/plugin-sdk"; @@ -10,7 +11,6 @@ import { clearAccountEntryFields, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, - formatPairingApproveHint, normalizeAccountId, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, @@ -121,21 +121,16 @@ export const nextcloudTalkPlugin: ChannelPlugin = }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean( - cfg.channels?.["nextcloud-talk"]?.accounts?.[resolvedAccountId], - ); - const basePath = useAccountPath - ? `channels.nextcloud-talk.accounts.${resolvedAccountId}.` - : "channels.nextcloud-talk."; - return { - policy: account.config.dmPolicy ?? "pairing", + return buildAccountScopedDmSecurityPolicy({ + cfg, + channelKey: "nextcloud-talk", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.config.dmPolicy, allowFrom: account.config.allowFrom ?? [], - policyPath: `${basePath}dmPolicy`, - allowFromPath: basePath, - approveHint: formatPairingApproveHint("nextcloud-talk"), + policyPathSuffix: "dmPolicy", normalizeEntry: (raw) => raw.replace(/^(nextcloud-talk|nc-talk|nc):/i, "").toLowerCase(), - }; + }); }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index b307e9b9478..52a59cc058b 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -1,4 +1,7 @@ -import { buildOpenGroupPolicyRestrictSendersWarning } from "openclaw/plugin-sdk"; +import { + buildAccountScopedDmSecurityPolicy, + buildOpenGroupPolicyRestrictSendersWarning, +} from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, buildBaseAccountStatusSnapshot, @@ -8,7 +11,6 @@ import { createDefaultChannelRuntimeState, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, - formatPairingApproveHint, getChatChannelMeta, listSignalAccountIds, looksLikeSignalTargetId, @@ -155,19 +157,16 @@ export const signalPlugin: ChannelPlugin = { }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean(cfg.channels?.signal?.accounts?.[resolvedAccountId]); - const basePath = useAccountPath - ? `channels.signal.accounts.${resolvedAccountId}.` - : "channels.signal."; - return { - policy: account.config.dmPolicy ?? "pairing", + return buildAccountScopedDmSecurityPolicy({ + cfg, + channelKey: "signal", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.config.dmPolicy, allowFrom: account.config.allowFrom ?? [], - policyPath: `${basePath}dmPolicy`, - allowFromPath: basePath, - approveHint: formatPairingApproveHint("signal"), + policyPathSuffix: "dmPolicy", normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()), - }; + }); }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index 20536444155..c3871182665 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -1,4 +1,5 @@ import { + buildAccountScopedDmSecurityPolicy, buildOpenGroupPolicyConfigureRouteAllowlistWarning, buildOpenGroupPolicyWarning, } from "openclaw/plugin-sdk"; @@ -9,7 +10,6 @@ import { DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, extractSlackToolSend, - formatPairingApproveHint, getChatChannelMeta, handleSlackMessageAction, inspectSlackAccount, @@ -177,18 +177,16 @@ export const slackPlugin: ChannelPlugin = { }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean(cfg.channels?.slack?.accounts?.[resolvedAccountId]); - const allowFromPath = useAccountPath - ? `channels.slack.accounts.${resolvedAccountId}.dm.` - : "channels.slack.dm."; - return { - policy: account.dm?.policy ?? "pairing", + return buildAccountScopedDmSecurityPolicy({ + cfg, + channelKey: "slack", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.dm?.policy, allowFrom: account.dm?.allowFrom ?? [], - allowFromPath, - approveHint: formatPairingApproveHint("slack"), + allowFromPathSuffix: "dm.", normalizeEntry: (raw) => raw.replace(/^(slack|user):/i, ""), - }; + }); }, collectWarnings: ({ account, cfg }) => { const warnings: string[] = []; diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 3fe18e3f288..65ece56e50e 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -1,4 +1,5 @@ import { + buildAccountScopedDmSecurityPolicy, buildOpenGroupPolicyNoRouteAllowlistWarning, buildOpenGroupPolicyRestrictSendersWarning, } from "openclaw/plugin-sdk"; @@ -10,7 +11,6 @@ import { collectTelegramStatusIssues, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, - formatPairingApproveHint, getChatChannelMeta, inspectTelegramAccount, listTelegramAccountIds, @@ -192,19 +192,16 @@ export const telegramPlugin: ChannelPlugin { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean(cfg.channels?.telegram?.accounts?.[resolvedAccountId]); - const basePath = useAccountPath - ? `channels.telegram.accounts.${resolvedAccountId}.` - : "channels.telegram."; - return { - policy: account.config.dmPolicy ?? "pairing", + return buildAccountScopedDmSecurityPolicy({ + cfg, + channelKey: "telegram", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.config.dmPolicy, allowFrom: account.config.allowFrom ?? [], - policyPath: `${basePath}dmPolicy`, - allowFromPath: basePath, - approveHint: formatPairingApproveHint("telegram"), + policyPathSuffix: "dmPolicy", normalizeEntry: (raw) => raw.replace(/^(telegram|tg):/i, ""), - }; + }); }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); diff --git a/extensions/whatsapp/src/channel.ts b/extensions/whatsapp/src/channel.ts index 60387ca0c34..c2dbc1e9e4b 100644 --- a/extensions/whatsapp/src/channel.ts +++ b/extensions/whatsapp/src/channel.ts @@ -1,4 +1,5 @@ import { + buildAccountScopedDmSecurityPolicy, buildOpenGroupPolicyNoRouteAllowlistWarning, buildOpenGroupPolicyRestrictSendersWarning, } from "openclaw/plugin-sdk"; @@ -8,7 +9,6 @@ import { collectWhatsAppStatusIssues, createActionGate, DEFAULT_ACCOUNT_ID, - formatPairingApproveHint, getChatChannelMeta, listWhatsAppAccountIds, listWhatsAppDirectoryGroupsFromConfig, @@ -125,19 +125,16 @@ export const whatsappPlugin: ChannelPlugin = { }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean(cfg.channels?.whatsapp?.accounts?.[resolvedAccountId]); - const basePath = useAccountPath - ? `channels.whatsapp.accounts.${resolvedAccountId}.` - : "channels.whatsapp."; - return { - policy: account.dmPolicy ?? "pairing", + return buildAccountScopedDmSecurityPolicy({ + cfg, + channelKey: "whatsapp", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.dmPolicy, allowFrom: account.allowFrom ?? [], - policyPath: `${basePath}dmPolicy`, - allowFromPath: basePath, - approveHint: formatPairingApproveHint("whatsapp"), + policyPathSuffix: "dmPolicy", normalizeEntry: (raw) => normalizeE164(raw), - }; + }); }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); diff --git a/extensions/zalo/src/channel.ts b/extensions/zalo/src/channel.ts index 1d403a1df34..f26d708b6e4 100644 --- a/extensions/zalo/src/channel.ts +++ b/extensions/zalo/src/channel.ts @@ -1,4 +1,5 @@ import { + buildAccountScopedDmSecurityPolicy, buildOpenGroupPolicyRestrictSendersWarning, buildOpenGroupPolicyWarning, } from "openclaw/plugin-sdk"; @@ -19,7 +20,6 @@ import { deleteAccountFromConfigSection, chunkTextForOutbound, formatAllowFromLowercase, - formatPairingApproveHint, migrateBaseNameToDefaultAccount, listDirectoryUserEntriesFromAllowFrom, normalizeAccountId, @@ -28,7 +28,6 @@ import { resolveOutboundMediaUrls, resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, - resolveChannelAccountConfigBasePath, sendPayloadWithChunkedTextAndMedia, setAccountEnabledInConfigSection, } from "openclaw/plugin-sdk/zalo"; @@ -142,20 +141,16 @@ export const zaloPlugin: ChannelPlugin = { }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const basePath = resolveChannelAccountConfigBasePath({ + return buildAccountScopedDmSecurityPolicy({ cfg, channelKey: "zalo", - accountId: resolvedAccountId, - }); - return { - policy: account.config.dmPolicy ?? "pairing", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.config.dmPolicy, allowFrom: account.config.allowFrom ?? [], - policyPath: `${basePath}dmPolicy`, - allowFromPath: basePath, - approveHint: formatPairingApproveHint("zalo"), + policyPathSuffix: "dmPolicy", normalizeEntry: (raw) => raw.replace(/^(zalo|zl):/i, ""), - }; + }); }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); diff --git a/extensions/zalouser/src/channel.ts b/extensions/zalouser/src/channel.ts index 2de31e9aa3e..9f6fa003a9f 100644 --- a/extensions/zalouser/src/channel.ts +++ b/extensions/zalouser/src/channel.ts @@ -1,3 +1,4 @@ +import { buildAccountScopedDmSecurityPolicy } from "openclaw/plugin-sdk"; import type { ChannelAccountSnapshot, ChannelDirectoryEntry, @@ -18,11 +19,9 @@ import { chunkTextForOutbound, deleteAccountFromConfigSection, formatAllowFromLowercase, - formatPairingApproveHint, isNumericTargetId, migrateBaseNameToDefaultAccount, normalizeAccountId, - resolveChannelAccountConfigBasePath, sendPayloadWithChunkedTextAndMedia, setAccountEnabledInConfigSection, } from "openclaw/plugin-sdk/zalouser"; @@ -282,20 +281,16 @@ export const zalouserPlugin: ChannelPlugin = { }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { - const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const basePath = resolveChannelAccountConfigBasePath({ + return buildAccountScopedDmSecurityPolicy({ cfg, channelKey: "zalouser", - accountId: resolvedAccountId, - }); - return { - policy: account.config.dmPolicy ?? "pairing", + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.config.dmPolicy, allowFrom: account.config.allowFrom ?? [], - policyPath: `${basePath}dmPolicy`, - allowFromPath: basePath, - approveHint: formatPairingApproveHint("zalouser"), + policyPathSuffix: "dmPolicy", normalizeEntry: (raw) => raw.replace(/^(zalouser|zlu):/i, ""), - }; + }); }, }, groups: { diff --git a/src/channels/plugins/helpers.test.ts b/src/channels/plugins/helpers.test.ts new file mode 100644 index 00000000000..2b85d7fea06 --- /dev/null +++ b/src/channels/plugins/helpers.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../../config/config.js"; +import { buildAccountScopedDmSecurityPolicy, formatPairingApproveHint } from "./helpers.js"; + +function cfgWithChannel(channelKey: string, accounts?: Record): OpenClawConfig { + return { + channels: { + [channelKey]: accounts ? { accounts } : {}, + }, + } as unknown as OpenClawConfig; +} + +describe("buildAccountScopedDmSecurityPolicy", () => { + it("builds top-level dm policy paths when no account config exists", () => { + expect( + buildAccountScopedDmSecurityPolicy({ + cfg: cfgWithChannel("telegram"), + channelKey: "telegram", + fallbackAccountId: "default", + policy: "pairing", + allowFrom: ["123"], + policyPathSuffix: "dmPolicy", + }), + ).toEqual({ + policy: "pairing", + allowFrom: ["123"], + policyPath: "channels.telegram.dmPolicy", + allowFromPath: "channels.telegram.", + approveHint: formatPairingApproveHint("telegram"), + normalizeEntry: undefined, + }); + }); + + it("uses account-scoped paths when account config exists", () => { + expect( + buildAccountScopedDmSecurityPolicy({ + cfg: cfgWithChannel("signal", { work: {} }), + channelKey: "signal", + accountId: "work", + fallbackAccountId: "default", + policy: "allowlist", + allowFrom: ["+12125551212"], + policyPathSuffix: "dmPolicy", + }), + ).toEqual({ + policy: "allowlist", + allowFrom: ["+12125551212"], + policyPath: "channels.signal.accounts.work.dmPolicy", + allowFromPath: "channels.signal.accounts.work.", + approveHint: formatPairingApproveHint("signal"), + normalizeEntry: undefined, + }); + }); + + it("supports nested dm paths without explicit policyPath", () => { + expect( + buildAccountScopedDmSecurityPolicy({ + cfg: cfgWithChannel("discord", { work: {} }), + channelKey: "discord", + accountId: "work", + policy: "pairing", + allowFrom: [], + allowFromPathSuffix: "dm.", + }), + ).toEqual({ + policy: "pairing", + allowFrom: [], + policyPath: undefined, + allowFromPath: "channels.discord.accounts.work.dm.", + approveHint: formatPairingApproveHint("discord"), + normalizeEntry: undefined, + }); + }); + + it("supports custom defaults and approve hints", () => { + expect( + buildAccountScopedDmSecurityPolicy({ + cfg: cfgWithChannel("synology-chat"), + channelKey: "synology-chat", + fallbackAccountId: "default", + allowFrom: ["user-1"], + defaultPolicy: "allowlist", + policyPathSuffix: "dmPolicy", + approveHint: "openclaw pairing approve synology-chat ", + }), + ).toEqual({ + policy: "allowlist", + allowFrom: ["user-1"], + policyPath: "channels.synology-chat.dmPolicy", + allowFromPath: "channels.synology-chat.", + approveHint: "openclaw pairing approve synology-chat ", + normalizeEntry: undefined, + }); + }); +}); diff --git a/src/channels/plugins/helpers.ts b/src/channels/plugins/helpers.ts index 9e7499c2375..135547d6e9a 100644 --- a/src/channels/plugins/helpers.ts +++ b/src/channels/plugins/helpers.ts @@ -1,6 +1,7 @@ import { formatCliCommand } from "../../cli/command-format.js"; import type { OpenClawConfig } from "../../config/config.js"; import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; +import type { ChannelSecurityDmPolicy } from "./types.core.js"; import type { ChannelPlugin } from "./types.js"; // Channel docking helper: use this when selecting the default account for a plugin. @@ -18,3 +19,40 @@ export function formatPairingApproveHint(channelId: string): string { const approveCmd = formatCliCommand(`openclaw pairing approve ${channelId} `); return `Approve via: ${listCmd} / ${approveCmd}`; } + +export function buildAccountScopedDmSecurityPolicy(params: { + cfg: OpenClawConfig; + channelKey: string; + accountId?: string | null; + fallbackAccountId?: string | null; + policy?: string | null; + allowFrom?: Array | null; + defaultPolicy?: string; + allowFromPathSuffix?: string; + policyPathSuffix?: string; + approveChannelId?: string; + approveHint?: string; + normalizeEntry?: (raw: string) => string; +}): ChannelSecurityDmPolicy { + const resolvedAccountId = params.accountId ?? params.fallbackAccountId ?? DEFAULT_ACCOUNT_ID; + const channelConfig = (params.cfg.channels as Record | undefined)?.[ + params.channelKey + ] as { accounts?: Record } | undefined; + const useAccountPath = Boolean(channelConfig?.accounts?.[resolvedAccountId]); + const basePath = useAccountPath + ? `channels.${params.channelKey}.accounts.${resolvedAccountId}.` + : `channels.${params.channelKey}.`; + const allowFromPath = `${basePath}${params.allowFromPathSuffix ?? ""}`; + const policyPath = + params.policyPathSuffix != null ? `${basePath}${params.policyPathSuffix}` : undefined; + + return { + policy: params.policy ?? params.defaultPolicy ?? "pairing", + allowFrom: params.allowFrom ?? [], + policyPath, + allowFromPath, + approveHint: + params.approveHint ?? formatPairingApproveHint(params.approveChannelId ?? params.channelKey), + normalizeEntry: params.normalizeEntry, + }; +} diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 684a4dbc4a6..6c1558185c2 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -537,7 +537,10 @@ export { buildOpenGroupPolicyRestrictSendersWarning, buildOpenGroupPolicyWarning, } from "../channels/plugins/group-policy-warnings.js"; -export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; +export { + buildAccountScopedDmSecurityPolicy, + formatPairingApproveHint, +} from "../channels/plugins/helpers.js"; export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js"; export type {