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.

g1t.sh
262 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Docs: the inbox, what lands in it and why, its tabs and actions1import { createRequestHandler } from "react-router";
2
Merge branch 'worktree-agent-a8385d293d42c913a'3import { identityClient, isNamespaceShaped } from "@g1t/contracts";
4
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address5import { addressesFor } from "../app/lib/addresses";
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today6import { visitorAsksFirst } from "../app/lib/analytics-consent";
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address7import { hardenRegistryHeaders } from "../app/lib/content-safety";
8import { withSiteHeaders } from "../app/lib/page-headers";
Docs: the inbox, what lands in it and why, its tabs and actions9import { finishResponse, withRequestPerf } from "../app/lib/perf.server";
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r910import { READ_DEADLINE_MS, withDeadline } from "../app/lib/deadline";
Developer mode: a page that fails can say why. Under Settings, Account, a Developer mode switch (kept per browser in a g1t_dev cookie, like Appearance) makes the error page show the error itself, its message and stack, the address, the time, whether it came from a page the server drew or a navigation in the browser, the routes it was drawn from, the site's build (its stylesheet's hashed name), the browser and the desktop app's version, with Copy details to paste it all into a report; with it off, the page says where to turn it on. A signed-in request with developer mode on is answered by React Router in its development mode (workers/app.ts, a second handler made the first time it is wanted), so an error on the server comes through in full instead of "Unexpected Server Error"; that mode differs from production only in that and in hooks only Vite's dev server sets, and only ever covers the person's own requests. Checked in a production build: a failing repository page shows "counts failed with status 503" and its stack with developer mode on, and the usual page with it off. The workspaces guide covers it under Developer mode.11import { wantsServerErrors } from "../app/lib/developer-mode";
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address12import { gitLimited, pageLimited, usercontentLimited } from "../app/lib/front-door-limits";
Docs: the inbox, what lands in it and why, its tabs and actions13import { goImport } from "../app/lib/go-get";
14import { repositoryOfPage, stillPublic } from "../app/lib/public-cache";
Merge branch 'worktree-agent-a8385d293d42c913a'15import { registryWorkspace, servicePath } from "../app/lib/registry-paths";
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address16import { usercontentPath } from "../app/lib/usercontent";
Merge main into Artifacts Phase 217import { TOKEN_CHALLENGE } from "../app/lib/website-token";
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address18import { serveUsercontent } from "./usercontent";
Docs: the inbox, what lands in it and why, its tabs and actions19
Merge the site's CPU: code highlighted once per data centre by content, markdown parsed once per text, and the request handler built once per isolate20const loadBuild = () => import("virtual:react-router/server-build");
21
22/**
23 * Given a function for the build, React Router derives everything from it
24 * again on every request: each route wrapped for the timings in
25 * entry.server.tsx, then the route table flattened and ranked, about a
26 * millisecond and a half of CPU a page. The build never changes while an
27 * isolate lives, so outside development it is loaded once and the handler
28 * made once. Development keeps the function, so a changed file is picked up.
29 * Only the made handler is kept, never a promise of one, so no request
30 * waits on another's (a build that fails to load is loaded again next time).
31 */
32let handler: ReturnType<typeof createRequestHandler> | undefined;
Developer mode: a page that fails can say why. Under Settings, Account, a Developer mode switch (kept per browser in a g1t_dev cookie, like Appearance) makes the error page show the error itself, its message and stack, the address, the time, whether it came from a page the server drew or a navigation in the browser, the routes it was drawn from, the site's build (its stylesheet's hashed name), the browser and the desktop app's version, with Copy details to paste it all into a report; with it off, the page says where to turn it on. A signed-in request with developer mode on is answered by React Router in its development mode (workers/app.ts, a second handler made the first time it is wanted), so an error on the server comes through in full instead of "Unexpected Server Error"; that mode differs from production only in that and in hooks only Vite's dev server sets, and only ever covers the person's own requests. Checked in a production build: a failing repository page shows "counts failed with status 503" and its stack with developer mode on, and the usual page with it off. The workspaces guide covers it under Developer mode.33/**
34 * The same, in React Router's development mode: for a signed-in request
35 * with developer mode on (app/lib/developer-mode.ts), so a page that fails
36 * shows the server's own error rather than "Unexpected Server Error". Made
37 * the first time someone asks for it.
38 */
39let developerHandler: ReturnType<typeof createRequestHandler> | undefined;
Merge the site's CPU: code highlighted once per data centre by content, markdown parsed once per text, and the request handler built once per isolate40async function requestHandler(request: Request): Promise<Response> {
41 if (import.meta.env.DEV) return createRequestHandler(loadBuild, import.meta.env.MODE)(request);
Developer mode: a page that fails can say why. Under Settings, Account, a Developer mode switch (kept per browser in a g1t_dev cookie, like Appearance) makes the error page show the error itself, its message and stack, the address, the time, whether it came from a page the server drew or a navigation in the browser, the routes it was drawn from, the site's build (its stylesheet's hashed name), the browser and the desktop app's version, with Copy details to paste it all into a report; with it off, the page says where to turn it on. A signed-in request with developer mode on is answered by React Router in its development mode (workers/app.ts, a second handler made the first time it is wanted), so an error on the server comes through in full instead of "Unexpected Server Error"; that mode differs from production only in that and in hooks only Vite's dev server sets, and only ever covers the person's own requests. Checked in a production build: a failing repository page shows "counts failed with status 503" and its stack with developer mode on, and the usual page with it off. The workspaces guide covers it under Developer mode.42 if (wantsServerErrors(request)) {
43 developerHandler ??= createRequestHandler(await loadBuild(), "development");
44 return developerHandler(request);
45 }
Merge the site's CPU: code highlighted once per data centre by content, markdown parsed once per text, and the request handler built once per isolate46 handler ??= createRequestHandler(await loadBuild(), import.meta.env.MODE);
47 return handler(request);
48}
Docs: the inbox, what lands in it and why, its tabs and actions49
50const DOCS = "https://docs.g1t.sh";
51
52/** Where the documentation pages that used to live under /docs are now. */
53const MOVED_DOCS: Record<string, string> = {
54 "/docs": "/quickstart/",
55 "/docs/concepts": "/concepts/overview/",
56 "/docs/authentication": "/guides/authentication/",
57 "/docs/git": "/guides/git/",
58 "/docs/agents": "/guides/bring-your-own-agent/",
59 "/docs/api": "/reference/api/",
60 "/docs/api/reference": "/reference/api/",
61};
62
63export default {
64 async fetch(request, env, ctx) {
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address65 // Repository files and avatars, on their own origin
66 // (g1tusercontent.com): answered before anything of the site's runs,
67 // so nothing there reads or sets the session cookie.
68 const usercontent = usercontentPath(new URL(request.url), addressesFor(env).usercontent);
69 if (usercontent !== null) {
70 return (await usercontentLimited(env, request, usercontent)) ?? serveUsercontent(env, ctx, request, usercontent);
Docs: the inbox, what lands in it and why, its tabs and actions71 }
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address72 // Every answer: no sniffing, a referrer of the origin alone, and no
73 // framing of pages (app/lib/page-headers.ts).
74 return withSiteHeaders(await site(request, env, ctx));
Docs: the inbox, what lands in it and why, its tabs and actions75 },
76} satisfies ExportedHandler<Env>;
77
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address78async function site(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
79 const { pathname } = new URL(request.url);
80 // Git over HTTPS shares this hostname but belongs to the repos service.
81 // Its answer goes back to the git client as it is: a repository under a
82 // renamed workspace's old name answers with a 301, which git follows and
83 // must see, so the redirect is never followed here.
84 // The container registry (`docker login g1t.sh`) and the npm registry
85 // (`g1t.sh/-/npm/`) are the packages
86 // service's, handed over the same way.
87 // `go get g1t.sh/<workspace>/<repo>`: where its code is, from the
88 // address alone, so it costs nothing and caches.
89 const go = request.method === "GET" ? goImport(new URL(request.url)) : null;
90 if (go) {
91 return new Response(go, {
92 headers: { "content-type": "text/html; charset=utf-8", "cache-control": "public, max-age=3600" },
93 });
94 }
95 const service = servicePath(pathname);
96 if (service === "git") {
97 // Per address without credentials, per credential with them
98 // (app/lib/front-door-limits.ts): anonymous clones are not free to
99 // the repository's owner.
100 return (await gitLimited(env, request)) ?? proxyGit(env, request);
101 }
102 if (service === "packages") {
103 return proxyPackages(env, request);
104 }
105 // The documentation is its own site.
106 if (pathname === "/docs" || pathname.startsWith("/docs/")) {
107 const page = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
108 const target = MOVED_DOCS[page] ?? "/";
109 return Response.redirect(DOCS + target, 301);
110 }
111 // Pages and data requests, limited per address signed out and per
112 // session signed in (app/lib/front-door-limits.ts).
113 const limited = await pageLimited(env, request, pathname);
114 if (limited) return limited;
115 // Every page and data request says where its time went (Server-Timing)
116 // and keeps the reader's D1 bookmarks (app/lib/perf.server.ts).
117 const render = () => withRequestPerf(request, async () => finishResponse(request, await requestHandler(request)));
118 if (anonymousPage(request, pathname)) return servePublic(env, request, ctx, render);
Merge main into Artifacts Phase 2119 const answer = await render();
120 // A token the site refused (app/lib/website-token.ts) is told so as the
121 // API tells it: React Router drops the headers of what middleware throws.
122 if (answer.status === 401 && request.headers.has("authorization")) answer.headers.set("www-authenticate", TOKEN_CHALLENGE);
123 return answer;
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address124}
125
Docs: the inbox, what lands in it and why, its tabs and actions126/**
127 * Public pages as someone signed out sees them: the same for every such
128 * visitor, so kept in this data centre's cache. Reserved first segments
129 * (settings, sign-in, invitations and the like) and workspace pages (`-`)
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.130 * are never kept. A repository's kept page is served only while repos
131 * says the repository is still public: one made private or deleted is
132 * never served from any data centre's copy.
Docs: the inbox, what lands in it and why, its tabs and actions133 */
134const PUBLIC_TOP = /^\/(?:|_root\.data|pricing|explore|security|support|policies(?:\/[a-z-]+)?)(?:\.data)?$/;
135const PUBLIC_PROJECT =
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)136 /^\/(?!(?:settings|u|auth|oauth|integrations|new|invite|workspaces|device|verify|confirm-email|login|register|logout|forgot|reset|search|status|avatars|docs)\/)[^/]+\/(?!-\/|-$)[^/]+(?:\/(?:code|commits|issues|pulls|pull\/\d+|issues\/\d+|commit\/[0-9a-f]+|tree\/.+|blob\/.+))?(?:\.data)?$/;
Docs: the inbox, what lands in it and why, its tabs and actions137/** Fresh for this long; then served once more while a new copy is made. */
138const PUBLIC_FRESH_SECONDS = 30;
139const PUBLIC_STALE_SECONDS = 300;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9140/** The longest a cache lookup holds up a public page. */
141const CACHE_DEADLINE_MS = 2_000;
Docs: the inbox, what lands in it and why, its tabs and actions142
143function anonymousPage(request: Request, pathname: string): boolean {
144 if (request.method !== "GET") return false;
145 if (/(?:^|;\s*)g1t_session=/.test(request.headers.get("cookie") ?? "")) return false;
Merge main into Artifacts Phase 2146 // A token signs the request in (app/lib/website-token.ts): never kept, never served a kept page.
147 if (request.headers.has("authorization")) return false;
Docs: the inbox, what lands in it and why, its tabs and actions148 return PUBLIC_TOP.test(pathname) || PUBLIC_PROJECT.test(pathname);
149}
150
151async function servePublic(env: Env, request: Request, ctx: ExecutionContext, render: () => Promise<Response>): Promise<Response> {
152 const cache = (caches as unknown as { default: Cache }).default;
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today153 // Visitors asked about analytics first get a page that says so, kept apart.
154 const keyUrl = new URL(request.url);
155 if (visitorAsksFirst(request)) keyUrl.searchParams.set("_g1t_consent", "1");
156 const key = new Request(keyUrl, { method: "GET" });
Docs: the inbox, what lands in it and why, its tabs and actions157 const repository = PUBLIC_PROJECT.test(new URL(request.url).pathname) ? repositoryOfPage(new URL(request.url).pathname) : null;
158 // Asked alongside the cache, so a hit waits for one indexed read at most.
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9159 // A cache that does not answer is a miss: the page is drawn afresh.
160 const [cached, visible] = await Promise.all([
161 withDeadline("cache", CACHE_DEADLINE_MS, () => cache.match(key)).catch(() => undefined),
162 repository ? isStillPublic(env, repository) : Promise.resolve(true),
163 ]);
Docs: the inbox, what lands in it and why, its tabs and actions164 if (cached && !visible) {
165 ctx.waitUntil(cache.delete(key).then(() => undefined, () => undefined));
166 return render();
167 }
168 const keptAt = Number(cached?.headers.get("x-g1t-kept-at") ?? 0);
169 const age = Math.round((Date.now() - keptAt) / 1000);
170 const refresh = async () => {
171 const fresh = await render();
172 // Only a plain answer for everyone: nothing that sets a cookie or says
173 // it is private.
174 const cacheable =
175 (fresh.status === 200 || fresh.status === 404) &&
176 !fresh.headers.has("set-cookie") &&
177 !/private|no-store/.test(fresh.headers.get("cache-control") ?? "");
178 if (cacheable) {
179 const copy = new Response(fresh.clone().body, fresh);
180 copy.headers.set("x-g1t-kept-at", String(Date.now()));
181 copy.headers.set("cache-control", `public, max-age=${PUBLIC_STALE_SECONDS}`);
182 ctx.waitUntil(cache.put(key, copy));
183 }
184 return fresh;
185 };
186 if (cached && keptAt > 0 && age < PUBLIC_STALE_SECONDS) {
187 if (age >= PUBLIC_FRESH_SECONDS) ctx.waitUntil(refresh().then(() => undefined, () => undefined));
188 const answer = new Response(cached.body, cached);
189 answer.headers.delete("x-g1t-kept-at");
190 answer.headers.delete("cache-control");
191 answer.headers.set("server-timing", `cache;desc="hit, ${age}s old"`);
192 return answer;
193 }
194 return refresh();
195}
196
197/** Whether the repository is there and public; anything else, including no answer, is no. */
198async function isStillPublic(env: Env, repository: string): Promise<boolean> {
199 try {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9200 return await withDeadline("repos visibility", READ_DEADLINE_MS, async (signal) => {
201 const answer = await env.REPOS.fetch("https://service/rpc/visibility", {
202 method: "POST",
203 headers: { "content-type": "application/json" },
204 body: JSON.stringify({ paths: [repository] }),
205 signal,
206 });
207 return answer.ok && stillPublic(await answer.json(), repository);
Docs: the inbox, what lands in it and why, its tabs and actions208 });
209 } catch {
210 return false;
211 }
212}
213
214/**
215 * A git request, answered by the repos service. Its `Server-Timing` header
216 * gains `repos`: how long the answer took to start from here, so the time
217 * between this Worker and the repos service shows beside the steps the
218 * repos service reports.
219 */
220async function proxyGit(env: Env, request: Request): Promise<Response> {
221 const started = Date.now();
222 const answer = await env.REPOS.fetch(new Request(request, { redirect: "manual" }));
223 const response = new Response(answer.body, answer);
224 response.headers.append("server-timing", `repos;dur=${Date.now() - started}`);
225 return response;
226}
227
228/**
229 * A registry request, answered by the packages service as it is: its
230 * redirects (a large blob sent to storage) go back to the client, which
Merge branch 'worktree-agent-a8385d293d42c913a'231 * follows them itself. One that found nothing under a workspace's old name
232 * or an alias staff set (`g1t` for `flagon-io`) is sent to the same path
233 * under the workspace's name: only the not-found answer pays for the lookup.
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address234 * Every answer runs nothing in a browser (app/lib/content-safety.ts): what
235 * a registry serves is its publisher's, on this origin.
Docs: the inbox, what lands in it and why, its tabs and actions236 */
237async function proxyPackages(env: Env, request: Request): Promise<Response> {
238 const started = Date.now();
239 const answer = await env.PACKAGES.fetch(new Request(request, { redirect: "manual" }));
Merge branch 'worktree-agent-a8385d293d42c913a'240 const moved = answer.status === 404 ? await registryMoved(env, request) : null;
241 const response = moved ?? new Response(answer.body, answer);
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address242 hardenRegistryHeaders(response.headers);
Docs: the inbox, what lands in it and why, its tabs and actions243 response.headers.append("server-timing", `packages;dur=${Date.now() - started}`);
244 return response;
245}
246
Merge branch 'worktree-agent-a8385d293d42c913a'247/** Where a registry request under an alias or old name goes now, or null. */
248async function registryMoved(env: Env, request: Request): Promise<Response | null> {
249 const url = new URL(request.url);
250 const named = registryWorkspace(url.pathname);
251 if (!named || !isNamespaceShaped(named.slug)) return null;
252 let current: string | null = null;
253 try {
254 current = await identityClient(env.IDENTITY).resolveSlug(named.slug);
255 } catch {
256 return null;
257 }
258 if (!current || current === named.slug) return null;
259 const get = request.method === "GET" || request.method === "HEAD";
260 // 308 keeps a publish a PUT, for the clients that follow it.
261 return new Response(null, { status: get ? 301 : 308, headers: { location: named.under(current) + url.search } });
Docs: the inbox, what lands in it and why, its tabs and actions262}

This file's history is long; its oldest lines are credited to the oldest commit read.