"use strict"; // Namecoin resolver — .bit names via a configured Namecoin JSON-RPC endpoint. // Reads the Namecoin domain record format (dot-bit spec) and follows an // `alias` or `translate` field to a clearnet host when present. Pure IP // records aren't followed yet — rendering those cleanly requires in-browser // DNS override + TLS pinning, which lives in a later phase. // // Namecoin has no public free RPC (unlike publicnode for Ethereum), so the // user must configure their own RPC URL in Settings › Names & Certificates // › Namecoin › Advanced. Common sources: a self-run namecoind, Zeronet's // name-lookup gateway, or a shared RPC from the Namecoin community. // // Namecoin record format (JSON, from name_show "d/"): // {"ip":"192.0.2.1", "ip6":"...", "translate":"example.com", // "alias":"other.bit", "map":{"www":{"ip":"..."}}, ...} // Spec: https://wiki.namecoin.org/index.php?title=Domain_Name_Specification_2.0 const POS_TTL_MS = 10 * 60 * 1000; const NEG_TTL_MS = 60 * 1000; const RPC_TIMEOUT_MS = 5000; const CACHE = new Map(); async function rpcCall(rpc, method, params) { const body = { jsonrpc: "1.0", id: "theseus", method, params }; const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(new Error("Namecoin RPC timeout")), RPC_TIMEOUT_MS); try { const res = await fetch(rpc, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: ctrl.signal, }); if (!res.ok && res.status !== 500) throw new Error(`Namecoin RPC HTTP ${res.status}`); const json = await res.json(); if (json.error) throw new Error(`Namecoin RPC error: ${json.error.message || JSON.stringify(json.error)}`); return json.result; } finally { clearTimeout(t); } } // Follow one hop of a dot-bit `alias` chain. `alias` points to another // Namecoin name (like `example.bit`); `translate` points to a clearnet host. // We walk at most 5 aliases to avoid loops. async function readRecord(rpc, name, hops = 0) { if (hops > 5) throw new Error("alias loop"); const key = `d/${name.replace(/\.bit$/i, "")}`; const row = await rpcCall(rpc, "name_show", [key]); if (!row || !row.value) throw new Error("name not registered"); let rec; try { rec = JSON.parse(row.value); } catch { throw new Error("record is not JSON"); } if (rec.alias && typeof rec.alias === "string") { // alias is another .bit name const next = rec.alias.replace(/\.bit$/i, ""); return readRecord(rpc, next, hops + 1); } return rec; } async function resolve(host, { rpc } = {}) { if (!host) return null; const name = String(host).toLowerCase(); if (!name.endsWith(".bit")) return null; const cached = CACHE.get(name); if (cached && cached.exp > Date.now()) return cached.result; if (!rpc) { const result = { error: "no RPC URL configured (Settings › Names & Certificates › Namecoin › Advanced)" }; CACHE.set(name, { result, exp: Date.now() + NEG_TTL_MS }); return result; } try { const rec = await readRecord(rpc, name); // Prefer translate (clearnet redirect) → navigate there. if (rec.translate && typeof rec.translate === "string") { const dest = rec.translate.replace(/^https?:\/\//, ""); const url = `https://${dest}`; const result = { url, protocol: "namecoin", kind: "translate", target: dest }; CACHE.set(name, { result, exp: Date.now() + POS_TTL_MS }); return result; } // Pure IP records: not followed (needs host-rules + cert handling). if (rec.ip || rec.ip6) { const result = { error: "pure IP records not supported yet; add a translate= field or wait for IP-record support" }; CACHE.set(name, { result, exp: Date.now() + NEG_TTL_MS }); return result; } const result = { error: "no translate/alias/ip record" }; CACHE.set(name, { result, exp: Date.now() + NEG_TTL_MS }); return result; } catch (err) { const result = { error: String(err?.message || err) }; CACHE.set(name, { result, exp: Date.now() + NEG_TTL_MS }); return result; } } function clearCache() { CACHE.clear(); } module.exports = { resolve, clearCache };