← back to Coming Soon Template
scripts/cf_dns.js
108 lines
#!/usr/bin/env node
/* Create the apex CNAME record in each Cloudflare zone so the domain
* serves its Cloudflare Pages site once nameservers flip.
*
* For every Pages project on the account whose name matches <zone-name>
* minus ".com", creates a proxied apex CNAME @ -> <project>.pages.dev.
* (CNAME flattening makes an apex CNAME valid when proxied.)
*
* Idempotent: an existing matching record is left alone; a wrong one is
* updated. Only touches zones that have a corresponding Pages project,
* so domains with no project (e.g. project-limit casualties) are skipped.
*
* Usage: node cf_dns.js
* Writes data/dns-setup.json (domain, project, record_id, status).
*/
'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
const ENV = path.join(os.homedir(), 'Projects/secrets-manager/.env');
const ROOT = path.join(os.homedir(), 'Projects/coming-soon-template');
const OUT = path.join(ROOT, 'data/dns-setup.json');
function envVal(key) {
const line = fs.readFileSync(ENV, 'utf8').split('\n').find(l => l.startsWith(key + '='));
return line ? line.slice(key.length + 1).trim() : '';
}
const TOK = envVal('CLOUDFLARE_API_TOKEN');
const ACCT = envVal('CLOUDFLARE_ACCOUNT_ID');
if (!TOK || !ACCT) { console.error('missing CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID in', ENV); process.exit(1); }
const CF = 'https://api.cloudflare.com/client/v4';
const HEADERS = { 'Authorization': `Bearer ${TOK}`, 'Content-Type': 'application/json' };
async function cf(method, urlPath, body) {
const res = await fetch(CF + urlPath, {
method, headers: HEADERS, body: body ? JSON.stringify(body) : undefined,
});
let json;
try { json = await res.json(); } catch { json = { success: false, errors: [{ code: res.status, message: 'non-JSON response' }] }; }
return { http: res.status, json };
}
const errStr = j => (j.errors || []).map(e => `${e.code}:${e.message}`).join(' | ') || '(no detail)';
async function listProjects() {
const names = [];
for (let page = 1; page <= 10; page++) {
const { json } = await cf('GET', `/accounts/${ACCT}/pages/projects?page=${page}`);
const r = json.result || [];
r.forEach(p => names.push(p.name));
if (!r.length || (json.result_info && page >= json.result_info.total_pages)) break;
}
return names;
}
async function main() {
const projects = new Set(await listProjects());
const zones = JSON.parse(fs.readFileSync(path.join(ROOT, 'data/zone-setup.json'), 'utf8'));
const results = [];
for (const z of zones) {
const domain = z.domain;
const project = domain.replace(/\.com$/, '');
if (!projects.has(project)) {
console.log(`SKIP ${domain} — no Pages project '${project}' (project-limit casualty)`);
results.push({ domain, status: 'skipped_no_project' });
continue;
}
const target = `${project}.pages.dev`;
const zid = z.zone_id;
// existing apex record?
const found = await cf('GET', `/zones/${zid}/dns_records?name=${domain}`);
const apex = (found.json.result || []).find(r => r.name === domain && (r.type === 'CNAME' || r.type === 'A' || r.type === 'AAAA'));
let rec;
if (apex && apex.type === 'CNAME' && apex.content === target && apex.proxied) {
rec = { domain, project, status: 'already_correct', record_id: apex.id };
console.log(`OK ${domain} [already correct] -> ${target}`);
} else if (apex) {
const upd = await cf('PUT', `/zones/${zid}/dns_records/${apex.id}`,
{ type: 'CNAME', name: domain, content: target, proxied: true, ttl: 1 });
if (upd.json.success) {
rec = { domain, project, status: 'updated', record_id: upd.json.result.id };
console.log(`OK ${domain} [updated apex] -> ${target}`);
} else {
rec = { domain, project, status: 'update_failed', error: errStr(upd.json) };
console.log(`FAIL ${domain} [HTTP ${upd.http}] ${rec.error}`);
}
} else {
const created = await cf('POST', `/zones/${zid}/dns_records`,
{ type: 'CNAME', name: domain, content: target, proxied: true, ttl: 1 });
if (created.json.success) {
rec = { domain, project, status: 'created', record_id: created.json.result.id };
console.log(`OK ${domain} [created apex CNAME] -> ${target}`);
} else {
rec = { domain, project, status: 'create_failed', error: errStr(created.json) };
console.log(`FAIL ${domain} [HTTP ${created.http}] ${rec.error}`);
}
}
results.push(rec);
}
fs.writeFileSync(OUT, JSON.stringify(results, null, 2));
const ok = results.filter(r => /already_correct|created|updated/.test(r.status)).length;
const skip = results.filter(r => r.status === 'skipped_no_project').length;
console.log(`\n--- ${ok} records ready, ${skip} skipped --- wrote ${OUT}`);
}
main().catch(e => { console.error('FATAL', e); process.exit(1); });