import { resolveEnvApiKey } from "../agents/model-auth-env.js"; import type { OpenClawConfig } from "../config/types.js"; import type { SecretInput } from "../config/types.secrets.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import { resolveSecretInputModeForEnvSelection, type SecretInputModePromptCopy, } from "./provider-auth-mode.js"; import { extractEnvVarFromSourceLabel, promptSecretRefForSetup, resolveRefFallbackInput, type SecretRefSetupPromptCopy, } from "./provider-auth-ref.js"; import type { SecretInputMode } from "./provider-auth-types.js"; export { extractEnvVarFromSourceLabel, promptSecretRefForSetup, resolveRefFallbackInput, type SecretRefSetupPromptCopy, } from "./provider-auth-ref.js"; export { resolveSecretInputModeForEnvSelection, type SecretInputModePromptCopy, } from "./provider-auth-mode.js"; const DEFAULT_KEY_PREVIEW = { head: 4, tail: 4 }; export function normalizeApiKeyInput(raw: string): string { const trimmed = String(raw ?? "").trim(); if (!trimmed) { return ""; } const assignmentMatch = trimmed.match(/^(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*(.+)$/); const valuePart = assignmentMatch ? assignmentMatch[1].trim() : trimmed; const unquoted = valuePart.length >= 2 && ((valuePart.startsWith('"') && valuePart.endsWith('"')) || (valuePart.startsWith("'") && valuePart.endsWith("'")) || (valuePart.startsWith("`") && valuePart.endsWith("`"))) ? valuePart.slice(1, -1) : valuePart; const withoutSemicolon = unquoted.endsWith(";") ? unquoted.slice(0, -1) : unquoted; return withoutSemicolon.trim(); } export const validateApiKeyInput = (value: string) => normalizeApiKeyInput(value).length > 0 ? undefined : "Required"; export function formatApiKeyPreview( raw: string, opts: { head?: number; tail?: number } = {}, ): string { const trimmed = raw.trim(); if (!trimmed) { return "…"; } const head = opts.head ?? DEFAULT_KEY_PREVIEW.head; const tail = opts.tail ?? DEFAULT_KEY_PREVIEW.tail; if (trimmed.length <= head + tail) { const shortHead = Math.min(2, trimmed.length); const shortTail = Math.min(2, trimmed.length - shortHead); if (shortTail <= 0) { return `${trimmed.slice(0, shortHead)}…`; } return `${trimmed.slice(0, shortHead)}…${trimmed.slice(-shortTail)}`; } return `${trimmed.slice(0, head)}…${trimmed.slice(-tail)}`; } export function normalizeTokenProviderInput( tokenProvider: string | null | undefined, ): string | undefined { const normalized = String(tokenProvider ?? "") .trim() .toLowerCase(); return normalized || undefined; } export function normalizeSecretInputModeInput( secretInputMode: string | null | undefined, ): SecretInputMode | undefined { const normalized = String(secretInputMode ?? "") .trim() .toLowerCase(); if (normalized === "plaintext" || normalized === "ref") { return normalized; } return undefined; } export async function maybeApplyApiKeyFromOption(params: { token: string | undefined; tokenProvider: string | undefined; secretInputMode?: SecretInputMode; expectedProviders: string[]; normalize: (value: string) => string; setCredential: (apiKey: SecretInput, mode?: SecretInputMode) => Promise; }): Promise { const tokenProvider = normalizeTokenProviderInput(params.tokenProvider); const expectedProviders = params.expectedProviders .map((provider) => normalizeTokenProviderInput(provider)) .filter((provider): provider is string => Boolean(provider)); if (!params.token || !tokenProvider || !expectedProviders.includes(tokenProvider)) { return undefined; } const apiKey = params.normalize(params.token); await params.setCredential(apiKey, params.secretInputMode); return apiKey; } export async function ensureApiKeyFromOptionEnvOrPrompt(params: { token: string | undefined; tokenProvider: string | undefined; secretInputMode?: SecretInputMode; config: OpenClawConfig; env?: NodeJS.ProcessEnv; expectedProviders: string[]; provider: string; envLabel: string; promptMessage: string; normalize: (value: string) => string; validate: (value: string) => string | undefined; prompter: WizardPrompter; setCredential: (apiKey: SecretInput, mode?: SecretInputMode) => Promise; noteMessage?: string; noteTitle?: string; }): Promise { const optionApiKey = await maybeApplyApiKeyFromOption({ token: params.token, tokenProvider: params.tokenProvider, secretInputMode: params.secretInputMode, expectedProviders: params.expectedProviders, normalize: params.normalize, setCredential: params.setCredential, }); if (optionApiKey) { return optionApiKey; } if (params.noteMessage) { await params.prompter.note(params.noteMessage, params.noteTitle); } return await ensureApiKeyFromEnvOrPrompt({ config: params.config, env: params.env, provider: params.provider, envLabel: params.envLabel, promptMessage: params.promptMessage, normalize: params.normalize, validate: params.validate, prompter: params.prompter, secretInputMode: params.secretInputMode, setCredential: params.setCredential, }); } export async function ensureApiKeyFromEnvOrPrompt(params: { config: OpenClawConfig; env?: NodeJS.ProcessEnv; provider: string; envLabel: string; promptMessage: string; normalize: (value: string) => string; validate: (value: string) => string | undefined; prompter: WizardPrompter; secretInputMode?: SecretInputMode; setCredential: (apiKey: SecretInput, mode?: SecretInputMode) => Promise; }): Promise { const selectedMode = await resolveSecretInputModeForEnvSelection({ prompter: params.prompter, explicitMode: params.secretInputMode, }); const env = params.env ?? process.env; const envKey = resolveEnvApiKey(params.provider, env); if (selectedMode === "ref") { if (typeof params.prompter.select !== "function") { const fallback = resolveRefFallbackInput({ config: params.config, provider: params.provider, preferredEnvVar: envKey?.source ? extractEnvVarFromSourceLabel(envKey.source) : undefined, env, }); await params.setCredential(fallback.ref, selectedMode); return fallback.resolvedValue; } const resolved = await promptSecretRefForSetup({ provider: params.provider, config: params.config, prompter: params.prompter, preferredEnvVar: envKey?.source ? extractEnvVarFromSourceLabel(envKey.source) : undefined, env, }); await params.setCredential(resolved.ref, selectedMode); return resolved.resolvedValue; } if (envKey && selectedMode === "plaintext") { const useExisting = await params.prompter.confirm({ message: `Use existing ${params.envLabel} (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, initialValue: true, }); if (useExisting) { await params.setCredential(envKey.apiKey, selectedMode); return envKey.apiKey; } } const key = await params.prompter.text({ message: params.promptMessage, validate: params.validate, }); const apiKey = params.normalize(String(key ?? "")); await params.setCredential(apiKey, selectedMode); return apiKey; }