refactor: unify onboarding secret-input prompt state wiring

This commit is contained in:
Peter Steinberger
2026-03-07 21:44:58 +00:00
parent 6b1c82c4f1
commit 5eba663c38
15 changed files with 172 additions and 60 deletions

View File

@@ -7,6 +7,7 @@ import type {
WizardPrompter,
} from "openclaw/plugin-sdk/feishu";
import {
buildSingleChannelSecretPromptState,
DEFAULT_ACCOUNT_ID,
formatDocsLink,
hasConfiguredSecretInput,
@@ -240,9 +241,12 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
const hasConfigCreds = Boolean(
typeof feishuCfg?.appId === "string" && feishuCfg.appId.trim() && hasConfigSecret,
);
const canUseEnv = Boolean(
!hasConfigCreds && process.env.FEISHU_APP_ID?.trim() && process.env.FEISHU_APP_SECRET?.trim(),
);
const appSecretPromptState = buildSingleChannelSecretPromptState({
accountConfigured: Boolean(resolved),
hasConfigToken: hasConfigSecret,
allowEnv: !hasConfigCreds && Boolean(process.env.FEISHU_APP_ID?.trim()),
envValue: process.env.FEISHU_APP_SECRET,
});
let next = cfg;
let appId: string | null = null;
@@ -258,9 +262,9 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
prompter,
providerHint: "feishu",
credentialLabel: "App Secret",
accountConfigured: Boolean(resolved),
canUseEnv,
hasConfigToken: hasConfigSecret,
accountConfigured: appSecretPromptState.accountConfigured,
canUseEnv: appSecretPromptState.canUseEnv,
hasConfigToken: appSecretPromptState.hasConfigToken,
envPrompt: "FEISHU_APP_ID + FEISHU_APP_SECRET detected. Use env vars?",
keepPrompt: "Feishu App Secret already configured. Keep it?",
inputPrompt: "Enter Feishu App Secret",
@@ -346,14 +350,19 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
if (connectionMode === "webhook") {
const currentVerificationToken = (next.channels?.feishu as FeishuConfig | undefined)
?.verificationToken;
const verificationTokenPromptState = buildSingleChannelSecretPromptState({
accountConfigured: hasConfiguredSecretInput(currentVerificationToken),
hasConfigToken: hasConfiguredSecretInput(currentVerificationToken),
allowEnv: false,
});
const verificationTokenResult = await promptSingleChannelSecretInput({
cfg: next,
prompter,
providerHint: "feishu-webhook",
credentialLabel: "verification token",
accountConfigured: hasConfiguredSecretInput(currentVerificationToken),
canUseEnv: false,
hasConfigToken: hasConfiguredSecretInput(currentVerificationToken),
accountConfigured: verificationTokenPromptState.accountConfigured,
canUseEnv: verificationTokenPromptState.canUseEnv,
hasConfigToken: verificationTokenPromptState.hasConfigToken,
envPrompt: "",
keepPrompt: "Feishu verification token already configured. Keep it?",
inputPrompt: "Enter Feishu verification token",

View File

@@ -1,6 +1,7 @@
import type { DmPolicy } from "openclaw/plugin-sdk/matrix";
import {
addWildcardAllowFrom,
buildSingleChannelSecretPromptState,
formatResolvedUnresolvedNote,
formatDocsLink,
hasConfiguredSecretInput,
@@ -323,14 +324,20 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
},
}),
).trim();
const passwordPromptState = buildSingleChannelSecretPromptState({
accountConfigured: Boolean(existingPasswordConfigured),
hasConfigToken: existingPasswordConfigured,
allowEnv: true,
envValue: envPassword,
});
const passwordResult = await promptSingleChannelSecretInput({
cfg: next,
prompter,
providerHint: "matrix",
credentialLabel: "password",
accountConfigured: Boolean(existingPasswordConfigured),
canUseEnv: Boolean(envPassword?.trim()) && !existingPasswordConfigured,
hasConfigToken: existingPasswordConfigured,
accountConfigured: passwordPromptState.accountConfigured,
canUseEnv: passwordPromptState.canUseEnv,
hasConfigToken: passwordPromptState.hasConfigToken,
envPrompt: "MATRIX_PASSWORD detected. Use env var?",
keepPrompt: "Matrix password already configured. Keep it?",
inputPrompt: "Matrix password",

View File

@@ -1,5 +1,6 @@
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
import {
buildSingleChannelSecretPromptState,
hasConfiguredSecretInput,
promptSingleChannelSecretInput,
type ChannelOnboardingAdapter,
@@ -84,12 +85,17 @@ export const mattermostOnboardingAdapter: ChannelOnboardingAdapter = {
});
const accountConfigured = Boolean(resolvedAccount.botToken && resolvedAccount.baseUrl);
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
const canUseEnv =
allowEnv &&
Boolean(process.env.MATTERMOST_BOT_TOKEN?.trim()) &&
Boolean(process.env.MATTERMOST_URL?.trim());
const hasConfigToken = hasConfiguredSecretInput(resolvedAccount.config.botToken);
const hasConfigValues = hasConfigToken || Boolean(resolvedAccount.config.baseUrl);
const tokenPromptState = buildSingleChannelSecretPromptState({
accountConfigured,
hasConfigToken,
allowEnv: allowEnv && !hasConfigValues,
envValue:
process.env.MATTERMOST_BOT_TOKEN?.trim() && process.env.MATTERMOST_URL?.trim()
? process.env.MATTERMOST_BOT_TOKEN
: undefined,
});
let botToken: SecretInput | null = null;
let baseUrl: string | null = null;
@@ -103,9 +109,9 @@ export const mattermostOnboardingAdapter: ChannelOnboardingAdapter = {
prompter,
providerHint: "mattermost",
credentialLabel: "bot token",
accountConfigured,
canUseEnv: canUseEnv && !hasConfigValues,
hasConfigToken,
accountConfigured: tokenPromptState.accountConfigured,
canUseEnv: tokenPromptState.canUseEnv,
hasConfigToken: tokenPromptState.hasConfigToken,
envPrompt: "MATTERMOST_BOT_TOKEN + MATTERMOST_URL detected. Use env vars?",
keepPrompt: "Mattermost bot token already configured. Keep it?",
inputPrompt: "Enter Mattermost bot token",

View File

@@ -1,4 +1,5 @@
import {
buildSingleChannelSecretPromptState,
formatDocsLink,
hasConfiguredSecretInput,
mergeAllowFromEntries,
@@ -209,11 +210,16 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
});
const accountConfigured = Boolean(resolvedAccount.secret && resolvedAccount.baseUrl);
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
const canUseEnv = allowEnv && Boolean(process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim());
const hasConfigSecret = Boolean(
hasConfiguredSecretInput(resolvedAccount.config.botSecret) ||
resolvedAccount.config.botSecretFile,
);
const secretPromptState = buildSingleChannelSecretPromptState({
accountConfigured,
hasConfigToken: hasConfigSecret,
allowEnv,
envValue: process.env.NEXTCLOUD_TALK_BOT_SECRET,
});
let baseUrl = resolvedAccount.baseUrl;
if (!baseUrl) {
@@ -244,9 +250,9 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
prompter,
providerHint: "nextcloud-talk",
credentialLabel: "bot secret",
accountConfigured,
canUseEnv: canUseEnv && !hasConfigSecret,
hasConfigToken: hasConfigSecret,
accountConfigured: secretPromptState.accountConfigured,
canUseEnv: secretPromptState.canUseEnv,
hasConfigToken: secretPromptState.hasConfigToken,
envPrompt: "NEXTCLOUD_TALK_BOT_SECRET detected. Use env var?",
keepPrompt: "Nextcloud Talk bot secret already configured. Keep it?",
inputPrompt: "Enter Nextcloud Talk bot secret",
@@ -285,9 +291,11 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
prompter,
providerHint: "nextcloud-talk-api",
credentialLabel: "API password",
accountConfigured: Boolean(existingApiUser && existingApiPasswordConfigured),
canUseEnv: false,
hasConfigToken: existingApiPasswordConfigured,
...buildSingleChannelSecretPromptState({
accountConfigured: Boolean(existingApiUser && existingApiPasswordConfigured),
hasConfigToken: existingApiPasswordConfigured,
allowEnv: false,
}),
envPrompt: "",
keepPrompt: "Nextcloud Talk API password already configured. Keep it?",
inputPrompt: "Enter Nextcloud Talk API password",

View File

@@ -6,6 +6,7 @@ import type {
WizardPrompter,
} from "openclaw/plugin-sdk/zalo";
import {
buildSingleChannelSecretPromptState,
DEFAULT_ACCOUNT_ID,
hasConfiguredSecretInput,
mergeAllowFromEntries,
@@ -251,10 +252,15 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
});
const accountConfigured = Boolean(resolvedAccount.token);
const allowEnv = zaloAccountId === DEFAULT_ACCOUNT_ID;
const canUseEnv = allowEnv && Boolean(process.env.ZALO_BOT_TOKEN?.trim());
const hasConfigToken = Boolean(
hasConfiguredSecretInput(resolvedAccount.config.botToken) || resolvedAccount.config.tokenFile,
);
const tokenPromptState = buildSingleChannelSecretPromptState({
accountConfigured,
hasConfigToken,
allowEnv,
envValue: process.env.ZALO_BOT_TOKEN,
});
let token: SecretInput | null = null;
if (!accountConfigured) {
@@ -265,9 +271,9 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
prompter,
providerHint: "zalo",
credentialLabel: "bot token",
accountConfigured,
canUseEnv: canUseEnv && !hasConfigToken,
hasConfigToken,
accountConfigured: tokenPromptState.accountConfigured,
canUseEnv: tokenPromptState.canUseEnv,
hasConfigToken: tokenPromptState.hasConfigToken,
envPrompt: "ZALO_BOT_TOKEN detected. Use env var?",
keepPrompt: "Zalo token already configured. Keep it?",
inputPrompt: "Enter Zalo bot token",
@@ -349,9 +355,11 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
prompter,
providerHint: "zalo-webhook",
credentialLabel: "webhook secret",
accountConfigured: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
canUseEnv: false,
hasConfigToken: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
...buildSingleChannelSecretPromptState({
accountConfigured: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
hasConfigToken: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
allowEnv: false,
}),
envPrompt: "",
keepPrompt: "Zalo webhook secret already configured. Keep it?",
inputPrompt: "Webhook secret (8-256 chars)",
@@ -368,9 +376,11 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
prompter,
providerHint: "zalo-webhook",
credentialLabel: "webhook secret",
accountConfigured: false,
canUseEnv: false,
hasConfigToken: false,
...buildSingleChannelSecretPromptState({
accountConfigured: false,
hasConfigToken: false,
allowEnv: false,
}),
envPrompt: "",
keepPrompt: "Zalo webhook secret already configured. Keep it?",
inputPrompt: "Webhook secret (8-256 chars)",

View File

@@ -20,6 +20,7 @@ import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onb
import { configureChannelAccessWithAllowlist } from "./channel-access-configure.js";
import {
applySingleTokenPromptResult,
buildSingleChannelSecretPromptState,
parseMentionOrPrefixedId,
noteChannelLookupFailure,
noteChannelLookupSummary,
@@ -177,12 +178,15 @@ export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
cfg: next,
accountId: discordAccountId,
});
const hasConfigToken = hasConfiguredSecretInput(resolvedAccount.config.token);
const accountConfigured = Boolean(resolvedAccount.token) || hasConfigToken;
const allowEnv = discordAccountId === DEFAULT_ACCOUNT_ID;
const canUseEnv = allowEnv && !hasConfigToken && Boolean(process.env.DISCORD_BOT_TOKEN?.trim());
const tokenPromptState = buildSingleChannelSecretPromptState({
accountConfigured: Boolean(resolvedAccount.token),
hasConfigToken: hasConfiguredSecretInput(resolvedAccount.config.token),
allowEnv,
envValue: process.env.DISCORD_BOT_TOKEN,
});
if (!accountConfigured) {
if (!tokenPromptState.accountConfigured) {
await noteDiscordTokenHelp(prompter);
}
@@ -192,9 +196,9 @@ export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
providerHint: "discord",
credentialLabel: "Discord bot token",
secretInputMode: options?.secretInputMode,
accountConfigured,
canUseEnv,
hasConfigToken,
accountConfigured: tokenPromptState.accountConfigured,
canUseEnv: tokenPromptState.canUseEnv,
hasConfigToken: tokenPromptState.hasConfigToken,
envPrompt: "DISCORD_BOT_TOKEN detected. Use env var?",
keepPrompt: "Discord token already configured. Keep it?",
inputPrompt: "Enter Discord bot token",

View File

@@ -9,6 +9,7 @@ vi.mock("../../../plugin-sdk/onboarding.js", () => ({
import {
applySingleTokenPromptResult,
buildSingleChannelSecretPromptState,
normalizeAllowFromEntries,
noteChannelLookupFailure,
noteChannelLookupSummary,
@@ -104,6 +105,38 @@ async function runPromptSingleToken(params: {
});
}
describe("buildSingleChannelSecretPromptState", () => {
it("enables env path only when env is present and no config token exists", () => {
expect(
buildSingleChannelSecretPromptState({
accountConfigured: false,
hasConfigToken: false,
allowEnv: true,
envValue: "token-from-env",
}),
).toEqual({
accountConfigured: false,
hasConfigToken: false,
canUseEnv: true,
});
});
it("disables env path when config token already exists", () => {
expect(
buildSingleChannelSecretPromptState({
accountConfigured: true,
hasConfigToken: true,
allowEnv: true,
envValue: "token-from-env",
}),
).toEqual({
accountConfigured: true,
hasConfigToken: true,
canUseEnv: false,
});
});
});
async function runPromptLegacyAllowFrom(params: {
cfg?: OpenClawConfig;
channel: "discord" | "slack";

View File

@@ -452,6 +452,23 @@ export function applySingleTokenPromptResult(params: {
return next;
}
export function buildSingleChannelSecretPromptState(params: {
accountConfigured: boolean;
hasConfigToken: boolean;
allowEnv: boolean;
envValue?: string;
}): {
accountConfigured: boolean;
hasConfigToken: boolean;
canUseEnv: boolean;
} {
return {
accountConfigured: params.accountConfigured,
hasConfigToken: params.hasConfigToken,
canUseEnv: params.allowEnv && Boolean(params.envValue?.trim()) && !params.hasConfigToken,
};
}
export async function promptSingleChannelToken(params: {
prompter: Pick<WizardPrompter, "confirm" | "text">;
accountConfigured: boolean;

View File

@@ -14,6 +14,7 @@ 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,
@@ -234,10 +235,18 @@ export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
const accountConfigured =
Boolean(resolvedAccount.botToken && resolvedAccount.appToken) || hasConfigTokens;
const allowEnv = slackAccountId === DEFAULT_ACCOUNT_ID;
const canUseBotEnv =
allowEnv && !hasConfiguredBotToken && Boolean(process.env.SLACK_BOT_TOKEN?.trim());
const canUseAppEnv =
allowEnv && !hasConfiguredAppToken && Boolean(process.env.SLACK_APP_TOKEN?.trim());
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({
@@ -254,9 +263,9 @@ export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
providerHint: "slack-bot",
credentialLabel: "Slack bot token",
secretInputMode: options?.secretInputMode,
accountConfigured: Boolean(resolvedAccount.botToken) || hasConfiguredBotToken,
canUseEnv: canUseBotEnv,
hasConfigToken: hasConfiguredBotToken,
accountConfigured: botPromptState.accountConfigured,
canUseEnv: botPromptState.canUseEnv,
hasConfigToken: botPromptState.hasConfigToken,
envPrompt: "SLACK_BOT_TOKEN detected. Use env var?",
keepPrompt: "Slack bot token already configured. Keep it?",
inputPrompt: "Enter Slack bot token (xoxb-...)",
@@ -280,9 +289,9 @@ export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
providerHint: "slack-app",
credentialLabel: "Slack app token",
secretInputMode: options?.secretInputMode,
accountConfigured: Boolean(resolvedAccount.appToken) || hasConfiguredAppToken,
canUseEnv: canUseAppEnv,
hasConfigToken: hasConfiguredAppToken,
accountConfigured: appPromptState.accountConfigured,
canUseEnv: appPromptState.canUseEnv,
hasConfigToken: appPromptState.hasConfigToken,
envPrompt: "SLACK_APP_TOKEN detected. Use env var?",
keepPrompt: "Slack app token already configured. Keep it?",
inputPrompt: "Enter Slack app token (xapp-...)",

View File

@@ -14,6 +14,7 @@ import { fetchTelegramChatId } from "../../telegram/api.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
import {
applySingleTokenPromptResult,
buildSingleChannelSecretPromptState,
patchChannelConfigForAccount,
promptSingleChannelSecretInput,
promptResolvedAllowFrom,
@@ -192,12 +193,15 @@ export const telegramOnboardingAdapter: ChannelOnboardingAdapter = {
const hasConfiguredBotToken = hasConfiguredSecretInput(resolvedAccount.config.botToken);
const hasConfigToken =
hasConfiguredBotToken || Boolean(resolvedAccount.config.tokenFile?.trim());
const accountConfigured = Boolean(resolvedAccount.token) || hasConfigToken;
const allowEnv = telegramAccountId === DEFAULT_ACCOUNT_ID;
const canUseEnv =
allowEnv && !hasConfigToken && Boolean(process.env.TELEGRAM_BOT_TOKEN?.trim());
const tokenPromptState = buildSingleChannelSecretPromptState({
accountConfigured: Boolean(resolvedAccount.token) || hasConfigToken,
hasConfigToken,
allowEnv,
envValue: process.env.TELEGRAM_BOT_TOKEN,
});
if (!accountConfigured) {
if (!tokenPromptState.accountConfigured) {
await noteTelegramTokenHelp(prompter);
}
@@ -207,9 +211,9 @@ export const telegramOnboardingAdapter: ChannelOnboardingAdapter = {
providerHint: "telegram",
credentialLabel: "Telegram bot token",
secretInputMode: options?.secretInputMode,
accountConfigured,
canUseEnv,
hasConfigToken,
accountConfigured: tokenPromptState.accountConfigured,
canUseEnv: tokenPromptState.canUseEnv,
hasConfigToken: tokenPromptState.hasConfigToken,
envPrompt: "TELEGRAM_BOT_TOKEN detected. Use env var?",
keepPrompt: "Telegram token already configured. Keep it?",
inputPrompt: "Enter Telegram bot token",

View File

@@ -16,6 +16,7 @@ export type {
ChannelOnboardingDmPolicy,
} from "../channels/plugins/onboarding-types.js";
export {
buildSingleChannelSecretPromptState,
addWildcardAllowFrom,
promptSingleChannelSecretInput,
setTopLevelChannelAllowFrom,

View File

@@ -33,6 +33,7 @@ export type {
} from "../channels/plugins/onboarding-types.js";
export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js";
export {
buildSingleChannelSecretPromptState,
addWildcardAllowFrom,
mergeAllowFromEntries,
promptSingleChannelSecretInput,

View File

@@ -30,6 +30,7 @@ export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
export type { ChannelOnboardingAdapter } from "../channels/plugins/onboarding-types.js";
export {
buildSingleChannelSecretPromptState,
promptAccountId,
promptSingleChannelSecretInput,
resolveAccountIdForConfigure,

View File

@@ -22,6 +22,7 @@ export type {
ChannelOnboardingDmPolicy,
} from "../channels/plugins/onboarding-types.js";
export {
buildSingleChannelSecretPromptState,
addWildcardAllowFrom,
mergeAllowFromEntries,
promptAccountId,

View File

@@ -16,6 +16,7 @@ export type {
ChannelOnboardingDmPolicy,
} from "../channels/plugins/onboarding-types.js";
export {
buildSingleChannelSecretPromptState,
addWildcardAllowFrom,
mergeAllowFromEntries,
promptAccountId,