Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/pwa/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,9 @@ VAPID_SUBJECT=mailto:you@example.com
# optional channel providers
TELEGRAM_BOT_TOKEN=
SLACK_WEBHOOK_URL=

# The Moshpit resolvers advertised on /pit/dns — addresses, optionally named.
# Unset, the page says they are not published yet and explains how to run one;
# it never invents an address for someone to paste into their DNS settings.
# MOSHPIT_DNS_RESOLVERS=dns1.pit.moshcode.sh=203.0.113.7,dns2.pit.moshcode.sh=203.0.113.8
# MOSHPIT_DOH_URL=https://dns.pit.moshcode.sh/dns-query
52 changes: 52 additions & 0 deletions apps/pwa/src/lib/moshpit-landing.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// What to offer someone who arrived by typing a Moshpit name.
//
// Somebody puts `mosh.whatever` in the address bar. Either a resolver sent
// them here or the gateway did, and the one thing they must not get is a 404 —
// they just demonstrated demand for a name, which is the entire product.
//
// What can honestly be offered depends on who holds the ending and whether they
// put a price on it. This picks the answer without a database or a request, so
// the wording is testable and cannot drift from the rules in `registerName`
// (owners mint for free) and `quoteName` (everyone else buys, if it is listed).

import { parseMoshpitName } from "./moshpit-name.mjs";

/**
* @param {object} state
* @param {boolean} state.tldOwned is `.whatever` claimed by anyone
* @param {boolean} state.ownedByViewer …by the person looking at the page
* @param {boolean} state.nameRegistered is `mosh.whatever` itself minted
* @param {string|null} state.target where the name points, when it points
* @param {number|null} state.priceUsd what the owner charges per name, if listed
*/
export function landingFor(input, state = {}) {
const parsed = parseMoshpitName(input);
if (!parsed) return { kind: "none" };
const { label, tld } = parsed;
const base = { label, tld, name: `${label}.${tld}` };

// Nobody holds the ending. The visitor can have it and everything under it —
// the best possible answer to "this name does not exist yet".
if (!state.tldOwned) return { ...base, kind: "claim-tld" };

// They hold it: this is the one case where the name is one form away.
if (state.ownedByViewer) {
return { ...base, kind: state.nameRegistered ? "yours" : "mint-name" };
}

// Someone else's ending. Taken is taken, whatever the price.
if (state.nameRegistered) {
return { ...base, kind: "taken", target: state.target ?? null };
}

// Free, and the operator has put a price on names under it — so this visitor
// can have the exact name they typed, right now, for that much.
const priceUsd = state.priceUsd;
if (priceUsd !== null && priceUsd !== undefined) {
return { ...base, kind: "buy", priceUsd };
}

// Free, but not for sale. Say so rather than inviting them into a checkout
// that `quoteName` would refuse.
return { ...base, kind: "not-for-sale" };
}
56 changes: 56 additions & 0 deletions apps/pwa/src/lib/moshpit-resolvers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// The public Moshpit resolvers, as advertised on /pit/dns.
//
// Read from the environment rather than hardcoded: the addresses are
// operational facts that change when a box moves, and a page that keeps
// telling people to use an address that moved is worse than one that says
// nothing at all.
//
// Addresses are validated here for the same reason. This list goes onto a page
// where strangers copy it into their network settings, so a typo in an env var
// has to disappear rather than be rendered as an instruction to point their
// DNS somewhere that is not an address.

const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;

export function isIpAddress(value) {
const raw = String(value ?? "").trim();
if (IPV4.test(raw)) return raw.split(".").every((octet) => Number(octet) <= 255);
// Loose on IPv6 by design: this only has to reject things that are plainly
// not addresses, not re-implement the grammar.
return /^[0-9a-f:]+$/i.test(raw) && raw.includes(":") && !raw.includes(":::");
}

/**
* Parse `dns1.pit.moshcode.sh=203.0.113.7, dns2.pit.moshcode.sh=203.0.113.8`.
*
* The name is optional — an address on its own is a complete instruction,
* since what a person types into their network settings is an address. A
* resolver's own hostname cannot be looked up until they already have a
* working resolver, so the name is only there to say which box they are
* pointing at.
*/
export function parseResolvers(spec) {
return String(spec ?? "")
.split(/[,\s]+/)
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => {
const at = entry.lastIndexOf("=");
const name = at > 0 ? entry.slice(0, at).trim() : "";
const address = (at > 0 ? entry.slice(at + 1) : entry).trim();
return { name: name || null, address };
})
.filter((resolver) => isIpAddress(resolver.address));
}

/** What /pit/dns knows about the published resolvers. */
export function resolverConfig(env = process.env) {
const resolvers = parseResolvers(env.MOSHPIT_DNS_RESOLVERS);
const doh = String(env.MOSHPIT_DOH_URL ?? "").trim();
return {
resolvers,
// Plain HTTP would be advertised to a browser as "secure DNS".
doh: /^https:\/\/\S+$/.test(doh) ? doh : null,
published: resolvers.length > 0,
};
}
Loading
Loading