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}
+ ${site ? `
+ ` : ""}
+
`;
+};
+
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) => `
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."}