From 88d76d4be51a0d468eca2077bd16bc14199de558 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 21 Jan 2026 18:21:19 +0000 Subject: [PATCH] refactor(channels): centralize match metadata --- src/channels/allowlist-match.ts | 4 ++- src/channels/channel-config.test.ts | 29 +++++++++++++++++++ src/channels/channel-config.ts | 18 ++++++++++++ src/channels/plugins/channel-config.ts | 2 ++ src/channels/plugins/index.ts | 2 ++ src/discord/monitor/allow-list.ts | 14 +++------ .../monitor/message-handler.preflight.ts | 9 ++---- src/slack/monitor/channel-config.ts | 18 ++++-------- src/slack/monitor/context.ts | 5 ++-- src/slack/monitor/message-handler/prepare.ts | 5 ++-- src/slack/monitor/slash.ts | 5 ++-- 11 files changed, 73 insertions(+), 38 deletions(-) diff --git a/src/channels/allowlist-match.ts b/src/channels/allowlist-match.ts index 69e797ed9..d77fac1f9 100644 --- a/src/channels/allowlist-match.ts +++ b/src/channels/allowlist-match.ts @@ -16,6 +16,8 @@ export type AllowlistMatch = { matchSource?: TSource; }; -export function formatAllowlistMatchMeta(match?: AllowlistMatch | null): string { +export function formatAllowlistMatchMeta( + match?: { matchKey?: string; matchSource?: string } | null, +): string { return `matchKey=${match?.matchKey ?? "none"} matchSource=${match?.matchSource ?? "none"}`; } diff --git a/src/channels/channel-config.test.ts b/src/channels/channel-config.test.ts index 25cee4ac2..984a486c0 100644 --- a/src/channels/channel-config.test.ts +++ b/src/channels/channel-config.test.ts @@ -6,6 +6,8 @@ import { resolveChannelEntryMatch, resolveChannelEntryMatchWithFallback, resolveNestedAllowlistDecision, + applyChannelMatchMeta, + resolveChannelMatchConfig, } from "./channel-config.js"; describe("buildChannelKeyCandidates", () => { @@ -90,6 +92,33 @@ describe("resolveChannelEntryMatchWithFallback", () => { }); }); +describe("applyChannelMatchMeta", () => { + it("copies match metadata onto resolved configs", () => { + const resolved = applyChannelMatchMeta( + { allowed: true }, + { matchKey: "general", matchSource: "direct" }, + ); + expect(resolved.matchKey).toBe("general"); + expect(resolved.matchSource).toBe("direct"); + }); +}); + +describe("resolveChannelMatchConfig", () => { + it("returns null when no entry is matched", () => { + const resolved = resolveChannelMatchConfig({ matchKey: "x" }, () => ({ allowed: true })); + expect(resolved).toBeNull(); + }); + + it("resolves entry and applies match metadata", () => { + const resolved = resolveChannelMatchConfig( + { entry: { allow: true }, matchKey: "*", matchSource: "wildcard" }, + () => ({ allowed: true }), + ); + expect(resolved?.matchKey).toBe("*"); + expect(resolved?.matchSource).toBe("wildcard"); + }); +}); + describe("resolveNestedAllowlistDecision", () => { it("allows when outer allowlist is disabled", () => { expect( diff --git a/src/channels/channel-config.ts b/src/channels/channel-config.ts index 6bf1300ce..af3898667 100644 --- a/src/channels/channel-config.ts +++ b/src/channels/channel-config.ts @@ -11,6 +11,24 @@ export type ChannelEntryMatch = { matchSource?: ChannelMatchSource; }; +export function applyChannelMatchMeta< + TResult extends { matchKey?: string; matchSource?: ChannelMatchSource }, +>(result: TResult, match: ChannelEntryMatch): TResult { + if (match.matchKey && match.matchSource) { + result.matchKey = match.matchKey; + result.matchSource = match.matchSource; + } + return result; +} + +export function resolveChannelMatchConfig< + TEntry, + TResult extends { matchKey?: string; matchSource?: ChannelMatchSource }, +>(match: ChannelEntryMatch, resolveEntry: (entry: TEntry) => TResult): TResult | null { + if (!match.entry) return null; + return applyChannelMatchMeta(resolveEntry(match.entry), match); +} + export function normalizeChannelSlug(value: string): string { return value .trim() diff --git a/src/channels/plugins/channel-config.ts b/src/channels/plugins/channel-config.ts index e2bd35cdc..9f3a5150b 100644 --- a/src/channels/plugins/channel-config.ts +++ b/src/channels/plugins/channel-config.ts @@ -1,8 +1,10 @@ export type { ChannelEntryMatch, ChannelMatchSource } from "../channel-config.js"; export { + applyChannelMatchMeta, buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatch, resolveChannelEntryMatchWithFallback, + resolveChannelMatchConfig, resolveNestedAllowlistDecision, } from "../channel-config.js"; diff --git a/src/channels/plugins/index.ts b/src/channels/plugins/index.ts index d3861f7fe..6d448df73 100644 --- a/src/channels/plugins/index.ts +++ b/src/channels/plugins/index.ts @@ -60,10 +60,12 @@ export { listWhatsAppDirectoryPeersFromConfig, } from "./directory-config.js"; export { + applyChannelMatchMeta, buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatch, resolveChannelEntryMatchWithFallback, + resolveChannelMatchConfig, resolveNestedAllowlistDecision, type ChannelEntryMatch, type ChannelMatchSource, diff --git a/src/discord/monitor/allow-list.ts b/src/discord/monitor/allow-list.ts index 236cac15e..7d495af66 100644 --- a/src/discord/monitor/allow-list.ts +++ b/src/discord/monitor/allow-list.ts @@ -3,6 +3,7 @@ import type { Guild, User } from "@buape/carbon"; import { buildChannelKeyCandidates, resolveChannelEntryMatchWithFallback, + resolveChannelMatchConfig, type ChannelMatchSource, } from "../../channels/channel-config.js"; import type { AllowlistMatch } from "../../channels/allowlist-match.js"; @@ -205,8 +206,6 @@ function resolveDiscordChannelEntryMatch( function resolveDiscordChannelConfigEntry( entry: DiscordChannelEntry, - matchKey: string | undefined, - matchSource: ChannelMatchSource, ): DiscordChannelConfigResolved { const resolved: DiscordChannelConfigResolved = { allowed: entry.allow !== false, @@ -217,8 +216,6 @@ function resolveDiscordChannelConfigEntry( systemPrompt: entry.systemPrompt, autoThread: entry.autoThread, }; - if (matchKey) resolved.matchKey = matchKey; - resolved.matchSource = matchSource; return resolved; } @@ -236,8 +233,8 @@ export function resolveDiscordChannelConfig(params: { name: channelName, slug: channelSlug, }); - if (!match.entry || !match.matchKey || !match.matchSource) return { allowed: false }; - return resolveDiscordChannelConfigEntry(match.entry, match.matchKey, match.matchSource); + const resolved = resolveChannelMatchConfig(match, resolveDiscordChannelConfigEntry); + return resolved ?? { allowed: false }; } export function resolveDiscordChannelConfigWithFallback(params: { @@ -279,10 +276,7 @@ export function resolveDiscordChannelConfigWithFallback(params: { } : undefined, ); - if (match.entry && match.matchKey && match.matchSource) { - return resolveDiscordChannelConfigEntry(match.entry, match.matchKey, match.matchSource); - } - return { allowed: false }; + return resolveChannelMatchConfig(match, resolveDiscordChannelConfigEntry) ?? { allowed: false }; } export function resolveDiscordShouldRequireMention(params: { diff --git a/src/discord/monitor/message-handler.preflight.ts b/src/discord/monitor/message-handler.preflight.ts index 070a8ef5b..6df141e35 100644 --- a/src/discord/monitor/message-handler.preflight.ts +++ b/src/discord/monitor/message-handler.preflight.ts @@ -15,6 +15,7 @@ import { } from "../../pairing/pairing-store.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { resolveMentionGatingWithBypass } from "../../channels/mention-gating.js"; +import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js"; import { sendMessageDiscord } from "../send.js"; import { resolveControlCommandGate } from "../../channels/command-gating.js"; import { @@ -100,9 +101,7 @@ export async function preflightDiscordMessage( }, }) : { allowed: false }; - const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${ - allowMatch.matchSource ?? "none" - }`; + const allowMatchMeta = formatAllowlistMatchMeta(allowMatch); const permitted = allowMatch.allowed; if (!permitted) { commandAuthorized = false; @@ -262,9 +261,7 @@ export async function preflightDiscordMessage( scope: threadChannel ? "thread" : "channel", }) : null; - const channelMatchMeta = `matchKey=${channelConfig?.matchKey ?? "none"} matchSource=${ - channelConfig?.matchSource ?? "none" - }`; + const channelMatchMeta = formatAllowlistMatchMeta(channelConfig); if (isGuildMessage && channelConfig?.enabled === false) { logVerbose( `Blocked discord channel ${message.channelId} (channel disabled, ${channelMatchMeta})`, diff --git a/src/slack/monitor/channel-config.ts b/src/slack/monitor/channel-config.ts index 2b9a31291..3e3c541c9 100644 --- a/src/slack/monitor/channel-config.ts +++ b/src/slack/monitor/channel-config.ts @@ -1,8 +1,10 @@ import type { SlackReactionNotificationMode } from "../../config/config.js"; import type { SlackMessageEvent } from "../types.js"; import { + applyChannelMatchMeta, buildChannelKeyCandidates, resolveChannelEntryMatchWithFallback, + type ChannelMatchSource, } from "../../channels/channel-config.js"; import { allowListMatches, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js"; @@ -14,7 +16,7 @@ export type SlackChannelConfigResolved = { skills?: string[]; systemPrompt?: string; matchKey?: string; - matchSource?: "direct" | "wildcard"; + matchSource?: ChannelMatchSource; }; function firstDefined(...values: Array) { @@ -89,16 +91,12 @@ export function resolveSlackChannelConfig(params: { directName, normalizedName, ); - const { - entry: matched, - wildcardEntry: fallback, - matchKey, - matchSource, - } = resolveChannelEntryMatchWithFallback({ + const match = resolveChannelEntryMatchWithFallback({ entries, keys: candidates, wildcardKey: "*", }); + const { entry: matched, wildcardEntry: fallback } = match; const requireMentionDefault = defaultRequireMention ?? true; if (keys.length === 0) { @@ -127,11 +125,7 @@ export function resolveSlackChannelConfig(params: { skills, systemPrompt, }; - if (matchKey) result.matchKey = matchKey; - if (matchSource === "direct" || matchSource === "wildcard") { - result.matchSource = matchSource; - } - return result; + return applyChannelMatchMeta(result, match); } export type { SlackMessageEvent }; diff --git a/src/slack/monitor/context.ts b/src/slack/monitor/context.ts index caeaac9b3..bd2425103 100644 --- a/src/slack/monitor/context.ts +++ b/src/slack/monitor/context.ts @@ -8,6 +8,7 @@ import { createDedupeCache } from "../../infra/dedupe.js"; import { getChildLogger } from "../../logging.js"; import type { RuntimeEnv } from "../../runtime.js"; import type { SlackMessageEvent } from "../types.js"; +import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js"; import { normalizeAllowList, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js"; import { resolveSlackChannelConfig } from "./channel-config.js"; @@ -310,9 +311,7 @@ export function createSlackMonitorContext(params: { channels: params.channelsConfig, defaultRequireMention, }); - const channelMatchMeta = `matchKey=${channelConfig?.matchKey ?? "none"} matchSource=${ - channelConfig?.matchSource ?? "none" - }`; + const channelMatchMeta = formatAllowlistMatchMeta(channelConfig); const channelAllowed = channelConfig?.allowed !== false; const channelAllowlistConfigured = Boolean(params.channelsConfig) && Object.keys(params.channelsConfig ?? {}).length > 0; diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index 849c08d31..b1d9a15c6 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -22,6 +22,7 @@ import { resolveThreadSessionKeys } from "../../../routing/session-key.js"; import { resolveMentionGatingWithBypass } from "../../../channels/mention-gating.js"; import { resolveConversationLabel } from "../../../channels/conversation-label.js"; import { resolveControlCommandGate } from "../../../channels/command-gating.js"; +import { formatAllowlistMatchMeta } from "../../../channels/allowlist-match.js"; import { readSessionUpdatedAt, recordSessionMetaFromInbound, @@ -131,9 +132,7 @@ export async function prepareSlackMessage(params: { allowList: allowFromLower, id: directUserId, }); - const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${ - allowMatch.matchSource ?? "none" - }`; + const allowMatchMeta = formatAllowlistMatchMeta(allowMatch); if (!allowMatch.allowed) { if (ctx.dmPolicy === "pairing") { const sender = await ctx.resolveUserName(directUserId); diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index 7221d86eb..d8e97dd43 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -21,6 +21,7 @@ import { import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { resolveConversationLabel } from "../../channels/conversation-label.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js"; +import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js"; import type { ResolvedSlackAccount } from "../accounts.js"; @@ -206,9 +207,7 @@ export function registerSlackMonitorSlashCommands(params: { id: command.user_id, name: senderName, }); - const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${ - allowMatch.matchSource ?? "none" - }`; + const allowMatchMeta = formatAllowlistMatchMeta(allowMatch); if (!allowMatch.allowed) { if (ctx.dmPolicy === "pairing") { const { code, created } = await upsertChannelPairingRequest({