From ace8a1b44e547c928538e1be5f91590ea079cc58 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 18 Jan 2026 22:51:01 +0000 Subject: [PATCH] feat: expand dm allowlist onboarding --- src/channels/plugins/onboarding-types.ts | 5 + src/channels/plugins/onboarding/discord.ts | 109 ++++++++++++++++++++ src/channels/plugins/onboarding/imessage.ts | 101 ++++++++++++++++++ src/channels/plugins/onboarding/signal.ts | 103 ++++++++++++++++++ src/channels/plugins/onboarding/slack.ts | 107 +++++++++++++++++++ src/channels/plugins/onboarding/telegram.ts | 81 ++++++++++++--- src/commands/onboard-channels.ts | 21 +++- 7 files changed, 512 insertions(+), 15 deletions(-) diff --git a/src/channels/plugins/onboarding-types.ts b/src/channels/plugins/onboarding-types.ts index 19840536b..1d8b54828 100644 --- a/src/channels/plugins/onboarding-types.ts +++ b/src/channels/plugins/onboarding-types.ts @@ -69,6 +69,11 @@ export type ChannelOnboardingDmPolicy = { allowFromKey: string; getCurrent: (cfg: ClawdbotConfig) => DmPolicy; setPolicy: (cfg: ClawdbotConfig, policy: DmPolicy) => ClawdbotConfig; + promptAllowFrom?: (params: { + cfg: ClawdbotConfig; + prompter: WizardPrompter; + accountId?: string; + }) => Promise; }; export type ChannelOnboardingAdapter = { diff --git a/src/channels/plugins/onboarding/discord.ts b/src/channels/plugins/onboarding/discord.ts index 69dc430d4..a63d6fc6a 100644 --- a/src/channels/plugins/onboarding/discord.ts +++ b/src/channels/plugins/onboarding/discord.ts @@ -7,6 +7,7 @@ import { resolveDiscordAccount, } from "../../../discord/accounts.js"; import { normalizeDiscordSlug } from "../../../discord/monitor/allow-list.js"; +import { resolveDiscordUserAllowlist } from "../../../discord/resolve-users.js"; import { resolveDiscordChannelAllowlist, type DiscordChannelResolution, @@ -148,6 +149,113 @@ function setDiscordGuildChannelAllowlist( }; } +function setDiscordAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig { + return { + ...cfg, + channels: { + ...cfg.channels, + discord: { + ...cfg.channels?.discord, + dm: { + ...cfg.channels?.discord?.dm, + enabled: cfg.channels?.discord?.dm?.enabled ?? true, + allowFrom, + }, + }, + }, + }; +} + +function parseDiscordAllowFromInput(raw: string): string[] { + return raw + .split(/[\n,;]+/g) + .map((entry) => entry.trim()) + .filter(Boolean); +} + +async function promptDiscordAllowFrom(params: { + cfg: ClawdbotConfig; + prompter: WizardPrompter; + accountId?: string; +}): Promise { + const accountId = + params.accountId && normalizeAccountId(params.accountId) + ? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID + : resolveDefaultDiscordAccountId(params.cfg); + const resolved = resolveDiscordAccount({ cfg: params.cfg, accountId }); + const token = resolved.token; + const existing = params.cfg.channels?.discord?.dm?.allowFrom ?? []; + await params.prompter.note( + [ + "Allowlist Discord DMs by username (we resolve to user ids).", + "Examples:", + "- 123456789012345678", + "- @alice", + "- alice#1234", + "Multiple entries: comma-separated.", + `Docs: ${formatDocsLink("/discord", "discord")}`, + ].join("\n"), + "Discord allowlist", + ); + + const parseInputs = (value: string) => parseDiscordAllowFromInput(value); + const parseId = (value: string) => { + const trimmed = value.trim(); + if (!trimmed) return null; + const mention = trimmed.match(/^<@!?(\d+)>$/); + if (mention) return mention[1]; + const prefixed = trimmed.replace(/^(user:|discord:)/i, ""); + if (/^\d+$/.test(prefixed)) return prefixed; + return null; + }; + + while (true) { + const entry = await params.prompter.text({ + message: "Discord allowFrom (usernames or ids)", + placeholder: "@alice, 123456789012345678", + initialValue: existing[0] ? String(existing[0]) : undefined, + validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), + }); + const parts = parseInputs(String(entry)); + if (!token) { + const ids = parts.map(parseId).filter(Boolean) as string[]; + if (ids.length !== parts.length) { + await params.prompter.note( + "Bot token missing; use numeric user ids (or mention form) only.", + "Discord allowlist", + ); + continue; + } + const unique = [...new Set([...existing.map((v) => String(v).trim()), ...ids])].filter( + Boolean, + ); + return setDiscordAllowFrom(params.cfg, unique); + } + + const results = await resolveDiscordUserAllowlist({ + token, + entries: parts, + }).catch(() => null); + if (!results) { + await params.prompter.note("Failed to resolve usernames. Try again.", "Discord allowlist"); + continue; + } + const unresolved = results.filter((res) => !res.resolved || !res.id); + if (unresolved.length > 0) { + await params.prompter.note( + `Could not resolve: ${unresolved.map((res) => res.input).join(", ")}`, + "Discord allowlist", + ); + continue; + } + const ids = results.map((res) => res.id as string); + const unique = [ + ...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids]), + ]; + return setDiscordAllowFrom(params.cfg, unique); + } +} + const dmPolicy: ChannelOnboardingDmPolicy = { label: "Discord", channel, @@ -155,6 +263,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = { allowFromKey: "channels.discord.dm.allowFrom", getCurrent: (cfg) => cfg.channels?.discord?.dm?.policy ?? "pairing", setPolicy: (cfg, policy) => setDiscordDmPolicy(cfg, policy), + promptAllowFrom: promptDiscordAllowFrom, }; export const discordOnboardingAdapter: ChannelOnboardingAdapter = { diff --git a/src/channels/plugins/onboarding/imessage.ts b/src/channels/plugins/onboarding/imessage.ts index bac337132..9d2d6278b 100644 --- a/src/channels/plugins/onboarding/imessage.ts +++ b/src/channels/plugins/onboarding/imessage.ts @@ -6,6 +6,7 @@ import { resolveDefaultIMessageAccountId, resolveIMessageAccount, } from "../../../imessage/accounts.js"; +import { normalizeIMessageHandle } from "../../../imessage/targets.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js"; import { formatDocsLink } from "../../../terminal/links.js"; import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js"; @@ -29,6 +30,105 @@ function setIMessageDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) { }; } +function setIMessageAllowFrom( + cfg: ClawdbotConfig, + accountId: string, + allowFrom: string[], +): ClawdbotConfig { + if (accountId === DEFAULT_ACCOUNT_ID) { + return { + ...cfg, + channels: { + ...cfg.channels, + imessage: { + ...cfg.channels?.imessage, + allowFrom, + }, + }, + }; + } + return { + ...cfg, + channels: { + ...cfg.channels, + imessage: { + ...cfg.channels?.imessage, + accounts: { + ...cfg.channels?.imessage?.accounts, + [accountId]: { + ...cfg.channels?.imessage?.accounts?.[accountId], + allowFrom, + }, + }, + }, + }, + }; +} + +function parseIMessageAllowFromInput(raw: string): string[] { + return raw + .split(/[\n,;]+/g) + .map((entry) => entry.trim()) + .filter(Boolean); +} + +async function promptIMessageAllowFrom(params: { + cfg: ClawdbotConfig; + prompter: WizardPrompter; + accountId?: string; +}): Promise { + const accountId = + params.accountId && normalizeAccountId(params.accountId) + ? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID + : resolveDefaultIMessageAccountId(params.cfg); + const resolved = resolveIMessageAccount({ cfg: params.cfg, accountId }); + const existing = resolved.config.allowFrom ?? []; + await params.prompter.note( + [ + "Allowlist iMessage DMs by handle or chat target.", + "Examples:", + "- +15555550123", + "- user@example.com", + "- chat_id:123", + "- chat_guid:... or chat_identifier:...", + "Multiple entries: comma-separated.", + `Docs: ${formatDocsLink("/imessage", "imessage")}`, + ].join("\n"), + "iMessage allowlist", + ); + const entry = await params.prompter.text({ + message: "iMessage allowFrom (handle or chat_id)", + placeholder: "+15555550123, user@example.com, chat_id:123", + initialValue: existing[0] ? String(existing[0]) : undefined, + validate: (value) => { + const raw = String(value ?? "").trim(); + if (!raw) return "Required"; + const parts = parseIMessageAllowFromInput(raw); + for (const part of parts) { + if (part === "*") continue; + if (part.toLowerCase().startsWith("chat_id:")) { + const id = part.slice("chat_id:".length).trim(); + if (!/^\d+$/.test(id)) return `Invalid chat_id: ${part}`; + continue; + } + if (part.toLowerCase().startsWith("chat_guid:")) { + if (!part.slice("chat_guid:".length).trim()) return "Invalid chat_guid entry"; + continue; + } + if (part.toLowerCase().startsWith("chat_identifier:")) { + if (!part.slice("chat_identifier:".length).trim()) return "Invalid chat_identifier entry"; + continue; + } + if (!normalizeIMessageHandle(part)) return `Invalid handle: ${part}`; + } + return undefined; + }, + }); + const parts = parseIMessageAllowFromInput(String(entry)); + const unique = [...new Set(parts)]; + return setIMessageAllowFrom(params.cfg, accountId, unique); +} + const dmPolicy: ChannelOnboardingDmPolicy = { label: "iMessage", channel, @@ -36,6 +136,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = { allowFromKey: "channels.imessage.allowFrom", getCurrent: (cfg) => cfg.channels?.imessage?.dmPolicy ?? "pairing", setPolicy: (cfg, policy) => setIMessageDmPolicy(cfg, policy), + promptAllowFrom: promptIMessageAllowFrom, }; export const imessageOnboardingAdapter: ChannelOnboardingAdapter = { diff --git a/src/channels/plugins/onboarding/signal.ts b/src/channels/plugins/onboarding/signal.ts index e916d82c8..de209b500 100644 --- a/src/channels/plugins/onboarding/signal.ts +++ b/src/channels/plugins/onboarding/signal.ts @@ -9,6 +9,7 @@ import { resolveSignalAccount, } from "../../../signal/accounts.js"; import { formatDocsLink } from "../../../terminal/links.js"; +import { normalizeE164 } from "../../../utils.js"; import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js"; import { addWildcardAllowFrom, promptAccountId } from "./helpers.js"; @@ -30,6 +31,107 @@ function setSignalDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) { }; } +function setSignalAllowFrom( + cfg: ClawdbotConfig, + accountId: string, + allowFrom: string[], +): ClawdbotConfig { + if (accountId === DEFAULT_ACCOUNT_ID) { + return { + ...cfg, + channels: { + ...cfg.channels, + signal: { + ...cfg.channels?.signal, + allowFrom, + }, + }, + }; + } + return { + ...cfg, + channels: { + ...cfg.channels, + signal: { + ...cfg.channels?.signal, + accounts: { + ...cfg.channels?.signal?.accounts, + [accountId]: { + ...cfg.channels?.signal?.accounts?.[accountId], + allowFrom, + }, + }, + }, + }, + }; +} + +function parseSignalAllowFromInput(raw: string): string[] { + return raw + .split(/[\n,;]+/g) + .map((entry) => entry.trim()) + .filter(Boolean); +} + +function isUuidLike(value: string): boolean { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value); +} + +async function promptSignalAllowFrom(params: { + cfg: ClawdbotConfig; + prompter: WizardPrompter; + accountId?: string; +}): Promise { + const accountId = + params.accountId && normalizeAccountId(params.accountId) + ? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID + : resolveDefaultSignalAccountId(params.cfg); + const resolved = resolveSignalAccount({ cfg: params.cfg, accountId }); + const existing = resolved.config.allowFrom ?? []; + await params.prompter.note( + [ + "Allowlist Signal DMs by sender id.", + "Examples:", + "- +15555550123", + "- uuid:123e4567-e89b-12d3-a456-426614174000", + "Multiple entries: comma-separated.", + `Docs: ${formatDocsLink("/signal", "signal")}`, + ].join("\n"), + "Signal allowlist", + ); + const entry = await params.prompter.text({ + message: "Signal allowFrom (E.164 or uuid)", + placeholder: "+15555550123, uuid:123e4567-e89b-12d3-a456-426614174000", + initialValue: existing[0] ? String(existing[0]) : undefined, + validate: (value) => { + const raw = String(value ?? "").trim(); + if (!raw) return "Required"; + const parts = parseSignalAllowFromInput(raw); + for (const part of parts) { + if (part === "*") continue; + if (part.toLowerCase().startsWith("uuid:")) { + if (!part.slice("uuid:".length).trim()) return "Invalid uuid entry"; + continue; + } + if (isUuidLike(part)) continue; + if (!normalizeE164(part)) return `Invalid entry: ${part}`; + } + return undefined; + }, + }); + const parts = parseSignalAllowFromInput(String(entry)); + const normalized = parts + .map((part) => { + if (part === "*") return "*"; + if (part.toLowerCase().startsWith("uuid:")) return `uuid:${part.slice(5).trim()}`; + if (isUuidLike(part)) return `uuid:${part}`; + return normalizeE164(part); + }) + .filter(Boolean); + const unique = [...new Set(normalized)]; + return setSignalAllowFrom(params.cfg, accountId, unique); +} + const dmPolicy: ChannelOnboardingDmPolicy = { label: "Signal", channel, @@ -37,6 +139,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = { allowFromKey: "channels.signal.allowFrom", getCurrent: (cfg) => cfg.channels?.signal?.dmPolicy ?? "pairing", setPolicy: (cfg, policy) => setSignalDmPolicy(cfg, policy), + promptAllowFrom: promptSignalAllowFrom, }; export const signalOnboardingAdapter: ChannelOnboardingAdapter = { diff --git a/src/channels/plugins/onboarding/slack.ts b/src/channels/plugins/onboarding/slack.ts index 76f5f6071..4b8144b19 100644 --- a/src/channels/plugins/onboarding/slack.ts +++ b/src/channels/plugins/onboarding/slack.ts @@ -7,6 +7,7 @@ import { resolveSlackAccount, } from "../../../slack/accounts.js"; import { resolveSlackChannelAllowlist } from "../../../slack/resolve-channels.js"; +import { resolveSlackUserAllowlist } from "../../../slack/resolve-users.js"; import { formatDocsLink } from "../../../terminal/links.js"; import type { WizardPrompter } from "../../../wizard/prompts.js"; import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js"; @@ -200,6 +201,111 @@ function setSlackChannelAllowlist( }; } +function setSlackAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig { + return { + ...cfg, + channels: { + ...cfg.channels, + slack: { + ...cfg.channels?.slack, + dm: { + ...cfg.channels?.slack?.dm, + enabled: cfg.channels?.slack?.dm?.enabled ?? true, + allowFrom, + }, + }, + }, + }; +} + +function parseSlackAllowFromInput(raw: string): string[] { + return raw + .split(/[\n,;]+/g) + .map((entry) => entry.trim()) + .filter(Boolean); +} + +async function promptSlackAllowFrom(params: { + cfg: ClawdbotConfig; + prompter: WizardPrompter; + accountId?: string; +}): Promise { + const accountId = + params.accountId && normalizeAccountId(params.accountId) + ? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID + : resolveDefaultSlackAccountId(params.cfg); + const resolved = resolveSlackAccount({ cfg: params.cfg, accountId }); + const token = resolved.config.userToken ?? resolved.config.botToken ?? ""; + const existing = params.cfg.channels?.slack?.dm?.allowFrom ?? []; + await params.prompter.note( + [ + "Allowlist Slack DMs by username (we resolve to user ids).", + "Examples:", + "- U12345678", + "- @alice", + "Multiple entries: comma-separated.", + `Docs: ${formatDocsLink("/slack", "slack")}`, + ].join("\n"), + "Slack allowlist", + ); + const parseInputs = (value: string) => parseSlackAllowFromInput(value); + const parseId = (value: string) => { + const trimmed = value.trim(); + if (!trimmed) return null; + const mention = trimmed.match(/^<@([A-Z0-9]+)>$/i); + if (mention) return mention[1]?.toUpperCase(); + const prefixed = trimmed.replace(/^(slack:|user:)/i, ""); + if (/^[A-Z][A-Z0-9]+$/i.test(prefixed)) return prefixed.toUpperCase(); + return null; + }; + + while (true) { + const entry = await params.prompter.text({ + message: "Slack allowFrom (usernames or ids)", + placeholder: "@alice, U12345678", + initialValue: existing[0] ? String(existing[0]) : undefined, + validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), + }); + const parts = parseInputs(String(entry)); + if (!token) { + const ids = parts.map(parseId).filter(Boolean) as string[]; + if (ids.length !== parts.length) { + await params.prompter.note( + "Slack token missing; use user ids (or mention form) only.", + "Slack allowlist", + ); + continue; + } + const unique = [...new Set([...existing.map((v) => String(v).trim()), ...ids])].filter( + Boolean, + ); + return setSlackAllowFrom(params.cfg, unique); + } + + const results = await resolveSlackUserAllowlist({ + token, + entries: parts, + }).catch(() => null); + if (!results) { + await params.prompter.note("Failed to resolve usernames. Try again.", "Slack allowlist"); + continue; + } + const unresolved = results.filter((res) => !res.resolved || !res.id); + if (unresolved.length > 0) { + await params.prompter.note( + `Could not resolve: ${unresolved.map((res) => res.input).join(", ")}`, + "Slack allowlist", + ); + continue; + } + const ids = results.map((res) => res.id as string); + const unique = [ + ...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids]), + ]; + return setSlackAllowFrom(params.cfg, unique); + } +} + const dmPolicy: ChannelOnboardingDmPolicy = { label: "Slack", channel, @@ -207,6 +313,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = { allowFromKey: "channels.slack.dm.allowFrom", getCurrent: (cfg) => cfg.channels?.slack?.dm?.policy ?? "pairing", setPolicy: (cfg, policy) => setSlackDmPolicy(cfg, policy), + promptAllowFrom: promptSlackAllowFrom, }; export const slackOnboardingAdapter: ChannelOnboardingAdapter = { diff --git a/src/channels/plugins/onboarding/telegram.ts b/src/channels/plugins/onboarding/telegram.ts index 65bab9a50..f0725c038 100644 --- a/src/channels/plugins/onboarding/telegram.ts +++ b/src/channels/plugins/onboarding/telegram.ts @@ -65,21 +65,59 @@ async function promptTelegramAllowFrom(params: { const resolved = resolveTelegramAccount({ cfg, accountId }); const existingAllowFrom = resolved.config.allowFrom ?? []; await noteTelegramUserIdHelp(prompter); - const entry = await prompter.text({ - message: "Telegram allowFrom (user id)", - placeholder: "123456789", - initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined, - validate: (value) => { - const raw = String(value ?? "").trim(); - if (!raw) return "Required"; - if (!/^\d+$/.test(raw)) return "Use a numeric Telegram user id"; - return undefined; - }, - }); - const normalized = String(entry).trim(); + + const token = resolved.token; + if (!token) { + await prompter.note("Telegram token missing; username lookup is unavailable.", "Telegram"); + } + + const resolveTelegramUserId = async (raw: string): Promise => { + const trimmed = raw.trim(); + if (!trimmed) return null; + const stripped = trimmed.replace(/^(telegram|tg):/i, "").trim(); + if (/^\d+$/.test(stripped)) return stripped; + if (!token) return null; + const username = stripped.startsWith("@") ? stripped : `@${stripped}`; + const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`; + const res = await fetch(url); + const data = (await res.json().catch(() => null)) as + | { ok?: boolean; result?: { id?: number | string } } + | null; + const id = data?.ok ? data?.result?.id : undefined; + if (typeof id === "number" || typeof id === "string") return String(id); + return null; + }; + + const parseInput = (value: string) => + value + .split(/[\n,;]+/g) + .map((entry) => entry.trim()) + .filter(Boolean); + + let resolvedIds: string[] = []; + while (resolvedIds.length === 0) { + const entry = await prompter.text({ + message: "Telegram allowFrom (username or user id)", + placeholder: "@username", + initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined, + validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), + }); + const parts = parseInput(String(entry)); + const results = await Promise.all(parts.map((part) => resolveTelegramUserId(part))); + const unresolved = parts.filter((_, idx) => !results[idx]); + if (unresolved.length > 0) { + await prompter.note( + `Could not resolve: ${unresolved.join(", ")}. Use @username or numeric id.`, + "Telegram allowlist", + ); + continue; + } + resolvedIds = results.filter(Boolean) as string[]; + } + const merged = [ ...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean), - normalized, + ...resolvedIds, ]; const unique = [...new Set(merged)]; @@ -119,6 +157,22 @@ async function promptTelegramAllowFrom(params: { }; } +async function promptTelegramAllowFromForAccount(params: { + cfg: ClawdbotConfig; + prompter: WizardPrompter; + accountId?: string; +}): Promise { + const accountId = + params.accountId && normalizeAccountId(params.accountId) + ? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID + : resolveDefaultTelegramAccountId(params.cfg); + return promptTelegramAllowFrom({ + cfg: params.cfg, + prompter: params.prompter, + accountId, + }); +} + const dmPolicy: ChannelOnboardingDmPolicy = { label: "Telegram", channel, @@ -126,6 +180,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = { allowFromKey: "channels.telegram.allowFrom", getCurrent: (cfg) => cfg.channels?.telegram?.dmPolicy ?? "pairing", setPolicy: (cfg, policy) => setTelegramDmPolicy(cfg, policy), + promptAllowFrom: promptTelegramAllowFromForAccount, }; export const telegramOnboardingAdapter: ChannelOnboardingAdapter = { diff --git a/src/commands/onboard-channels.ts b/src/commands/onboard-channels.ts index 73d4e416f..744100e8b 100644 --- a/src/commands/onboard-channels.ts +++ b/src/commands/onboard-channels.ts @@ -214,8 +214,9 @@ async function maybeConfigureDmPolicies(params: { cfg: ClawdbotConfig; selection: ChannelChoice[]; prompter: WizardPrompter; + accountIdsByChannel?: Map; }): Promise { - const { selection, prompter } = params; + const { selection, prompter, accountIdsByChannel } = params; const dmPolicies = selection .map((channel) => getChannelOnboardingAdapter(channel)?.dmPolicy) .filter(Boolean) as ChannelOnboardingDmPolicy[]; @@ -233,6 +234,7 @@ async function maybeConfigureDmPolicies(params: { [ "Default: pairing (unknown DMs get a pairing code).", `Approve: clawdbot pairing approve ${policy.channel} `, + `Allowlist DMs: ${policy.policyKey}="allowlist" + ${policy.allowFromKey} entries.`, `Public DMs: ${policy.policyKey}="open" + ${policy.allowFromKey} includes "*".`, 'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.', `Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`, @@ -243,6 +245,7 @@ async function maybeConfigureDmPolicies(params: { message: `${policy.label} DM policy`, options: [ { value: "pairing", label: "Pairing (recommended)" }, + { value: "allowlist", label: "Allowlist (specific users only)" }, { value: "open", label: "Open (public inbound DMs)" }, { value: "disabled", label: "Disabled (ignore DMs)" }, ], @@ -255,6 +258,13 @@ async function maybeConfigureDmPolicies(params: { if (nextPolicy !== current) { cfg = policy.setPolicy(cfg, nextPolicy); } + if (nextPolicy === "allowlist" && policy.promptAllowFrom) { + cfg = await policy.promptAllowFrom({ + cfg, + prompter, + accountId: accountIdsByChannel?.get(policy.channel), + }); + } } return cfg; @@ -320,10 +330,12 @@ export async function setupChannels( options?.initialSelection?.[0] ?? resolveQuickstartDefault(statusByChannel); const shouldPromptAccountIds = options?.promptAccountIds === true; + const accountIdsByChannel = new Map(); const recordAccount = (channel: ChannelChoice, accountId: string) => { options?.onAccountId?.(channel, accountId); const adapter = getChannelOnboardingAdapter(channel); adapter?.onAccountRecorded?.(accountId, options); + accountIdsByChannel.set(channel, accountId); }; const selection: ChannelChoice[] = []; @@ -614,7 +626,12 @@ export async function setupChannels( } if (!options?.skipDmPolicyPrompt) { - next = await maybeConfigureDmPolicies({ cfg: next, selection, prompter }); + next = await maybeConfigureDmPolicies({ + cfg: next, + selection, + prompter, + accountIdsByChannel, + }); } return next;