← 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); });