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