// 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");
}
});