// Publishes one HTML page to Cloudflare Pages (Direct Upload) with the saved Cloudflare token. const API = "https://api.cloudflare.com/client/v4"; type CfResponse = { success?: boolean; result?: T; errors?: { code?: number; message?: string }[] }; function toBase64(text: string) { const bytes = new TextEncoder().encode(text); let bin = ""; for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); return btoa(bin); } async function hashOf(b64: string, ext = "html") { const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${b64}${ext}`)); return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 32); } /** Accepts one HTML page or a map of files (path -> text, or a data URL for images/fonts). */ export async function deployToPages(projectName: string, site: string | Record) { const files = typeof site === "string" ? { "index.html": site } : site; const { contentType, parseDataUrl } = await import("./codehub"); const token = process.env["Cloudfare_Api_keys"]?.trim(); if (!token) return { error: "The Cloudflare key is missing from project secrets." } as const; const auth = { Authorization: `Bearer ${token}` }; const call = async (path: string, init: RequestInit = {}, headers: Record = auth) => { const res = await fetch(`${API}${path}`, { ...init, headers: { ...headers, ...(init.headers as Record | undefined) } }); const json = (await res.json().catch(() => ({}))) as CfResponse; return { ok: res.ok && json.success !== false, status: res.status, json }; }; const accounts = await call<{ id: string }[]>("/accounts"); if (accounts.status === 401 || accounts.status === 403) return { error: "Cloudflare rejected the saved key. Check that it has Pages edit access." } as const; const accountId = accounts.json.result?.[0]?.id; if (!accountId) return { error: "No Cloudflare account was found for the saved key." } as const; const base = `/accounts/${accountId}/pages/projects`; let project = await call<{ subdomain?: string }>(`${base}/${projectName}`); if (!project.ok) { project = await call<{ subdomain?: string }>(base, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: projectName, production_branch: "main" }), }); if (!project.ok) return { error: "Cloudflare could not create the site project." } as const; } const upload = await call<{ jwt?: string }>(`${base}/${projectName}/upload-token`); const jwt = upload.json.result?.jwt; if (!jwt) return { error: "Cloudflare did not allow the upload." } as const; const jwtHeaders = { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" }; const manifest: Record = {}; const payload = await Promise.all(Object.entries(files).map(async ([path, body]) => { const stored = parseDataUrl(body); const b64 = stored ? stored.base64 : toBase64(body); const key = await hashOf(b64, path.split(".").pop() ?? ""); manifest[`/${path}`] = key; return { key, value: b64, metadata: { contentType: stored?.mime ?? contentType(path) }, base64: true }; })); const sent = await call("/pages/assets/upload", { method: "POST", body: JSON.stringify(payload) }, jwtHeaders); if (!sent.ok) return { error: "Cloudflare could not store the files." } as const; await call("/pages/assets/upsert-hashes", { method: "POST", body: JSON.stringify({ hashes: payload.map((p) => p.key) }) }, jwtHeaders); const form = new FormData(); form.append("manifest", JSON.stringify(manifest)); const deployment = await call<{ url?: string }>(`${base}/${projectName}/deployments`, { method: "POST", body: form }); if (!deployment.ok) return { error: "Cloudflare could not publish the site." } as const; const subdomain = project.json.result?.subdomain ?? `${projectName}.pages.dev`; return { url: `https://${subdomain}`, deploymentUrl: deployment.json.result?.url ?? null } as const; } /** Attaches a custom domain to an existing Pages project and, when the domain's zone is in the same account, adds the DNS record. */ export async function connectPagesDomain(projectName: string, domain: string) { const token = process.env["Cloudfare_Api_keys"]?.trim(); if (!token) return { error: "The Cloudflare key is missing from project secrets." } as const; const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }; const call = async (path: string, init: RequestInit = {}) => { const res = await fetch(`${API}${path}`, { ...init, headers }); const json = (await res.json().catch(() => ({}))) as CfResponse; return { ok: res.ok && json.success !== false, status: res.status, json, message: json.errors?.[0]?.message }; }; const accounts = await call<{ id: string }[]>("/accounts"); const accountId = accounts.json.result?.[0]?.id; if (!accountId) return { error: "Cloudflare rejected the saved key or no account was found." } as const; const base = `/accounts/${accountId}/pages/projects/${projectName}`; const project = await call<{ subdomain?: string }>(base); if (!project.ok) return { error: "Publish this project first, then connect your domain." } as const; const target = project.json.result?.subdomain ?? `${projectName}.pages.dev`; const added = await call<{ status?: string }>(`${base}/domains`, { method: "POST", body: JSON.stringify({ name: domain }) }); const already = !added.ok && /already/i.test(added.message ?? ""); if (!added.ok && !already) { if (added.status === 403) return { error: "Your Cloudflare key needs 'Cloudflare Pages: Edit' permission to add domains." } as const; return { error: `Cloudflare could not add the domain: ${added.message ?? "unknown error"}.` } as const; } // Auto-DNS: only when the domain's zone lives in this Cloudflare account and the key can edit DNS. let dns: "added" | "exists" | "manual" = "manual"; const parts = domain.split("."); for (let i = 0; i < parts.length - 1 && dns === "manual"; i++) { const zoneName = parts.slice(i).join("."); const zones = await call<{ id: string }[]>(`/zones?name=${encodeURIComponent(zoneName)}`); const zoneId = zones.json.result?.[0]?.id; if (!zoneId) continue; const existing = await call<{ id: string }[]>(`/zones/${zoneId}/dns_records?name=${encodeURIComponent(domain)}`); if (existing.json.result?.length) { dns = "exists"; break; } const rec = await call(`/zones/${zoneId}/dns_records`, { method: "POST", body: JSON.stringify({ type: "CNAME", name: domain, content: target, proxied: true, ttl: 1 }) }); if (rec.ok) dns = "added"; break; } const status = (await call<{ status?: string }>(`${base}/domains/${domain}`)).json.result?.status ?? added.json.result?.status ?? "pending"; return { domain, target, status, dns } as const; }