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

Initial g1t: services, event bus, intents and attempts1import {
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";
Initial g1t: services, event bus, intents and attempts10
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";
API and MCP server, Rust identity service, registration, site redesign17import { identity } from "./services.server";
18
Initial g1t: services, event bus, intents and attempts19const SESSION_COOKIE = "g1t_session";
20const SESSION_TTL_SECONDS = 30 * 24 * 60 * 60;
21
22const viewerContext = createContext<Viewer>(null);
23
24function sessionToken(request: Request): string | null {
25 const cookies = request.headers.get("cookie") ?? "";
26 const match = new RegExp(`(?:^|; )${SESSION_COOKIE}=([0-9a-f]{64})`).exec(cookies);
27 return match ? match[1] : null;
28}
29
30function sessionCookie(value: string, maxAge: number): string {
31 return `${SESSION_COOKIE}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`;
32}
Agents as a team: lifecycle, merge queue, billing and a new shell33
34/**
35 * Root middleware: resolves the signed-in user once per request.
36 *
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)37 * An account that has not confirmed its email address is sent to confirm
38 * it, from any page but the few that needs (lib/confirm-gate.ts).
39 *
Agents as a team: lifecycle, merge queue, billing and a new shell40 * Everything on g1t lives in a workspace, so a confirmed account with none
41 * is sent to create one, from wherever it was going, and returned there
42 * afterwards.
43 */
Initial g1t: services, event bus, intents and attempts44export const viewerMiddleware: MiddlewareFunction<Response> = async ({
45 request,
46 context,
47}) => {
48 const token = sessionToken(request);
Agents as a team: lifecycle, merge queue, billing and a new shell49 if (!token) return;
50 const viewer = await identity.userForSession(token);
51 context.set(viewerContext, viewer);
52
53 const { pathname, search } = new URL(request.url);
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)54 // An account that has not confirmed its email address does that first,
55 // from wherever it was going (lib/confirm-gate.ts).
56 const gated = confirmGate(pathname, search, viewer);
57 if (gated) throw redirect(gated);
Chat and workspace agents: channels, DMs and named agents you talk to58 // A member without Code access in a workspace (docs/WORKSPACE.md,
59 // "Members without Code"): their Home in place of Mission control, and
60 // the page that says to ask an owner in place of anything of Code's.
61 // The services enforce it too; this keeps the site from offering it.
62 const noCode = (viewer?.workspaces ?? []).filter((m) => !hasCodeAccess(m)).map((m) => m.slug.toLowerCase());
63 if (request.method === "GET" && noCode.length > 0) {
64 const chosen = chosenWorkspace(viewer?.workspaces ?? [], readCookie(request.headers.get("cookie"), WORKSPACE_COOKIE));
65 const around = codeGate(pathname, search, noCode, chosen?.slug ?? null);
66 if (around) throw redirect(around);
67 }
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)68 // Nobody uses g1t without a workspace: someone with none makes one, or
69 // answers an invitation to one, before anything else (lib/workspace-gate.ts).
70 // Data requests too, so a page is never loaded behind its back.
71 if (request.method === "GET") {
72 const around = workspaceGate(pageOf(pathname), search, viewer);
73 if (around) throw redirect(around);
Initial g1t: services, event bus, intents and attempts74 }
75};
76
77type Context = Readonly<RouterContextProvider>;
78
79export function getViewer(context: Context): Viewer {
80 return context.get(viewerContext);
81}
82
Agents as a team: lifecycle, merge queue, billing and a new shell83/** The viewer's role in a workspace, or null if they are not a member. */
84export function roleIn(viewer: Viewer, slug: string): Role | null {
85 const wanted = slug.toLowerCase();
86 return (
87 viewer?.workspaces?.find((membership) => membership.slug === wanted)?.role ?? null
88 );
89}
90
Merge main (membership, two-factor, GitHub repo roles) into tokens91/** Whether the viewer may manage a workspace's billing: an owner or a billing manager. */
92export function managesBilling(viewer: Viewer, slug: string): boolean {
93 const membership = viewer?.workspaces?.find((m) => m.slug === slug.toLowerCase());
94 return membership?.role === "owner" || !!membership?.org_roles?.includes("billing_manager");
95}
96
97/** Whether the viewer may manage security across a workspace: an owner or a security manager. */
98export function managesSecurity(viewer: Viewer, slug: string): boolean {
99 const membership = viewer?.workspaces?.find((m) => m.slug === slug.toLowerCase());
100 return membership?.role === "owner" || !!membership?.org_roles?.includes("security_manager");
101}
102
Initial g1t: services, event bus, intents and attempts103export function requireUser(context: Context, request: Request): User {
104 const viewer = getViewer(context);
105 if (!viewer) {
Device sign-in replaces registering and minting tokens over the API106 // Keep the query string: a device sign-in link carries its code there.
107 const { pathname, search } = new URL(request.url);
108 throw redirect(`/login?next=${encodeURIComponent(pathname + search)}`);
Initial g1t: services, event bus, intents and attempts109 }
110 return viewer;
111}
112
API and MCP server, Rust identity service, registration, site redesign113/**
114 * Where to go after signing in. Only same-site paths are honoured, so
115 * `next` cannot redirect off g1t.
116 */
117export function nextPath(request: Request): string {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put118 return safeNext(new URL(request.url).searchParams.get("next"));
API and MCP server, Rust identity service, registration, site redesign119}
120
Initial g1t: services, event bus, intents and attempts121/** `Set-Cookie` value that starts a session. */
122export function startSession(token: string): string {
123 return sessionCookie(token, SESSION_TTL_SECONDS);
124}
125
126/** Ends the session and returns the `Set-Cookie` value that clears it. */
127export async function endSession(request: Request): Promise<string> {
128 const token = sessionToken(request);
API and MCP server, Rust identity service, registration, site redesign129 if (token) await identity.signOut(token);
Initial g1t: services, event bus, intents and attempts130 return sessionCookie("", 0);
131}
132
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look133/** The session token the request carries, for proof of a recent sign-in. */
134export function sessionTokenOf(request: Request): string | null {
135 return sessionToken(request);
136}
137
138/** The visitor's IP address, as Cloudflare saw it, for rate limits. */
139export function clientOf(request: Request): string | null {
140 return request.headers.get("cf-connecting-ip");
141}
142
Initial g1t: services, event bus, intents and attempts143/** Rejects cross-site form posts; call at the top of every action. */
144export function assertSameOrigin(request: Request): void {
145 const origin = request.headers.get("origin");
146 if (origin && origin !== new URL(request.url).origin) {
147 throw new Response("Cross-origin request rejected", { status: 403 });
148 }
149}
150
151/** The value of a service result, or the matching HTTP error. */
152export function unwrap<T>(result: Result<T>): T {
153 if (result.ok) return result.value;
154 throw data(result.error.message, { status: httpStatus(result.error) });
155}

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