// 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;
}