mirror of https://github.com/openclaw/openclaw.git
218 lines
7.0 KiB
TypeScript
218 lines
7.0 KiB
TypeScript
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<void>;
|
|
}): Promise<string | undefined> {
|
|
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;
|
|
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<void>;
|
|
noteMessage?: string;
|
|
noteTitle?: string;
|
|
}): Promise<string> {
|
|
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,
|
|
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;
|
|
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<void>;
|
|
}): Promise<string> {
|
|
const selectedMode = await resolveSecretInputModeForEnvSelection({
|
|
prompter: params.prompter,
|
|
explicitMode: params.secretInputMode,
|
|
});
|
|
const envKey = resolveEnvApiKey(params.provider);
|
|
|
|
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,
|
|
});
|
|
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,
|
|
});
|
|
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;
|
|
}
|