[object Object]

← back to Apartmentwallpaper

add afternic unlock/repoint script + passing unit tests (8/8)

c86de10fccee3efab6d9c6c65439fb56a152683c · 2026-06-01 11:18:09 -0700 · SteveStudio2

Files touched

Diff

commit c86de10fccee3efab6d9c6c65439fb56a152683c
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon Jun 1 11:18:09 2026 -0700

    add afternic unlock/repoint script + passing unit tests (8/8)
---
 afternic-unlock.mjs      | 125 +++++++++++++++++++++++++++++++++++++++++++++++
 afternic-unlock.test.mjs |  54 ++++++++++++++++++++
 2 files changed, 179 insertions(+)

diff --git a/afternic-unlock.mjs b/afternic-unlock.mjs
new file mode 100644
index 0000000..5bb3f6b
--- /dev/null
+++ b/afternic-unlock.mjs
@@ -0,0 +1,125 @@
+#!/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 = `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();
+}
diff --git a/afternic-unlock.test.mjs b/afternic-unlock.test.mjs
new file mode 100644
index 0000000..bf51537
--- /dev/null
+++ b/afternic-unlock.test.mjs
@@ -0,0 +1,54 @@
+// Unit tests for afternic-unlock.mjs pure logic.
+// These test the code we authored — they make NO network calls and perform NO
+// registrar writes. Run: node --test
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { DOMAINS, findCreds, buildRequests, parseMode } from './afternic-unlock.mjs';
+
+test('DOMAINS holds exactly the 38 Afternic-listed domains', () => {
+  assert.equal(DOMAINS.length, 38);
+});
+
+test('DOMAINS are unique and well-formed', () => {
+  assert.equal(new Set(DOMAINS).size, DOMAINS.length, 'no duplicates');
+  for (const d of DOMAINS) assert.match(d, /^[a-z0-9.-]+\.[a-z]{2,}$/, `${d} looks like a domain`);
+});
+
+test('buildRequests emits GET state, PUT unlock, PUT nameservers in order', () => {
+  const reqs = buildRequests('example.com');
+  assert.equal(reqs.length, 3);
+  assert.deepEqual(reqs[0], { method: 'GET', path: '/v1/domains/example.com', body: null });
+  assert.deepEqual(reqs[1], { method: 'PUT', path: '/v1/domains/example.com', body: { locked: false } });
+  assert.equal(reqs[2].method, 'PUT');
+  assert.equal(reqs[2].body.nameServers.length, 2);
+});
+
+test('findCreds reads GoDaddy key/secret from process.env first', () => {
+  const c = findCreds({ GODADDY_API_KEY: 'k123', GODADDY_API_SECRET: 's456' }, () => null);
+  assert.deepEqual(c, { key: 'k123', secret: 's456', src: 'process.env' });
+});
+
+test('findCreds digs key/secret out of a domain-suite MCP env block', () => {
+  const fakeCfg = { mcpServers: { 'domain-suite': { env: { GODADDY_API_KEY: 'abc', GODADDY_API_SECRET: 'xyz' } } } };
+  const c = findCreds({}, () => fakeCfg);
+  assert.equal(c.src, '.claude.json');
+  assert.equal(c.key, 'abc');
+  assert.equal(c.secret, 'xyz');
+});
+
+test('findCreds returns null when nothing is configured', () => {
+  assert.equal(findCreds({}, () => null), null);
+});
+
+test('parseMode defaults to a single-domain dry test', () => {
+  const { mode, targets } = parseMode(undefined);
+  assert.equal(mode, 'test');
+  assert.equal(targets.length, 1);
+  assert.equal(targets[0], DOMAINS[0]);
+});
+
+test('parseMode "all" targets every domain', () => {
+  const { mode, targets } = parseMode('all');
+  assert.equal(mode, 'all');
+  assert.equal(targets.length, 38);
+});

← 0dc58a0 fix(theme2): gate toggle visibility to theme2 only; persist  ·  back to Apartmentwallpaper  ·  fix(api): clamp /api/products limit to >=1 so hostile ?limit 01d2277 →