← back to Domain Sniper

register.js

181 lines

#!/usr/bin/env node
// register.js — atomic domain registration via direct registrar API.
//
// SAFETY: defaults to DRY-RUN. Pass --confirm to actually spend money.
// In YOLO autonomous mode, NEVER pass --confirm — registrations are not reversible.
//
// Backend auto-detection (cheapest/simplest first):
//   - NAMESILO_API_KEY                          → NameSilo  (~$9 .com, no IP whitelist)
//   - NAMECHEAP_API_USER + NAMECHEAP_API_KEY    → Namecheap (needs IP whitelist + $50 prefund)
//   - GODADDY_KEY + GODADDY_SECRET              → GoDaddy   (needs payment on file)
//
// Skip-check pattern: this script DOES NOT call any availability API before
// registering. The registrar tells us success-or-taken in one round-trip. The
// "check" step IS the leak we're trying to avoid.
//
// Usage:
//   node register.js <domain>              # dry-run
//   node register.js <domain> --confirm    # actually buy

const https = require('https');
const fs = require('fs');
const path = require('path');

const { isOwned } = require('./owned-check');

const DATA_DIR = path.join(__dirname, 'data');
const LOG_FILE = path.join(DATA_DIR, 'registrations.jsonl');

function detectBackend() {
  if (process.env.NAMESILO_API_KEY) return 'namesilo';
  if (process.env.NAMECHEAP_API_USER && process.env.NAMECHEAP_API_KEY) return 'namecheap';
  if (process.env.GODADDY_KEY && process.env.GODADDY_SECRET) return 'godaddy';
  return null;
}

function httpsRequest(opts, body) {
  return new Promise((resolve, reject) => {
    const req = https.request(opts, (res) => {
      let d = '';
      res.on('data', (c) => (d += c));
      res.on('end', () => resolve({ status: res.statusCode, body: d }));
    });
    req.on('error', reject);
    if (body) req.write(body);
    req.end();
  });
}

async function registerNamesilo(domain) {
  const key = process.env.NAMESILO_API_KEY;
  const url = `/api/registerDomain?version=1&type=xml&key=${encodeURIComponent(key)}&domain=${encodeURIComponent(domain)}&years=1&private=1&auto_renew=0`;
  const r = await httpsRequest({ hostname: 'www.namesilo.com', path: url, method: 'GET' });
  const code = (r.body.match(/<code>(\d+)<\/code>/) || [])[1];
  const detail = (r.body.match(/<detail>([^<]+)<\/detail>/) || [])[1];
  // NameSilo 300 = success
  return { success: code === '300', code, detail, http: r.status };
}

async function registerNamecheap(domain) {
  const user = process.env.NAMECHEAP_API_USER;
  const key = process.env.NAMECHEAP_API_KEY;
  const clientIp = process.env.NAMECHEAP_CLIENT_IP || '127.0.0.1';
  const username = process.env.NAMECHEAP_USERNAME || user;
  const contact = process.env.NAMECHEAP_CONTACT_JSON
    ? JSON.parse(process.env.NAMECHEAP_CONTACT_JSON)
    : null;
  if (!contact) {
    return {
      success: false,
      code: 'NO_CONTACT',
      detail: 'NAMECHEAP_CONTACT_JSON env required (firstName/lastName/address1/city/stateProvince/postalCode/country/phone/emailAddress). Namecheap registration needs full registrant contact.',
    };
  }
  const params = new URLSearchParams({
    ApiUser: user, ApiKey: key, UserName: username, ClientIp: clientIp,
    Command: 'namecheap.domains.create', DomainName: domain, Years: '1',
  });
  for (const role of ['Registrant', 'Tech', 'Admin', 'AuxBilling']) {
    for (const [k, v] of Object.entries(contact)) {
      params.set(role + k.charAt(0).toUpperCase() + k.slice(1), v);
    }
  }
  const r = await httpsRequest(
    { hostname: 'api.namecheap.com', path: '/xml.response?' + params.toString(), method: 'GET' },
    null,
  );
  // Two-layer success check: the API call must have Status="OK" AND the
  // inner <DomainCreateResult Registered="true"...> must confirm the actual
  // registration. Status="OK" alone only means "API request was valid" — it
  // does NOT mean the domain was registered (the create result can still
  // come back Registered="false" with the call still nominally OK).
  const apiOk = /Status="OK"/.test(r.body);
  const regOk = /<DomainCreateResult\b[^>]*\bRegistered="true"/i.test(r.body);
  const success = apiOk && regOk;
  const errMatch = r.body.match(/<Error[^>]*>([^<]+)<\/Error>/);
  let detail;
  if (success) detail = 'registered';
  else if (!apiOk && errMatch) detail = errMatch[1];
  else if (apiOk && !regOk) detail = 'api ok but DomainCreateResult.Registered != true';
  else detail = 'unknown';
  return { success, code: success ? 'OK' : 'ERROR', detail, http: r.status };
}

