feat(theseus): a subdomain rule applies wherever the name is served from
The gateway checks a name's host rules before it decides what to serve, so a blocked or redirected subdomain behaves the same whatever record the name carries. Theseus only inherited that for names it proxies through the gateway's /bns/ mount. A name with both s3 and ip — the shape that caused the 2026-08-13 subdomain bug — would have had its blocked subdomain answer anyway, because Theseus talks straight to the IP. It now asks the gateway for the host's verified rule before taking either of the paths it serves itself, and only for those paths, so an ordinary subdomain navigation gains no round trip. Verification stays in one place: the client reads a decision, it does not re-derive one. The spec catches up with what is implemented — it still described v1 and called hosts a future idea.
This commit is contained in:
parent
27819684d5
commit
8d96e979fa
1 changed files with 29 additions and 0 deletions
29
main.js
29
main.js
|
|
@ -240,6 +240,27 @@ function dnsRecordsCached(name) {
|
|||
const c = dnsRecordsCache.get(name);
|
||||
return c && Date.now() - c.at < DNS_RECORDS_TTL ? c.value : undefined;
|
||||
}
|
||||
// The routing half of a name's signed manifest for ONE host, verified by the
|
||||
// gateway (which already holds that code) and cached per host. Only consulted
|
||||
// for a subdomain that Theseus serves itself — an `ip` or inline `h` record —
|
||||
// because anything going through the gateway's /bns/ mount already had the
|
||||
// rule applied there. KEEP IN STEP with public-gateway.mjs serve().
|
||||
const hostActionCache = new Map();
|
||||
async function fetchHostAction(host) {
|
||||
const c = hostActionCache.get(host);
|
||||
if (c && Date.now() - c.at < DNS_RECORDS_TTL) return c.value;
|
||||
let value = null;
|
||||
try {
|
||||
const r = await fetch(`${GATEWAY}/api/dns/${encodeURIComponent(host)}`, { signal: AbortSignal.timeout(3000), cache: "no-store" });
|
||||
if (r.ok) {
|
||||
const j = await r.json();
|
||||
const a = j?.host_action;
|
||||
if (a && typeof a === "object" && typeof a.kind === "string") value = a;
|
||||
}
|
||||
} catch { /* offline or no manifest — the name keeps its chain behaviour */ }
|
||||
hostActionCache.set(host, { value, at: Date.now() });
|
||||
return value;
|
||||
}
|
||||
function fetchDnsRecords(name) {
|
||||
const c = dnsRecordsCache.get(name);
|
||||
if (c?.pending) return c.pending;
|
||||
|
|
@ -1696,6 +1717,14 @@ async function serveBns(request) {
|
|||
return new Response(body, { status: up.status, headers: { "content-type": ct } });
|
||||
};
|
||||
try {
|
||||
// A subdomain the owner has ruled on: blocked, or sent elsewhere. The
|
||||
// gateway applies this for anything it serves, so this only covers the
|
||||
// paths Theseus takes on its own — `ip` and inline `h`.
|
||||
if (isSubdomain && (r.ip || r.h)) {
|
||||
const act = await fetchHostAction(host);
|
||||
if (act?.kind === "block") return new Response("not found", { status: 404 });
|
||||
if (act?.kind === "redirect" && act.url) return Response.redirect(act.url, 302);
|
||||
}
|
||||
if (isSubdomain && r.ip) return await serveIp();
|
||||
if (r.h) { if (reqPath === "/") return new Response(r.h, { headers: { "content-type": "text/html; charset=utf-8" } }); return new Response("not found", { status: 404 }); }
|
||||
if (r.s3) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue