← back to Apartmentwallpaper
add afternic unlock/repoint script + passing unit tests (8/8)
c86de10fccee3efab6d9c6c65439fb56a152683c · 2026-06-01 11:18:09 -0700 · SteveStudio2
Files touched
A afternic-unlock.mjsA afternic-unlock.test.mjs
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 →