async function registerGodaddy(domain) {
  // GoDaddy /v1/domains/purchase needs full contact JSON + signed consent.
  // Implementing this safely requires Steve's stored contact + payment method.
  // For now, refuse to call so we don't silently fail-open and bill him.
  return {
    success: false,
    code: 'NOT_WIRED',
    detail: 'GoDaddy direct-purchase needs full contact + consent payload + saved payment method. Wire via secrets-manager + GODADDY_PURCHASE_PAYLOAD env before use.',
  };
}

async function main() {
  const args = process.argv.slice(2);
  const domain = args.find((a) => !a.startsWith('--'));
  const confirm = args.includes('--confirm');

  if (!domain) {
    console.error('usage: node register.js <domain> [--confirm]');
    console.error('  default = DRY-RUN. Pass --confirm to actually buy.');
    process.exit(2);
  }
  if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z]{2,})+$/i.test(domain)) {
    console.error(`invalid domain syntax: ${domain}`);
    process.exit(2);
  }
  if (isOwned(domain)) {
    console.error(`\n  \x1b[31mREFUSED:\x1b[0m ${domain} is in data/owned.json — already owned.`);
    console.error(`  Remove it from owned.json first if you really intend to re-register.\n`);
    process.exit(5);
  }

  const backend = detectBackend();

  console.log(`\n  domain:   ${domain}`);
  console.log(`  backend:  ${backend || '(none — credentials missing)'}`);
  console.log(`  mode:     ${confirm ? '\x1b[31mREAL PURCHASE\x1b[0m' : 'dry-run'}`);

  if (!backend) {
    console.error(`\n  no registrar credentials found in env. add via the \`secrets\` skill:`);
    console.error(`    NAMESILO_API_KEY                        # cheapest, simplest`);
    console.error(`    NAMECHEAP_API_USER + NAMECHEAP_API_KEY  # needs IP whitelist + prefund`);
    console.error(`    GODADDY_KEY + GODADDY_SECRET            # needs payment-on-file`);
    process.exit(3);
  }

  if (!confirm) {
    console.log(`\n  [DRY-RUN] would register ${domain} via ${backend}.`);
    console.log(`  pass --confirm to spend money. registration is NOT reversible.\n`);
    return;
  }

  // Real call
  let result;
  const start = Date.now();
  try {
    if (backend === 'namesilo') result = await registerNamesilo(domain);
    else if (backend === 'namecheap') result = await registerNamecheap(domain);
    else if (backend === 'godaddy') result = await registerGodaddy(domain);
  } catch (e) {
    result = { success: false, code: 'EXCEPTION', detail: e.message };
  }
  const elapsed = Date.now() - start;

  const rec = { ts: new Date().toISOString(), domain, backend, result, elapsedMs: elapsed };
  fs.mkdirSync(DATA_DIR, { recursive: true });
  fs.appendFileSync(LOG_FILE, JSON.stringify(rec) + '\n');

  if (result.success) {
    console.log(`\n  \x1b[32m✓ REGISTERED ${domain}\x1b[0m  (${elapsed}ms via ${backend})`);
    console.log(`    log: ${LOG_FILE}\n`);
  } else {
    console.log(`\n  \x1b[31m✗ FAILED:\x1b[0m ${result.code || ''} — ${result.detail || ''}\n`);
    process.exit(4);
  }
}

main().catch((e) => { console.error('error:', e.message); process.exit(1); });