Skip to content
99 linesCodeBlameRaw
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
8export const DOMAIN = "g1t.page";
9/** Where custom domains point (the Cloudflare for SaaS fallback origin). */
10export const FALLBACK = `domains.${DOMAIN}`;
11
12export type Route =
13 | { kind: "home" }
14 | { kind: "fallback" }
15 | { kind: "app"; label: string }
16 | { kind: "custom"; hostname: string }
17 | { kind: "invalid" };
18
19const LABEL = /^[a-z0-9-]{1,63}$/;
20const HOST = /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/;
21
22export 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/**
34 * What the deployments service notes for an app, under its own hostname,
35 * for observability: each request's line names its project and deployment.
36 */
37export type AppTelemetry = { project: string; deployment: string | null; environment: string | null; service: string; commit: string | null };
38
39/**
40 * What the deployments service writes under a custom hostname, an app's
41 * old hostname (a redirect), or an app's own hostname (its `telemetry`).
42 */
43export type DomainEntry = { script: string; redirect: string | null; telemetry?: AppTelemetry | null };
44
45/** Reads a stored entry, or null if it is not one. */
46export function parseEntry(value: unknown): DomainEntry | null {
47 if (!value || typeof value !== "object") return null;
48 const entry = value as Record<string, unknown>;
49 if (typeof entry.script !== "string" || !LABEL.test(entry.script)) return null;
50 const redirect = typeof entry.redirect === "string" && HOST.test(entry.redirect) ? entry.redirect : null;
51 const t = entry.telemetry as Record<string, unknown> | null | undefined;
52 const text = (v: unknown) => (typeof v === "string" ? v : null);
53 const telemetry =
54 t && typeof t === "object" && typeof t.project === "string"
55 ? { project: t.project, deployment: text(t.deployment), environment: text(t.environment), service: text(t.service) ?? entry.script, commit: text(t.commit) }
56 : null;
57 return { script: entry.script, redirect, telemetry };
58}
59
60/** The same path and query on `target`, over https. */
61export function redirectTo(requestUrl: string, target: string): string {
62 const url = new URL(requestUrl);
63 return `https://${target}${url.pathname}${url.search}`;
64}
65
66/**
67 * What one request to an app may use: CPU time, not counting time spent
68 * waiting on the network, and requests it makes of its own. Static assets
69 * are served without running the app, and use neither. No plan sets
70 * others; the Deployments guide names these.
71 */
72export const APP_LIMITS = { cpuMs: 50, subRequests: 50 } as const;
73
74/** Whether an app's failure was going over `APP_LIMITS`, as the runtime words it. */
75export function overLimits(error: unknown): boolean {
76 return /exceeded (its )?cpu|cpu time limit|too many subrequests|subrequest limit/i.test(String(error));
77}
78
79/** The header each app is handed with its request's id, which its logs can name. */
80export const REQUEST_ID_HEADER = "x-g1t-request-id";
81/** The first word of each request's line, which observability reads (services/telemetry). */
82export const REQUEST_MARK = "g1t-request";
83
84function hex(bytes: number): string {
85 return [...crypto.getRandomValues(new Uint8Array(bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
86}
87
88/**
89 * A request's id and trace: the trace the caller's `traceparent` began,
90 * or a new one. The id is the dispatcher's span in it, which the app's
91 * own spans (from an OpenTelemetry SDK that reads `traceparent`) are under.
92 */
93export function traceFor(traceparent: string | null): { id: string; trace: string; traceparent: string; flags: string } {
94 const m = /^[0-9a-f]{2}-([0-9a-f]{32})-[0-9a-f]{16}-([0-9a-f]{2})$/.exec((traceparent ?? "").trim().toLowerCase());
95 const trace = m && !/^0+$/.test(m[1]) ? m[1] : hex(16);
96 const flags = m ? m[2] : "01";
97 const id = hex(8);
98 return { id, trace, traceparent: `00-${trace}-${id}-${flags}`, flags };
99}