100 lines
4.1 KiB
JavaScript
100 lines
4.1 KiB
JavaScript
|
|
"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 };
|