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.
This commit is contained in:
parent
6001686f09
commit
227d013013
5 changed files with 198 additions and 17 deletions
58
lib/resolvers/handshake.cjs
Normal file
58
lib/resolvers/handshake.cjs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"use strict";
|
||||
// Handshake resolver — routes non-ICANN TLDs through a public HNS web
|
||||
// gateway (hns.to by default). hns.to receives `foo.<tld>.hns.to`, looks up
|
||||
// the Handshake TLD, fetches the on-chain site, and serves it back. From
|
||||
// Theseus's side the adapter is a one-liner URL rewrite + a collision check
|
||||
// to decide whether to touch the hostname at all.
|
||||
//
|
||||
// The collision check matters: Handshake auctioned on-chain TLDs that
|
||||
// collide with real DNS names (e.g. ".app", ".dev"). We don't want to
|
||||
// hijack those by default. With collision_policy = "icann-wins" (default),
|
||||
// the adapter returns null for any TLD in a bundled "common ICANN" list so
|
||||
// clearnet takes over. With "hns-wins", we always try HNS first.
|
||||
//
|
||||
// The list isn't the full IANA root zone (~1600 entries). It's the few
|
||||
// hundred TLDs real users actually see: original gTLDs, popular new gTLDs,
|
||||
// every ccTLD. If a TLD escapes the list and happens to collide with an
|
||||
// HNS registration, the user can switch to "icann-wins" and whitelist via
|
||||
// per-host overrides (future). Nothing breaks silently — the collision is
|
||||
// opt-in by the user turning HNS on.
|
||||
|
||||
const CACHE_TTL_MS = 10 * 60 * 1000;
|
||||
const CACHE = new Map();
|
||||
|
||||
// Common ICANN TLDs — the ones a browser would actually see in practice.
|
||||
// Not exhaustive; grown as needed.
|
||||
const ICANN_TLDS = new Set([
|
||||
// original gTLDs
|
||||
"com","org","net","edu","gov","mil","int","arpa","info","biz","name","pro","museum","aero","coop","jobs","mobi","tel","travel","cat","asia","post","tv","me","cc","ws","bz","su","ac",
|
||||
// common new gTLDs
|
||||
"app","dev","io","co","xyz","online","site","shop","store","tech","cloud","blog","news","world","world","global","global","email","agency","media","design","photo","art","gallery","pub","bar","cafe","wine","rest","restaurant","hotel","travel","fitness","gym","run","bike","boats","car","cars","fish","flight","flights","garden","farm","pet","pizza","realtor","salon","school","soccer","software","solar","taxi","vet","yoga","academy","band","bank","beer","beauty","best","buzz","build","builders","cash","chat","church","city","claims","clinic","clothing","club","coach","codes","coffee","community","company","computer","construction","contact","contractors","cool","country","credit","dance","deals","delivery","dental","dentist","digital","direct","directory","discount","doctor","download","education","energy","engineer","engineering","enterprises","equipment","events","expert","exposed","express","family","fashion","finance","financial","fit","fitness","food","foundation","fund","furniture","games","gift","glass","gold","graphics","group","guide","guru","haus","healthcare","help","holdings","holiday","home","host","house","icu","immo","industries","ink","insurance","international","investments","jewelry","kitchen","land","lawyer","legal","life","lighting","limited","limo","link","live","loan","loans","lol","love","ltd","management","market","marketing","marketplace","mba","media","medical","money","mortgage","movie","music","network","ninja","parts","party","photo","photography","photos","pics","pictures","plumbing","plus","press","productions","properties","pub","quest","radio","recipes","reviews","rip","run","sale","salon","school","science","security","services","shoes","shop","show","singles","soccer","software","solar","solutions","space","sport","store","studio","style","supplies","supply","support","surgery","systems","taxi","team","tech","technology","theatre","tickets","today","tools","tours","town","trade","training","transfer","travel","tv","university","vacations","ventures","vet","video","villas","vision","vote","voyage","watch","website","wedding","wiki","win","work","works","world","xxx","zone","gay","ltda","health","hiphop","fail","flowers","forsale","free","garden","gives","giving","golf","loan","map","mom","organic","place","play","promo","review","rodeo","show","shows","sky","soccer","social","spa","style","sucks","survey","tech","tires","top","vote","wtf","vip","hey","kiwi","earth",
|
||||
// common ccTLDs (two-letter)
|
||||
"ac","ad","ae","af","ag","ai","al","am","ao","aq","ar","as","at","au","aw","ax","az","ba","bb","bd","be","bf","bg","bh","bi","bj","bm","bn","bo","br","bs","bt","bv","bw","by","bz","ca","cc","cd","cf","cg","ch","ci","ck","cl","cm","cn","co","cr","cu","cv","cw","cx","cy","cz","de","dj","dk","dm","do","dz","ec","ee","eg","er","es","et","eu","fi","fj","fk","fm","fo","fr","ga","gb","gd","ge","gf","gg","gh","gi","gl","gm","gn","gp","gq","gr","gs","gt","gu","gw","gy","hk","hm","hn","hr","ht","hu","id","ie","il","im","in","io","iq","ir","is","it","je","jm","jo","jp","ke","kg","kh","ki","km","kn","kp","kr","kw","ky","kz","la","lb","lc","li","lk","lr","ls","lt","lu","lv","ly","ma","mc","md","me","mg","mh","mk","ml","mm","mn","mo","mp","mq","mr","ms","mt","mu","mv","mw","mx","my","mz","na","nc","ne","nf","ng","ni","nl","no","np","nr","nu","nz","om","pa","pe","pf","pg","ph","pk","pl","pm","pn","pr","ps","pt","pw","py","qa","re","ro","rs","ru","rw","sa","sb","sc","sd","se","sg","sh","si","sj","sk","sl","sm","sn","so","sr","ss","st","sv","sx","sy","sz","tc","td","tf","tg","th","tj","tk","tl","tm","tn","to","tr","tt","tv","tw","tz","ua","ug","uk","us","uy","uz","va","vc","ve","vg","vi","vn","vu","wf","ws","ye","yt","za","zm","zw",
|
||||
]);
|
||||
|
||||
async function resolve(host, { endpoint, collision_policy } = {}) {
|
||||
if (!host || !host.includes(".")) return null;
|
||||
const name = String(host).toLowerCase();
|
||||
// Collision gate — only skip ICANN TLDs when icann-wins is set (the default).
|
||||
const tld = name.split(".").pop();
|
||||
if ((collision_policy || "icann-wins") === "icann-wins" && ICANN_TLDS.has(tld)) return null;
|
||||
// BNS TLDs are already handled by the normal BNS path upstream; HNS adapter
|
||||
// runs for every other dotted host. .eth / .bit go through their own adapters
|
||||
// first, so by the time we get here they've been eliminated.
|
||||
if (tld === "eth" || tld === "bit") return null;
|
||||
|
||||
const cached = CACHE.get(name);
|
||||
if (cached && cached.exp > Date.now()) return cached.result;
|
||||
|
||||
const gw = (endpoint || "https://hns.to").replace(/\/+$/, "").replace(/^https?:\/\//, "");
|
||||
const url = `https://${name}.${gw}`;
|
||||
const result = { url, protocol: "handshake", gateway: gw };
|
||||
CACHE.set(name, { result, exp: Date.now() + CACHE_TTL_MS });
|
||||
return result;
|
||||
}
|
||||
|
||||
function clearCache() { CACHE.clear(); }
|
||||
|
||||
module.exports = { resolve, clearCache, ICANN_TLDS };
|
||||
99
lib/resolvers/namecoin.cjs
Normal file
99
lib/resolvers/namecoin.cjs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
"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 };
|
||||
50
main.js
50
main.js
|
|
@ -888,7 +888,7 @@ const DEFAULT_EXTRA_RESOLVERS = {
|
|||
resolvers: {
|
||||
ens: { enabled: false, rpc: "https://ethereum-rpc.publicnode.com", gateway: "https://ipfs.io" },
|
||||
namecoin: { enabled: false, rpc: "" },
|
||||
handshake: { enabled: false, endpoint: "https://hnsrecursive.easyhandshake.com", collision_policy: "icann-wins" },
|
||||
handshake: { enabled: false, endpoint: "https://hns.to", collision_policy: "icann-wins" },
|
||||
sns: { enabled: false, rpc: "https://api.mainnet-beta.solana.com" },
|
||||
unstoppable: { enabled: false, endpoint: "https://resolve.unstoppabledomains.com" },
|
||||
},
|
||||
|
|
@ -5784,6 +5784,19 @@ async function tryExtraResolvers(t, id, host, rest) {
|
|||
const resolvers = cfg?.resolvers || {};
|
||||
const h = String(host || "").toLowerCase();
|
||||
|
||||
// Shared helper: load the gateway URL + rest into the tab, update prov,
|
||||
// and return true so the caller knows to stop looking at later adapters.
|
||||
async function navigateToExtra(result, kind, extraProv) {
|
||||
const path = rest && rest !== "/" ? rest : "/";
|
||||
const target = result.url + (path === "/" ? "" : path);
|
||||
t.url = target;
|
||||
t.prov = { host: h, kind, ...(extraProv || {}) };
|
||||
if (isActiveId(id)) pushNav(t.prov);
|
||||
emitTabs();
|
||||
await t.view.webContents.loadURL(target).catch(() => {});
|
||||
return true;
|
||||
}
|
||||
|
||||
// ENS — .eth → ENS registry → content hash → IPFS/IPNS gateway.
|
||||
if (h.endsWith(".eth") && resolvers.ens?.enabled) {
|
||||
try {
|
||||
|
|
@ -5792,21 +5805,32 @@ async function tryExtraResolvers(t, id, host, rest) {
|
|||
rpc: resolvers.ens.rpc,
|
||||
gateway: resolvers.ens.gateway,
|
||||
});
|
||||
if (result && result.url) {
|
||||
const path = rest && rest !== "/" ? rest : "/";
|
||||
// Path-based IPFS gateways accept /ipfs/<cid>/<path>. Keep the query
|
||||
// string intact if the caller carried one through `rest`.
|
||||
const target = result.url + (path === "/" ? "" : path);
|
||||
t.url = target;
|
||||
t.prov = { host: h, kind: "ens", protocol: result.protocol, cid: result.cid, uri: result.uri };
|
||||
if (isActiveId(id)) pushNav(t.prov);
|
||||
emitTabs();
|
||||
await t.view.webContents.loadURL(target).catch(() => {});
|
||||
return true;
|
||||
}
|
||||
if (result && result.url) return navigateToExtra(result, "ens", { protocol: result.protocol, cid: result.cid, uri: result.uri });
|
||||
} catch (e) { console.warn("[ens] resolve failed:", e?.message); }
|
||||
}
|
||||
|
||||
// Namecoin — .bit → name_show "d/<name>" on the configured RPC → follow
|
||||
// translate/alias to a clearnet host.
|
||||
if (h.endsWith(".bit") && resolvers.namecoin?.enabled) {
|
||||
try {
|
||||
const nmc = require("./lib/resolvers/namecoin.cjs");
|
||||
const result = await nmc.resolve(h, { rpc: resolvers.namecoin.rpc });
|
||||
if (result && result.url) return navigateToExtra(result, "namecoin", { target: result.target });
|
||||
} catch (e) { console.warn("[namecoin] resolve failed:", e?.message); }
|
||||
}
|
||||
|
||||
// Handshake — route non-ICANN TLDs through hns.to (or configured gateway).
|
||||
if (resolvers.handshake?.enabled) {
|
||||
try {
|
||||
const hns = require("./lib/resolvers/handshake.cjs");
|
||||
const result = await hns.resolve(h, {
|
||||
endpoint: resolvers.handshake.endpoint,
|
||||
collision_policy: resolvers.handshake.collision_policy,
|
||||
});
|
||||
if (result && result.url) return navigateToExtra(result, "handshake", { gateway: result.gateway });
|
||||
} catch (e) { console.warn("[handshake] resolve failed:", e?.message); }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "theseus-navigator",
|
||||
"version": "0.3.97",
|
||||
"version": "0.3.98",
|
||||
"license": "MPL-2.0",
|
||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||
"author": "Silent Mode",
|
||||
|
|
|
|||
|
|
@ -493,7 +493,7 @@
|
|||
<div class="row">
|
||||
<div class="txt">
|
||||
<div class="t">Namecoin</div>
|
||||
<div class="d"><i>.bit</i> → DNS record from the Namecoin blockchain. <span class="muted" id="rvNotWiredNmc">Not wired yet — coming in 0.3.97.</span></div>
|
||||
<div class="d"><i>.bit</i> → record from the Namecoin blockchain; <code>translate</code> or <code>alias</code> redirects are followed. <b>Needs an RPC URL</b> — Namecoin has no public free endpoint; set yours in Advanced.</div>
|
||||
<div class="acts"><details class="adv"><summary>Advanced</summary>
|
||||
<label>Namecoin JSON-RPC URL</label><input type="text" id="rvNmcRpc" placeholder="http://user:pass@127.0.0.1:8336">
|
||||
</details></div>
|
||||
|
|
@ -504,9 +504,9 @@
|
|||
<div class="row">
|
||||
<div class="txt">
|
||||
<div class="t">Handshake (HNS)</div>
|
||||
<div class="d">User-registered TLDs on the Handshake chain. Handshake auctioned many 3-letter and ICANN-style TLDs on-chain, so collisions are common. <span class="muted" id="rvNotWiredHns">Not wired yet — coming in 0.3.97.</span></div>
|
||||
<div class="d">User-registered TLDs on the Handshake chain, served through the public <code>hns.to</code> web gateway. The gateway proxies the request to Handshake's own resolver and serves the on-chain content. Handshake auctioned many 3-letter and ICANN-style TLDs on-chain, so the default collision policy keeps ICANN TLDs in clearnet.</div>
|
||||
<div class="acts"><details class="adv"><summary>Advanced</summary>
|
||||
<label>Handshake resolver endpoint</label><input type="text" id="rvHnsEndpoint" placeholder="https://hnsrecursive.easyhandshake.com">
|
||||
<label>Handshake gateway</label><input type="text" id="rvHnsEndpoint" placeholder="https://hns.to">
|
||||
<label>Collision policy</label>
|
||||
<select id="rvHnsCollisionPolicy">
|
||||
<option value="icann-wins">ICANN wins (safer; HNS only resolves TLDs the DNS root doesn't delegate)</option>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue