// Vercel REST API helpers — all calls require a bearer token import type { VercelDeployment, DeploymentEvent } from '../types' const BASE = 'https://api.vercel.com' /** Only allow genuine Vercel deploy hook URLs through triggerDeploy */ const VERCEL_HOOK_RE = /^https:\/\/api\.vercel\.com\/v1\/integrations\/deploy\// async function vercelFetch(path: string, token: string, init?: RequestInit): Promise { const res = await fetch(`${BASE}${path}`, { ...init, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...init?.headers, }, }) if (!res.ok) { const hint = res.status === 401 ? ' — token is invalid or expired. Reconnect your API token.' : res.status === 403 ? ' — token lacks the required permissions. Ensure it has Full Account scope.' : res.status === 404 ? ' — resource not found. Check the deploy hook URL and team ID.' : res.status === 429 ? ' — rate limit reached. Wait a moment and try again.' : res.status >= 500 ? ' — Vercel is experiencing issues. Try again shortly.' : '' throw new Error(`Vercel API ${res.status}${hint}`) } return res.json() as Promise } /** Fetch the last N deployments triggered by a specific deploy hook */ export async function listDeployments(opts: { projectId: string hookId: string token: string teamId?: string limit?: number }): Promise { const params = new URLSearchParams({ projectId: opts.projectId, 'meta-deployHookId': opts.hookId, limit: String(opts.limit ?? 10), }) if (opts.teamId) params.set('teamId', opts.teamId) const data = await vercelFetch<{ deployments: VercelDeployment[] }>( `/v6/deployments?${params}`, opts.token, ) return data.deployments ?? [] } /** Cancel an in-progress deployment */ export async function cancelDeployment(opts: { deploymentId: string token: string teamId?: string }): Promise { const params = opts.teamId ? `?teamId=${opts.teamId}` : '' await vercelFetch(`/v12/deployments/${opts.deploymentId}/cancel${params}`, opts.token, { method: 'PATCH', }) } /** * Trigger a deploy by POSTing to the hook URL. * Validates the URL is a genuine Vercel hook before calling to prevent * SSRF if a document is tampered with outside the Studio schema. */ export async function triggerDeploy(hookUrl: string): Promise { if (!VERCEL_HOOK_RE.test(hookUrl)) { throw new Error('Invalid deploy hook URL — must be a Vercel hook (api.vercel.com/v1/integrations/deploy/…)') } const res = await fetch(hookUrl, { method: 'POST' }) if (!res.ok) throw new Error(`Deploy hook returned ${res.status}`) } /** * Fetch build events for a deployment. * Returns up to 100 events in reverse chronological order, * filtered to lines with actual text content. */ export async function getDeploymentEvents(opts: { deploymentId: string token: string teamId?: string }): Promise { const params = new URLSearchParams({ limit: '100', direction: 'backward' }) if (opts.teamId) params.set('teamId', opts.teamId) // API returns either a plain array or a wrapped object depending on version const raw = await vercelFetch( `/v2/deployments/${opts.deploymentId}/events?${params}`, opts.token, ) const events: DeploymentEvent[] = Array.isArray(raw) ? raw : (raw.events ?? []) return events.filter(e => e.text?.trim()) }