Skip to content
96 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor1/**
2 * Hostnames people bring for their apps: checked, spelled the one way
3 * Cloudflare and browsers agree on (lowercase, punycode), and paired with
4 * their www or apex twin.
5 *
6 * Kept free of imports so its tests run on Node as they are.
7 */
8
9/** Domains a custom hostname may never be on: g1t's own. */
10const RESERVED = ["g1t.page", "g1t.sh", "g1t.dev"];
11
12/**
13 * Public suffixes of more than one label, the common ones. A hostname
14 * directly under one of these is an apex (`shop.co.uk`), not a subdomain.
15 */
16const MULTI_LABEL_SUFFIXES = new Set([
17 "co.uk", "org.uk", "me.uk", "ltd.uk", "plc.uk", "net.uk", "ac.uk", "gov.uk",
18 "com.au", "net.au", "org.au", "edu.au", "id.au",
19 "co.nz", "net.nz", "org.nz",
20 "co.jp", "ne.jp", "or.jp",
21 "co.za", "org.za",
22 "com.br", "net.br", "org.br",
23 "com.mx", "org.mx",
24 "com.ar", "com.co", "com.tr", "com.sg", "com.hk", "com.tw", "com.cn", "com.my", "com.ph",
25 "co.in", "net.in", "org.in", "co.id", "co.il", "co.kr", "or.kr",
26 "com.es", "com.pl", "com.ua",
27]);
28
29const LABEL = /^(?!-)[a-z0-9-]{1,63}(?<!-)$/;
30
31export type HostnameCheck = { ok: true; hostname: string } | { ok: false; reason: string };
32
33/**
34 * `input` as a hostname g1t can serve: lowercase and punycode, with no
35 * scheme, path, port or trailing dot; at least two labels, each a valid
36 * label; never a wildcard, an IP address or one of g1t's own domains.
37 */
38export function checkHostname(input: string): HostnameCheck {
39 let text = String(input ?? "").trim().toLowerCase();
40 // What people paste: a URL, or a hostname with a path or a trailing dot.
41 text = text.replace(/^[a-z][a-z0-9+.-]*:\/\//, "").replace(/[/?#].*$/, "").replace(/\.$/, "");
42 if (!text) return { ok: false, reason: "Enter a domain, such as example.com or www.example.com." };
43 if (text.includes("*")) return { ok: false, reason: "Wildcard domains are not supported yet; add each hostname on its own." };
44 if (/:\d+$/.test(text)) return { ok: false, reason: "Leave out the port: apps are served on 443." };
45 if (/[\s@:]/.test(text)) return { ok: false, reason: "That is not a domain." };
46 let hostname: string;
47 try {
48 // The URL parser applies IDNA, so `bücher.example` becomes `xn--bcher-kva.example`.
49 hostname = new URL(`http://${text}/`).hostname;
50 } catch {
51 return { ok: false, reason: "That is not a domain." };
52 }
53 if (/^\[|^\d+(\.\d+){3}$/.test(hostname)) return { ok: false, reason: "Use a domain name, not an IP address." };
54 if (hostname.length > 253) return { ok: false, reason: "That domain is too long." };
55 const labels = hostname.split(".");
56 if (labels.length < 2) return { ok: false, reason: "Include the domain's ending, such as .com or .io." };
57 if (labels.some((label) => !LABEL.test(label))) {
58 return { ok: false, reason: "Each part of a domain is 1 to 63 letters, digits or hyphens, and does not start or end with a hyphen." };
59 }
60 if (/^\d+$/.test(labels[labels.length - 1])) return { ok: false, reason: "That is not a domain." };
61 for (const reserved of RESERVED) {
62 if (hostname === reserved || hostname.endsWith(`.${reserved}`)) {
63 return { ok: false, reason: `${reserved} is g1t's own domain; every app already has an address on it.` };
64 }
65 }
66 return { ok: true, hostname };
67}
68
69/** The registrable domain `hostname` is under: `example.com` for `www.example.com`. */
70export function apexOf(hostname: string): string {
71 const labels = hostname.split(".");
72 const two = labels.slice(-2).join(".");
73 const keep = MULTI_LABEL_SUFFIXES.has(two) ? 3 : 2;
74 return labels.slice(-keep).join(".");
75}
76
77/** Whether `hostname` is a registrable domain itself, which DNS cannot point with a CNAME. */
78export function isApex(hostname: string): boolean {
79 return apexOf(hostname) === hostname;
80}
81
82/**
83 * The other half of a www/apex pair: `www.example.com` for `example.com`
84 * and back. Null for any other subdomain.
85 */
86export function twinOf(hostname: string): string | null {
87 if (isApex(hostname)) return `www.${hostname}`;
88 if (hostname.startsWith("www.") && isApex(hostname.slice(4))) return hostname.slice(4);
89 return null;
90}
91
92/** What a hostname's own record is called in its DNS zone: `@` for the apex, else the part before it. */
93export function recordName(hostname: string): string {
94 const apex = apexOf(hostname);
95 return hostname === apex ? "@" : hostname.slice(0, -apex.length - 1);
96}

This file's history is long; its oldest lines are credited to the oldest commit read.