Files
openclaw/extensions/clickclack/src/access.ts
Michael Appel 10546e57dd clickclack: enforce inbound sender allowlist [AI] (#83741)
* fix: enforce clickclack sender allowlist

* addressing codex review

* test(clickclack): drop removed senderIsOwner from inbound test fixture
2026-05-26 18:41:12 -07:00

76 lines
2.5 KiB
TypeScript

import {
resolveStableChannelMessageIngress,
type StableChannelIngressIdentityParams,
} from "openclaw/plugin-sdk/channel-ingress-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { getClickClackRuntime } from "./runtime.js";
import type { ClickClackMessage, CoreConfig, ResolvedClickClackAccount } from "./types.js";
const CHANNEL_ID = "clickclack" as const;
function normalizeClickClackUserId(value: string): string | null {
const trimmed = value.trim();
if (!trimmed) {
return null;
}
const withoutProvider = trimmed.replace(/^(clickclack|cc):/i, "").trim();
const directTarget = withoutProvider.match(/^dm:(.+)$/i);
return directTarget?.[1]?.trim() || withoutProvider || null;
}
const clickClackIngressIdentity = {
key: "user-id",
normalizeEntry: normalizeClickClackUserId,
normalizeSubject: normalizeClickClackUserId,
isWildcardEntry: (entry) => normalizeClickClackUserId(entry) === "*",
entryIdPrefix: "clickclack-user",
} satisfies StableChannelIngressIdentityParams;
export type ClickClackInboundAccess = {
shouldDispatch: boolean;
commandAuthorized: boolean;
};
export async function resolveClickClackInboundAccess(params: {
account: ResolvedClickClackAccount;
config: CoreConfig;
message: ClickClackMessage;
}): Promise<ClickClackInboundAccess> {
const runtime = getClickClackRuntime();
const isDirect = Boolean(params.message.direct_conversation_id);
const cfg = params.config as OpenClawConfig;
const shouldCheckCommand = runtime.channel.commands.shouldComputeCommandAuthorized(
params.message.body,
cfg,
);
const resolved = await resolveStableChannelMessageIngress({
channelId: CHANNEL_ID,
accountId: params.account.accountId,
identity: clickClackIngressIdentity,
cfg,
subject: { stableId: params.message.author_id },
conversation: {
kind: isDirect ? "direct" : "group",
id: isDirect
? (params.message.direct_conversation_id ?? params.message.author_id)
: (params.message.channel_id ?? params.message.thread_root_id),
},
allowFrom: params.account.allowFrom,
dmPolicy: "allowlist",
groupPolicy: "allowlist",
command: shouldCheckCommand
? {
cfg,
modeWhenAccessGroupsOff: "configured",
}
: false,
});
return {
shouldDispatch: resolved.ingress.admission === "dispatch",
commandAuthorized: resolved.commandAccess.requested
? resolved.commandAccess.authorized
: resolved.senderAccess.allowed,
};
}