← back to Dw Domain Fleet

scripts/dns-cutover.js

113 lines

#!/usr/bin/env node
/**
 * dns-cutover.js — repoint fleet domains from GoDaddy 301-forwarding to the
 * Kamatera origin (45.61.58.125), and remove the GoDaddy domain-forwarding
 * rule so the domain stops redirecting to designerwallcoverings.com.
 *
 * Usage:
 *   node dns-cutover.js --check                 # report current DNS state for all
 *   node dns-cutover.js --batch <domain,...>    # cut over a comma-list
 *   node dns-cutover.js --batch-n <N> --offset <O>   # cut over N domains starting at O
 *
 * Requires GODADDY_API_KEY / GODADDY_API_SECRET in env (sourced from
 * ~/Projects/secrets-manager/.env by the wrapper).
 *
 * SAFE BY DEFAULT: --check makes zero writes. Cutover only on --batch / --batch-n.
 */
const fs = require('fs');
const path = require('path');
const https = require('https');

const ORIGIN_IP = '45.61.58.125';
const KEY = process.env.GODADDY_API_KEY;
const SECRET = process.env.GODADDY_API_SECRET;
if (!KEY || !SECRET) { console.error('FATAL: GODADDY_API_KEY / GODADDY_API_SECRET not set'); process.exit(1); }

const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'data', 'manifest.json'), 'utf8'));
const AUTH = process.env.GODADDY_PAT ? `Bearer ${process.env.GODADDY_PAT}` : `sso-key ${KEY}:${SECRET}`;

function api(method, p, body) {
  return new Promise((resolve, reject) => {
    const data = body ? JSON.stringify(body) : null;
    const req = https.request('https://api.godaddy.com' + p, {
      method,
      headers: {
        Authorization: AUTH,
        'Content-Type': 'application/json',
        ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {})
      }
    }, (res) => {
      let b = '';
      res.on('data', c => (b += c));
      res.on('end', () => resolve({ status: res.statusCode, body: b }));
    });
    req.on('error', reject);
    if (data) req.write(data);
    req.end();
  });
}

const FORWARDING_IPS = ['15.197.', '3.33.', '3.223.', '34.', '23.']; // GoDaddy/AWS forwarding ranges (heuristic)

async function checkOne(domain) {
  const r = await api('GET', `/v1/domains/${domain}/records/A/@`);
  let a = [];
  try { a = JSON.parse(r.body); } catch (e) {}
  const ips = a.map(x => x.data);
  const onOrigin = ips.includes(ORIGIN_IP);
  const forwarding = ips.some(ip => FORWARDING_IPS.some(f => ip.startsWith(f)));
  return { domain, ips, onOrigin, forwarding };
}

async function cutOne(domain) {
  // 1. replace the @ A record(s) with the Kamatera origin
  const put = await api('PUT', `/v1/domains/${domain}/records/A/@`, [
    { data: ORIGIN_IP, ttl: 600 }
  ]);
  // 2. remove GoDaddy domain forwarding (DELETE returns 204 or 404 if none)
  const del = await api('DELETE', `/v1/domains/${domain}/forwards/${domain}`);
  return { domain, aStatus: put.status, fwdStatus: del.status };
}

(async () => {
  const arg = (name) => {
    const i = process.argv.indexOf(name);
    return i >= 0 ? process.argv[i + 1] : null;
  };

  if (process.argv.includes('--check')) {
    console.log(`${'DOMAIN'.padEnd(34)} ON_ORIGIN  FORWARDING  CURRENT_A`);
    for (const m of manifest) {
      const c = await checkOne(m.domain);
      console.log(`${m.domain.padEnd(34)} ${String(c.onOrigin).padEnd(10)} ${String(c.forwarding).padEnd(11)} ${c.ips.join(',')}`);
      await new Promise(r => setTimeout(r, 200));
    }
    return;
  }

  let targets = [];
  const batch = arg('--batch');
  const batchN = arg('--batch-n');
  if (batch) {
    targets = batch.split(',').map(s => s.trim()).filter(Boolean);
  } else if (batchN) {
    const off = parseInt(arg('--offset') || '0', 10);
    targets = manifest.slice(off, off + parseInt(batchN, 10)).map(m => m.domain);
  } else {
    console.error('Specify --check, --batch <list>, or --batch-n <N> --offset <O>');
    process.exit(1);
  }

  console.log(`Cutting over ${targets.length} domain(s) → ${ORIGIN_IP}`);
  for (const d of targets) {
    try {
      const r = await cutOne(d);
      console.log(`  ${d}: A=${r.aStatus} forward-delete=${r.fwdStatus}`);
    } catch (e) {
      console.log(`  ${d}: ERROR ${e.message}`);
    }
    await new Promise(r => setTimeout(r, 400));
  }
  console.log('Done. DNS propagation ~5-30 min. Smoke-test before next batch.');
})();