g1t/services/pages/src/route.ts
| 1 | /** |
| 2 | * What a request's host is: g1t.page itself, an app's address on it, the |
| 3 | * hostname custom domains point at, or a custom domain. |
| 4 | * |
| 5 | * Kept free of imports so its tests run on Node as they are. |
| 6 | */ |
| 7 | |
| 8 | export const DOMAIN = "g1t.page"; |
| 9 | /** Where custom domains point (the Cloudflare for SaaS fallback origin). */ |
| 10 | export const FALLBACK = `domains.${DOMAIN}`; |
| 11 | |
| 12 | export type Route = |
| 13 | | { kind: "home" } |
| 14 | | { kind: "fallback" } |
| 15 | | { kind: "app"; label: string } |
| 16 | | { kind: "custom"; hostname: string } |
| 17 | | { kind: "invalid" }; |
| 18 | |
| 19 | const LABEL = /^[a-z0-9-]{1,63}$/; |
| 20 | const HOST = /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/; |
| 21 | |
| 22 | export function route(rawHost: string): Route { |
| 23 | const host = rawHost.toLowerCase().replace(/\.$/, ""); |
| 24 | if (host === DOMAIN) return { kind: "home" }; |
| 25 | if (host === FALLBACK) return { kind: "fallback" }; |
| 26 | if (host.endsWith(`.${DOMAIN}`)) { |
| 27 | const label = host.slice(0, -DOMAIN.length - 1); |
| 28 | return LABEL.test(label) ? { kind: "app", label } : { kind: "invalid" }; |
| 29 | } |
| 30 | return HOST.test(host) && host.length <= 253 ? { kind: "custom", hostname: host } : { kind: "invalid" }; |
| 31 | } |
| 32 | |
| 33 | /** What the deployments service writes under a custom hostname. */ |
| 34 | export type DomainEntry = { script: string; redirect: string | null; project?: string }; |
| 35 | |
| 36 | /** Reads a stored entry, or null if it is not one. */ |
| 37 | export function parseEntry(value: unknown): DomainEntry | null { |
| 38 | if (!value || typeof value !== "object") return null; |
| 39 | const entry = value as Record<string, unknown>; |
| 40 | if (typeof entry.script !== "string" || !LABEL.test(entry.script)) return null; |
| 41 | const redirect = typeof entry.redirect === "string" && HOST.test(entry.redirect) ? entry.redirect : null; |
| 42 | return { script: entry.script, redirect }; |
| 43 | } |
| 44 | |
| 45 | /** The same path and query on `target`, over https. */ |
| 46 | export function redirectTo(requestUrl: string, target: string): string { |
| 47 | const url = new URL(requestUrl); |
| 48 | return `https://${target}${url.pathname}${url.search}`; |
| 49 | } |