diff --git a/src/app/(dashboard)/dashboard/proxy-pools/page.js b/src/app/(dashboard)/dashboard/proxy-pools/page.js index cae61044..6c6fc4b1 100644 --- a/src/app/(dashboard)/dashboard/proxy-pools/page.js +++ b/src/app/(dashboard)/dashboard/proxy-pools/page.js @@ -33,10 +33,12 @@ export default function ProxyPoolsPage() { const [showFormModal, setShowFormModal] = useState(false); const [showBatchImportModal, setShowBatchImportModal] = useState(false); const [showVercelModal, setShowVercelModal] = useState(false); + const [showCloudflareModal, setShowCloudflareModal] = useState(false); const [editingProxyPool, setEditingProxyPool] = useState(null); const [formData, setFormData] = useState(normalizeFormData()); const [batchImportText, setBatchImportText] = useState(""); const [vercelForm, setVercelForm] = useState({ vercelToken: "", projectName: "vercel-relay" }); + const [cloudflareForm, setCloudflareForm] = useState({ accountId: "", apiToken: "", projectName: "cloudflare-relay" }); const [saving, setSaving] = useState(false); const [importing, setImporting] = useState(false); const [deploying, setDeploying] = useState(false); @@ -334,6 +336,16 @@ export default function ProxyPoolsPage() { setShowVercelModal(false); }; + const openCloudflareModal = () => { + setCloudflareForm({ accountId: "", apiToken: "", projectName: "cloudflare-relay" }); + setShowCloudflareModal(true); + }; + + const closeCloudflareModal = () => { + if (deploying) return; + setShowCloudflareModal(false); + }; + const handleVercelDeploy = async () => { if (!vercelForm.vercelToken.trim()) return; setDeploying(true); @@ -359,6 +371,31 @@ export default function ProxyPoolsPage() { } }; + const handleCloudflareDeploy = async () => { + if (!cloudflareForm.accountId.trim() || !cloudflareForm.apiToken.trim()) return; + setDeploying(true); + try { + const res = await fetch("/api/proxy-pools/cloudflare-deploy", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(cloudflareForm), + }); + const data = await res.json(); + if (res.ok) { + await fetchProxyPools(); + closeCloudflareModal(); + notify.success(`Deployed: ${data.deployUrl}`); + } else { + notify.error(data.error || "Deploy failed"); + } + } catch (error) { + console.log("Error deploying Cloudflare relay:", error); + notify.error("Deploy failed"); + } finally { + setDeploying(false); + } + }; + const parseProxyLine = (line) => { const trimmed = line.trim(); if (!trimmed) return null; @@ -495,6 +532,9 @@ export default function ProxyPoolsPage() {
+ @@ -588,6 +628,9 @@ export default function ProxyPoolsPage() { {pool.type === "vercel" && ( vercel relay )} + {pool.type === "cloudflare" && ( + cloudflare relay + )} {pool.boundConnectionCount || 0} bound @@ -722,6 +765,70 @@ export default function ProxyPoolsPage() {
+ +
+
+

What is Cloudflare Relay?

+

+ Deploys a Cloudflare Worker as a proxy relay. All AI provider requests will be forwarded through Cloudflare's global edge network. +

+
    +
  • High performance global routing and IP masking via Cloudflare Workers
  • +
  • Free tier: 100,000 requests per day
  • +
  • Requires Cloudflare Account ID and a Workers API Token (Edit Workers permission)
  • +
+
+

How to generate your API Token:

+
    +
  1. Go to My ProfileAPI TokensCreate Token
  2. +
  3. Scroll down to Custom Token and click Get started
  4. +
  5. Under Permissions: Account | Workers Scripts | Edit
  6. +
  7. Under Account Resources: Include | Account | Your Account Name
  8. +
  9. Click Continue to summaryCreate Token
  10. +
+
+
+ setCloudflareForm((prev) => ({ ...prev, accountId: e.target.value }))} + placeholder="your-cloudflare-account-id" + hint={<>Found on the right side of the Cloudflare dashboard overview page.} + /> + setCloudflareForm((prev) => ({ ...prev, apiToken: e.target.value }))} + placeholder="your-cloudflare-api-token" + hint={<>Requires "Workers Scripts: Edit" permission. Get token →} + type="password" + /> + setCloudflareForm((prev) => ({ ...prev, projectName: e.target.value }))} + placeholder="my-relay" + hint="Unique name for your Cloudflare Worker. Leave empty for auto-generated name." + /> +
+ + +
+
+
+ ({})); + console.error("Cloudflare upload error:", err); + return NextResponse.json( + { error: err.errors?.[0]?.message || "Failed to upload Worker to Cloudflare" }, + { status: uploadRes.status } + ); + } + + // 2. Enable workers.dev subdomain for the script + const enableSubdomainRes = await fetch(`${workerScriptUrl}/subdomain`, { + method: "POST", + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ enabled: true }), + }); + + if (!enableSubdomainRes.ok) { + const err = await enableSubdomainRes.json().catch(() => ({})); + console.error("Cloudflare subdomain enable error:", err); + // We don't fail completely here, just continue + } + + // 3. Get the workers.dev subdomain for the account to construct the final URL + let deployUrl = ""; + const subdomainRes = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/subdomain`, { + method: "GET", + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json", + }, + }); + + if (subdomainRes.ok) { + const subdomainData = await subdomainRes.json(); + if (subdomainData.result && subdomainData.result.subdomain) { + deployUrl = `https://${projectName}.${subdomainData.result.subdomain}.workers.dev`; + } + } + + if (!deployUrl) { + return NextResponse.json( + { error: "Worker deployed but failed to retrieve workers.dev subdomain. Make sure you have setup a workers.dev subdomain in Cloudflare Dashboard." }, + { status: 400 } + ); + } + + // Create proxy pool entry with type cloudflare + const proxyPool = await createProxyPool({ + name: projectName, + proxyUrl: deployUrl, + type: "cloudflare", + noProxy: "", + isActive: true, + strictProxy: false, + }); + + return NextResponse.json({ proxyPool, deployUrl }, { status: 201 }); + } catch (error) { + console.log("Error deploying Cloudflare relay:", error); + return NextResponse.json({ error: error.message || "Deploy failed" }, { status: 500 }); + } +} diff --git a/src/app/api/proxy-pools/route.js b/src/app/api/proxy-pools/route.js index 998a0d50..ce119f26 100644 --- a/src/app/api/proxy-pools/route.js +++ b/src/app/api/proxy-pools/route.js @@ -7,7 +7,7 @@ function toBoolean(value) { return undefined; } -const VALID_PROXY_TYPES = ["http", "vercel"]; +const VALID_PROXY_TYPES = ["http", "vercel", "cloudflare"]; function normalizeProxyPoolInput(body = {}) { const name = typeof body?.name === "string" ? body.name.trim() : ""; diff --git a/src/lib/network/connectionProxy.js b/src/lib/network/connectionProxy.js index e7836dd8..ca590819 100644 --- a/src/lib/network/connectionProxy.js +++ b/src/lib/network/connectionProxy.js @@ -68,12 +68,12 @@ export async function resolveConnectionProxyConfig( if (isValidPool) { /** - * Vercel relay proxies use base URL rewriting + * Vercel/Cloudflare relay proxies use base URL rewriting * instead of HTTP_PROXY environment variables. */ - if (proxyPool.type === "vercel") { + if (proxyPool.type === "vercel" || proxyPool.type === "cloudflare") { return { - source: "vercel", + source: proxyPool.type, proxyPoolId, proxyPool, @@ -84,7 +84,7 @@ export async function resolveConnectionProxyConfig( strictProxy: proxyPool.strictProxy === true, - vercelRelayUrl: proxyUrl, + vercelRelayUrl: proxyUrl, // Still mapped to vercelRelayUrl in the unified payload since they use the exact same header spec }; }