refactor: share onboarding secret prompt flows

This commit is contained in:
Peter Steinberger
2026-03-10 20:28:25 +00:00
parent 00170f8e1a
commit 725958c66f
9 changed files with 239 additions and 215 deletions

View File

@@ -20,15 +20,14 @@ import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onb
import { configureChannelAccessWithAllowlist } from "./channel-access-configure.js";
import {
applySingleTokenPromptResult,
buildSingleChannelSecretPromptState,
parseMentionOrPrefixedId,
noteChannelLookupFailure,
noteChannelLookupSummary,
patchChannelConfigForAccount,
promptLegacyChannelAllowFrom,
promptSingleChannelSecretInput,
resolveAccountIdForConfigure,
resolveOnboardingAccountId,
runSingleChannelSecretStep,
setAccountGroupPolicyForChannel,
setLegacyChannelDmPolicyWithAllowFrom,
setOnboardingChannelEnabled,
@@ -179,52 +178,39 @@ export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
accountId: discordAccountId,
});
const allowEnv = discordAccountId === DEFAULT_ACCOUNT_ID;
const tokenPromptState = buildSingleChannelSecretPromptState({
accountConfigured: Boolean(resolvedAccount.token),
hasConfigToken: hasConfiguredSecretInput(resolvedAccount.config.token),
allowEnv,
envValue: process.env.DISCORD_BOT_TOKEN,
});
if (!tokenPromptState.accountConfigured) {
await noteDiscordTokenHelp(prompter);
}
const tokenResult = await promptSingleChannelSecretInput({
const tokenStep = await runSingleChannelSecretStep({
cfg: next,
prompter,
providerHint: "discord",
credentialLabel: "Discord bot token",
secretInputMode: options?.secretInputMode,
accountConfigured: tokenPromptState.accountConfigured,
canUseEnv: tokenPromptState.canUseEnv,
hasConfigToken: tokenPromptState.hasConfigToken,
accountConfigured: Boolean(resolvedAccount.token),
hasConfigToken: hasConfiguredSecretInput(resolvedAccount.config.token),
allowEnv,
envValue: process.env.DISCORD_BOT_TOKEN,
envPrompt: "DISCORD_BOT_TOKEN detected. Use env var?",
keepPrompt: "Discord token already configured. Keep it?",
inputPrompt: "Enter Discord bot token",
preferredEnvVar: allowEnv ? "DISCORD_BOT_TOKEN" : undefined,
onMissingConfigured: async () => await noteDiscordTokenHelp(prompter),
applyUseEnv: async (cfg) =>
applySingleTokenPromptResult({
cfg,
channel: "discord",
accountId: discordAccountId,
tokenPatchKey: "token",
tokenResult: { useEnv: true, token: null },
}),
applySet: async (cfg, value) =>
applySingleTokenPromptResult({
cfg,
channel: "discord",
accountId: discordAccountId,
tokenPatchKey: "token",
tokenResult: { useEnv: false, token: value },
}),
});
let resolvedTokenForAllowlist: string | undefined;
if (tokenResult.action === "use-env") {
next = applySingleTokenPromptResult({
cfg: next,
channel: "discord",
accountId: discordAccountId,
tokenPatchKey: "token",
tokenResult: { useEnv: true, token: null },
});
resolvedTokenForAllowlist = process.env.DISCORD_BOT_TOKEN?.trim() || undefined;
} else if (tokenResult.action === "set") {
next = applySingleTokenPromptResult({
cfg: next,
channel: "discord",
accountId: discordAccountId,
tokenPatchKey: "token",
tokenResult: { useEnv: false, token: tokenResult.value },
});
resolvedTokenForAllowlist = tokenResult.resolvedValue;
}
next = tokenStep.cfg;
const currentEntries = Object.entries(resolvedAccount.config.guilds ?? {}).flatMap(
([guildKey, value]) => {
@@ -261,7 +247,7 @@ export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
input,
resolved: false,
}));
const activeToken = accountWithTokens.token || resolvedTokenForAllowlist || "";
const activeToken = accountWithTokens.token || tokenStep.resolvedValue || "";
if (activeToken && entries.length > 0) {
try {
resolved = await resolveDiscordChannelAllowlist({

View File

@@ -482,6 +482,82 @@ export type SingleChannelSecretInputPromptResult =
| { action: "use-env" }
| { action: "set"; value: SecretInput; resolvedValue: string };
export async function runSingleChannelSecretStep(params: {
cfg: OpenClawConfig;
prompter: Pick<WizardPrompter, "confirm" | "text" | "select" | "note">;
providerHint: string;
credentialLabel: string;
secretInputMode?: "plaintext" | "ref";
accountConfigured: boolean;
hasConfigToken: boolean;
allowEnv: boolean;
envValue?: string;
envPrompt: string;
keepPrompt: string;
inputPrompt: string;
preferredEnvVar?: string;
onMissingConfigured?: () => Promise<void>;
applyUseEnv?: (cfg: OpenClawConfig) => OpenClawConfig | Promise<OpenClawConfig>;
applySet?: (
cfg: OpenClawConfig,
value: SecretInput,
resolvedValue: string,
) => OpenClawConfig | Promise<OpenClawConfig>;
}): Promise<{
cfg: OpenClawConfig;
action: SingleChannelSecretInputPromptResult["action"];
resolvedValue?: string;
}> {
const promptState = buildSingleChannelSecretPromptState({
accountConfigured: params.accountConfigured,
hasConfigToken: params.hasConfigToken,
allowEnv: params.allowEnv,
envValue: params.envValue,
});
if (!promptState.accountConfigured && params.onMissingConfigured) {
await params.onMissingConfigured();
}
const result = await promptSingleChannelSecretInput({
cfg: params.cfg,
prompter: params.prompter,
providerHint: params.providerHint,
credentialLabel: params.credentialLabel,
secretInputMode: params.secretInputMode,
accountConfigured: promptState.accountConfigured,
canUseEnv: promptState.canUseEnv,
hasConfigToken: promptState.hasConfigToken,
envPrompt: params.envPrompt,
keepPrompt: params.keepPrompt,
inputPrompt: params.inputPrompt,
preferredEnvVar: params.preferredEnvVar,
});
if (result.action === "use-env") {
return {
cfg: params.applyUseEnv ? await params.applyUseEnv(params.cfg) : params.cfg,
action: result.action,
resolvedValue: params.envValue?.trim() || undefined,
};
}
if (result.action === "set") {
return {
cfg: params.applySet
? await params.applySet(params.cfg, result.value, result.resolvedValue)
: params.cfg,
action: result.action,
resolvedValue: result.resolvedValue,
};
}
return {
cfg: params.cfg,
action: result.action,
};
}
export async function promptSingleChannelSecretInput(params: {
cfg: OpenClawConfig;
prompter: Pick<WizardPrompter, "confirm" | "text" | "select" | "note">;

View File

@@ -14,15 +14,14 @@ import type { WizardPrompter } from "../../../wizard/prompts.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
import { configureChannelAccessWithAllowlist } from "./channel-access-configure.js";
import {
buildSingleChannelSecretPromptState,
parseMentionOrPrefixedId,
noteChannelLookupFailure,
noteChannelLookupSummary,
patchChannelConfigForAccount,
promptLegacyChannelAllowFrom,
promptSingleChannelSecretInput,
resolveAccountIdForConfigure,
resolveOnboardingAccountId,
runSingleChannelSecretStep,
setAccountGroupPolicyForChannel,
setLegacyChannelDmPolicyWithAllowFrom,
setOnboardingChannelEnabled,
@@ -235,18 +234,6 @@ export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
const accountConfigured =
Boolean(resolvedAccount.botToken && resolvedAccount.appToken) || hasConfigTokens;
const allowEnv = slackAccountId === DEFAULT_ACCOUNT_ID;
const botPromptState = buildSingleChannelSecretPromptState({
accountConfigured: Boolean(resolvedAccount.botToken) || hasConfiguredBotToken,
hasConfigToken: hasConfiguredBotToken,
allowEnv,
envValue: process.env.SLACK_BOT_TOKEN,
});
const appPromptState = buildSingleChannelSecretPromptState({
accountConfigured: Boolean(resolvedAccount.appToken) || hasConfiguredAppToken,
hasConfigToken: hasConfiguredAppToken,
allowEnv,
envValue: process.env.SLACK_APP_TOKEN,
});
let resolvedBotTokenForAllowlist = resolvedAccount.botToken;
const slackBotName = String(
await prompter.text({
@@ -257,54 +244,56 @@ export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
if (!accountConfigured) {
await noteSlackTokenHelp(prompter, slackBotName);
}
const botTokenResult = await promptSingleChannelSecretInput({
const botTokenStep = await runSingleChannelSecretStep({
cfg: next,
prompter,
providerHint: "slack-bot",
credentialLabel: "Slack bot token",
secretInputMode: options?.secretInputMode,
accountConfigured: botPromptState.accountConfigured,
canUseEnv: botPromptState.canUseEnv,
hasConfigToken: botPromptState.hasConfigToken,
accountConfigured: Boolean(resolvedAccount.botToken) || hasConfiguredBotToken,
hasConfigToken: hasConfiguredBotToken,
allowEnv,
envValue: process.env.SLACK_BOT_TOKEN,
envPrompt: "SLACK_BOT_TOKEN detected. Use env var?",
keepPrompt: "Slack bot token already configured. Keep it?",
inputPrompt: "Enter Slack bot token (xoxb-...)",
preferredEnvVar: allowEnv ? "SLACK_BOT_TOKEN" : undefined,
applySet: async (cfg, value) =>
patchChannelConfigForAccount({
cfg,
channel: "slack",
accountId: slackAccountId,
patch: { botToken: value },
}),
});
if (botTokenResult.action === "use-env") {
resolvedBotTokenForAllowlist = process.env.SLACK_BOT_TOKEN?.trim() || undefined;
} else if (botTokenResult.action === "set") {
next = patchChannelConfigForAccount({
cfg: next,
channel: "slack",
accountId: slackAccountId,
patch: { botToken: botTokenResult.value },
});
resolvedBotTokenForAllowlist = botTokenResult.resolvedValue;
next = botTokenStep.cfg;
if (botTokenStep.resolvedValue) {
resolvedBotTokenForAllowlist = botTokenStep.resolvedValue;
}
const appTokenResult = await promptSingleChannelSecretInput({
const appTokenStep = await runSingleChannelSecretStep({
cfg: next,
prompter,
providerHint: "slack-app",
credentialLabel: "Slack app token",
secretInputMode: options?.secretInputMode,
accountConfigured: appPromptState.accountConfigured,
canUseEnv: appPromptState.canUseEnv,
hasConfigToken: appPromptState.hasConfigToken,
accountConfigured: Boolean(resolvedAccount.appToken) || hasConfiguredAppToken,
hasConfigToken: hasConfiguredAppToken,
allowEnv,
envValue: process.env.SLACK_APP_TOKEN,
envPrompt: "SLACK_APP_TOKEN detected. Use env var?",
keepPrompt: "Slack app token already configured. Keep it?",
inputPrompt: "Enter Slack app token (xapp-...)",
preferredEnvVar: allowEnv ? "SLACK_APP_TOKEN" : undefined,
applySet: async (cfg, value) =>
patchChannelConfigForAccount({
cfg,
channel: "slack",
accountId: slackAccountId,
patch: { appToken: value },
}),
});
if (appTokenResult.action === "set") {
next = patchChannelConfigForAccount({
cfg: next,
channel: "slack",
accountId: slackAccountId,
patch: { appToken: appTokenResult.value },
});
}
next = appTokenStep.cfg;
next = await configureChannelAccessWithAllowlist({
cfg: next,

View File

@@ -14,12 +14,11 @@ import { fetchTelegramChatId } from "../../telegram/api.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
import {
applySingleTokenPromptResult,
buildSingleChannelSecretPromptState,
patchChannelConfigForAccount,
promptSingleChannelSecretInput,
promptResolvedAllowFrom,
resolveAccountIdForConfigure,
resolveOnboardingAccountId,
runSingleChannelSecretStep,
setChannelDmPolicyWithAllowFrom,
setOnboardingChannelEnabled,
splitOnboardingEntries,
@@ -194,59 +193,46 @@ export const telegramOnboardingAdapter: ChannelOnboardingAdapter = {
const hasConfigToken =
hasConfiguredBotToken || Boolean(resolvedAccount.config.tokenFile?.trim());
const allowEnv = telegramAccountId === DEFAULT_ACCOUNT_ID;
const tokenPromptState = buildSingleChannelSecretPromptState({
accountConfigured: Boolean(resolvedAccount.token) || hasConfigToken,
hasConfigToken,
allowEnv,
envValue: process.env.TELEGRAM_BOT_TOKEN,
});
if (!tokenPromptState.accountConfigured) {
await noteTelegramTokenHelp(prompter);
}
const tokenResult = await promptSingleChannelSecretInput({
const tokenStep = await runSingleChannelSecretStep({
cfg: next,
prompter,
providerHint: "telegram",
credentialLabel: "Telegram bot token",
secretInputMode: options?.secretInputMode,
accountConfigured: tokenPromptState.accountConfigured,
canUseEnv: tokenPromptState.canUseEnv,
hasConfigToken: tokenPromptState.hasConfigToken,
accountConfigured: Boolean(resolvedAccount.token) || hasConfigToken,
hasConfigToken,
allowEnv,
envValue: process.env.TELEGRAM_BOT_TOKEN,
envPrompt: "TELEGRAM_BOT_TOKEN detected. Use env var?",
keepPrompt: "Telegram token already configured. Keep it?",
inputPrompt: "Enter Telegram bot token",
preferredEnvVar: allowEnv ? "TELEGRAM_BOT_TOKEN" : undefined,
onMissingConfigured: async () => await noteTelegramTokenHelp(prompter),
applyUseEnv: async (cfg) =>
applySingleTokenPromptResult({
cfg,
channel: "telegram",
accountId: telegramAccountId,
tokenPatchKey: "botToken",
tokenResult: { useEnv: true, token: null },
}),
applySet: async (cfg, value) =>
applySingleTokenPromptResult({
cfg,
channel: "telegram",
accountId: telegramAccountId,
tokenPatchKey: "botToken",
tokenResult: { useEnv: false, token: value },
}),
});
let resolvedTokenForAllowFrom: string | undefined;
if (tokenResult.action === "use-env") {
next = applySingleTokenPromptResult({
cfg: next,
channel: "telegram",
accountId: telegramAccountId,
tokenPatchKey: "botToken",
tokenResult: { useEnv: true, token: null },
});
resolvedTokenForAllowFrom = process.env.TELEGRAM_BOT_TOKEN?.trim() || undefined;
} else if (tokenResult.action === "set") {
next = applySingleTokenPromptResult({
cfg: next,
channel: "telegram",
accountId: telegramAccountId,
tokenPatchKey: "botToken",
tokenResult: { useEnv: false, token: tokenResult.value },
});
resolvedTokenForAllowFrom = tokenResult.resolvedValue;
}
next = tokenStep.cfg;
if (forceAllowFrom) {
next = await promptTelegramAllowFrom({
cfg: next,
prompter,
accountId: telegramAccountId,
tokenOverride: resolvedTokenForAllowFrom,
tokenOverride: tokenStep.resolvedValue,
});
}

View File

@@ -33,6 +33,7 @@ export {
buildSingleChannelSecretPromptState,
promptAccountId,
promptSingleChannelSecretInput,
runSingleChannelSecretStep,
resolveAccountIdForConfigure,
} from "../channels/plugins/onboarding/helpers.js";
export {

View File

@@ -27,6 +27,7 @@ export {
mergeAllowFromEntries,
promptAccountId,
promptSingleChannelSecretInput,
runSingleChannelSecretStep,
resolveAccountIdForConfigure,
setTopLevelChannelDmPolicyWithAllowFrom,
} from "../channels/plugins/onboarding/helpers.js";

View File

@@ -21,6 +21,7 @@ export {
mergeAllowFromEntries,
promptAccountId,
promptSingleChannelSecretInput,
runSingleChannelSecretStep,
resolveAccountIdForConfigure,
setTopLevelChannelDmPolicyWithAllowFrom,
} from "../channels/plugins/onboarding/helpers.js";