From 22e17f08fc7d4dbd02dcc2145a94258bbe14f128 Mon Sep 17 00:00:00 2001 From: dynamo-pentester Date: Sat, 6 Jun 2026 01:02:23 +0530 Subject: [PATCH 1/2] fix(deploy): enhance API token handling and improve user feedback --- app/api/deploy/vercel/route.ts | 65 +++++--- .../playground/components/deploy-dialog.tsx | 143 ++++++++++-------- 2 files changed, 120 insertions(+), 88 deletions(-) diff --git a/app/api/deploy/vercel/route.ts b/app/api/deploy/vercel/route.ts index e4a1897b..e734b84e 100644 --- a/app/api/deploy/vercel/route.ts +++ b/app/api/deploy/vercel/route.ts @@ -1,40 +1,65 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/auth"; import { VERCEL_API } from "@/lib/constants/config"; +import { rateLimit } from "@/lib/api-utils"; -export async function POST(req: Request) { +export async function POST(req: NextRequest) { try { const session = await auth(); - if (!session?.user) { + if (!session?.user?.id) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } + // Mirror the same rate limit as the Netlify deploy route (5 deploys / minute) + const { allowed, remaining } = await rateLimit( + `deploy-vercel:${session.user.id}`, + 5, + 60_000 + ); + + if (!allowed) { + return NextResponse.json( + { error: "Rate limit exceeded. Please wait before deploying again." }, + { + status: 429, + headers: { + "Retry-After": "60", + "X-RateLimit-Limit": "5", + "X-RateLimit-Remaining": String(remaining), + }, + } + ); + } + const { files, name, userApiKey } = await req.json(); if (!files || !Array.isArray(files)) { return NextResponse.json({ error: "No files provided" }, { status: 400 }); } - // Try user key first, fallback to Editron Master Key - const token = userApiKey || process.env.VERCEL_MASTER_TOKEN; - + // Require the caller to supply their own token. + // VERCEL_MASTER_TOKEN is intentionally NOT used as a fallback here — + // doing so would let any authenticated user deploy on the server's + // Vercel account without explicit per-deployment consent (issue #449). + const token = (userApiKey as string | undefined)?.trim(); if (!token) { return NextResponse.json( - { error: "No Vercel API token provided and no master token available." }, + { + error: + "A Vercel API key is required. Please provide your own token in the deploy dialog.", + }, { status: 400 } ); } - // Vercel API requires an array of standard file objects: - // [{ file: "index.html", data: "..." }] - - // Convert our internal `TemplateData` format to flat Vercel format - const flatFiles = files.map(f => ({ + const flatFiles = files.map((f: { path: string; content: string }) => ({ file: f.path, - data: f.content + data: f.content, })); - const projectName = name ? name.toLowerCase().replace(/[^a-z0-9-]/g, '-') : "editron-deploy"; + const projectName = name + ? (name as string).toLowerCase().replace(/[^a-z0-9-]/g, "-") + : "editron-deploy"; const response = await fetch(VERCEL_API.DEPLOYMENTS, { method: "POST", @@ -45,7 +70,7 @@ export async function POST(req: Request) { body: JSON.stringify({ name: projectName, files: flatFiles, - target: "production" + target: "production", }), }); @@ -61,14 +86,10 @@ export async function POST(req: Request) { return NextResponse.json({ url: data.url, deploymentId: data.id, - readyState: data.readyState + readyState: data.readyState, }); - } catch (error) { console.error("Vercel deployment error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } -} +} \ No newline at end of file diff --git a/modules/playground/components/deploy-dialog.tsx b/modules/playground/components/deploy-dialog.tsx index a0d314a8..1109a3a5 100644 --- a/modules/playground/components/deploy-dialog.tsx +++ b/modules/playground/components/deploy-dialog.tsx @@ -25,11 +25,12 @@ interface DeployDialogProps { export function DeployDialog({ open, onOpenChange, templateData, projectName }: DeployDialogProps) { const [provider, setProvider] = useState<"vercel" | "netlify" | "cloudflare">("vercel"); - const [useMasterKey, setUseMasterKey] = useState(true); const [userKey, setUserKey] = useState(""); const [isDeploying, setIsDeploying] = useState(false); const [deployedUrl, setDeployedUrl] = useState(""); + const providerLabel = provider === "vercel" ? "Vercel" : provider === "netlify" ? "Netlify" : "Cloudflare"; + const flattenFileTree = (data: TemplateFolder, parentPath = ""): { path: string; content: string }[] => { let files: { path: string; content: string }[] = []; if (!data) return files; @@ -42,7 +43,7 @@ export function DeployDialog({ open, onOpenChange, templateData, projectName }: const extension = item.fileExtension ? `.${item.fileExtension}` : ""; files.push({ path: `${parentPath}${item.filename}${extension}`, - content: item.content + content: item.content, }); } } @@ -50,8 +51,8 @@ export function DeployDialog({ open, onOpenChange, templateData, projectName }: }; const handleDeploy = async () => { - if (!useMasterKey && !userKey) { - toast.error(`Please enter your ${provider === "vercel" ? "Vercel" : provider === "netlify" ? "Netlify" : "Cloudflare"} API key`); + if (!userKey.trim()) { + toast.error(`Please enter your ${providerLabel} API key.`); return; } @@ -65,9 +66,9 @@ export function DeployDialog({ open, onOpenChange, templateData, projectName }: try { const flatFiles = flattenFileTree(templateData); - - // We don't deploy node_modules or big cache folders - const filteredFiles = flatFiles.filter(f => !f.path.startsWith("node_modules/") && !f.path.startsWith(".next/")); + const filteredFiles = flatFiles.filter( + (f) => !f.path.startsWith("node_modules/") && !f.path.startsWith(".next/") + ); const res = await fetch(`/api/deploy/${provider}`, { method: "POST", @@ -75,8 +76,8 @@ export function DeployDialog({ open, onOpenChange, templateData, projectName }: body: JSON.stringify({ files: filteredFiles, name: projectName, - userApiKey: useMasterKey ? undefined : userKey - }) + userApiKey: userKey.trim(), + }), }); const data = await res.json(); @@ -87,10 +88,13 @@ export function DeployDialog({ open, onOpenChange, templateData, projectName }: setDeployedUrl(data.url.startsWith("http") ? data.url : `https://${data.url}`); toast.success("Successfully deployed!"); - } catch (error: unknown) { console.error(error); - toast.error(error instanceof Error ? error.message : "Failed to deploy. Check your API key or try again."); + toast.error( + error instanceof Error + ? error.message + : "Failed to deploy. Check your API key or try again." + ); } finally { setIsDeploying(false); } @@ -115,16 +119,33 @@ export function DeployDialog({ open, onOpenChange, templateData, projectName }:

Deployment Successful!

- + {deployedUrl} - ) : (
- { if (v === "vercel" || v === "netlify" || v === "cloudflare") { setProvider(v); } }}> + { + if (v === "vercel" || v === "netlify" || v === "cloudflare") { + setProvider(v); + setUserKey(""); + } + }} + > Vercel Netlify @@ -132,64 +153,54 @@ export function DeployDialog({ open, onOpenChange, templateData, projectName }: -
-
- -
-
-
setUseMasterKey(true)} - > -
- {useMasterKey &&
} -
-
-

Use Editron Deployment

-

Deploy immediately using our master key. Ideal for quick demos.

-
-
- -
setUseMasterKey(false)} - > -
-
- {!useMasterKey &&
} -
-
-

Bring Your Own Key

-

Deploy to your personal {provider === 'vercel' ? 'Vercel' : provider === 'netlify' ? 'Netlify' : 'Cloudflare'} account.

-
-
- - {!useMasterKey && ( -
- setUserKey(e.target.value)} - type="password" - autoFocus - /> -

- Tokens are only used for this deployment and never saved. -

-
- )} -
-
+
+ + setUserKey(e.target.value)} + type="password" + /> +

+ Tokens are used only for this deployment and are never stored.{" "} + {provider === "vercel" && ( + + Get a Vercel token + + )} + {provider === "netlify" && ( + + Get a Netlify token + + )} +

- @@ -203,4 +214,4 @@ export function DeployDialog({ open, onOpenChange, templateData, projectName }: ); -} +} \ No newline at end of file From 99ed9631f03cdf90af0db13f70b4964d0c6b4ea6 Mon Sep 17 00:00:00 2001 From: dynamo-pentester Date: Sat, 6 Jun 2026 01:15:59 +0530 Subject: [PATCH 2/2] fix(deploy): validate Vercel API key and handle missing deployment URL Signed-off-by: dynamo-pentester --- app/api/deploy/vercel/route.ts | 12 +++++++++++- modules/playground/components/deploy-dialog.tsx | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/api/deploy/vercel/route.ts b/app/api/deploy/vercel/route.ts index e734b84e..0d81c214 100644 --- a/app/api/deploy/vercel/route.ts +++ b/app/api/deploy/vercel/route.ts @@ -41,7 +41,17 @@ export async function POST(req: NextRequest) { // VERCEL_MASTER_TOKEN is intentionally NOT used as a fallback here — // doing so would let any authenticated user deploy on the server's // Vercel account without explicit per-deployment consent (issue #449). - const token = (userApiKey as string | undefined)?.trim(); + if (typeof userApiKey !== "string") { + return NextResponse.json( + { + error: + "A Vercel API key is required. Please provide your own token in the deploy dialog.", + }, + { status: 400 } + ); + } + + const token = userApiKey.trim(); if (!token) { return NextResponse.json( { diff --git a/modules/playground/components/deploy-dialog.tsx b/modules/playground/components/deploy-dialog.tsx index 1109a3a5..708573c5 100644 --- a/modules/playground/components/deploy-dialog.tsx +++ b/modules/playground/components/deploy-dialog.tsx @@ -86,6 +86,10 @@ export function DeployDialog({ open, onOpenChange, templateData, projectName }: throw new Error(data.error || "Deployment failed"); } + if (typeof data.url !== "string" || !data.url.trim()) { + throw new Error("Deployment URL missing from response"); + } + setDeployedUrl(data.url.startsWith("http") ? data.url : `https://${data.url}`); toast.success("Successfully deployed!"); } catch (error: unknown) {