refactor: deduplicate channel config adapters

This commit is contained in:
Peter Steinberger
2026-03-18 04:51:01 +00:00
parent 2c5fd8e0c1
commit 05603e4e6c
34 changed files with 605 additions and 321 deletions

View File

@@ -1,7 +1,6 @@
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
import {
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
@@ -47,8 +46,12 @@ const loadBlueBubblesChannelRuntime = createLazyRuntimeNamedExport(
"blueBubblesChannelRuntime",
);
const bluebubblesConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveBlueBubblesAccount({ cfg, accountId }),
const bluebubblesConfigAdapter = createScopedChannelConfigAdapter<ResolvedBlueBubblesAccount>({
sectionKey: "bluebubbles",
listAccountIds: listBlueBubblesAccountIds,
resolveAccount: (cfg, accountId) => resolveBlueBubblesAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultBlueBubblesAccountId,
clearBaseFields: ["serverUrl", "password", "name", "webhookPath"],
resolveAllowFrom: (account: ResolvedBlueBubblesAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
formatNormalizedAllowFromEntries({
@@ -57,14 +60,6 @@ const bluebubblesConfigAccessors = createScopedAccountConfigAccessors({
}),
});
const bluebubblesConfigBase = createScopedChannelConfigBase<ResolvedBlueBubblesAccount>({
sectionKey: "bluebubbles",
listAccountIds: listBlueBubblesAccountIds,
resolveAccount: (cfg, accountId) => resolveBlueBubblesAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultBlueBubblesAccountId,
clearBaseFields: ["serverUrl", "password", "name", "webhookPath"],
});
const resolveBlueBubblesDmPolicy = createScopedDmSecurityResolver<ResolvedBlueBubblesAccount>({
channelKey: "bluebubbles",
resolvePolicy: (account) => account.config.dmPolicy,
@@ -115,7 +110,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
configSchema: buildChannelConfigSchema(BlueBubblesConfigSchema),
setupWizard: blueBubblesSetupWizard,
config: {
...bluebubblesConfigBase,
...bluebubblesConfigAdapter,
isConfigured: (account) => account.configured,
describeAccount: (account): ChannelAccountSnapshot => ({
accountId: account.accountId,
@@ -124,7 +119,6 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
configured: account.configured,
baseUrl: account.baseUrl,
}),
...bluebubblesConfigAccessors,
},
actions: bluebubblesMessageActions,
security: {

View File

@@ -53,7 +53,7 @@ import {
import { getDiscordRuntime } from "./runtime.js";
import { fetchChannelPermissionsDiscord } from "./send.js";
import { discordSetupAdapter } from "./setup-core.js";
import { createDiscordPluginBase, discordConfigAccessors } from "./shared.js";
import { createDiscordPluginBase, discordConfigAdapter } from "./shared.js";
import { collectDiscordStatusIssues } from "./status-issues.js";
import { parseDiscordTarget } from "./targets.js";
import { DiscordUiContainer } from "./ui.js";
@@ -307,7 +307,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
applyConfigEdit: buildAccountScopedAllowlistConfigEditor({
channelId: "discord",
normalize: ({ cfg, accountId, values }) =>
discordConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
discordConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
resolvePaths: resolveLegacyDmAllowlistConfigPaths,
}),
},

View File

@@ -23,8 +23,11 @@ export {
listDiscordDirectoryPeersFromConfig,
} from "./directory-config.js";
export {
createHybridChannelConfigAdapter,
createScopedChannelConfigAdapter,
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createTopLevelChannelConfigAdapter,
} from "openclaw/plugin-sdk/channel-config-helpers";
export {
createAccountActionGate,

View File

@@ -8,8 +8,7 @@ import {
type ResolvedDiscordAccount,
} from "./accounts.js";
import {
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createScopedChannelConfigAdapter,
buildChannelConfigSchema,
DiscordConfigSchema,
getChatChannelMeta,
@@ -27,20 +26,16 @@ export const discordSetupWizard = createDiscordSetupWizardProxy(
async () => (await loadDiscordChannelRuntime()).discordSetupWizard,
);
export const discordConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveDiscordAccount({ cfg, accountId }),
resolveAllowFrom: (account: ResolvedDiscordAccount) => account.config.dm?.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account: ResolvedDiscordAccount) => account.config.defaultTo,
});
export const discordConfigBase = createScopedChannelConfigBase<ResolvedDiscordAccount>({
export const discordConfigAdapter = createScopedChannelConfigAdapter<ResolvedDiscordAccount>({
sectionKey: DISCORD_CHANNEL,
listAccountIds: listDiscordAccountIds,
resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
inspectAccount: (cfg, accountId) => inspectDiscordAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultDiscordAccountId,
clearBaseFields: ["token", "name"],
resolveAllowFrom: (account: ResolvedDiscordAccount) => account.config.dm?.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account: ResolvedDiscordAccount) => account.config.defaultTo,
});
export function createDiscordPluginBase(params: {
@@ -75,7 +70,7 @@ export function createDiscordPluginBase(params: {
reload: { configPrefixes: ["channels.discord"] },
configSchema: buildChannelConfigSchema(DiscordConfigSchema),
config: {
...discordConfigBase,
...discordConfigAdapter,
isConfigured: (account) => Boolean(account.token?.trim()),
describeAccount: (account) => ({
accountId: account.accountId,
@@ -84,7 +79,6 @@ export function createDiscordPluginBase(params: {
configured: Boolean(account.token?.trim()),
tokenSource: account.tokenSource,
}),
...discordConfigAccessors,
},
setup: params.setup,
}) as Pick<

View File

@@ -1,8 +1,5 @@
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import {
createHybridChannelConfigBase,
createScopedAccountConfigAccessors,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createHybridChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import { collectAllowlistProviderRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy";
import { createMessageToolCardSchema } from "openclaw/plugin-sdk/channel-runtime";
import type {
@@ -130,17 +127,16 @@ function setFeishuNamedAccountEnabled(
};
}
const feishuConfigBase = createHybridChannelConfigBase<ResolvedFeishuAccount, ClawdbotConfig>({
const feishuConfigAdapter = createHybridChannelConfigAdapter<
ResolvedFeishuAccount,
ResolvedFeishuAccount,
ClawdbotConfig
>({
sectionKey: "feishu",
listAccountIds: listFeishuAccountIds,
resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultFeishuAccountId,
clearBaseFields: [],
});
const feishuConfigAccessors = createScopedAccountConfigAccessors<ResolvedFeishuAccount>({
resolveAccount: ({ cfg, accountId }) =>
resolveFeishuAccount({ cfg: cfg as ClawdbotConfig, accountId }),
resolveAllowFrom: (account) => account.config.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
});
@@ -396,7 +392,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
reload: { configPrefixes: ["channels.feishu"] },
configSchema: buildChannelConfigSchema(FeishuConfigSchema),
config: {
...feishuConfigBase,
...feishuConfigAdapter,
setAccountEnabled: ({ cfg, accountId, enabled }) => {
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
if (isDefault) {
@@ -454,7 +450,6 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
appId: account.appId,
domain: account.domain,
}),
...feishuConfigAccessors,
},
actions: {
describeMessageTool: describeFeishuMessageTool,

View File

@@ -9,8 +9,11 @@ export {
readStringParam,
} from "../../src/agents/tools/common.js";
export {
createScopedChannelConfigAdapter,
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createTopLevelChannelConfigAdapter,
createHybridChannelConfigAdapter,
createScopedDmSecurityResolver,
} from "../../src/plugin-sdk/channel-config-helpers.js";
export {

View File

@@ -1,7 +1,6 @@
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
import {
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import {
@@ -61,18 +60,7 @@ const formatAllowFromEntry = (entry: string) =>
.replace(/^users\//i, "")
.toLowerCase();
const googleChatConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveGoogleChatAccount({ cfg, accountId }),
resolveAllowFrom: (account: ResolvedGoogleChatAccount) => account.config.dm?.allowFrom,
formatAllowFrom: (allowFrom) =>
formatNormalizedAllowFromEntries({
allowFrom,
normalizeEntry: formatAllowFromEntry,
}),
resolveDefaultTo: (account: ResolvedGoogleChatAccount) => account.config.defaultTo,
});
const googleChatConfigBase = createScopedChannelConfigBase<ResolvedGoogleChatAccount>({
const googleChatConfigAdapter = createScopedChannelConfigAdapter<ResolvedGoogleChatAccount>({
sectionKey: "googlechat",
listAccountIds: listGoogleChatAccountIds,
resolveAccount: (cfg, accountId) => resolveGoogleChatAccount({ cfg, accountId }),
@@ -87,6 +75,13 @@ const googleChatConfigBase = createScopedChannelConfigBase<ResolvedGoogleChatAcc
"botUser",
"name",
],
resolveAllowFrom: (account: ResolvedGoogleChatAccount) => account.config.dm?.allowFrom,
formatAllowFrom: (allowFrom) =>
formatNormalizedAllowFromEntries({
allowFrom,
normalizeEntry: formatAllowFromEntry,
}),
resolveDefaultTo: (account: ResolvedGoogleChatAccount) => account.config.defaultTo,
});
const resolveGoogleChatDmPolicy = createScopedDmSecurityResolver<ResolvedGoogleChatAccount>({
@@ -146,7 +141,7 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
reload: { configPrefixes: ["channels.googlechat"] },
configSchema: buildChannelConfigSchema(GoogleChatConfigSchema),
config: {
...googleChatConfigBase,
...googleChatConfigAdapter,
isConfigured: (account) => account.credentialSource !== "none",
describeAccount: (account) => ({
accountId: account.accountId,
@@ -155,7 +150,6 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
configured: account.credentialSource !== "none",
credentialSource: account.credentialSource,
}),
...googleChatConfigAccessors,
},
security: {
resolveDmPolicy: resolveGoogleChatDmPolicy,

View File

@@ -1,8 +1,8 @@
import {
collectAllowlistProviderRestrictSendersWarnings,
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
formatTrimmedAllowFromEntries,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
import {
@@ -29,19 +29,15 @@ export const imessageSetupWizard = createIMessageSetupWizardProxy(
async () => (await loadIMessageChannelRuntime()).imessageSetupWizard,
);
export const imessageConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveIMessageAccount({ cfg, accountId }),
resolveAllowFrom: (account: ResolvedIMessageAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) => allowFrom.map((entry) => String(entry).trim()).filter(Boolean),
resolveDefaultTo: (account: ResolvedIMessageAccount) => account.config.defaultTo,
});
export const imessageConfigBase = createScopedChannelConfigBase<ResolvedIMessageAccount>({
export const imessageConfigAdapter = createScopedChannelConfigAdapter<ResolvedIMessageAccount>({
sectionKey: IMESSAGE_CHANNEL,
listAccountIds: listIMessageAccountIds,
resolveAccount: (cfg, accountId) => resolveIMessageAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultIMessageAccountId,
clearBaseFields: ["cliPath", "dbPath", "service", "region", "name"],
resolveAllowFrom: (account: ResolvedIMessageAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) => formatTrimmedAllowFromEntries(allowFrom),
resolveDefaultTo: (account: ResolvedIMessageAccount) => account.config.defaultTo,
});
export const imessageResolveDmPolicy = createScopedDmSecurityResolver<ResolvedIMessageAccount>({
@@ -97,7 +93,7 @@ export function createIMessagePluginBase(params: {
reload: { configPrefixes: ["channels.imessage"] },
configSchema: buildChannelConfigSchema(IMessageConfigSchema),
config: {
...imessageConfigBase,
...imessageConfigAdapter,
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
accountId: account.accountId,
@@ -105,7 +101,6 @@ export function createIMessagePluginBase(params: {
enabled: account.enabled,
configured: account.configured,
}),
...imessageConfigAccessors,
},
security: {
resolveDmPolicy: imessageResolveDmPolicy,

View File

@@ -1,7 +1,6 @@
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
import {
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import {
@@ -51,18 +50,11 @@ function normalizePairingTarget(raw: string): string {
return normalized.split(/[!@]/, 1)[0]?.trim() ?? "";
}
const ircConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }),
resolveAllowFrom: (account: ResolvedIrcAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
formatNormalizedAllowFromEntries({
allowFrom,
normalizeEntry: normalizeIrcAllowEntry,
}),
resolveDefaultTo: (account: ResolvedIrcAccount) => account.config.defaultTo,
});
const ircConfigBase = createScopedChannelConfigBase<ResolvedIrcAccount, CoreConfig>({
const ircConfigAdapter = createScopedChannelConfigAdapter<
ResolvedIrcAccount,
ResolvedIrcAccount,
CoreConfig
>({
sectionKey: "irc",
listAccountIds: listIrcAccountIds,
resolveAccount: (cfg, accountId) => resolveIrcAccount({ cfg, accountId }),
@@ -79,6 +71,13 @@ const ircConfigBase = createScopedChannelConfigBase<ResolvedIrcAccount, CoreConf
"passwordFile",
"channels",
],
resolveAllowFrom: (account: ResolvedIrcAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
formatNormalizedAllowFromEntries({
allowFrom,
normalizeEntry: normalizeIrcAllowEntry,
}),
resolveDefaultTo: (account: ResolvedIrcAccount) => account.config.defaultTo,
});
const resolveIrcDmPolicy = createScopedDmSecurityResolver<ResolvedIrcAccount>({
@@ -116,7 +115,7 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = {
reload: { configPrefixes: ["channels.irc"] },
configSchema: buildChannelConfigSchema(IrcConfigSchema),
config: {
...ircConfigBase,
...ircConfigAdapter,
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
accountId: account.accountId,
@@ -129,7 +128,6 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = {
nick: account.nick,
passwordSource: account.passwordSource,
}),
...ircConfigAccessors,
},
security: {
resolveDmPolicy: resolveIrcDmPolicy,

View File

@@ -2,10 +2,9 @@ import {
buildChannelConfigSchema,
LineConfigSchema,
type ChannelPlugin,
type OpenClawConfig,
type ResolvedLineAccount,
} from "../api.js";
import { listLineAccountIds, resolveDefaultLineAccountId, resolveLineAccount } from "../api.js";
import { lineConfigAdapter } from "./config-adapter.js";
import { lineSetupAdapter } from "./setup-core.js";
import { lineSetupWizard } from "./setup-surface.js";
@@ -20,8 +19,6 @@ const meta = {
systemImage: "message.fill",
} as const;
const normalizeLineAllowFrom = (entry: string) => entry.replace(/^line:(?:user:)?/i, "");
export const lineSetupPlugin: ChannelPlugin<ResolvedLineAccount> = {
id: "line",
meta: {
@@ -39,10 +36,7 @@ export const lineSetupPlugin: ChannelPlugin<ResolvedLineAccount> = {
reload: { configPrefixes: ["channels.line"] },
configSchema: buildChannelConfigSchema(LineConfigSchema),
config: {
listAccountIds: (cfg: OpenClawConfig) => listLineAccountIds(cfg),
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) =>
resolveLineAccount({ cfg, accountId: accountId ?? undefined }),
defaultAccountId: (cfg: OpenClawConfig) => resolveDefaultLineAccountId(cfg),
...lineConfigAdapter,
isConfigured: (account) =>
Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
describeAccount: (account) => ({
@@ -52,13 +46,6 @@ export const lineSetupPlugin: ChannelPlugin<ResolvedLineAccount> = {
configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
tokenSource: account.tokenSource ?? undefined,
}),
resolveAllowFrom: ({ cfg, accountId }) =>
resolveLineAccount({ cfg, accountId: accountId ?? undefined }).config.allowFrom,
formatAllowFrom: ({ allowFrom }) =>
allowFrom
.map((entry) => String(entry).trim())
.filter(Boolean)
.map((entry) => normalizeLineAllowFrom(entry)),
},
setupWizard: lineSetupWizard,
setup: lineSetupAdapter,

View File

@@ -1,8 +1,4 @@
import {
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
import { collectAllowlistProviderRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy";
import {
buildChannelConfigSchema,
@@ -14,11 +10,11 @@ import {
processLineMessage,
type ChannelPlugin,
type ChannelStatusIssue,
type OpenClawConfig,
type LineConfig,
type LineChannelData,
type ResolvedLineAccount,
} from "../api.js";
import { lineConfigAdapter } from "./config-adapter.js";
import { resolveLineGroupRequireMention } from "./group-policy.js";
import { getLineRuntime } from "./runtime.js";
import { lineSetupAdapter } from "./setup-core.js";
@@ -36,26 +32,6 @@ const meta = {
systemImage: "message.fill",
};
const lineConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) =>
getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId: accountId ?? undefined }),
resolveAllowFrom: (account: ResolvedLineAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
allowFrom
.map((entry) => String(entry).trim())
.filter(Boolean)
.map((entry) => entry.replace(/^line:(?:user:)?/i, "")),
});
const lineConfigBase = createScopedChannelConfigBase<ResolvedLineAccount, OpenClawConfig>({
sectionKey: "line",
listAccountIds: (cfg) => getLineRuntime().channel.line.listLineAccountIds(cfg),
resolveAccount: (cfg, accountId) =>
getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId: accountId ?? undefined }),
defaultAccountId: (cfg) => getLineRuntime().channel.line.resolveDefaultLineAccountId(cfg),
clearBaseFields: ["channelSecret", "tokenFile", "secretFile"],
});
const resolveLineDmPolicy = createScopedDmSecurityResolver<ResolvedLineAccount>({
channelKey: "line",
resolvePolicy: (account) => account.config.dmPolicy,
@@ -100,7 +76,7 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
configSchema: buildChannelConfigSchema(LineConfigSchema),
setupWizard: lineSetupWizard,
config: {
...lineConfigBase,
...lineConfigAdapter,
isConfigured: (account) =>
Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
describeAccount: (account) => ({
@@ -110,7 +86,6 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
tokenSource: account.tokenSource ?? undefined,
}),
...lineConfigAccessors,
},
security: {
resolveDmPolicy: resolveLineDmPolicy,

View File

@@ -0,0 +1,32 @@
import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import type { OpenClawConfig, ResolvedLineAccount } from "../api.js";
import { getLineRuntime } from "./runtime.js";
function resolveLineRuntimeAccount(cfg: OpenClawConfig, accountId?: string | null) {
return getLineRuntime().channel.line.resolveLineAccount({
cfg,
accountId: accountId ?? undefined,
});
}
export function normalizeLineAllowFrom(entry: string): string {
return entry.replace(/^line:(?:user:)?/i, "");
}
export const lineConfigAdapter = createScopedChannelConfigAdapter<
ResolvedLineAccount,
ResolvedLineAccount,
OpenClawConfig
>({
sectionKey: "line",
listAccountIds: (cfg) => getLineRuntime().channel.line.listLineAccountIds(cfg),
resolveAccount: (cfg, accountId) => resolveLineRuntimeAccount(cfg, accountId),
defaultAccountId: (cfg) => getLineRuntime().channel.line.resolveDefaultLineAccountId(cfg),
clearBaseFields: ["channelSecret", "tokenFile", "secretFile"],
resolveAllowFrom: (account) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
allowFrom
.map((entry) => String(entry).trim())
.filter(Boolean)
.map(normalizeLineAllowFrom),
});

View File

@@ -1,6 +1,5 @@
import {
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import {
@@ -69,17 +68,16 @@ function normalizeMatrixMessagingTarget(raw: string): string | undefined {
return stripped || undefined;
}
const matrixConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) =>
resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId }),
resolveAllowFrom: (account) => account.dm?.allowFrom,
formatAllowFrom: (allowFrom) => normalizeMatrixAllowList(allowFrom),
});
const matrixConfigBase = createScopedChannelConfigBase<ResolvedMatrixAccount, CoreConfig>({
const matrixConfigAdapter = createScopedChannelConfigAdapter<
ResolvedMatrixAccount,
ReturnType<typeof resolveMatrixAccountConfig>,
CoreConfig
>({
sectionKey: "matrix",
listAccountIds: listMatrixAccountIds,
resolveAccount: (cfg, accountId) => resolveMatrixAccount({ cfg, accountId }),
resolveAccessorAccount: ({ cfg, accountId }) =>
resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId }),
defaultAccountId: resolveDefaultMatrixAccountId,
clearBaseFields: [
"name",
@@ -90,6 +88,8 @@ const matrixConfigBase = createScopedChannelConfigBase<ResolvedMatrixAccount, Co
"deviceName",
"initialSyncLimit",
],
resolveAllowFrom: (account) => account.dm?.allowFrom,
formatAllowFrom: (allowFrom) => normalizeMatrixAllowList(allowFrom),
});
const resolveMatrixDmPolicy = createScopedDmSecurityResolver<ResolvedMatrixAccount>({
@@ -122,7 +122,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
reload: { configPrefixes: ["channels.matrix"] },
configSchema: buildChannelConfigSchema(MatrixConfigSchema),
config: {
...matrixConfigBase,
...matrixConfigAdapter,
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
accountId: account.accountId,
@@ -131,7 +131,6 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
configured: account.configured,
baseUrl: account.homeserver,
}),
...matrixConfigAccessors,
},
security: {
resolveDmPolicy: resolveMatrixDmPolicy,

View File

@@ -1,7 +1,6 @@
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
import {
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { collectAllowlistProviderRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy";
@@ -248,8 +247,12 @@ function formatAllowEntry(entry: string): string {
return trimmed.replace(/^(mattermost|user):/i, "").toLowerCase();
}
const mattermostConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveMattermostAccount({ cfg, accountId }),
const mattermostConfigAdapter = createScopedChannelConfigAdapter<ResolvedMattermostAccount>({
sectionKey: "mattermost",
listAccountIds: listMattermostAccountIds,
resolveAccount: (cfg, accountId) => resolveMattermostAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultMattermostAccountId,
clearBaseFields: ["botToken", "baseUrl", "name"],
resolveAllowFrom: (account: ResolvedMattermostAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
formatNormalizedAllowFromEntries({
@@ -258,14 +261,6 @@ const mattermostConfigAccessors = createScopedAccountConfigAccessors({
}),
});
const mattermostConfigBase = createScopedChannelConfigBase<ResolvedMattermostAccount>({
sectionKey: "mattermost",
listAccountIds: listMattermostAccountIds,
resolveAccount: (cfg, accountId) => resolveMattermostAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultMattermostAccountId,
clearBaseFields: ["botToken", "baseUrl", "name"],
});
const resolveMattermostDmPolicy = createScopedDmSecurityResolver<ResolvedMattermostAccount>({
channelKey: "mattermost",
resolvePolicy: (account) => account.config.dmPolicy,
@@ -311,7 +306,7 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
reload: { configPrefixes: ["channels.mattermost"] },
configSchema: buildChannelConfigSchema(MattermostConfigSchema),
config: {
...mattermostConfigBase,
...mattermostConfigAdapter,
isConfigured: (account) => Boolean(account.botToken && account.baseUrl),
describeAccount: (account) => ({
accountId: account.accountId,
@@ -321,7 +316,6 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
botTokenSource: account.botTokenSource,
baseUrl: account.baseUrl,
}),
...mattermostConfigAccessors,
},
security: {
resolveDmPolicy: resolveMattermostDmPolicy,

View File

@@ -1,8 +1,5 @@
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import {
createScopedAccountConfigAccessors,
createTopLevelChannelConfigBase,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import { collectAllowlistProviderRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy";
import { createMessageToolCardSchema } from "openclaw/plugin-sdk/channel-runtime";
import type {
@@ -73,20 +70,20 @@ const resolveMSTeamsChannelConfig = (cfg: OpenClawConfig) => ({
defaultTo: cfg.channels?.msteams?.defaultTo,
});
const msteamsConfigBase = createTopLevelChannelConfigBase<ResolvedMSTeamsAccount>({
const msteamsConfigAdapter = createTopLevelChannelConfigAdapter<
ResolvedMSTeamsAccount,
{
allowFrom?: Array<string | number>;
defaultTo?: string;
}
>({
sectionKey: "msteams",
resolveAccount: (cfg) => ({
accountId: DEFAULT_ACCOUNT_ID,
enabled: cfg.channels?.msteams?.enabled !== false,
configured: Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
}),
});
const msteamsConfigAccessors = createScopedAccountConfigAccessors<{
allowFrom?: Array<string | number>;
defaultTo?: string;
}>({
resolveAccount: ({ cfg }) => resolveMSTeamsChannelConfig(cfg),
resolveAccessorAccount: ({ cfg }) => resolveMSTeamsChannelConfig(cfg),
resolveAllowFrom: (account) => account.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account) => account.defaultTo,
@@ -157,14 +154,13 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
reload: { configPrefixes: ["channels.msteams"] },
configSchema: buildChannelConfigSchema(MSTeamsConfigSchema),
config: {
...msteamsConfigBase,
...msteamsConfigAdapter,
isConfigured: (_account, cfg) => Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
describeAccount: (account) => ({
accountId: account.accountId,
enabled: account.enabled,
configured: account.configured,
}),
...msteamsConfigAccessors,
},
security: {
collectWarnings: ({ cfg }) => {

View File

@@ -1,7 +1,6 @@
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import {
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
@@ -51,19 +50,8 @@ const meta = {
quickstartAllowFrom: true,
};
const nextcloudTalkConfigAccessors =
createScopedAccountConfigAccessors<ResolvedNextcloudTalkAccount>({
resolveAccount: ({ cfg, accountId }) =>
resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }),
resolveAllowFrom: (account) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
formatAllowFromLowercase({
allowFrom,
stripPrefixRe: /^(nextcloud-talk|nc-talk|nc):/i,
}),
});
const nextcloudTalkConfigBase = createScopedChannelConfigBase<
const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter<
ResolvedNextcloudTalkAccount,
ResolvedNextcloudTalkAccount,
CoreConfig
>({
@@ -72,6 +60,12 @@ const nextcloudTalkConfigBase = createScopedChannelConfigBase<
resolveAccount: (cfg, accountId) => resolveNextcloudTalkAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultNextcloudTalkAccountId,
clearBaseFields: ["botSecret", "botSecretFile", "baseUrl", "name"],
resolveAllowFrom: (account) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
formatAllowFromLowercase({
allowFrom,
stripPrefixRe: /^(nextcloud-talk|nc-talk|nc):/i,
}),
});
const resolveNextcloudTalkDmPolicy = createScopedDmSecurityResolver<ResolvedNextcloudTalkAccount>({
@@ -105,7 +99,7 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
reload: { configPrefixes: ["channels.nextcloud-talk"] },
configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
config: {
...nextcloudTalkConfigBase,
...nextcloudTalkConfigAdapter,
isConfigured: (account) => Boolean(account.secret?.trim() && account.baseUrl?.trim()),
describeAccount: (account) => ({
accountId: account.accountId,
@@ -115,7 +109,6 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
secretSource: account.secretSource,
baseUrl: account.baseUrl ? "[set]" : "[missing]",
}),
...nextcloudTalkConfigAccessors,
},
security: {
resolveDmPolicy: resolveNextcloudTalkDmPolicy,

View File

@@ -1,11 +1,13 @@
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
import {
createScopedDmSecurityResolver,
createTopLevelChannelConfigAdapter,
} from "openclaw/plugin-sdk/channel-config-helpers";
import {
buildChannelConfigSchema,
collectStatusIssuesFromLastError,
createDefaultChannelRuntimeState,
DEFAULT_ACCOUNT_ID,
formatPairingApproveHint,
mapAllowFromEntries,
type ChannelPlugin,
} from "openclaw/plugin-sdk/nostr";
import {
@@ -49,6 +51,39 @@ const resolveNostrDmPolicy = createScopedDmSecurityResolver<ResolvedNostrAccount
},
});
const nostrConfigAdapter = createTopLevelChannelConfigAdapter<ResolvedNostrAccount>({
sectionKey: "nostr",
resolveAccount: (cfg) => resolveNostrAccount({ cfg }),
listAccountIds: listNostrAccountIds,
defaultAccountId: resolveDefaultNostrAccountId,
deleteMode: "clear-fields",
clearBaseFields: [
"name",
"defaultAccount",
"privateKey",
"relays",
"dmPolicy",
"allowFrom",
"profile",
],
resolveAllowFrom: (account) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
allowFrom
.map((entry) => String(entry).trim())
.filter(Boolean)
.map((entry) => {
if (entry === "*") {
return "*";
}
try {
return normalizePubkey(entry);
} catch {
return entry;
}
})
.filter(Boolean),
});
export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
id: "nostr",
meta: {
@@ -70,9 +105,7 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
setupWizard: nostrSetupWizard,
config: {
listAccountIds: (cfg) => listNostrAccountIds(cfg),
resolveAccount: (cfg, accountId) => resolveNostrAccount({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultNostrAccountId(cfg),
...nostrConfigAdapter,
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
accountId: account.accountId,
@@ -81,23 +114,6 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
configured: account.configured,
publicKey: account.publicKey,
}),
resolveAllowFrom: ({ cfg, accountId }) =>
mapAllowFromEntries(resolveNostrAccount({ cfg, accountId }).config.allowFrom),
formatAllowFrom: ({ allowFrom }) =>
allowFrom
.map((entry) => String(entry).trim())
.filter(Boolean)
.map((entry) => {
if (entry === "*") {
return "*";
}
try {
return normalizePubkey(entry);
} catch {
return entry; // Keep as-is if normalization fails
}
})
.filter(Boolean),
},
pairing: {

View File

@@ -31,8 +31,8 @@ import { getSignalRuntime } from "./runtime.js";
import { signalSetupAdapter } from "./setup-core.js";
import {
collectSignalSecurityWarnings,
signalConfigAdapter,
createSignalPluginBase,
signalConfigAccessors,
signalResolveDmPolicy,
signalSetupWizard,
} from "./shared.js";
@@ -290,7 +290,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
applyConfigEdit: buildAccountScopedAllowlistConfigEditor({
channelId: "signal",
normalize: ({ cfg, accountId, values }) =>
signalConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
signalConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
resolvePaths: (scope) => ({
readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]],
writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"],

View File

@@ -1,7 +1,6 @@
import {
collectAllowlistProviderRestrictSendersWarnings,
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
@@ -30,8 +29,12 @@ export const signalSetupWizard = createSignalSetupWizardProxy(
async () => (await loadSignalChannelRuntime()).signalSetupWizard,
);
export const signalConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveSignalAccount({ cfg, accountId }),
export const signalConfigAdapter = createScopedChannelConfigAdapter<ResolvedSignalAccount>({
sectionKey: SIGNAL_CHANNEL,
listAccountIds: listSignalAccountIds,
resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultSignalAccountId,
clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
resolveAllowFrom: (account: ResolvedSignalAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
allowFrom
@@ -42,14 +45,6 @@ export const signalConfigAccessors = createScopedAccountConfigAccessors({
resolveDefaultTo: (account: ResolvedSignalAccount) => account.config.defaultTo,
});
export const signalConfigBase = createScopedChannelConfigBase<ResolvedSignalAccount>({
sectionKey: SIGNAL_CHANNEL,
listAccountIds: listSignalAccountIds,
resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultSignalAccountId,
clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
});
export const signalResolveDmPolicy = createScopedDmSecurityResolver<ResolvedSignalAccount>({
channelKey: SIGNAL_CHANNEL,
resolvePolicy: (account) => account.config.dmPolicy,
@@ -107,7 +102,7 @@ export function createSignalPluginBase(params: {
reload: { configPrefixes: ["channels.signal"] },
configSchema: buildChannelConfigSchema(SignalConfigSchema),
config: {
...signalConfigBase,
...signalConfigAdapter,
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
accountId: account.accountId,
@@ -116,7 +111,6 @@ export function createSignalPluginBase(params: {
configured: account.configured,
baseUrl: account.baseUrl,
}),
...signalConfigAccessors,
},
security: {
resolveDmPolicy: signalResolveDmPolicy,

View File

@@ -44,7 +44,7 @@ import { slackSetupWizard } from "./setup-surface.js";
import {
createSlackPluginBase,
isSlackPluginAccountConfigured,
slackConfigAccessors,
slackConfigAdapter,
SLACK_CHANNEL,
} from "./shared.js";
import { parseSlackTarget } from "./targets.js";
@@ -352,7 +352,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
applyConfigEdit: buildAccountScopedAllowlistConfigEditor({
channelId: "slack",
normalize: ({ cfg, accountId, values }) =>
slackConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
slackConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
resolvePaths: resolveLegacyDmAllowlistConfigPaths,
}),
},

View File

@@ -1,8 +1,5 @@
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import {
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
import {
formatDocsLink,
@@ -145,20 +142,16 @@ export function isSlackSetupAccountConfigured(account: ResolvedSlackAccount): bo
return hasConfiguredBotToken && hasConfiguredAppToken;
}
export const slackConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveSlackAccount({ cfg, accountId }),
resolveAllowFrom: (account: ResolvedSlackAccount) => account.dm?.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account: ResolvedSlackAccount) => account.config.defaultTo,
});
export const slackConfigBase = createScopedChannelConfigBase({
export const slackConfigAdapter = createScopedChannelConfigAdapter<ResolvedSlackAccount>({
sectionKey: SLACK_CHANNEL,
listAccountIds: listSlackAccountIds,
resolveAccount: (cfg, accountId) => resolveSlackAccount({ cfg, accountId }),
inspectAccount: (cfg, accountId) => inspectSlackAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultSlackAccountId,
clearBaseFields: ["botToken", "appToken", "name"],
resolveAllowFrom: (account: ResolvedSlackAccount) => account.dm?.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account: ResolvedSlackAccount) => account.config.defaultTo,
});
export function createSlackPluginBase(params: {
@@ -208,7 +201,7 @@ export function createSlackPluginBase(params: {
reload: { configPrefixes: ["channels.slack"] },
configSchema: buildChannelConfigSchema(SlackConfigSchema),
config: {
...slackConfigBase,
...slackConfigAdapter,
isConfigured: (account) => isSlackPluginAccountConfigured(account),
describeAccount: (account) => ({
accountId: account.accountId,
@@ -218,7 +211,6 @@ export function createSlackPluginBase(params: {
botTokenSource: account.botTokenSource,
appTokenSource: account.appTokenSource,
}),
...slackConfigAccessors,
},
setup: params.setup,
}) as Pick<

View File

@@ -57,6 +57,16 @@ describe("createSynologyChatPlugin", () => {
const plugin = createSynologyChatPlugin();
expect(plugin.config.defaultAccountId?.({})).toBe("default");
});
it("formats allowFrom entries through the shared adapter", () => {
const plugin = createSynologyChatPlugin();
expect(
plugin.config.formatAllowFrom?.({
cfg: {},
allowFrom: [" USER1 ", 42],
}),
).toEqual(["user1", "42"]);
});
});
describe("security", () => {

View File

@@ -5,7 +5,7 @@
*/
import {
createHybridChannelConfigBase,
createHybridChannelConfigAdapter,
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { z } from "zod";
@@ -32,7 +32,7 @@ const resolveSynologyChatDmPolicy = createScopedDmSecurityResolver<ResolvedSynol
normalizeEntry: (raw) => raw.toLowerCase().trim(),
});
const synologyChatConfigBase = createHybridChannelConfigBase<ResolvedSynologyChatAccount>({
const synologyChatConfigAdapter = createHybridChannelConfigAdapter<ResolvedSynologyChatAccount>({
sectionKey: CHANNEL_ID,
listAccountIds: (cfg: any) => listAccountIds(cfg),
resolveAccount: (cfg: any, accountId?: string | null) => resolveAccount(cfg, accountId),
@@ -48,6 +48,9 @@ const synologyChatConfigBase = createHybridChannelConfigBase<ResolvedSynologyCha
"botName",
"allowInsecureSsl",
],
resolveAllowFrom: (account) => account.allowedUserIds,
formatAllowFrom: (allowFrom) =>
allowFrom.map((entry) => String(entry).trim().toLowerCase()).filter(Boolean),
});
function waitUntilAbort(signal?: AbortSignal, onAbort?: () => void): Promise<void> {
@@ -100,7 +103,7 @@ export function createSynologyChatPlugin() {
setupWizard: synologyChatSetupWizard,
config: {
...synologyChatConfigBase,
...synologyChatConfigAdapter,
},
pairing: {

View File

@@ -55,7 +55,7 @@ import {
createTelegramPluginBase,
findTelegramTokenOwnerAccountId,
formatDuplicateTelegramTokenReason,
telegramConfigAccessors,
telegramConfigAdapter,
} from "./shared.js";
import { collectTelegramStatusIssues } from "./status-issues.js";
import { parseTelegramTarget } from "./targets.js";
@@ -325,7 +325,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
applyConfigEdit: buildAccountScopedAllowlistConfigEditor({
channelId: "telegram",
normalize: ({ cfg, accountId, values }) =>
telegramConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
telegramConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
resolvePaths: (scope) => ({
readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]],
writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"],

View File

@@ -1,8 +1,5 @@
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import {
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
import {
buildChannelConfigSchema,
@@ -56,21 +53,17 @@ export function formatDuplicateTelegramTokenReason(params: {
);
}
export const telegramConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveTelegramAccount({ cfg, accountId }),
resolveAllowFrom: (account: ResolvedTelegramAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(telegram|tg):/i }),
resolveDefaultTo: (account: ResolvedTelegramAccount) => account.config.defaultTo,
});
export const telegramConfigBase = createScopedChannelConfigBase<ResolvedTelegramAccount>({
export const telegramConfigAdapter = createScopedChannelConfigAdapter<ResolvedTelegramAccount>({
sectionKey: TELEGRAM_CHANNEL,
listAccountIds: listTelegramAccountIds,
resolveAccount: (cfg, accountId) => resolveTelegramAccount({ cfg, accountId }),
inspectAccount: (cfg, accountId) => inspectTelegramAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultTelegramAccountId,
clearBaseFields: ["botToken", "tokenFile", "name"],
resolveAllowFrom: (account: ResolvedTelegramAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(telegram|tg):/i }),
resolveDefaultTo: (account: ResolvedTelegramAccount) => account.config.defaultTo,
});
export function createTelegramPluginBase(params: {
@@ -99,7 +92,7 @@ export function createTelegramPluginBase(params: {
reload: { configPrefixes: ["channels.telegram"] },
configSchema: buildChannelConfigSchema(TelegramConfigSchema),
config: {
...telegramConfigBase,
...telegramConfigAdapter,
isConfigured: (account, cfg) => {
if (!account.token?.trim()) {
return false;
@@ -131,7 +124,6 @@ export function createTelegramPluginBase(params: {
!findTelegramTokenOwnerAccountId({ cfg, accountId: account.accountId }),
tokenSource: account.tokenSource,
}),
...telegramConfigAccessors,
},
setup: params.setup,
}) as Pick<

View File

@@ -0,0 +1,32 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/tlon";
import { describe, expect, it } from "vitest";
import { tlonPlugin } from "./channel.js";
describe("tlonPlugin config", () => {
it("formats dm allowlist entries through the shared hybrid adapter", () => {
expect(
tlonPlugin.config.formatAllowFrom?.({
cfg: {} as OpenClawConfig,
allowFrom: ["zod", " ~nec "],
}),
).toEqual(["~zod", "~nec"]);
});
it("resolves dm allowlist from the default account", () => {
expect(
tlonPlugin.config.resolveAllowFrom?.({
cfg: {
channels: {
tlon: {
ship: "~sampel-palnet",
url: "https://urbit.example.com",
code: "lidlut-tabwed-pillex-ridrup",
dmAllowlist: ["~zod"],
},
},
} as OpenClawConfig,
accountId: "default",
}),
).toEqual(["~zod"]);
});
});

View File

@@ -1,4 +1,4 @@
import { createHybridChannelConfigBase } from "openclaw/plugin-sdk/channel-config-helpers";
import { createHybridChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import type { ChannelAccountSnapshot, ChannelPlugin } from "openclaw/plugin-sdk/channel-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
@@ -39,7 +39,7 @@ const tlonSetupWizardProxy = createTlonSetupWizardBase({
).tlonSetupWizard.finalize!(params),
}) satisfies NonNullable<ChannelPlugin["setupWizard"]>;
const tlonConfigBase = createHybridChannelConfigBase({
const tlonConfigAdapter = createHybridChannelConfigAdapter({
sectionKey: TLON_CHANNEL_ID,
listAccountIds: (cfg: OpenClawConfig) => listTlonAccountIds(cfg),
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) =>
@@ -47,6 +47,9 @@ const tlonConfigBase = createHybridChannelConfigBase({
defaultAccountId: () => "default",
clearBaseFields: ["ship", "code", "url", "name"],
preserveSectionOnDefaultDelete: true,
resolveAllowFrom: (account) => account.dmAllowlist,
formatAllowFrom: (allowFrom) =>
allowFrom.map((entry) => normalizeShip(String(entry))).filter(Boolean),
});
export const tlonPlugin: ChannelPlugin = {
@@ -72,7 +75,7 @@ export const tlonPlugin: ChannelPlugin = {
reload: { configPrefixes: ["channels.tlon"] },
configSchema: tlonChannelConfigSchema,
config: {
...tlonConfigBase,
...tlonConfigAdapter,
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
accountId: account.accountId,

View File

@@ -1,8 +1,7 @@
import {
collectAllowlistProviderGroupPolicyWarnings,
collectOpenGroupPolicyRouteAllowlistWarnings,
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
@@ -37,17 +36,13 @@ export const whatsappSetupWizardProxy = createWhatsAppSetupWizardProxy(
async () => (await loadWhatsAppChannelRuntime()).whatsappSetupWizard,
);
const whatsappConfigBase = createScopedChannelConfigBase<ResolvedWhatsAppAccount>({
const whatsappConfigAdapter = createScopedChannelConfigAdapter<ResolvedWhatsAppAccount>({
sectionKey: WHATSAPP_CHANNEL,
listAccountIds: listWhatsAppAccountIds,
resolveAccount: (cfg, accountId) => resolveWhatsAppAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultWhatsAppAccountId,
clearBaseFields: [],
allowTopLevel: false,
});
const whatsappConfigAccessors = createScopedAccountConfigAccessors<ResolvedWhatsAppAccount>({
resolveAccount: ({ cfg, accountId }) => resolveWhatsAppAccount({ cfg, accountId }),
resolveAllowFrom: (account) => account.allowFrom,
formatAllowFrom: (allowFrom) => formatWhatsAppConfigAllowFromEntries(allowFrom),
resolveDefaultTo: (account) => account.defaultTo,
@@ -133,7 +128,7 @@ export function createWhatsAppPluginBase(params: {
gatewayMethods: ["web.login.start", "web.login.wait"],
configSchema: buildChannelConfigSchema(WhatsAppConfigSchema),
config: {
...whatsappConfigBase,
...whatsappConfigAdapter,
isEnabled: (account, cfg) => account.enabled && cfg.web?.enabled !== false,
disabledReason: () => "disabled",
isConfigured: params.isConfigured,
@@ -147,7 +142,6 @@ export function createWhatsAppPluginBase(params: {
dmPolicy: account.dmPolicy,
allowFrom: account.allowFrom,
}),
...whatsappConfigAccessors,
},
security: {
resolveDmPolicy: whatsappResolveDmPolicy,

View File

@@ -1,6 +1,5 @@
import {
createScopedAccountConfigAccessors,
createScopedChannelConfigBase,
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
mapAllowFromEntries,
} from "openclaw/plugin-sdk/channel-config-helpers";
@@ -62,19 +61,15 @@ function normalizeZaloMessagingTarget(raw: string): string | undefined {
const loadZaloChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js"));
const zaloConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveZaloAccount({ cfg, accountId }),
resolveAllowFrom: (account: ResolvedZaloAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalo|zl):/i }),
});
const zaloConfigBase = createScopedChannelConfigBase<ResolvedZaloAccount>({
const zaloConfigAdapter = createScopedChannelConfigAdapter<ResolvedZaloAccount>({
sectionKey: "zalo",
listAccountIds: listZaloAccountIds,
resolveAccount: (cfg, accountId) => resolveZaloAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultZaloAccountId,
clearBaseFields: ["botToken", "tokenFile", "name"],
resolveAllowFrom: (account: ResolvedZaloAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalo|zl):/i }),
});
const resolveZaloDmPolicy = createScopedDmSecurityResolver<ResolvedZaloAccount>({
@@ -102,7 +97,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
reload: { configPrefixes: ["channels.zalo"] },
configSchema: buildChannelConfigSchema(ZaloConfigSchema),
config: {
...zaloConfigBase,
...zaloConfigAdapter,
isConfigured: (account) => Boolean(account.token?.trim()),
describeAccount: (account): ChannelAccountSnapshot => ({
accountId: account.accountId,
@@ -111,7 +106,6 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
configured: Boolean(account.token?.trim()),
tokenSource: account.tokenSource,
}),
...zaloConfigAccessors,
},
security: {
resolveDmPolicy: resolveZaloDmPolicy,

View File

@@ -1,11 +1,6 @@
import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import type { ChannelPlugin } from "openclaw/plugin-sdk/zalouser";
import {
buildChannelConfigSchema,
deleteAccountFromConfigSection,
formatAllowFromLowercase,
setAccountEnabledInConfigSection,
} from "openclaw/plugin-sdk/zalouser";
import { buildChannelConfigSchema, formatAllowFromLowercase } from "openclaw/plugin-sdk/zalouser";
import {
listZalouserAccountIds,
resolveDefaultZalouserAccountId,
@@ -27,6 +22,27 @@ export const zalouserMeta = {
quickstartAllowFrom: false,
} satisfies ChannelPlugin<ResolvedZalouserAccount>["meta"];
const zalouserConfigAdapter = createScopedChannelConfigAdapter<ResolvedZalouserAccount>({
sectionKey: "zalouser",
listAccountIds: listZalouserAccountIds,
resolveAccount: (cfg, accountId) => resolveZalouserAccountSync({ cfg, accountId }),
defaultAccountId: resolveDefaultZalouserAccountId,
clearBaseFields: [
"profile",
"name",
"dmPolicy",
"allowFrom",
"historyLimit",
"groupAllowFrom",
"groupPolicy",
"groups",
"messagePrefix",
],
resolveAllowFrom: (account) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }),
});
export function createZalouserPluginBase(params: {
setupWizard: NonNullable<ChannelPlugin<ResolvedZalouserAccount>["setupWizard"]>;
setup: NonNullable<ChannelPlugin<ResolvedZalouserAccount>["setup"]>;
@@ -50,34 +66,7 @@ export function createZalouserPluginBase(params: {
reload: { configPrefixes: ["channels.zalouser"] },
configSchema: buildChannelConfigSchema(ZalouserConfigSchema),
config: {
listAccountIds: (cfg) => listZalouserAccountIds(cfg),
resolveAccount: (cfg, accountId) => resolveZalouserAccountSync({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultZalouserAccountId(cfg),
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
cfg,
sectionKey: "zalouser",
accountId,
enabled,
allowTopLevel: true,
}),
deleteAccount: ({ cfg, accountId }) =>
deleteAccountFromConfigSection({
cfg,
sectionKey: "zalouser",
accountId,
clearBaseFields: [
"profile",
"name",
"dmPolicy",
"allowFrom",
"historyLimit",
"groupAllowFrom",
"groupPolicy",
"groups",
"messagePrefix",
],
}),
...zalouserConfigAdapter,
isConfigured: async (account) => await checkZcaAuthenticated(account.profile),
describeAccount: (account) => ({
accountId: account.accountId,
@@ -85,10 +74,6 @@ export function createZalouserPluginBase(params: {
enabled: account.enabled,
configured: undefined,
}),
resolveAllowFrom: ({ cfg, accountId }) =>
mapAllowFromEntries(resolveZalouserAccountSync({ cfg, accountId }).config.allowFrom),
formatAllowFrom: ({ allowFrom }) =>
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }),
},
setup: params.setup,
};

View File

@@ -1,8 +1,11 @@
import { describe, expect, it } from "vitest";
import {
createScopedAccountConfigAccessors,
createScopedChannelConfigAdapter,
createScopedChannelConfigBase,
createScopedDmSecurityResolver,
createHybridChannelConfigAdapter,
createTopLevelChannelConfigAdapter,
createTopLevelChannelConfigBase,
createHybridChannelConfigBase,
mapAllowFromEntries,
@@ -160,6 +163,41 @@ describe("createScopedChannelConfigBase", () => {
});
});
describe("createScopedChannelConfigAdapter", () => {
it("combines scoped CRUD and allowFrom accessors", () => {
const adapter = createScopedChannelConfigAdapter({
sectionKey: "demo",
listAccountIds: () => ["default", "alt"],
resolveAccount: (_cfg, accountId) => ({
accountId: accountId ?? "default",
allowFrom: accountId ? [accountId] : ["fallback"],
defaultTo: " room:123 ",
}),
defaultAccountId: () => "default",
clearBaseFields: ["token"],
resolveAllowFrom: (account) => account.allowFrom,
formatAllowFrom: (allowFrom) => allowFrom.map((entry) => String(entry).toUpperCase()),
resolveDefaultTo: (account) => account.defaultTo,
});
expect(adapter.listAccountIds({})).toEqual(["default", "alt"]);
expect(adapter.resolveAccount({}, "alt")).toEqual({
accountId: "alt",
allowFrom: ["alt"],
defaultTo: " room:123 ",
});
expect(adapter.resolveAllowFrom?.({ cfg: {}, accountId: "alt" })).toEqual(["alt"]);
expect(adapter.resolveDefaultTo?.({ cfg: {}, accountId: "alt" })).toBe("room:123");
expect(
adapter.setAccountEnabled!({
cfg: {},
accountId: "default",
enabled: true,
}).channels?.demo,
).toEqual({ enabled: true });
});
});
describe("createScopedDmSecurityResolver", () => {
it("builds account-aware DM policy payloads", () => {
const resolveDmPolicy = createScopedDmSecurityResolver<{
@@ -232,6 +270,69 @@ describe("createTopLevelChannelConfigBase", () => {
}).channels,
).toBeUndefined();
});
it("can clear only account-scoped fields while preserving channel settings", () => {
const base = createTopLevelChannelConfigBase({
sectionKey: "demo",
resolveAccount: () => ({ accountId: "default" }),
deleteMode: "clear-fields",
clearBaseFields: ["token", "allowFrom"],
});
expect(
base.deleteAccount!({
cfg: {
channels: {
demo: {
token: "secret",
allowFrom: ["owner"],
markdown: { tables: false },
},
},
},
accountId: "default",
}).channels?.demo,
).toEqual({
markdown: { tables: false },
});
});
});
describe("createTopLevelChannelConfigAdapter", () => {
it("combines top-level CRUD with separate accessor account resolution", () => {
const adapter = createTopLevelChannelConfigAdapter<
{ accountId: string; enabled: boolean },
{ allowFrom: string[]; defaultTo: string }
>({
sectionKey: "demo",
resolveAccount: () => ({ accountId: "default", enabled: true }),
resolveAccessorAccount: () => ({ allowFrom: ["owner"], defaultTo: " chat:123 " }),
deleteMode: "clear-fields",
clearBaseFields: ["token"],
resolveAllowFrom: (account) => account.allowFrom,
formatAllowFrom: (allowFrom) => allowFrom.map((entry) => String(entry)),
resolveDefaultTo: (account) => account.defaultTo,
});
expect(adapter.resolveAccount({})).toEqual({ accountId: "default", enabled: true });
expect(adapter.resolveAllowFrom?.({ cfg: {} })).toEqual(["owner"]);
expect(adapter.resolveDefaultTo?.({ cfg: {} })).toBe("chat:123");
expect(
adapter.deleteAccount!({
cfg: {
channels: {
demo: {
token: "secret",
markdown: { tables: false },
},
},
},
accountId: "default",
}).channels?.demo,
).toEqual({
markdown: { tables: false },
});
});
});
describe("createHybridChannelConfigBase", () => {
@@ -309,3 +410,54 @@ describe("createHybridChannelConfigBase", () => {
});
});
});
describe("createHybridChannelConfigAdapter", () => {
it("combines hybrid CRUD with allowFrom/defaultTo accessors", () => {
const adapter = createHybridChannelConfigAdapter<
{ accountId: string; enabled: boolean },
{ allowFrom: string[]; defaultTo: string }
>({
sectionKey: "demo",
listAccountIds: () => ["default", "alt"],
resolveAccount: (_cfg, accountId) => ({
accountId: accountId ?? "default",
enabled: true,
}),
resolveAccessorAccount: ({ accountId }) => ({
allowFrom: [accountId ?? "default"],
defaultTo: " room:123 ",
}),
defaultAccountId: () => "default",
clearBaseFields: ["token"],
preserveSectionOnDefaultDelete: true,
resolveAllowFrom: (account) => account.allowFrom,
formatAllowFrom: (allowFrom) => allowFrom.map((entry) => String(entry).toUpperCase()),
resolveDefaultTo: (account) => account.defaultTo,
});
expect(adapter.resolveAllowFrom?.({ cfg: {}, accountId: "alt" })).toEqual(["alt"]);
expect(adapter.resolveDefaultTo?.({ cfg: {}, accountId: "alt" })).toBe("room:123");
expect(
adapter.setAccountEnabled!({
cfg: {},
accountId: "default",
enabled: true,
}).channels?.demo,
).toEqual({ enabled: true });
expect(
adapter.deleteAccount!({
cfg: {
channels: {
demo: {
token: "secret",
markdown: { tables: false },
},
},
},
accountId: "default",
}).channels?.demo,
).toEqual({
markdown: { tables: false },
});
});
});

View File

@@ -116,6 +116,59 @@ export function createScopedChannelConfigBase<
};
}
/** Build the full shared config adapter for account-scoped channels with allowlist/default target accessors. */
export function createScopedChannelConfigAdapter<
ResolvedAccount,
AccessorAccount = ResolvedAccount,
Config extends OpenClawConfig = OpenClawConfig,
>(params: {
sectionKey: string;
listAccountIds: (cfg: Config) => string[];
resolveAccount: (cfg: Config, accountId?: string | null) => ResolvedAccount;
resolveAccessorAccount?: (params: { cfg: Config; accountId?: string | null }) => AccessorAccount;
defaultAccountId: (cfg: Config) => string;
inspectAccount?: (cfg: Config, accountId?: string | null) => unknown;
clearBaseFields: string[];
allowTopLevel?: boolean;
resolveAllowFrom: (account: AccessorAccount) => Array<string | number> | null | undefined;
formatAllowFrom: (allowFrom: Array<string | number>) => string[];
resolveDefaultTo?: (account: AccessorAccount) => string | number | null | undefined;
}): Pick<
ChannelConfigAdapter<ResolvedAccount>,
| "listAccountIds"
| "resolveAccount"
| "inspectAccount"
| "defaultAccountId"
| "setAccountEnabled"
| "deleteAccount"
| "resolveAllowFrom"
| "formatAllowFrom"
| "resolveDefaultTo"
> {
const resolveAccessorAccount =
params.resolveAccessorAccount ??
(({ cfg, accountId }: { cfg: Config; accountId?: string | null }) =>
params.resolveAccount(cfg, accountId) as unknown as AccessorAccount);
return {
...createScopedChannelConfigBase<ResolvedAccount, Config>({
sectionKey: params.sectionKey,
listAccountIds: params.listAccountIds,
resolveAccount: params.resolveAccount,
inspectAccount: params.inspectAccount,
defaultAccountId: params.defaultAccountId,
clearBaseFields: params.clearBaseFields,
allowTopLevel: params.allowTopLevel,
}),
...createScopedAccountConfigAccessors<AccessorAccount>({
resolveAccount: resolveAccessorAccount,
resolveAllowFrom: params.resolveAllowFrom,
formatAllowFrom: params.formatAllowFrom,
resolveDefaultTo: params.resolveDefaultTo,
}),
};
}
function setTopLevelChannelEnabledInConfigSection<Config extends OpenClawConfig>(params: {
cfg: Config;
sectionKey: string;
@@ -219,6 +272,59 @@ export function createTopLevelChannelConfigBase<
};
}
/** Build the full shared config adapter for top-level single-account channels with allowlist/default target accessors. */
export function createTopLevelChannelConfigAdapter<
ResolvedAccount,
AccessorAccount = ResolvedAccount,
Config extends OpenClawConfig = OpenClawConfig,
>(params: {
sectionKey: string;
resolveAccount: (cfg: Config) => ResolvedAccount;
resolveAccessorAccount?: (params: { cfg: Config; accountId?: string | null }) => AccessorAccount;
listAccountIds?: (cfg: Config) => string[];
defaultAccountId?: (cfg: Config) => string;
inspectAccount?: (cfg: Config) => unknown;
deleteMode?: "remove-section" | "clear-fields";
clearBaseFields?: string[];
resolveAllowFrom: (account: AccessorAccount) => Array<string | number> | null | undefined;
formatAllowFrom: (allowFrom: Array<string | number>) => string[];
resolveDefaultTo?: (account: AccessorAccount) => string | number | null | undefined;
}): Pick<
ChannelConfigAdapter<ResolvedAccount>,
| "listAccountIds"
| "resolveAccount"
| "inspectAccount"
| "defaultAccountId"
| "setAccountEnabled"
| "deleteAccount"
| "resolveAllowFrom"
| "formatAllowFrom"
| "resolveDefaultTo"
> {
const resolveAccessorAccount =
params.resolveAccessorAccount ??
(({ cfg }: { cfg: Config; accountId?: string | null }) =>
params.resolveAccount(cfg) as unknown as AccessorAccount);
return {
...createTopLevelChannelConfigBase<ResolvedAccount, Config>({
sectionKey: params.sectionKey,
resolveAccount: params.resolveAccount,
listAccountIds: params.listAccountIds,
defaultAccountId: params.defaultAccountId,
inspectAccount: params.inspectAccount,
deleteMode: params.deleteMode,
clearBaseFields: params.clearBaseFields,
}),
...createScopedAccountConfigAccessors<AccessorAccount>({
resolveAccount: resolveAccessorAccount,
resolveAllowFrom: params.resolveAllowFrom,
formatAllowFrom: params.formatAllowFrom,
resolveDefaultTo: params.resolveDefaultTo,
}),
};
}
/** Build CRUD/config helpers for channels where the default account lives at channel root and named accounts live under `accounts`. */
export function createHybridChannelConfigBase<
ResolvedAccount,
@@ -288,6 +394,59 @@ export function createHybridChannelConfigBase<
};
}
/** Build the full shared config adapter for hybrid channels with allowlist/default target accessors. */
export function createHybridChannelConfigAdapter<
ResolvedAccount,
AccessorAccount = ResolvedAccount,
Config extends OpenClawConfig = OpenClawConfig,
>(params: {
sectionKey: string;
listAccountIds: (cfg: Config) => string[];
resolveAccount: (cfg: Config, accountId?: string | null) => ResolvedAccount;
resolveAccessorAccount?: (params: { cfg: Config; accountId?: string | null }) => AccessorAccount;
defaultAccountId: (cfg: Config) => string;
inspectAccount?: (cfg: Config, accountId?: string | null) => unknown;
clearBaseFields: string[];
preserveSectionOnDefaultDelete?: boolean;
resolveAllowFrom: (account: AccessorAccount) => Array<string | number> | null | undefined;
formatAllowFrom: (allowFrom: Array<string | number>) => string[];
resolveDefaultTo?: (account: AccessorAccount) => string | number | null | undefined;
}): Pick<
ChannelConfigAdapter<ResolvedAccount>,
| "listAccountIds"
| "resolveAccount"
| "inspectAccount"
| "defaultAccountId"
| "setAccountEnabled"
| "deleteAccount"
| "resolveAllowFrom"
| "formatAllowFrom"
| "resolveDefaultTo"
> {
const resolveAccessorAccount =
params.resolveAccessorAccount ??
(({ cfg, accountId }: { cfg: Config; accountId?: string | null }) =>
params.resolveAccount(cfg, accountId) as unknown as AccessorAccount);
return {
...createHybridChannelConfigBase<ResolvedAccount, Config>({
sectionKey: params.sectionKey,
listAccountIds: params.listAccountIds,
resolveAccount: params.resolveAccount,
inspectAccount: params.inspectAccount,
defaultAccountId: params.defaultAccountId,
clearBaseFields: params.clearBaseFields,
preserveSectionOnDefaultDelete: params.preserveSectionOnDefaultDelete,
}),
...createScopedAccountConfigAccessors<AccessorAccount>({
resolveAccount: resolveAccessorAccount,
resolveAllowFrom: params.resolveAllowFrom,
formatAllowFrom: params.formatAllowFrom,
resolveDefaultTo: params.resolveDefaultTo,
}),
};
}
/** Convert account-specific DM security fields into the shared runtime policy resolver shape. */
export function createScopedDmSecurityResolver<
ResolvedAccount extends { accountId?: string | null },

View File

@@ -25,10 +25,13 @@ export { createPluginRuntimeStore } from "./runtime-store.js";
export { KeyedAsyncQueue } from "./keyed-async-queue.js";
export {
createHybridChannelConfigAdapter,
createHybridChannelConfigBase,
createScopedAccountConfigAccessors,
createScopedChannelConfigAdapter,
createScopedChannelConfigBase,
createScopedDmSecurityResolver,
createTopLevelChannelConfigAdapter,
createTopLevelChannelConfigBase,
mapAllowFromEntries,
} from "./channel-config-helpers.js";

View File

@@ -62,6 +62,9 @@ describe("plugin-sdk subpath exports", () => {
it("exports compat helpers", () => {
expect(typeof compatSdk.emptyPluginConfigSchema).toBe("function");
expect(typeof compatSdk.resolveControlCommandGate).toBe("function");
expect(typeof compatSdk.createScopedChannelConfigAdapter).toBe("function");
expect(typeof compatSdk.createTopLevelChannelConfigAdapter).toBe("function");
expect(typeof compatSdk.createHybridChannelConfigAdapter).toBe("function");
});
it("keeps core focused on generic shared exports", () => {