argus/src/lib/tlds.js
Local Dev a45320db7d Initial commit — Silent Mode baseline (2026-07-29)
Snapshot of the decentralized-web stack at the point of the resolver+Theseus
rebuild deploy. Includes:

- Argus (BNS engine + resolver daemon + Sia gateway)
- AriadneResolver (Windows Inno installer bundle + Android APK sources +
  Firefox extension)
- TheseusNavigator (Electron browser)
- site/ (silentmode.st content, deployed to Sia at bns/silentmode/)
- design docs, roadmap, protocol spec

Secrets excluded via .gitignore: Argus/sia-s3.json, Argus/wallets.json,
Argus/ca/*.key,*.crt. Build outputs, node_modules, and bundled runtimes
also excluded.

Shipped hashes on dl.silentmode.st at this commit:
  AriadneResolver-Setup-0.1.0.exe    5bcb216eef31ea28ed767e4134ab74bd5ac69dfbd365fd249e9e6938e55c986a
  TheseusNavigator-Setup-0.0.1.exe   7c735e88bad2da3347145adba3016c8f626a18b8422289c8c6ba471972e2952b
  TheseusNavigator-0.0.1-portable.exe 008fd84445babeabb401b2bca40ea9466b24b0e6d6c85104da7640c5c5c84521
  ariadne-v0.2.apk                   635c8f04d44ef855a8390b9eeddb8cd2d50622e81cc4e5004e8daffc1bb0425c
2026-07-29 13:54:34 +02:00

84 lines
3.9 KiB
JavaScript

// The TLDs this installation will mint TLS certificates for.
//
// The BNS protocol is deliberately TLD-agnostic: bns.js accepts any
// /^[a-z0-9-]{1,16}$/ as a TLD, because the chain decides what exists, not this
// file. TLS is the one place that cannot stay open-ended. The local root CA is
// installed into the machine trust store, and an X.509 name-constrained CA needs
// a *finite* list of permitted dNSName subtrees (RFC 5280 §4.2.1.10). Without
// one the root can vouch for every domain on the internet.
//
// So: resolution stays unbounded, certificate issuance is enumerated. A name on
// a TLD that is not listed here still resolves and still serves over plain HTTP
// — it just gets no certificate from the local CA.
//
// Adding a TLD requires regenerating the root CA and re-importing it on every
// machine (see setup/install.ps1). That cost is the point: it is what stops this
// root from being a general-purpose signing oracle.
import { readFileSync, existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
// RFC 2606 reserves `.test` forever, and none of the others are ICANN-delegated.
export const DEFAULT_TLDS = ["bch", "p2p", "bit", "deviant", "nav", "test", "bcnr"];
// A tripwire, not a complete list — ICANN has ~1500 delegated TLDs and adds more.
// Its job is to make the obvious mistake loud, not to be authoritative. Anything
// added to the permitted list must be checked against the real IANA root zone:
// https://data.iana.org/TLD/tlds-alpha-by-domain.txt
const ICANN_TRIPWIRE = new Set([
"com", "net", "org", "edu", "gov", "mil", "int", "info", "biz", "io", "co",
"app", "dev", "xyz", "online", "site", "cloud", "ai", "me", "tv", "sh", "gg",
"uk", "us", "de", "fr", "nl", "eu", "ru", "cn", "jp", "ca", "au", "ch", "it",
]);
const TLD_RE = /^[a-z0-9-]{1,16}$/;
const CONFIG_FILE = fileURLToPath(new URL("../../ariadne.config.json", import.meta.url));
function validate(tlds) {
const out = [];
for (const raw of tlds) {
const tld = String(raw).trim().toLowerCase().replace(/^\.+/, "");
if (!TLD_RE.test(tld)) throw new Error(`invalid TLD "${raw}" in permitted list (allowed: [a-z0-9-]{1,16})`);
if (ICANN_TRIPWIRE.has(tld)) {
throw new Error(
`refusing to permit ".${tld}" — it is an ICANN-delegated TLD. Adding it would let the ` +
`local root CA issue trusted certificates for real internet domains on every machine ` +
`that installed it. If this is genuinely intended, edit ICANN_TRIPWIRE in src/lib/tlds.js.`,
);
}
if (!out.includes(tld)) out.push(tld);
}
if (!out.length) throw new Error("permitted TLD list is empty — the CA could issue nothing");
return out;
}
// Effective list = ariadne.config.json "tlds", else the built-in default.
// Read once at import: the root CA is baked against this list, so changing it at
// runtime would silently desync the constraints from the certificates.
export const PERMITTED_TLDS = (() => {
try {
if (existsSync(CONFIG_FILE)) {
const cfg = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
if (Array.isArray(cfg.tlds) && cfg.tlds.length) return validate(cfg.tlds);
}
} catch (e) {
if (e.message.startsWith("refusing") || e.message.startsWith("invalid TLD")) throw e;
console.error(`tlds: ${CONFIG_FILE} unreadable (${e.message}) — using defaults`);
}
return validate(DEFAULT_TLDS);
})();
// Rightmost label of a hostname, minus any port or trailing dot. null if unusable.
export function hostTld(host) {
if (typeof host !== "string" || !host) return null;
const bare = host.trim().toLowerCase().replace(/\.$/, "").replace(/:\d+$/, "");
if (!bare || bare.includes("/")) return null;
const tld = bare.slice(bare.lastIndexOf(".") + 1);
return TLD_RE.test(tld) ? tld : null;
}
// May the local CA issue a certificate for this hostname?
export function isPermittedHost(host, tlds = PERMITTED_TLDS) {
const tld = hostTld(host);
return tld !== null && tlds.includes(tld);
}