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