// BNS public gateway (the "remote relay", convenience tier). // Runs on the VPS. Resolves .bch from the chain (zero-dep resolver) and serves // the content over plain HTTPS so ANY device's browser can view .bch sites with // no install. Two URL forms: // path form: https://navigate.st/bns/.bch/... ( injected) // subdomain form: https://.bch.navigate.st/ (own origin, no ) // where dots in a multi-level name become dashes: coinspectrum.deviant.bch // -> coinspectrum-deviant.bch.navigate.st // // Honest disclosure (convenience tier, not the trustless one): the operator can // see which .bch sites you request. No logs — but that's trust. Install the // resolver or Theseus for trustless resolution; use Tor for IP privacy. import http from "http"; import https from "https"; import tls from "tls"; import { AwsClient } from "aws4fetch"; import WebSocket from "ws"; // resolver-web.js lives at ../lib/, not alongside this file — the previous // `./resolver-web.js` import was broken (file was moved into lib/ but this // import wasn't updated). Fix while touching the file. import { resolveName, buildIndex, normalizeName, CHIPNET_ELECTRUM } from "../lib/resolver-web.js"; import { pickRecord, isSubdomainOf } from "../lib/record-picker.js"; import { buildTldSnapshot } from "../lib/tld-index.js"; // TLD-certificate beacon scripthash (Decentralized.DNS/DESIGN-tld-registry.md §3). // Constant; keep in step with Argus/src/config.js:TLD_BEACON and the indexer's // BNS_TLD_BEACON_SCRIPTHASH default. const TLD_BEACON_SCRIPTHASH = "c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8"; // Legacy TLDs to omit from the public `/api/tlds` listing by default. A legacy // entry here is one that a name was accidentally minted under (typo, test // artefact) that we do not want a name shop advertising as a browseable // namespace. They still resolve, and pass `?hidden=1` to see them. // `silentmode` — one-off (`releases.silentmode`) meant to be .st or .bch. const HIDDEN_LEGACY_TLDS = new Set(["silentmode"]); // Full-index cache (rebuilt every 30s) for fast availability lookups. let _idx = { at: 0, index: null }; async function getIndex() { if (!_idx.index || Date.now() - _idx.at > 30000) { _idx = { at: Date.now(), index: await buildIndex({ WebSocket }) }; } return _idx.index; } // Minimal newline-delimited JSON-RPC electrum client over a single WS. Same // framing as resolver-web.js's internal Electrum class and bns-indexer.test.mjs // — kept inline here so the gateway can query an arbitrary scripthash (the // resolver-web helpers only know the name beacon) without exporting internals. async function elCall(el, method, params = []) { return new Promise((resolve, reject) => { const id = ++el.id; el.pending.set(id, { resolve, reject }); el.ws.send(JSON.stringify({ id, method, params }) + "\n"); setTimeout(() => { if (el.pending.has(id)) { el.pending.delete(id); reject(new Error(`electrum timeout: ${method}`)); } }, 20000); }); } function elConnect(url) { return new Promise((resolve, reject) => { const ws = new WebSocket(url); const el = { ws, id: 0, pending: new Map(), buf: "" }; ws.on("open", () => resolve(el)); ws.on("error", (e) => reject(e)); ws.on("message", (data) => { el.buf += data.toString(); // Newline-delimited messages first — Fulcrum's normal framing. let nl; while ((nl = el.buf.indexOf("\n")) >= 0) { const line = el.buf.slice(0, nl).trim(); el.buf = el.buf.slice(nl + 1); if (!line) continue; try { const m = JSON.parse(line); const p = el.pending.get(m.id); if (p) { el.pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); } } catch { /* ignore stray frame */ } } // Some Fulcrum builds (and chipnet.bch.ninja specifically) omit the // trailing newline. If what's left in the buffer parses as a complete // JSON message, settle it. Same pattern as bns-indexer.test.mjs's // "some servers omit the trailing newline" branch. const rest = el.buf.trim(); if (rest) { try { const m = JSON.parse(rest); const p = el.pending.get(m.id); if (p) { el.pending.delete(m.id); el.buf = ""; m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); } } catch { /* still partial — wait for more bytes */ } } }); }); } // TLD snapshot cache (30s), same TTL as the name index. let _tld = { at: 0, snapshot: null }; async function getTldSnapshot() { if (_tld.snapshot && Date.now() - _tld.at < 30000) return _tld.snapshot; // Prefer the local BCHN-backed indexer (127.0.0.1:50010) — it indexes both // the name beacon and the TLD beacon on one WS since bns-indexer.js is // multi-beacon. Falls back to the public pool if the local endpoint is // unreachable (a dev running the gateway without a local indexer). const candidates = [ process.env.BNS_ELECTRUM_LOCAL || "ws://127.0.0.1:50010", ...CHIPNET_ELECTRUM.map((s) => s.url), ]; let el, lastErr; for (const url of candidates) { try { el = await elConnect(url); break; } catch (e) { lastErr = e; } } if (!el) throw lastErr ?? new Error("no electrum reachable for TLD snapshot"); try { await elCall(el, "server.version", ["bns-gateway", "1.4.3"]); const history = await elCall(el, "blockchain.scripthash.get_history", [TLD_BEACON_SCRIPTHASH]); const txs = new Map(); for (const { tx_hash } of history) { try { txs.set(tx_hash, await elCall(el, "blockchain.transaction.get", [tx_hash, true])); } catch { /* unreadable tx — skip */ } } const snapshot = buildTldSnapshot({ history, txs }); _tld = { at: Date.now(), snapshot }; return snapshot; } finally { try { el.ws.close(); } catch {} } } const MOUNT = process.env.BNS_MOUNT || "/bns"; const PORT = Number(process.env.BNS_PORT || 8700); const S3_ENDPOINT = process.env.BNS_S3 || "http://127.0.0.1:8500"; const SUB_BASE = (process.env.BNS_SUB_BASE || "bch.navigate.st").toLowerCase(); // subdomain suffix const aws = new AwsClient({ accessKeyId: process.env.AK, secretAccessKey: process.env.SK, service: "s3", region: "us-east-1", }); const cache = new Map(); async function resolve(host) { const c = cache.get(host); if (c && Date.now() - c.at < 30000) return c.entry; const entry = await resolveName(host, { WebSocket }); // resolveName normalizes any .