Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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