175 lines
5.1 KiB
JavaScript
175 lines
5.1 KiB
JavaScript
|
|
import { NextResponse } from "next/server";
|
||
|
|
import { createProxyPool } from "@/models";
|
||
|
|
|
||
|
|
const DENO_V2_API = "https://api.deno.com/v2";
|
||
|
|
|
||
|
|
const DENO_RELAY_CODE = `Deno.serve(async (request) => {
|
||
|
|
const target = request.headers.get("x-relay-target");
|
||
|
|
const relayPath = request.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 newHeaders = new Headers(request.headers);
|
||
|
|
newHeaders.delete("x-relay-target");
|
||
|
|
newHeaders.delete("x-relay-path");
|
||
|
|
newHeaders.delete("host");
|
||
|
|
|
||
|
|
const init = {
|
||
|
|
method: request.method,
|
||
|
|
headers: newHeaders,
|
||
|
|
};
|
||
|
|
|
||
|
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
||
|
|
init.body = request.body;
|
||
|
|
init.duplex = "half";
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch(targetUrl, init);
|
||
|
|
return new Response(response.body, {
|
||
|
|
status: response.status,
|
||
|
|
headers: response.headers,
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
return new Response(JSON.stringify({ error: error.message }), {
|
||
|
|
status: 502,
|
||
|
|
headers: { "content-type": "application/json" },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});`;
|
||
|
|
|
||
|
|
export async function POST(request) {
|
||
|
|
try {
|
||
|
|
const body = await request.json();
|
||
|
|
const denoToken = body.denoToken?.trim();
|
||
|
|
const orgDomain = body.orgDomain?.trim();
|
||
|
|
const projectName = body.projectName?.trim() || `relay-${Date.now().toString(36)}`;
|
||
|
|
|
||
|
|
if (!orgDomain) {
|
||
|
|
return NextResponse.json({ error: "Organization domain is required" }, { status: 400 });
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!denoToken) {
|
||
|
|
return NextResponse.json({ error: "Deno Deploy API token is required" }, { status: 400 });
|
||
|
|
}
|
||
|
|
|
||
|
|
const headers = {
|
||
|
|
Authorization: `Bearer ${denoToken}`,
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
};
|
||
|
|
|
||
|
|
const createAppRes = await fetch(`${DENO_V2_API}/apps`, {
|
||
|
|
method: "POST",
|
||
|
|
headers,
|
||
|
|
body: JSON.stringify({
|
||
|
|
slug: projectName,
|
||
|
|
labels: { "custom.kind": "9router-relay" },
|
||
|
|
config: {
|
||
|
|
install: "deno install",
|
||
|
|
runtime: {
|
||
|
|
type: "dynamic",
|
||
|
|
entrypoint: "main.ts",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!createAppRes.ok) {
|
||
|
|
const text = await createAppRes.text().catch(() => "");
|
||
|
|
if (createAppRes.status === 409) {
|
||
|
|
return NextResponse.json(
|
||
|
|
{ error: `App "${projectName}" already exists. Choose a different name.` },
|
||
|
|
{ status: 409 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return NextResponse.json(
|
||
|
|
{ error: `Failed to create app (${createAppRes.status}): ${text}` },
|
||
|
|
{ status: createAppRes.status }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const app = await createAppRes.json();
|
||
|
|
|
||
|
|
const deployRes = await fetch(`${DENO_V2_API}/apps/${app.id}/deploy`, {
|
||
|
|
method: "POST",
|
||
|
|
headers,
|
||
|
|
body: JSON.stringify({
|
||
|
|
assets: {
|
||
|
|
"main.ts": {
|
||
|
|
kind: "file",
|
||
|
|
content: DENO_RELAY_CODE,
|
||
|
|
encoding: "utf-8",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!deployRes.ok) {
|
||
|
|
const text = await deployRes.text().catch(() => "");
|
||
|
|
console.error("Deno Deploy error:", deployRes.status, text);
|
||
|
|
await fetch(`${DENO_V2_API}/apps/${app.id}`, {
|
||
|
|
method: "DELETE",
|
||
|
|
headers: { Authorization: `Bearer ${denoToken}` },
|
||
|
|
}).catch(() => {});
|
||
|
|
return NextResponse.json(
|
||
|
|
{ error: `Deploy failed (${deployRes.status}): ${text}` },
|
||
|
|
{ status: deployRes.status }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const revision = await deployRes.json();
|
||
|
|
const revisionId = revision.id;
|
||
|
|
|
||
|
|
let status = revision.status;
|
||
|
|
let attempts = 0;
|
||
|
|
const maxAttempts = 30; // 30 * 2s = 60s max
|
||
|
|
while (status === "queued" || status === "building") {
|
||
|
|
if (attempts >= maxAttempts) {
|
||
|
|
throw new Error("Deploy timed out after 60 seconds");
|
||
|
|
}
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||
|
|
const statusRes = await fetch(`${DENO_V2_API}/revisions/${revisionId}`, {
|
||
|
|
headers: { Authorization: `Bearer ${denoToken}` },
|
||
|
|
});
|
||
|
|
if (!statusRes.ok) break;
|
||
|
|
const statusData = await statusRes.json();
|
||
|
|
status = statusData.status;
|
||
|
|
attempts++;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (status !== "succeeded") {
|
||
|
|
await fetch(`${DENO_V2_API}/apps/${app.id}`, {
|
||
|
|
method: "DELETE",
|
||
|
|
headers: { Authorization: `Bearer ${denoToken}` },
|
||
|
|
}).catch(() => {});
|
||
|
|
return NextResponse.json(
|
||
|
|
{ error: `Deploy failed with status: ${status}` },
|
||
|
|
{ status: 500 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const orgSlug = orgDomain.split(".")[0];
|
||
|
|
const deployUrl = `https://${projectName}.${orgSlug}.deno.net`;
|
||
|
|
console.log("Deno deployUrl:", deployUrl);
|
||
|
|
|
||
|
|
const proxyPool = await createProxyPool({
|
||
|
|
name: projectName,
|
||
|
|
proxyUrl: deployUrl,
|
||
|
|
type: "deno",
|
||
|
|
noProxy: "",
|
||
|
|
isActive: true,
|
||
|
|
strictProxy: false,
|
||
|
|
});
|
||
|
|
|
||
|
|
return NextResponse.json({ proxyPool, deployUrl }, { status: 201 });
|
||
|
|
} catch (error) {
|
||
|
|
console.log("Error deploying Deno Deploy relay:", error);
|
||
|
|
return NextResponse.json({ error: error.message || "Deploy failed" }, { status: 500 });
|
||
|
|
}
|
||
|
|
}
|