theseus/lib/resolvers/namecoin.cjs

100 lines
4.1 KiB
JavaScript
Raw Normal View History

"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/<name>"):
// {"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 };