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

268 lines8,528 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 | { kind: "workspace"; slug: string; name: string; description: string | null; projects: number }
25 | {
26 kind: "project";
27 owner: string;
28 repo: string;
29 name: string;
30 description: string | null;
31 issues: number;
32 pulls: number;
33 }
34 | {
35 kind: "issue";
36 owner: string;
37 repo: string;
38 number: number;
39 title: string;
40 state: IssueState;
41 author: string;
42 }
43 | {
44 kind: "pull";
45 owner: string;
46 repo: string;
47 number: number;
48 title: string;
49 state: PullStatus;
50 author: string;
51 }
52 | { kind: "soon"; owner: string; repo: string; title: string; summary: string; section: string }
53 | { kind: "docs"; title: string; section: string | null; description: string | null };
54
55export const BRAND: Card = { kind: "brand" };
56
57/** The services a card is looked up in: only the calls it needs. */
58export type Sources = {
59 identity: Pick<IdentityApi, "getWorkspace">;
60 repos: Pick<ReposApi, "get">;
61 work: Pick<WorkApi, "counts" | "getIssue" | "getPull">;
62 projects: Pick<ProjectsApi, "get" | "list">;
63};
64
65/**
66 * The site's own pages, which no workspace can be named. Those that say
67 * what g1t is get a card of their own; the rest (sign-in, settings and the
68 * like) get the brand card.
69 */
70const PAGES: Record<string, Card> = {
71 "": BRAND,
72 pricing: {
73 kind: "page",
74 address: "g1t.sh/pricing",
75 eyebrow: "Pricing",
76 title: "What it costs us, plus a markup",
77 description:
78 "g1t passes its costs through: what Cloudflare and model providers charge g1t, plus a set markup. No seats.",
79 },
80 explore: {
81 kind: "page",
82 address: "g1t.sh/explore",
83 eyebrow: "Explore",
84 title: "Public repositories on g1t",
85 description: "Every public project on g1t, and the agents at work on them.",
86 },
87 search: {
88 kind: "page",
89 address: "g1t.sh/search",
90 eyebrow: "Search",
91 title: "Search repositories on g1t",
92 description: "Find public projects by name or description.",
93 },
94 register: {
95 kind: "page",
96 address: "g1t.sh/register",
97 eyebrow: "Get started",
98 title: "Hand off the outcome. A team of agents ships it.",
99 description: "Create an account on g1t, the git forge for teams of agents.",
100 },
101};
102
103const RESERVED = new Set([
104 "login",
105 "register",
106 "logout",
107 "verify",
108 "forgot",
109 "reset",
110 "device",
111 "oauth",
112 "new",
113 "settings",
114 "explore",
115 "pricing",
116 "search",
117 "workspaces",
118 "brand",
119 "llms.txt",
120 "favicon.ico",
121 "favicon.svg",
122]);
123
124/** A name as it may appear in an address: no slashes, dots only inside. */
125const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
126
127/** The path's segments, decoded; null if it is not a path on the site. */
128export function segments(path: string): string[] | null {
129 if (!path.startsWith("/") || path.startsWith("//")) return null;
130 const bare = path.split(/[?#]/, 1)[0];
131 try {
132 return bare
133 .split("/")
134 .filter((part) => part !== "")
135 .map((part) => decodeURIComponent(part));
136 } catch {
137 return null;
138 }
139}
140
141/** The card for a page of g1t.sh, as an anonymous visitor would see it. */
142export async function resolve(path: string, sources: Sources): Promise<Card> {
143 const parts = segments(path);
144 if (!parts) return BRAND;
145 try {
146 return (await lookUp(parts, sources)) ?? BRAND;
147 } catch {
148 // A service that is down must not break a link preview.
149 return { kind: "brand", failed: true };
150 }
151}
152
153async function lookUp(parts: string[], { identity, repos, work, projects }: Sources): Promise<Card | null> {
154 const [owner, repo, section, item] = parts;
155 if (owner === undefined) return PAGES[""];
156 if (RESERVED.has(owner.toLowerCase())) return parts.length === 1 ? (PAGES[owner.toLowerCase()] ?? null) : null;
157 if (!NAME.test(owner)) return null;
158
159 // A workspace, or one of its own pages under `-`.
160 if (repo === undefined || repo === "-") {
161 const workspace = await identity.getWorkspace(owner.toLowerCase());
162 if (!workspace) return null;
163 const listed = await projects.list(workspace.slug, null).catch(() => null);
164 return {
165 kind: "workspace",
166 slug: workspace.slug,
167 name: workspace.name || workspace.slug,
168 description: workspace.description,
169 projects: listed?.ok ? listed.value.filter((p) => !p.private).length : 0,
170 };
171 }
172
173 // A project, through its repository: nothing is shown unless the
174 // repository is public.
175 if (!NAME.test(repo)) return null;
176 const path = { namespace: owner, name: repo };
177 const found = await repos.get(path, null);
178 if (!found.ok || found.value.isPrivate) return null;
179 const { namespace, name } = found.value;
180 const canonical = { namespace, name };
181
182 if (section === "issues" && item !== undefined && isNumber(item) && parts.length === 4) {
183 const issue = await work.getIssue(canonical, Number(item), null);
184 if (issue.ok) {
185 return {
186 kind: "issue",
187 owner: namespace,
188 repo: name,
189 number: issue.value.issue.number,
190 title: issue.value.issue.title,
191 state: issueState(issue.value.issue),
192 author: issue.value.issue.author.username,
193 };
194 }
195 }
196
197 if (section === "pull" && item !== undefined && isNumber(item)) {
198 const pull = await work.getPull(canonical, Number(item), null);
199 if (pull.ok) {
200 return {
201 kind: "pull",
202 owner: namespace,
203 repo: name,
204 number: pull.value.pull.number,
205 title: pull.value.pull.title,
206 state: pull.value.pull.status,
207 author: pull.value.pull.author.username,
208 };
209 }
210 }
211
212 if (section === "soon" && item !== undefined && parts.length === 4) {
213 const feature = roadmapItem(item);
214 if (feature) {
215 return {
216 kind: "soon",
217 owner: namespace,
218 repo: name,
219 title: feature.title,
220 summary: feature.summary,
221 section: feature.section,
222 };
223 }
224 }
225
226 const [project, counts] = await Promise.all([
227 projects.get(namespace, name.toLowerCase(), null).catch(() => null),
228 work.counts(canonical, null).catch(() => null),
229 ]);
230 const shown: Project | null = project?.ok && !project.value.private ? project.value : null;
231 return {
232 kind: "project",
233 owner: namespace,
234 repo: name,
235 name: shown?.name ?? name,
236 description: shown?.description ?? found.value.description,
237 issues: counts?.ok ? counts.value.issues : 0,
238 pulls: counts?.ok ? counts.value.pulls : 0,
239 };
240}
241
242function isNumber(value: string): boolean {
243 return /^[1-9][0-9]{0,8}$/.test(value);
244}
245
246function issueState(issue: Issue): IssueState {
247 if (issue.state === "open") return "open";
248 return issue.reason === "not_planned" ? "not_planned" : "completed";
249}
250
251/** The card for a page of the docs, from what the page says about itself. */
252export function docsCard(query: URLSearchParams): Card {
253 const title = clean(query.get("title"), 160);
254 if (!title) return { kind: "docs", title: "g1t docs", section: null, description: clean(query.get("description"), 300) };
255 return {
256 kind: "docs",
257 title,
258 section: clean(query.get("section"), 60),
259 description: clean(query.get("description"), 300),
260 };
261}
262
263/** Text from a query string: trimmed, single-spaced and bounded. */
264export function clean(value: string | null, max: number): string | null {
265 const text = (value ?? "").replace(/\s+/g, " ").trim();
266 if (!text) return null;
267 return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text;
268}