Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
6.82 kB
import { randomBytes } from "crypto";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { cloudflareDeploySchema } from "@/shared/validation/freeProxySchemas";
import { createProxy } from "@/lib/localDb";
import { encrypt } from "@/lib/db/encryption";
import { buildCloudflareWorkerScript } from "@/lib/proxyRelay/cloudflareWorkerScript";
// Port of upstream decolua/9router PR #1360 — Cloudflare Workers proxy relay.
// Architecture mirrors src/app/api/settings/proxy/vercel-deploy/route.ts so the
// shared proxyFetch relay short-circuit, x-relay-auth scheme, and inline SSRF
// guard work unchanged. Only the deployment surface differs (Cloudflare Workers
// API instead of Vercel /v13/deployments).
const CLOUDFLARE_API_BASE = process.env.CLOUDFLARE_API_BASE || "https://api.cloudflare.com/client/v4";
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown = {};
try {
rawBody = await request.json();
} catch {
return createErrorResponse({
status: 400,
message: "Invalid JSON body",
type: "invalid_request",
});
}
const validation = validateBody(cloudflareDeploySchema, rawBody);
if (isValidationFailure(validation)) {
return createErrorResponse({
status: 400,
message: validation.error.message,
type: "invalid_request",
});
}
const { accountId, apiToken, projectName } = validation.data;
// Generate random auth secret for the relay — stored in proxy notes, never
// returned to client. Same scheme as the Vercel relay so the deployed worker
// is not an open SSRF proxy reachable from any third party with the workers.dev URL.
const relayAuth = randomBytes(24).toString("hex");
const workerScript = buildCloudflareWorkerScript(relayAuth);
try {
// 1. PUT the Worker script — Cloudflare requires multipart/form-data with
// main_module + a metadata blob describing the upload.
const workerScriptUrl = `${CLOUDFLARE_API_BASE}/accounts/${accountId}/workers/scripts/${projectName}`;
const formData = new FormData();
formData.append(
"index.js",
// Cloudflare's script-upload API only accepts application/javascript,
// text/javascript, or multipart/form-data for the script part and rejects
// "application/javascript+module" outright (#5128). ES-module semantics
// come from `main_module` in the metadata blob below, not this MIME type.
new Blob([workerScript], { type: "application/javascript" }),
"index.js"
);
formData.append(
"metadata",
new Blob(
[
JSON.stringify({
main_module: "index.js",
compatibility_date: "2026-03-20",
observability: { enabled: true },
}),
],
{ type: "application/json" }
),
"metadata.json"
);
const uploadRes = await fetch(workerScriptUrl, {
method: "PUT",
headers: { Authorization: `Bearer ${apiToken}` },
body: formData,
});
if (!uploadRes.ok) {
// Surface only the canonical Cloudflare error message; never forward raw
// response text (may carry internal IDs / token hints).
let upstreamMessage = "Cloudflare API rejected the Worker upload";
try {
const parsed = (await uploadRes.json().catch(() => null)) as {
errors?: Array<{ message?: string }>;
} | null;
const candidate = parsed?.errors?.[0]?.message;
if (typeof candidate === "string" && candidate.trim()) {
upstreamMessage = candidate.trim().slice(0, 200);
}
} catch {
/* fall through to generic message */
}
return createErrorResponse({
status: uploadRes.status,
message: `Cloudflare Worker upload failed: ${upstreamMessage}`,
type: "upstream_error",
});
}
// 2. Enable the workers.dev subdomain for this script so it is reachable.
// A failure here is non-fatal (some accounts already enable subdomains
// by default); the next call surfaces the correct error if anything is
// actually missing.
await fetch(`${workerScriptUrl}/subdomain`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ enabled: true }),
}).catch(() => {});
// 3. Look up the account's workers.dev subdomain to build the final URL.
const subdomainRes = await fetch(
`${CLOUDFLARE_API_BASE}/accounts/${accountId}/workers/subdomain`,
{
method: "GET",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
}
);
let deployUrl = "";
if (subdomainRes.ok) {
const subdomainData = (await subdomainRes.json().catch(() => null)) as {
result?: { subdomain?: string };
} | null;
const sub = subdomainData?.result?.subdomain;
if (typeof sub === "string" && sub) {
deployUrl = `https://${projectName}.${sub}.workers.dev`;
}
}
if (!deployUrl) {
return createErrorResponse({
status: 400,
message:
"Worker deployed but failed to retrieve workers.dev subdomain. Set up a workers.dev subdomain in the Cloudflare dashboard first.",
type: "upstream_error",
});
}
// Store as proxy pool entry — apiToken is NOT stored. relayAuth is
// encrypted at rest when STORAGE_ENCRYPTION_KEY is configured (encrypt() is
// a no-op in passthrough mode); the redactor strips both shapes from API responses.
const encryptedRelayAuth = encrypt(relayAuth);
const notesPayload =
encryptedRelayAuth && encryptedRelayAuth !== relayAuth
? { relayAuthEnc: encryptedRelayAuth }
: { relayAuth };
// deployUrl is "https://<name>.<sub>.workers.dev" — strip the protocol so
// the `host` column matches the Vercel-relay shape (proxyFetch prepends
// "https://" when routing).
const hostOnly = deployUrl.replace(/^https?:\/\//, "");
const poolProxy = await createProxy({
name: `Cloudflare Relay (${projectName})`,
type: "cloudflare",
host: hostOnly,
port: 443,
notes: JSON.stringify(notesPayload),
source: "cloudflare-relay",
});
return Response.json({
success: true,
relayUrl: deployUrl,
poolProxyId: poolProxy?.id,
});
} catch (error) {
return createErrorResponseFromUnknown(error, "Cloudflare deploy failed");
}
}