flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/apps/web/app/lib/session.server.ts

117 lines3,906 bytesCodeBlame

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
Agents as a team: lifecycle, merge queue, billing and a new shell9import { type Result, type Role, type User, type Viewer, httpStatus } from "@g1t/contracts";
Initial g1t: services, event bus, intents and attempts10
API and MCP server, Rust identity service, registration, site redesign11import { identity } from "./services.server";
12
Initial g1t: services, event bus, intents and attempts13const SESSION_COOKIE = "g1t_session";
14const SESSION_TTL_SECONDS = 30 * 24 * 60 * 60;
15
16const viewerContext = createContext<Viewer>(null);
17
18function sessionToken(request: Request): string | null {
19 const cookies = request.headers.get("cookie") ?? "";
20 const match = new RegExp(`(?:^|; )${SESSION_COOKIE}=([0-9a-f]{64})`).exec(cookies);
21 return match ? match[1] : null;
22}
23
24function sessionCookie(value: string, maxAge: number): string {
25 return `${SESSION_COOKIE}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`;
26}
27
Agents as a team: lifecycle, merge queue, billing and a new shell28/** Pages a signed-in person can use before they have a workspace. */
29const BEFORE_WORKSPACE = ["/workspaces/new", "/settings", "/verify", "/logout"];
30
31/**
32 * Root middleware: resolves the signed-in user once per request.
33 *
34 * Everything on g1t lives in a workspace, so a confirmed account with none
35 * is sent to create one, from wherever it was going, and returned there
36 * afterwards.
37 */
Initial g1t: services, event bus, intents and attempts38export const viewerMiddleware: MiddlewareFunction<Response> = async ({
39 request,
40 context,
41}) => {
42 const token = sessionToken(request);
Agents as a team: lifecycle, merge queue, billing and a new shell43 if (!token) return;
44 const viewer = await identity.userForSession(token);
45 context.set(viewerContext, viewer);
46
47 const { pathname, search } = new URL(request.url);
48 if (
49 request.method === "GET" &&
50 viewer?.verified &&
51 (viewer.workspaces ?? []).length === 0 &&
52 !BEFORE_WORKSPACE.includes(pathname) &&
53 !pathname.endsWith(".data")
54 ) {
55 const next = pathname === "/" ? "" : `?next=${encodeURIComponent(pathname + search)}`;
56 throw redirect(`/workspaces/new${next}`);
Initial g1t: services, event bus, intents and attempts57 }
58};
59
60type Context = Readonly<RouterContextProvider>;
61
62export function getViewer(context: Context): Viewer {
63 return context.get(viewerContext);
64}
65
Agents as a team: lifecycle, merge queue, billing and a new shell66/** The viewer's role in a workspace, or null if they are not a member. */
67export function roleIn(viewer: Viewer, slug: string): Role | null {
68 const wanted = slug.toLowerCase();
69 return (
70 viewer?.workspaces?.find((membership) => membership.slug === wanted)?.role ?? null
71 );
72}
73
Initial g1t: services, event bus, intents and attempts74export function requireUser(context: Context, request: Request): User {
75 const viewer = getViewer(context);
76 if (!viewer) {
Device sign-in replaces registering and minting tokens over the API77 // Keep the query string: a device sign-in link carries its code there.
78 const { pathname, search } = new URL(request.url);
79 throw redirect(`/login?next=${encodeURIComponent(pathname + search)}`);
Initial g1t: services, event bus, intents and attempts80 }
81 return viewer;
82}
83
API and MCP server, Rust identity service, registration, site redesign84/**
85 * Where to go after signing in. Only same-site paths are honoured, so
86 * `next` cannot redirect off g1t.
87 */
88export function nextPath(request: Request): string {
89 const next = new URL(request.url).searchParams.get("next") ?? "/";
90 return next.startsWith("/") && !next.startsWith("//") ? next : "/";
91}
92
Initial g1t: services, event bus, intents and attempts93/** `Set-Cookie` value that starts a session. */
94export function startSession(token: string): string {
95 return sessionCookie(token, SESSION_TTL_SECONDS);
96}
97
98/** Ends the session and returns the `Set-Cookie` value that clears it. */
99export async function endSession(request: Request): Promise<string> {
100 const token = sessionToken(request);
API and MCP server, Rust identity service, registration, site redesign101 if (token) await identity.signOut(token);
Initial g1t: services, event bus, intents and attempts102 return sessionCookie("", 0);
103}
104
105/** Rejects cross-site form posts; call at the top of every action. */
106export function assertSameOrigin(request: Request): void {
107 const origin = request.headers.get("origin");
108 if (origin && origin !== new URL(request.url).origin) {
109 throw new Response("Cross-origin request rejected", { status: 403 });
110 }
111}
112
113/** The value of a service result, or the matching HTTP error. */
114export function unwrap<T>(result: Result<T>): T {
115 if (result.ok) return result.value;
116 throw data(result.error.message, { status: httpStatus(result.error) });
117}