← back to Apartmentwallpaper

afternic-unlock.mjs

128 lines

#!/usr/bin/env node
// Raw GoDaddy API attempt: unlock + repoint nameservers off Afternic.
// Run staged:  node afternic-unlock.mjs test     -> tries ONE domain, prints raw response
//              node afternic-unlock.mjs all       -> fans out to all 38 (only after test looks good)
//
// NOTE: This does NOT remove the Afternic for-sale LISTING (no API exists for that).
// It only attempts the registrar-level unlock + nameserver replace. If GoDaddy has
// deprecated these write endpoints for this account you'll see 403 / 422 and we pivot
// to the browser path.
//
// Pure logic (DOMAINS, findCreds, buildRequests, parseMode) is exported for unit tests;
// network/registrar writes only run when this file is executed directly as main.

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

export const DOMAINS = [
  'wallpapercontractor.com','wallpaperdistributors.com','wallpaperestimator.com','allnewsdaily.com',
  'bestactorawards.com','bestdocumentaryfilms.com','cannabisofthemonthclub.com','cleaninggrants.com',
  'cleaninggrants.org','commercialdesignreps.com','designerrepresentatives.com','designerssalesreps.com',
  'designtradelive.com','dsgntv.com','ephedrafreeherbs.com','fauxnewslive.com','hempofthemonthclub.com',
  'herbsmadeinchina.com','homesonspec.com','hospitalitydesignreps.com','hospitalityfabrics.net',
  'hospitalitysalesrepresentatives.com','hospitalitywalls.com','marijuanabuyersexchange.com',
  'marijuanaofthemonthclub.com','mohairvelvet.com','mymemosamples.com','nationalvotingday.org',
  'nodailyworries.com','notvrequired.com','petitionyour.com','petitionyour.org','potofthemonthclub.com',
  'protectyourspec.com','urthfriendly.com','wall-papers.org','wallpapernashville.com','weedofthemonthclub.com'
];

// --- pure: locate GoDaddy key/secret (env first, then domain-suite MCP env block) ---
export function findCreds(env = process.env, readClaudeJson = defaultReadClaudeJson) {
  let key = env.GODADDY_API_KEY || env.GODADDY_KEY;
  let secret = env.GODADDY_API_SECRET || env.GODADDY_SECRET;
  if (key && secret) return { key, secret, src: 'process.env' };
  const cfg = readClaudeJson();
  if (cfg) {
    const hits = [];
    (function walk(o) {
      for (const k in o) {
        if (k === 'mcpServers' && o[k]) {
          for (const s in o[k]) if (/domain|godaddy/i.test(s)) hits.push(o[k][s].env || {});
        } else if (o[k] && typeof o[k] === 'object') walk(o[k]);
      }
    })(cfg);
    for (const e of hits) {
      const kEntry = Object.entries(e).find(([n]) => /godaddy.*key|gd.*key/i.test(n));
      const sEntry = Object.entries(e).find(([n]) => /godaddy.*secret|gd.*secret/i.test(n));
      if (kEntry && sEntry) return { key: kEntry[1], secret: sEntry[1], src: '.claude.json' };
    }
  }
  return null;
}

function defaultReadClaudeJson() {
  try { return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8')); }
  catch { return null; }
}

// --- pure: the exact sequence of API requests we'd make for one domain ---
export function buildRequests(domain) {
  return [
    { method: 'GET', path: `/v1/domains/${domain}`, body: null },
    { method: 'PUT', path: `/v1/domains/${domain}`, body: { locked: false } },
    { method: 'PUT', path: `/v1/domains/${domain}`, body: { nameServers: ['ns01.domaincontrol.com','ns02.domaincontrol.com'] } },
  ];
}

// --- pure: CLI mode -> target list ---
export function parseMode(arg, domains = DOMAINS) {
  const mode = arg === 'all' ? 'all' : 'test';
  return { mode, targets: mode === 'all' ? domains : [domains[0]] };
}

// ---------------------------------------------------------------------------
// main (only runs when executed directly; never on import)
// ---------------------------------------------------------------------------
async function main() {
  const creds = findCreds();
  if (!creds) {
    console.error('Could not find GoDaddy key/secret. Set GODADDY_API_KEY and GODADDY_API_SECRET and re-run.');
    process.exit(1);
  }
  const AUTH = process.env.GODADDY_PAT
    ? `Bearer ${process.env.GODADDY_PAT}`
    : `sso-key ${creds.key}:${creds.secret}`;
  const BASE = process.env.GODADDY_API_BASE || 'https://api.godaddy.com';
  console.error(`Using creds from ${creds.src}; base=${BASE}; key=${String(creds.key).slice(0,4)}…`);

  async function gd(method, urlPath, body) {
    const res = await fetch(BASE + urlPath, {
      method,
      headers: { Authorization: AUTH, 'Content-Type': 'application/json' },
      body: body ? JSON.stringify(body) : undefined,
    });
    return { status: res.status, body: await res.text() };
  }

  async function processDomain(d) {
    const out = { domain: d };
    const [getReq, unlockReq, nsReq] = buildRequests(d);
    const cur = await gd(getReq.method, getReq.path);
    out.before = cur.status === 200
      ? (() => { try { const j = JSON.parse(cur.body); return { locked: j.locked, ns: j.nameServers }; } catch { return cur.body.slice(0,120); } })()
      : `GET ${cur.status}: ${cur.body.slice(0,120)}`;
    const unlock = await gd(unlockReq.method, unlockReq.path, unlockReq.body);
    out.unlock = `${unlock.status}${unlock.body ? ' ' + unlock.body.slice(0,160) : ''}`;
    const ns = await gd(nsReq.method, nsReq.path, nsReq.body);
    out.nameservers = `${ns.status}${ns.body ? ' ' + ns.body.slice(0,160) : ''}`;
    return out;
  }

  const { mode, targets } = parseMode(process.argv[2]);
  console.error(`Mode: ${mode} — ${targets.length} domain(s)\n`);
  const results = [];
  for (const d of targets) {
    const r = await processDomain(d);
    results.push(r);
    console.log(JSON.stringify(r));
  }
  const ok = results.filter(r => /^20[04]/.test(r.unlock)).length;
  console.error(`\nDone. unlock 2xx: ${ok}/${results.length}. (Listing removal still requires the Afternic dashboard.)`);
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  main();
}