← back to Coming Soon Template

scripts/cf_setup.js

98 lines

#!/usr/bin/env node
/* Cloudflare zone setup for the 21 abrams/butler coming-soon domains.
 * Replaces the broken bash+python scripts (their python one-liners had
 * backslash-escaped quotes inside f-strings -> SyntaxError, swallowed
 * every real CF error as "").
 *
 * Usage:
 *   node cf_setup.js            # create CF zones, capture real errors
 *   node cf_setup.js --ns-plan  # after zones exist: print GoDaddy NS-flip plan
 *
 * Idempotent: an existing zone is reused, never duplicated.
 * Writes data/zone-setup.json (domain, status, zone_id, cf_ns[], live_mx).
 */
'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execSync } = require('child_process');

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/zone-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 manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'manifest.json'), 'utf8'));
const domains = manifest.domains.map(d => d.domain);

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

function digMX(domain) {
  try {
    return execSync(`/usr/bin/dig +short MX ${domain}`, { encoding: 'utf8' })
      .trim().split('\n').filter(Boolean).slice(0, 3).join('; ');
  } catch { return ''; }
}

async function nsPlan() {
  if (!fs.existsSync(OUT)) { console.error('run `node cf_setup.js` first to create zones'); process.exit(1); }
  const zones = JSON.parse(fs.readFileSync(OUT, 'utf8'));
  console.log('=== GoDaddy NS-flip plan (domains with live MX are HELD BACK) ===\n');
  for (const z of zones) {
    if (!['created', 'already_exists'].includes(z.status)) { console.log(`SKIP  ${z.domain}  — zone status: ${z.status}`); continue; }
    if (z.live_mx) { console.log(`HOLD  ${z.domain}  — live MX present (${z.live_mx})`); continue; }
    console.log(`FLIP  ${z.domain}  -> ${(z.cf_ns || []).join(', ')}`);
  }
}

async function main() {
  if (process.argv.includes('--ns-plan')) return nsPlan();

  const results = [];
  for (const domain of domains) {
    // existing zone?
    const found = await cf('GET', `/zones?name=${domain}&account.id=${ACCT}`);
    const existing = (found.json.result || [])[0];
    let rec;
    if (existing) {
      rec = { domain, status: 'already_exists', zone_id: existing.id, cf_ns: existing.name_servers || [] };
      console.log(`OK    ${domain}  [already_exists]  ns=${(rec.cf_ns).join(',')}`);
    } else {
      const created = await cf('POST', '/zones', { account: { id: ACCT }, name: domain, type: 'full' });
      if (created.json.success) {
        const r = created.json.result;
        rec = { domain, status: 'created', zone_id: r.id, cf_ns: r.name_servers || [] };
        console.log(`OK    ${domain}  [created]  ns=${(rec.cf_ns).join(',')}`);
      } else {
        const errs = (created.json.errors || []).map(e => `${e.code}:${e.message}`).join(' | ');
        rec = { domain, status: 'create_failed', http: created.http, error: errs || '(no error detail)' };
        console.log(`FAIL  ${domain}  [HTTP ${created.http}]  ${rec.error}`);
      }
    }
    rec.live_mx = digMX(domain);
    if (rec.live_mx) console.log(`        ^ live MX: ${rec.live_mx}`);
    results.push(rec);
  }
  fs.writeFileSync(OUT, JSON.stringify(results, null, 2));
  const ok = results.filter(r => r.status !== 'create_failed').length;
  console.log(`\n--- ${ok}/${results.length} zones ready --- wrote ${OUT}`);
}
main().catch(e => { console.error('FATAL', e); process.exit(1); });