argus/test/manifest-hosts.test.mjs
Silent Mode 9c8b2bfafe feat(gateway): a name's owner decides what its subdomains serve
Until now a subdomain served the folder its labels spelled out, and that was
the only thing it could ever do. There was no way to point one somewhere
else, send one to another site, or stop one answering — the mapping was a
convention with no override.

A verified hosts entry now decides: a different folder, a redirect, or
nothing at all. Block beats redirect beats path, so a host that is both
blocked and redirected stays blocked. The manifest is fetched only when the
request is actually for a subdomain, and the result — including the absence
of one — is cached for a minute, so the apex pays nothing and a name that
never published one pays one lookup per minute rather than one per request.
A manifest that fails to verify is treated exactly like no manifest: the
name keeps serving from its chain records instead of failing shut.

The folder derivation moves next to the picking rule and grows tests,
because getting it wrong does not raise an error — it quietly serves the
wrong bytes. The subtle parts are that labels reverse (DNS reads outward, a
path reads inward) and that a .bch name indexes as <label>.bch even though
normalizeName collapses it to a bare label; matching the bare label would
have served the bucket root for every subdomain of every .bch name.

/api/dns answers for a host too. A host with its own dns block owns its
record set outright, the way classic DNS works; one without inherits the
apex, the way everything else on BNS does.
2026-09-28 01:04:36 +02:00

240 lines
11 KiB
JavaScript

