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/github.ts

88 lines3,401 bytesCodeBlame
1/**
2 * The parts of signing in with GitHub and installing g1t's GitHub App that
3 * the site itself decides: the cookies that bind a trip to GitHub to the
4 * browser that started it, and checking what comes back.
5 */
6
7/** The state sent to GitHub to sign in or link, for ten minutes. */
8export const STATE_COOKIE = "g1t_github_state";
9/** A GitHub sign-in waiting on a username, or on signing in to link. */
10export const PENDING_COOKIE = "g1t_github_pending";
11/** An installation under way: its state and the workspace it is for. */
12export const INSTALL_COOKIE = "g1t_github_install";
13
14/** The value of a cookie, or null. Values here are hex and slugs only. */
15export function readCookie(header: string | null, name: string): string | null {
16 for (const part of (header ?? "").split(";")) {
17 const [key, ...rest] = part.trim().split("=");
18 if (key === name) {
19 const value = rest.join("=");
20 return /^[0-9a-z.-]{1,200}$/.test(value) ? value : null;
21 }
22 }
23 return null;
24}
25
26/** A `Set-Cookie` value: HttpOnly, Secure, same-site Lax; 0 clears it. */
27export function cookie(name: string, value: string, maxAge: number): string {
28 return `${name}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`;
29}
30
31/** Compares two strings in time that does not depend on where they differ. */
32export function sameString(a: string, b: string): boolean {
33 if (a.length !== b.length) return false;
34 let difference = 0;
35 for (let i = 0; i < a.length; i++) difference |= a.charCodeAt(i) ^ b.charCodeAt(i);
36 return difference === 0;
37}
38
39/**
40 * Whether the state GitHub sent back is the one this browser was given.
41 * Both must be present: a callback without the cookie came from somewhere
42 * else.
43 */
44export function stateMatches(fromCookie: string | null, fromGithub: string | null): boolean {
45 if (!fromCookie || !fromGithub) return false;
46 return sameString(fromCookie, fromGithub);
47}
48
49/** The install cookie's value: `<state>.<workspace>`. */
50export function installCookieValue(state: string, workspace: string): string {
51 return `${state}.${workspace}`;
52}
53
54/** The workspace an installation was started for, if the state matches. */
55export function installWorkspace(fromCookie: string | null, fromGithub: string | null): string | null {
56 if (!fromCookie) return null;
57 const dot = fromCookie.indexOf(".");
58 if (dot < 0) return null;
59 const state = fromCookie.slice(0, dot);
60 const workspace = fromCookie.slice(dot + 1);
61 return stateMatches(state, fromGithub) && workspace ? workspace : null;
62}
63
64/** A fresh random state: 32 bytes, hex. */
65export function newState(): string {
66 const bytes = new Uint8Array(32);
67 crypto.getRandomValues(bytes);
68 return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
69}
70
71/** What each way of bringing a repository across does, for the picker. */
72export const MODES = [
73 {
74 id: "import",
75 title: "Import",
76 text: "Copy it once: every branch and tag, and its issues if you like. The copy on g1t is then its own.",
77 },
78 {
79 id: "mirror",
80 title: "Mirror",
81 text: "Keep the code on GitHub. Every push there is fetched into g1t, so agents can work on it, and deployments build it once you turn them on.",
82 },
83 {
84 id: "push",
85 title: "Move to g1t",
86 text: "Work on g1t from now on. Every push here is pushed to GitHub, so it stays up to date for everyone else.",
87 },
88] as const;