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.
| Commit pages credit co-authors instead of printing their trailers | 1 | import { CircleX, KeyRound } from "lucide-react"; |
| 2 | import { Form, redirect } from "react-router"; | |
| 3 | ||
| 4 | import { decodeOAuthClient, isRegisteredRedirect } from "@g1t/contracts"; | |
| 5 | ||
| 6 | import type { Route } from "./+types/oauth-authorize"; | |
| Social cards for every page: og.g1t.sh | 7 | import { page } from "../lib/meta"; |
| Merge branch 'worktree-agent-a985c01f43538af01' | 8 | import { 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 step | 9 | import { ScopeChecklist } from "../components/token-scopes"; |
| Commit pages credit co-authors instead of printing their trailers | 10 | import { identity } from "../lib/services.server"; |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 11 | import { 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 step | 12 | import { consentedScopes, requestedScopes } from "../lib/token-scopes"; |
| Commit pages credit co-authors instead of printing their trailers | 13 | import { assertSameOrigin, requireUser } from "../lib/session.server"; |
| 14 | ||
| Social cards for every page: og.g1t.sh | 15 | export function meta(args: Route.MetaArgs) { |
| 16 | return page(args, { title: "Sign in to an application · g1t" }); | |
| Commit pages credit co-authors instead of printing their trailers | 17 | } |
| 18 | ||
| 19 | type 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 | */ | |
| 35 | function 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. */ | |
| 68 | function 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. */ | |
| 77 | export 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 step | 83 | // What the application asked for; nothing usable means the default set. |
| 84 | requested: requestedScopes(searchParams.get("scope")), | |
| Commit pages credit co-authors instead of printing their trailers | 85 | // Sent back unchanged when the person decides. |
| 86 | query: Object.fromEntries(searchParams), | |
| 87 | }; | |
| 88 | } | |
| 89 | ||
| 90 | export 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 step | 101 | // 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). |
| Commit pages credit co-authors instead of printing their trailers | 121 | throw redirect( |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 122 | callback(checked.redirectUri, { code, state: checked.state, iss: addresses().api }), |
| Commit pages credit co-authors instead of printing their trailers | 123 | ); |
| 124 | } | |
| 125 | ||
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 126 | export 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(); | |
| Commit pages credit co-authors instead of printing their trailers | 130 | |
| 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"> |
| Commit pages credit co-authors instead of printing their trailers | 134 | <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"> |
| Commit pages credit co-authors instead of printing their trailers | 145 | <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 step | 154 | <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"> | |
| Commit pages credit co-authors instead of printing their trailers | 168 | <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 step | 174 | </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 step | 183 | 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 step | 186 | 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 step | 188 | </div> |
| Commit pages credit co-authors instead of printing their trailers | 189 | </div> |
| 190 | </Form> | |
| 191 | </main> | |
| 192 | ); | |
| 193 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.