From 7849059c01e1166d752d1e9abb022600eef83830 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 9 Nov 2026 11:56:39 +0000 Subject: [PATCH] feat(pit): set and show a name's c0mpute site on the DNS Records page Each name on /pit/records now shows the c0mpute site it points at (linked to gateway.c0mpute.com/s//, served by the configured edge), with a field to set it and a Clear button. The form writes through setNameC0mpute, the same function behind PUT /api/moshpit/tlds/:tld/names/c0mpute and `moshcode name c0mpute`. The card explains that Moshpit resolvers answer an address question from published A/AAAA/CNAME records before following the edge, and flags a name that publishes both. GET /api/moshpit/records now carries `c0mpute` and `edge`, so an empty zone on a c0mpute-served name no longer reads as parked. Co-Authored-By: Claude Opus 5.5 --- apps/pwa/src/moshpit.mjs | 6 +- apps/pwa/src/routes/moshpit.mjs | 114 +++++++++++++++++++- apps/pwa/test/moshpit-records-page.test.mjs | 105 ++++++++++++++++++ 3 files changed, 220 insertions(+), 5 deletions(-) diff --git a/apps/pwa/src/moshpit.mjs b/apps/pwa/src/moshpit.mjs index accf6672..9d90c0d8 100644 --- a/apps/pwa/src/moshpit.mjs +++ b/apps/pwa/src/moshpit.mjs @@ -1042,6 +1042,10 @@ export async function recordsForName(input) { label: parsed.label, name_registered: resolution.name_registered, target: resolution.target, + // The c0mpute site the name points at, or null. Records alone cannot say + // this: a c0mpute name usually publishes nothing, and without it the zone + // reads as empty when the name is in fact served by the c0mpute edge. + c0mpute: resolution.c0mpute ?? null, records: await listRecords(parsed.tld, parsed.label), }; } @@ -1156,7 +1160,7 @@ export async function listRecordNames(userId, { like = null, exact = "", limit = : `n.user_id = ?`; const params = like ? [userId, like] : [userId]; return all( - `SELECT n.tld, n.label, n.target, + `SELECT n.tld, n.label, n.target, n.c0mpute_site, (SELECT COUNT(*) FROM moshpit_records r WHERE r.tld = n.tld AND r.label = n.label) AS record_count FROM moshpit_names n WHERE ${where} diff --git a/apps/pwa/src/routes/moshpit.mjs b/apps/pwa/src/routes/moshpit.mjs index 344b700d..ad08f8ae 100644 --- a/apps/pwa/src/routes/moshpit.mjs +++ b/apps/pwa/src/routes/moshpit.mjs @@ -187,7 +187,7 @@ const page = (opts) => shellPage({ ...opts, tracker: config.crawlproofSite }); import { shortLinkUrl } from "../lib/moshpit-links.mjs"; import { caEnabled, caMaterial } from "../lib/moshpit-ca.mjs"; import { getNameCertificate, issueNameCertificate, listNameCertificates } from "../lib/moshpit-certs.mjs"; -import { c0mputeEdgeHost } from "../lib/moshpit-c0mpute.mjs"; +import { c0mputeEdgeHost, effectiveC0mputeTarget } from "../lib/moshpit-c0mpute.mjs"; export const moshpitRouter = Router(); @@ -880,6 +880,13 @@ moshpitRouter.get("/api/moshpit/records", async (req, res) => { resolved: found.resolved, name_registered: found.name_registered, target: found.target, + // A name pointed at a c0mpute site usually publishes no records at all, and + // an empty list read alone says "parked" when the name is in fact served. + // `c0mpute` is the site id (null for every other name); `edge` is the host + // that serves it, null while no edge is configured and the site is only + // stored. + c0mpute: found.c0mpute, + edge: found.c0mpute ? c0mputeEdgeHost() : null, records: found.records.map((r) => ({ type: r.type, value: r.value, ttl: r.ttl, ...(r.priority === null ? {} : { priority: r.priority }), })), @@ -2925,7 +2932,10 @@ const PIT_CSS = ` .pit-btn-x{background:transparent;border:1px solid var(--line-2);border-radius:6px;color:var(--dim); font-family:var(--mono);font-size:.7rem;padding:5px 9px;cursor:pointer} .pit-btn-x:hover{border-color:var(--danger);color:var(--danger)} -.pit-empty{font-family:var(--mono);font-size:.74rem;color:var(--faint);margin:10px 0 0}`; +.pit-empty{font-family:var(--mono);font-size:.74rem;color:var(--faint);margin:10px 0 0} +.pit-c0{margin-top:12px;padding-top:10px;border-top:1px dashed var(--line)} +.pit-c0 .pit-rec-add{margin-top:8px;padding-top:0;border-top:0} +.pit-c0-on{font-family:var(--mono);font-size:.82rem;margin:0;word-break:break-all}`; /** * The tab strip: the endings you hold, the ones you can buy from, and how to @@ -3547,8 +3557,72 @@ const addRecordRow = (req, name, ctx, { type = "AAAA", value = "", ttl = "", pri * is nothing published yet — so hiding it behind "names with records" would * leave the page blank for exactly the person who came to fill it. */ +/** + * The c0mpute half of a name's card: which c0mpute site serves it, and the + * form that sets or clears it. + * + * One field, the site id. The copies of the site live on c0mpute hosting nodes + * (d1sks), but which nodes is c0mpute's business, not the name's: any c0mpute + * gateway serves any site, so the id is all the registry needs. + * + * Writes through setNameC0mpute, the function behind + * PUT /api/moshpit/tlds/:tld/names/c0mpute and `moshcode name c0mpute`, so the + * page cannot drift from the CLI. + */ +const C0MPUTE_GATEWAY = "https://gateway.c0mpute.com"; + +const c0mputeBlock = (req, label, site, records, ctx) => { + const edge = c0mputeEdgeHost(); + // What the bridge and the DoH server actually do (src/dns.mjs addressAnswer): + // an address question is answered from published A/AAAA records first, then a + // CNAME, and only then by following the target, which is where the edge is. + // So these records shadow the c0mpute site for every Moshpit resolver. + const shadowing = records.filter((r) => r.type === "A" || r.type === "AAAA" || r.type === "CNAME"); + const action = `${csrfInput(req)}${whereYouWere(ctx)}`; + const status = site + ? `

c0mpute site: ${esc(site)} ${ + edge ? `(served by ${esc(edge)})` + : `(stored; no c0mpute edge is configured, so nothing serves it yet)`}

+

While a c0mpute site is set, the name resolves to + ${edge ? `${esc(edge)}` : "the c0mpute edge"}, which holds a Moshpit + certificate for it and fetches the site from healthy c0mpute gateways. Moshpit resolvers still + answer an address question from A, AAAA or CNAME records published here before they follow the + edge, so remove those or visitors reach them instead of the site. MX and TXT records keep working. + Setting a target with moshcode name point clears the site.

${ + shadowing.length && edge ? ` +

This name also publishes ${ + [...new Set(shadowing.map((r) => r.type))].join(", ")}: Moshpit resolvers answer with + ${shadowing.length === 1 ? "that record" : "those records"}, not the c0mpute edge.

` : ""}` + : `

Serve this name from a static site on c0mpute: paste the 20-character site id + c0mpute hosting publish printed. The name then resolves to the c0mpute + edge instead of its stored target.

`; + return ` +
+ ${status} +
+ ${action} + + +
${site ? ` +
+ ${action} + + +
` : ""} +
`; +}; + const recordCard = (req, name, records, target, ctx) => { const label = `${name.label}.${name.tld}`; + const site = name.c0mpute_site || null; + // What resolvers are told, not the stored column: a c0mpute site wins over a + // stored target while an edge is configured, and the card should say where + // the name actually goes. + target = effectiveC0mputeTarget({ target, site }); const rows = records.map((r) => ` ${r.type} @@ -3568,7 +3642,7 @@ const recordCard = (req, name, records, target, ctx) => { `).join(""); return ` -
+

${esc(label)}

${ @@ -3586,8 +3660,11 @@ const recordCard = (req, name, records, target, ctx) => { as a zone file
${esc(records.map((r) => zoneLine(label, r)).join("\n"))}
` : ` -

No records yet — an AAAA pointing at the box that serves it is the usual first one.

`} +

No records yet — ${site + ? "none needed, the c0mpute site below serves this name." + : "an AAAA pointing at the box that serves it is the usual first one."}

`} ${addRecordRow(req, label, ctx)} + ${c0mputeBlock(req, label, site, records, ctx)}
`; }; @@ -3758,6 +3835,11 @@ curl -X POST ${esc(config.pitOrigin)}/api/moshpit/tlds/eggs/records \\ # read them back — no key needed, DNS is public curl ${esc(config.pitOrigin)}/api/moshpit/records?name=blue.eggs +# serve the name from a c0mpute site ("site":"" clears it) +curl -X PUT ${esc(config.pitOrigin)}/api/moshpit/tlds/eggs/names/c0mpute \\ + -H "authorization: Bearer $MOSH_KEY" -H 'content-type: application/json' \\ + -d '{"label":"blue","site":"53021d6e8d5a0e1aac48"}' + # withdraw one curl -X DELETE ${esc(config.pitOrigin)}/api/moshpit/tlds/eggs/records \\ -H "authorization: Bearer $MOSH_KEY" -H 'content-type: application/json' \\ @@ -3795,6 +3877,30 @@ moshpitRouter.post("/pit/records", requireAuth, async (req, res) => { }); }); +/** + * Set or clear the c0mpute site on a name, from the records tab. + * + * The same write as PUT /api/moshpit/tlds/:tld/names/c0mpute (setNameC0mpute), + * behind a form so an owner does not need the CLI for it. An empty `site` + * clears it. + */ +moshpitRouter.post("/pit/records/c0mpute", requireAuth, async (req, res) => { + const parsed = parseMoshpitName(req.body?.name); + if (!parsed) return backToRecords(req, res, { err: "which name? that is not one." }); + const name = `${parsed.label}.${parsed.tld}`; + const raw = String(req.body?.site ?? "").trim(); + + const result = await setNameC0mpute({ tld: parsed.tld, label: parsed.label, userId: req.user.id, site: raw }); + if (!result.ok) return backToRecords(req, res, { err: result.error || "could not set that c0mpute site" }); + if (!result.site) return backToRecords(req, res, { ok: `${name} no longer points at a c0mpute site.` }); + const edge = c0mputeEdgeHost(); + backToRecords(req, res, { + ok: edge + ? `${name} is now served from c0mpute site ${result.site} by ${edge}.` + : `${name} points at c0mpute site ${result.site}. No c0mpute edge is configured yet, so nothing serves it until one is.`, + }); +}); + moshpitRouter.post("/pit/records/delete", requireAuth, async (req, res) => { const parsed = parseMoshpitName(req.body?.name); if (!parsed) return backToRecords(req, res, { err: "which name? that is not one." }); diff --git a/apps/pwa/test/moshpit-records-page.test.mjs b/apps/pwa/test/moshpit-records-page.test.mjs index 0af93892..d7b3ba4d 100644 --- a/apps/pwa/test/moshpit-records-page.test.mjs +++ b/apps/pwa/test/moshpit-records-page.test.mjs @@ -251,3 +251,108 @@ test("records: resolve stays cheap unless the caller asks for the set", skip, as const full = await json("/api/moshpit/resolve?name=name00.eggs&records=1"); assert.deepEqual(full.body.records, [{ type: "AAAA", value: V6, ttl: 300 }]); }); + +/* ---- the c0mpute site on a name ---- */ + +const SITE = "53021d6e8d5a0e1aac48"; +const EDGE = "edge.example.net"; + +/** Run `fn` with a c0mpute edge configured (or none), restoring whatever was there. */ +async function withEdge(fn, edge = EDGE) { + const saved = process.env.MOSHPIT_C0MPUTE_EDGE; + if (edge) process.env.MOSHPIT_C0MPUTE_EDGE = edge; + else delete process.env.MOSHPIT_C0MPUTE_EDGE; + try { return await fn(); } finally { + if (saved === undefined) delete process.env.MOSHPIT_C0MPUTE_EDGE; + else process.env.MOSHPIT_C0MPUTE_EDGE = saved; + } +} + +test("records: every name offers a c0mpute site field", skip, async () => { + const { get } = await app(); + const html = panel((await get("/pit/records?q=name10.eggs")).html); + assert.match(html, /action="\/pit\/records\/c0mpute"/); + assert.match(html, /name="site"/); + assert.doesNotMatch(html, /Clear c0mpute site/, "nothing to clear on a name with no site"); +}); + +test("records: saving a c0mpute site shows it, links the gateway and repoints the name", skip, async () => { + await withEdge(async () => { + const { get, post, json } = await app(); + + const saved = await post("/pit/records/c0mpute", { name: "name11.eggs", site: ` ${SITE.toUpperCase()} `, q: "name11.eggs" }); + assert.equal(saved.status, 302); + const back = params(saved.location); + assert.match(back.get("ok") ?? "", /name11\.eggs .*53021d6e8d5a0e1aac48 by edge\.example\.net/); + assert.equal(back.get("q"), "name11.eggs", "the filter was dropped on the way back"); + + const html = panel((await get("/pit/records?q=name11.eggs")).html); + assert.match(html, /c0mpute site: ]*href="https:\/\/gateway\.c0mpute\.com\/s\/53021d6e8d5a0e1aac48\/"/); + assert.match(html, /served by edge\.example\.net<\/span>/); + assert.match(html, /resolves to edge\.example\.net<\/span>/, "the card shows the stored target, not where it goes"); + assert.match(html, /Clear c0mpute site/); + assert.match(html, /A, AAAA or CNAME records/, "the page does not say what records do in this mode"); + + // The same thing the CLI and resolvers see. + const resolved = await json("/api/moshpit/resolve?name=name11.eggs"); + assert.equal(resolved.body.c0mpute, SITE); + assert.equal(resolved.body.target, EDGE); + + // And the records API carries it, so an empty zone is not read as parked. + const records = await json("/api/moshpit/records?name=name11.eggs"); + assert.equal(records.body.c0mpute, SITE); + assert.equal(records.body.edge, EDGE); + assert.deepEqual(records.body.records, []); + }); +}); + +test("records: without an edge the site is stored and the page says nothing serves it", skip, async () => { + await withEdge(async () => { + const { get, post, json } = await app(); + const saved = await post("/pit/records/c0mpute", { name: "name12.eggs", site: SITE }); + assert.match(params(saved.location).get("ok") ?? "", /No c0mpute edge is configured/); + const html = panel((await get("/pit/records?q=name12.eggs")).html); + assert.match(html, /nothing serves it yet/); + const records = await json("/api/moshpit/records?name=name12.eggs"); + assert.equal(records.body.c0mpute, SITE); + assert.equal(records.body.edge, null); + }, null); +}); + +test("records: a c0mpute site beside an address record is called out", skip, async () => { + await withEdge(async () => { + const { get, post } = await app(); + await post("/pit/records", { name: "name13.eggs", type: "AAAA", value: V6 }); + await post("/pit/records/c0mpute", { name: "name13.eggs", site: SITE }); + const html = panel((await get("/pit/records?q=name13.eggs")).html); + assert.match(html, /also publishes AAAA/); + }); +}); + +test("records: clearing the c0mpute site takes it off", skip, async () => { + await withEdge(async () => { + const { get, post, json } = await app(); + await post("/pit/records/c0mpute", { name: "name14.eggs", site: SITE }); + const cleared = await post("/pit/records/c0mpute", { name: "name14.eggs", site: "" }); + assert.match(params(cleared.location).get("ok") ?? "", /no longer points at a c0mpute site/); + + const html = panel((await get("/pit/records?q=name14.eggs")).html); + assert.doesNotMatch(html, /Clear c0mpute site/); + assert.match(html, /nothing published · parked/, "a cleared name with no target is parked again"); + const records = await json("/api/moshpit/records?name=name14.eggs"); + assert.equal(records.body.c0mpute, null); + assert.equal(records.body.edge, null); + }); +}); + +test("records: a bad site id is refused, and nobody else's name can be pointed", skip, async () => { + const { post, json } = await app(); + + const bad = await post("/pit/records/c0mpute", { name: "name15.eggs", site: "not-a-site" }); + assert.match(params(bad.location).get("err") ?? "", /20 hex/); + assert.equal((await json("/api/moshpit/resolve?name=name15.eggs")).body.c0mpute, null); + + const theirs = await post("/pit/records/c0mpute", { name: "secret.theirs", site: SITE }); + assert.match(params(theirs.location).get("err") ?? "", /do not own/); + assert.equal((await json("/api/moshpit/resolve?name=secret.theirs")).body.c0mpute, null); +});