2api
feat: configurable stream stall timeout + per-provider UI
88c4c60
Raw
History Blame Contribute Delete
4.4 kB
import { NextResponse } from "next/server";
import { createProxyPool } from "@/models";
const VERCEL_API = "https://api.vercel.com";
// Relay function source code deployed to Vercel
// Forwards requests to target URL specified in x-relay-target header
const RELAY_FUNCTION_CODE = `
export const config = { runtime: "edge" };
export default async function handler(req) {
const target = req.headers.get("x-relay-target");
const relayPath = req.headers.get("x-relay-path") || "/";
if (!target) {
return new Response(JSON.stringify({ error: "Missing x-relay-target header" }), {
status: 400,
headers: { "content-type": "application/json" },
});
}
const targetUrl = target.replace(/\\/$/, "") + relayPath;
const headers = new Headers(req.headers);
headers.delete("x-relay-target");
headers.delete("x-relay-path");
headers.delete("host");
const response = await fetch(targetUrl, {
method: req.method,
headers,
body: req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined,
duplex: "half",
});
return new Response(response.body, {
status: response.status,
headers: response.headers,
});
}
`;
async function pollDeployment(deploymentId, token, maxMs = 120000) {
const start = Date.now();
while (Date.now() - start < maxMs) {
const res = await fetch(`${VERCEL_API}/v13/deployments/${deploymentId}`, {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();
if (data.readyState === "READY") return data;
if (data.readyState === "ERROR" || data.readyState === "CANCELED") {
throw new Error(`Deployment failed: ${data.readyState}`);
}
await new Promise((r) => setTimeout(r, 3000));
}
throw new Error("Deployment timed out");
}
// POST /api/proxy-pools/vercel-deploy
export async function POST(request) {
try {
const body = await request.json();
const vercelToken = body.vercelToken;
const projectName = body.projectName?.trim() || `relay-${Date.now().toString(36)}`;
if (!vercelToken) {
return NextResponse.json({ error: "Vercel API token is required" }, { status: 400 });
}
// Deploy relay function to Vercel
const deployRes = await fetch(`${VERCEL_API}/v13/deployments`, {
method: "POST",
headers: {
Authorization: `Bearer ${vercelToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: projectName,
files: [
{
file: "api/relay.js",
data: RELAY_FUNCTION_CODE,
},
{
file: "package.json",
data: JSON.stringify({ name: projectName, version: "1.0.0" }),
},
{
file: "vercel.json",
data: JSON.stringify({
rewrites: [{ source: "/(.*)", destination: "/api/relay" }],
}),
},
],
projectSettings: {
framework: null,
},
target: "production",
}),
});
if (!deployRes.ok) {
const err = await deployRes.json().catch(() => ({}));
return NextResponse.json(
{ error: err.error?.message || "Failed to create Vercel deployment" },
{ status: deployRes.status }
);
}
const deployment = await deployRes.json();
const deploymentId = deployment.id || deployment.uid;
// Disable deployment protection (Vercel Authentication)
const projectId = deployment.projectId || projectName;
await fetch(`${VERCEL_API}/v9/projects/${projectId}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${vercelToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ ssoProtection: null }),
});
// Poll until deployment is ready
const ready = await pollDeployment(deploymentId, vercelToken);
const deployUrl = `https://${ready.url}`;
// Create proxy pool entry with type vercel
const proxyPool = await createProxyPool({
name: projectName,
proxyUrl: deployUrl,
type: "vercel",
noProxy: "",
isActive: true,
strictProxy: false,
});
return NextResponse.json({ proxyPool, deployUrl }, { status: 201 });
} catch (error) {
console.log("Error deploying Vercel relay:", error);
return NextResponse.json({ error: error.message || "Deploy failed" }, { status: 500 });
}
}