refactor: deduplicate plugin setup and channel config helpers
This commit is contained in:
parent
9e556f75f5
commit
9350cb19dd
@ -1,10 +1,11 @@
|
||||
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
|
||||
import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectOpenGroupPolicyRestrictSendersWarnings,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
createScopedAccountConfigAccessors,
|
||||
createScopedChannelConfigBase,
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
|
||||
import { collectOpenGroupPolicyRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy";
|
||||
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import {
|
||||
listBlueBubblesAccountIds,
|
||||
@ -22,11 +23,9 @@ import {
|
||||
buildProbeChannelStatusSummary,
|
||||
collectBlueBubblesStatusIssues,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
resolveBlueBubblesGroupRequireMention,
|
||||
resolveBlueBubblesGroupToolPolicy,
|
||||
setAccountEnabledInConfigSection,
|
||||
} from "./runtime-api.js";
|
||||
import { blueBubblesSetupAdapter } from "./setup-core.js";
|
||||
import { blueBubblesSetupWizard } from "./setup-surface.js";
|
||||
@ -43,6 +42,32 @@ const loadBlueBubblesChannelRuntime = createLazyRuntimeNamedExport(
|
||||
"blueBubblesChannelRuntime",
|
||||
);
|
||||
|
||||
const bluebubblesConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }) => resolveBlueBubblesAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account: ResolvedBlueBubblesAccount) => account.config.allowFrom,
|
||||
formatAllowFrom: (allowFrom) =>
|
||||
formatNormalizedAllowFromEntries({
|
||||
allowFrom,
|
||||
normalizeEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")),
|
||||
}),
|
||||
});
|
||||
|
||||
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,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeBlueBubblesHandle(raw.replace(/^bluebubbles:/i, "")),
|
||||
});
|
||||
|
||||
const meta = {
|
||||
id: "bluebubbles",
|
||||
label: "BlueBubbles",
|
||||
@ -85,24 +110,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
configSchema: buildChannelConfigSchema(BlueBubblesConfigSchema),
|
||||
setupWizard: blueBubblesSetupWizard,
|
||||
config: {
|
||||
listAccountIds: (cfg) => listBlueBubblesAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveBlueBubblesAccount({ cfg: cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultBlueBubblesAccountId(cfg),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg: cfg,
|
||||
sectionKey: "bluebubbles",
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg: cfg,
|
||||
sectionKey: "bluebubbles",
|
||||
accountId,
|
||||
clearBaseFields: ["serverUrl", "password", "name", "webhookPath"],
|
||||
}),
|
||||
...bluebubblesConfigBase,
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account): ChannelAccountSnapshot => ({
|
||||
accountId: account.accountId,
|
||||
@ -111,28 +119,11 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
configured: account.configured,
|
||||
baseUrl: account.baseUrl,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||
mapAllowFromEntries(resolveBlueBubblesAccount({ cfg: cfg, accountId }).config.allowFrom),
|
||||
formatAllowFrom: ({ allowFrom }) =>
|
||||
formatNormalizedAllowFromEntries({
|
||||
allowFrom,
|
||||
normalizeEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")),
|
||||
}),
|
||||
...bluebubblesConfigAccessors,
|
||||
},
|
||||
actions: bluebubblesMessageActions,
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
return buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "bluebubbles",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeBlueBubblesHandle(raw.replace(/^bluebubbles:/i, "")),
|
||||
});
|
||||
},
|
||||
resolveDmPolicy: resolveBlueBubblesDmPolicy,
|
||||
collectWarnings: ({ account }) => {
|
||||
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
||||
return collectOpenGroupPolicyRestrictSendersWarnings({
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import {
|
||||
createTopLevelChannelDmPolicySetter,
|
||||
normalizeAccountId,
|
||||
patchScopedAccountConfig,
|
||||
prepareScopedSetupConfig,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
type ChannelSetupAdapter,
|
||||
type DmPolicy,
|
||||
type OpenClawConfig,
|
||||
@ -10,13 +10,12 @@ import {
|
||||
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
|
||||
|
||||
const channel = "bluebubbles" as const;
|
||||
const setBlueBubblesTopLevelDmPolicy = createTopLevelChannelDmPolicySetter({
|
||||
channel,
|
||||
});
|
||||
|
||||
export function setBlueBubblesDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy): OpenClawConfig {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy,
|
||||
});
|
||||
return setBlueBubblesTopLevelDmPolicy(cfg, dmPolicy);
|
||||
}
|
||||
|
||||
export function setBlueBubblesAllowFrom(
|
||||
|
||||
@ -3,8 +3,8 @@ import {
|
||||
buildAccountScopedAllowlistConfigEditor,
|
||||
resolveLegacyDmAllowlistConfigPaths,
|
||||
} from "openclaw/plugin-sdk/allowlist-config-edit";
|
||||
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectOpenGroupPolicyConfiguredRouteWarnings,
|
||||
collectOpenProviderGroupPolicyWarnings,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
@ -61,6 +61,14 @@ type DiscordSendFn = ReturnType<
|
||||
const meta = getChatChannelMeta("discord");
|
||||
const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
|
||||
|
||||
const resolveDiscordDmPolicy = createScopedDmSecurityResolver<ResolvedDiscordAccount>({
|
||||
channelKey: "discord",
|
||||
resolvePolicy: (account) => account.config.dm?.policy,
|
||||
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
|
||||
allowFromPathSuffix: "dm.",
|
||||
normalizeEntry: (raw) => raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"),
|
||||
});
|
||||
|
||||
function formatDiscordIntents(intents?: {
|
||||
messageContent?: string;
|
||||
guildMembers?: string;
|
||||
@ -300,18 +308,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
return buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "discord",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dm?.policy,
|
||||
allowFrom: account.config.dm?.allowFrom ?? [],
|
||||
allowFromPathSuffix: "dm.",
|
||||
normalizeEntry: (raw) => raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"),
|
||||
});
|
||||
},
|
||||
resolveDmPolicy: resolveDiscordDmPolicy,
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
const guildEntries = account.config.guilds ?? {};
|
||||
const guildsConfigured = Object.keys(guildEntries).length > 0;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import type { DiscordGuildEntry } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
createAccountScopedAllowFromSection,
|
||||
createAccountScopedGroupAccessSection,
|
||||
createLegacyCompatChannelDmPolicy,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
createEnvPatchedAccountSetupAdapter,
|
||||
noteChannelLookupFailure,
|
||||
noteChannelLookupSummary,
|
||||
parseMentionOrPrefixedId,
|
||||
patchChannelConfigForAccount,
|
||||
setLegacyChannelDmPolicyWithAllowFrom,
|
||||
setSetupChannelEnabled,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
@ -88,21 +88,11 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
NonNullable<NonNullable<ChannelSetupWizard["groupAccess"]>["resolveAllowlist"]>
|
||||
>;
|
||||
}) {
|
||||
const discordDmPolicy: ChannelSetupDmPolicy = {
|
||||
const discordDmPolicy: ChannelSetupDmPolicy = createLegacyCompatChannelDmPolicy({
|
||||
label: "Discord",
|
||||
channel,
|
||||
policyKey: "channels.discord.dmPolicy",
|
||||
allowFromKey: "channels.discord.allowFrom",
|
||||
getCurrent: (cfg: OpenClawConfig) =>
|
||||
cfg.channels?.discord?.dmPolicy ?? cfg.channels?.discord?.dm?.policy ?? "pairing",
|
||||
setPolicy: (cfg: OpenClawConfig, policy) =>
|
||||
setLegacyChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy: policy,
|
||||
}),
|
||||
promptAllowFrom: handlers.promptAllowFrom,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
channel,
|
||||
@ -145,7 +135,8 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
},
|
||||
},
|
||||
],
|
||||
groupAccess: {
|
||||
groupAccess: createAccountScopedGroupAccessSection({
|
||||
channel,
|
||||
label: "Discord channels",
|
||||
placeholder: "My Server/#general, guildId/channelId, #support",
|
||||
currentPolicy: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) =>
|
||||
@ -164,57 +155,8 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
),
|
||||
updatePrompt: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) =>
|
||||
Boolean(resolveDiscordAccount({ cfg, accountId }).config.guilds),
|
||||
setPolicy: ({
|
||||
cfg,
|
||||
accountId,
|
||||
policy,
|
||||
}: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
policy: "open" | "allowlist" | "disabled";
|
||||
}) =>
|
||||
patchChannelConfigForAccount({
|
||||
cfg,
|
||||
channel,
|
||||
accountId,
|
||||
patch: { groupPolicy: policy },
|
||||
}),
|
||||
resolveAllowlist: async ({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
}: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
credentialValues: { token?: string };
|
||||
entries: string[];
|
||||
prompter: { note: (message: string, title?: string) => Promise<void> };
|
||||
}) => {
|
||||
try {
|
||||
return await handlers.resolveGroupAllowlist({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
});
|
||||
} catch (error) {
|
||||
await noteChannelLookupFailure({
|
||||
prompter,
|
||||
label: "Discord channels",
|
||||
error,
|
||||
});
|
||||
await noteChannelLookupSummary({
|
||||
prompter,
|
||||
label: "Discord channels",
|
||||
resolvedSections: [],
|
||||
unresolved: entries,
|
||||
});
|
||||
return entries.map((input) => ({ input, resolved: false }));
|
||||
}
|
||||
},
|
||||
resolveAllowlist: handlers.resolveGroupAllowlist,
|
||||
fallbackResolved: (entries) => entries.map((input) => ({ input, resolved: false })),
|
||||
applyAllowlist: ({
|
||||
cfg,
|
||||
accountId,
|
||||
@ -224,8 +166,9 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
accountId: string;
|
||||
resolved: unknown;
|
||||
}) => setDiscordGuildChannelAllowlist(cfg, accountId, resolved as never),
|
||||
},
|
||||
allowFrom: {
|
||||
}),
|
||||
allowFrom: createAccountScopedAllowFromSection({
|
||||
channel,
|
||||
credentialInputKey: "token",
|
||||
helpTitle: "Discord allowlist",
|
||||
helpLines: [
|
||||
@ -242,33 +185,8 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
invalidWithoutCredentialNote:
|
||||
"Bot token missing; use numeric user ids (or mention form) only.",
|
||||
parseId: parseDiscordAllowFromId,
|
||||
resolveEntries: async ({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
}: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
credentialValues: { token?: string };
|
||||
entries: string[];
|
||||
}) => await handlers.resolveAllowFromEntries({ cfg, accountId, credentialValues, entries }),
|
||||
apply: async ({
|
||||
cfg,
|
||||
accountId,
|
||||
allowFrom,
|
||||
}: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
allowFrom: string[];
|
||||
}) =>
|
||||
patchChannelConfigForAccount({
|
||||
cfg,
|
||||
channel,
|
||||
accountId,
|
||||
patch: { dmPolicy: "allowlist", allowFrom },
|
||||
}),
|
||||
},
|
||||
resolveEntries: handlers.resolveAllowFromEntries,
|
||||
}),
|
||||
dmPolicy: discordDmPolicy,
|
||||
disable: (cfg: OpenClawConfig) => setSetupChannelEnabled(cfg, channel, false),
|
||||
} satisfies ChannelSetupWizard;
|
||||
|
||||
@ -1,17 +1,13 @@
|
||||
import {
|
||||
resolveEntriesWithOptionalToken,
|
||||
type OpenClawConfig,
|
||||
promptLegacyChannelAllowFrom,
|
||||
resolveSetupAccountId,
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { resolveDefaultDiscordAccountId, resolveDiscordAccount } from "./accounts.js";
|
||||
import { normalizeDiscordSlug } from "./monitor/allow-list.js";
|
||||
import {
|
||||
resolveDiscordChannelAllowlist,
|
||||
type DiscordChannelResolution,
|
||||
} from "./resolve-channels.js";
|
||||
import { resolveDiscordChannelAllowlist } from "./resolve-channels.js";
|
||||
import { resolveDiscordUserAllowlist } from "./resolve-users.js";
|
||||
import {
|
||||
createDiscordSetupWizardBase,
|
||||
@ -23,22 +19,26 @@ import {
|
||||
const channel = "discord" as const;
|
||||
|
||||
async function resolveDiscordAllowFromEntries(params: { token?: string; entries: string[] }) {
|
||||
if (!params.token?.trim()) {
|
||||
return params.entries.map((input) => ({
|
||||
return await resolveEntriesWithOptionalToken({
|
||||
token: params.token,
|
||||
entries: params.entries,
|
||||
buildWithoutToken: (input) => ({
|
||||
input,
|
||||
resolved: false,
|
||||
id: null,
|
||||
}));
|
||||
}
|
||||
const resolved = await resolveDiscordUserAllowlist({
|
||||
token: params.token,
|
||||
entries: params.entries,
|
||||
}),
|
||||
resolveEntries: async ({ token, entries }) =>
|
||||
(
|
||||
await resolveDiscordUserAllowlist({
|
||||
token,
|
||||
entries,
|
||||
})
|
||||
).map((entry) => ({
|
||||
input: entry.input,
|
||||
resolved: entry.resolved,
|
||||
id: entry.id ?? null,
|
||||
})),
|
||||
});
|
||||
return resolved.map((entry) => ({
|
||||
input: entry.input,
|
||||
resolved: entry.resolved,
|
||||
id: entry.id ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
async function promptDiscordAllowFrom(params: {
|
||||
@ -46,17 +46,15 @@ async function promptDiscordAllowFrom(params: {
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const accountId = resolveSetupAccountId({
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultDiscordAccountId(params.cfg),
|
||||
});
|
||||
const resolved = resolveDiscordAccount({ cfg: params.cfg, accountId });
|
||||
return promptLegacyChannelAllowFrom({
|
||||
return await promptLegacyChannelAllowFromForAccount({
|
||||
cfg: params.cfg,
|
||||
channel,
|
||||
prompter: params.prompter,
|
||||
existing: resolved.config.allowFrom ?? resolved.config.dm?.allowFrom ?? [],
|
||||
token: resolved.token,
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultDiscordAccountId(params.cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
|
||||
resolveExisting: (account) => account.config.allowFrom ?? account.config.dm?.allowFrom ?? [],
|
||||
resolveToken: (account) => account.token,
|
||||
noteTitle: "Discord allowlist",
|
||||
noteLines: [
|
||||
"Allowlist Discord DMs by username (we resolve to user ids).",
|
||||
@ -71,11 +69,17 @@ async function promptDiscordAllowFrom(params: {
|
||||
placeholder: "@alice, 123456789012345678",
|
||||
parseId: parseDiscordAllowFromId,
|
||||
invalidWithoutTokenNote: "Bot token missing; use numeric user ids (or mention form) only.",
|
||||
resolveEntries: ({ token, entries }) =>
|
||||
resolveDiscordUserAllowlist({
|
||||
token,
|
||||
entries,
|
||||
}),
|
||||
resolveEntries: async ({ token, entries }) =>
|
||||
(
|
||||
await resolveDiscordUserAllowlist({
|
||||
token,
|
||||
entries,
|
||||
})
|
||||
).map((entry) => ({
|
||||
input: entry.input,
|
||||
resolved: entry.resolved,
|
||||
id: entry.id ?? null,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
@ -85,18 +89,20 @@ async function resolveDiscordGroupAllowlist(params: {
|
||||
credentialValues: { token?: string };
|
||||
entries: string[];
|
||||
}) {
|
||||
const token =
|
||||
resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId }).token ||
|
||||
(typeof params.credentialValues.token === "string" ? params.credentialValues.token : "");
|
||||
if (!token || params.entries.length === 0) {
|
||||
return params.entries.map((input) => ({
|
||||
return await resolveEntriesWithOptionalToken({
|
||||
token:
|
||||
resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId }).token ||
|
||||
(typeof params.credentialValues.token === "string" ? params.credentialValues.token : ""),
|
||||
entries: params.entries,
|
||||
buildWithoutToken: (input) => ({
|
||||
input,
|
||||
resolved: false,
|
||||
}));
|
||||
}
|
||||
return await resolveDiscordChannelAllowlist({
|
||||
token,
|
||||
entries: params.entries,
|
||||
}),
|
||||
resolveEntries: async ({ token, entries }) =>
|
||||
await resolveDiscordChannelAllowlist({
|
||||
token,
|
||||
entries,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
|
||||
import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import {
|
||||
createHybridChannelConfigBase,
|
||||
createScopedAccountConfigAccessors,
|
||||
} 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 {
|
||||
@ -126,6 +129,21 @@ function setFeishuNamedAccountEnabled(
|
||||
};
|
||||
}
|
||||
|
||||
const feishuConfigBase = createHybridChannelConfigBase<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 }),
|
||||
});
|
||||
|
||||
function isFeishuReactionsActionEnabled(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
account: ResolvedFeishuAccount;
|
||||
@ -377,15 +395,10 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
||||
reload: { configPrefixes: ["channels.feishu"] },
|
||||
configSchema: buildChannelConfigSchema(FeishuConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listFeishuAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
|
||||
...feishuConfigBase,
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
||||
|
||||
if (isDefault) {
|
||||
// For default account, set top-level enabled
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
@ -397,8 +410,6 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// For named accounts, set enabled in accounts[accountId]
|
||||
return setFeishuNamedAccountEnabled(cfg, accountId, enabled);
|
||||
},
|
||||
deleteAccount: ({ cfg, accountId }) => {
|
||||
@ -442,11 +453,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
||||
appId: account.appId,
|
||||
domain: account.domain,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg, accountId }) => {
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
return mapAllowFromEntries(account.config?.allowFrom);
|
||||
},
|
||||
formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({ allowFrom }),
|
||||
...feishuConfigAccessors,
|
||||
},
|
||||
actions: {
|
||||
describeMessageTool: describeFeishuMessageTool,
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import {
|
||||
buildSingleChannelSecretPromptState,
|
||||
createTopLevelChannelAllowFromSetter,
|
||||
createTopLevelChannelDmPolicy,
|
||||
createTopLevelChannelGroupPolicySetter,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
hasConfiguredSecretInput,
|
||||
mergeAllowFromEntries,
|
||||
patchTopLevelChannelConfigSection,
|
||||
promptSingleChannelSecretInput,
|
||||
setTopLevelChannelAllowFrom,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
setTopLevelChannelGroupPolicy,
|
||||
splitSetupEntries,
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type DmPolicy,
|
||||
type OpenClawConfig,
|
||||
type SecretInput,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
@ -21,6 +21,13 @@ import { feishuSetupAdapter } from "./setup-core.js";
|
||||
import type { FeishuConfig } from "./types.js";
|
||||
|
||||
const channel = "feishu" as const;
|
||||
const setFeishuAllowFrom = createTopLevelChannelAllowFromSetter({
|
||||
channel,
|
||||
});
|
||||
const setFeishuGroupPolicy = createTopLevelChannelGroupPolicySetter({
|
||||
channel,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
function normalizeString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
@ -30,34 +37,6 @@ function normalizeString(value: unknown): string | undefined {
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function setFeishuDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy): OpenClawConfig {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy,
|
||||
}) as OpenClawConfig;
|
||||
}
|
||||
|
||||
function setFeishuAllowFrom(cfg: OpenClawConfig, allowFrom: string[]): OpenClawConfig {
|
||||
return setTopLevelChannelAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
allowFrom,
|
||||
}) as OpenClawConfig;
|
||||
}
|
||||
|
||||
function setFeishuGroupPolicy(
|
||||
cfg: OpenClawConfig,
|
||||
groupPolicy: "open" | "allowlist" | "disabled",
|
||||
): OpenClawConfig {
|
||||
return setTopLevelChannelGroupPolicy({
|
||||
cfg,
|
||||
channel,
|
||||
groupPolicy,
|
||||
enabled: true,
|
||||
}) as OpenClawConfig;
|
||||
}
|
||||
|
||||
function setFeishuGroupAllowFrom(cfg: OpenClawConfig, groupAllowFrom: string[]): OpenClawConfig {
|
||||
return {
|
||||
...cfg,
|
||||
@ -177,15 +156,14 @@ async function promptFeishuAppId(params: {
|
||||
).trim();
|
||||
}
|
||||
|
||||
const feishuDmPolicy: ChannelSetupDmPolicy = {
|
||||
const feishuDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
|
||||
label: "Feishu",
|
||||
channel,
|
||||
policyKey: "channels.feishu.dmPolicy",
|
||||
allowFromKey: "channels.feishu.allowFrom",
|
||||
getCurrent: (cfg) => (cfg.channels?.feishu as FeishuConfig | undefined)?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setFeishuDmPolicy(cfg as OpenClawConfig, policy),
|
||||
promptAllowFrom: promptFeishuAllowFrom,
|
||||
};
|
||||
});
|
||||
|
||||
export { feishuSetupAdapter } from "./setup-core.js";
|
||||
|
||||
@ -263,13 +241,12 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
||||
});
|
||||
|
||||
if (appSecretResult.action === "use-env") {
|
||||
next = {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
feishu: { ...next.channels?.feishu, enabled: true },
|
||||
},
|
||||
};
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
enabled: true,
|
||||
patch: {},
|
||||
}) as OpenClawConfig;
|
||||
} else if (appSecretResult.action === "set") {
|
||||
appSecret = appSecretResult.value;
|
||||
appSecretProbeValue = appSecretResult.resolvedValue;
|
||||
@ -281,18 +258,15 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
||||
}
|
||||
|
||||
if (appId && appSecret) {
|
||||
next = {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
feishu: {
|
||||
...next.channels?.feishu,
|
||||
enabled: true,
|
||||
appId,
|
||||
appSecret,
|
||||
},
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
enabled: true,
|
||||
patch: {
|
||||
appId,
|
||||
appSecret,
|
||||
},
|
||||
};
|
||||
}) as OpenClawConfig;
|
||||
|
||||
try {
|
||||
const probe = await probeFeishu({
|
||||
@ -326,16 +300,11 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
||||
],
|
||||
initialValue: currentMode,
|
||||
})) as "websocket" | "webhook";
|
||||
next = {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
feishu: {
|
||||
...next.channels?.feishu,
|
||||
connectionMode,
|
||||
},
|
||||
},
|
||||
};
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
patch: { connectionMode },
|
||||
}) as OpenClawConfig;
|
||||
|
||||
if (connectionMode === "webhook") {
|
||||
const currentVerificationToken = (next.channels?.feishu as FeishuConfig | undefined)
|
||||
@ -357,16 +326,11 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
||||
preferredEnvVar: "FEISHU_VERIFICATION_TOKEN",
|
||||
});
|
||||
if (verificationTokenResult.action === "set") {
|
||||
next = {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
feishu: {
|
||||
...next.channels?.feishu,
|
||||
verificationToken: verificationTokenResult.value,
|
||||
},
|
||||
},
|
||||
};
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
patch: { verificationToken: verificationTokenResult.value },
|
||||
}) as OpenClawConfig;
|
||||
}
|
||||
|
||||
const currentEncryptKey = (next.channels?.feishu as FeishuConfig | undefined)?.encryptKey;
|
||||
@ -387,16 +351,11 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
||||
preferredEnvVar: "FEISHU_ENCRYPT_KEY",
|
||||
});
|
||||
if (encryptKeyResult.action === "set") {
|
||||
next = {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
feishu: {
|
||||
...next.channels?.feishu,
|
||||
encryptKey: encryptKeyResult.value,
|
||||
},
|
||||
},
|
||||
};
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
patch: { encryptKey: encryptKeyResult.value },
|
||||
}) as OpenClawConfig;
|
||||
}
|
||||
|
||||
const currentWebhookPath = (next.channels?.feishu as FeishuConfig | undefined)?.webhookPath;
|
||||
@ -407,16 +366,11 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
next = {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
feishu: {
|
||||
...next.channels?.feishu,
|
||||
webhookPath,
|
||||
},
|
||||
},
|
||||
};
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
patch: { webhookPath },
|
||||
}) as OpenClawConfig;
|
||||
}
|
||||
|
||||
const currentDomain = (next.channels?.feishu as FeishuConfig | undefined)?.domain ?? "feishu";
|
||||
@ -428,16 +382,11 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
||||
],
|
||||
initialValue: currentDomain,
|
||||
});
|
||||
next = {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
feishu: {
|
||||
...next.channels?.feishu,
|
||||
domain: domain as "feishu" | "lark",
|
||||
},
|
||||
},
|
||||
};
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
patch: { domain: domain as "feishu" | "lark" },
|
||||
}) as OpenClawConfig;
|
||||
|
||||
const groupPolicy = (await prompter.select({
|
||||
message: "Group chat policy",
|
||||
@ -468,11 +417,10 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
||||
return { cfg: next };
|
||||
},
|
||||
dmPolicy: feishuDmPolicy,
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
feishu: { ...cfg.channels?.feishu, enabled: false },
|
||||
},
|
||||
}),
|
||||
disable: (cfg) =>
|
||||
patchTopLevelChannelConfigSection({
|
||||
cfg,
|
||||
channel,
|
||||
patch: { enabled: false },
|
||||
}),
|
||||
};
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import {
|
||||
addWildcardAllowFrom,
|
||||
applySetupAccountConfigPatch,
|
||||
createNestedChannelDmPolicy,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
mergeAllowFromEntries,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
patchNestedChannelConfigSection,
|
||||
splitSetupEntries,
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type DmPolicy,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
@ -25,25 +24,6 @@ const ENV_SERVICE_ACCOUNT_FILE = "GOOGLE_CHAT_SERVICE_ACCOUNT_FILE";
|
||||
const USE_ENV_FLAG = "__googlechatUseEnv";
|
||||
const AUTH_METHOD_FLAG = "__googlechatAuthMethod";
|
||||
|
||||
function setGoogleChatDmPolicy(cfg: OpenClawConfig, policy: DmPolicy) {
|
||||
const allowFrom =
|
||||
policy === "open" ? addWildcardAllowFrom(cfg.channels?.googlechat?.dm?.allowFrom) : undefined;
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
googlechat: {
|
||||
...cfg.channels?.googlechat,
|
||||
dm: {
|
||||
...cfg.channels?.googlechat?.dm,
|
||||
policy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function promptAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: Parameters<NonNullable<ChannelSetupDmPolicy["promptAllowFrom"]>>[0]["prompter"];
|
||||
@ -57,32 +37,28 @@ async function promptAllowFrom(params: {
|
||||
});
|
||||
const parts = splitSetupEntries(String(entry));
|
||||
const unique = mergeAllowFromEntries(undefined, parts);
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
googlechat: {
|
||||
...params.cfg.channels?.googlechat,
|
||||
enabled: true,
|
||||
dm: {
|
||||
...params.cfg.channels?.googlechat?.dm,
|
||||
policy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
},
|
||||
return patchNestedChannelConfigSection({
|
||||
cfg: params.cfg,
|
||||
channel,
|
||||
section: "dm",
|
||||
enabled: true,
|
||||
patch: {
|
||||
policy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const googlechatDmPolicy: ChannelSetupDmPolicy = {
|
||||
const googlechatDmPolicy: ChannelSetupDmPolicy = createNestedChannelDmPolicy({
|
||||
label: "Google Chat",
|
||||
channel,
|
||||
section: "dm",
|
||||
policyKey: "channels.googlechat.dm.policy",
|
||||
allowFromKey: "channels.googlechat.dm.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.googlechat?.dm?.policy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setGoogleChatDmPolicy(cfg, policy),
|
||||
promptAllowFrom,
|
||||
};
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
export { googlechatSetupAdapter } from "./setup-core.js";
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { buildAccountScopedAllowlistConfigEditor } from "openclaw/plugin-sdk/allowlist-config-edit";
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
createScopedDmSecurityResolver,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-runtime";
|
||||
@ -26,6 +26,13 @@ import { normalizeIMessageHandle, parseIMessageTarget } from "./targets.js";
|
||||
|
||||
const loadIMessageChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js"));
|
||||
|
||||
const resolveIMessageDmPolicy = createScopedDmSecurityResolver<ResolvedIMessageAccount>({
|
||||
channelKey: "imessage",
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
});
|
||||
|
||||
function buildIMessageBaseSessionKey(params: {
|
||||
cfg: Parameters<typeof resolveIMessageAccount>[0]["cfg"];
|
||||
agentId: string;
|
||||
@ -127,17 +134,7 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
return buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "imessage",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
});
|
||||
},
|
||||
resolveDmPolicy: resolveIMessageDmPolicy,
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
return collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg,
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
|
||||
import { createScopedAccountConfigAccessors } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
createScopedAccountConfigAccessors,
|
||||
createScopedChannelConfigBase,
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import {
|
||||
buildOpenGroupPolicyWarning,
|
||||
collectAllowlistProviderGroupPolicyWarnings,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
@ -11,10 +14,8 @@ import {
|
||||
buildChannelConfigSchema,
|
||||
createAccountStatusSink,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
getChatChannelMeta,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/irc";
|
||||
import { runStoppablePassiveMonitor } from "../../shared/passive-monitor.js";
|
||||
@ -61,6 +62,33 @@ const ircConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveDefaultTo: (account: ResolvedIrcAccount) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
const ircConfigBase = createScopedChannelConfigBase<ResolvedIrcAccount, CoreConfig>({
|
||||
sectionKey: "irc",
|
||||
listAccountIds: listIrcAccountIds,
|
||||
resolveAccount: (cfg, accountId) => resolveIrcAccount({ cfg, accountId }),
|
||||
defaultAccountId: resolveDefaultIrcAccountId,
|
||||
clearBaseFields: [
|
||||
"name",
|
||||
"host",
|
||||
"port",
|
||||
"tls",
|
||||
"nick",
|
||||
"username",
|
||||
"realname",
|
||||
"password",
|
||||
"passwordFile",
|
||||
"channels",
|
||||
],
|
||||
});
|
||||
|
||||
const resolveIrcDmPolicy = createScopedDmSecurityResolver<ResolvedIrcAccount>({
|
||||
channelKey: "irc",
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeIrcAllowEntry(raw),
|
||||
});
|
||||
|
||||
export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = {
|
||||
id: "irc",
|
||||
meta: {
|
||||
@ -88,35 +116,7 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = {
|
||||
reload: { configPrefixes: ["channels.irc"] },
|
||||
configSchema: buildChannelConfigSchema(IrcConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listIrcAccountIds(cfg as CoreConfig),
|
||||
resolveAccount: (cfg, accountId) => resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultIrcAccountId(cfg as CoreConfig),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg: cfg as CoreConfig,
|
||||
sectionKey: "irc",
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg: cfg as CoreConfig,
|
||||
sectionKey: "irc",
|
||||
accountId,
|
||||
clearBaseFields: [
|
||||
"name",
|
||||
"host",
|
||||
"port",
|
||||
"tls",
|
||||
"nick",
|
||||
"username",
|
||||
"realname",
|
||||
"password",
|
||||
"passwordFile",
|
||||
"channels",
|
||||
],
|
||||
}),
|
||||
...ircConfigBase,
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
@ -132,18 +132,7 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = {
|
||||
...ircConfigAccessors,
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
return buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "irc",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeIrcAllowEntry(raw),
|
||||
});
|
||||
},
|
||||
resolveDmPolicy: resolveIrcDmPolicy,
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
const warnings = collectAllowlistProviderGroupPolicyWarnings({
|
||||
cfg,
|
||||
|
||||
@ -4,15 +4,19 @@ import type { DmPolicy } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
createTopLevelChannelAllowFromSetter,
|
||||
createTopLevelChannelDmPolicySetter,
|
||||
patchScopedAccountConfig,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
setTopLevelChannelAllowFrom,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js";
|
||||
|
||||
const channel = "irc" as const;
|
||||
const setIrcTopLevelDmPolicy = createTopLevelChannelDmPolicySetter({
|
||||
channel,
|
||||
});
|
||||
const setIrcTopLevelAllowFrom = createTopLevelChannelAllowFromSetter({
|
||||
channel,
|
||||
});
|
||||
|
||||
type IrcSetupInput = ChannelSetupInput & {
|
||||
host?: string;
|
||||
@ -53,19 +57,11 @@ export function updateIrcAccountConfig(
|
||||
}
|
||||
|
||||
export function setIrcDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy,
|
||||
}) as CoreConfig;
|
||||
return setIrcTopLevelDmPolicy(cfg, dmPolicy) as CoreConfig;
|
||||
}
|
||||
|
||||
export function setIrcAllowFrom(cfg: CoreConfig, allowFrom: string[]): CoreConfig {
|
||||
return setTopLevelChannelAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
allowFrom,
|
||||
}) as CoreConfig;
|
||||
return setIrcTopLevelAllowFrom(cfg, allowFrom) as CoreConfig;
|
||||
}
|
||||
|
||||
export function setIrcNickServ(
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import {
|
||||
createTopLevelChannelDmPolicy,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
resolveLineAccount,
|
||||
setSetupChannelEnabled,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
splitSetupEntries,
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
@ -35,19 +35,13 @@ const LINE_ALLOW_FROM_HELP_LINES = [
|
||||
`Docs: ${formatDocsLink("/channels/line", "channels/line")}`,
|
||||
];
|
||||
|
||||
const lineDmPolicy: ChannelSetupDmPolicy = {
|
||||
const lineDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
|
||||
label: "LINE",
|
||||
channel,
|
||||
policyKey: "channels.line.dmPolicy",
|
||||
allowFromKey: "channels.line.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.line?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) =>
|
||||
setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy: policy,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
export { lineSetupAdapter } from "./setup-core.js";
|
||||
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import {
|
||||
addWildcardAllowFrom,
|
||||
buildSingleChannelSecretPromptState,
|
||||
createNestedChannelDmPolicy,
|
||||
createTopLevelChannelGroupPolicySetter,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
formatResolvedUnresolvedNote,
|
||||
hasConfiguredSecretInput,
|
||||
mergeAllowFromEntries,
|
||||
patchNestedChannelConfigSection,
|
||||
promptSingleChannelSecretInput,
|
||||
setTopLevelChannelGroupPolicy,
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type DmPolicy,
|
||||
type OpenClawConfig,
|
||||
type SecretInput,
|
||||
type WizardPrompter,
|
||||
@ -23,25 +23,10 @@ import { buildMatrixConfigUpdate, matrixSetupAdapter } from "./setup-core.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
const channel = "matrix" as const;
|
||||
|
||||
function setMatrixDmPolicy(cfg: CoreConfig, policy: DmPolicy) {
|
||||
const allowFrom =
|
||||
policy === "open" ? addWildcardAllowFrom(cfg.channels?.matrix?.dm?.allowFrom) : undefined;
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
matrix: {
|
||||
...cfg.channels?.matrix,
|
||||
dm: {
|
||||
...cfg.channels?.matrix?.dm,
|
||||
policy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const setMatrixGroupPolicy = createTopLevelChannelGroupPolicySetter({
|
||||
channel,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
async function noteMatrixAuthHelp(prompter: WizardPrompter): Promise<void> {
|
||||
await prompter.note(
|
||||
@ -128,33 +113,19 @@ async function promptMatrixAllowFrom(params: {
|
||||
}
|
||||
|
||||
const unique = mergeAllowFromEntries(existingAllowFrom, resolvedIds);
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
matrix: {
|
||||
...cfg.channels?.matrix,
|
||||
enabled: true,
|
||||
dm: {
|
||||
...cfg.channels?.matrix?.dm,
|
||||
policy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
},
|
||||
return patchNestedChannelConfigSection({
|
||||
cfg,
|
||||
channel,
|
||||
section: "dm",
|
||||
enabled: true,
|
||||
patch: {
|
||||
policy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
};
|
||||
}) as CoreConfig;
|
||||
}
|
||||
}
|
||||
|
||||
function setMatrixGroupPolicy(cfg: CoreConfig, groupPolicy: "open" | "allowlist" | "disabled") {
|
||||
return setTopLevelChannelGroupPolicy({
|
||||
cfg,
|
||||
channel: "matrix",
|
||||
groupPolicy,
|
||||
enabled: true,
|
||||
}) as CoreConfig;
|
||||
}
|
||||
|
||||
function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[]) {
|
||||
const groups = Object.fromEntries(roomKeys.map((key) => [key, { allow: true }]));
|
||||
return {
|
||||
@ -242,15 +213,16 @@ const matrixGroupAccess: NonNullable<ChannelSetupWizard["groupAccess"]> = {
|
||||
setMatrixGroupRooms(cfg as CoreConfig, resolved as string[]),
|
||||
};
|
||||
|
||||
const matrixDmPolicy: ChannelSetupDmPolicy = {
|
||||
const matrixDmPolicy: ChannelSetupDmPolicy = createNestedChannelDmPolicy({
|
||||
label: "Matrix",
|
||||
channel,
|
||||
section: "dm",
|
||||
policyKey: "channels.matrix.dm.policy",
|
||||
allowFromKey: "channels.matrix.dm.allowFrom",
|
||||
getCurrent: (cfg) => (cfg as CoreConfig).channels?.matrix?.dm?.policy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setMatrixDmPolicy(cfg as CoreConfig, policy),
|
||||
promptAllowFrom: promptMatrixAllowFrom,
|
||||
};
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
export { matrixSetupAdapter } from "./setup-core.js";
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
|
||||
import { createScopedAccountConfigAccessors } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
createScopedAccountConfigAccessors,
|
||||
createScopedChannelConfigBase,
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { collectAllowlistProviderRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy";
|
||||
import { createMessageToolButtonsSchema } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type { ChannelMessageToolDiscovery } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
|
||||
@ -31,10 +32,8 @@ import {
|
||||
buildChannelConfigSchema,
|
||||
createAccountStatusSink,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelMessageActionAdapter,
|
||||
type ChannelMessageActionName,
|
||||
type ChannelPlugin,
|
||||
@ -258,6 +257,22 @@ 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,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeAllowEntry(raw),
|
||||
});
|
||||
|
||||
export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
|
||||
id: "mattermost",
|
||||
meta: {
|
||||
@ -295,24 +310,7 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
|
||||
reload: { configPrefixes: ["channels.mattermost"] },
|
||||
configSchema: buildChannelConfigSchema(MattermostConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listMattermostAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveMattermostAccount({ cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultMattermostAccountId(cfg),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg,
|
||||
sectionKey: "mattermost",
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg,
|
||||
sectionKey: "mattermost",
|
||||
accountId,
|
||||
clearBaseFields: ["botToken", "baseUrl", "name"],
|
||||
}),
|
||||
...mattermostConfigBase,
|
||||
isConfigured: (account) => Boolean(account.botToken && account.baseUrl),
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
@ -325,18 +323,7 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
|
||||
...mattermostConfigAccessors,
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
return buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "mattermost",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeAllowEntry(raw),
|
||||
});
|
||||
},
|
||||
resolveDmPolicy: resolveMattermostDmPolicy,
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
return collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg,
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
|
||||
import {
|
||||
createScopedAccountConfigAccessors,
|
||||
createTopLevelChannelConfigBase,
|
||||
} 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 {
|
||||
@ -63,6 +67,30 @@ const loadMSTeamsChannelRuntime = createLazyRuntimeNamedExport(
|
||||
"msTeamsChannelRuntime",
|
||||
);
|
||||
|
||||
const resolveMSTeamsChannelConfig = (cfg: OpenClawConfig) => ({
|
||||
allowFrom: cfg.channels?.msteams?.allowFrom,
|
||||
defaultTo: cfg.channels?.msteams?.defaultTo,
|
||||
});
|
||||
|
||||
const msteamsConfigBase = createTopLevelChannelConfigBase<ResolvedMSTeamsAccount>({
|
||||
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),
|
||||
resolveAllowFrom: (account) => account.allowFrom,
|
||||
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
|
||||
resolveDefaultTo: (account) => account.defaultTo,
|
||||
});
|
||||
|
||||
function describeMSTeamsMessageTool({
|
||||
cfg,
|
||||
}: Parameters<
|
||||
@ -128,43 +156,14 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
reload: { configPrefixes: ["channels.msteams"] },
|
||||
configSchema: buildChannelConfigSchema(MSTeamsConfigSchema),
|
||||
config: {
|
||||
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
||||
resolveAccount: (cfg) => ({
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
enabled: cfg.channels?.msteams?.enabled !== false,
|
||||
configured: Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
|
||||
}),
|
||||
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
setAccountEnabled: ({ cfg, enabled }) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
msteams: {
|
||||
...cfg.channels?.msteams,
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
}),
|
||||
deleteAccount: ({ cfg }) => {
|
||||
const next = { ...cfg } as OpenClawConfig;
|
||||
const nextChannels = { ...cfg.channels };
|
||||
delete nextChannels.msteams;
|
||||
if (Object.keys(nextChannels).length > 0) {
|
||||
next.channels = nextChannels;
|
||||
} else {
|
||||
delete next.channels;
|
||||
}
|
||||
return next;
|
||||
},
|
||||
...msteamsConfigBase,
|
||||
isConfigured: (_account, cfg) => Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg }) => cfg.channels?.msteams?.allowFrom ?? [],
|
||||
formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({ allowFrom }),
|
||||
resolveDefaultTo: ({ cfg }) => cfg.channels?.msteams?.defaultTo?.trim() || undefined,
|
||||
...msteamsConfigAccessors,
|
||||
},
|
||||
security: {
|
||||
collectWarnings: ({ cfg }) => {
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import {
|
||||
createTopLevelChannelAllowFromSetter,
|
||||
createTopLevelChannelDmPolicy,
|
||||
createTopLevelChannelGroupPolicySetter,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
mergeAllowFromEntries,
|
||||
setTopLevelChannelAllowFrom,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
setTopLevelChannelGroupPolicy,
|
||||
splitSetupEntries,
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type DmPolicy,
|
||||
type OpenClawConfig,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
@ -23,22 +22,13 @@ import { msteamsSetupAdapter } from "./setup-core.js";
|
||||
import { hasConfiguredMSTeamsCredentials, resolveMSTeamsCredentials } from "./token.js";
|
||||
|
||||
const channel = "msteams" as const;
|
||||
|
||||
function setMSTeamsDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy) {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy,
|
||||
});
|
||||
}
|
||||
|
||||
function setMSTeamsAllowFrom(cfg: OpenClawConfig, allowFrom: string[]): OpenClawConfig {
|
||||
return setTopLevelChannelAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
allowFrom,
|
||||
});
|
||||
}
|
||||
const setMSTeamsAllowFrom = createTopLevelChannelAllowFromSetter({
|
||||
channel,
|
||||
});
|
||||
const setMSTeamsGroupPolicy = createTopLevelChannelGroupPolicySetter({
|
||||
channel,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
function looksLikeGuid(value: string): boolean {
|
||||
return /^[0-9a-fA-F-]{16,}$/.test(value);
|
||||
@ -146,18 +136,6 @@ async function noteMSTeamsCredentialHelp(prompter: WizardPrompter): Promise<void
|
||||
);
|
||||
}
|
||||
|
||||
function setMSTeamsGroupPolicy(
|
||||
cfg: OpenClawConfig,
|
||||
groupPolicy: "open" | "allowlist" | "disabled",
|
||||
): OpenClawConfig {
|
||||
return setTopLevelChannelGroupPolicy({
|
||||
cfg,
|
||||
channel,
|
||||
groupPolicy,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
function setMSTeamsTeamsAllowlist(
|
||||
cfg: OpenClawConfig,
|
||||
entries: Array<{ teamKey: string; channelKey?: string }>,
|
||||
@ -281,15 +259,14 @@ const msteamsGroupAccess: NonNullable<ChannelSetupWizard["groupAccess"]> = {
|
||||
setMSTeamsTeamsAllowlist(cfg, resolved as Array<{ teamKey: string; channelKey?: string }>),
|
||||
};
|
||||
|
||||
const msteamsDmPolicy: ChannelSetupDmPolicy = {
|
||||
const msteamsDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
|
||||
label: "MS Teams",
|
||||
channel,
|
||||
policyKey: "channels.msteams.dmPolicy",
|
||||
allowFromKey: "channels.msteams.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.msteams?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setMSTeamsDmPolicy(cfg, policy),
|
||||
promptAllowFrom: promptMSTeamsAllowFrom,
|
||||
};
|
||||
});
|
||||
|
||||
export { msteamsSetupAdapter } from "./setup-core.js";
|
||||
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
|
||||
import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import {
|
||||
createScopedAccountConfigAccessors,
|
||||
createScopedChannelConfigBase,
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectAllowlistProviderGroupPolicyWarnings,
|
||||
collectOpenGroupPolicyRouteAllowlistWarnings,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
@ -13,8 +16,6 @@ import {
|
||||
buildRuntimeAccountStatusSnapshot,
|
||||
clearAccountEntryFields,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelPlugin,
|
||||
type OpenClawConfig,
|
||||
} from "../runtime-api.js";
|
||||
@ -49,6 +50,37 @@ 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<
|
||||
ResolvedNextcloudTalkAccount,
|
||||
CoreConfig
|
||||
>({
|
||||
sectionKey: "nextcloud-talk",
|
||||
listAccountIds: listNextcloudTalkAccountIds,
|
||||
resolveAccount: (cfg, accountId) => resolveNextcloudTalkAccount({ cfg, accountId }),
|
||||
defaultAccountId: resolveDefaultNextcloudTalkAccountId,
|
||||
clearBaseFields: ["botSecret", "botSecretFile", "baseUrl", "name"],
|
||||
});
|
||||
|
||||
const resolveNextcloudTalkDmPolicy = createScopedDmSecurityResolver<ResolvedNextcloudTalkAccount>({
|
||||
channelKey: "nextcloud-talk",
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => raw.replace(/^(nextcloud-talk|nc-talk|nc):/i, "").toLowerCase(),
|
||||
});
|
||||
|
||||
export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> = {
|
||||
id: "nextcloud-talk",
|
||||
meta,
|
||||
@ -72,25 +104,7 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
||||
reload: { configPrefixes: ["channels.nextcloud-talk"] },
|
||||
configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listNextcloudTalkAccountIds(cfg as CoreConfig),
|
||||
resolveAccount: (cfg, accountId) =>
|
||||
resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultNextcloudTalkAccountId(cfg as CoreConfig),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg,
|
||||
sectionKey: "nextcloud-talk",
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg,
|
||||
sectionKey: "nextcloud-talk",
|
||||
accountId,
|
||||
clearBaseFields: ["botSecret", "botSecretFile", "baseUrl", "name"],
|
||||
}),
|
||||
...nextcloudTalkConfigBase,
|
||||
isConfigured: (account) => Boolean(account.secret?.trim() && account.baseUrl?.trim()),
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
@ -100,29 +114,10 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
||||
secretSource: account.secretSource,
|
||||
baseUrl: account.baseUrl ? "[set]" : "[missing]",
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||
mapAllowFromEntries(
|
||||
resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom,
|
||||
).map((entry) => entry.toLowerCase()),
|
||||
formatAllowFrom: ({ allowFrom }) =>
|
||||
formatAllowFromLowercase({
|
||||
allowFrom,
|
||||
stripPrefixRe: /^(nextcloud-talk|nc-talk|nc):/i,
|
||||
}),
|
||||
...nextcloudTalkConfigAccessors,
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
return buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "nextcloud-talk",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => raw.replace(/^(nextcloud-talk|nc-talk|nc):/i, "").toLowerCase(),
|
||||
});
|
||||
},
|
||||
resolveDmPolicy: resolveNextcloudTalkDmPolicy,
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
const roomAllowlistConfigured =
|
||||
account.config.rooms && Object.keys(account.config.rooms).length > 0;
|
||||
|
||||
@ -8,9 +8,9 @@ import {
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
mergeAllowFromEntries,
|
||||
createTopLevelChannelDmPolicy,
|
||||
resolveSetupAccountId,
|
||||
setSetupChannelEnabled,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import type { ChannelSetupDmPolicy } from "openclaw/plugin-sdk/setup";
|
||||
import { type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
||||
@ -21,7 +21,7 @@ import {
|
||||
resolveDefaultNextcloudTalkAccountId,
|
||||
resolveNextcloudTalkAccount,
|
||||
} from "./accounts.js";
|
||||
import type { CoreConfig, DmPolicy } from "./types.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
const channel = "nextcloud-talk" as const;
|
||||
|
||||
@ -46,14 +46,6 @@ export function validateNextcloudTalkBaseUrl(value: string): string | undefined
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function setNextcloudTalkDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy,
|
||||
}) as CoreConfig;
|
||||
}
|
||||
|
||||
export function setNextcloudTalkAccountConfig(
|
||||
cfg: CoreConfig,
|
||||
accountId: string,
|
||||
@ -174,15 +166,14 @@ async function promptNextcloudTalkAllowFromForAccount(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export const nextcloudTalkDmPolicy: ChannelSetupDmPolicy = {
|
||||
export const nextcloudTalkDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
|
||||
label: "Nextcloud Talk",
|
||||
channel,
|
||||
policyKey: "channels.nextcloud-talk.dmPolicy",
|
||||
allowFromKey: "channels.nextcloud-talk.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.["nextcloud-talk"]?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setNextcloudTalkDmPolicy(cfg as CoreConfig, policy as DmPolicy),
|
||||
promptAllowFrom: promptNextcloudTalkAllowFromForAccount,
|
||||
};
|
||||
});
|
||||
|
||||
export const nextcloudTalkSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
collectStatusIssuesFromLastError,
|
||||
@ -31,6 +32,22 @@ const activeBuses = new Map<string, NostrBusHandle>();
|
||||
// Store metrics snapshots per account (for status reporting)
|
||||
const metricsSnapshots = new Map<string, MetricsSnapshot>();
|
||||
|
||||
const resolveNostrDmPolicy = createScopedDmSecurityResolver<ResolvedNostrAccount>({
|
||||
channelKey: "nostr",
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
defaultPolicy: "pairing",
|
||||
approveHint: formatPairingApproveHint("nostr"),
|
||||
normalizeEntry: (raw) => {
|
||||
try {
|
||||
return normalizePubkey(raw.replace(/^nostr:/i, "").trim());
|
||||
} catch {
|
||||
return raw.trim();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
||||
id: "nostr",
|
||||
meta: {
|
||||
@ -101,22 +118,7 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
||||
},
|
||||
|
||||
security: {
|
||||
resolveDmPolicy: ({ account }) => {
|
||||
return {
|
||||
policy: account.config.dmPolicy ?? "pairing",
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPath: "channels.nostr.dmPolicy",
|
||||
allowFromPath: "channels.nostr.allowFrom",
|
||||
approveHint: formatPairingApproveHint("nostr"),
|
||||
normalizeEntry: (raw) => {
|
||||
try {
|
||||
return normalizePubkey(raw.replace(/^nostr:/i, "").trim());
|
||||
} catch {
|
||||
return raw.trim();
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
resolveDmPolicy: resolveNostrDmPolicy,
|
||||
},
|
||||
|
||||
messaging: {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import type { ChannelSetupAdapter } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { DmPolicy } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
createTopLevelChannelAllowFromSetter,
|
||||
createTopLevelChannelDmPolicy,
|
||||
mergeAllowFromEntries,
|
||||
parseSetupEntriesWithParser,
|
||||
setTopLevelChannelAllowFrom,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
patchTopLevelChannelConfigSection,
|
||||
splitSetupEntries,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import type { ChannelSetupDmPolicy } from "openclaw/plugin-sdk/setup";
|
||||
@ -18,6 +18,9 @@ import { getPublicKeyFromPrivate, normalizePubkey } from "./nostr-bus.js";
|
||||
import { resolveNostrAccount } from "./types.js";
|
||||
|
||||
const channel = "nostr" as const;
|
||||
const setNostrAllowFrom = createTopLevelChannelAllowFromSetter({
|
||||
channel,
|
||||
});
|
||||
|
||||
const NOSTR_SETUP_HELP_LINES = [
|
||||
"Use a Nostr private key in nsec or 64-character hex format.",
|
||||
@ -36,46 +39,6 @@ const NOSTR_ALLOW_FROM_HELP_LINES = [
|
||||
`Docs: ${formatDocsLink("/channels/nostr", "channels/nostr")}`,
|
||||
];
|
||||
|
||||
function patchNostrConfig(params: {
|
||||
cfg: OpenClawConfig;
|
||||
patch: Record<string, unknown>;
|
||||
clearFields?: string[];
|
||||
enabled?: boolean;
|
||||
}): OpenClawConfig {
|
||||
const existing = (params.cfg.channels?.nostr ?? {}) as Record<string, unknown>;
|
||||
const nextNostr = { ...existing };
|
||||
for (const field of params.clearFields ?? []) {
|
||||
delete nextNostr[field];
|
||||
}
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
nostr: {
|
||||
...nextNostr,
|
||||
...(params.enabled ? { enabled: true } : {}),
|
||||
...params.patch,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setNostrDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy): OpenClawConfig {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy,
|
||||
});
|
||||
}
|
||||
|
||||
function setNostrAllowFrom(cfg: OpenClawConfig, allowFrom: string[]): OpenClawConfig {
|
||||
return setTopLevelChannelAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
allowFrom,
|
||||
});
|
||||
}
|
||||
|
||||
function parseRelayUrls(raw: string): { relays: string[]; error?: string } {
|
||||
const entries = splitSetupEntries(raw);
|
||||
const relays: string[] = [];
|
||||
@ -126,21 +89,21 @@ async function promptNostrAllowFrom(params: {
|
||||
return setNostrAllowFrom(params.cfg, mergeAllowFromEntries(existing, parsed.entries));
|
||||
}
|
||||
|
||||
const nostrDmPolicy: ChannelSetupDmPolicy = {
|
||||
const nostrDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
|
||||
label: "Nostr",
|
||||
channel,
|
||||
policyKey: "channels.nostr.dmPolicy",
|
||||
allowFromKey: "channels.nostr.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.nostr?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setNostrDmPolicy(cfg, policy),
|
||||
promptAllowFrom: promptNostrAllowFrom,
|
||||
};
|
||||
});
|
||||
|
||||
export const nostrSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
applyAccountName: ({ cfg, name }) =>
|
||||
patchNostrConfig({
|
||||
patchTopLevelChannelConfigSection({
|
||||
cfg,
|
||||
channel,
|
||||
patch: name?.trim() ? { name: name.trim() } : {},
|
||||
}),
|
||||
validateInput: ({ input }) => {
|
||||
@ -174,8 +137,9 @@ export const nostrSetupAdapter: ChannelSetupAdapter = {
|
||||
const relayResult = typedInput.relayUrls?.trim()
|
||||
? parseRelayUrls(typedInput.relayUrls)
|
||||
: { relays: [] };
|
||||
return patchNostrConfig({
|
||||
return patchTopLevelChannelConfigSection({
|
||||
cfg,
|
||||
channel,
|
||||
enabled: true,
|
||||
clearFields: typedInput.useEnv ? ["privateKey"] : undefined,
|
||||
patch: {
|
||||
@ -218,8 +182,9 @@ export const nostrSetupWizard: ChannelSetupWizard = {
|
||||
Boolean(process.env.NOSTR_PRIVATE_KEY?.trim()) &&
|
||||
!resolveNostrAccount({ cfg, accountId }).config.privateKey?.trim(),
|
||||
apply: async ({ cfg }) =>
|
||||
patchNostrConfig({
|
||||
patchTopLevelChannelConfigSection({
|
||||
cfg,
|
||||
channel,
|
||||
enabled: true,
|
||||
clearFields: ["privateKey"],
|
||||
patch: {},
|
||||
@ -247,15 +212,17 @@ export const nostrSetupWizard: ChannelSetupWizard = {
|
||||
};
|
||||
},
|
||||
applyUseEnv: async ({ cfg }) =>
|
||||
patchNostrConfig({
|
||||
patchTopLevelChannelConfigSection({
|
||||
cfg,
|
||||
channel,
|
||||
enabled: true,
|
||||
clearFields: ["privateKey"],
|
||||
patch: {},
|
||||
}),
|
||||
applySet: async ({ cfg, resolvedValue }) =>
|
||||
patchNostrConfig({
|
||||
patchTopLevelChannelConfigSection({
|
||||
cfg,
|
||||
channel,
|
||||
enabled: true,
|
||||
patch: { privateKey: resolvedValue },
|
||||
}),
|
||||
@ -280,8 +247,9 @@ export const nostrSetupWizard: ChannelSetupWizard = {
|
||||
validate: ({ value }) => parseRelayUrls(value).error,
|
||||
applySet: async ({ cfg, value }) => {
|
||||
const relayResult = parseRelayUrls(value);
|
||||
return patchNostrConfig({
|
||||
return patchTopLevelChannelConfigSection({
|
||||
cfg,
|
||||
channel,
|
||||
enabled: true,
|
||||
clearFields: relayResult.relays.length > 0 ? undefined : ["relays"],
|
||||
patch: relayResult.relays.length > 0 ? { relays: relayResult.relays } : {},
|
||||
@ -291,8 +259,9 @@ export const nostrSetupWizard: ChannelSetupWizard = {
|
||||
],
|
||||
dmPolicy: nostrDmPolicy,
|
||||
disable: (cfg) =>
|
||||
patchNostrConfig({
|
||||
patchTopLevelChannelConfigSection({
|
||||
cfg,
|
||||
channel,
|
||||
patch: { enabled: false },
|
||||
}),
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { buildAccountScopedAllowlistConfigEditor } from "openclaw/plugin-sdk/allowlist-config-edit";
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
|
||||
@ -35,6 +35,13 @@ import { getSignalRuntime } from "./runtime.js";
|
||||
import { signalSetupAdapter } from "./setup-core.js";
|
||||
import { createSignalPluginBase, signalConfigAccessors, signalSetupWizard } from "./shared.js";
|
||||
|
||||
const resolveSignalDmPolicy = createScopedDmSecurityResolver<ResolvedSignalAccount>({
|
||||
channelKey: "signal",
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
|
||||
});
|
||||
type SignalSendFn = ReturnType<typeof getSignalRuntime>["channel"]["signal"]["sendMessageSignal"];
|
||||
|
||||
function resolveSignalSendContext(params: {
|
||||
@ -297,18 +304,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
return buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "signal",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
|
||||
});
|
||||
},
|
||||
resolveDmPolicy: resolveSignalDmPolicy,
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
return collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg,
|
||||
|
||||
@ -3,7 +3,7 @@ import {
|
||||
resolveLegacyDmAllowlistConfigPaths,
|
||||
} from "openclaw/plugin-sdk/allowlist-config-edit";
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
createScopedDmSecurityResolver,
|
||||
collectOpenGroupPolicyConfiguredRouteWarnings,
|
||||
collectOpenProviderGroupPolicyWarnings,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
@ -54,6 +54,14 @@ import { buildSlackThreadingToolContext } from "./threading-tool-context.js";
|
||||
|
||||
const SLACK_CHANNEL_TYPE_CACHE = new Map<string, "channel" | "group" | "dm" | "unknown">();
|
||||
|
||||
const resolveSlackDmPolicy = createScopedDmSecurityResolver<ResolvedSlackAccount>({
|
||||
channelKey: "slack",
|
||||
resolvePolicy: (account) => account.dm?.policy,
|
||||
resolveAllowFrom: (account) => account.dm?.allowFrom,
|
||||
allowFromPathSuffix: "dm.",
|
||||
normalizeEntry: (raw) => raw.replace(/^(slack|user):/i, ""),
|
||||
});
|
||||
|
||||
// Select the appropriate Slack token for read/write operations.
|
||||
function getTokenForOperation(
|
||||
account: ResolvedSlackAccount,
|
||||
@ -351,18 +359,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
return buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "slack",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.dm?.policy,
|
||||
allowFrom: account.dm?.allowFrom ?? [],
|
||||
allowFromPathSuffix: "dm.",
|
||||
normalizeEntry: (raw) => raw.replace(/^(slack|user):/i, ""),
|
||||
});
|
||||
},
|
||||
resolveDmPolicy: resolveSlackDmPolicy,
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
const channelAllowlistConfigured =
|
||||
Boolean(account.config.channels) && Object.keys(account.config.channels ?? {}).length > 0;
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import {
|
||||
createAllowlistSetupWizardProxy,
|
||||
createAccountScopedAllowFromSection,
|
||||
createAccountScopedGroupAccessSection,
|
||||
createLegacyCompatChannelDmPolicy,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
createEnvPatchedAccountSetupAdapter,
|
||||
hasConfiguredSecretInput,
|
||||
type OpenClawConfig,
|
||||
noteChannelLookupFailure,
|
||||
noteChannelLookupSummary,
|
||||
parseMentionOrPrefixedId,
|
||||
patchChannelConfigForAccount,
|
||||
setAccountGroupPolicyForChannel,
|
||||
setLegacyChannelDmPolicyWithAllowFrom,
|
||||
setSetupChannelEnabled,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
@ -112,21 +111,11 @@ export function createSlackSetupWizardBase(handlers: {
|
||||
NonNullable<NonNullable<ChannelSetupWizard["groupAccess"]>["resolveAllowlist"]>
|
||||
>;
|
||||
}) {
|
||||
const slackDmPolicy: ChannelSetupDmPolicy = {
|
||||
const slackDmPolicy: ChannelSetupDmPolicy = createLegacyCompatChannelDmPolicy({
|
||||
label: "Slack",
|
||||
channel,
|
||||
policyKey: "channels.slack.dmPolicy",
|
||||
allowFromKey: "channels.slack.allowFrom",
|
||||
getCurrent: (cfg: OpenClawConfig) =>
|
||||
cfg.channels?.slack?.dmPolicy ?? cfg.channels?.slack?.dm?.policy ?? "pairing",
|
||||
setPolicy: (cfg: OpenClawConfig, policy) =>
|
||||
setLegacyChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy: policy,
|
||||
}),
|
||||
promptAllowFrom: handlers.promptAllowFrom,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
channel,
|
||||
@ -178,7 +167,9 @@ export function createSlackSetupWizardBase(handlers: {
|
||||
}),
|
||||
],
|
||||
dmPolicy: slackDmPolicy,
|
||||
allowFrom: {
|
||||
allowFrom: createAccountScopedAllowFromSection({
|
||||
channel,
|
||||
credentialInputKey: "botToken",
|
||||
helpTitle: "Slack allowlist",
|
||||
helpLines: [
|
||||
"Allowlist Slack DMs by username (we resolve to user ids).",
|
||||
@ -188,7 +179,6 @@ export function createSlackSetupWizardBase(handlers: {
|
||||
"Multiple entries: comma-separated.",
|
||||
`Docs: ${formatDocsLink("/slack", "slack")}`,
|
||||
],
|
||||
credentialInputKey: "botToken",
|
||||
message: "Slack allowFrom (usernames or ids)",
|
||||
placeholder: "@alice, U12345678",
|
||||
invalidWithoutCredentialNote: "Slack token missing; use user ids (or mention form) only.",
|
||||
@ -200,34 +190,10 @@ export function createSlackSetupWizardBase(handlers: {
|
||||
idPattern: /^[A-Z][A-Z0-9]+$/i,
|
||||
normalizeId: (id) => id.toUpperCase(),
|
||||
}),
|
||||
resolveEntries: async ({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
}: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
credentialValues: { botToken?: string };
|
||||
entries: string[];
|
||||
}) => await handlers.resolveAllowFromEntries({ cfg, accountId, credentialValues, entries }),
|
||||
apply: ({
|
||||
cfg,
|
||||
accountId,
|
||||
allowFrom,
|
||||
}: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
allowFrom: string[];
|
||||
}) =>
|
||||
patchChannelConfigForAccount({
|
||||
cfg,
|
||||
channel,
|
||||
accountId,
|
||||
patch: { dmPolicy: "allowlist", allowFrom },
|
||||
}),
|
||||
},
|
||||
groupAccess: {
|
||||
resolveEntries: handlers.resolveAllowFromEntries,
|
||||
}),
|
||||
groupAccess: createAccountScopedGroupAccessSection({
|
||||
channel,
|
||||
label: "Slack channels",
|
||||
placeholder: "#general, #private, C123",
|
||||
currentPolicy: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) =>
|
||||
@ -238,57 +204,8 @@ export function createSlackSetupWizardBase(handlers: {
|
||||
.map(([key]) => key),
|
||||
updatePrompt: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) =>
|
||||
Boolean(resolveSlackAccount({ cfg, accountId }).config.channels),
|
||||
setPolicy: ({
|
||||
cfg,
|
||||
accountId,
|
||||
policy,
|
||||
}: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
policy: "open" | "allowlist" | "disabled";
|
||||
}) =>
|
||||
setAccountGroupPolicyForChannel({
|
||||
cfg,
|
||||
channel,
|
||||
accountId,
|
||||
groupPolicy: policy,
|
||||
}),
|
||||
resolveAllowlist: async ({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
}: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
credentialValues: { botToken?: string };
|
||||
entries: string[];
|
||||
prompter: { note: (message: string, title?: string) => Promise<void> };
|
||||
}) => {
|
||||
try {
|
||||
return await handlers.resolveGroupAllowlist({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
});
|
||||
} catch (error) {
|
||||
await noteChannelLookupFailure({
|
||||
prompter,
|
||||
label: "Slack channels",
|
||||
error,
|
||||
});
|
||||
await noteChannelLookupSummary({
|
||||
prompter,
|
||||
label: "Slack channels",
|
||||
resolvedSections: [],
|
||||
unresolved: entries,
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
},
|
||||
resolveAllowlist: handlers.resolveGroupAllowlist,
|
||||
fallbackResolved: (entries) => entries,
|
||||
applyAllowlist: ({
|
||||
cfg,
|
||||
accountId,
|
||||
@ -298,7 +215,7 @@ export function createSlackSetupWizardBase(handlers: {
|
||||
accountId: string;
|
||||
resolved: unknown;
|
||||
}) => setSlackChannelAllowlist(cfg, accountId, resolved as string[]),
|
||||
},
|
||||
}),
|
||||
disable: (cfg: OpenClawConfig) => setSetupChannelEnabled(cfg, channel, false),
|
||||
} satisfies ChannelSetupWizard;
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import {
|
||||
noteChannelLookupFailure,
|
||||
noteChannelLookupSummary,
|
||||
resolveEntriesWithOptionalToken,
|
||||
type OpenClawConfig,
|
||||
parseMentionOrPrefixedId,
|
||||
promptLegacyChannelAllowFrom,
|
||||
resolveSetupAccountId,
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import type {
|
||||
@ -22,22 +22,26 @@ async function resolveSlackAllowFromEntries(params: {
|
||||
token?: string;
|
||||
entries: string[];
|
||||
}): Promise<ChannelSetupWizardAllowFromEntry[]> {
|
||||
if (!params.token?.trim()) {
|
||||
return params.entries.map((input) => ({
|
||||
return await resolveEntriesWithOptionalToken({
|
||||
token: params.token,
|
||||
entries: params.entries,
|
||||
buildWithoutToken: (input) => ({
|
||||
input,
|
||||
resolved: false,
|
||||
id: null,
|
||||
}));
|
||||
}
|
||||
const resolved = await resolveSlackUserAllowlist({
|
||||
token: params.token,
|
||||
entries: params.entries,
|
||||
}),
|
||||
resolveEntries: async ({ token, entries }) =>
|
||||
(
|
||||
await resolveSlackUserAllowlist({
|
||||
token,
|
||||
entries,
|
||||
})
|
||||
).map((entry) => ({
|
||||
input: entry.input,
|
||||
resolved: entry.resolved,
|
||||
id: entry.id ?? null,
|
||||
})),
|
||||
});
|
||||
return resolved.map((entry) => ({
|
||||
input: entry.input,
|
||||
resolved: entry.resolved,
|
||||
id: entry.id ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
async function promptSlackAllowFrom(params: {
|
||||
@ -45,14 +49,6 @@ async function promptSlackAllowFrom(params: {
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const accountId = resolveSetupAccountId({
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultSlackAccountId(params.cfg),
|
||||
});
|
||||
const resolved = resolveSlackAccount({ cfg: params.cfg, accountId });
|
||||
const token = resolved.userToken ?? resolved.botToken ?? "";
|
||||
const existing =
|
||||
params.cfg.channels?.slack?.allowFrom ?? params.cfg.channels?.slack?.dm?.allowFrom ?? [];
|
||||
const parseId = (value: string) =>
|
||||
parseMentionOrPrefixedId({
|
||||
value,
|
||||
@ -62,12 +58,16 @@ async function promptSlackAllowFrom(params: {
|
||||
normalizeId: (id) => id.toUpperCase(),
|
||||
});
|
||||
|
||||
return promptLegacyChannelAllowFrom({
|
||||
return await promptLegacyChannelAllowFromForAccount({
|
||||
cfg: params.cfg,
|
||||
channel,
|
||||
prompter: params.prompter,
|
||||
existing,
|
||||
token,
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultSlackAccountId(params.cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveSlackAccount({ cfg, accountId }),
|
||||
resolveExisting: (_account, cfg) =>
|
||||
cfg.channels?.slack?.allowFrom ?? cfg.channels?.slack?.dm?.allowFrom ?? [],
|
||||
resolveToken: (account) => account.userToken ?? account.botToken ?? "",
|
||||
noteTitle: "Slack allowlist",
|
||||
noteLines: [
|
||||
"Allowlist Slack DMs by username (we resolve to user ids).",
|
||||
@ -81,11 +81,17 @@ async function promptSlackAllowFrom(params: {
|
||||
placeholder: "@alice, U12345678",
|
||||
parseId,
|
||||
invalidWithoutTokenNote: "Slack token missing; use user ids (or mention form) only.",
|
||||
resolveEntries: ({ token, entries }) =>
|
||||
resolveSlackUserAllowlist({
|
||||
token,
|
||||
entries,
|
||||
}),
|
||||
resolveEntries: async ({ token, entries }) =>
|
||||
(
|
||||
await resolveSlackUserAllowlist({
|
||||
token,
|
||||
entries,
|
||||
})
|
||||
).map((entry) => ({
|
||||
input: entry.input,
|
||||
resolved: entry.resolved,
|
||||
id: entry.id ?? null,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
@ -102,11 +108,21 @@ async function resolveSlackGroupAllowlist(params: {
|
||||
accountId: params.accountId,
|
||||
});
|
||||
const activeBotToken = accountWithTokens.botToken || params.credentialValues.botToken || "";
|
||||
if (activeBotToken && params.entries.length > 0) {
|
||||
if (params.entries.length > 0) {
|
||||
try {
|
||||
const resolved = await resolveSlackChannelAllowlist({
|
||||
const resolved = await resolveEntriesWithOptionalToken<{
|
||||
input: string;
|
||||
resolved: boolean;
|
||||
id?: string;
|
||||
}>({
|
||||
token: activeBotToken,
|
||||
entries: params.entries,
|
||||
buildWithoutToken: (input) => ({ input, resolved: false, id: undefined }),
|
||||
resolveEntries: async ({ token, entries }) =>
|
||||
await resolveSlackChannelAllowlist({
|
||||
token,
|
||||
entries,
|
||||
}),
|
||||
});
|
||||
const resolvedKeys = resolved
|
||||
.filter((entry) => entry.resolved && entry.id)
|
||||
|
||||
@ -55,7 +55,7 @@ describe("createSynologyChatPlugin", () => {
|
||||
|
||||
it("defaultAccountId returns 'default'", () => {
|
||||
const plugin = createSynologyChatPlugin();
|
||||
expect(plugin.config.defaultAccountId({})).toBe("default");
|
||||
expect(plugin.config.defaultAccountId?.({})).toBe("default");
|
||||
});
|
||||
});
|
||||
|
||||
@ -79,7 +79,7 @@ describe("createSynologyChatPlugin", () => {
|
||||
expect(result.policy).toBe("allowlist");
|
||||
expect(result.allowFrom).toEqual(["user1"]);
|
||||
expect(typeof result.normalizeEntry).toBe("function");
|
||||
expect(result.normalizeEntry(" USER1 ")).toBe("user1");
|
||||
expect(result.normalizeEntry?.(" USER1 ")).toBe("user1");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -4,13 +4,12 @@
|
||||
* Implements the ChannelPlugin interface following the LINE pattern.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
setAccountEnabledInConfigSection,
|
||||
registerPluginHttpRoute,
|
||||
buildChannelConfigSchema,
|
||||
} from "../api.js";
|
||||
createHybridChannelConfigBase,
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { z } from "zod";
|
||||
import { DEFAULT_ACCOUNT_ID, registerPluginHttpRoute, buildChannelConfigSchema } from "../api.js";
|
||||
import { listAccountIds, resolveAccount } from "./accounts.js";
|
||||
import { sendMessage, sendFileUrl } from "./client.js";
|
||||
import { getSynologyRuntime } from "./runtime.js";
|
||||
@ -23,6 +22,34 @@ const SynologyChatConfigSchema = buildChannelConfigSchema(z.object({}).passthrou
|
||||
|
||||
const activeRouteUnregisters = new Map<string, () => void>();
|
||||
|
||||
const resolveSynologyChatDmPolicy = createScopedDmSecurityResolver<ResolvedSynologyChatAccount>({
|
||||
channelKey: CHANNEL_ID,
|
||||
resolvePolicy: (account) => account.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.allowedUserIds,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
defaultPolicy: "allowlist",
|
||||
approveHint: "openclaw pairing approve synology-chat <code>",
|
||||
normalizeEntry: (raw) => raw.toLowerCase().trim(),
|
||||
});
|
||||
|
||||
const synologyChatConfigBase = createHybridChannelConfigBase<ResolvedSynologyChatAccount>({
|
||||
sectionKey: CHANNEL_ID,
|
||||
listAccountIds: (cfg: any) => listAccountIds(cfg),
|
||||
resolveAccount: (cfg: any, accountId?: string | null) => resolveAccount(cfg, accountId),
|
||||
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
clearBaseFields: [
|
||||
"token",
|
||||
"incomingUrl",
|
||||
"nasHost",
|
||||
"webhookPath",
|
||||
"dmPolicy",
|
||||
"allowedUserIds",
|
||||
"rateLimitPerMinute",
|
||||
"botName",
|
||||
"allowInsecureSsl",
|
||||
],
|
||||
});
|
||||
|
||||
function waitUntilAbort(signal?: AbortSignal, onAbort?: () => void): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const complete = () => {
|
||||
@ -73,30 +100,7 @@ export function createSynologyChatPlugin() {
|
||||
setupWizard: synologyChatSetupWizard,
|
||||
|
||||
config: {
|
||||
listAccountIds: (cfg: any) => listAccountIds(cfg),
|
||||
|
||||
resolveAccount: (cfg: any, accountId?: string | null) => resolveAccount(cfg, accountId),
|
||||
|
||||
defaultAccountId: (_cfg: any) => DEFAULT_ACCOUNT_ID,
|
||||
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }: any) => {
|
||||
const channelConfig = cfg?.channels?.[CHANNEL_ID] ?? {};
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
[CHANNEL_ID]: { ...channelConfig, enabled },
|
||||
},
|
||||
};
|
||||
}
|
||||
return setAccountEnabledInConfigSection({
|
||||
cfg,
|
||||
sectionKey: `channels.${CHANNEL_ID}`,
|
||||
accountId,
|
||||
enabled,
|
||||
});
|
||||
},
|
||||
...synologyChatConfigBase,
|
||||
},
|
||||
|
||||
pairing: {
|
||||
@ -115,30 +119,7 @@ export function createSynologyChatPlugin() {
|
||||
},
|
||||
|
||||
security: {
|
||||
resolveDmPolicy: ({
|
||||
cfg,
|
||||
accountId,
|
||||
account,
|
||||
}: {
|
||||
cfg: any;
|
||||
accountId?: string | null;
|
||||
account: ResolvedSynologyChatAccount;
|
||||
}) => {
|
||||
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const channelCfg = (cfg as any).channels?.["synology-chat"];
|
||||
const useAccountPath = Boolean(channelCfg?.accounts?.[resolvedAccountId]);
|
||||
const basePath = useAccountPath
|
||||
? `channels.synology-chat.accounts.${resolvedAccountId}.`
|
||||
: "channels.synology-chat.";
|
||||
return {
|
||||
policy: account.dmPolicy ?? "allowlist",
|
||||
allowFrom: account.allowedUserIds ?? [],
|
||||
policyPath: `${basePath}dmPolicy`,
|
||||
allowFromPath: basePath,
|
||||
approveHint: "openclaw pairing approve synology-chat <code>",
|
||||
normalizeEntry: (raw: string) => raw.toLowerCase().trim(),
|
||||
};
|
||||
},
|
||||
resolveDmPolicy: resolveSynologyChatDmPolicy,
|
||||
collectWarnings: ({ account }: { account: ResolvedSynologyChatAccount }) => {
|
||||
const warnings: string[] = [];
|
||||
if (!account.token) {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { createHybridChannelConfigBase } 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";
|
||||
@ -37,6 +38,16 @@ const tlonSetupWizardProxy = createTlonSetupWizardBase({
|
||||
).tlonSetupWizard.finalize!(params),
|
||||
}) satisfies NonNullable<ChannelPlugin["setupWizard"]>;
|
||||
|
||||
const tlonConfigBase = createHybridChannelConfigBase({
|
||||
sectionKey: TLON_CHANNEL_ID,
|
||||
listAccountIds: (cfg: OpenClawConfig) => listTlonAccountIds(cfg),
|
||||
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) =>
|
||||
resolveTlonAccount(cfg, accountId ?? undefined),
|
||||
defaultAccountId: () => "default",
|
||||
clearBaseFields: ["ship", "code", "url", "name"],
|
||||
preserveSectionOnDefaultDelete: true,
|
||||
});
|
||||
|
||||
export const tlonPlugin: ChannelPlugin = {
|
||||
id: TLON_CHANNEL_ID,
|
||||
meta: {
|
||||
@ -60,70 +71,7 @@ export const tlonPlugin: ChannelPlugin = {
|
||||
reload: { configPrefixes: ["channels.tlon"] },
|
||||
configSchema: tlonChannelConfigSchema,
|
||||
config: {
|
||||
listAccountIds: (cfg) => listTlonAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveTlonAccount(cfg, accountId ?? undefined),
|
||||
defaultAccountId: () => "default",
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
||||
const useDefault = !accountId || accountId === "default";
|
||||
if (useDefault) {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
tlon: {
|
||||
...cfg.channels?.tlon,
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
tlon: {
|
||||
...cfg.channels?.tlon,
|
||||
accounts: {
|
||||
...cfg.channels?.tlon?.accounts,
|
||||
[accountId]: {
|
||||
...cfg.channels?.tlon?.accounts?.[accountId],
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
},
|
||||
deleteAccount: ({ cfg, accountId }) => {
|
||||
const useDefault = !accountId || accountId === "default";
|
||||
if (useDefault) {
|
||||
const {
|
||||
ship: _ship,
|
||||
code: _code,
|
||||
url: _url,
|
||||
name: _name,
|
||||
...rest
|
||||
} = cfg.channels?.tlon ?? {};
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
tlon: rest,
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
const { [accountId]: _removed, ...remainingAccounts } = cfg.channels?.tlon?.accounts ?? {};
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
tlon: {
|
||||
...cfg.channels?.tlon,
|
||||
accounts: remainingAccounts,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
},
|
||||
...tlonConfigBase,
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
createScopedAccountConfigAccessors,
|
||||
createScopedChannelConfigBase,
|
||||
createScopedDmSecurityResolver,
|
||||
mapAllowFromEntries,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import {
|
||||
buildOpenGroupPolicyRestrictSendersWarning,
|
||||
buildOpenGroupPolicyWarning,
|
||||
collectOpenProviderGroupPolicyWarnings,
|
||||
@ -17,13 +21,11 @@ import {
|
||||
buildTokenChannelStatusSummary,
|
||||
buildChannelSendResult,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
chunkTextForOutbound,
|
||||
formatAllowFromLowercase,
|
||||
listDirectoryUserEntriesFromAllowFrom,
|
||||
isNumericTargetId,
|
||||
sendPayloadWithChunkedTextAndMedia,
|
||||
setAccountEnabledInConfigSection,
|
||||
} from "openclaw/plugin-sdk/zalo";
|
||||
import {
|
||||
listZaloAccountIds,
|
||||
@ -59,6 +61,29 @@ 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>({
|
||||
sectionKey: "zalo",
|
||||
listAccountIds: listZaloAccountIds,
|
||||
resolveAccount: (cfg, accountId) => resolveZaloAccount({ cfg, accountId }),
|
||||
defaultAccountId: resolveDefaultZaloAccountId,
|
||||
clearBaseFields: ["botToken", "tokenFile", "name"],
|
||||
});
|
||||
|
||||
const resolveZaloDmPolicy = createScopedDmSecurityResolver<ResolvedZaloAccount>({
|
||||
channelKey: "zalo",
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => raw.replace(/^(zalo|zl):/i, ""),
|
||||
});
|
||||
|
||||
export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
||||
id: "zalo",
|
||||
meta,
|
||||
@ -76,24 +101,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
||||
reload: { configPrefixes: ["channels.zalo"] },
|
||||
configSchema: buildChannelConfigSchema(ZaloConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listZaloAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveZaloAccount({ cfg: cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultZaloAccountId(cfg),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg: cfg,
|
||||
sectionKey: "zalo",
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg: cfg,
|
||||
sectionKey: "zalo",
|
||||
accountId,
|
||||
clearBaseFields: ["botToken", "tokenFile", "name"],
|
||||
}),
|
||||
...zaloConfigBase,
|
||||
isConfigured: (account) => Boolean(account.token?.trim()),
|
||||
describeAccount: (account): ChannelAccountSnapshot => ({
|
||||
accountId: account.accountId,
|
||||
@ -102,24 +110,10 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
||||
configured: Boolean(account.token?.trim()),
|
||||
tokenSource: account.tokenSource,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||
mapAllowFromEntries(resolveZaloAccount({ cfg: cfg, accountId }).config.allowFrom),
|
||||
formatAllowFrom: ({ allowFrom }) =>
|
||||
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalo|zl):/i }),
|
||||
...zaloConfigAccessors,
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
return buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "zalo",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => raw.replace(/^(zalo|zl):/i, ""),
|
||||
});
|
||||
},
|
||||
resolveDmPolicy: resolveZaloDmPolicy,
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
return collectOpenProviderGroupPolicyWarnings({
|
||||
cfg,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
buildSingleChannelSecretPromptState,
|
||||
createTopLevelChannelDmPolicy,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
hasConfiguredSecretInput,
|
||||
@ -7,7 +8,6 @@ import {
|
||||
normalizeAccountId,
|
||||
promptSingleChannelSecretInput,
|
||||
runSingleChannelSecretStep,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type OpenClawConfig,
|
||||
@ -20,17 +20,6 @@ const channel = "zalo" as const;
|
||||
|
||||
type UpdateMode = "polling" | "webhook";
|
||||
|
||||
function setZaloDmPolicy(
|
||||
cfg: OpenClawConfig,
|
||||
dmPolicy: "pairing" | "allowlist" | "open" | "disabled",
|
||||
) {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy,
|
||||
}) as OpenClawConfig;
|
||||
}
|
||||
|
||||
function setZaloUpdateMode(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
@ -183,13 +172,12 @@ async function promptZaloAllowFrom(params: {
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
const zaloDmPolicy: ChannelSetupDmPolicy = {
|
||||
const zaloDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
|
||||
label: "Zalo",
|
||||
channel,
|
||||
policyKey: "channels.zalo.dmPolicy",
|
||||
allowFromKey: "channels.zalo.allowFrom",
|
||||
getCurrent: (cfg) => (cfg.channels?.zalo?.dmPolicy ?? "pairing") as "pairing",
|
||||
setPolicy: (cfg, policy) => setZaloDmPolicy(cfg as OpenClawConfig, policy),
|
||||
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
|
||||
const id =
|
||||
accountId && normalizeAccountId(accountId)
|
||||
@ -201,7 +189,7 @@ const zaloDmPolicy: ChannelSetupDmPolicy = {
|
||||
accountId: id,
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export { zaloSetupAdapter } from "./setup-core.js";
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
|
||||
import { buildAccountScopedDmSecurityPolicy } from "openclaw/plugin-sdk/channel-policy";
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelDirectoryEntry,
|
||||
@ -217,6 +217,14 @@ function resolveZalouserRequireMention(params: ChannelGroupContext): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
const resolveZalouserDmPolicy = createScopedDmSecurityResolver<ResolvedZalouserAccount>({
|
||||
channelKey: "zalouser",
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => raw.replace(/^(zalouser|zlu):/i, ""),
|
||||
});
|
||||
|
||||
const zalouserMessageActions: ChannelMessageActionAdapter = {
|
||||
describeMessageTool: ({ cfg }) => {
|
||||
const accounts = listZalouserAccountIds(cfg)
|
||||
@ -292,18 +300,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
||||
setup: zalouserSetupAdapter,
|
||||
}),
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
return buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "zalouser",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => raw.replace(/^(zalouser|zlu):/i, ""),
|
||||
});
|
||||
},
|
||||
resolveDmPolicy: resolveZalouserDmPolicy,
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveZalouserRequireMention,
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import {
|
||||
createTopLevelChannelDmPolicy,
|
||||
createTopLevelChannelDmPolicySetter,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatCliCommand,
|
||||
formatDocsLink,
|
||||
@ -6,7 +8,6 @@ import {
|
||||
mergeAllowFromEntries,
|
||||
normalizeAccountId,
|
||||
patchScopedAccountConfig,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type DmPolicy,
|
||||
@ -29,6 +30,9 @@ import {
|
||||
} from "./zalo-js.js";
|
||||
|
||||
const channel = "zalouser" as const;
|
||||
const setZalouserDmPolicy = createTopLevelChannelDmPolicySetter({
|
||||
channel,
|
||||
});
|
||||
const ZALOUSER_ALLOW_FROM_PLACEHOLDER = "Alice, 123456789, or leave empty to configure later";
|
||||
const ZALOUSER_GROUPS_PLACEHOLDER = "Family, Work, 123456789, or leave empty for now";
|
||||
const ZALOUSER_DM_ACCESS_TITLE = "Zalo Personal DM access";
|
||||
@ -57,14 +61,6 @@ function setZalouserAccountScopedConfig(
|
||||
}) as OpenClawConfig;
|
||||
}
|
||||
|
||||
function setZalouserDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy): OpenClawConfig {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy,
|
||||
}) as OpenClawConfig;
|
||||
}
|
||||
|
||||
function setZalouserGroupPolicy(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
@ -193,13 +189,12 @@ async function promptZalouserAllowFrom(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const zalouserDmPolicy: ChannelSetupDmPolicy = {
|
||||
const zalouserDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
|
||||
label: "Zalo Personal",
|
||||
channel,
|
||||
policyKey: "channels.zalouser.dmPolicy",
|
||||
allowFromKey: "channels.zalouser.allowFrom",
|
||||
getCurrent: (cfg) => (cfg.channels?.zalouser?.dmPolicy ?? "pairing") as DmPolicy,
|
||||
setPolicy: (cfg, policy) => setZalouserDmPolicy(cfg as OpenClawConfig, policy),
|
||||
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
|
||||
const id =
|
||||
accountId && normalizeAccountId(accountId)
|
||||
@ -211,7 +206,7 @@ const zalouserDmPolicy: ChannelSetupDmPolicy = {
|
||||
accountId: id,
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
async function promptZalouserQuickstartDmPolicy(params: {
|
||||
cfg: OpenClawConfig;
|
||||
|
||||
@ -4,27 +4,45 @@ import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import {
|
||||
applySingleTokenPromptResult,
|
||||
buildSingleChannelSecretPromptState,
|
||||
createAccountScopedAllowFromSection,
|
||||
createAccountScopedGroupAccessSection,
|
||||
createLegacyCompatChannelDmPolicy,
|
||||
createNestedChannelAllowFromSetter,
|
||||
createNestedChannelDmPolicy,
|
||||
createNestedChannelDmPolicySetter,
|
||||
createTopLevelChannelAllowFromSetter,
|
||||
createTopLevelChannelDmPolicy,
|
||||
createTopLevelChannelDmPolicySetter,
|
||||
createTopLevelChannelGroupPolicySetter,
|
||||
normalizeAllowFromEntries,
|
||||
noteChannelLookupFailure,
|
||||
noteChannelLookupSummary,
|
||||
parseMentionOrPrefixedId,
|
||||
parseSetupEntriesAllowingWildcard,
|
||||
patchChannelConfigForAccount,
|
||||
patchNestedChannelConfigSection,
|
||||
patchLegacyDmChannelConfig,
|
||||
patchTopLevelChannelConfigSection,
|
||||
promptLegacyChannelAllowFrom,
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
parseSetupEntriesWithParser,
|
||||
promptParsedAllowFromForScopedChannel,
|
||||
promptSingleChannelSecretInput,
|
||||
promptSingleChannelToken,
|
||||
promptResolvedAllowFrom,
|
||||
resolveAccountIdForConfigure,
|
||||
resolveEntriesWithOptionalToken,
|
||||
resolveGroupAllowlistWithLookupNotes,
|
||||
resolveSetupAccountId,
|
||||
setAccountDmAllowFromForChannel,
|
||||
setAccountAllowFromForChannel,
|
||||
setAccountGroupPolicyForChannel,
|
||||
setChannelDmPolicyWithAllowFrom,
|
||||
setTopLevelChannelAllowFrom,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
setTopLevelChannelGroupPolicy,
|
||||
setNestedChannelAllowFrom,
|
||||
setNestedChannelDmPolicyWithAllowFrom,
|
||||
setLegacyChannelAllowFrom,
|
||||
setLegacyChannelDmPolicyWithAllowFrom,
|
||||
setSetupChannelEnabled,
|
||||
@ -265,6 +283,45 @@ describe("promptLegacyChannelAllowFrom", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("promptLegacyChannelAllowFromForAccount", () => {
|
||||
it("resolves the account before delegating to the shared prompt flow", async () => {
|
||||
const prompter = createPrompter(["alice"]);
|
||||
|
||||
const next = await promptLegacyChannelAllowFromForAccount({
|
||||
cfg: {
|
||||
channels: {
|
||||
slack: {
|
||||
dm: {
|
||||
allowFrom: ["U0"],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
channel: "slack",
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
prompter: prompter as any,
|
||||
defaultAccountId: DEFAULT_ACCOUNT_ID,
|
||||
resolveAccount: () => ({
|
||||
botToken: "xoxb-token",
|
||||
dmAllowFrom: ["U0"],
|
||||
}),
|
||||
resolveExisting: (account) => account.dmAllowFrom,
|
||||
resolveToken: (account) => account.botToken,
|
||||
noteTitle: "Slack allowlist",
|
||||
noteLines: ["line"],
|
||||
message: "Slack allowFrom",
|
||||
placeholder: "@alice",
|
||||
parseId: () => null,
|
||||
invalidWithoutTokenNote: "need ids",
|
||||
resolveEntries: async ({ entries }) =>
|
||||
entries.map((input) => ({ input, resolved: true, id: input.toUpperCase() })),
|
||||
});
|
||||
|
||||
expect(next.channels?.slack?.allowFrom).toEqual(["U0", "ALICE"]);
|
||||
expect(prompter.note).toHaveBeenCalledWith("line", "Slack allowlist");
|
||||
});
|
||||
});
|
||||
|
||||
describe("promptSingleChannelToken", () => {
|
||||
it("uses env tokens when confirmed", async () => {
|
||||
const prompter = createTokenPrompter({ confirms: [true], texts: [] });
|
||||
@ -1005,6 +1062,400 @@ describe("setTopLevelChannelGroupPolicy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("patchTopLevelChannelConfigSection", () => {
|
||||
it("clears requested fields before applying a patch", () => {
|
||||
const next = patchTopLevelChannelConfigSection({
|
||||
cfg: {
|
||||
channels: {
|
||||
nostr: {
|
||||
privateKey: "nsec1",
|
||||
relays: ["wss://old.example"],
|
||||
},
|
||||
},
|
||||
},
|
||||
channel: "nostr",
|
||||
clearFields: ["privateKey"],
|
||||
patch: { relays: ["wss://new.example"] },
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
expect(next.channels?.nostr?.privateKey).toBeUndefined();
|
||||
expect(next.channels?.nostr?.relays).toEqual(["wss://new.example"]);
|
||||
expect(next.channels?.nostr?.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("patchNestedChannelConfigSection", () => {
|
||||
it("clears requested nested fields before applying a patch", () => {
|
||||
const next = patchNestedChannelConfigSection({
|
||||
cfg: {
|
||||
channels: {
|
||||
matrix: {
|
||||
dm: {
|
||||
policy: "pairing",
|
||||
allowFrom: ["@alice:example.org"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
channel: "matrix",
|
||||
section: "dm",
|
||||
clearFields: ["allowFrom"],
|
||||
enabled: true,
|
||||
patch: { policy: "disabled" },
|
||||
});
|
||||
|
||||
expect(next.channels?.matrix?.enabled).toBe(true);
|
||||
expect(next.channels?.matrix?.dm?.policy).toBe("disabled");
|
||||
expect(next.channels?.matrix?.dm?.allowFrom).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTopLevelChannelDmPolicy", () => {
|
||||
it("creates a reusable dm policy definition", () => {
|
||||
const dmPolicy = createTopLevelChannelDmPolicy({
|
||||
label: "LINE",
|
||||
channel: "line",
|
||||
policyKey: "channels.line.dmPolicy",
|
||||
allowFromKey: "channels.line.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.line?.dmPolicy ?? "pairing",
|
||||
});
|
||||
|
||||
const next = dmPolicy.setPolicy(
|
||||
{
|
||||
channels: {
|
||||
line: {
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: ["U123"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"open",
|
||||
);
|
||||
|
||||
expect(dmPolicy.getCurrent({})).toBe("pairing");
|
||||
expect(next.channels?.line?.dmPolicy).toBe("open");
|
||||
expect(next.channels?.line?.allowFrom).toEqual(["U123", "*"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTopLevelChannelDmPolicySetter", () => {
|
||||
it("reuses the shared top-level dmPolicy writer", () => {
|
||||
const setPolicy = createTopLevelChannelDmPolicySetter({
|
||||
channel: "zalo",
|
||||
});
|
||||
const next = setPolicy(
|
||||
{
|
||||
channels: {
|
||||
zalo: {
|
||||
allowFrom: ["12345"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"open",
|
||||
);
|
||||
|
||||
expect(next.channels?.zalo?.dmPolicy).toBe("open");
|
||||
expect(next.channels?.zalo?.allowFrom).toEqual(["12345", "*"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setNestedChannelAllowFrom", () => {
|
||||
it("writes nested allowFrom and can force enabled state", () => {
|
||||
const next = setNestedChannelAllowFrom({
|
||||
cfg: {},
|
||||
channel: "googlechat",
|
||||
section: "dm",
|
||||
allowFrom: ["users/123"],
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
expect(next.channels?.googlechat?.enabled).toBe(true);
|
||||
expect(next.channels?.googlechat?.dm?.allowFrom).toEqual(["users/123"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setNestedChannelDmPolicyWithAllowFrom", () => {
|
||||
it("adds wildcard allowFrom for open policy inside a nested section", () => {
|
||||
const next = setNestedChannelDmPolicyWithAllowFrom({
|
||||
cfg: {
|
||||
channels: {
|
||||
matrix: {
|
||||
dm: {
|
||||
policy: "pairing",
|
||||
allowFrom: ["@alice:example.org"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
channel: "matrix",
|
||||
section: "dm",
|
||||
dmPolicy: "open",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
expect(next.channels?.matrix?.enabled).toBe(true);
|
||||
expect(next.channels?.matrix?.dm?.policy).toBe("open");
|
||||
expect(next.channels?.matrix?.dm?.allowFrom).toEqual(["@alice:example.org", "*"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createNestedChannelDmPolicy", () => {
|
||||
it("creates a reusable nested dm policy definition", () => {
|
||||
const dmPolicy = createNestedChannelDmPolicy({
|
||||
label: "Matrix",
|
||||
channel: "matrix",
|
||||
section: "dm",
|
||||
policyKey: "channels.matrix.dm.policy",
|
||||
allowFromKey: "channels.matrix.dm.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.matrix?.dm?.policy ?? "pairing",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const next = dmPolicy.setPolicy(
|
||||
{
|
||||
channels: {
|
||||
matrix: {
|
||||
dm: {
|
||||
allowFrom: ["@alice:example.org"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"open",
|
||||
);
|
||||
|
||||
expect(next.channels?.matrix?.enabled).toBe(true);
|
||||
expect(next.channels?.matrix?.dm?.policy).toBe("open");
|
||||
expect(next.channels?.matrix?.dm?.allowFrom).toEqual(["@alice:example.org", "*"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createNestedChannelDmPolicySetter", () => {
|
||||
it("reuses the shared nested dmPolicy writer", () => {
|
||||
const setPolicy = createNestedChannelDmPolicySetter({
|
||||
channel: "googlechat",
|
||||
section: "dm",
|
||||
enabled: true,
|
||||
});
|
||||
const next = setPolicy({}, "disabled");
|
||||
|
||||
expect(next.channels?.googlechat?.enabled).toBe(true);
|
||||
expect(next.channels?.googlechat?.dm?.policy).toBe("disabled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createNestedChannelAllowFromSetter", () => {
|
||||
it("reuses the shared nested allowFrom writer", () => {
|
||||
const setAllowFrom = createNestedChannelAllowFromSetter({
|
||||
channel: "googlechat",
|
||||
section: "dm",
|
||||
enabled: true,
|
||||
});
|
||||
const next = setAllowFrom({}, ["users/123"]);
|
||||
|
||||
expect(next.channels?.googlechat?.enabled).toBe(true);
|
||||
expect(next.channels?.googlechat?.dm?.allowFrom).toEqual(["users/123"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTopLevelChannelAllowFromSetter", () => {
|
||||
it("reuses the shared top-level allowFrom writer", () => {
|
||||
const setAllowFrom = createTopLevelChannelAllowFromSetter({
|
||||
channel: "msteams",
|
||||
enabled: true,
|
||||
});
|
||||
const next = setAllowFrom({}, ["user-1"]);
|
||||
|
||||
expect(next.channels?.msteams?.allowFrom).toEqual(["user-1"]);
|
||||
expect(next.channels?.msteams?.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createLegacyCompatChannelDmPolicy", () => {
|
||||
it("reads nested legacy dm policy and writes top-level compat fields", () => {
|
||||
const dmPolicy = createLegacyCompatChannelDmPolicy({
|
||||
label: "Slack",
|
||||
channel: "slack",
|
||||
});
|
||||
|
||||
expect(
|
||||
dmPolicy.getCurrent({
|
||||
channels: {
|
||||
slack: {
|
||||
dm: {
|
||||
policy: "open",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe("open");
|
||||
|
||||
const next = dmPolicy.setPolicy(
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
dm: {
|
||||
allowFrom: ["U123"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"open",
|
||||
);
|
||||
|
||||
expect(next.channels?.slack?.dmPolicy).toBe("open");
|
||||
expect(next.channels?.slack?.allowFrom).toEqual(["U123", "*"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTopLevelChannelGroupPolicySetter", () => {
|
||||
it("reuses the shared top-level groupPolicy writer", () => {
|
||||
const setGroupPolicy = createTopLevelChannelGroupPolicySetter({
|
||||
channel: "feishu",
|
||||
enabled: true,
|
||||
});
|
||||
const next = setGroupPolicy({}, "allowlist");
|
||||
|
||||
expect(next.channels?.feishu?.groupPolicy).toBe("allowlist");
|
||||
expect(next.channels?.feishu?.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setAccountDmAllowFromForChannel", () => {
|
||||
it("writes account-scoped allowlist dm config", () => {
|
||||
const next = setAccountDmAllowFromForChannel({
|
||||
cfg: {},
|
||||
channel: "discord",
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
allowFrom: ["123"],
|
||||
});
|
||||
|
||||
expect(next.channels?.discord?.dmPolicy).toBe("allowlist");
|
||||
expect(next.channels?.discord?.allowFrom).toEqual(["123"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveGroupAllowlistWithLookupNotes", () => {
|
||||
it("returns resolved values when lookup succeeds", async () => {
|
||||
const prompter = createPrompter([]);
|
||||
await expect(
|
||||
resolveGroupAllowlistWithLookupNotes({
|
||||
label: "Discord channels",
|
||||
prompter,
|
||||
entries: ["general"],
|
||||
fallback: [],
|
||||
resolve: async () => ["guild/channel"],
|
||||
}),
|
||||
).resolves.toEqual(["guild/channel"]);
|
||||
expect(prompter.note).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("notes lookup failure and returns the fallback", async () => {
|
||||
const prompter = createPrompter([]);
|
||||
await expect(
|
||||
resolveGroupAllowlistWithLookupNotes({
|
||||
label: "Slack channels",
|
||||
prompter,
|
||||
entries: ["general"],
|
||||
fallback: ["general"],
|
||||
resolve: async () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(["general"]);
|
||||
expect(prompter.note).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAccountScopedAllowFromSection", () => {
|
||||
it("builds an account-scoped allowFrom section with shared apply wiring", async () => {
|
||||
const section = createAccountScopedAllowFromSection({
|
||||
channel: "discord",
|
||||
credentialInputKey: "token",
|
||||
message: "Discord allowFrom",
|
||||
placeholder: "@alice",
|
||||
invalidWithoutCredentialNote: "need ids",
|
||||
parseId: (value) => value.trim() || null,
|
||||
resolveEntries: async ({ entries }) =>
|
||||
entries.map((input) => ({ input, resolved: true, id: input.toUpperCase() })),
|
||||
});
|
||||
|
||||
expect(section.credentialInputKey).toBe("token");
|
||||
await expect(
|
||||
section.resolveEntries({
|
||||
cfg: {},
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
credentialValues: {},
|
||||
entries: ["alice"],
|
||||
}),
|
||||
).resolves.toEqual([{ input: "alice", resolved: true, id: "ALICE" }]);
|
||||
|
||||
const next = await section.apply({
|
||||
cfg: {},
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
allowFrom: ["123"],
|
||||
});
|
||||
|
||||
expect(next.channels?.discord?.dmPolicy).toBe("allowlist");
|
||||
expect(next.channels?.discord?.allowFrom).toEqual(["123"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAccountScopedGroupAccessSection", () => {
|
||||
it("builds group access with shared setPolicy and fallback lookup notes", async () => {
|
||||
const prompter = createPrompter([]);
|
||||
const section = createAccountScopedGroupAccessSection({
|
||||
channel: "slack",
|
||||
label: "Slack channels",
|
||||
placeholder: "#general",
|
||||
currentPolicy: () => "allowlist",
|
||||
currentEntries: () => [],
|
||||
updatePrompt: () => false,
|
||||
resolveAllowlist: async () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
fallbackResolved: (entries) => entries,
|
||||
applyAllowlist: ({ cfg, resolved, accountId }) =>
|
||||
patchChannelConfigForAccount({
|
||||
cfg,
|
||||
channel: "slack",
|
||||
accountId,
|
||||
patch: {
|
||||
channels: Object.fromEntries(resolved.map((entry) => [entry, { allow: true }])),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const policyNext = section.setPolicy({
|
||||
cfg: {},
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
policy: "open",
|
||||
});
|
||||
expect(policyNext.channels?.slack?.groupPolicy).toBe("open");
|
||||
|
||||
await expect(
|
||||
section.resolveAllowlist?.({
|
||||
cfg: {},
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
credentialValues: {},
|
||||
entries: ["general"],
|
||||
prompter,
|
||||
}),
|
||||
).resolves.toEqual(["general"]);
|
||||
expect(prompter.note).toHaveBeenCalledTimes(2);
|
||||
|
||||
const allowlistNext = section.applyAllowlist?.({
|
||||
cfg: {},
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
resolved: ["C123"],
|
||||
});
|
||||
expect(allowlistNext?.channels?.slack?.channels).toEqual({
|
||||
C123: { allow: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitSetupEntries", () => {
|
||||
it("splits comma/newline/semicolon input and trims blanks", () => {
|
||||
expect(splitSetupEntries(" alice, bob \ncarol; ;\n")).toEqual(["alice", "bob", "carol"]);
|
||||
@ -1060,6 +1511,39 @@ describe("parseSetupEntriesAllowingWildcard", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveEntriesWithOptionalToken", () => {
|
||||
it("returns unresolved entries when token is missing", async () => {
|
||||
await expect(
|
||||
resolveEntriesWithOptionalToken({
|
||||
entries: ["alice", "bob"],
|
||||
buildWithoutToken: (input) => ({ input, resolved: false, id: null }),
|
||||
resolveEntries: async () => {
|
||||
throw new Error("should not run");
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{ input: "alice", resolved: false, id: null },
|
||||
{ input: "bob", resolved: false, id: null },
|
||||
]);
|
||||
});
|
||||
|
||||
it("delegates to the resolver when token exists", async () => {
|
||||
await expect(
|
||||
resolveEntriesWithOptionalToken<{
|
||||
input: string;
|
||||
resolved: boolean;
|
||||
id: string | null;
|
||||
}>({
|
||||
token: "xoxb-test",
|
||||
entries: ["alice"],
|
||||
buildWithoutToken: (input) => ({ input, resolved: false, id: null }),
|
||||
resolveEntries: async ({ token, entries }) =>
|
||||
entries.map((input) => ({ input, resolved: true, id: `${token}:${input}` })),
|
||||
}),
|
||||
).resolves.toEqual([{ input: "alice", resolved: true, id: "xoxb-test:alice" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseMentionOrPrefixedId", () => {
|
||||
it("parses mention ids", () => {
|
||||
expect(
|
||||
|
||||
@ -11,7 +11,12 @@ import {
|
||||
moveSingleAccountChannelSectionToDefaultAccount,
|
||||
patchScopedAccountConfig,
|
||||
} from "./setup-helpers.js";
|
||||
import type { PromptAccountId, PromptAccountIdParams } from "./setup-wizard-types.js";
|
||||
import type {
|
||||
ChannelSetupDmPolicy,
|
||||
PromptAccountId,
|
||||
PromptAccountIdParams,
|
||||
} from "./setup-wizard-types.js";
|
||||
import type { ChannelSetupWizard } from "./setup-wizard.js";
|
||||
|
||||
export const promptAccountId: PromptAccountId = async (params: PromptAccountIdParams) => {
|
||||
const existingIds = params.listAccountIds(params.cfg);
|
||||
@ -192,14 +197,19 @@ export function setAccountAllowFromForChannel(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function patchTopLevelChannelConfig(params: {
|
||||
export function patchTopLevelChannelConfigSection(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
enabled?: boolean;
|
||||
clearFields?: string[];
|
||||
patch: Record<string, unknown>;
|
||||
}): OpenClawConfig {
|
||||
const channelConfig =
|
||||
(params.cfg.channels?.[params.channel] as Record<string, unknown> | undefined) ?? {};
|
||||
const channelConfig = {
|
||||
...(params.cfg.channels?.[params.channel] as Record<string, unknown> | undefined),
|
||||
};
|
||||
for (const field of params.clearFields ?? []) {
|
||||
delete channelConfig[field];
|
||||
}
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
@ -213,13 +223,46 @@ function patchTopLevelChannelConfig(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function patchNestedChannelConfigSection(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
section: string;
|
||||
enabled?: boolean;
|
||||
clearFields?: string[];
|
||||
patch: Record<string, unknown>;
|
||||
}): OpenClawConfig {
|
||||
const channelConfig = {
|
||||
...(params.cfg.channels?.[params.channel] as Record<string, unknown> | undefined),
|
||||
};
|
||||
const sectionConfig = {
|
||||
...(channelConfig[params.section] as Record<string, unknown> | undefined),
|
||||
};
|
||||
for (const field of params.clearFields ?? []) {
|
||||
delete sectionConfig[field];
|
||||
}
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
[params.channel]: {
|
||||
...channelConfig,
|
||||
...(params.enabled ? { enabled: true } : {}),
|
||||
[params.section]: {
|
||||
...sectionConfig,
|
||||
...params.patch,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function setTopLevelChannelAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
allowFrom: string[];
|
||||
enabled?: boolean;
|
||||
}): OpenClawConfig {
|
||||
return patchTopLevelChannelConfig({
|
||||
return patchTopLevelChannelConfigSection({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
enabled: params.enabled,
|
||||
@ -227,6 +270,22 @@ export function setTopLevelChannelAllowFrom(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export function setNestedChannelAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
section: string;
|
||||
allowFrom: string[];
|
||||
enabled?: boolean;
|
||||
}): OpenClawConfig {
|
||||
return patchNestedChannelConfigSection({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
section: params.section,
|
||||
enabled: params.enabled,
|
||||
patch: { allowFrom: params.allowFrom },
|
||||
});
|
||||
}
|
||||
|
||||
export function setTopLevelChannelDmPolicyWithAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
@ -241,7 +300,7 @@ export function setTopLevelChannelDmPolicyWithAllowFrom(params: {
|
||||
undefined;
|
||||
const allowFrom =
|
||||
params.dmPolicy === "open" ? addWildcardAllowFrom(existingAllowFrom) : undefined;
|
||||
return patchTopLevelChannelConfig({
|
||||
return patchTopLevelChannelConfigSection({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
patch: {
|
||||
@ -251,13 +310,43 @@ export function setTopLevelChannelDmPolicyWithAllowFrom(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export function setNestedChannelDmPolicyWithAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
section: string;
|
||||
dmPolicy: DmPolicy;
|
||||
getAllowFrom?: (cfg: OpenClawConfig) => Array<string | number> | undefined;
|
||||
enabled?: boolean;
|
||||
}): OpenClawConfig {
|
||||
const channelConfig =
|
||||
(params.cfg.channels?.[params.channel] as Record<string, unknown> | undefined) ?? {};
|
||||
const sectionConfig =
|
||||
(channelConfig[params.section] as Record<string, unknown> | undefined) ?? {};
|
||||
const existingAllowFrom =
|
||||
params.getAllowFrom?.(params.cfg) ??
|
||||
(sectionConfig.allowFrom as Array<string | number> | undefined) ??
|
||||
undefined;
|
||||
const allowFrom =
|
||||
params.dmPolicy === "open" ? addWildcardAllowFrom(existingAllowFrom) : undefined;
|
||||
return patchNestedChannelConfigSection({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
section: params.section,
|
||||
enabled: params.enabled,
|
||||
patch: {
|
||||
policy: params.dmPolicy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function setTopLevelChannelGroupPolicy(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
groupPolicy: GroupPolicy;
|
||||
enabled?: boolean;
|
||||
}): OpenClawConfig {
|
||||
return patchTopLevelChannelConfig({
|
||||
return patchTopLevelChannelConfigSection({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
enabled: params.enabled,
|
||||
@ -265,6 +354,129 @@ export function setTopLevelChannelGroupPolicy(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export function createTopLevelChannelDmPolicy(params: {
|
||||
label: string;
|
||||
channel: string;
|
||||
policyKey: string;
|
||||
allowFromKey: string;
|
||||
getCurrent: (cfg: OpenClawConfig) => DmPolicy;
|
||||
promptAllowFrom?: ChannelSetupDmPolicy["promptAllowFrom"];
|
||||
getAllowFrom?: (cfg: OpenClawConfig) => Array<string | number> | undefined;
|
||||
}): ChannelSetupDmPolicy {
|
||||
const setPolicy = createTopLevelChannelDmPolicySetter({
|
||||
channel: params.channel,
|
||||
getAllowFrom: params.getAllowFrom,
|
||||
});
|
||||
return {
|
||||
label: params.label,
|
||||
channel: params.channel,
|
||||
policyKey: params.policyKey,
|
||||
allowFromKey: params.allowFromKey,
|
||||
getCurrent: params.getCurrent,
|
||||
setPolicy,
|
||||
...(params.promptAllowFrom ? { promptAllowFrom: params.promptAllowFrom } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createNestedChannelDmPolicy(params: {
|
||||
label: string;
|
||||
channel: string;
|
||||
section: string;
|
||||
policyKey: string;
|
||||
allowFromKey: string;
|
||||
getCurrent: (cfg: OpenClawConfig) => DmPolicy;
|
||||
promptAllowFrom?: ChannelSetupDmPolicy["promptAllowFrom"];
|
||||
getAllowFrom?: (cfg: OpenClawConfig) => Array<string | number> | undefined;
|
||||
enabled?: boolean;
|
||||
}): ChannelSetupDmPolicy {
|
||||
const setPolicy = createNestedChannelDmPolicySetter({
|
||||
channel: params.channel,
|
||||
section: params.section,
|
||||
getAllowFrom: params.getAllowFrom,
|
||||
enabled: params.enabled,
|
||||
});
|
||||
return {
|
||||
label: params.label,
|
||||
channel: params.channel,
|
||||
policyKey: params.policyKey,
|
||||
allowFromKey: params.allowFromKey,
|
||||
getCurrent: params.getCurrent,
|
||||
setPolicy,
|
||||
...(params.promptAllowFrom ? { promptAllowFrom: params.promptAllowFrom } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createTopLevelChannelDmPolicySetter(params: {
|
||||
channel: string;
|
||||
getAllowFrom?: (cfg: OpenClawConfig) => Array<string | number> | undefined;
|
||||
}): (cfg: OpenClawConfig, dmPolicy: DmPolicy) => OpenClawConfig {
|
||||
return (cfg, dmPolicy) =>
|
||||
setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel: params.channel,
|
||||
dmPolicy,
|
||||
getAllowFrom: params.getAllowFrom,
|
||||
});
|
||||
}
|
||||
|
||||
export function createNestedChannelDmPolicySetter(params: {
|
||||
channel: string;
|
||||
section: string;
|
||||
getAllowFrom?: (cfg: OpenClawConfig) => Array<string | number> | undefined;
|
||||
enabled?: boolean;
|
||||
}): (cfg: OpenClawConfig, dmPolicy: DmPolicy) => OpenClawConfig {
|
||||
return (cfg, dmPolicy) =>
|
||||
setNestedChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel: params.channel,
|
||||
section: params.section,
|
||||
dmPolicy,
|
||||
getAllowFrom: params.getAllowFrom,
|
||||
enabled: params.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function createTopLevelChannelAllowFromSetter(params: {
|
||||
channel: string;
|
||||
enabled?: boolean;
|
||||
}): (cfg: OpenClawConfig, allowFrom: string[]) => OpenClawConfig {
|
||||
return (cfg, allowFrom) =>
|
||||
setTopLevelChannelAllowFrom({
|
||||
cfg,
|
||||
channel: params.channel,
|
||||
allowFrom,
|
||||
enabled: params.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function createNestedChannelAllowFromSetter(params: {
|
||||
channel: string;
|
||||
section: string;
|
||||
enabled?: boolean;
|
||||
}): (cfg: OpenClawConfig, allowFrom: string[]) => OpenClawConfig {
|
||||
return (cfg, allowFrom) =>
|
||||
setNestedChannelAllowFrom({
|
||||
cfg,
|
||||
channel: params.channel,
|
||||
section: params.section,
|
||||
allowFrom,
|
||||
enabled: params.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function createTopLevelChannelGroupPolicySetter(params: {
|
||||
channel: string;
|
||||
enabled?: boolean;
|
||||
}): (cfg: OpenClawConfig, groupPolicy: "open" | "allowlist" | "disabled") => OpenClawConfig {
|
||||
return (cfg, groupPolicy) =>
|
||||
setTopLevelChannelGroupPolicy({
|
||||
cfg,
|
||||
channel: params.channel,
|
||||
groupPolicy,
|
||||
enabled: params.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function setChannelDmPolicyWithAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: "imessage" | "signal" | "telegram";
|
||||
@ -339,6 +551,177 @@ export function setAccountGroupPolicyForChannel(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export function setAccountDmAllowFromForChannel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: "discord" | "slack";
|
||||
accountId: string;
|
||||
allowFrom: string[];
|
||||
}): OpenClawConfig {
|
||||
return patchChannelConfigForAccount({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
accountId: params.accountId,
|
||||
patch: { dmPolicy: "allowlist", allowFrom: params.allowFrom },
|
||||
});
|
||||
}
|
||||
|
||||
export function createLegacyCompatChannelDmPolicy(params: {
|
||||
label: string;
|
||||
channel: LegacyDmChannel;
|
||||
promptAllowFrom?: ChannelSetupDmPolicy["promptAllowFrom"];
|
||||
}): ChannelSetupDmPolicy {
|
||||
return {
|
||||
label: params.label,
|
||||
channel: params.channel,
|
||||
policyKey: `channels.${params.channel}.dmPolicy`,
|
||||
allowFromKey: `channels.${params.channel}.allowFrom`,
|
||||
getCurrent: (cfg) =>
|
||||
(
|
||||
cfg.channels?.[params.channel] as
|
||||
| {
|
||||
dmPolicy?: DmPolicy;
|
||||
dm?: { policy?: DmPolicy };
|
||||
}
|
||||
| undefined
|
||||
)?.dmPolicy ??
|
||||
(
|
||||
cfg.channels?.[params.channel] as
|
||||
| {
|
||||
dmPolicy?: DmPolicy;
|
||||
dm?: { policy?: DmPolicy };
|
||||
}
|
||||
| undefined
|
||||
)?.dm?.policy ??
|
||||
"pairing",
|
||||
setPolicy: (cfg, policy) =>
|
||||
setLegacyChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel: params.channel,
|
||||
dmPolicy: policy,
|
||||
}),
|
||||
...(params.promptAllowFrom ? { promptAllowFrom: params.promptAllowFrom } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveGroupAllowlistWithLookupNotes<TResolved>(params: {
|
||||
label: string;
|
||||
prompter: Pick<WizardPrompter, "note">;
|
||||
entries: string[];
|
||||
fallback: TResolved;
|
||||
resolve: () => Promise<TResolved>;
|
||||
}): Promise<TResolved> {
|
||||
try {
|
||||
return await params.resolve();
|
||||
} catch (error) {
|
||||
await noteChannelLookupFailure({
|
||||
prompter: params.prompter,
|
||||
label: params.label,
|
||||
error,
|
||||
});
|
||||
await noteChannelLookupSummary({
|
||||
prompter: params.prompter,
|
||||
label: params.label,
|
||||
resolvedSections: [],
|
||||
unresolved: params.entries,
|
||||
});
|
||||
return params.fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export function createAccountScopedAllowFromSection(params: {
|
||||
channel: "discord" | "slack";
|
||||
credentialInputKey?: NonNullable<ChannelSetupWizard["allowFrom"]>["credentialInputKey"];
|
||||
helpTitle?: string;
|
||||
helpLines?: string[];
|
||||
message: string;
|
||||
placeholder: string;
|
||||
invalidWithoutCredentialNote: string;
|
||||
parseId: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["parseId"]>;
|
||||
resolveEntries: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["resolveEntries"]>;
|
||||
}): NonNullable<ChannelSetupWizard["allowFrom"]> {
|
||||
return {
|
||||
...(params.helpTitle ? { helpTitle: params.helpTitle } : {}),
|
||||
...(params.helpLines ? { helpLines: params.helpLines } : {}),
|
||||
...(params.credentialInputKey ? { credentialInputKey: params.credentialInputKey } : {}),
|
||||
message: params.message,
|
||||
placeholder: params.placeholder,
|
||||
invalidWithoutCredentialNote: params.invalidWithoutCredentialNote,
|
||||
parseId: params.parseId,
|
||||
resolveEntries: params.resolveEntries,
|
||||
apply: ({ cfg, accountId, allowFrom }) =>
|
||||
setAccountDmAllowFromForChannel({
|
||||
cfg,
|
||||
channel: params.channel,
|
||||
accountId,
|
||||
allowFrom,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createAccountScopedGroupAccessSection<TResolved>(params: {
|
||||
channel: "discord" | "slack";
|
||||
label: string;
|
||||
placeholder: string;
|
||||
helpTitle?: string;
|
||||
helpLines?: string[];
|
||||
skipAllowlistEntries?: boolean;
|
||||
currentPolicy: NonNullable<ChannelSetupWizard["groupAccess"]>["currentPolicy"];
|
||||
currentEntries: NonNullable<ChannelSetupWizard["groupAccess"]>["currentEntries"];
|
||||
updatePrompt: NonNullable<ChannelSetupWizard["groupAccess"]>["updatePrompt"];
|
||||
resolveAllowlist?: NonNullable<
|
||||
NonNullable<ChannelSetupWizard["groupAccess"]>["resolveAllowlist"]
|
||||
>;
|
||||
fallbackResolved: (entries: string[]) => TResolved;
|
||||
applyAllowlist: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
resolved: TResolved;
|
||||
}) => OpenClawConfig;
|
||||
}): NonNullable<ChannelSetupWizard["groupAccess"]> {
|
||||
return {
|
||||
label: params.label,
|
||||
placeholder: params.placeholder,
|
||||
...(params.helpTitle ? { helpTitle: params.helpTitle } : {}),
|
||||
...(params.helpLines ? { helpLines: params.helpLines } : {}),
|
||||
...(params.skipAllowlistEntries ? { skipAllowlistEntries: true } : {}),
|
||||
currentPolicy: params.currentPolicy,
|
||||
currentEntries: params.currentEntries,
|
||||
updatePrompt: params.updatePrompt,
|
||||
setPolicy: ({ cfg, accountId, policy }) =>
|
||||
setAccountGroupPolicyForChannel({
|
||||
cfg,
|
||||
channel: params.channel,
|
||||
accountId,
|
||||
groupPolicy: policy,
|
||||
}),
|
||||
...(params.resolveAllowlist
|
||||
? {
|
||||
resolveAllowlist: ({ cfg, accountId, credentialValues, entries, prompter }) =>
|
||||
resolveGroupAllowlistWithLookupNotes({
|
||||
label: params.label,
|
||||
prompter,
|
||||
entries,
|
||||
fallback: params.fallbackResolved(entries),
|
||||
resolve: async () =>
|
||||
await params.resolveAllowlist!({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
applyAllowlist: ({ cfg, accountId, resolved }) =>
|
||||
params.applyAllowlist({
|
||||
cfg,
|
||||
accountId,
|
||||
resolved: resolved as TResolved,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
type AccountScopedChannel = "discord" | "slack" | "telegram" | "imessage" | "signal";
|
||||
type LegacyDmChannel = "discord" | "slack";
|
||||
|
||||
@ -753,6 +1136,22 @@ type AllowFromResolution = {
|
||||
id?: string | null;
|
||||
};
|
||||
|
||||
export async function resolveEntriesWithOptionalToken<TResult>(params: {
|
||||
token?: string | null;
|
||||
entries: string[];
|
||||
buildWithoutToken: (input: string) => TResult;
|
||||
resolveEntries: (params: { token: string; entries: string[] }) => Promise<TResult[]>;
|
||||
}): Promise<TResult[]> {
|
||||
const token = params.token?.trim();
|
||||
if (!token) {
|
||||
return params.entries.map(params.buildWithoutToken);
|
||||
}
|
||||
return await params.resolveEntries({
|
||||
token,
|
||||
entries: params.entries,
|
||||
});
|
||||
}
|
||||
|
||||
export async function promptResolvedAllowFrom(params: {
|
||||
prompter: WizardPrompter;
|
||||
existing: Array<string | number>;
|
||||
@ -838,3 +1237,41 @@ export async function promptLegacyChannelAllowFrom(params: {
|
||||
allowFrom: unique,
|
||||
});
|
||||
}
|
||||
|
||||
export async function promptLegacyChannelAllowFromForAccount<TAccount>(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: LegacyDmChannel;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
defaultAccountId: string;
|
||||
resolveAccount: (cfg: OpenClawConfig, accountId: string) => TAccount;
|
||||
resolveExisting: (account: TAccount, cfg: OpenClawConfig) => Array<string | number>;
|
||||
resolveToken: (account: TAccount) => string | null | undefined;
|
||||
noteTitle: string;
|
||||
noteLines: string[];
|
||||
message: string;
|
||||
placeholder: string;
|
||||
parseId: (value: string) => string | null;
|
||||
invalidWithoutTokenNote: string;
|
||||
resolveEntries: (params: { token: string; entries: string[] }) => Promise<AllowFromResolution[]>;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const accountId = resolveSetupAccountId({
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: params.defaultAccountId,
|
||||
});
|
||||
const account = params.resolveAccount(params.cfg, accountId);
|
||||
return await promptLegacyChannelAllowFrom({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
prompter: params.prompter,
|
||||
existing: params.resolveExisting(account, params.cfg),
|
||||
token: params.resolveToken(account),
|
||||
noteTitle: params.noteTitle,
|
||||
noteLines: params.noteLines,
|
||||
message: params.message,
|
||||
placeholder: params.placeholder,
|
||||
parseId: params.parseId,
|
||||
invalidWithoutTokenNote: params.invalidWithoutTokenNote,
|
||||
resolveEntries: params.resolveEntries,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createScopedAccountConfigAccessors,
|
||||
createScopedChannelConfigBase,
|
||||
createScopedDmSecurityResolver,
|
||||
createTopLevelChannelConfigBase,
|
||||
createHybridChannelConfigBase,
|
||||
mapAllowFromEntries,
|
||||
resolveOptionalConfigString,
|
||||
} from "./channel-config-helpers.js";
|
||||
@ -72,3 +76,188 @@ describe("createScopedAccountConfigAccessors", () => {
|
||||
expect(accessors.resolveDefaultTo).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createScopedChannelConfigBase", () => {
|
||||
it("wires shared account config CRUD through the section helper", () => {
|
||||
const base = createScopedChannelConfigBase({
|
||||
sectionKey: "demo",
|
||||
listAccountIds: () => ["default", "alt"],
|
||||
resolveAccount: (_cfg, accountId) => ({ accountId: accountId ?? "default" }),
|
||||
defaultAccountId: () => "default",
|
||||
clearBaseFields: ["token"],
|
||||
});
|
||||
|
||||
expect(base.listAccountIds({})).toEqual(["default", "alt"]);
|
||||
expect(base.resolveAccount({}, "alt")).toEqual({ accountId: "alt" });
|
||||
expect(base.defaultAccountId!({})).toBe("default");
|
||||
expect(
|
||||
base.setAccountEnabled!({
|
||||
cfg: {},
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
}).channels?.demo,
|
||||
).toEqual({ enabled: true });
|
||||
expect(
|
||||
base.deleteAccount!({
|
||||
cfg: {
|
||||
channels: {
|
||||
demo: {
|
||||
token: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
accountId: "default",
|
||||
}).channels,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createScopedDmSecurityResolver", () => {
|
||||
it("builds account-aware DM policy payloads", () => {
|
||||
const resolveDmPolicy = createScopedDmSecurityResolver<{
|
||||
accountId?: string | null;
|
||||
dmPolicy?: string;
|
||||
allowFrom?: string[];
|
||||
}>({
|
||||
channelKey: "demo",
|
||||
resolvePolicy: (account) => account.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => raw.toLowerCase(),
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveDmPolicy({
|
||||
cfg: {
|
||||
channels: {
|
||||
demo: {
|
||||
accounts: {
|
||||
alt: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountId: "alt",
|
||||
account: {
|
||||
accountId: "alt",
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["Owner"],
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
policy: "allowlist",
|
||||
allowFrom: ["Owner"],
|
||||
policyPath: "channels.demo.accounts.alt.dmPolicy",
|
||||
allowFromPath: "channels.demo.accounts.alt.",
|
||||
approveHint: "Approve via: openclaw pairing list demo / openclaw pairing approve demo <code>",
|
||||
normalizeEntry: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTopLevelChannelConfigBase", () => {
|
||||
it("wires top-level enable/delete semantics", () => {
|
||||
const base = createTopLevelChannelConfigBase({
|
||||
sectionKey: "demo",
|
||||
resolveAccount: () => ({ accountId: "default" }),
|
||||
});
|
||||
|
||||
expect(base.listAccountIds({})).toEqual(["default"]);
|
||||
expect(base.defaultAccountId!({})).toBe("default");
|
||||
expect(
|
||||
base.setAccountEnabled!({
|
||||
cfg: {},
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
}).channels?.demo,
|
||||
).toEqual({ enabled: true });
|
||||
expect(
|
||||
base.deleteAccount!({
|
||||
cfg: {
|
||||
channels: {
|
||||
demo: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
accountId: "default",
|
||||
}).channels,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createHybridChannelConfigBase", () => {
|
||||
it("writes default account enable at the channel root and named accounts under accounts", () => {
|
||||
const base = createHybridChannelConfigBase({
|
||||
sectionKey: "demo",
|
||||
listAccountIds: () => ["default", "alt"],
|
||||
resolveAccount: (_cfg, accountId) => ({ accountId: accountId ?? "default" }),
|
||||
defaultAccountId: () => "default",
|
||||
clearBaseFields: ["token"],
|
||||
});
|
||||
|
||||
expect(
|
||||
base.setAccountEnabled!({
|
||||
cfg: {
|
||||
channels: {
|
||||
demo: {
|
||||
accounts: {
|
||||
alt: { enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
}).channels?.demo,
|
||||
).toEqual({
|
||||
accounts: {
|
||||
alt: { enabled: false },
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
expect(
|
||||
base.setAccountEnabled!({
|
||||
cfg: {},
|
||||
accountId: "alt",
|
||||
enabled: true,
|
||||
}).channels?.demo,
|
||||
).toEqual({
|
||||
accounts: {
|
||||
alt: { enabled: true },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("can preserve the section when deleting the default account", () => {
|
||||
const base = createHybridChannelConfigBase({
|
||||
sectionKey: "demo",
|
||||
listAccountIds: () => ["default", "alt"],
|
||||
resolveAccount: (_cfg, accountId) => ({ accountId: accountId ?? "default" }),
|
||||
defaultAccountId: () => "default",
|
||||
clearBaseFields: ["token", "name"],
|
||||
preserveSectionOnDefaultDelete: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
base.deleteAccount!({
|
||||
cfg: {
|
||||
channels: {
|
||||
demo: {
|
||||
token: "secret",
|
||||
name: "bot",
|
||||
accounts: {
|
||||
alt: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountId: "default",
|
||||
}).channels?.demo,
|
||||
).toEqual({
|
||||
accounts: {
|
||||
alt: { enabled: true },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,7 +14,7 @@ import { normalizeWhatsAppAllowFromEntries } from "../channels/plugins/normalize
|
||||
import { getChannelPlugin } from "../channels/plugins/registry.js";
|
||||
import type { ChannelConfigAdapter } from "../channels/plugins/types.adapters.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||
|
||||
/** Coerce mixed allowlist config values into plain strings without trimming or deduping. */
|
||||
@ -116,6 +116,178 @@ export function createScopedChannelConfigBase<
|
||||
};
|
||||
}
|
||||
|
||||
function setTopLevelChannelEnabledInConfigSection<Config extends OpenClawConfig>(params: {
|
||||
cfg: Config;
|
||||
sectionKey: string;
|
||||
enabled: boolean;
|
||||
}): Config {
|
||||
const section = params.cfg.channels?.[params.sectionKey] as Record<string, unknown> | undefined;
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
[params.sectionKey]: {
|
||||
...section,
|
||||
enabled: params.enabled,
|
||||
},
|
||||
},
|
||||
} as Config;
|
||||
}
|
||||
|
||||
function removeTopLevelChannelConfigSection<Config extends OpenClawConfig>(params: {
|
||||
cfg: Config;
|
||||
sectionKey: string;
|
||||
}): Config {
|
||||
const nextChannels = { ...params.cfg.channels } as Record<string, unknown>;
|
||||
delete nextChannels[params.sectionKey];
|
||||
const nextCfg = { ...params.cfg };
|
||||
if (Object.keys(nextChannels).length > 0) {
|
||||
nextCfg.channels = nextChannels as Config["channels"];
|
||||
} else {
|
||||
delete nextCfg.channels;
|
||||
}
|
||||
return nextCfg;
|
||||
}
|
||||
|
||||
function clearTopLevelChannelConfigFields<Config extends OpenClawConfig>(params: {
|
||||
cfg: Config;
|
||||
sectionKey: string;
|
||||
clearBaseFields: string[];
|
||||
}): Config {
|
||||
const section = params.cfg.channels?.[params.sectionKey] as Record<string, unknown> | undefined;
|
||||
if (!section) {
|
||||
return params.cfg;
|
||||
}
|
||||
const nextSection = { ...section };
|
||||
for (const field of params.clearBaseFields) {
|
||||
delete nextSection[field];
|
||||
}
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
[params.sectionKey]: nextSection,
|
||||
},
|
||||
} as Config;
|
||||
}
|
||||
|
||||
/** Build CRUD/config helpers for top-level single-account channels. */
|
||||
export function createTopLevelChannelConfigBase<
|
||||
ResolvedAccount,
|
||||
Config extends OpenClawConfig = OpenClawConfig,
|
||||
>(params: {
|
||||
sectionKey: string;
|
||||
resolveAccount: (cfg: Config) => ResolvedAccount;
|
||||
listAccountIds?: (cfg: Config) => string[];
|
||||
defaultAccountId?: (cfg: Config) => string;
|
||||
inspectAccount?: (cfg: Config) => unknown;
|
||||
deleteMode?: "remove-section" | "clear-fields";
|
||||
clearBaseFields?: string[];
|
||||
}): Pick<
|
||||
ChannelConfigAdapter<ResolvedAccount>,
|
||||
| "listAccountIds"
|
||||
| "resolveAccount"
|
||||
| "inspectAccount"
|
||||
| "defaultAccountId"
|
||||
| "setAccountEnabled"
|
||||
| "deleteAccount"
|
||||
> {
|
||||
return {
|
||||
listAccountIds: (cfg) => params.listAccountIds?.(cfg as Config) ?? [DEFAULT_ACCOUNT_ID],
|
||||
resolveAccount: (cfg) => params.resolveAccount(cfg as Config),
|
||||
inspectAccount: params.inspectAccount
|
||||
? (cfg) => params.inspectAccount?.(cfg as Config)
|
||||
: undefined,
|
||||
defaultAccountId: (cfg) => params.defaultAccountId?.(cfg as Config) ?? DEFAULT_ACCOUNT_ID,
|
||||
setAccountEnabled: ({ cfg, enabled }) =>
|
||||
setTopLevelChannelEnabledInConfigSection({
|
||||
cfg: cfg as Config,
|
||||
sectionKey: params.sectionKey,
|
||||
enabled,
|
||||
}),
|
||||
deleteAccount: ({ cfg }) =>
|
||||
params.deleteMode === "clear-fields"
|
||||
? clearTopLevelChannelConfigFields({
|
||||
cfg: cfg as Config,
|
||||
sectionKey: params.sectionKey,
|
||||
clearBaseFields: params.clearBaseFields ?? [],
|
||||
})
|
||||
: removeTopLevelChannelConfigSection({
|
||||
cfg: cfg as Config,
|
||||
sectionKey: params.sectionKey,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Build CRUD/config helpers for channels where the default account lives at channel root and named accounts live under `accounts`. */
|
||||
export function createHybridChannelConfigBase<
|
||||
ResolvedAccount,
|
||||
Config extends OpenClawConfig = OpenClawConfig,
|
||||
>(params: {
|
||||
sectionKey: string;
|
||||
listAccountIds: (cfg: Config) => string[];
|
||||
resolveAccount: (cfg: Config, accountId?: string | null) => ResolvedAccount;
|
||||
defaultAccountId: (cfg: Config) => string;
|
||||
inspectAccount?: (cfg: Config, accountId?: string | null) => unknown;
|
||||
clearBaseFields: string[];
|
||||
preserveSectionOnDefaultDelete?: boolean;
|
||||
}): Pick<
|
||||
ChannelConfigAdapter<ResolvedAccount>,
|
||||
| "listAccountIds"
|
||||
| "resolveAccount"
|
||||
| "inspectAccount"
|
||||
| "defaultAccountId"
|
||||
| "setAccountEnabled"
|
||||
| "deleteAccount"
|
||||
> {
|
||||
return {
|
||||
listAccountIds: (cfg) => params.listAccountIds(cfg as Config),
|
||||
resolveAccount: (cfg, accountId) => params.resolveAccount(cfg as Config, accountId),
|
||||
inspectAccount: params.inspectAccount
|
||||
? (cfg, accountId) => params.inspectAccount?.(cfg as Config, accountId)
|
||||
: undefined,
|
||||
defaultAccountId: (cfg) => params.defaultAccountId(cfg as Config),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
||||
if (normalizeAccountId(accountId) === DEFAULT_ACCOUNT_ID) {
|
||||
return setTopLevelChannelEnabledInConfigSection({
|
||||
cfg: cfg as Config,
|
||||
sectionKey: params.sectionKey,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
return setAccountEnabledInConfigSection({
|
||||
cfg: cfg as Config,
|
||||
sectionKey: params.sectionKey,
|
||||
accountId,
|
||||
enabled,
|
||||
});
|
||||
},
|
||||
deleteAccount: ({ cfg, accountId }) => {
|
||||
if (normalizeAccountId(accountId) === DEFAULT_ACCOUNT_ID) {
|
||||
if (params.preserveSectionOnDefaultDelete) {
|
||||
return clearTopLevelChannelConfigFields({
|
||||
cfg: cfg as Config,
|
||||
sectionKey: params.sectionKey,
|
||||
clearBaseFields: params.clearBaseFields,
|
||||
});
|
||||
}
|
||||
return deleteAccountFromConfigSection({
|
||||
cfg: cfg as Config,
|
||||
sectionKey: params.sectionKey,
|
||||
accountId,
|
||||
clearBaseFields: params.clearBaseFields,
|
||||
});
|
||||
}
|
||||
return deleteAccountFromConfigSection({
|
||||
cfg: cfg as Config,
|
||||
sectionKey: params.sectionKey,
|
||||
accountId,
|
||||
clearBaseFields: params.clearBaseFields,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Convert account-specific DM security fields into the shared runtime policy resolver shape. */
|
||||
export function createScopedDmSecurityResolver<
|
||||
ResolvedAccount extends { accountId?: string | null },
|
||||
|
||||
@ -25,9 +25,11 @@ export { createPluginRuntimeStore } from "./runtime-store.js";
|
||||
export { KeyedAsyncQueue } from "./keyed-async-queue.js";
|
||||
|
||||
export {
|
||||
createHybridChannelConfigBase,
|
||||
createScopedAccountConfigAccessors,
|
||||
createScopedChannelConfigBase,
|
||||
createScopedDmSecurityResolver,
|
||||
createTopLevelChannelConfigBase,
|
||||
mapAllowFromEntries,
|
||||
} from "./channel-config-helpers.js";
|
||||
export { formatAllowFromLowercase, formatNormalizedAllowFromEntries } from "./allow-from.js";
|
||||
|
||||
@ -33,6 +33,16 @@ export {
|
||||
export {
|
||||
addWildcardAllowFrom,
|
||||
buildSingleChannelSecretPromptState,
|
||||
createAccountScopedAllowFromSection,
|
||||
createAccountScopedGroupAccessSection,
|
||||
createLegacyCompatChannelDmPolicy,
|
||||
createNestedChannelAllowFromSetter,
|
||||
createNestedChannelDmPolicy,
|
||||
createNestedChannelDmPolicySetter,
|
||||
createTopLevelChannelAllowFromSetter,
|
||||
createTopLevelChannelDmPolicy,
|
||||
createTopLevelChannelDmPolicySetter,
|
||||
createTopLevelChannelGroupPolicySetter,
|
||||
mergeAllowFromEntries,
|
||||
normalizeAllowFromEntries,
|
||||
noteChannelLookupFailure,
|
||||
@ -40,16 +50,24 @@ export {
|
||||
parseMentionOrPrefixedId,
|
||||
parseSetupEntriesAllowingWildcard,
|
||||
parseSetupEntriesWithParser,
|
||||
patchNestedChannelConfigSection,
|
||||
patchTopLevelChannelConfigSection,
|
||||
patchChannelConfigForAccount,
|
||||
promptLegacyChannelAllowFrom,
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
promptParsedAllowFromForScopedChannel,
|
||||
promptSingleChannelSecretInput,
|
||||
promptResolvedAllowFrom,
|
||||
resolveEntriesWithOptionalToken,
|
||||
resolveSetupAccountId,
|
||||
resolveGroupAllowlistWithLookupNotes,
|
||||
runSingleChannelSecretStep,
|
||||
setAccountDmAllowFromForChannel,
|
||||
setAccountGroupPolicyForChannel,
|
||||
setChannelDmPolicyWithAllowFrom,
|
||||
setLegacyChannelDmPolicyWithAllowFrom,
|
||||
setNestedChannelAllowFrom,
|
||||
setNestedChannelDmPolicyWithAllowFrom,
|
||||
setSetupChannelEnabled,
|
||||
setTopLevelChannelAllowFrom,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
|
||||
@ -97,8 +97,18 @@ describe("plugin-sdk subpath exports", () => {
|
||||
|
||||
it("exports shared setup helpers from the dedicated subpath", () => {
|
||||
expect(typeof setupSdk.DEFAULT_ACCOUNT_ID).toBe("string");
|
||||
expect(typeof setupSdk.createAccountScopedAllowFromSection).toBe("function");
|
||||
expect(typeof setupSdk.createAccountScopedGroupAccessSection).toBe("function");
|
||||
expect(typeof setupSdk.createLegacyCompatChannelDmPolicy).toBe("function");
|
||||
expect(typeof setupSdk.createNestedChannelDmPolicy).toBe("function");
|
||||
expect(typeof setupSdk.createTopLevelChannelDmPolicy).toBe("function");
|
||||
expect(typeof setupSdk.createTopLevelChannelDmPolicySetter).toBe("function");
|
||||
expect(typeof setupSdk.formatDocsLink).toBe("function");
|
||||
expect(typeof setupSdk.mergeAllowFromEntries).toBe("function");
|
||||
expect(typeof setupSdk.patchNestedChannelConfigSection).toBe("function");
|
||||
expect(typeof setupSdk.patchTopLevelChannelConfigSection).toBe("function");
|
||||
expect(typeof setupSdk.resolveGroupAllowlistWithLookupNotes).toBe("function");
|
||||
expect(typeof setupSdk.setAccountDmAllowFromForChannel).toBe("function");
|
||||
expect(typeof setupSdk.setTopLevelChannelDmPolicyWithAllowFrom).toBe("function");
|
||||
expect(typeof setupSdk.formatResolvedUnresolvedNote).toBe("function");
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user