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

Automations: rules in .g1t/automations that act when something happens1import { CircleX, KeyRound } from "lucide-react";
2import { Form, redirect } from "react-router";
3
4import { decodeOAuthClient, isRegisteredRedirect } from "@g1t/contracts";
5
6import type { Route } from "./+types/oauth-authorize";
Social cards for every page: og.g1t.sh7import { page } from "../lib/meta";
Merge branch 'worktree-agent-a985c01f43538af01'8import { ErrorText, SubmitButton, usePending } from "../components/ui";
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step9import { ScopeChecklist } from "../components/token-scopes";
Automations: rules in .g1t/automations that act when something happens10import { identity } from "../lib/services.server";
Merge branch 'worktree-agent-aaf03bdceac799c89'11import { addresses } from "../lib/addresses.server";
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step12import { consentedScopes, requestedScopes } from "../lib/token-scopes";
Automations: rules in .g1t/automations that act when something happens13import { assertSameOrigin, requireUser } from "../lib/session.server";
14
Social cards for every page: og.g1t.sh15export function meta(args: Route.MetaArgs) {
16 return page(args, { title: "Sign in to an application · g1t" });
Automations: rules in .g1t/automations that act when something happens17}
18
19type Checked =
20 | { ok: false; problem: string }
21 | {
22 ok: true;
23 clientId: string;
24 clientName: string;
25 redirectUri: string;
26 codeChallenge: string;
27 state: string;
28 };
29
30/**
31 * Checks an authorization request. A request that names a client or a
32 * redirect address we cannot vouch for is never redirected anywhere; the
33 * person is told instead.
34 */
35function check(params: URLSearchParams | FormData): Checked {
36 const get = (key: string) => String(params.get(key) ?? "");
37 const client = decodeOAuthClient(get("client_id"));
38 if (!client) {
39 return { ok: false, problem: "This sign-in link names an application g1t does not recognise." };
40 }
41 const redirectUri = get("redirect_uri") || client.redirectUris[0];
42 if (!isRegisteredRedirect(client, redirectUri)) {
43 return {
44 ok: false,
45 problem: "This sign-in link would send you somewhere the application did not register.",
46 };
47 }
48 if (get("response_type") !== "code") {
49 return { ok: false, problem: "This sign-in link asks for a kind of access g1t does not offer." };
50 }
51 if (!get("code_challenge") || get("code_challenge_method") !== "S256") {
52 return {
53 ok: false,
54 problem: "This application did not protect its sign-in with PKCE (S256), which g1t requires.",
55 };
56 }
57 return {
58 ok: true,
59 clientId: get("client_id"),
60 clientName: client.name,
61 redirectUri,
62 codeChallenge: get("code_challenge"),
63 state: get("state"),
64 };
65}
66
67/** The application's redirect address with the outcome added to it. */
68function callback(redirectUri: string, params: Record<string, string>): string {
69 const url = new URL(redirectUri);
70 for (const [key, value] of Object.entries(params)) {
71 if (value) url.searchParams.set(key, value);
72 }
73 return url.toString();
74}
75
76/** Where an application sends a person to approve its sign-in. */
77export function loader({ request, context }: Route.LoaderArgs) {
78 const user = requireUser(context, request);
79 const { searchParams } = new URL(request.url);
80 return {
81 user,
82 request: check(searchParams),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step83 // What the application asked for; nothing usable means the default set.
84 requested: requestedScopes(searchParams.get("scope")),
Automations: rules in .g1t/automations that act when something happens85 // Sent back unchanged when the person decides.
86 query: Object.fromEntries(searchParams),
87 };
88}
89
90export async function action({ request, context }: Route.ActionArgs) {
91 assertSameOrigin(request);
92 const user = requireUser(context, request);
93 const form = await request.formData();
94 const checked = check(form);
95 if (!checked.ok) return null;
96 if (form.get("decision") !== "approve") {
97 throw redirect(
98 callback(checked.redirectUri, { error: "access_denied", state: checked.state }),
99 );
100 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step101 // Only what the application asked for, never more, whatever the form says.
102 const scopes = consentedScopes(form, requestedScopes(String(form.get("scope") ?? "")));
103 if (scopes.length === 0) {
104 return { error: "Leave at least one box ticked, or deny." };
105 }
Merge branch 'worktree-agent-af9f0f386d52b6a73'106 let code: string;
107 try {
108 ({ code } = await identity.oauthAuthorize(user, {
109 clientId: checked.clientId,
110 clientName: checked.clientName,
111 redirectUri: checked.redirectUri,
112 codeChallenge: checked.codeChallenge,
113 scopes,
114 }));
115 } catch (error) {
116 // The service's own words are for the log; the person can try again.
117 console.warn("oauth-authorize:", error);
118 return { error: "g1t could not approve this sign-in just now. Try again in a moment." };
119 }
Merge branch 'worktree-agent-aaf03bdceac799c89'120 // `iss` is the API's origin, the issuer its metadata names (RFC 9207).
Automations: rules in .g1t/automations that act when something happens121 throw redirect(
Merge branch 'worktree-agent-aaf03bdceac799c89'122 callback(checked.redirectUri, { code, state: checked.state, iss: addresses().api }),
Automations: rules in .g1t/automations that act when something happens123 );
124}
125
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step126export default function Authorize({ loaderData, actionData }: Route.ComponentProps) {
127 const { user, request, requested, query } = loaderData;
Merge branch 'worktree-agent-a985c01f43538af01'128 // Either answer turns both buttons off until it is in.
129 const deciding = usePending();
Automations: rules in .g1t/automations that act when something happens130
131 if (!request.ok) {
132 return (
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.133 <main className="mx-auto max-w-md pt-10 text-center">
Automations: rules in .g1t/automations that act when something happens134 <CircleX size={40} className="mx-auto text-muted" />
135 <h1 className="mt-6 text-2xl font-semibold tracking-tight">This link cannot be used</h1>
136 <p className="mt-2 text-muted">{request.problem}</p>
137 <p className="mt-2 text-sm text-faint">Nothing was given access to your account.</p>
138 </main>
139 );
140 }
141
142 const destination = new URL(request.redirectUri);
143 return (
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.144 <main className="mx-auto max-w-md pt-8">
Automations: rules in .g1t/automations that act when something happens145 <KeyRound size={36} className="text-accent" />
146 <h1 className="mt-6 text-2xl font-semibold tracking-tight">
147 Sign in to {request.clientName}
148 </h1>
149 <p className="mt-2 text-muted">
150 <span className="font-medium text-fg">{request.clientName}</span> wants to act as{" "}
151 <span className="font-mono font-medium text-fg">{user.username}</span> on g1t.
152 </p>
153
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step154 <Form method="post" className="mt-6 space-y-6">
155 {Object.entries(query).map(([name, value]) => (
156 <input key={name} type="hidden" name={name} value={value} />
157 ))}
158 <section>
159 <h2 className="text-sm font-medium">It will be able to</h2>
160 <p className="mt-0.5 text-xs text-faint">
161 Everywhere you can, as you. Untick anything you would rather it could not do.
162 </p>
163 <div className="mt-3">
164 <ScopeChecklist initial={requested} only={requested} allowFull={false} />
165 </div>
166 </section>
167 <dl className="rounded-xl border border-line bg-surface p-4 text-sm">
Automations: rules in .g1t/automations that act when something happens168 <dt className="text-xs text-faint">You will be sent back to</dt>
169 <dd className="mt-0.5 font-mono text-[0.8125rem] break-all">
170 {destination.protocol === "https:" || destination.protocol === "http:"
171 ? destination.host + destination.pathname
172 : request.redirectUri}
173 </dd>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step174 </dl>
175 <div>
176 <p className="text-xs text-faint">
177 Approve only if you started this from {request.clientName} yourself. You can change
178 what it may do, or sign it out, in Settings.
179 </p>
180 <ErrorText>{actionData?.error}</ErrorText>
181 <div className="mt-4 flex gap-2">
Merge branch 'worktree-agent-a985c01f43538af01'182 <SubmitButton variant="accent" name="decision" value="approve" pending="Approving…" disabled={deciding}>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step183 Approve
Merge branch 'worktree-agent-a985c01f43538af01'184 </SubmitButton>
185 <SubmitButton variant="quiet" name="decision" value="deny" pending="Denying…" disabled={deciding}>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step186 Deny
Merge branch 'worktree-agent-a985c01f43538af01'187 </SubmitButton>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step188 </div>
Automations: rules in .g1t/automations that act when something happens189 </div>
190 </Form>
191 </main>
192 );
193}

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