// 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, connectAny, CHIPNET_ELECTRUM } from "./resolver-web.js"; import { pickRecord, isSubdomainOf } from "./record-picker.js"; // 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; } const MOUNT = process.env.BNS_MOUNT || "/bns"; const PORT = Number(process.env.BNS_PORT || 8700); // Names hidden from /api/search/ results — direct navigation still works. const HIDDEN_FROM_SEARCH = new Set(["syskypo.de"]); // TLDs the operator has flagged "hidden" — kept off public /api/tlds // listings so name-search UIs stop offering them. On-chain registrations // under a hidden TLD still exist; this is a UX filter, not enforcement. // Persisted to a JSON file next to this script so it survives restarts. import { readFileSync as _readFile, writeFileSync as _writeFile, existsSync as _existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname as _dirname, join as _join } from "node:path"; const HIDDEN_TLDS_FILE = process.env.BNS_HIDDEN_TLDS_FILE || _join(_dirname(fileURLToPath(import.meta.url)), "hidden-tlds.json"); // Operator gate — POSTs to /api/tld-visibility require a matching bearer. // Set BNS_OPERATOR_TOKEN in the service env; if unset, the endpoint refuses // every write (safer than allowing anonymous writes to a public gateway). const OPERATOR_TOKEN = process.env.BNS_OPERATOR_TOKEN || null; function readHiddenTlds() { try { if (!_existsSync(HIDDEN_TLDS_FILE)) return new Set(); const raw = JSON.parse(_readFile(HIDDEN_TLDS_FILE, "utf8")); return new Set(Array.isArray(raw?.hidden) ? raw.hidden : []); } catch { return new Set(); } } function writeHiddenTlds(set) { _writeFile(HIDDEN_TLDS_FILE, JSON.stringify({ hidden: [...set].sort() }, null, 2) + "\n", "utf8"); } let HIDDEN_TLDS = readHiddenTlds(); 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 .