feat: validate runway api key format
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-21 00:41:23 +08:00
parent 1eb23a015f
commit 404e50a4d6
2 changed files with 53 additions and 1 deletions

View File

@ -6877,7 +6877,13 @@ export function AdminSettingsClient() {
value={providerForm.apiKey}
onChange={(e) => setProviderForm({ ...providerForm, apiKey: e.target.value })}
autoComplete="new-password"
placeholder={editingProvider ? "留空则不修改" : "sk-..."}
placeholder={
editingProvider
? "留空则不修改"
: providerForm.type === "runway"
? "key_ 后跟 128 位十六进制字符"
: "sk-..."
}
/>
<Button
type="button" variant="ghost" size="sm"
@ -6887,6 +6893,11 @@ export function AdminSettingsClient() {
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
{providerForm.type === "runway" && (
<p className="text-xs text-muted-foreground">
Runway Key 必须以 `key_` 开头,后面是 128 位十六进制字符。不要粘贴带“Bearer ”、“API Key:”或中文标点的内容。
</p>
)}
</div>
<div className="flex items-center gap-2">
<Switch

View File

@ -61,6 +61,7 @@ const OPENAI_IMAGE_BASE_URL = "https://api.openai.com/v1";
const MIDJOURNEY_BASE_URL = "https://your-midjourney-api.example.com";
const TRIPO_3D_BASE_URL = "https://api.tripo3d.ai/v2/openapi";
const RUNWAY_BASE_URL = "https://api.dev.runwayml.com";
const RUNWAY_API_KEY_PATTERN = /^key_[0-9a-f]{128}$/;
function normalizeProviderTypeForStorage(
type?: ProviderType
@ -70,6 +71,26 @@ function normalizeProviderTypeForStorage(
return type;
}
function normalizeRunwayApiKey(value: string) {
return String(value || "")
.replace(/\u00a0/g, " ")
.trim()
.normalize("NFKC")
.replace(/^bearer\s+/i, "")
.replace(/^key\s+/i, "")
.replace(/^(?:runway[_-]?api[_-]?key|api[_-]?key|token)\s*[::]\s*/i, "")
.replace(/[^\x20-\x7E]/g, "");
}
function validateRunwayApiKey(apiKey: string) {
if (!RUNWAY_API_KEY_PATTERN.test(apiKey)) {
throw new ChatSDKError(
"bad_request:api",
"Runway API Key 格式无效,应为 key_ 开头并跟随 128 位十六进制字符"
);
}
}
function isZhipuBaseUrl(baseUrl: string) {
return baseUrl.includes("open.bigmodel.cn");
}
@ -154,11 +175,17 @@ export async function POST(request: Request) {
console.log("[Admin API] Create provider request body:", JSON.stringify(sanitizeProviderPayload(body)));
const data = createProviderSchema.parse(body);
const normalizedApiKey =
data.type === "runway" ? normalizeRunwayApiKey(data.apiKey) : data.apiKey;
if (data.type === "runway") {
validateRunwayApiKey(normalizedApiKey);
}
console.log("[Admin API] Validation passed, creating provider...");
const storageType = normalizeProviderTypeForStorage(data.type) ?? "custom";
const provider = await createAiProvider({
...data,
apiKey: normalizedApiKey,
type: storageType,
baseUrl:
data.type === "zhipu"
@ -204,9 +231,23 @@ export async function PUT(request: Request) {
const body = await request.json();
const data = updateProviderSchema.parse(body);
const existingProvider = await getAiProviderById({ id: data.id });
if (!existingProvider) {
return new ChatSDKError("bad_request:api", "Provider not found").toResponse();
}
const effectiveType = data.type || (existingProvider.type as ProviderType);
const normalizedApiKey =
effectiveType === "runway" && data.apiKey !== undefined
? normalizeRunwayApiKey(data.apiKey)
: data.apiKey;
if (effectiveType === "runway" && normalizedApiKey !== undefined) {
validateRunwayApiKey(normalizedApiKey);
}
const provider = await updateAiProvider({
...data,
apiKey: normalizedApiKey,
type: normalizeProviderTypeForStorage(data.type),
updatedBy: session.user.id,
});