diff --git a/README.md b/README.md index 0e2e8d9f..b7c13a48 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ Default URLs: Claude Code
- Claude Code + Claude-Code OpenClaw
@@ -193,7 +193,36 @@ Default URLs: ## 🌐 Supported Providers -### 🆓 Free Providers (Unlimited) +### 🔐 OAuth Providers + +
+ + + + + + + + +
+ Claude Code
+ Claude-Code +
+ Antigravity
+ Antigravity +
+ Codex
+ Codex +
+ GitHub
+ GitHub +
+ Cursor
+ Cursor +
+
+ +### 🆓 Free Providers
@@ -222,35 +251,6 @@ Default URLs:
-### 🔐 OAuth Providers - -
- - - - - - - - -
- Claude Code
- Claude Code -
- Antigravity
- Antigravity -
- Codex
- Codex -
- GitHub
- GitHub -
- Cursor
- Cursor -
-
- ### 🔑 API Key Providers (40+)
diff --git a/package.json b/package.json index d2547dc2..26b6e015 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.3.32", + "version": "0.3.33", "description": "9Router web dashboard", "private": true, "scripts": { diff --git a/public/providers/chutes.png b/public/providers/chutes.png index 07e4d1db..f39a6c21 100644 Binary files a/public/providers/chutes.png and b/public/providers/chutes.png differ diff --git a/public/providers/kilocode.png b/public/providers/kilocode.png index 147dac0c..b1272cfe 100644 Binary files a/public/providers/kilocode.png and b/public/providers/kilocode.png differ diff --git a/public/providers/siliconflow.png b/public/providers/siliconflow.png index a7358145..d599df2e 100644 Binary files a/public/providers/siliconflow.png and b/public/providers/siliconflow.png differ diff --git a/src/app/api/provider-nodes/validate/route.js b/src/app/api/provider-nodes/validate/route.js index 6d70a373..f85d8964 100644 --- a/src/app/api/provider-nodes/validate/route.js +++ b/src/app/api/provider-nodes/validate/route.js @@ -1,5 +1,37 @@ import { NextResponse } from "next/server"; +// Fetch with timeout wrapper +const fetchWithTimeout = (url, options, timeout = 10000) => { + return Promise.race([ + fetch(url, options), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Request timeout")), timeout) + ) + ]); +}; + +// Validate URL format +const isValidUrl = (url) => { + try { + new URL(url); + return true; + } catch { + return false; + } +}; + +// Parse error details for user-friendly messages +const getErrorMessage = (error) => { + if (error.cause?.code === "ECONNREFUSED") return "Connection refused - provider node offline or unreachable"; + if (error.cause?.code === "ENOTFOUND") return "DNS lookup failed - invalid domain or network issue"; + if (error.cause?.code === "ETIMEDOUT") return "Connection timeout - provider node too slow"; + if (error.message.includes("timeout")) return "Request timeout (>10s) - provider node not responding"; + if (error.cause?.code === "CERT_HAS_EXPIRED") return "SSL certificate expired"; + if (error.cause?.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") return "SSL certificate verification failed"; + if (error.cause?.code) return `Network error: ${error.cause.code}`; + return "Network connection failed - check URL and network connectivity"; +}; + // POST /api/provider-nodes/validate - Validate API key against base URL export async function POST(request) { try { @@ -10,6 +42,11 @@ export async function POST(request) { return NextResponse.json({ error: "Base URL and API key required" }, { status: 400 }); } + // Validate URL format + if (!isValidUrl(baseUrl)) { + return NextResponse.json({ error: "Invalid URL format" }, { status: 400 }); + } + // Anthropic Compatible Validation if (type === "anthropic-compatible") { // Robustly construct URL: remove trailing slash, and remove trailing /messages if user added it @@ -21,7 +58,7 @@ export async function POST(request) { // Use /models endpoint for validation as many compatible providers support it (like OpenAI) const modelsUrl = `${normalizedBase}/models`; - const res = await fetch(modelsUrl, { + const res = await fetchWithTimeout(modelsUrl, { method: "GET", headers: { "x-api-key": apiKey, @@ -30,18 +67,27 @@ export async function POST(request) { } }); - return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key" }); + return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key or unauthorized" }); } // OpenAI Compatible Validation (Default) const modelsUrl = `${baseUrl.replace(/\/$/, "")}/models`; - const res = await fetch(modelsUrl, { + const res = await fetchWithTimeout(modelsUrl, { headers: { "Authorization": `Bearer ${apiKey}` }, }); - return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key" }); + return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key or unauthorized" }); } catch (error) { - console.log("Error validating provider node:", error); - return NextResponse.json({ error: "Validation failed" }, { status: 500 }); + const errorMessage = getErrorMessage(error); + console.error("Error validating provider node:", { + message: error.message, + cause: error.cause, + code: error.cause?.code, + userMessage: errorMessage + }); + return NextResponse.json({ + valid: false, + error: errorMessage + }, { status: 500 }); } } diff --git a/src/mitm/manager.js b/src/mitm/manager.js index 168d51f6..5a68d5d5 100644 --- a/src/mitm/manager.js +++ b/src/mitm/manager.js @@ -374,7 +374,7 @@ async function startServer(apiKey, sudoPassword) { ].join("\r\n"); const tmpVbs = path.join(os.tmpdir(), `mitm_uac_${Date.now()}.vbs`); fs.writeFileSync(tmpVbs, vbs, "utf8"); - spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: false, detached: true }).unref(); + spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: true, detached: true }).unref(); await new Promise((resolve, reject) => { const deadline = Date.now() + 90000; @@ -511,7 +511,7 @@ async function stopServer(sudoPassword) { ].join("\r\n"); const tmpVbs = path.join(os.tmpdir(), "mitm_stop_uac.vbs"); fs.writeFileSync(tmpVbs, vbs, "utf8"); - spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: false, detached: true }).unref(); + spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: true, detached: true }).unref(); await new Promise((resolve) => { const deadline = Date.now() + 30000;