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.
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1 | import { CheckCircle2, Ticket } from "lucide-react"; |
| 2 | import { Form, Link, data, redirect } from "react-router"; | |
| 3 | ||
| 4 | import type { InvitePreview } from "@g1t/contracts"; | |
| API and MCP server, Rust identity service, registration, site redesign | 5 | |
| 6 | import type { Route } from "./+types/register"; | |
| Social cards for every page: og.g1t.sh | 7 | import { page } from "../lib/meta"; |
| API and MCP server, Rust identity service, registration, site redesign | 8 | import { AuthCard } from "../components/auth-card"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 9 | import { ContinueWithGithub, OrDivider } from "../components/github"; |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 10 | import { Honeypot } from "../components/honeypot"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 11 | import { githubSignInEnabled } from "../lib/github.server"; |
| Merge branch 'worktree-agent-a985c01f43538af01' | 12 | import { Avatar, Button, ErrorText, Field, Input, SubmitButton } from "../components/ui"; |
| API and MCP server, Rust identity service, registration, site redesign | 13 | import { identity } from "../lib/services.server"; |
| Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm | 14 | import { cleanCode, cleanProof, invitePath, looksAutomated, sharedDomainsHint, sharedInviteLine } from "../lib/invites"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 15 | import { clientKey, registrationMode } from "../lib/registration.server"; |
| API and MCP server, Rust identity service, registration, site redesign | 16 | import { |
| 17 | assertSameOrigin, | |
| 18 | getViewer, | |
| Device sign-in replaces registering and minting tokens over the API | 19 | nextPath, |
| API and MCP server, Rust identity service, registration, site redesign | 20 | startSession, |
| 21 | } from "../lib/session.server"; | |
| 22 | ||
| Social cards for every page: og.g1t.sh | 23 | export function meta(args: Route.MetaArgs) { |
| 24 | return page(args, { title: "Create an account · g1t" }); | |
| API and MCP server, Rust identity service, registration, site redesign | 25 | } |
| 26 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 27 | export async function loader({ request, context }: Route.LoaderArgs) { |
| Device sign-in replaces registering and minting tokens over the API | 28 | if (getViewer(context)) throw redirect(nextPath(request)); |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 29 | const inviteOnly = (await registrationMode()) !== "open"; |
| 30 | const code = cleanCode(new URL(request.url).searchParams.get("invite")); | |
| 31 | // The invite is checked before anything else on the page: without a good | |
| 32 | // one, there is no form to fill in. | |
| 33 | const checked = code ? await identity.checkInvite(code, clientKey(request)) : null; | |
| 34 | const invite: InvitePreview | null = checked?.ok ? checked.value : null; | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 35 | // A good invite is used on its own page, which knows whom it is from and |
| Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038) | 36 | // where it leads; it signs up, joins and lands in one go. A shared link |
| 37 | // for a group signs up here: it joins nothing, and the form says which | |
| 38 | // group it is for. | |
| Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm | 39 | // The invite email's proof goes with it, so the address it proves stays |
| 40 | // confirmed there. | |
| 41 | if (invite && code && !invite.sharedLabel) { | |
| 42 | throw redirect(invitePath(code, cleanProof(new URL(request.url).searchParams.get("proof")))); | |
| 43 | } | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 44 | // Signing up with GitHub carries the invite code and `next` through it. |
| 45 | const params = new URLSearchParams(); | |
| 46 | if (code) params.set("invite", code); | |
| 47 | if (nextPath(request) !== "/") params.set("next", nextPath(request)); | |
| 48 | const query = params.toString(); | |
| 49 | return { | |
| 50 | inviteOnly, | |
| 51 | code, | |
| 52 | invite, | |
| 53 | inviteError: checked && !checked.ok ? checked.error.message : null, | |
| 54 | github: await githubSignInEnabled(), | |
| 55 | githubUrl: query ? `/auth/github?${query}` : "/auth/github", | |
| 56 | started: Date.now(), | |
| 57 | }; | |
| API and MCP server, Rust identity service, registration, site redesign | 58 | } |
| 59 | ||
| 60 | export async function action({ request }: Route.ActionArgs) { | |
| 61 | assertSameOrigin(request); | |
| 62 | const form = await request.formData(); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 63 | const client = clientKey(request); |
| 64 | ||
| 65 | if (form.get("intent") === "request-access") { | |
| 66 | // A bot gets the same thanks as a person, and nothing is kept. | |
| 67 | if (looksAutomated(form)) return { requested: true }; | |
| 68 | const result = await identity.requestAccess( | |
| 69 | String(form.get("email") ?? ""), | |
| 70 | String(form.get("about") ?? ""), | |
| 71 | client, | |
| 72 | ); | |
| 73 | if (!result.ok) return data({ requestError: result.error.message }, { status: 422 }); | |
| 74 | return { requested: true }; | |
| 75 | } | |
| 76 | ||
| 77 | const code = cleanCode(String(form.get("invite") ?? "")); | |
| 78 | if (looksAutomated(form)) return data({ error: "Something went wrong. Try again." }, { status: 422 }); | |
| 79 | // The invite first, so its problem is the one shown. | |
| 80 | let joins: string | null = null; | |
| 81 | if (code) { | |
| 82 | const checked = await identity.checkInvite(code, client); | |
| 83 | if (!checked.ok) return data({ error: checked.error.message }, { status: 422 }); | |
| 84 | joins = checked.value.workspace?.slug ?? null; | |
| 85 | } | |
| API and MCP server, Rust identity service, registration, site redesign | 86 | const result = await identity.register( |
| 87 | String(form.get("username") ?? ""), | |
| 88 | String(form.get("email") ?? ""), | |
| 89 | String(form.get("password") ?? ""), | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 90 | code || null, |
| 91 | client, | |
| API and MCP server, Rust identity service, registration, site redesign | 92 | ); |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 93 | if (!result.ok) return data({ error: result.error.message }, { status: 422 }); |
| 94 | // An invite to a workspace lands in it; anything else goes where it was going. | |
| 95 | const next = nextPath(request); | |
| 96 | throw redirect(joins && next === "/" ? `/${joins}` : next, { | |
| API and MCP server, Rust identity service, registration, site redesign | 97 | headers: { "set-cookie": startSession(result.value.sessionToken) }, |
| 98 | }); | |
| 99 | } | |
| 100 | ||
| Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038) | 101 | /** Who sent the invite and what it joins, above the form; for a shared link, the group it is for. */ |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 102 | function InvitedBy({ invite }: { invite: InvitePreview }) { |
| 103 | const from = invite.invitedBy; | |
| Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038) | 104 | const group = sharedInviteLine(invite.sharedLabel); |
| 105 | if (group) { | |
| 106 | return ( | |
| 107 | <div className="mb-6 flex items-center gap-3 rounded-lg border border-accent/30 bg-accent/5 p-3" role="status"> | |
| 108 | <span className="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-accent/15 text-accent"> | |
| 109 | <Ticket size={18} /> | |
| 110 | </span> | |
| 111 | <p className="min-w-0 text-sm leading-5 font-medium text-fg">{group}</p> | |
| 112 | </div> | |
| 113 | ); | |
| 114 | } | |
| API and MCP server, Rust identity service, registration, site redesign | 115 | return ( |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 116 | <div className="mb-6 flex items-center gap-3 rounded-lg border border-accent/30 bg-accent/5 p-3" role="status"> |
| 117 | {invite.workspace ? ( | |
| 118 | <Avatar name={invite.workspace.slug} image={invite.workspace.avatar} size={36} square /> | |
| 119 | ) : from ? ( | |
| 120 | <Avatar name={from.username} image={from.avatar} size={36} /> | |
| 121 | ) : ( | |
| 122 | <span className="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-accent/15 text-accent"> | |
| 123 | <Ticket size={18} /> | |
| 124 | </span> | |
| 125 | )} | |
| 126 | <p className="min-w-0 text-sm leading-5"> | |
| 127 | {from ? ( | |
| 128 | <> | |
| 129 | <span className="font-medium text-fg">{from.name ?? from.username}</span> | |
| 130 | {from.name && <span className="font-mono text-muted"> @{from.username}</span>} | |
| 131 | </> | |
| 132 | ) : ( | |
| 133 | <span className="font-medium text-fg">The g1t team</span> | |
| 134 | )}{" "} | |
| 135 | <span className="text-muted"> | |
| 136 | invited you | |
| 137 | {invite.workspace ? ( | |
| 138 | <> | |
| 139 | {" "}to <span className="font-medium text-fg">{invite.workspace.name}</span> | |
| 140 | </> | |
| 141 | ) : ( | |
| 142 | " to g1t" | |
| 143 | )} | |
| 144 | . | |
| 145 | </span> | |
| 146 | </p> | |
| 147 | </div> | |
| 148 | ); | |
| 149 | } | |
| 150 | ||
| 151 | function SignUpForm({ | |
| 152 | loaderData, | |
| 153 | error, | |
| 154 | }: { | |
| 155 | loaderData: Route.ComponentProps["loaderData"]; | |
| 156 | error: string | undefined; | |
| 157 | }) { | |
| 158 | const { invite, code, inviteOnly } = loaderData; | |
| 159 | return ( | |
| 160 | <> | |
| 161 | {invite && <InvitedBy invite={invite} />} | |
| 162 | {loaderData.github && ( | |
| API and MCP server, Rust identity service, registration, site redesign | 163 | <> |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 164 | <ContinueWithGithub href={loaderData.githubUrl} /> |
| 165 | <OrDivider /> | |
| API and MCP server, Rust identity service, registration, site redesign | 166 | </> |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 167 | )} |
| 168 | <Form method="post" className="relative space-y-4"> | |
| 169 | <Honeypot started={loaderData.started} /> | |
| 170 | {inviteOnly || code ? ( | |
| 171 | <Field label="Invite code" hint={invite ? undefined : "From your invite link or email."}> | |
| 172 | <span className="block font-mono"> | |
| 173 | <Input name="invite" defaultValue={code} required={inviteOnly} spellCheck={false} autoCapitalize="none" /> | |
| 174 | </span> | |
| 175 | </Field> | |
| 176 | ) : null} | |
| API and MCP server, Rust identity service, registration, site redesign | 177 | <Field |
| 178 | label="Username" | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 179 | hint="Lowercase letters, digits and hyphens. It is how you sign in and how others see you." |
| API and MCP server, Rust identity service, registration, site redesign | 180 | > |
| 181 | <Input | |
| 182 | name="username" | |
| 183 | autoComplete="username" | |
| 184 | required | |
| 185 | autoFocus | |
| 186 | maxLength={39} | |
| 187 | pattern="[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9]))*" | |
| 188 | /> | |
| 189 | </Field> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 190 | <Field |
| 191 | label="Email" | |
| Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038) | 192 | hint={invite?.email ? `This invite is for ${invite.email}. Use that address.` : sharedDomainsHint(invite?.sharedDomains)} |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 193 | > |
| API and MCP server, Rust identity service, registration, site redesign | 194 | <Input name="email" type="email" autoComplete="email" required /> |
| 195 | </Field> | |
| 196 | <Field label="Password" hint="At least 10 characters."> | |
| 197 | <Input | |
| 198 | name="password" | |
| 199 | type="password" | |
| 200 | autoComplete="new-password" | |
| 201 | required | |
| 202 | minLength={10} | |
| 203 | /> | |
| 204 | </Field> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 205 | <ErrorText>{error}</ErrorText> |
| API and MCP server, Rust identity service, registration, site redesign | 206 | <div className="pt-2 *:w-full"> |
| Merge branch 'worktree-agent-a985c01f43538af01' | 207 | <SubmitButton pending="Creating account…"> |
| 208 | {invite?.workspace ? `Create account and join ${invite.workspace.slug}` : "Create account"} | |
| 209 | </SubmitButton> | |
| API and MCP server, Rust identity service, registration, site redesign | 210 | </div> |
| 211 | </Form> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 212 | </> |
| 213 | ); | |
| 214 | } | |
| 215 | ||
| 216 | /** Invite-only and no good code: enter one, or ask for access. */ | |
| 217 | function InviteOnly({ | |
| 218 | loaderData, | |
| 219 | actionData, | |
| 220 | }: { | |
| 221 | loaderData: Route.ComponentProps["loaderData"]; | |
| 222 | actionData: Route.ComponentProps["actionData"]; | |
| 223 | }) { | |
| 224 | const requested = actionData && "requested" in actionData; | |
| 225 | return ( | |
| 226 | <div className="space-y-8"> | |
| 227 | <section id="invite" className="scroll-mt-24"> | |
| 228 | <h2 className="text-sm font-medium">Have an invite?</h2> | |
| 229 | <p className="mt-1 text-sm text-muted">Paste the code or link from your invite.</p> | |
| 230 | <Form method="get" className="mt-3 flex gap-2"> | |
| 231 | <div className="min-w-0 grow font-mono"> | |
| 232 | <Input | |
| 233 | name="invite" | |
| 234 | aria-label="Invite code" | |
| 235 | placeholder="g1t-xxxx-xxxx-…" | |
| 236 | defaultValue={loaderData.code} | |
| 237 | required | |
| 238 | spellCheck={false} | |
| 239 | autoCapitalize="none" | |
| 240 | /> | |
| 241 | </div> | |
| 242 | <Button type="submit" variant="quiet"> | |
| 243 | Continue | |
| 244 | </Button> | |
| 245 | </Form> | |
| 246 | {loaderData.inviteError && ( | |
| 247 | <div className="mt-2"> | |
| 248 | <ErrorText>{loaderData.inviteError}</ErrorText> | |
| 249 | </div> | |
| 250 | )} | |
| 251 | </section> | |
| 252 | ||
| 253 | <section id="request" className="scroll-mt-24 border-t border-line pt-8"> | |
| 254 | <h2 className="text-sm font-medium">Request access</h2> | |
| 255 | {requested ? ( | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 256 | <p className="mt-3 flex items-start gap-2 rounded-md border border-success/40 bg-surface p-3 text-sm" role="status"> |
| 257 | <CheckCircle2 size={16} className="mt-0.5 shrink-0 text-success" /> | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 258 | <span> |
| 259 | You are on the list, and your invite comes to that address when a place opens. The first time an address | |
| 260 | asks, we email it to confirm. | |
| 261 | </span> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 262 | </p> |
| 263 | ) : ( | |
| 264 | <> | |
| 265 | <p className="mt-1 text-sm text-muted"> | |
| 266 | Leave your email and we will send an invite as we open up. Someone already on g1t can also invite you. | |
| 267 | </p> | |
| 268 | <Form method="post" className="relative mt-3 space-y-3"> | |
| 269 | <input type="hidden" name="intent" value="request-access" /> | |
| 270 | <Honeypot started={loaderData.started} /> | |
| 271 | <Field label="Email"> | |
| 272 | <Input name="email" type="email" autoComplete="email" required maxLength={254} /> | |
| 273 | </Field> | |
| 274 | <Field label="What will you build? (optional)"> | |
| 275 | <textarea | |
| 276 | name="about" | |
| 277 | rows={3} | |
| 278 | maxLength={1000} | |
| 279 | className="w-full rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim" | |
| 280 | /> | |
| 281 | </Field> | |
| 282 | <ErrorText>{actionData && "requestError" in actionData ? actionData.requestError : undefined}</ErrorText> | |
| 283 | <div className="*:w-full"> | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 284 | <SubmitButton pending="Sending…" match={{ intent: "request-access" }}> |
| 285 | Request access | |
| 286 | </SubmitButton> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 287 | </div> |
| 288 | </Form> | |
| 289 | </> | |
| 290 | )} | |
| 291 | </section> | |
| 292 | </div> | |
| 293 | ); | |
| 294 | } | |
| 295 | ||
| 296 | export default function Register({ loaderData, actionData }: Route.ComponentProps) { | |
| 297 | const { inviteOnly, invite } = loaderData; | |
| 298 | const error = actionData && "error" in actionData ? actionData.error : undefined; | |
| 299 | // Invite-only: the sign-up form only appears with a good code (or a | |
| 300 | // submitted one that failed, so the person can correct the rest). | |
| 301 | const showForm = !inviteOnly || invite !== null; | |
| 302 | return ( | |
| 303 | <AuthCard | |
| 304 | title={showForm ? "Create your account" : "g1t is invite-only for now"} | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 305 | subtitle={showForm ? "Your team and its agents, working in one place" : "Enter your invite, or ask for one"} |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 306 | footer={ |
| 307 | <> | |
| 308 | Already have an account?{" "} | |
| 309 | <Link to="/login" className="text-fg underline underline-offset-4"> | |
| 310 | Sign in | |
| 311 | </Link> | |
| 312 | </> | |
| 313 | } | |
| 314 | > | |
| 315 | {showForm ? <SignUpForm loaderData={loaderData} error={error} /> : <InviteOnly loaderData={loaderData} actionData={actionData} />} | |
| API and MCP server, Rust identity service, registration, site redesign | 316 | </AuthCard> |
| 317 | ); | |
| 318 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.