flagon-io/g1t

public

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

g1t/services/og/src/resolve.ts

312 lines9,921 bytesCodeBlame
1/**
2 * Which card a page of g1t.sh gets, and what it says.
3 *
4 * Everything here is looked up with no viewer, so a card can only ever show
5 * what an anonymous visitor to the page could see. A private repository, a
6 * page that does not exist, or anything that fails to load gets the generic
7 * card: never a name or a title that a link preview could leak.
8 */
9import type { IdentityApi, Issue, Project, PullStatus, ReposApi, WorkApi, ProjectsApi } from "@g1t/contracts";
10
11// The one list of Soon pages, shared with the site so the two never disagree.
12import { roadmapItem } from "../../../apps/web/app/lib/roadmap.ts";
13
14export type IssueState = "open" | "completed" | "not_planned";
15
16export type Card =
17 /**
18 * The brand card: the lockup and the tagline. `failed` when a service
19 * could not be reached, so the card is not kept for long.
20 */
21 | { kind: "brand"; failed?: true }
22 /** A page of the site that says what g1t is. */
23 | { kind: "page"; address: string; eyebrow: string; title: string; description: string }
24 | {
25 kind: "workspace";
26 slug: string;
27 name: string;
28 description: string | null;
29 projects: number;
30 /** The uploaded icon's hash, if it has one. */
31 avatar?: string | null;
32 /** That icon as a data URI, once loaded; see `index.ts`. */
33 icon?: string;
34 }
35 | {
36 /** A person's profile, at `/u/<username>`. */
37 kind: "person";
38 username: string;
39 name: string | null;
40 bio: string | null;
41 /** Over public repositories only. */
42 pullsMerged: number;
43 pullsOpen: number;
44 issues: number;
45 avatar?: string | null;
46 icon?: string;
47 }
48 | {
49 kind: "project";
50 owner: string;
51 repo: string;
52 name: string;
53 description: string | null;
54 issues: number;
55 pulls: number;
56 }
57 | {
58 kind: "issue";
59 owner: string;
60 repo: string;
61 number: number;
62 title: string;
63 state: IssueState;
64 author: string;
65 }
66 | {
67 kind: "pull";
68 owner: string;
69 repo: string;
70 number: number;
71 title: string;
72 state: PullStatus;
73 author: string;
74 }
75 | { kind: "soon"; owner: string; repo: string; title: string; summary: string; section: string }
76 | { kind: "docs"; title: string; section: string | null; description: string | null };
77
78export const BRAND: Card = { kind: "brand" };
79
80/** The services a card is looked up in: only the calls it needs. */
81export type Sources = {
82 identity: Pick<IdentityApi, "getWorkspace" | "profile">;
83 repos: Pick<ReposApi, "get">;
84 work: Pick<WorkApi, "counts" | "getIssue" | "getPull" | "byAuthor">;
85 projects: Pick<ProjectsApi, "get" | "list">;
86};
87
88/**
89 * The site's own pages, which no workspace can be named. Those that say
90 * what g1t is get a card of their own; the rest (sign-in, settings and the
91 * like) get the brand card.
92 */
93const PAGES: Record<string, Card> = {
94 "": BRAND,
95 pricing: {
96 kind: "page",
97 address: "g1t.sh/pricing",
98 eyebrow: "Pricing",
99 title: "What it costs us, plus a markup",
100 description:
101 "g1t passes its costs through: what Cloudflare and model providers charge g1t, plus a set markup. No seats.",
102 },
103 explore: {
104 kind: "page",
105 address: "g1t.sh/explore",
106 eyebrow: "Explore",
107 title: "Public projects on g1t",
108 description: "Recently active and new public projects, by language and topic.",
109 },
110 search: {
111 kind: "page",
112 address: "g1t.sh/search",
113 eyebrow: "Search",
114 title: "Search all of g1t",
115 description: "Repositories, code, issues, pull requests and people, in one search.",
116 },
117 register: {
118 kind: "page",
119 address: "g1t.sh/register",
120 eyebrow: "Get started",
121 title: "Hand off the outcome. A team of agents ships it.",
122 description: "Create an account on g1t, the git forge for teams of agents.",
123 },
124};
125
126const RESERVED = new Set([
127 "login",
128 "register",
129 "logout",
130 "verify",
131 "forgot",
132 "reset",
133 "device",
134 "oauth",
135 "new",
136 "settings",
137 "explore",
138 "pricing",
139 "search",
140 "workspaces",
141 "brand",
142 "avatars",
143 "llms.txt",
144 "favicon.ico",
145 "favicon.svg",
146]);
147
148/** A name as it may appear in an address: no slashes, dots only inside. */
149const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
150
151/** The path's segments, decoded; null if it is not a path on the site. */
152export function segments(path: string): string[] | null {
153 if (!path.startsWith("/") || path.startsWith("//")) return null;
154 const bare = path.split(/[?#]/, 1)[0];
155 try {
156 return bare
157 .split("/")
158 .filter((part) => part !== "")
159 .map((part) => decodeURIComponent(part));
160 } catch {
161 return null;
162 }
163}
164
165/** The card for a page of g1t.sh, as an anonymous visitor would see it. */
166export async function resolve(path: string, sources: Sources): Promise<Card> {
167 const parts = segments(path);
168 if (!parts) return BRAND;
169 try {
170 return (await lookUp(parts, sources)) ?? BRAND;
171 } catch {
172 // A service that is down must not break a link preview.
173 return { kind: "brand", failed: true };
174 }
175}
176
177async function lookUp(parts: string[], sources: Sources): Promise<Card | null> {
178 const { identity, repos, work, projects } = sources;
179 const [owner, repo, section, item] = parts;
180 if (owner === undefined) return PAGES[""];
181 // A person, under `u`. Counted with no viewer: public repositories only.
182 if (owner.toLowerCase() === "u") {
183 if (repo === undefined || parts.length !== 2 || !NAME.test(repo)) return null;
184 const profile = await identity.profile(repo.toLowerCase());
185 if (!profile) return null;
186 const authored = await work.byAuthor(profile.username, null, { limit: 1 }).catch(() => null);
187 const counts = authored?.ok ? authored.value.counts : null;
188 return {
189 kind: "person",
190 username: profile.username,
191 name: profile.name,
192 bio: profile.bio,
193 pullsMerged: counts?.pullsMerged ?? 0,
194 pullsOpen: counts?.pullsOpen ?? 0,
195 issues: counts?.issues ?? 0,
196 avatar: profile.avatar,
197 };
198 }
199 if (RESERVED.has(owner.toLowerCase())) return parts.length === 1 ? (PAGES[owner.toLowerCase()] ?? null) : null;
200 if (!NAME.test(owner)) return null;
201
202 // A workspace, or one of its own pages under `-`.
203 if (repo === undefined || repo === "-") {
204 const workspace = await identity.getWorkspace(owner.toLowerCase());
205 if (!workspace) return null;
206 const listed = await projects.list(workspace.slug, null).catch(() => null);
207 return {
208 kind: "workspace",
209 slug: workspace.slug,
210 name: workspace.name || workspace.slug,
211 description: workspace.description,
212 projects: listed?.ok ? listed.value.filter((p) => !p.private).length : 0,
213 avatar: workspace.avatar ?? null,
214 };
215 }
216
217 // A project, through its repository: nothing is shown unless the
218 // repository is public.
219 if (!NAME.test(repo)) return null;
220 const path = { namespace: owner, name: repo };
221 const found = await repos.get(path, null);
222 if (!found.ok || found.value.isPrivate) return null;
223 const { namespace, name } = found.value;
224 const canonical = { namespace, name };
225
226 if (section === "issues" && item !== undefined && isNumber(item) && parts.length === 4) {
227 const issue = await work.getIssue(canonical, Number(item), null);
228 if (issue.ok) {
229 return {
230 kind: "issue",
231 owner: namespace,
232 repo: name,
233 number: issue.value.issue.number,
234 title: issue.value.issue.title,
235 state: issueState(issue.value.issue),
236 author: issue.value.issue.author.username,
237 };
238 }
239 }
240
241 if (section === "pull" && item !== undefined && isNumber(item)) {
242 const pull = await work.getPull(canonical, Number(item), null);
243 if (pull.ok) {
244 return {
245 kind: "pull",
246 owner: namespace,
247 repo: name,
248 number: pull.value.pull.number,
249 title: pull.value.pull.title,
250 state: pull.value.pull.status,
251 author: pull.value.pull.author.username,
252 };
253 }
254 }
255
256 if (section === "soon" && item !== undefined && parts.length === 4) {
257 const feature = roadmapItem(item);
258 if (feature) {
259 return {
260 kind: "soon",
261 owner: namespace,
262 repo: name,
263 title: feature.title,
264 summary: feature.summary,
265 section: feature.section,
266 };
267 }
268 }
269
270 const [project, counts] = await Promise.all([
271 projects.get(namespace, name.toLowerCase(), null).catch(() => null),
272 work.counts(canonical, null).catch(() => null),
273 ]);
274 const shown: Project | null = project?.ok && !project.value.private ? project.value : null;
275 return {
276 kind: "project",
277 owner: namespace,
278 repo: name,
279 name: shown?.name ?? name,
280 description: shown?.description ?? found.value.description,
281 issues: counts?.ok ? counts.value.issues : 0,
282 pulls: counts?.ok ? counts.value.pulls : 0,
283 };
284}
285
286function isNumber(value: string): boolean {
287 return /^[1-9][0-9]{0,8}$/.test(value);
288}
289
290function issueState(issue: Issue): IssueState {
291 if (issue.state === "open") return "open";
292 return issue.reason === "not_planned" ? "not_planned" : "completed";
293}
294
295/** The card for a page of the docs, from what the page says about itself. */
296export function docsCard(query: URLSearchParams): Card {
297 const title = clean(query.get("title"), 160);
298 if (!title) return { kind: "docs", title: "g1t docs", section: null, description: clean(query.get("description"), 300) };
299 return {
300 kind: "docs",
301 title,
302 section: clean(query.get("section"), 60),
303 description: clean(query.get("description"), 300),
304 };
305}
306
307/** Text from a query string: trimmed, single-spaced and bounded. */
308export function clean(value: string | null, max: number): string | null {
309 const text = (value ?? "").replace(/\s+/g, " ").trim();
310 if (!text) return null;
311 return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text;
312}