// Per-host manifest entries: the key rule, the validation, and the signature
// check that makes any of it trustworthy.
//
// Two things these tests exist to stop:
// 1. An apex being governed by `hosts`. The main site's content comes from
// the chain; if a signed blob could redirect it, the trust boundary for
// the name would move without the owner ever spending a transaction.
// 2. A manifest being trusted because pubkey recovery "worked". Recovery
// always yields SOME key — only comparing the derived address to the
// on-chain owner proves anything.
import test from "node:test";
import assert from "node:assert/strict";
import { subLabelsOf, hostOverride, hostAction } from "../src/lib/record-picker.js";
import { validateHosts, verifyManifest, canonicalBytes, canonicalize } from "../src/lib/manifest.js";
import { sha256, utf8ToBin, secp256k1, ripemd160, encodeCashAddress, binToBase64 } from "@bitauth/libauth";
// ---------- subLabelsOf ----------
test("subLabelsOf: apex has no labels above the name", () => {
assert.equal(subLabelsOf("aegis.x", "aegis.x"), "");
assert.equal(subLabelsOf("Aegis.X", "aegis.x"), "");
});
test("subLabelsOf: one and many labels, in DNS order", () => {
assert.equal(subLabelsOf("www.aegis.x", "aegis.x"), "www");
assert.equal(subLabelsOf("chipnet.explorer.aegis.x", "aegis.x"), "chipnet.explorer");
});
test("subLabelsOf: a name that merely ends in the same text is not a subdomain", () => {
// "notaegis.x" ends with "aegis.x" as a STRING but not as a label boundary.
assert.equal(subLabelsOf("notaegis.x", "aegis.x"), "");
});
// ---------- hostOverride ----------
const manifest = {
v: 2, name: "aegis.x", seq: 1,
hosts: {
www: { path: "" },
shop: { path: "store/v2/" },
blog: { u: "https://blog.example.com" },
old: { block: true },
"*": { u: "https://aegis.x/" },
},
};
test("hostOverride: the apex is never governed by hosts", () => {
assert.equal(hostOverride(manifest, "aegis.x", "aegis.x"), null);
});
test("hostOverride: exact key wins", () => {
assert.deepEqual(hostOverride(manifest, "shop.aegis.x", "aegis.x"), { path: "store/v2/" });
assert.deepEqual(hostOverride(manifest, "old.aegis.x", "aegis.x"), { block: true });
});
test("hostOverride: the wildcard catches anything not named", () => {
assert.deepEqual(hostOverride(manifest, "anything.aegis.x", "aegis.x"), { u: "https://aegis.x/" });
});
test("hostOverride: a named host opts out of the wildcard", () => {
assert.deepEqual(hostOverride(manifest, "www.aegis.x", "aegis.x"), { path: "" });
});
test("hostOverride: no hosts block means no override, never a throw", () => {
assert.equal(hostOverride({ v: 1, name: "aegis.x" }, "www.aegis.x", "aegis.x"), null);
assert.equal(hostOverride(null, "www.aegis.x", "aegis.x"), null);
});
test("hostOverride: inherited Object properties are not host entries", () => {
// A manifest is attacker-supplied JSON; "constructor" must not resolve.
assert.equal(hostOverride({ hosts: {} }, "constructor.aegis.x", "aegis.x"), null);
assert.equal(hostOverride({ hosts: {} }, "toString.aegis.x", "aegis.x"), null);
});
// ---------- hostAction ----------
test("hostAction: block beats redirect beats path", () => {
assert.deepEqual(hostAction({ block: true, u: "https://x.test/", path: "a/" }), { kind: "block" });
assert.deepEqual(hostAction({ u: "https://x.test/", path: "a/" }), { kind: "redirect", url: "https://x.test/" });
assert.deepEqual(hostAction({ path: "a/" }), { kind: "path", path: "a/" });
});
test("hostAction: an empty path is a real instruction — serve the bucket root", () => {
// Distinct from "no path at all": `{path:""}` points a subdomain at the apex
// folder, which is how `www` is made to mirror the main site.
assert.deepEqual(hostAction({ path: "" }), { kind: "path", path: "" });
assert.equal(hostAction({ dns: { A: ["1.2.3.4"] } }), null);
});
// ---------- validateHosts ----------
test("validateHosts: accepts the shapes the dashboard writes", () => {
assert.equal(validateHosts(undefined), null);
assert.equal(validateHosts({ www: { path: "" }, "a.b": { path: "deep/" }, "*": { block: true } }), null);
assert.equal(validateHosts({ mail: { dns: { A: ["1.2.3.4"] } } }), null);
});
test("validateHosts: rejects path traversal and absolute paths", () => {
assert.match(validateHosts({ x: { path: "../secrets/" } }), /\.\./);
assert.match(validateHosts({ x: { path: "/etc/passwd" } }), /leading/);
});
test("validateHosts: rejects a host key that is not a label path", () => {
assert.match(validateHosts({ "has space": { path: "" } }), /invalid host key/);
assert.match(validateHosts({ "UPPER": { path: "" } }), /invalid host key/);
assert.match(validateHosts({ "-leading": { path: "" } }), /invalid host key/);
});
test("validateHosts: rejects a non-http redirect target", () => {
assert.match(validateHosts({ x: { u: "javascript:alert(1)" } }), /http\(s\) URL/);
assert.match(validateHosts({ x: { u: "file:///etc/passwd" } }), /http\(s\) URL/);
});
test("validateHosts: rejects an entry that declares nothing", () => {
assert.match(validateHosts({ x: {} }), /declares nothing/);
});
// ---------- verifyManifest ----------
// A throwaway key, and a manifest genuinely signed with it.
const privKey = sha256.hash(utf8ToBin("manifest-hosts-test-key"));
const pubKey = secp256k1.derivePublicKeyCompressed(privKey);
const ownerEnc = encodeCashAddress({ prefix: "bchtest", type: "p2pkh", payload: ripemd160.hash(sha256.hash(pubKey)) });
const OWNER = typeof ownerEnc === "string" ? ownerEnc : ownerEnc.address;
function sign(m) {
const digest = sha256.hash(utf8ToBin(canonicalBytes(m)));
const r = secp256k1.signMessageHashRecoverableCompact(privKey, digest);
const sig = new Uint8Array(65);
sig[0] = 27 + r.recoveryId + 4; // compressed-pubkey flag
sig.set(r.signature, 1);
return { ...m, sig: binToBase64(sig) };
}
test("verifyManifest: accepts a v2 manifest signed by the owner", () => {
const m = sign({ v: 2, name: "aegis.x", seq: 7, hosts: { www: { path: "" } } });
assert.deepEqual(verifyManifest(m, { name: "aegis.x", owner: OWNER }), { ok: true, seq: 7 });
});
test("verifyManifest: v1 stays valid — no migration is forced on anyone", () => {
const m = sign({ v: 1, name: "aegis.x", seq: 1, dns: { A: ["1.2.3.4"] } });
assert.equal(verifyManifest(m, { name: "aegis.x", owner: OWNER }).ok, true);
});
test("verifyManifest: a signature from the wrong key is refused, not merely recovered", () => {
const m = sign({ v: 2, name: "aegis.x", seq: 7, hosts: { www: { path: "" } } });
const other = "bchtest:qzew3dqcsj0guwe5y3uxf9q2xs5ajgy9lca4z093cu";
const out = verifyManifest(m, { name: "aegis.x", owner: other });
assert.equal(out.ok, false);
assert.match(out.error, /does not match the current NFT owner/);
});
test("verifyManifest: any tampering after signing invalidates it", () => {
const m = sign({ v: 2, name: "aegis.x", seq: 7, hosts: { www: { path: "" } } });
const tampered = { ...m, hosts: { www: { u: "https://evil.test/" } } };
assert.equal(verifyManifest(tampered, { name: "aegis.x", owner: OWNER }).ok, false);
});
test("verifyManifest: a manifest cannot be replayed for another name", () => {
const m = sign({ v: 2, name: "aegis.x", seq: 7 });
const out = verifyManifest(m, { name: "other.x", owner: OWNER });
assert.equal(out.ok, false);
assert.match(out.error, /name field/);
});
test("verifyManifest: an older seq is refused, an equal one is not", () => {
const m = sign({ v: 2, name: "aegis.x", seq: 5 });
assert.match(verifyManifest(m, { name: "aegis.x", owner: OWNER, seenSeq: 9 }).error, /stale manifest/);
assert.equal(verifyManifest(m, { name: "aegis.x", owner: OWNER, seenSeq: 5 }).ok, true);
});
test("verifyManifest: a malformed hosts block is refused before the signature is trusted", () => {
const m = sign({ v: 2, name: "aegis.x", seq: 7, hosts: { x: { path: "../../etc/" } } });
assert.equal(verifyManifest(m, { name: "aegis.x", owner: OWNER }).ok, false);
});
// ---------- canonicalisation ----------
test("canonicalize: key order and dropped nulls are stable, so signing is reproducible", () => {
const a = canonicalize({ b: 1, a: { d: null, c: [1, null, 2] } });
const b = canonicalize({ a: { c: [1, 2], d: undefined }, b: 1 });
assert.equal(JSON.stringify(a), JSON.stringify(b));
assert.equal(JSON.stringify(a), '{"a":{"c":[1,2]},"b":1}');
});
// ---------- subFolderFor ----------
// The derivation that decides which bytes a visitor gets. A mistake here does
// not error — it silently serves the wrong folder, or the whole site under a
// subdomain that was meant to show one page.
import { subFolderFor } from "../src/lib/record-picker.js";
test("subFolderFor: the apex is the bucket root", () => {
assert.equal(subFolderFor("aegis.x", "aegis.x", null), "");
});
test("subFolderFor: one label is one folder", () => {
assert.equal(subFolderFor("brand.aegis.x", "aegis.x", null), "brand");
});
test("subFolderFor: labels reverse — DNS reads outward, a path reads inward", () => {
// chipnet.explorer.aegis.x and aegis.x/explorer/chipnet are the same page.
assert.equal(subFolderFor("chipnet.explorer.aegis.x", "aegis.x", null), "explorer/chipnet");
assert.equal(subFolderFor("d.c.b.a.aegis.x", "aegis.x", null), "a/b/c/d");
});
test("subFolderFor: an override replaces the derived folder", () => {
const m = { hosts: { shop: { path: "store/v2/" } } };
assert.equal(subFolderFor("shop.aegis.x", "aegis.x", m), "store/v2");
});
test("subFolderFor: an empty override points a subdomain at the apex folder", () => {
const m = { hosts: { www: { path: "" } } };
assert.equal(subFolderFor("www.aegis.x", "aegis.x", m), "");
});
test("subFolderFor: slashes are normalised so the caller can always append one", () => {
assert.equal(subFolderFor("a.aegis.x", "aegis.x", { hosts: { a: { path: "/x/y/" } } }), "x/y");
});
test("subFolderFor: a redirect or block never yields a folder", () => {
assert.equal(subFolderFor("a.aegis.x", "aegis.x", { hosts: { a: { u: "https://x.test/" } } }), "a");
assert.equal(subFolderFor("a.aegis.x", "aegis.x", { hosts: { a: { block: true } } }), "a");
});
test("subFolderFor: an unverified/absent manifest falls back to the convention", () => {
assert.equal(subFolderFor("brand.aegis.x", "aegis.x", undefined), "brand");
assert.equal(subFolderFor("brand.aegis.x", "aegis.x", { hosts: {} }), "brand");
});
test("subFolderFor: .bch names index as <label>.bch, so the suffix match still works", () => {
// normalizeName() collapses "silentmode.bch" to the bare label "silentmode",
// but the INDEX key — and so entry.name — is always "<label>.<tld>". Matching
// against the bare label would silently serve the bucket root for every
// subdomain of every .bch name. Verified live: theseus.silentmode.bch and
// silentmode.bch/theseus/ return the same bytes.
assert.equal(subFolderFor("theseus.silentmode.bch", "silentmode.bch", null), "theseus");
assert.equal(subFolderFor("silentmode.bch", "silentmode.bch", null), "");
});