theseus/lib/resolvers/handshake.cjs
Local Dev 208a192fc9 Theseus 0.3.99: SNS + Unstoppable adapters, Hermes moves to the sidebar
Two more resolvers under Settings › Names & Certificates. SNS — .sol names
resolve through Bonfida's public SDK proxy, preferring an explicit URL
record over an IPFS record (served via a configurable IPFS gateway).
Unstoppable Domains — the full UD TLD roster (.crypto, .nft, .wallet,
.bitcoin, .blockchain, .dao, …) through resolve.unstoppabledomains.com;
browser.redirect_url takes precedence over an ipfs record. .x stays with
BNS — the UD adapter explicitly skips it so a Sirius registration can
never be shadowed by an Unstoppable one.

Both proxies are Cloudflare-fronted; users on blocked IPs can swap the
endpoint in Advanced. The handshake adapter's skip-list grows to cover
.sol and every UD TLD so a user with HNS + UD + SNS all enabled gets the
specific adapter, never the generic hns.to gateway, for those names.

Hermes.X moves from the home page's Silent Mode card grid to the Theseus
left-sidebar quick-link strip, slotting in right after Proton Mail. The
default list grows to 6 entries; existing profiles pick it up through the
LATER_DEFAULTS one-time offer (same mechanism Proton used in 0.3.88).
Users who still have Hermes on their custom home page can remove it manually.
2026-10-21 19:12:19 +03:00

68 lines
6.7 KiB
JavaScript

"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;
// Other extra-resolver TLDs run before HNS in tryExtraResolvers — return
// null here so Handshake doesn't shadow them when their toggle is off and
// the user's intent was clearnet (e.g. a .crypto name that's not registered
// on UD should 404 clearnet, not get hijacked into HNS).
const CLAIMED = new Set([
"eth", "bit", "sol",
// Unstoppable Domains TLDs
"crypto","nft","wallet","bitcoin","blockchain","dao","888","polygon",
"klever","hi","kresus","anime","manga","go","unstoppable","pudgy",
"binanceus","austin","bitget","tball","secret","farms","raiin","stepn",
"emir","smokey","lfg","pog","witg","metropolis","wrkx","benji","altimist",
"ethermail",
]);
if (CLAIMED.has(tld)) 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 };