From 8049f33435bfb2d9a526d0d0d1aa553053fe6864 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 11:18:31 +0000 Subject: [PATCH] chore: sanitize onboarding api keys --- CHANGELOG.md | 32 ++++++++++++- src/commands/auth-choice.test.ts | 4 +- src/commands/auth-choice.ts | 78 +++++++++++++++++++++++++------- 3 files changed, 95 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f54cbd323..33984bcca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,37 @@ # Changelog -## 2026.1.12 +## 2026.1.12-1 ### Changes -- Cron: add optional `agentId` binding (CLI `--agent` / `--clear-agent`), route cron runs + summaries to the chosen agent, and document/test the fallback to the default agent. (#770) +- Heartbeat: raise default `ackMaxChars` to 300 so any `HEARTBEAT_OK` replies with short padding stay internal (fewer noisy heartbeat posts on providers). +- Onboarding: normalize API key inputs (strip `export KEY=...` wrappers) so shell-style entries paste cleanly. + +## 2026.1.11-5 + +### Fixes +- Auto-reply: prevent duplicate /status replies (including /usage alias) and add tests for inline + standalone cases. + +## 2026.1.11-4 + +### Fixes +- CLI: read the git commit hash from the package root so npm installs show it. + +## 2026.1.11-3 + +### Fixes +- CLI: avoid top-level await warnings in the entrypoint on fresh installs. +- CLI: show a commit hash in the banner for npm installs (package.json gitHead fallback). + +## 2026.1.11-2 + +### Fixes +- Installer: ensure the CLI entrypoint is executable after npm installs. +- Packaging: include `dist/plugins/` in the npm package to avoid missing module errors. + +## 2026.1.11-1 + +### Fixes +- Installer: include `patches/` in the npm package so postinstall patching works for npm/bun installs. ## 2026.1.11 diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index a9888a635..4d0bf524b 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -51,7 +51,9 @@ describe("applyAuthChoice", () => { process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent"); process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR; - const text = vi.fn().mockResolvedValue("sk-minimax-test"); + const text = vi + .fn() + .mockResolvedValue('export MINIMAX_API_KEY="sk-minimax-test"'); const select: WizardPrompter["select"] = vi.fn( async (params) => params.options[0]?.value as never, ); diff --git a/src/commands/auth-choice.ts b/src/commands/auth-choice.ts index f0f18f1e7..d59ceda85 100644 --- a/src/commands/auth-choice.ts +++ b/src/commands/auth-choice.ts @@ -70,6 +70,34 @@ import { OPENCODE_ZEN_DEFAULT_MODEL } from "./opencode-zen-model-default.js"; const DEFAULT_KEY_PREVIEW = { head: 4, tail: 4 }; +function normalizeApiKeyInput(raw: string): string { + const trimmed = String(raw ?? "").trim(); + if (!trimmed) return ""; + + // Handle shell-style assignments: export KEY="value" or KEY=value + 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(); +} + +const validateApiKeyInput = (value: unknown) => + normalizeApiKeyInput(String(value ?? "")).length > 0 ? undefined : "Required"; + function formatApiKeyPreview( raw: string, opts: { head?: number; tail?: number } = {}, @@ -381,9 +409,9 @@ export async function applyAuthChoice(params: { const key = await params.prompter.text({ message: "Enter OpenAI API key", - validate: (value) => (value?.trim() ? undefined : "Required"), + validate: validateApiKeyInput, }); - const trimmed = String(key).trim(); + const trimmed = normalizeApiKeyInput(String(key)); const result = upsertSharedEnvVar({ key: "OPENAI_API_KEY", value: trimmed, @@ -440,9 +468,12 @@ export async function applyAuthChoice(params: { if (!hasCredential) { const key = await params.prompter.text({ message: "Enter OpenRouter API key", - validate: (value) => (value?.trim() ? undefined : "Required"), + validate: validateApiKeyInput, }); - await setOpenrouterApiKey(String(key).trim(), params.agentDir); + await setOpenrouterApiKey( + normalizeApiKeyInput(String(key)), + params.agentDir, + ); hasCredential = true; } @@ -480,9 +511,12 @@ export async function applyAuthChoice(params: { if (!hasCredential) { const key = await params.prompter.text({ message: "Enter Moonshot API key", - validate: (value) => (value?.trim() ? undefined : "Required"), + validate: validateApiKeyInput, }); - await setMoonshotApiKey(String(key).trim(), params.agentDir); + await setMoonshotApiKey( + normalizeApiKeyInput(String(key)), + params.agentDir, + ); } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "moonshot:default", @@ -723,9 +757,12 @@ export async function applyAuthChoice(params: { if (!hasCredential) { const key = await params.prompter.text({ message: "Enter Gemini API key", - validate: (value) => (value?.trim() ? undefined : "Required"), + validate: validateApiKeyInput, }); - await setGeminiApiKey(String(key).trim(), params.agentDir); + await setGeminiApiKey( + normalizeApiKeyInput(String(key)), + params.agentDir, + ); } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "google:default", @@ -761,9 +798,9 @@ export async function applyAuthChoice(params: { if (!hasCredential) { const key = await params.prompter.text({ message: "Enter Z.AI API key", - validate: (value) => (value?.trim() ? undefined : "Required"), + validate: validateApiKeyInput, }); - await setZaiApiKey(String(key).trim(), params.agentDir); + await setZaiApiKey(normalizeApiKeyInput(String(key)), params.agentDir); } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "zai:default", @@ -814,9 +851,12 @@ export async function applyAuthChoice(params: { if (!hasCredential) { const key = await params.prompter.text({ message: "Enter Anthropic API key", - validate: (value) => (value?.trim() ? undefined : "Required"), + validate: validateApiKeyInput, }); - await setAnthropicApiKey(String(key).trim(), params.agentDir); + await setAnthropicApiKey( + normalizeApiKeyInput(String(key)), + params.agentDir, + ); } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "anthropic:default", @@ -847,9 +887,12 @@ export async function applyAuthChoice(params: { if (!hasCredential) { const key = await params.prompter.text({ message: "Enter MiniMax API key", - validate: (value) => (value?.trim() ? undefined : "Required"), + validate: validateApiKeyInput, }); - await setMinimaxApiKey(String(key).trim(), params.agentDir); + await setMinimaxApiKey( + normalizeApiKeyInput(String(key)), + params.agentDir, + ); } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "minimax:default", @@ -896,9 +939,12 @@ export async function applyAuthChoice(params: { if (!hasCredential) { const key = await params.prompter.text({ message: "Enter OpenCode Zen API key", - validate: (value) => (value?.trim() ? undefined : "Required"), + validate: validateApiKeyInput, }); - await setOpencodeZenApiKey(String(key).trim(), params.agentDir); + await setOpencodeZenApiKey( + normalizeApiKeyInput(String(key)), + params.agentDir, + ); } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "opencode:default",