feat: validate runway api key format
This commit is contained in:
parent
1eb23a015f
commit
404e50a4d6
@ -6877,7 +6877,13 @@ export function AdminSettingsClient() {
|
|||||||
value={providerForm.apiKey}
|
value={providerForm.apiKey}
|
||||||
onChange={(e) => setProviderForm({ ...providerForm, apiKey: e.target.value })}
|
onChange={(e) => setProviderForm({ ...providerForm, apiKey: e.target.value })}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder={editingProvider ? "留空则不修改" : "sk-..."}
|
placeholder={
|
||||||
|
editingProvider
|
||||||
|
? "留空则不修改"
|
||||||
|
: providerForm.type === "runway"
|
||||||
|
? "key_ 后跟 128 位十六进制字符"
|
||||||
|
: "sk-..."
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button" variant="ghost" size="sm"
|
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" />}
|
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{providerForm.type === "runway" && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Runway Key 必须以 `key_` 开头,后面是 128 位十六进制字符。不要粘贴带“Bearer ”、“API Key:”或中文标点的内容。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@ -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 MIDJOURNEY_BASE_URL = "https://your-midjourney-api.example.com";
|
||||||
const TRIPO_3D_BASE_URL = "https://api.tripo3d.ai/v2/openapi";
|
const TRIPO_3D_BASE_URL = "https://api.tripo3d.ai/v2/openapi";
|
||||||
const RUNWAY_BASE_URL = "https://api.dev.runwayml.com";
|
const RUNWAY_BASE_URL = "https://api.dev.runwayml.com";
|
||||||
|
const RUNWAY_API_KEY_PATTERN = /^key_[0-9a-f]{128}$/;
|
||||||
|
|
||||||
function normalizeProviderTypeForStorage(
|
function normalizeProviderTypeForStorage(
|
||||||
type?: ProviderType
|
type?: ProviderType
|
||||||
@ -70,6 +71,26 @@ function normalizeProviderTypeForStorage(
|
|||||||
return type;
|
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) {
|
function isZhipuBaseUrl(baseUrl: string) {
|
||||||
return baseUrl.includes("open.bigmodel.cn");
|
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)));
|
console.log("[Admin API] Create provider request body:", JSON.stringify(sanitizeProviderPayload(body)));
|
||||||
|
|
||||||
const data = createProviderSchema.parse(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...");
|
console.log("[Admin API] Validation passed, creating provider...");
|
||||||
const storageType = normalizeProviderTypeForStorage(data.type) ?? "custom";
|
const storageType = normalizeProviderTypeForStorage(data.type) ?? "custom";
|
||||||
|
|
||||||
const provider = await createAiProvider({
|
const provider = await createAiProvider({
|
||||||
...data,
|
...data,
|
||||||
|
apiKey: normalizedApiKey,
|
||||||
type: storageType,
|
type: storageType,
|
||||||
baseUrl:
|
baseUrl:
|
||||||
data.type === "zhipu"
|
data.type === "zhipu"
|
||||||
@ -204,9 +231,23 @@ export async function PUT(request: Request) {
|
|||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const data = updateProviderSchema.parse(body);
|
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({
|
const provider = await updateAiProvider({
|
||||||
...data,
|
...data,
|
||||||
|
apiKey: normalizedApiKey,
|
||||||
type: normalizeProviderTypeForStorage(data.type),
|
type: normalizeProviderTypeForStorage(data.type),
|
||||||
updatedBy: session.user.id,
|
updatedBy: session.user.id,
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user