// The record-picker rule. Extracted from the 2026-08-13 subdomain-Sia bug: // `checkers.game.x` inherits the parent `game.x`'s records (which include BOTH // `s3` and `ip`); the gateway and Theseus were preferring `s3`, constructing a // Sia key from the subdomain name, and serving a NoSuchKey XML error to every // visitor. The parent's `ip` — configured on the VPS to Host-route the // subdomain — was ignored. // // These tests assert the rule that fixes it, not the code that implements it. // They will catch any regression that reintroduces s3-first-for-subdomains, and // they will fail on any future change to `pickRecord` that violates a stated // invariant even if the change looks locally reasonable. import test from "node:test"; import assert from "node:assert/strict"; import { pickRecord, isSubdomainOf } from "../src/lib/record-picker.js"; // A convenience — the exact bug case, in one place, so a search for it lands here. const gameXRecords = { ip: "130.12.242.84", tls: "6af5f7cb3715e3b81bbabb51d0947cc8611a3e66ad7bc7940a447c2f66deec20", s3: "bns/game/", }; // ---------- isSubdomainOf ---------- test("isSubdomainOf: apex is not a subdomain of itself", () => { assert.equal(isSubdomainOf("game.x", "game.x"), false); assert.equal(isSubdomainOf("silentmode.bch", "silentmode.bch"), false); }); test("isSubdomainOf: any label above the registered name counts as a subdomain", () => { assert.equal(isSubdomainOf("checkers.game.x", "game.x"), true); assert.equal(isSubdomainOf("blog.alice.p2p", "alice.p2p"), true); assert.equal(isSubdomainOf("a.b.c.alice.p2p", "alice.p2p"), true); }); test("isSubdomainOf: case-insensitive, so a badly-cased Host header does not fool it", () => { assert.equal(isSubdomainOf("Game.X", "game.x"), false); assert.equal(isSubdomainOf("Checkers.Game.X", "game.x"), true); }); test("isSubdomainOf: a missing entry name means no inheritance decision to make", () => { assert.equal(isSubdomainOf("anything.x", ""), false); assert.equal(isSubdomainOf("anything.x", undefined), false); }); // ---------- pickRecord: the regression from 2026-08-13 ---------- test("SUBDOMAIN with parent's {s3, ip}: picks ip — this is the 2026-08-13 fix", () => { // The exact wrong behaviour was picking s3 here. assert.equal(pickRecord(gameXRecords, { isSubdomain: true }), "ip"); }); test("APEX with the same {s3, ip}: picks s3 — the previous behaviour is unchanged", () => { // If this ever flips, the apex Sia landing page (game.x itself) breaks. assert.equal(pickRecord(gameXRecords, { isSubdomain: false }), "s3"); }); // ---------- pickRecord: full priority ordering ---------- test("apex priority is h → s3 → ip → u — matches the pre-2026-08-13 behaviour", () => { const isSubdomain = false; assert.equal(pickRecord({ h: "hi", s3: "bns/x/", ip: "1.2.3.4", u: "https://x" }, { isSubdomain }), "h"); assert.equal(pickRecord({ s3: "bns/x/", ip: "1.2.3.4", u: "https://x" }, { isSubdomain }), "s3"); assert.equal(pickRecord({ ip: "1.2.3.4", u: "https://x" }, { isSubdomain }), "ip"); assert.equal(pickRecord({ u: "https://x" }, { isSubdomain }), "u"); }); test("subdomain: ip wins over s3 AND over h — inheritance via Host, not per-name content", () => { // Parent's `h` would render the same tiny HTML for every subdomain. That's // the same class of wrong-behaviour as inherited `s3`. So subdomain-with-ip // wins over `h` too. const isSubdomain = true; assert.equal(pickRecord({ h: "hi", ip: "1.2.3.4" }, { isSubdomain }), "ip"); assert.equal(pickRecord({ s3: "bns/x/", ip: "1.2.3.4" }, { isSubdomain }), "ip"); assert.equal(pickRecord({ h: "hi", s3: "bns/x/", ip: "1.2.3.4" }, { isSubdomain }), "ip"); }); // ---------- pickRecord: does NOT break Sia-per-subdomain owners ---------- test("subdomain, parent has ONLY s3 (no ip): still picks s3 — Sia-per-subdomain works", () => { // An owner who wants every *.foo.p2p served from Sia should either register // each subdomain with its own s3, or simply not publish `ip` on the parent. // This asserts the second case still routes to Sia — the fix is targeted at // the s3+ip case, not a broad "never inherit s3" rule. const isSubdomain = true; assert.equal(pickRecord({ s3: "bns/foo/" }, { isSubdomain }), "s3"); assert.equal(pickRecord({ h: "hi" }, { isSubdomain }), "h"); assert.equal(pickRecord({ u: "https://x" }, { isSubdomain }), "u"); }); // ---------- pickRecord: edge cases ---------- test("empty records → null (caller renders the entry as JSON, or 404s)", () => { assert.equal(pickRecord({}, { isSubdomain: false }), null); assert.equal(pickRecord({}, { isSubdomain: true }), null); }); test("null/undefined records → null (do not throw)", () => { assert.equal(pickRecord(null, { isSubdomain: false }), null); assert.equal(pickRecord(undefined, { isSubdomain: true }), null); }); test("missing options default to apex behaviour — safe default is not the fix path", () => { // If a caller forgets to pass isSubdomain, we must not silently apply the // subdomain rule; that could hide bugs where isSubdomain was miscomputed. assert.equal(pickRecord(gameXRecords), "s3"); }); test("unrelated fields (tls, r, arbitrary) do not affect the pick", () => { const records = { tls: "abc123", r: "phishing", extra: "ignored", ip: "1.2.3.4" }; assert.equal(pickRecord(records, { isSubdomain: false }), "ip"); assert.equal(pickRecord(records, { isSubdomain: true }), "ip"); }); // ---------- pickRecord: the `p` record (reverse proxy), added 2026-08-31 ---------- test("apex with ONLY p → picks p (not fall through to u or null)", () => { // The reason `p` was added: a name that just wants to mirror an upstream // via a full URL. Before, this fell through the ordering unnoticed and // the gateway JSON-dumped the entry. assert.equal(pickRecord({ p: "https://syskypo.de" }, { isSubdomain: false }), "p"); }); test("apex with s3 + p → s3 wins (owned bucket beats proxy)", () => { // If the owner uploaded content, they meant that content. `p` is the // "I don't have static content, mirror this instead" case. assert.equal(pickRecord({ s3: "bns/x/", p: "https://x" }, { isSubdomain: false }), "s3"); }); test("apex with ip + p → ip wins (backward compat for on-chain-pinned proxies)", () => { // coinspectrum.bch has both {ip, p, tls}. Its historical behaviour is // "pin the upstream via chain-committed TLS fingerprint on the raw IP" — // stricter than public-CA proxying via `p`. Do NOT flip that under it. assert.equal(pickRecord({ ip: "1.2.3.4", p: "https://x", tls: "abc" }, { isSubdomain: false }), "ip"); }); test("apex with p + u → p wins (mirror beats redirect)", () => { // p keeps the BNS address bar; u throws the visitor to the upstream URL. // If the owner has both, they meant "mirror" — u is a fallback for clients // that can't proxy. assert.equal(pickRecord({ p: "https://x", u: "https://x" }, { isSubdomain: false }), "p"); }); test("subdomain inheritance does NOT use parent's p — p is single-URL by intent", () => { // A parent with only `p` proxies its own content to one URL. Silently // inheriting that for a subdomain query would route the subdomain's traffic // through an upstream that doesn't know about it — usually the wrong thing. // If the owner wants subdomain mirroring, they set `ip` (Host-routed). assert.equal(pickRecord({ p: "https://x" }, { isSubdomain: true }), null); assert.equal(pickRecord({ p: "https://x", u: "https://u" }, { isSubdomain: true }), "u"); }); // ---------- The invariant that must hold across any future change ---------- test("INVARIANT: for any records with both s3 and ip, the pick differs between apex and subdomain", () => { // This is the core property of the fix. If some future edit makes them the // same, the 2026-08-13 bug has been reintroduced regardless of what the // ordering table now says. const cases = [ { s3: "bns/a/", ip: "1.1.1.1" }, { s3: "bns/a/", ip: "1.1.1.1", u: "https://x" }, { s3: "bns/a/", ip: "1.1.1.1", tls: "abc" }, ]; for (const r of cases) { const apex = pickRecord(r, { isSubdomain: false }); const sub = pickRecord(r, { isSubdomain: true }); assert.notEqual(apex, sub, `apex ${apex} === subdomain ${sub} for ${JSON.stringify(r)}`); assert.equal(sub, "ip", "the subdomain path must pick ip when it is available"); } });