Skip to content
175 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.

Integrations: your own model provider, alerts that open issues, tickets agents read1import {
2 type MiddlewareFunction,
3 type RouterContextProvider,
4 createContext,
5 data,
6 redirect,
7} from "react-router";
8
Chat and workspace agents: channels, DMs and named agents you talk to9import { type Result, type Role, type User, type Viewer, hasCodeAccess, httpStatus } from "@g1t/contracts";
Integrations: your own model provider, alerts that open issues, tickets agents read10
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)11import { confirmGate, pageOf } from "./confirm-gate";
12import { workspaceGate } from "./workspace-gate";
Chat and workspace agents: channels, DMs and named agents you talk to13import { readCookie } from "./mission";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put14import { safeNext } from "./next";
Chat and workspace agents: channels, DMs and named agents you talk to15import { WORKSPACE_COOKIE, chosenWorkspace } from "./workspace-choice";
16import { codeGate } from "./workspace-nav";
Integrations: your own model provider, alerts that open issues, tickets agents read17import { identity } from "./services.server";
Automation driving a browser can use g1t.sh as a person by sending their access token as Authorization: Bearer on every request, once the token has Use the website as you ticked on its form: no cookie or session is made, the token is checked on each request and refused at once when deleted, expired or revoked, never read from a query string or cookie, counted at the API's limit per token, and never served or kept as a public page, while tokens, two-factor authentication, emails, keys, the account, applications, device and application sign-ins, workspace deletion and transfer, and payment pages answer This needs you to sign in, a token without the permission is signed out with a 401 for data requests and form posts, and form posts from other sites are refused for cookies and tokens alike.18import { TOKEN_CHALLENGE, bearerToken, tokenVerdict } from "./website-token";
19import { crossOrigin } from "./same-origin";
Integrations: your own model provider, alerts that open issues, tickets agents read20
21const SESSION_COOKIE = "g1t_session";
22const SESSION_TTL_SECONDS = 30 * 24 * 60 * 60;
23
24const viewerContext = createContext<Viewer>(null);
25
26function sessionToken(request: Request): string | null {
Automation driving a browser can use g1t.sh as a person by sending their access token as Authorization: Bearer on every request, once the token has Use the website as you ticked on its form: no cookie or session is made, the token is checked on each request and refused at once when deleted, expired or revoked, never read from a query string or cookie, counted at the API's limit per token, and never served or kept as a public page, while tokens, two-factor authentication, emails, keys, the account, applications, device and application sign-ins, workspace deletion and transfer, and payment pages answer This needs you to sign in, a token without the permission is signed out with a 401 for data requests and form posts, and form posts from other sites are refused for cookies and tokens alike.27 // A request with a token is the token's alone (lib/website-token.ts).
28 if (bearerToken(request) !== null) return null;
Integrations: your own model provider, alerts that open issues, tickets agents read29 const cookies = request.headers.get("cookie") ?? "";
30 const match = new RegExp(`(?:^|; )${SESSION_COOKIE}=([0-9a-f]{64})`).exec(cookies);
31 return match ? match[1] : null;
32}
33
34function sessionCookie(value: string, maxAge: number): string {
35 return `${SESSION_COOKIE}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`;
36}
37
38/**
39 * Root middleware: resolves the signed-in user once per request.
40 *
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)41 * An account that has not confirmed its email address is sent to confirm
42 * it, from any page but the few that needs (lib/confirm-gate.ts).
43 *
Integrations: your own model provider, alerts that open issues, tickets agents read44 * Everything on g1t lives in a workspace, so a confirmed account with none
45 * is sent to create one, from wherever it was going, and returned there
46 * afterwards.
Automation driving a browser can use g1t.sh as a person by sending their access token as Authorization: Bearer on every request, once the token has Use the website as you ticked on its form: no cookie or session is made, the token is checked on each request and refused at once when deleted, expired or revoked, never read from a query string or cookie, counted at the API's limit per token, and never served or kept as a public page, while tokens, two-factor authentication, emails, keys, the account, applications, device and application sign-ins, workspace deletion and transfer, and payment pages answer This needs you to sign in, a token without the permission is signed out with a 401 for data requests and form posts, and form posts from other sites are refused for cookies and tokens alike.47 *
48 * Automation can send an access token as `Authorization: Bearer` in place
49 * of the cookie, when its owner let it use the website; the rules are in
50 * lib/website-token.ts.
Integrations: your own model provider, alerts that open issues, tickets agents read51 */
Automation driving a browser can use g1t.sh as a person by sending their access token as Authorization: Bearer on every request, once the token has Use the website as you ticked on its form: no cookie or session is made, the token is checked on each request and refused at once when deleted, expired or revoked, never read from a query string or cookie, counted at the API's limit per token, and never served or kept as a public page, while tokens, two-factor authentication, emails, keys, the account, applications, device and application sign-ins, workspace deletion and transfer, and payment pages answer This needs you to sign in, a token without the permission is signed out with a 401 for data requests and form posts, and form posts from other sites are refused for cookies and tokens alike.52export const viewerMiddleware: MiddlewareFunction<Response> = async ({ request, context }, next) => {
53 const verdict = await tokenVerdict(request, (token) => identity.userForAccessToken(token));
54 if (verdict.kind === "refused") {
55 const headers: HeadersInit = verdict.status === 401 ? { "www-authenticate": TOKEN_CHALLENGE } : {};
56 throw data(verdict.body, { status: verdict.status, headers });
57 }
58 if (verdict.kind === "signed-out") {
59 // The page as anyone signed out sees it, saying the token was not taken.
60 const response = await next();
61 response.headers.set("www-authenticate", TOKEN_CHALLENGE);
62 return response;
63 }
64 let viewer: Viewer;
65 if (verdict.kind === "signed-in") {
66 viewer = verdict.user;
67 } else {
68 const token = sessionToken(request);
69 if (!token) return;
70 viewer = await identity.userForSession(token);
71 }
Integrations: your own model provider, alerts that open issues, tickets agents read72 context.set(viewerContext, viewer);
73
74 const { pathname, search } = new URL(request.url);
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)75 // An account that has not confirmed its email address does that first,
76 // from wherever it was going (lib/confirm-gate.ts).
77 const gated = confirmGate(pathname, search, viewer);
78 if (gated) throw redirect(gated);
Chat and workspace agents: channels, DMs and named agents you talk to79 // A member without Code access in a workspace (docs/WORKSPACE.md,
80 // "Members without Code"): their Home in place of Mission control, and
81 // the page that says to ask an owner in place of anything of Code's.
82 // The services enforce it too; this keeps the site from offering it.
83 const noCode = (viewer?.workspaces ?? []).filter((m) => !hasCodeAccess(m)).map((m) => m.slug.toLowerCase());
84 if (request.method === "GET" && noCode.length > 0) {
85 const chosen = chosenWorkspace(viewer?.workspaces ?? [], readCookie(request.headers.get("cookie"), WORKSPACE_COOKIE));
86 const around = codeGate(pathname, search, noCode, chosen?.slug ?? null);
87 if (around) throw redirect(around);
88 }
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)89 // Nobody uses g1t without a workspace: someone with none makes one, or
90 // answers an invitation to one, before anything else (lib/workspace-gate.ts).
91 // Data requests too, so a page is never loaded behind its back.
92 if (request.method === "GET") {
93 const around = workspaceGate(pageOf(pathname), search, viewer);
94 if (around) throw redirect(around);
Integrations: your own model provider, alerts that open issues, tickets agents read95 }
96};
97
98type Context = Readonly<RouterContextProvider>;
99
100export function getViewer(context: Context): Viewer {
101 return context.get(viewerContext);
102}
103
104/** The viewer's role in a workspace, or null if they are not a member. */
105export function roleIn(viewer: Viewer, slug: string): Role | null {
106 const wanted = slug.toLowerCase();
107 return (
108 viewer?.workspaces?.find((membership) => membership.slug === wanted)?.role ?? null
109 );
110}
111
Merge main (membership, two-factor, GitHub repo roles) into tokens112/** Whether the viewer may manage a workspace's billing: an owner or a billing manager. */
113export function managesBilling(viewer: Viewer, slug: string): boolean {
114 const membership = viewer?.workspaces?.find((m) => m.slug === slug.toLowerCase());
115 return membership?.role === "owner" || !!membership?.org_roles?.includes("billing_manager");
116}
117
118/** Whether the viewer may manage security across a workspace: an owner or a security manager. */
119export function managesSecurity(viewer: Viewer, slug: string): boolean {
120 const membership = viewer?.workspaces?.find((m) => m.slug === slug.toLowerCase());
121 return membership?.role === "owner" || !!membership?.org_roles?.includes("security_manager");
122}
123
Integrations: your own model provider, alerts that open issues, tickets agents read124export function requireUser(context: Context, request: Request): User {
125 const viewer = getViewer(context);
126 if (!viewer) {
127 // Keep the query string: a device sign-in link carries its code there.
128 const { pathname, search } = new URL(request.url);
129 throw redirect(`/login?next=${encodeURIComponent(pathname + search)}`);
130 }
131 return viewer;
132}
133
134/**
135 * Where to go after signing in. Only same-site paths are honoured, so
136 * `next` cannot redirect off g1t.
137 */
138export function nextPath(request: Request): string {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put139 return safeNext(new URL(request.url).searchParams.get("next"));
Integrations: your own model provider, alerts that open issues, tickets agents read140}
141
142/** `Set-Cookie` value that starts a session. */
143export function startSession(token: string): string {
144 return sessionCookie(token, SESSION_TTL_SECONDS);
145}
146
147/** Ends the session and returns the `Set-Cookie` value that clears it. */
148export async function endSession(request: Request): Promise<string> {
149 const token = sessionToken(request);
150 if (token) await identity.signOut(token);
151 return sessionCookie("", 0);
152}
153
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look154/** The session token the request carries, for proof of a recent sign-in. */
155export function sessionTokenOf(request: Request): string | null {
156 return sessionToken(request);
157}
158
159/** The visitor's IP address, as Cloudflare saw it, for rate limits. */
160export function clientOf(request: Request): string | null {
161 return request.headers.get("cf-connecting-ip");
162}
163
Integrations: your own model provider, alerts that open issues, tickets agents read164/** Rejects cross-site form posts; call at the top of every action. */
165export function assertSameOrigin(request: Request): void {
Automation driving a browser can use g1t.sh as a person by sending their access token as Authorization: Bearer on every request, once the token has Use the website as you ticked on its form: no cookie or session is made, the token is checked on each request and refused at once when deleted, expired or revoked, never read from a query string or cookie, counted at the API's limit per token, and never served or kept as a public page, while tokens, two-factor authentication, emails, keys, the account, applications, device and application sign-ins, workspace deletion and transfer, and payment pages answer This needs you to sign in, a token without the permission is signed out with a 401 for data requests and form posts, and form posts from other sites are refused for cookies and tokens alike.166 if (crossOrigin(request)) {
Integrations: your own model provider, alerts that open issues, tickets agents read167 throw new Response("Cross-origin request rejected", { status: 403 });
168 }
169}
170
171/** The value of a service result, or the matching HTTP error. */
172export function unwrap<T>(result: Result<T>): T {
173 if (result.ok) return result.value;
174 throw data(result.error.message, { status: httpStatus(result.error) });
175}

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