fix(outbound): restore generic delivery and security seams

This commit is contained in:
Peter Steinberger
2026-04-03 17:55:27 +01:00
parent ab96520bba
commit 856592cf00
57 changed files with 1930 additions and 1517 deletions

View File

@@ -1,3 +1,4 @@
import { normalizeAnyChannelId } from "../channels/registry.js";
import type { OutboundSendDeps } from "../infra/outbound/deliver.js";
/**
@@ -6,23 +7,39 @@ import type { OutboundSendDeps } from "../infra/outbound/deliver.js";
*/
export type CliOutboundSendSource = { [channelId: string]: unknown };
const LEGACY_SOURCE_TO_CHANNEL = {
sendMessageWhatsApp: "whatsapp",
sendMessageTelegram: "telegram",
sendMessageDiscord: "discord",
sendMessageSlack: "slack",
sendMessageSignal: "signal",
sendMessageIMessage: "imessage",
} as const;
function normalizeLegacyChannelStem(raw: string): string {
return raw
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
.replace(/_/g, "-")
.trim()
.toLowerCase()
.replace(/-/g, "");
}
const CHANNEL_TO_LEGACY_DEP_KEY = {
whatsapp: "sendWhatsApp",
telegram: "sendTelegram",
discord: "sendDiscord",
slack: "sendSlack",
signal: "sendSignal",
imessage: "sendIMessage",
} as const;
function resolveChannelIdFromLegacySourceKey(key: string): string | undefined {
const match = key.match(/^sendMessage(.+)$/);
if (!match) {
return undefined;
}
const normalizedStem = normalizeLegacyChannelStem(match[1] ?? "");
return normalizeAnyChannelId(normalizedStem) ?? (normalizedStem || undefined);
}
function resolveLegacyDepKeysForChannel(channelId: string): string[] {
const compact = channelId.replace(/[^a-z0-9]+/gi, "");
if (!compact) {
return [];
}
const pascal = compact.charAt(0).toUpperCase() + compact.slice(1);
const keys = new Set<string>([`send${pascal}`]);
if (pascal.startsWith("I") && pascal.length > 1) {
keys.add(`sendI${pascal.slice(1)}`);
}
if (pascal.startsWith("Ms") && pascal.length > 2) {
keys.add(`sendMS${pascal.slice(2)}`);
}
return [...keys];
}
/**
* Pass CLI send sources through as-is — both CliOutboundSendSource and
@@ -31,17 +48,26 @@ const CHANNEL_TO_LEGACY_DEP_KEY = {
export function createOutboundSendDepsFromCliSource(deps: CliOutboundSendSource): OutboundSendDeps {
const outbound: OutboundSendDeps = { ...deps };
for (const [legacySourceKey, channelId] of Object.entries(LEGACY_SOURCE_TO_CHANNEL)) {
for (const legacySourceKey of Object.keys(deps)) {
const channelId = resolveChannelIdFromLegacySourceKey(legacySourceKey);
if (!channelId) {
continue;
}
const sourceValue = deps[legacySourceKey];
if (sourceValue !== undefined && outbound[channelId] === undefined) {
outbound[channelId] = sourceValue;
}
}
for (const [channelId, legacyDepKey] of Object.entries(CHANNEL_TO_LEGACY_DEP_KEY)) {
for (const channelId of Object.keys(outbound)) {
const sourceValue = outbound[channelId];
if (sourceValue !== undefined && outbound[legacyDepKey] === undefined) {
outbound[legacyDepKey] = sourceValue;
if (sourceValue === undefined) {
continue;
}
for (const legacyDepKey of resolveLegacyDepKeysForChannel(channelId)) {
if (outbound[legacyDepKey] === undefined) {
outbound[legacyDepKey] = sourceValue;
}
}
}

View File

@@ -1,7 +1,6 @@
import { createPluginBoundaryRuntimeSend } from "./plugin-boundary-send.js";
import { createChannelOutboundRuntimeSend } from "./channel-outbound-send.js";
export const runtimeSend = createPluginBoundaryRuntimeSend({
pluginId: "discord",
exportName: "sendMessageDiscord",
missingLabel: "Discord plugin runtime",
export const runtimeSend = createChannelOutboundRuntimeSend({
channelId: "discord",
unavailableMessage: "Discord outbound adapter is unavailable.",
});

View File

@@ -1,37 +0,0 @@
import { createCachedPluginBoundaryModuleLoader } from "../../plugins/runtime/runtime-plugin-boundary.js";
type RuntimeSendModule = Record<string, unknown>;
export type RuntimeSend = {
sendMessage: (...args: unknown[]) => Promise<unknown>;
};
function resolveRuntimeExport(
module: RuntimeSendModule | null,
pluginId: string,
exportName: string,
): (...args: unknown[]) => Promise<unknown> {
const candidate = module?.[exportName];
if (typeof candidate !== "function") {
throw new Error(`${pluginId} plugin runtime is unavailable: missing export '${exportName}'`);
}
return candidate as (...args: unknown[]) => Promise<unknown>;
}
export function createPluginBoundaryRuntimeSend(params: {
pluginId: string;
exportName: string;
missingLabel: string;
}): RuntimeSend {
const loadRuntimeModuleSync = createCachedPluginBoundaryModuleLoader<RuntimeSendModule>({
pluginId: params.pluginId,
entryBaseName: "runtime-api",
required: true,
missingLabel: params.missingLabel,
});
return {
sendMessage: (...args) =>
resolveRuntimeExport(loadRuntimeModuleSync(), params.pluginId, params.exportName)(...args),
};
}

View File

@@ -1,7 +1,6 @@
import { createPluginBoundaryRuntimeSend } from "./plugin-boundary-send.js";
import { createChannelOutboundRuntimeSend } from "./channel-outbound-send.js";
export const runtimeSend = createPluginBoundaryRuntimeSend({
pluginId: "signal",
exportName: "sendMessageSignal",
missingLabel: "Signal plugin runtime",
export const runtimeSend = createChannelOutboundRuntimeSend({
channelId: "signal",
unavailableMessage: "Signal outbound adapter is unavailable.",
});

View File

@@ -1,7 +1,6 @@
import { createPluginBoundaryRuntimeSend } from "./plugin-boundary-send.js";
import { createChannelOutboundRuntimeSend } from "./channel-outbound-send.js";
export const runtimeSend = createPluginBoundaryRuntimeSend({
pluginId: "slack",
exportName: "sendMessageSlack",
missingLabel: "Slack plugin runtime",
export const runtimeSend = createChannelOutboundRuntimeSend({
channelId: "slack",
unavailableMessage: "Slack outbound adapter is unavailable.",
});

View File

@@ -1,39 +1,6 @@
import { loadChannelOutboundAdapter } from "../../channels/plugins/outbound/load.js";
import { loadConfig } from "../../config/config.js";
import { createChannelOutboundRuntimeSend } from "./channel-outbound-send.js";
type TelegramRuntimeSendOpts = {
cfg?: ReturnType<typeof loadConfig>;
mediaUrl?: string;
mediaLocalRoots?: readonly string[];
accountId?: string;
messageThreadId?: string | number;
replyToMessageId?: string | number;
silent?: boolean;
forceDocument?: boolean;
gatewayClientScopes?: readonly string[];
};
export const runtimeSend = {
sendMessage: async (to: string, text: string, opts: TelegramRuntimeSendOpts = {}) => {
const outbound = await loadChannelOutboundAdapter("telegram");
if (!outbound?.sendText) {
throw new Error("Telegram outbound adapter is unavailable.");
}
return await outbound.sendText({
cfg: opts.cfg ?? loadConfig(),
to,
text,
mediaUrl: opts.mediaUrl,
mediaLocalRoots: opts.mediaLocalRoots,
accountId: opts.accountId,
threadId: opts.messageThreadId,
replyToId:
opts.replyToMessageId == null
? undefined
: String(opts.replyToMessageId).trim() || undefined,
silent: opts.silent,
forceDocument: opts.forceDocument,
gatewayClientScopes: opts.gatewayClientScopes,
});
},
};
export const runtimeSend = createChannelOutboundRuntimeSend({
channelId: "telegram",
unavailableMessage: "Telegram outbound adapter is unavailable.",
});

View File

@@ -1,7 +1,6 @@
import { createPluginBoundaryRuntimeSend } from "./plugin-boundary-send.js";
import { createChannelOutboundRuntimeSend } from "./channel-outbound-send.js";
export const runtimeSend = createPluginBoundaryRuntimeSend({
pluginId: "whatsapp",
exportName: "sendMessageWhatsApp",
missingLabel: "WhatsApp plugin runtime",
export const runtimeSend = createChannelOutboundRuntimeSend({
channelId: "whatsapp",
unavailableMessage: "WhatsApp outbound adapter is unavailable.",
});