import path from "node:path"; import { isDeepStrictEqual } from "node:util"; import { confirm, select, text } from "@clack/prompts"; import { listAgentIds, resolveAgentDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import type { AuthProfileStore } from "../agents/auth-profiles.js"; import { AUTH_STORE_VERSION } from "../agents/auth-profiles/constants.js"; import { resolveAuthStorePath } from "../agents/auth-profiles/paths.js"; import type { OpenClawConfig } from "../config/config.js"; import type { SecretProviderConfig, SecretRef, SecretRefSource } from "../config/types.secrets.js"; import { isSafeExecutableValue } from "../infra/exec-safety.js"; import { normalizeAgentId } from "../routing/session-key.js"; import { runSecretsApply, type SecretsApplyResult } from "./apply.js"; import { createSecretsConfigIO } from "./config-io.js"; import { buildConfigureCandidatesForScope, buildSecretsConfigurePlan, collectConfigureProviderChanges, hasConfigurePlanChanges, type ConfigureCandidate, } from "./configure-plan.js"; import type { SecretsApplyPlan } from "./plan.js"; import { PROVIDER_ENV_VARS } from "./provider-env-vars.js"; import { formatExecSecretRefIdValidationMessage, isValidExecSecretRefId, isValidSecretProviderAlias, resolveDefaultSecretProviderAlias, } from "./ref-contract.js"; import { resolveSecretRefValue } from "./resolve.js"; import { assertExpectedResolvedSecretValue } from "./secret-value.js"; import { isRecord } from "./shared.js"; import { readJsonObjectIfExists } from "./storage-scan.js"; export type SecretsConfigureResult = { plan: SecretsApplyPlan; preflight: SecretsApplyResult; }; const ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{0,127}$/; const WINDOWS_ABS_PATH_PATTERN = /^[A-Za-z]:[\\/]/; const WINDOWS_UNC_PATH_PATTERN = /^\\\\[^\\]+\\[^\\]+/; function isAbsolutePathValue(value: string): boolean { return ( path.isAbsolute(value) || WINDOWS_ABS_PATH_PATTERN.test(value) || WINDOWS_UNC_PATH_PATTERN.test(value) ); } function parseCsv(value: string): string[] { return value .split(",") .map((entry) => entry.trim()) .filter((entry) => entry.length > 0); } function parseOptionalPositiveInt(value: string, max: number): number | undefined { const trimmed = value.trim(); if (!trimmed) { return undefined; } if (!/^\d+$/.test(trimmed)) { return undefined; } const parsed = Number.parseInt(trimmed, 10); if (!Number.isFinite(parsed) || parsed <= 0 || parsed > max) { return undefined; } return parsed; } function getSecretProviders(config: OpenClawConfig): Record { if (!isRecord(config.secrets?.providers)) { return {}; } return config.secrets.providers; } function setSecretProvider( config: OpenClawConfig, providerAlias: string, providerConfig: SecretProviderConfig, ): void { config.secrets ??= {}; if (!isRecord(config.secrets.providers)) { config.secrets.providers = {}; } config.secrets.providers[providerAlias] = providerConfig; } function removeSecretProvider(config: OpenClawConfig, providerAlias: string): boolean { if (!isRecord(config.secrets?.providers)) { return false; } const providers = config.secrets.providers; if (!Object.prototype.hasOwnProperty.call(providers, providerAlias)) { return false; } delete providers[providerAlias]; if (Object.keys(providers).length === 0) { delete config.secrets?.providers; } if (isRecord(config.secrets?.defaults)) { const defaults = config.secrets.defaults; if (defaults?.env === providerAlias) { delete defaults.env; } if (defaults?.file === providerAlias) { delete defaults.file; } if (defaults?.exec === providerAlias) { delete defaults.exec; } if ( defaults && defaults.env === undefined && defaults.file === undefined && defaults.exec === undefined ) { delete config.secrets?.defaults; } } return true; } function providerHint(provider: SecretProviderConfig): string { if (provider.source === "env") { return provider.allowlist?.length ? `env (${provider.allowlist.length} allowlisted)` : "env"; } if (provider.source === "file") { return `file (${provider.mode ?? "json"})`; } return `exec (${provider.jsonOnly === false ? "json+text" : "json"})`; } function toSourceChoices(config: OpenClawConfig): Array<{ value: SecretRefSource; label: string }> { const hasSource = (source: SecretRefSource) => Object.values(config.secrets?.providers ?? {}).some((provider) => provider?.source === source); const choices: Array<{ value: SecretRefSource; label: string }> = [ { value: "env", label: "env", }, ]; if (hasSource("file")) { choices.push({ value: "file", label: "file" }); } if (hasSource("exec")) { choices.push({ value: "exec", label: "exec" }); } return choices; } function assertNoCancel(value: T | symbol, message: string): T { if (typeof value === "symbol") { throw new Error(message); } return value; } const AUTH_PROFILE_ID_PATTERN = /^[A-Za-z0-9:_-]{1,128}$/; function validateEnvNameCsv(value: string): string | undefined { const entries = parseCsv(value); for (const entry of entries) { if (!ENV_NAME_PATTERN.test(entry)) { return `Invalid env name: ${entry}`; } } return undefined; } async function promptEnvNameCsv(params: { message: string; initialValue: string; }): Promise { const raw = assertNoCancel( await text({ message: params.message, initialValue: params.initialValue, validate: (value) => validateEnvNameCsv(String(value ?? "")), }), "Secrets configure cancelled.", ); return parseCsv(String(raw ?? "")); } async function promptOptionalPositiveInt(params: { message: string; initialValue?: number; max: number; }): Promise { const raw = assertNoCancel( await text({ message: params.message, initialValue: params.initialValue === undefined ? "" : String(params.initialValue), validate: (value) => { const trimmed = String(value ?? "").trim(); if (!trimmed) { return undefined; } const parsed = parseOptionalPositiveInt(trimmed, params.max); if (parsed === undefined) { return `Must be an integer between 1 and ${params.max}`; } return undefined; }, }), "Secrets configure cancelled.", ); const parsed = parseOptionalPositiveInt(String(raw ?? ""), params.max); return parsed; } function configureCandidateKey(candidate: { configFile: "openclaw.json" | "auth-profiles.json"; path: string; agentId?: string; }): string { if (candidate.configFile === "auth-profiles.json") { return `auth-profiles:${String(candidate.agentId ?? "").trim()}:${candidate.path}`; } return `openclaw:${candidate.path}`; } function hasSourceChoice( sourceChoices: Array<{ value: SecretRefSource; label: string }>, source: SecretRefSource, ): boolean { return sourceChoices.some((entry) => entry.value === source); } function resolveCandidateProviderHint(candidate: ConfigureCandidate): string | undefined { if (typeof candidate.authProfileProvider === "string" && candidate.authProfileProvider.trim()) { return candidate.authProfileProvider.trim().toLowerCase(); } if (typeof candidate.providerId === "string" && candidate.providerId.trim()) { return candidate.providerId.trim().toLowerCase(); } return undefined; } function resolveSuggestedEnvSecretId(candidate: ConfigureCandidate): string | undefined { const hintedProvider = resolveCandidateProviderHint(candidate); if (!hintedProvider) { return undefined; } const envCandidates = PROVIDER_ENV_VARS[hintedProvider]; if (!Array.isArray(envCandidates) || envCandidates.length === 0) { return undefined; } return envCandidates[0]; } function resolveConfigureAgentId(config: OpenClawConfig, explicitAgentId?: string): string { const knownAgentIds = new Set(listAgentIds(config)); if (!explicitAgentId) { return resolveDefaultAgentId(config); } const normalized = normalizeAgentId(explicitAgentId); if (knownAgentIds.has(normalized)) { return normalized; } const known = [...knownAgentIds].toSorted().join(", "); throw new Error( `Unknown agent id "${explicitAgentId}". Known agents: ${known || "none configured"}.`, ); } function normalizeAuthStoreForConfigure( raw: Record | null, storePath: string, ): AuthProfileStore { if (!raw) { return { version: AUTH_STORE_VERSION, profiles: {}, }; } if (!isRecord(raw.profiles)) { throw new Error( `Cannot run interactive secrets configure because ${storePath} is invalid (missing "profiles" object).`, ); } const version = typeof raw.version === "number" && Number.isFinite(raw.version) ? raw.version : 1; return { version, profiles: raw.profiles as AuthProfileStore["profiles"], ...(isRecord(raw.order) ? { order: raw.order as AuthProfileStore["order"] } : {}), ...(isRecord(raw.lastGood) ? { lastGood: raw.lastGood as AuthProfileStore["lastGood"] } : {}), ...(isRecord(raw.usageStats) ? { usageStats: raw.usageStats as AuthProfileStore["usageStats"] } : {}), }; } function loadAuthProfileStoreForConfigure(params: { config: OpenClawConfig; agentId: string; }): AuthProfileStore { const agentDir = resolveAgentDir(params.config, params.agentId); const storePath = resolveAuthStorePath(agentDir); const parsed = readJsonObjectIfExists(storePath); if (parsed.error) { throw new Error( `Cannot run interactive secrets configure because ${storePath} could not be read: ${parsed.error}`, ); } return normalizeAuthStoreForConfigure(parsed.value, storePath); } async function promptNewAuthProfileCandidate(agentId: string): Promise { const profileId = assertNoCancel( await text({ message: "Auth profile id", validate: (value) => { const trimmed = String(value ?? "").trim(); if (!trimmed) { return "Required"; } if (!AUTH_PROFILE_ID_PATTERN.test(trimmed)) { return 'Use letters/numbers/":"/"_"/"-" only.'; } return undefined; }, }), "Secrets configure cancelled.", ); const credentialType = assertNoCancel( await select({ message: "Auth profile credential type", options: [ { value: "api_key", label: "api_key (key/keyRef)" }, { value: "token", label: "token (token/tokenRef)" }, ], }), "Secrets configure cancelled.", ); const provider = assertNoCancel( await text({ message: "Provider id", validate: (value) => (String(value ?? "").trim().length > 0 ? undefined : "Required"), }), "Secrets configure cancelled.", ); const profileIdTrimmed = String(profileId).trim(); const providerTrimmed = String(provider).trim(); if (credentialType === "token") { return { type: "auth-profiles.token.token", path: `profiles.${profileIdTrimmed}.token`, pathSegments: ["profiles", profileIdTrimmed, "token"], label: `profiles.${profileIdTrimmed}.token (auth profile, agent ${agentId})`, configFile: "auth-profiles.json", agentId, authProfileProvider: providerTrimmed, expectedResolvedValue: "string", }; } return { type: "auth-profiles.api_key.key", path: `profiles.${profileIdTrimmed}.key`, pathSegments: ["profiles", profileIdTrimmed, "key"], label: `profiles.${profileIdTrimmed}.key (auth profile, agent ${agentId})`, configFile: "auth-profiles.json", agentId, authProfileProvider: providerTrimmed, expectedResolvedValue: "string", }; } async function promptProviderAlias(params: { existingAliases: Set }): Promise { const alias = assertNoCancel( await text({ message: "Provider alias", initialValue: "default", validate: (value) => { const trimmed = String(value ?? "").trim(); if (!trimmed) { return "Required"; } if (!isValidSecretProviderAlias(trimmed)) { return "Must match /^[a-z][a-z0-9_-]{0,63}$/"; } if (params.existingAliases.has(trimmed)) { return "Alias already exists"; } return undefined; }, }), "Secrets configure cancelled.", ); return String(alias).trim(); } async function promptProviderSource(initial?: SecretRefSource): Promise { const source = assertNoCancel( await select({ message: "Provider source", options: [ { value: "env", label: "env" }, { value: "file", label: "file" }, { value: "exec", label: "exec" }, ], initialValue: initial, }), "Secrets configure cancelled.", ); return source as SecretRefSource; } async function promptEnvProvider( base?: Extract, ): Promise> { const allowlist = await promptEnvNameCsv({ message: "Env allowlist (comma-separated, blank for unrestricted)", initialValue: base?.allowlist?.join(",") ?? "", }); return { source: "env", ...(allowlist.length > 0 ? { allowlist } : {}), }; } async function promptFileProvider( base?: Extract, ): Promise> { const filePath = assertNoCancel( await text({ message: "File path (absolute)", initialValue: base?.path ?? "", validate: (value) => { const trimmed = String(value ?? "").trim(); if (!trimmed) { return "Required"; } if (!isAbsolutePathValue(trimmed)) { return "Must be an absolute path"; } return undefined; }, }), "Secrets configure cancelled.", ); const mode = assertNoCancel( await select({ message: "File mode", options: [ { value: "json", label: "json" }, { value: "singleValue", label: "singleValue" }, ], initialValue: base?.mode ?? "json", }), "Secrets configure cancelled.", ); const timeoutMs = await promptOptionalPositiveInt({ message: "Timeout ms (blank for default)", initialValue: base?.timeoutMs, max: 120000, }); const maxBytes = await promptOptionalPositiveInt({ message: "Max bytes (blank for default)", initialValue: base?.maxBytes, max: 20 * 1024 * 1024, }); return { source: "file", path: String(filePath).trim(), mode, ...(timeoutMs ? { timeoutMs } : {}), ...(maxBytes ? { maxBytes } : {}), }; } async function parseArgsInput(rawValue: string): Promise { const trimmed = rawValue.trim(); if (!trimmed) { return undefined; } const parsed = JSON.parse(trimmed) as unknown; if (!Array.isArray(parsed) || !parsed.every((entry) => typeof entry === "string")) { throw new Error("args must be a JSON array of strings"); } return parsed; } async function promptExecProvider( base?: Extract, ): Promise> { const command = assertNoCancel( await text({ message: "Command path (absolute)", initialValue: base?.command ?? "", validate: (value) => { const trimmed = String(value ?? "").trim(); if (!trimmed) { return "Required"; } if (!isAbsolutePathValue(trimmed)) { return "Must be an absolute path"; } if (!isSafeExecutableValue(trimmed)) { return "Command value is not allowed"; } return undefined; }, }), "Secrets configure cancelled.", ); const argsRaw = assertNoCancel( await text({ message: "Args JSON array (blank for none)", initialValue: JSON.stringify(base?.args ?? []), validate: (value) => { const trimmed = String(value ?? "").trim(); if (!trimmed) { return undefined; } try { const parsed = JSON.parse(trimmed) as unknown; if (!Array.isArray(parsed) || !parsed.every((entry) => typeof entry === "string")) { return "Must be a JSON array of strings"; } return undefined; } catch { return "Must be valid JSON"; } }, }), "Secrets configure cancelled.", ); const timeoutMs = await promptOptionalPositiveInt({ message: "Timeout ms (blank for default)", initialValue: base?.timeoutMs, max: 120000, }); const noOutputTimeoutMs = await promptOptionalPositiveInt({ message: "No-output timeout ms (blank for default)", initialValue: base?.noOutputTimeoutMs, max: 120000, }); const maxOutputBytes = await promptOptionalPositiveInt({ message: "Max output bytes (blank for default)", initialValue: base?.maxOutputBytes, max: 20 * 1024 * 1024, }); const jsonOnly = assertNoCancel( await confirm({ message: "Require JSON-only response?", initialValue: base?.jsonOnly ?? true, }), "Secrets configure cancelled.", ); const passEnv = await promptEnvNameCsv({ message: "Pass-through env vars (comma-separated, blank for none)", initialValue: base?.passEnv?.join(",") ?? "", }); const trustedDirsRaw = assertNoCancel( await text({ message: "Trusted dirs (comma-separated absolute paths, blank for none)", initialValue: base?.trustedDirs?.join(",") ?? "", validate: (value) => { const entries = parseCsv(String(value ?? "")); for (const entry of entries) { if (!isAbsolutePathValue(entry)) { return `Trusted dir must be absolute: ${entry}`; } } return undefined; }, }), "Secrets configure cancelled.", ); const allowInsecurePath = assertNoCancel( await confirm({ message: "Allow insecure command path checks?", initialValue: base?.allowInsecurePath ?? false, }), "Secrets configure cancelled.", ); const allowSymlinkCommand = assertNoCancel( await confirm({ message: "Allow symlink command path?", initialValue: base?.allowSymlinkCommand ?? false, }), "Secrets configure cancelled.", ); const args = await parseArgsInput(String(argsRaw ?? "")); const trustedDirs = parseCsv(String(trustedDirsRaw ?? "")); return { source: "exec", command: String(command).trim(), ...(args && args.length > 0 ? { args } : {}), ...(timeoutMs ? { timeoutMs } : {}), ...(noOutputTimeoutMs ? { noOutputTimeoutMs } : {}), ...(maxOutputBytes ? { maxOutputBytes } : {}), ...(jsonOnly ? { jsonOnly } : { jsonOnly: false }), ...(passEnv.length > 0 ? { passEnv } : {}), ...(trustedDirs.length > 0 ? { trustedDirs } : {}), ...(allowInsecurePath ? { allowInsecurePath: true } : {}), ...(allowSymlinkCommand ? { allowSymlinkCommand: true } : {}), ...(isRecord(base?.env) ? { env: base.env } : {}), }; } async function promptProviderConfig( source: SecretRefSource, current?: SecretProviderConfig, ): Promise { if (source === "env") { return await promptEnvProvider(current?.source === "env" ? current : undefined); } if (source === "file") { return await promptFileProvider(current?.source === "file" ? current : undefined); } return await promptExecProvider(current?.source === "exec" ? current : undefined); } async function configureProvidersInteractive(config: OpenClawConfig): Promise { while (true) { const providers = getSecretProviders(config); const providerEntries = Object.entries(providers).toSorted(([left], [right]) => left.localeCompare(right), ); const actionOptions: Array<{ value: string; label: string; hint?: string }> = [ { value: "add", label: "Add provider", hint: "Define a new env/file/exec provider", }, ]; if (providerEntries.length > 0) { actionOptions.push({ value: "edit", label: "Edit provider", hint: "Update an existing provider", }); actionOptions.push({ value: "remove", label: "Remove provider", hint: "Delete a provider alias", }); } actionOptions.push({ value: "continue", label: "Continue", hint: "Move to credential mapping", }); const action = assertNoCancel( await select({ message: providerEntries.length > 0 ? "Configure secret providers" : "Configure secret providers (only env refs are available until file/exec providers are added)", options: actionOptions, }), "Secrets configure cancelled.", ); if (action === "continue") { return; } if (action === "add") { const source = await promptProviderSource(); const alias = await promptProviderAlias({ existingAliases: new Set(providerEntries.map(([providerAlias]) => providerAlias)), }); const providerConfig = await promptProviderConfig(source); setSecretProvider(config, alias, providerConfig); continue; } if (action === "edit") { const alias = assertNoCancel( await select({ message: "Select provider to edit", options: providerEntries.map(([providerAlias, providerConfig]) => ({ value: providerAlias, label: providerAlias, hint: providerHint(providerConfig), })), }), "Secrets configure cancelled.", ); const current = providers[alias]; if (!current) { continue; } const source = await promptProviderSource(current.source); const nextProviderConfig = await promptProviderConfig(source, current); if (!isDeepStrictEqual(current, nextProviderConfig)) { setSecretProvider(config, alias, nextProviderConfig); } continue; } if (action === "remove") { const alias = assertNoCancel( await select({ message: "Select provider to remove", options: providerEntries.map(([providerAlias, providerConfig]) => ({ value: providerAlias, label: providerAlias, hint: providerHint(providerConfig), })), }), "Secrets configure cancelled.", ); const shouldRemove = assertNoCancel( await confirm({ message: `Remove provider "${alias}"?`, initialValue: false, }), "Secrets configure cancelled.", ); if (shouldRemove) { removeSecretProvider(config, alias); } } } } export async function runSecretsConfigureInteractive( params: { env?: NodeJS.ProcessEnv; providersOnly?: boolean; skipProviderSetup?: boolean; agentId?: string; } = {}, ): Promise { if (!process.stdin.isTTY) { throw new Error("secrets configure requires an interactive TTY."); } if (params.providersOnly && params.skipProviderSetup) { throw new Error("Cannot combine --providers-only with --skip-provider-setup."); } const env = params.env ?? process.env; const io = createSecretsConfigIO({ env }); const { snapshot } = await io.readConfigFileSnapshotForWrite(); if (!snapshot.valid) { throw new Error("Cannot run interactive secrets configure because config is invalid."); } const stagedConfig = structuredClone(snapshot.config); if (!params.skipProviderSetup) { await configureProvidersInteractive(stagedConfig); } const providerChanges = collectConfigureProviderChanges({ original: snapshot.config, next: stagedConfig, }); const selectedByPath = new Map(); if (!params.providersOnly) { const configureAgentId = resolveConfigureAgentId(snapshot.config, params.agentId); const authStore = loadAuthProfileStoreForConfigure({ config: snapshot.config, agentId: configureAgentId, }); const candidates = buildConfigureCandidatesForScope({ config: stagedConfig, authoredOpenClawConfig: snapshot.resolved, authProfiles: { agentId: configureAgentId, store: authStore, }, }); if (candidates.length === 0) { throw new Error("No configurable secret-bearing fields found for this agent scope."); } const sourceChoices = toSourceChoices(stagedConfig); const hasDerivedCandidates = candidates.some((candidate) => candidate.isDerived === true); let showDerivedCandidates = false; while (true) { const visibleCandidates = showDerivedCandidates ? candidates : candidates.filter((candidate) => candidate.isDerived !== true); const options = visibleCandidates.map((candidate) => ({ value: configureCandidateKey(candidate), label: candidate.label, hint: [ candidate.configFile === "auth-profiles.json" ? "auth-profiles.json" : "openclaw.json", candidate.isDerived === true ? "derived" : undefined, ] .filter(Boolean) .join(" | "), })); options.push({ value: "__create_auth_profile__", label: "Create auth profile mapping", hint: `Add a new auth-profiles target for agent ${configureAgentId}`, }); if (hasDerivedCandidates) { options.push({ value: "__toggle_derived__", label: showDerivedCandidates ? "Hide derived targets" : "Show derived targets", hint: showDerivedCandidates ? "Show only fields authored directly in config" : "Include normalized/derived aliases", }); } if (selectedByPath.size > 0) { options.unshift({ value: "__done__", label: "Done", hint: "Finish and run preflight", }); } const selectedPath = assertNoCancel( await select({ message: "Select credential field", options, }), "Secrets configure cancelled.", ); if (selectedPath === "__done__") { break; } if (selectedPath === "__create_auth_profile__") { const createdCandidate = await promptNewAuthProfileCandidate(configureAgentId); const key = configureCandidateKey(createdCandidate); const existingIndex = candidates.findIndex((entry) => configureCandidateKey(entry) === key); if (existingIndex >= 0) { candidates[existingIndex] = createdCandidate; } else { candidates.push(createdCandidate); } continue; } if (selectedPath === "__toggle_derived__") { showDerivedCandidates = !showDerivedCandidates; continue; } const candidate = visibleCandidates.find( (entry) => configureCandidateKey(entry) === selectedPath, ); if (!candidate) { throw new Error(`Unknown configure target: ${selectedPath}`); } const candidateKey = configureCandidateKey(candidate); const priorSelection = selectedByPath.get(candidateKey); const existingRef = priorSelection?.ref ?? candidate.existingRef; const sourceInitialValue = existingRef && hasSourceChoice(sourceChoices, existingRef.source) ? existingRef.source : undefined; const source = assertNoCancel( await select({ message: "Secret source", options: sourceChoices, initialValue: sourceInitialValue, }), "Secrets configure cancelled.", ) as SecretRefSource; const defaultAlias = resolveDefaultSecretProviderAlias(stagedConfig, source, { preferFirstProviderForSource: true, }); const providerInitialValue = existingRef?.source === source ? existingRef.provider : defaultAlias; const provider = assertNoCancel( await text({ message: "Provider alias", initialValue: providerInitialValue, validate: (value) => { const trimmed = String(value ?? "").trim(); if (!trimmed) { return "Required"; } if (!isValidSecretProviderAlias(trimmed)) { return "Must match /^[a-z][a-z0-9_-]{0,63}$/"; } return undefined; }, }), "Secrets configure cancelled.", ); const providerAlias = String(provider).trim(); const suggestedIdFromExistingRef = existingRef?.source === source ? existingRef.id : undefined; let suggestedId = suggestedIdFromExistingRef; if (!suggestedId && source === "env") { suggestedId = resolveSuggestedEnvSecretId(candidate); } if (!suggestedId && source === "file") { const configuredProvider = stagedConfig.secrets?.providers?.[providerAlias]; if (configuredProvider?.source === "file" && configuredProvider.mode === "singleValue") { suggestedId = "value"; } } const id = assertNoCancel( await text({ message: "Secret id", initialValue: suggestedId, validate: (value) => { const trimmed = String(value ?? "").trim(); if (!trimmed) { return "Required"; } if (source === "exec" && !isValidExecSecretRefId(trimmed)) { return formatExecSecretRefIdValidationMessage(); } return undefined; }, }), "Secrets configure cancelled.", ); const ref: SecretRef = { source, provider: providerAlias, id: String(id).trim(), }; const resolved = await resolveSecretRefValue(ref, { config: stagedConfig, env, }); assertExpectedResolvedSecretValue({ value: resolved, expected: candidate.expectedResolvedValue, errorMessage: candidate.expectedResolvedValue === "string" ? `Ref ${ref.source}:${ref.provider}:${ref.id} did not resolve to a non-empty string.` : `Ref ${ref.source}:${ref.provider}:${ref.id} did not resolve to a supported value type.`, }); const next = { ...candidate, ref, }; selectedByPath.set(candidateKey, next); const addMore = assertNoCancel( await confirm({ message: "Configure another credential?", initialValue: true, }), "Secrets configure cancelled.", ); if (!addMore) { break; } } } if (!hasConfigurePlanChanges({ selectedTargets: selectedByPath, providerChanges })) { throw new Error("No secrets changes were selected."); } const plan = buildSecretsConfigurePlan({ selectedTargets: selectedByPath, providerChanges, }); const preflight = await runSecretsApply({ plan, env, write: false, }); return { plan, preflight }; }