theseus/lib/resolvers/namecoin.cjs
Local Dev 227d013013 Theseus 0.3.98: Namecoin + Handshake adapters
Two more adapters under Settings › Names & Certificates:

Namecoin — .bit names resolve by calling name_show "d/<name>" on the
configured JSON-RPC endpoint and following `translate` or `alias` records
to a clearnet host. Pure IP records aren't followed yet (that needs
in-browser DNS override + TLS pinning, which comes later). Namecoin has
no free public RPC, so the toggle is off by default and the row carries a
"Needs an RPC URL" note instead of a "Not wired yet" one; set yours in
Advanced.

Handshake — non-ICANN hostnames resolve through the public hns.to web
gateway (user-overridable in Advanced). hns.to proxies the request to a
Handshake node and serves the on-chain content. A bundled list of common
ICANN TLDs (original gTLDs + popular new gTLDs + every ccTLD) keeps the
default collision policy ("icann-wins") from hijacking clearnet names.
Setting the policy to "hns-wins" removes that gate.

Both adapters follow the ENS adapter's shape — tryExtraResolvers in main.js
calls each in turn after the BNS lookup; the first one that returns a URL
loads it. Settings copy on both rows updated.
2026-10-21 02:40:33 +03:00

99 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 };