96 lines
3.9 KiB
JavaScript
96 lines
3.9 KiB
JavaScript
|
|
"use strict";
|
||
|
|
// Unstoppable Domains resolver — the whole UD TLD roster except .x, which
|
||
|
|
// belongs to BNS/Sirius. "BNS wins on .x" is the design rule: a Sirius
|
||
|
|
// registration must always resolve to the Sirius record, so this adapter
|
||
|
|
// treats .x as not-ours and returns null, letting the chain fall through.
|
||
|
|
//
|
||
|
|
// Resolution: HTTPS GET to resolve.unstoppabledomains.com/domains/<name>.
|
||
|
|
// Response shape (as of 2026-10):
|
||
|
|
// { records: {
|
||
|
|
// "browser.redirect_url": "https://…",
|
||
|
|
// "ipfs.html.value": "<cid>",
|
||
|
|
// "dweb.ipfs.hash": "<cid>",
|
||
|
|
// ...
|
||
|
|
// },
|
||
|
|
// meta: { owner, resolver, blockchain, networkId } }
|
||
|
|
//
|
||
|
|
// Precedence inside the record set: browser.redirect_url > ipfs.html.value
|
||
|
|
// > dweb.ipfs.hash. Users who want a different gateway swap it in Advanced.
|
||
|
|
|
||
|
|
const POS_TTL_MS = 10 * 60 * 1000;
|
||
|
|
const NEG_TTL_MS = 60 * 1000;
|
||
|
|
const RPC_TIMEOUT_MS = 5000;
|
||
|
|
const CACHE = new Map();
|
||
|
|
|
||
|
|
// TLDs on Unstoppable's registries (crypto + .nft + the Polygon-era batch).
|
||
|
|
// Trimmed, not exhaustive — Unstoppable keeps adding new ones; a user whose
|
||
|
|
// TLD isn't here can toggle Handshake "hns-wins" or add a per-host override
|
||
|
|
// later. We explicitly skip .x — BNS owns .x in Theseus.
|
||
|
|
const UD_TLDS = new Set([
|
||
|
|
"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",
|
||
|
|
]);
|
||
|
|
|
||
|
|
async function udFetch(base, name) {
|
||
|
|
const url = base.replace(/\/+$/, "") + "/domains/" + encodeURIComponent(name);
|
||
|
|
const ctrl = new AbortController();
|
||
|
|
const t = setTimeout(() => ctrl.abort(new Error("Unstoppable timeout")), RPC_TIMEOUT_MS);
|
||
|
|
try {
|
||
|
|
const res = await fetch(url, {
|
||
|
|
method: "GET",
|
||
|
|
headers: { "accept": "application/json", "user-agent": "Theseus/0.3.99 (silentmode.st)" },
|
||
|
|
signal: ctrl.signal,
|
||
|
|
});
|
||
|
|
if (!res.ok) throw new Error(`Unstoppable HTTP ${res.status}`);
|
||
|
|
return await res.json();
|
||
|
|
} finally { clearTimeout(t); }
|
||
|
|
}
|
||
|
|
|
||
|
|
async function resolve(host, { endpoint, gateway } = {}) {
|
||
|
|
if (!host || !host.includes(".")) return null;
|
||
|
|
const name = String(host).toLowerCase();
|
||
|
|
const tld = name.split(".").pop();
|
||
|
|
if (!UD_TLDS.has(tld)) return null; // not an Unstoppable TLD
|
||
|
|
// BNS owns .x — never shadow a Sirius registration with an Unstoppable one.
|
||
|
|
if (tld === "x") return null;
|
||
|
|
|
||
|
|
const cached = CACHE.get(name);
|
||
|
|
if (cached && cached.exp > Date.now()) return cached.result;
|
||
|
|
|
||
|
|
const base = endpoint || "https://resolve.unstoppabledomains.com";
|
||
|
|
const gw = (gateway || "https://ipfs.io").replace(/\/+$/, "");
|
||
|
|
|
||
|
|
try {
|
||
|
|
const data = await udFetch(base, name);
|
||
|
|
const records = data?.records || {};
|
||
|
|
const redirect = records["browser.redirect_url"];
|
||
|
|
if (redirect && typeof redirect === "string") {
|
||
|
|
const u = /^https?:\/\//i.test(redirect) ? redirect : `https://${redirect}`;
|
||
|
|
const result = { url: u, protocol: "unstoppable", kind: "redirect", target: u };
|
||
|
|
CACHE.set(name, { result, exp: Date.now() + POS_TTL_MS });
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
const ipfs = records["ipfs.html.value"] || records["dweb.ipfs.hash"];
|
||
|
|
if (ipfs && typeof ipfs === "string") {
|
||
|
|
const result = { url: `${gw}/ipfs/${ipfs}`, protocol: "unstoppable", kind: "ipfs", cid: ipfs };
|
||
|
|
CACHE.set(name, { result, exp: Date.now() + POS_TTL_MS });
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
const result = { error: "no browser.redirect_url or ipfs 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, UD_TLDS };
|