fix(auth): recover from malformed API-key profiles (#97520)

* fix: reject malformed API-key auth profiles

* fix(auth): detect onboard command API-key variants

* fix(auth): reject malformed API-key flags
This commit is contained in:
xingzhou
2026-06-29 02:54:24 +08:00
committed by GitHub
parent 4c477ee632
commit fb7e10e868
12 changed files with 256 additions and 28 deletions

View File

@@ -7,6 +7,7 @@ import {
maybeApplyApiKeyFromOption,
normalizeApiKeyInput,
normalizeTokenProviderInput,
validateApiKeyInput,
} from "./provider-auth-input.js";
const acceptAnyApiKeyInput = () => undefined;
@@ -240,6 +241,19 @@ describe("normalizeApiKeyInput", () => {
});
});
describe("validateApiKeyInput", () => {
it.each([
"openclaw onboard --auth-choice zai-coding-global",
"openclaw onboard --auth-choice=zai-coding-global",
"openclaw onboard --non-interactive --auth-choice zai-coding-global --zai-api-key $ZAI_API_KEY",
"openclaw onboard --non-interactive --auth-choice=zai-coding-global --zai-api-key $ZAI_API_KEY",
])("rejects pasted OpenClaw onboarding command %p", (value) => {
expect(validateApiKeyInput(value)).toBe(
"Paste the API key value, not an OpenClaw onboarding command.",
);
});
});
describe("maybeApplyApiKeyFromOption", () => {
it.each(["demo-provider", " DeMo-PrOvIdEr "])(
"stores normalized token when provider %p matches",
@@ -265,6 +279,23 @@ describe("maybeApplyApiKeyFromOption", () => {
expect(result).toBeUndefined();
expect(setCredential).not.toHaveBeenCalled();
});
it("rejects malformed command-shaped option keys before storing them", async () => {
const setCredential = vi.fn(async () => undefined);
await expect(
maybeApplyApiKeyFromOption({
token:
"openclaw onboard --non-interactive --auth-choice=zai-coding-global --zai-api-key $ZAI_API_KEY",
tokenProvider: "zai",
expectedProviders: ["zai"],
normalize: normalizeApiKeyInput,
validate: validateApiKeyInput,
setCredential,
}),
).rejects.toThrow("Paste the API key value, not an OpenClaw onboarding command.");
expect(setCredential).not.toHaveBeenCalled();
});
});
describe("ensureApiKeyFromEnvOrPrompt", () => {

View File

@@ -3,6 +3,7 @@ import {
normalizeOptionalLowercaseString,
normalizeStringifiedOptionalString,
} from "@openclaw/normalization-core/string-coerce";
import { isMalformedApiKeyInput } from "../agents/auth-profiles/credential-state.js";
import { resolveEnvApiKey } from "../agents/model-auth-env.js";
import type { OpenClawConfig } from "../config/types.js";
import type { SecretInput } from "../config/types.secrets.js";
@@ -55,8 +56,16 @@ export function normalizeApiKeyInput(raw: string): string {
}
/** Validates required API-key input for setup prompts. */
export const validateApiKeyInput = (value: string) =>
normalizeApiKeyInput(value).length > 0 ? undefined : "Required";
export const validateApiKeyInput = (value: string) => {
const normalized = normalizeApiKeyInput(value);
if (!normalized) {
return "Required";
}
if (isMalformedApiKeyInput(normalized)) {
return "Paste the API key value, not an OpenClaw onboarding command.";
}
return undefined;
};
/** Formats a redacted API-key preview for setup confirmation prompts. */
export function formatApiKeyPreview(
@@ -105,6 +114,7 @@ export async function maybeApplyApiKeyFromOption(params: {
secretInputMode?: SecretInputMode;
expectedProviders: string[];
normalize: (value: string) => string;
validate?: (value: string) => string | undefined;
setCredential: (apiKey: SecretInput, mode?: SecretInputMode) => Promise<void>;
}): Promise<string | undefined> {
const tokenProvider = normalizeTokenProviderInput(params.tokenProvider);
@@ -115,6 +125,10 @@ export async function maybeApplyApiKeyFromOption(params: {
return undefined;
}
const apiKey = params.normalize(params.token);
const validationError = params.validate?.(apiKey);
if (validationError) {
throw new Error(validationError);
}
await params.setCredential(apiKey, params.secretInputMode);
return apiKey;
}
@@ -143,6 +157,7 @@ export async function ensureApiKeyFromOptionEnvOrPrompt(params: {
secretInputMode: params.secretInputMode,
expectedProviders: params.expectedProviders,
normalize: params.normalize,
validate: params.validate,
setCredential: params.setCredential,
});
if (optionApiKey) {