refactor(discord): split component auth helpers

This commit is contained in:
Peter Steinberger
2026-04-29 16:56:12 +01:00
parent 334f4624e0
commit e8b82d1cf9
12 changed files with 646 additions and 690 deletions

View File

@@ -1,3 +1,3 @@
// Keep bundled registration fast: runtime wiring only needs the store setter,
// while runtime-api.js remains the broad compatibility barrel.
// while runtime-api.js remains the broad runtime surface.
export { setDiscordRuntime } from "./src/runtime.js";

View File

@@ -32,6 +32,7 @@ import {
resolveEventCoverImage,
} from "../send.js";
import {
createDiscordActionOptions,
readDiscordChannelCreateParams,
readDiscordChannelEditParams,
readDiscordChannelMoveParams,
@@ -70,7 +71,7 @@ type DiscordRoleMutation = (
) => Promise<unknown>;
async function runRoleMutation(params: {
cfgOptions: { cfg: OpenClawConfig };
cfg: OpenClawConfig;
accountId?: string;
values: Record<string, unknown>;
mutate: DiscordRoleMutation;
@@ -80,10 +81,7 @@ async function runRoleMutation(params: {
const roleId = readStringParam(params.values, "roleId", { required: true });
await params.mutate(
{ guildId, userId, roleId },
{
...params.cfgOptions,
...(params.accountId ? { accountId: params.accountId } : {}),
},
createDiscordActionOptions({ cfg: params.cfg, accountId: params.accountId }),
);
}
@@ -105,12 +103,8 @@ export async function handleDiscordGuildAction(
if (!cfg) {
throw new Error("Discord guild actions require a resolved runtime config.");
}
const cfgOptions = { cfg };
const withOpts = (extra?: Record<string, unknown>) => ({
...cfgOptions,
...(accountId ? { accountId } : {}),
...extra,
});
const withOpts = (extra?: Record<string, unknown>) =>
createDiscordActionOptions({ cfg, accountId, extra });
switch (action) {
case "memberInfo": {
if (!isActionEnabled("memberInfo")) {
@@ -123,12 +117,11 @@ export async function handleDiscordGuildAction(
required: true,
});
const effectiveAccountId = accountId ?? resolveDefaultDiscordAccountId(cfg);
const member = effectiveAccountId
? await discordGuildActionRuntime.fetchMemberInfoDiscord(guildId, userId, {
...cfgOptions,
accountId: effectiveAccountId,
})
: await discordGuildActionRuntime.fetchMemberInfoDiscord(guildId, userId, cfgOptions);
const member = await discordGuildActionRuntime.fetchMemberInfoDiscord(
guildId,
userId,
createDiscordActionOptions({ cfg, accountId: effectiveAccountId }),
);
const presence = getPresence(effectiveAccountId, userId);
const activities = presence?.activities ?? undefined;
const status = presence?.status ?? undefined;
@@ -209,7 +202,7 @@ export async function handleDiscordGuildAction(
throw new Error("Discord role changes are disabled.");
}
await runRoleMutation({
cfgOptions,
cfg,
accountId,
values: params,
mutate: discordGuildActionRuntime.addRoleDiscord,
@@ -221,7 +214,7 @@ export async function handleDiscordGuildAction(
throw new Error("Discord role changes are disabled.");
}
await runRoleMutation({
cfgOptions,
cfg,
accountId,
values: params,
mutate: discordGuildActionRuntime.removeRoleDiscord,

View File

@@ -44,6 +44,7 @@ import {
type DiscordSendEmbeds,
} from "../send.shared.js";
import { resolveDiscordChannelId } from "../targets.js";
import { createDiscordActionOptions } from "./runtime.shared.js";
export const discordMessagingActionRuntime = {
createThreadDiscord,
@@ -135,6 +136,8 @@ export async function handleDiscordMessagingAction(
throw new Error("Discord messaging actions require a resolved runtime config.");
}
const cfgOptions = { cfg };
const withOpts = (extra?: Record<string, unknown>) =>
createDiscordActionOptions({ cfg, accountId, extra });
const resolvedReactionAccountId = accountId ?? resolveDefaultDiscordAccountId(cfg);
const resolveReactionChannelId = async () => {
const target =
@@ -227,11 +230,7 @@ export async function handleDiscordMessagingAction(
required: true,
label: "stickerIds",
});
await discordMessagingActionRuntime.sendStickerDiscord(to, stickerIds, {
...cfgOptions,
...(accountId ? { accountId } : {}),
content,
});
await discordMessagingActionRuntime.sendStickerDiscord(to, stickerIds, withOpts({ content }));
return jsonResult({ ok: true });
}
case "poll": {
@@ -253,7 +252,7 @@ export async function handleDiscordMessagingAction(
await discordMessagingActionRuntime.sendPollDiscord(
to,
{ question, options: answers, maxSelections, durationHours },
{ ...cfgOptions, ...(accountId ? { accountId } : {}), content },
withOpts({ content }),
);
return jsonResult({ ok: true });
}
@@ -262,12 +261,10 @@ export async function handleDiscordMessagingAction(
throw new Error("Discord permissions are disabled.");
}
const channelId = resolveChannelId();
const permissions = accountId
? await discordMessagingActionRuntime.fetchChannelPermissionsDiscord(channelId, {
...cfgOptions,
accountId,
})
: await discordMessagingActionRuntime.fetchChannelPermissionsDiscord(channelId, cfgOptions);
const permissions = await discordMessagingActionRuntime.fetchChannelPermissionsDiscord(
channelId,
withOpts(),
);
return jsonResult({ ok: true, permissions });
}
case "fetchMessage": {
@@ -289,12 +286,11 @@ export async function handleDiscordMessagingAction(
"Discord message fetch requires guildId, channelId, and messageId (or a valid messageLink).",
);
}
const message = accountId
? await discordMessagingActionRuntime.fetchMessageDiscord(channelId, messageId, {
...cfgOptions,
accountId,
})
: await discordMessagingActionRuntime.fetchMessageDiscord(channelId, messageId, cfgOptions);
const message = await discordMessagingActionRuntime.fetchMessageDiscord(
channelId,
messageId,
withOpts(),
);
return jsonResult({
ok: true,
message: normalizeMessage(message),
@@ -314,12 +310,11 @@ export async function handleDiscordMessagingAction(
after: readStringParam(params, "after"),
around: readStringParam(params, "around"),
};
const messages = accountId
? await discordMessagingActionRuntime.readMessagesDiscord(channelId, query, {
...cfgOptions,
accountId,
})
: await discordMessagingActionRuntime.readMessagesDiscord(channelId, query, cfgOptions);
const messages = await discordMessagingActionRuntime.readMessagesDiscord(
channelId,
query,
withOpts(),
);
return jsonResult({
ok: true,
messages: messages.map((message) => normalizeMessage(message)),
@@ -372,8 +367,7 @@ export async function handleDiscordMessagingAction(
to,
payload,
{
...cfgOptions,
...(accountId ? { accountId } : {}),
...withOpts(),
silent,
replyTo: replyTo ?? undefined,
sessionKey: sessionKey ?? undefined,
@@ -399,8 +393,7 @@ export async function handleDiscordMessagingAction(
}
assertMediaNotDataUrl(mediaUrl);
const result = await discordMessagingActionRuntime.sendVoiceMessageDiscord(to, mediaUrl, {
...cfgOptions,
...(accountId ? { accountId } : {}),
...withOpts(),
replyTo,
silent,
});
@@ -408,8 +401,7 @@ export async function handleDiscordMessagingAction(
}
const result = await discordMessagingActionRuntime.sendMessageDiscord(to, content ?? "", {
...cfgOptions,
...(accountId ? { accountId } : {}),
...withOpts(),
mediaUrl,
filename: filename ?? undefined,
mediaLocalRoots: options?.mediaLocalRoots,
@@ -432,19 +424,12 @@ export async function handleDiscordMessagingAction(
const content = readStringParam(params, "content", {
required: true,
});
const message = accountId
? await discordMessagingActionRuntime.editMessageDiscord(
channelId,
messageId,
{ content },
{ ...cfgOptions, accountId },
)
: await discordMessagingActionRuntime.editMessageDiscord(
channelId,
messageId,
{ content },
cfgOptions,
);
const message = await discordMessagingActionRuntime.editMessageDiscord(
channelId,
messageId,
{ content },
withOpts(),
);
return jsonResult({ ok: true, message });
}
case "deleteMessage": {
@@ -455,14 +440,7 @@ export async function handleDiscordMessagingAction(
const messageId = readStringParam(params, "messageId", {
required: true,
});
if (accountId) {
await discordMessagingActionRuntime.deleteMessageDiscord(channelId, messageId, {
...cfgOptions,
accountId,
});
} else {
await discordMessagingActionRuntime.deleteMessageDiscord(channelId, messageId, cfgOptions);
}
await discordMessagingActionRuntime.deleteMessageDiscord(channelId, messageId, withOpts());
return jsonResult({ ok: true });
}
case "threadCreate": {
@@ -482,12 +460,11 @@ export async function handleDiscordMessagingAction(
content,
appliedTags: appliedTags ?? undefined,
};
const thread = accountId
? await discordMessagingActionRuntime.createThreadDiscord(channelId, payload, {
...cfgOptions,
accountId,
})
: await discordMessagingActionRuntime.createThreadDiscord(channelId, payload, cfgOptions);
const thread = await discordMessagingActionRuntime.createThreadDiscord(
channelId,
payload,
withOpts(),
);
return jsonResult({ ok: true, thread });
}
case "threadList": {
@@ -501,27 +478,16 @@ export async function handleDiscordMessagingAction(
const includeArchived = readBooleanParam(params, "includeArchived");
const before = readStringParam(params, "before");
const limit = readNumberParam(params, "limit");
const threads = accountId
? await discordMessagingActionRuntime.listThreadsDiscord(
{
guildId,
channelId,
includeArchived,
before,
limit,
},
{ ...cfgOptions, accountId },
)
: await discordMessagingActionRuntime.listThreadsDiscord(
{
guildId,
channelId,
includeArchived,
before,
limit,
},
cfgOptions,
);
const threads = await discordMessagingActionRuntime.listThreadsDiscord(
{
guildId,
channelId,
includeArchived,
before,
limit,
},
withOpts(),
);
return jsonResult({ ok: true, threads });
}
case "threadReply": {
@@ -538,8 +504,7 @@ export async function handleDiscordMessagingAction(
`channel:${channelId}`,
content,
{
...cfgOptions,
...(accountId ? { accountId } : {}),
...withOpts(),
mediaUrl,
mediaLocalRoots: options?.mediaLocalRoots,
mediaReadFile: options?.mediaReadFile,
@@ -556,14 +521,7 @@ export async function handleDiscordMessagingAction(
const messageId = readStringParam(params, "messageId", {
required: true,
});
if (accountId) {
await discordMessagingActionRuntime.pinMessageDiscord(channelId, messageId, {
...cfgOptions,
accountId,
});
} else {
await discordMessagingActionRuntime.pinMessageDiscord(channelId, messageId, cfgOptions);
}
await discordMessagingActionRuntime.pinMessageDiscord(channelId, messageId, withOpts());
return jsonResult({ ok: true });
}
case "unpinMessage": {
@@ -574,14 +532,7 @@ export async function handleDiscordMessagingAction(
const messageId = readStringParam(params, "messageId", {
required: true,
});
if (accountId) {
await discordMessagingActionRuntime.unpinMessageDiscord(channelId, messageId, {
...cfgOptions,
accountId,
});
} else {
await discordMessagingActionRuntime.unpinMessageDiscord(channelId, messageId, cfgOptions);
}
await discordMessagingActionRuntime.unpinMessageDiscord(channelId, messageId, withOpts());
return jsonResult({ ok: true });
}
case "listPins": {
@@ -589,12 +540,7 @@ export async function handleDiscordMessagingAction(
throw new Error("Discord pins are disabled.");
}
const channelId = resolveChannelId();
const pins = accountId
? await discordMessagingActionRuntime.listPinsDiscord(channelId, {
...cfgOptions,
accountId,
})
: await discordMessagingActionRuntime.listPinsDiscord(channelId, cfgOptions);
const pins = await discordMessagingActionRuntime.listPinsDiscord(channelId, withOpts());
return jsonResult({ ok: true, pins: pins.map((pin) => normalizeMessage(pin)) });
}
case "searchMessages": {
@@ -614,27 +560,16 @@ export async function handleDiscordMessagingAction(
const limit = readNumberParam(params, "limit");
const channelIdList = [...(channelIds ?? []), ...(channelId ? [channelId] : [])];
const authorIdList = [...(authorIds ?? []), ...(authorId ? [authorId] : [])];
const results = accountId
? await discordMessagingActionRuntime.searchMessagesDiscord(
{
guildId,
content,
channelIds: channelIdList.length ? channelIdList : undefined,
authorIds: authorIdList.length ? authorIdList : undefined,
limit,
},
{ ...cfgOptions, accountId },
)
: await discordMessagingActionRuntime.searchMessagesDiscord(
{
guildId,
content,
channelIds: channelIdList.length ? channelIdList : undefined,
authorIds: authorIdList.length ? authorIdList : undefined,
limit,
},
cfgOptions,
);
const results = await discordMessagingActionRuntime.searchMessagesDiscord(
{
guildId,
content,
channelIds: channelIdList.length ? channelIdList : undefined,
authorIds: authorIdList.length ? authorIdList : undefined,
limit,
},
withOpts(),
);
if (!results || typeof results !== "object") {
return jsonResult({ ok: true, results });
}

View File

@@ -17,6 +17,7 @@ import {
readDiscordModerationCommand,
requiredGuildPermissionForModerationAction,
} from "./runtime.moderation-shared.js";
import { createDiscordActionOptions } from "./runtime.shared.js";
export const discordModerationActionRuntime = {
banMemberDiscord,
@@ -30,7 +31,7 @@ async function verifySenderModerationPermission(params: {
senderUserId?: string;
requiredPermission: bigint;
accountId?: string;
cfgOptions: { cfg: OpenClawConfig };
cfg: OpenClawConfig;
}) {
// CLI/manual flows may not have sender context; enforce only when present.
if (!params.senderUserId) {
@@ -40,10 +41,7 @@ async function verifySenderModerationPermission(params: {
params.guildId,
params.senderUserId,
[params.requiredPermission],
{
...params.cfgOptions,
...(params.accountId ? { accountId: params.accountId } : {}),
},
createDiscordActionOptions({ cfg: params.cfg, accountId: params.accountId }),
);
if (!hasPermission) {
throw new Error("Sender does not have required permissions for this moderation action.");
@@ -65,20 +63,16 @@ export async function handleDiscordModerationAction(
if (!cfg) {
throw new Error("Discord moderation actions require a resolved runtime config.");
}
const cfgOptions = { cfg };
const command = readDiscordModerationCommand(action, params);
const accountId = readStringParam(params, "accountId");
const command = readDiscordModerationCommand(action, params);
const senderUserId = readStringParam(params, "senderUserId");
const withOpts = () => ({
...cfgOptions,
...(accountId ? { accountId } : {}),
});
const withOpts = () => createDiscordActionOptions({ cfg, accountId });
await verifySenderModerationPermission({
guildId: command.guildId,
senderUserId,
requiredPermission: requiredGuildPermissionForModerationAction(command.action),
accountId,
cfgOptions,
cfg,
});
switch (command.action) {
case "timeout": {

View File

@@ -1,4 +1,5 @@
import { parseAvailableTags, readNumberParam, readStringParam } from "../runtime-api.js";
import type { OpenClawConfig } from "../runtime-api.js";
import type {
DiscordChannelCreate,
DiscordChannelEdit,
@@ -24,6 +25,20 @@ function readDiscordBooleanParam(
return typeof params[key] === "boolean" ? params[key] : undefined;
}
export function createDiscordActionOptions<
T extends Record<string, unknown> = Record<string, never>,
>(params: {
cfg: OpenClawConfig;
accountId?: string;
extra?: T;
}): { cfg: OpenClawConfig; accountId?: string } & T {
return {
cfg: params.cfg,
...(params.accountId ? { accountId: params.accountId } : {}),
...(params.extra ?? ({} as T)),
};
}
export function readDiscordChannelCreateParams(
params: Record<string, unknown>,
): DiscordChannelCreate {

View File

@@ -1,514 +1,8 @@
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
import { resolveCommandAuthorizedFromAuthorizers } from "openclaw/plugin-sdk/command-auth-native";
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/runtime-group-policy";
import type { DiscordComponentEntry } from "../components.js";
import {
resolveComponentInteractionContext,
resolveDiscordChannelContext,
} from "./agent-components-context.js";
import {
readStoreAllowFromForDmPolicy,
upsertChannelPairingRequest,
} from "./agent-components-helpers.runtime.js";
import {
type AgentComponentContext,
type AgentComponentInteraction,
type ComponentInteractionContext,
type DiscordChannelContext,
type DiscordUser,
} from "./agent-components.types.js";
import {
isDiscordGroupAllowedByPolicy,
normalizeDiscordAllowList,
resolveDiscordAllowListMatch,
resolveDiscordChannelConfigWithFallback,
resolveDiscordGuildEntry,
resolveDiscordMemberAccessState,
resolveDiscordOwnerAccess,
resolveGroupDmAllow,
} from "./allow-list.js";
import { formatDiscordUserTag } from "./format.js";
async function replySilently(
interaction: AgentComponentInteraction,
params: { content: string; ephemeral?: boolean },
) {
try {
await interaction.reply(params);
} catch {}
}
export async function ensureGuildComponentMemberAllowed(params: {
interaction: AgentComponentInteraction;
guildInfo: ReturnType<typeof resolveDiscordGuildEntry>;
channelId: string;
rawGuildId: string | undefined;
channelCtx: DiscordChannelContext;
memberRoleIds: string[];
user: DiscordUser;
replyOpts: { ephemeral?: boolean };
componentLabel: string;
unauthorizedReply: string;
allowNameMatching: boolean;
groupPolicy: "open" | "disabled" | "allowlist";
}) {
const {
interaction,
guildInfo,
channelId,
rawGuildId,
channelCtx,
memberRoleIds,
user,
replyOpts,
componentLabel,
unauthorizedReply,
} = params;
if (!rawGuildId) {
return true;
}
const replyUnauthorized = async () => {
await replySilently(interaction, { content: unauthorizedReply, ...replyOpts });
};
const channelConfig = resolveDiscordChannelConfigWithFallback({
guildInfo,
channelId,
channelName: channelCtx.channelName,
channelSlug: channelCtx.channelSlug,
parentId: channelCtx.parentId,
parentName: channelCtx.parentName,
parentSlug: channelCtx.parentSlug,
scope: channelCtx.isThread ? "thread" : "channel",
});
if (channelConfig?.enabled === false) {
await replyUnauthorized();
return false;
}
const channelAllowlistConfigured =
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
const channelAllowed = channelConfig?.allowed !== false;
if (
!isDiscordGroupAllowedByPolicy({
groupPolicy: params.groupPolicy,
guildAllowlisted: Boolean(guildInfo),
channelAllowlistConfigured,
channelAllowed,
})
) {
await replyUnauthorized();
return false;
}
if (channelConfig?.allowed === false) {
await replyUnauthorized();
return false;
}
const { memberAllowed } = resolveDiscordMemberAccessState({
channelConfig,
guildInfo,
memberRoleIds,
sender: {
id: user.id,
name: user.username,
tag: user.discriminator ? `${user.username}#${user.discriminator}` : undefined,
},
allowNameMatching: params.allowNameMatching,
});
if (memberAllowed) {
return true;
}
logVerbose(`agent ${componentLabel}: blocked user ${user.id} (not in users/roles allowlist)`);
await replyUnauthorized();
return false;
}
export async function ensureComponentUserAllowed(params: {
entry: DiscordComponentEntry;
interaction: AgentComponentInteraction;
user: DiscordUser;
replyOpts: { ephemeral?: boolean };
componentLabel: string;
unauthorizedReply: string;
allowNameMatching: boolean;
}) {
const allowList = normalizeDiscordAllowList(params.entry.allowedUsers, [
"discord:",
"user:",
"pk:",
]);
if (!allowList) {
return true;
}
const match = resolveDiscordAllowListMatch({
allowList,
candidate: {
id: params.user.id,
name: params.user.username,
tag: formatDiscordUserTag(params.user),
},
allowNameMatching: params.allowNameMatching,
});
if (match.allowed) {
return true;
}
logVerbose(
`discord component ${params.componentLabel}: blocked user ${params.user.id} (not in allowedUsers)`,
);
await replySilently(params.interaction, {
content: params.unauthorizedReply,
...params.replyOpts,
});
return false;
}
export async function ensureAgentComponentInteractionAllowed(params: {
ctx: AgentComponentContext;
interaction: AgentComponentInteraction;
channelId: string;
rawGuildId: string | undefined;
memberRoleIds: string[];
user: DiscordUser;
replyOpts: { ephemeral?: boolean };
componentLabel: string;
unauthorizedReply: string;
}) {
const guildInfo = resolveDiscordGuildEntry({
guild: params.interaction.guild ?? undefined,
guildId: params.rawGuildId,
guildEntries: params.ctx.guildEntries,
});
const channelCtx = resolveDiscordChannelContext(params.interaction);
const memberAllowed = await ensureGuildComponentMemberAllowed({
interaction: params.interaction,
guildInfo,
channelId: params.channelId,
rawGuildId: params.rawGuildId,
channelCtx,
memberRoleIds: params.memberRoleIds,
user: params.user,
replyOpts: params.replyOpts,
componentLabel: params.componentLabel,
unauthorizedReply: params.unauthorizedReply,
allowNameMatching: isDangerousNameMatchingEnabled(params.ctx.discordConfig),
groupPolicy: resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: params.ctx.cfg.channels?.discord !== undefined,
groupPolicy: params.ctx.discordConfig?.groupPolicy,
defaultGroupPolicy: params.ctx.cfg.channels?.defaults?.groupPolicy,
}).groupPolicy,
});
if (!memberAllowed) {
return null;
}
return { parentId: channelCtx.parentId };
}
async function ensureDmComponentAuthorized(params: {
ctx: AgentComponentContext;
interaction: AgentComponentInteraction;
user: DiscordUser;
componentLabel: string;
replyOpts: { ephemeral?: boolean };
}) {
const { ctx, interaction, user, componentLabel, replyOpts } = params;
const allowFromPrefixes = ["discord:", "user:", "pk:"];
const resolveAllowMatch = (entries: string[]) => {
const allowList = normalizeDiscordAllowList(entries, allowFromPrefixes);
return allowList
? resolveDiscordAllowListMatch({
allowList,
candidate: {
id: user.id,
name: user.username,
tag: formatDiscordUserTag(user),
},
allowNameMatching: isDangerousNameMatchingEnabled(ctx.discordConfig),
})
: { allowed: false };
};
const dmPolicy = ctx.dmPolicy ?? "pairing";
if (dmPolicy === "disabled") {
logVerbose(`agent ${componentLabel}: blocked (DM policy disabled)`);
await replySilently(interaction, { content: "DM interactions are disabled.", ...replyOpts });
return false;
}
if (dmPolicy === "allowlist") {
const allowMatch = resolveAllowMatch(ctx.allowFrom ?? []);
if (allowMatch.allowed) {
return true;
}
logVerbose(`agent ${componentLabel}: blocked DM user ${user.id} (not in allowFrom)`);
await replySilently(interaction, {
content: `You are not authorized to use this ${componentLabel}.`,
...replyOpts,
});
return false;
}
const storeAllowFrom =
dmPolicy === "open"
? []
: await readStoreAllowFromForDmPolicy({
provider: "discord",
accountId: ctx.accountId,
dmPolicy,
});
const allowMatch = resolveAllowMatch([...(ctx.allowFrom ?? []), ...storeAllowFrom]);
if (allowMatch.allowed) {
return true;
}
if (dmPolicy === "pairing") {
const pairingResult = await createChannelPairingChallengeIssuer({
channel: "discord",
upsertPairingRequest: async ({ id, meta }) => {
return await upsertChannelPairingRequest({
channel: "discord",
id,
accountId: ctx.accountId,
meta,
});
},
})({
senderId: user.id,
senderIdLine: `Your Discord user id: ${user.id}`,
meta: {
tag: formatDiscordUserTag(user),
name: user.username,
},
sendPairingReply: async (text) => {
await interaction.reply({
content: text,
...replyOpts,
});
},
});
if (!pairingResult.created) {
await replySilently(interaction, {
content: "Pairing already requested. Ask the bot owner to approve your code.",
...replyOpts,
});
}
return false;
}
logVerbose(`agent ${componentLabel}: blocked DM user ${user.id} (not in allowFrom)`);
await replySilently(interaction, {
content: `You are not authorized to use this ${componentLabel}.`,
...replyOpts,
});
return false;
}
async function ensureGroupDmComponentAuthorized(params: {
ctx: AgentComponentContext;
interaction: AgentComponentInteraction;
channelId: string;
componentLabel: string;
replyOpts: { ephemeral?: boolean };
}) {
const { ctx, interaction, channelId, componentLabel, replyOpts } = params;
const groupDmEnabled = ctx.discordConfig?.dm?.groupEnabled ?? false;
if (!groupDmEnabled) {
logVerbose(`agent ${componentLabel}: blocked group dm ${channelId} (group DMs disabled)`);
await replySilently(interaction, {
content: "Group DM interactions are disabled.",
...replyOpts,
});
return false;
}
const channelCtx = resolveDiscordChannelContext(interaction);
const allowed = resolveGroupDmAllow({
channels: ctx.discordConfig?.dm?.groupChannels,
channelId,
channelName: channelCtx.channelName,
channelSlug: channelCtx.channelSlug,
});
if (allowed) {
return true;
}
logVerbose(`agent ${componentLabel}: blocked group dm ${channelId} (not allowlisted)`);
await replySilently(interaction, {
content: `You are not authorized to use this ${componentLabel}.`,
...replyOpts,
});
return false;
}
export async function resolveInteractionContextWithDmAuth(params: {
ctx: AgentComponentContext;
interaction: AgentComponentInteraction;
label: string;
componentLabel: string;
defer?: boolean;
}) {
const interactionCtx = await resolveComponentInteractionContext({
interaction: params.interaction,
label: params.label,
defer: params.defer,
});
if (!interactionCtx) {
return null;
}
if (interactionCtx.isDirectMessage) {
const authorized = await ensureDmComponentAuthorized({
ctx: params.ctx,
interaction: params.interaction,
user: interactionCtx.user,
componentLabel: params.componentLabel,
replyOpts: interactionCtx.replyOpts,
});
if (!authorized) {
return null;
}
}
if (interactionCtx.isGroupDm) {
const authorized = await ensureGroupDmComponentAuthorized({
ctx: params.ctx,
interaction: params.interaction,
channelId: interactionCtx.channelId,
componentLabel: params.componentLabel,
replyOpts: interactionCtx.replyOpts,
});
if (!authorized) {
return null;
}
}
return interactionCtx;
}
export async function resolveAuthorizedComponentInteraction(params: {
ctx: AgentComponentContext;
interaction: AgentComponentInteraction;
label: string;
componentLabel: string;
unauthorizedReply: string;
defer?: boolean;
}) {
const interactionCtx = await resolveInteractionContextWithDmAuth({
ctx: params.ctx,
interaction: params.interaction,
label: params.label,
componentLabel: params.componentLabel,
defer: params.defer,
});
if (!interactionCtx) {
return null;
}
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
const guildInfo = resolveDiscordGuildEntry({
guild: params.interaction.guild ?? undefined,
guildId: rawGuildId,
guildEntries: params.ctx.guildEntries,
});
const channelCtx = resolveDiscordChannelContext(params.interaction);
const allowNameMatching = isDangerousNameMatchingEnabled(params.ctx.discordConfig);
const channelConfig = resolveDiscordChannelConfigWithFallback({
guildInfo,
channelId,
channelName: channelCtx.channelName,
channelSlug: channelCtx.channelSlug,
parentId: channelCtx.parentId,
parentName: channelCtx.parentName,
parentSlug: channelCtx.parentSlug,
scope: channelCtx.isThread ? "thread" : "channel",
});
const memberAllowed = await ensureGuildComponentMemberAllowed({
interaction: params.interaction,
guildInfo,
channelId,
rawGuildId,
channelCtx,
memberRoleIds,
user,
replyOpts,
componentLabel: params.componentLabel,
unauthorizedReply: params.unauthorizedReply,
allowNameMatching,
groupPolicy: resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: params.ctx.cfg.channels?.discord !== undefined,
groupPolicy: params.ctx.discordConfig?.groupPolicy,
defaultGroupPolicy: params.ctx.cfg.channels?.defaults?.groupPolicy,
}).groupPolicy,
});
if (!memberAllowed) {
return null;
}
const commandAuthorized = resolveComponentCommandAuthorized({
ctx: params.ctx,
interactionCtx,
channelConfig,
guildInfo,
allowNameMatching,
});
return {
interactionCtx,
channelCtx,
guildInfo,
channelConfig,
allowNameMatching,
commandAuthorized,
user,
replyOpts,
};
}
export function resolveComponentCommandAuthorized(params: {
ctx: AgentComponentContext;
interactionCtx: ComponentInteractionContext;
channelConfig: ReturnType<typeof resolveDiscordChannelConfigWithFallback>;
guildInfo: ReturnType<typeof resolveDiscordGuildEntry>;
allowNameMatching: boolean;
}) {
const { ctx, interactionCtx, channelConfig, guildInfo } = params;
if (interactionCtx.isDirectMessage) {
return true;
}
const { ownerAllowList, ownerAllowed: ownerOk } = resolveDiscordOwnerAccess({
allowFrom: ctx.allowFrom,
sender: {
id: interactionCtx.user.id,
name: interactionCtx.user.username,
tag: formatDiscordUserTag(interactionCtx.user),
},
allowNameMatching: params.allowNameMatching,
});
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
channelConfig,
guildInfo,
memberRoleIds: interactionCtx.memberRoleIds,
sender: {
id: interactionCtx.user.id,
name: interactionCtx.user.username,
tag: formatDiscordUserTag(interactionCtx.user),
},
allowNameMatching: params.allowNameMatching,
});
const useAccessGroups = ctx.cfg.commands?.useAccessGroups !== false;
const authorizers = useAccessGroups
? [
{ configured: ownerAllowList != null, allowed: ownerOk },
{ configured: hasAccessRestrictions, allowed: memberAllowed },
]
: [{ configured: hasAccessRestrictions, allowed: memberAllowed }];
return resolveCommandAuthorizedFromAuthorizers({
useAccessGroups,
authorizers,
modeWhenAccessGroupsOff: "configured",
});
}
export { resolveInteractionContextWithDmAuth } from "./agent-components-dm-auth.js";
export {
ensureAgentComponentInteractionAllowed,
ensureComponentUserAllowed,
ensureGuildComponentMemberAllowed,
resolveAuthorizedComponentInteraction,
resolveComponentCommandAuthorized,
} from "./agent-components-guild-auth.js";

View File

@@ -0,0 +1,199 @@
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import {
resolveComponentInteractionContext,
resolveDiscordChannelContext,
} from "./agent-components-context.js";
import {
readStoreAllowFromForDmPolicy,
upsertChannelPairingRequest,
} from "./agent-components-helpers.runtime.js";
import { replySilently } from "./agent-components-reply.js";
import type {
AgentComponentContext,
AgentComponentInteraction,
DiscordUser,
} from "./agent-components.types.js";
import {
normalizeDiscordAllowList,
resolveDiscordAllowListMatch,
resolveGroupDmAllow,
} from "./allow-list.js";
import { formatDiscordUserTag } from "./format.js";
async function ensureDmComponentAuthorized(params: {
ctx: AgentComponentContext;
interaction: AgentComponentInteraction;
user: DiscordUser;
componentLabel: string;
replyOpts: { ephemeral?: boolean };
}) {
const { ctx, interaction, user, componentLabel, replyOpts } = params;
const allowFromPrefixes = ["discord:", "user:", "pk:"];
const resolveAllowMatch = (entries: string[]) => {
const allowList = normalizeDiscordAllowList(entries, allowFromPrefixes);
return allowList
? resolveDiscordAllowListMatch({
allowList,
candidate: {
id: user.id,
name: user.username,
tag: formatDiscordUserTag(user),
},
allowNameMatching: isDangerousNameMatchingEnabled(ctx.discordConfig),
})
: { allowed: false };
};
const dmPolicy = ctx.dmPolicy ?? "pairing";
if (dmPolicy === "disabled") {
logVerbose(`agent ${componentLabel}: blocked (DM policy disabled)`);
await replySilently(interaction, { content: "DM interactions are disabled.", ...replyOpts });
return false;
}
if (dmPolicy === "allowlist") {
const allowMatch = resolveAllowMatch(ctx.allowFrom ?? []);
if (allowMatch.allowed) {
return true;
}
logVerbose(`agent ${componentLabel}: blocked DM user ${user.id} (not in allowFrom)`);
await replySilently(interaction, {
content: `You are not authorized to use this ${componentLabel}.`,
...replyOpts,
});
return false;
}
const storeAllowFrom =
dmPolicy === "open"
? []
: await readStoreAllowFromForDmPolicy({
provider: "discord",
accountId: ctx.accountId,
dmPolicy,
});
const allowMatch = resolveAllowMatch([...(ctx.allowFrom ?? []), ...storeAllowFrom]);
if (allowMatch.allowed) {
return true;
}
if (dmPolicy === "pairing") {
const pairingResult = await createChannelPairingChallengeIssuer({
channel: "discord",
upsertPairingRequest: async ({ id, meta }) => {
return await upsertChannelPairingRequest({
channel: "discord",
id,
accountId: ctx.accountId,
meta,
});
},
})({
senderId: user.id,
senderIdLine: `Your Discord user id: ${user.id}`,
meta: {
tag: formatDiscordUserTag(user),
name: user.username,
},
sendPairingReply: async (text) => {
await interaction.reply({
content: text,
...replyOpts,
});
},
});
if (!pairingResult.created) {
await replySilently(interaction, {
content: "Pairing already requested. Ask the bot owner to approve your code.",
...replyOpts,
});
}
return false;
}
logVerbose(`agent ${componentLabel}: blocked DM user ${user.id} (not in allowFrom)`);
await replySilently(interaction, {
content: `You are not authorized to use this ${componentLabel}.`,
...replyOpts,
});
return false;
}
async function ensureGroupDmComponentAuthorized(params: {
ctx: AgentComponentContext;
interaction: AgentComponentInteraction;
channelId: string;
componentLabel: string;
replyOpts: { ephemeral?: boolean };
}) {
const { ctx, interaction, channelId, componentLabel, replyOpts } = params;
const groupDmEnabled = ctx.discordConfig?.dm?.groupEnabled ?? false;
if (!groupDmEnabled) {
logVerbose(`agent ${componentLabel}: blocked group dm ${channelId} (group DMs disabled)`);
await replySilently(interaction, {
content: "Group DM interactions are disabled.",
...replyOpts,
});
return false;
}
const channelCtx = resolveDiscordChannelContext(interaction);
const allowed = resolveGroupDmAllow({
channels: ctx.discordConfig?.dm?.groupChannels,
channelId,
channelName: channelCtx.channelName,
channelSlug: channelCtx.channelSlug,
});
if (allowed) {
return true;
}
logVerbose(`agent ${componentLabel}: blocked group dm ${channelId} (not allowlisted)`);
await replySilently(interaction, {
content: `You are not authorized to use this ${componentLabel}.`,
...replyOpts,
});
return false;
}
export async function resolveInteractionContextWithDmAuth(params: {
ctx: AgentComponentContext;
interaction: AgentComponentInteraction;
label: string;
componentLabel: string;
defer?: boolean;
}) {
const interactionCtx = await resolveComponentInteractionContext({
interaction: params.interaction,
label: params.label,
defer: params.defer,
});
if (!interactionCtx) {
return null;
}
if (interactionCtx.isDirectMessage) {
const authorized = await ensureDmComponentAuthorized({
ctx: params.ctx,
interaction: params.interaction,
user: interactionCtx.user,
componentLabel: params.componentLabel,
replyOpts: interactionCtx.replyOpts,
});
if (!authorized) {
return null;
}
}
if (interactionCtx.isGroupDm) {
const authorized = await ensureGroupDmComponentAuthorized({
ctx: params.ctx,
interaction: params.interaction,
channelId: interactionCtx.channelId,
componentLabel: params.componentLabel,
replyOpts: interactionCtx.replyOpts,
});
if (!authorized) {
return null;
}
}
return interactionCtx;
}

View File

@@ -0,0 +1,322 @@
import { resolveCommandAuthorizedFromAuthorizers } from "openclaw/plugin-sdk/command-auth-native";
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/runtime-group-policy";
import type { DiscordComponentEntry } from "../components.js";
import { resolveDiscordChannelContext } from "./agent-components-context.js";
import { resolveInteractionContextWithDmAuth } from "./agent-components-dm-auth.js";
import { replySilently } from "./agent-components-reply.js";
import type {
AgentComponentContext,
AgentComponentInteraction,
ComponentInteractionContext,
DiscordChannelContext,
DiscordUser,
} from "./agent-components.types.js";
import {
isDiscordGroupAllowedByPolicy,
normalizeDiscordAllowList,
resolveDiscordAllowListMatch,
resolveDiscordChannelConfigWithFallback,
resolveDiscordGuildEntry,
resolveDiscordMemberAccessState,
resolveDiscordOwnerAccess,
} from "./allow-list.js";
import { formatDiscordUserTag } from "./format.js";
function resolveComponentRuntimeGroupPolicy(ctx: AgentComponentContext) {
return resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: ctx.cfg.channels?.discord !== undefined,
groupPolicy: ctx.discordConfig?.groupPolicy,
defaultGroupPolicy: ctx.cfg.channels?.defaults?.groupPolicy,
}).groupPolicy;
}
export async function ensureGuildComponentMemberAllowed(params: {
interaction: AgentComponentInteraction;
guildInfo: ReturnType<typeof resolveDiscordGuildEntry>;
channelId: string;
rawGuildId: string | undefined;
channelCtx: DiscordChannelContext;
memberRoleIds: string[];
user: DiscordUser;
replyOpts: { ephemeral?: boolean };
componentLabel: string;
unauthorizedReply: string;
allowNameMatching: boolean;
groupPolicy: "open" | "disabled" | "allowlist";
}) {
const {
interaction,
guildInfo,
channelId,
rawGuildId,
channelCtx,
memberRoleIds,
user,
replyOpts,
componentLabel,
unauthorizedReply,
} = params;
if (!rawGuildId) {
return true;
}
const replyUnauthorized = async () => {
await replySilently(interaction, { content: unauthorizedReply, ...replyOpts });
};
const channelConfig = resolveDiscordChannelConfigWithFallback({
guildInfo,
channelId,
channelName: channelCtx.channelName,
channelSlug: channelCtx.channelSlug,
parentId: channelCtx.parentId,
parentName: channelCtx.parentName,
parentSlug: channelCtx.parentSlug,
scope: channelCtx.isThread ? "thread" : "channel",
});
if (channelConfig?.enabled === false) {
await replyUnauthorized();
return false;
}
const channelAllowlistConfigured =
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
const channelAllowed = channelConfig?.allowed !== false;
if (
!isDiscordGroupAllowedByPolicy({
groupPolicy: params.groupPolicy,
guildAllowlisted: Boolean(guildInfo),
channelAllowlistConfigured,
channelAllowed,
})
) {
await replyUnauthorized();
return false;
}
if (channelConfig?.allowed === false) {
await replyUnauthorized();
return false;
}
const { memberAllowed } = resolveDiscordMemberAccessState({
channelConfig,
guildInfo,
memberRoleIds,
sender: {
id: user.id,
name: user.username,
tag: user.discriminator ? `${user.username}#${user.discriminator}` : undefined,
},
allowNameMatching: params.allowNameMatching,
});
if (memberAllowed) {
return true;
}
logVerbose(`agent ${componentLabel}: blocked user ${user.id} (not in users/roles allowlist)`);
await replyUnauthorized();
return false;
}
export async function ensureComponentUserAllowed(params: {
entry: DiscordComponentEntry;
interaction: AgentComponentInteraction;
user: DiscordUser;
replyOpts: { ephemeral?: boolean };
componentLabel: string;
unauthorizedReply: string;
allowNameMatching: boolean;
}) {
const allowList = normalizeDiscordAllowList(params.entry.allowedUsers, [
"discord:",
"user:",
"pk:",
]);
if (!allowList) {
return true;
}
const match = resolveDiscordAllowListMatch({
allowList,
candidate: {
id: params.user.id,
name: params.user.username,
tag: formatDiscordUserTag(params.user),
},
allowNameMatching: params.allowNameMatching,
});
if (match.allowed) {
return true;
}
logVerbose(
`discord component ${params.componentLabel}: blocked user ${params.user.id} (not in allowedUsers)`,
);
await replySilently(params.interaction, {
content: params.unauthorizedReply,
...params.replyOpts,
});
return false;
}
export async function ensureAgentComponentInteractionAllowed(params: {
ctx: AgentComponentContext;
interaction: AgentComponentInteraction;
channelId: string;
rawGuildId: string | undefined;
memberRoleIds: string[];
user: DiscordUser;
replyOpts: { ephemeral?: boolean };
componentLabel: string;
unauthorizedReply: string;
}) {
const guildInfo = resolveDiscordGuildEntry({
guild: params.interaction.guild ?? undefined,
guildId: params.rawGuildId,
guildEntries: params.ctx.guildEntries,
});
const channelCtx = resolveDiscordChannelContext(params.interaction);
const memberAllowed = await ensureGuildComponentMemberAllowed({
interaction: params.interaction,
guildInfo,
channelId: params.channelId,
rawGuildId: params.rawGuildId,
channelCtx,
memberRoleIds: params.memberRoleIds,
user: params.user,
replyOpts: params.replyOpts,
componentLabel: params.componentLabel,
unauthorizedReply: params.unauthorizedReply,
allowNameMatching: isDangerousNameMatchingEnabled(params.ctx.discordConfig),
groupPolicy: resolveComponentRuntimeGroupPolicy(params.ctx),
});
if (!memberAllowed) {
return null;
}
return { parentId: channelCtx.parentId };
}
export async function resolveAuthorizedComponentInteraction(params: {
ctx: AgentComponentContext;
interaction: AgentComponentInteraction;
label: string;
componentLabel: string;
unauthorizedReply: string;
defer?: boolean;
}) {
const interactionCtx = await resolveInteractionContextWithDmAuth({
ctx: params.ctx,
interaction: params.interaction,
label: params.label,
componentLabel: params.componentLabel,
defer: params.defer,
});
if (!interactionCtx) {
return null;
}
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
const guildInfo = resolveDiscordGuildEntry({
guild: params.interaction.guild ?? undefined,
guildId: rawGuildId,
guildEntries: params.ctx.guildEntries,
});
const channelCtx = resolveDiscordChannelContext(params.interaction);
const allowNameMatching = isDangerousNameMatchingEnabled(params.ctx.discordConfig);
const channelConfig = resolveDiscordChannelConfigWithFallback({
guildInfo,
channelId,
channelName: channelCtx.channelName,
channelSlug: channelCtx.channelSlug,
parentId: channelCtx.parentId,
parentName: channelCtx.parentName,
parentSlug: channelCtx.parentSlug,
scope: channelCtx.isThread ? "thread" : "channel",
});
const memberAllowed = await ensureGuildComponentMemberAllowed({
interaction: params.interaction,
guildInfo,
channelId,
rawGuildId,
channelCtx,
memberRoleIds,
user,
replyOpts,
componentLabel: params.componentLabel,
unauthorizedReply: params.unauthorizedReply,
allowNameMatching,
groupPolicy: resolveComponentRuntimeGroupPolicy(params.ctx),
});
if (!memberAllowed) {
return null;
}
const commandAuthorized = resolveComponentCommandAuthorized({
ctx: params.ctx,
interactionCtx,
channelConfig,
guildInfo,
allowNameMatching,
});
return {
interactionCtx,
channelCtx,
guildInfo,
channelConfig,
allowNameMatching,
commandAuthorized,
user,
replyOpts,
};
}
export function resolveComponentCommandAuthorized(params: {
ctx: AgentComponentContext;
interactionCtx: ComponentInteractionContext;
channelConfig: ReturnType<typeof resolveDiscordChannelConfigWithFallback>;
guildInfo: ReturnType<typeof resolveDiscordGuildEntry>;
allowNameMatching: boolean;
}) {
const { ctx, interactionCtx, channelConfig, guildInfo } = params;
if (interactionCtx.isDirectMessage) {
return true;
}
const { ownerAllowList, ownerAllowed: ownerOk } = resolveDiscordOwnerAccess({
allowFrom: ctx.allowFrom,
sender: {
id: interactionCtx.user.id,
name: interactionCtx.user.username,
tag: formatDiscordUserTag(interactionCtx.user),
},
allowNameMatching: params.allowNameMatching,
});
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
channelConfig,
guildInfo,
memberRoleIds: interactionCtx.memberRoleIds,
sender: {
id: interactionCtx.user.id,
name: interactionCtx.user.username,
tag: formatDiscordUserTag(interactionCtx.user),
},
allowNameMatching: params.allowNameMatching,
});
const useAccessGroups = ctx.cfg.commands?.useAccessGroups !== false;
const authorizers = useAccessGroups
? [
{ configured: ownerAllowList != null, allowed: ownerOk },
{ configured: hasAccessRestrictions, allowed: memberAllowed },
]
: [{ configured: hasAccessRestrictions, allowed: memberAllowed }];
return resolveCommandAuthorizedFromAuthorizers({
useAccessGroups,
authorizers,
modeWhenAccessGroupsOff: "configured",
});
}

View File

@@ -0,0 +1,10 @@
import type { AgentComponentInteraction } from "./agent-components.types.js";
export async function replySilently(
interaction: AgentComponentInteraction,
params: { content: string; ephemeral?: boolean },
) {
try {
await interaction.reply(params);
} catch {}
}

View File

@@ -8,7 +8,7 @@ const mocks = vi.hoisted(() => {
text: params.spec.text ?? "",
})),
collectDiscordStatusIssues: vi.fn(() => []),
discordOnboardingAdapter: { kind: "legacy-onboarding" },
discordOnboardingAdapter: { kind: "discord-onboarding" },
inspectDiscordAccount: vi.fn(() => ({ accountId: "default" })),
listDiscordAccountIds: vi.fn(() => ["default"]),
listDiscordDirectoryGroupsFromConfig: vi.fn(() => []),

View File

@@ -9,25 +9,11 @@ const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
[bundledPluginFile({ rootDir: ROOT_DIR, pluginId: "discord", relativePath: "runtime-api.ts" })]: [
'export { auditDiscordChannelPermissions, collectDiscordAuditChannelIds } from "./src/audit.js";',
'export { handleDiscordAction } from "./src/actions/runtime.js";',
'export { isDiscordModerationAction, readDiscordModerationCommand, requiredGuildPermissionForModerationAction, type DiscordModerationAction, type DiscordModerationCommand } from "./src/actions/runtime.moderation-shared.js";',
'export { readDiscordChannelCreateParams, readDiscordChannelEditParams, readDiscordChannelMoveParams, readDiscordParentIdParam } from "./src/actions/runtime.shared.js";',
'export { discordMessageActions } from "./src/channel-actions.js";',
'export { listDiscordDirectoryGroupsLive, listDiscordDirectoryPeersLive } from "./src/directory-live.js";',
'export { allowListMatches, buildDiscordMediaPayload, createDiscordMessageHandler, createDiscordNativeCommand, isDiscordGroupAllowedByPolicy, monitorDiscordProvider, normalizeDiscordAllowList, normalizeDiscordSlug, registerDiscordListener, resolveDiscordChannelConfig, resolveDiscordChannelConfigWithFallback, resolveDiscordCommandAuthorized, resolveDiscordGuildEntry, resolveDiscordReplyTarget, resolveDiscordShouldRequireMention, resolveGroupDmAllow, sanitizeDiscordThreadName, shouldEmitDiscordReactionNotification, type DiscordAllowList, type DiscordChannelConfigResolved, type DiscordGuildEntryResolved, type DiscordMessageEvent, type DiscordMessageHandler, type MonitorDiscordOpts } from "./src/monitor.js";',
'export { createDiscordGatewayPlugin, resolveDiscordGatewayIntents, waitForDiscordGatewayPluginRegistration } from "./src/monitor/gateway-plugin.js";',
'export { clearGateways, getGateway, registerGateway, unregisterGateway } from "./src/monitor/gateway-registry.js";',
'export { clearPresences, getPresence, presenceCacheSize, setPresence } from "./src/monitor/presence-cache.js";',
'export { __testing, autoBindSpawnedDiscordSubagent, createNoopThreadBindingManager, createThreadBindingManager, formatThreadBindingDurationLabel, getThreadBindingManager, isRecentlyUnboundThreadWebhookMessage, listThreadBindingsBySessionKey, listThreadBindingsForAccount, reconcileAcpThreadBindingsOnStartup, resolveDiscordThreadBindingIdleTimeoutMs, resolveDiscordThreadBindingMaxAgeMs, resolveThreadBindingIdleTimeoutMs, resolveThreadBindingInactivityExpiresAt, resolveThreadBindingIntroText, resolveThreadBindingMaxAgeExpiresAt, resolveThreadBindingMaxAgeMs, resolveThreadBindingPersona, resolveThreadBindingPersonaFromRecord, resolveThreadBindingsEnabled, resolveThreadBindingThreadName, setThreadBindingIdleTimeoutBySessionKey, setThreadBindingMaxAgeBySessionKey, unbindThreadBindingsBySessionKey, type AcpThreadBindingReconciliationResult, type ThreadBindingManager, type ThreadBindingRecord, type ThreadBindingTargetKind } from "./src/monitor/thread-bindings.js";',
'export { DISCORD_ATTACHMENT_IDLE_TIMEOUT_MS, DISCORD_ATTACHMENT_TOTAL_TIMEOUT_MS, DISCORD_DEFAULT_INBOUND_WORKER_TIMEOUT_MS, DISCORD_DEFAULT_LISTENER_TIMEOUT_MS, mergeAbortSignals } from "./src/monitor/timeouts.js";',
'export { fetchDiscordApplicationId, fetchDiscordApplicationSummary, parseApplicationIdFromToken, probeDiscord, resolveDiscordPrivilegedIntentsFromFlags, type DiscordApplicationSummary, type DiscordPrivilegedIntentsSummary, type DiscordPrivilegedIntentStatus, type DiscordProbe } from "./src/probe.js";',
'export { resolveDiscordChannelAllowlist, type DiscordChannelResolution } from "./src/resolve-channels.js";',
'export { resolveDiscordUserAllowlist, type DiscordUserResolution } from "./src/resolve-users.js";',
'export { resolveDiscordOutboundSessionRoute, type ResolveDiscordOutboundSessionRouteParams } from "./src/outbound-session-route.js";',
'export { addRoleDiscord, banMemberDiscord, createChannelDiscord, createScheduledEventDiscord, createThreadDiscord, deleteChannelDiscord, deleteMessageDiscord, DiscordSendError, editChannelDiscord, editMessageDiscord, fetchChannelInfoDiscord, fetchChannelPermissionsDiscord, fetchMemberGuildPermissionsDiscord, fetchMemberInfoDiscord, fetchMessageDiscord, fetchReactionsDiscord, fetchRoleInfoDiscord, fetchVoiceStatusDiscord, hasAllGuildPermissionsDiscord, hasAnyGuildPermissionDiscord, kickMemberDiscord, listGuildChannelsDiscord, listGuildEmojisDiscord, listPinsDiscord, listScheduledEventsDiscord, listThreadsDiscord, moveChannelDiscord, pinMessageDiscord, reactMessageDiscord, readMessagesDiscord, removeChannelPermissionDiscord, removeOwnReactionsDiscord, removeReactionDiscord, removeRoleDiscord, resolveEventCoverImage, searchMessagesDiscord, sendMessageDiscord, sendPollDiscord, sendStickerDiscord, sendTypingDiscord, sendVoiceMessageDiscord, sendWebhookMessageDiscord, setChannelPermissionDiscord, timeoutMemberDiscord, unpinMessageDiscord, uploadEmojiDiscord, uploadStickerDiscord, type DiscordChannelCreate, type DiscordChannelEdit, type DiscordChannelMove, type DiscordChannelPermissionSet, type DiscordEmojiUpload, type DiscordMessageEdit, type DiscordMessageQuery, type DiscordModerationTarget, type DiscordPermissionsSummary, type DiscordReactionRuntimeContext, type DiscordReactionSummary, type DiscordReactionUser, type DiscordReactOpts, type DiscordRoleChange, type DiscordRuntimeAccountContext, type DiscordSearchQuery, type DiscordSendResult, type DiscordStickerUpload, type DiscordThreadCreate, type DiscordThreadList, type DiscordTimeoutTarget } from "./src/send.js";',
'export { editDiscordComponentMessage, registerBuiltDiscordComponentMessage, sendDiscordComponentMessage } from "./src/send.components.js";',
'export { setDiscordRuntime } from "./src/runtime.js";',
'export { discordMessageActions, handleDiscordAction, isDiscordModerationAction, readDiscordChannelCreateParams, readDiscordChannelEditParams, readDiscordChannelMoveParams, readDiscordModerationCommand, readDiscordParentIdParam, requiredGuildPermissionForModerationAction, type DiscordModerationAction, type DiscordModerationCommand } from "./runtime-api.actions.js";',
'export { auditDiscordChannelPermissions, collectDiscordAuditChannelIds, fetchDiscordApplicationId, fetchDiscordApplicationSummary, listDiscordDirectoryGroupsLive, listDiscordDirectoryPeersLive, parseApplicationIdFromToken, probeDiscord, resolveDiscordChannelAllowlist, resolveDiscordPrivilegedIntentsFromFlags, resolveDiscordUserAllowlist, setDiscordRuntime, type DiscordApplicationSummary, type DiscordChannelResolution, type DiscordPrivilegedIntentsSummary, type DiscordPrivilegedIntentStatus, type DiscordProbe, type DiscordUserResolution } from "./runtime-api.lookup.js";',
'export { DISCORD_ATTACHMENT_IDLE_TIMEOUT_MS, DISCORD_ATTACHMENT_TOTAL_TIMEOUT_MS, DISCORD_DEFAULT_INBOUND_WORKER_TIMEOUT_MS, DISCORD_DEFAULT_LISTENER_TIMEOUT_MS, allowListMatches, buildDiscordMediaPayload, clearGateways, clearPresences, createDiscordGatewayPlugin, createDiscordMessageHandler, createDiscordNativeCommand, getGateway, getPresence, isDiscordGroupAllowedByPolicy, mergeAbortSignals, monitorDiscordProvider, normalizeDiscordAllowList, normalizeDiscordSlug, presenceCacheSize, registerDiscordListener, registerGateway, resolveDiscordChannelConfig, resolveDiscordChannelConfigWithFallback, resolveDiscordCommandAuthorized, resolveDiscordGatewayIntents, resolveDiscordGuildEntry, resolveDiscordReplyTarget, resolveDiscordShouldRequireMention, resolveGroupDmAllow, sanitizeDiscordThreadName, setPresence, shouldEmitDiscordReactionNotification, unregisterGateway, waitForDiscordGatewayPluginRegistration, type DiscordAllowList, type DiscordChannelConfigResolved, type DiscordGuildEntryResolved, type DiscordMessageEvent, type DiscordMessageHandler, type MonitorDiscordOpts } from "./runtime-api.monitor.js";',
'export { DiscordSendError, addRoleDiscord, banMemberDiscord, createChannelDiscord, createScheduledEventDiscord, createThreadDiscord, deleteChannelDiscord, deleteMessageDiscord, editChannelDiscord, editDiscordComponentMessage, editMessageDiscord, fetchChannelInfoDiscord, fetchChannelPermissionsDiscord, fetchMemberGuildPermissionsDiscord, fetchMemberInfoDiscord, fetchMessageDiscord, fetchReactionsDiscord, fetchRoleInfoDiscord, fetchVoiceStatusDiscord, hasAllGuildPermissionsDiscord, hasAnyGuildPermissionDiscord, kickMemberDiscord, listGuildChannelsDiscord, listGuildEmojisDiscord, listPinsDiscord, listScheduledEventsDiscord, listThreadsDiscord, moveChannelDiscord, pinMessageDiscord, reactMessageDiscord, readMessagesDiscord, registerBuiltDiscordComponentMessage, removeChannelPermissionDiscord, removeOwnReactionsDiscord, removeReactionDiscord, removeRoleDiscord, resolveDiscordOutboundSessionRoute, resolveEventCoverImage, searchMessagesDiscord, sendDiscordComponentMessage, sendMessageDiscord, sendPollDiscord, sendStickerDiscord, sendTypingDiscord, sendVoiceMessageDiscord, sendWebhookMessageDiscord, setChannelPermissionDiscord, timeoutMemberDiscord, unpinMessageDiscord, uploadEmojiDiscord, uploadStickerDiscord, type DiscordChannelCreate, type DiscordChannelEdit, type DiscordChannelMove, type DiscordChannelPermissionSet, type DiscordEmojiUpload, type DiscordMessageEdit, type DiscordMessageQuery, type DiscordModerationTarget, type DiscordPermissionsSummary, type DiscordReactionRuntimeContext, type DiscordReactionSummary, type DiscordReactionUser, type DiscordReactOpts, type DiscordRoleChange, type DiscordRuntimeAccountContext, type DiscordSearchQuery, type DiscordSendResult, type DiscordStickerUpload, type DiscordThreadCreate, type DiscordThreadList, type DiscordTimeoutTarget, type ResolveDiscordOutboundSessionRouteParams } from "./runtime-api.send.js";',
'export { __testing, autoBindSpawnedDiscordSubagent, createNoopThreadBindingManager, createThreadBindingManager, formatThreadBindingDurationLabel, getThreadBindingManager, isRecentlyUnboundThreadWebhookMessage, listThreadBindingsBySessionKey, listThreadBindingsForAccount, reconcileAcpThreadBindingsOnStartup, resolveDiscordThreadBindingIdleTimeoutMs, resolveDiscordThreadBindingMaxAgeMs, resolveThreadBindingIdleTimeoutMs, resolveThreadBindingInactivityExpiresAt, resolveThreadBindingIntroText, resolveThreadBindingMaxAgeExpiresAt, resolveThreadBindingMaxAgeMs, resolveThreadBindingPersona, resolveThreadBindingPersonaFromRecord, resolveThreadBindingsEnabled, resolveThreadBindingThreadName, setThreadBindingIdleTimeoutBySessionKey, setThreadBindingMaxAgeBySessionKey, unbindThreadBindingsBySessionKey, type AcpThreadBindingReconciliationResult, type ThreadBindingManager, type ThreadBindingRecord, type ThreadBindingTargetKind } from "./runtime-api.threads.js";',
],
[bundledPluginFile({ rootDir: ROOT_DIR, pluginId: "imessage", relativePath: "runtime-api.ts" })]:
[

View File

@@ -1,8 +1,16 @@
import { relative, resolve } from "node:path";
import { loadPluginManifestRegistry } from "../../manifest-registry.js";
const sourceExtensionsDir = resolve(process.cwd(), "extensions");
const bundledPluginRoots = new Map(
loadPluginManifestRegistry({ config: {} })
loadPluginManifestRegistry({
config: {},
env: {
...process.env,
OPENCLAW_BUNDLED_PLUGINS_DIR: sourceExtensionsDir,
OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1",
},
})
.plugins.filter((plugin) => plugin.origin === "bundled")
.map((plugin) => [plugin.id, plugin.rootDir] as const),
);