Skip to content

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.sh
215 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.

Download ZIP reads blobs eight batches at a time and deflates files together1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now.5 filterInvitations,
6 filterMembers,
7 invitationCounts,
8 invitationFilter,
9 invitationFilterOf,
10 invitationTone,
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)11 inviteKind,
Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now.12 memberFilters,
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)13 invitePageCopy,
14 workspaceInviteCopy,
Download ZIP reads blobs eight batches at a time and deflates files together15 cleanCode,
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 confirm16 cleanProof,
17 invitePath,
18 inviteSignUpCopy,
Download ZIP reads blobs eight batches at a time and deflates files together19 inviteFor,
20 inviteLink,
21 inviteState,
22 landingFor,
23 looksAutomated,
24 signUpCopy,
25 suggestUsername,
26 welcomeCookie,
27 clearWelcome,
28 welcomes,
29} from "./invites.ts";
30
31const CODE = "g1t-k7m2-q9xd-4hpw-abcd-0123-4567-89ef-ghjk";
32
Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now.33test("the Invitations page sorts every state under one filter, and counts them", () => {
34 const statuses = ["pending", "awaiting_confirmation", "awaiting_answer", "redeemed", "declined", "expired", "revoked"] as const;
35 const invites = statuses.map((status) => ({ status }));
36 assert.equal(invitationFilterOf("pending"), "pending");
37 assert.equal(invitationFilterOf("awaiting_confirmation"), "pending");
38 assert.equal(invitationFilterOf("awaiting_answer"), "pending");
39 assert.equal(invitationFilterOf("redeemed"), "accepted");
40 assert.equal(invitationFilterOf("declined"), "declined");
41 assert.equal(invitationFilterOf("expired"), "expired");
42 assert.equal(invitationFilterOf("revoked"), "revoked");
43 assert.deepEqual(invitationCounts(invites), { all: 7, pending: 3, accepted: 1, declined: 1, expired: 1, revoked: 1 });
44 assert.equal(filterInvitations(invites, "all").length, 7);
45 assert.deepEqual(filterInvitations(invites, "pending").map((invite) => invite.status), ["pending", "awaiting_confirmation", "awaiting_answer"]);
46 assert.deepEqual(filterInvitations(invites, "accepted").map((invite) => invite.status), ["redeemed"]);
47 // The query names a filter, or means All.
48 assert.equal(invitationFilter("pending"), "pending");
49 assert.equal(invitationFilter("failed"), "all");
50 assert.equal(invitationFilter(null), "all");
51 // Waiting is amber, joined green, a no red, and over is quiet.
52 assert.equal(invitationTone("awaiting_answer"), "warn");
53 assert.equal(invitationTone("redeemed"), "success");
54 assert.equal(invitationTone("revoked"), "danger");
55 assert.equal(invitationTone("expired"), "neutral");
56});
57
58test("the Members page filters by role, two-factor, team and words", () => {
59 const members = [
60 { username: "ada", name: "Ada Lovelace", role: "owner" as const, two_factor: true },
61 { username: "grace", name: "Grace Hopper", role: "member" as const, two_factor: false },
62 { username: "linus", name: null, role: "member" as const, two_factor: true },
63 ];
64 const teams = { ada: [{ slug: "core" }], grace: [{ slug: "web" }, { slug: "core" }] };
65 assert.deepEqual(memberFilters(""), { role: "all", twoFactor: "all", team: null, query: "" });
66 assert.deepEqual(memberFilters("?role=owner&2fa=off&team=Core&q=%20ada%20"), { role: "owner", twoFactor: "off", team: "core", query: "ada" });
67 assert.deepEqual(memberFilters("?role=admin&2fa=maybe"), { role: "all", twoFactor: "all", team: null, query: "" });
68 const names = (filters: string) => filterMembers(members, memberFilters(filters), teams).map((member) => member.username);
69 assert.deepEqual(names(""), ["ada", "grace", "linus"]);
70 assert.deepEqual(names("?role=member"), ["grace", "linus"]);
71 assert.deepEqual(names("?2fa=off"), ["grace"]);
72 assert.deepEqual(names("?team=core"), ["ada", "grace"]);
73 assert.deepEqual(names("?q=hopper"), ["grace"]);
74 assert.deepEqual(names("?q=@LIN"), ["linus"]);
75 assert.deepEqual(names("?role=owner&team=web"), []);
76});
77
Merge branch 'open-signups'78test("everyone is offered a plain sign-up", () => {
Sign-up buttons say Sign up everywhere; only the sign-up page says registration takes an invite79 assert.deepEqual(signUpCopy(), { primary: "Sign up", secondary: null });
Download ZIP reads blobs eight batches at a time and deflates files together80});
81
82test("an invite link is on g1t.sh unless told otherwise", () => {
83 assert.equal(inviteLink(CODE), `https://g1t.sh/invite/${CODE}`);
84 assert.equal(inviteLink(CODE, "http://localhost:8787/"), `http://localhost:8787/invite/${CODE}`);
85});
86
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 confirm87const PROOF = "4f9c2a7e0b13d5c84f9c2a7e0b13d5c84f9c2a7e0b13d5c84f9c2a7e0b13d5c8";
88
89test("an invite email's proof is kept only when it looks like one, and goes along to the invite's page", () => {
90 assert.equal(cleanProof(PROOF), PROOF);
91 assert.equal(cleanProof(` ${PROOF.toUpperCase()} `), PROOF);
92 assert.equal(cleanProof("not-a-proof"), null);
93 assert.equal(cleanProof("abc"), null);
94 assert.equal(cleanProof("a".repeat(500)), null);
95 assert.equal(cleanProof(null), null);
96 assert.equal(invitePath(CODE, PROOF), `/invite/${CODE}?proof=${PROOF}`);
97 assert.equal(invitePath(CODE, null), `/invite/${CODE}`);
98 assert.equal(invitePath(CODE), `/invite/${CODE}`);
99});
100
101test("signing up from the invite email says the address is confirmed already; otherwise the code step applies", () => {
102 const base = { address: "ada@example.com", emailProven: false, workspace: { name: "Flagon, Inc." }, repository: null };
103 const proven = inviteSignUpCopy({ ...base, emailProven: true });
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)104 assert.equal(proven.intro, "You can join Flagon, Inc. as soon as you create it: accept the invitation then.");
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 confirm105 assert.match(proven.confirmed ?? "", /^ada@example\.com is confirmed: you came here from the invite we emailed to it/);
106 assert.match(proven.hint, /confirmed already/);
107 assert.doesNotMatch(proven.hint, /code/);
108
109 // No proof (a code typed in, or a link passed on): nothing new is said.
110 const plain = inviteSignUpCopy(base);
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)111 assert.equal(plain.intro, "You can join Flagon, Inc. as soon as you confirm your email: accept the invitation then.");
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 confirm112 assert.equal(plain.confirmed, null);
113 assert.equal(plain.hint, "Your invite was sent here. We email it a code to confirm it before you start.");
114
115 // An invite for anyone with the code has no address to prove.
116 const open = inviteSignUpCopy({ ...base, address: null, emailProven: true, workspace: null });
117 assert.equal(open.confirmed, null);
118 assert.equal(open.intro, "It takes a minute.");
119 assert.equal(open.hint, "We email it a code to confirm it before you start.");
120
121 const repo = inviteSignUpCopy({ ...base, workspace: null, repository: { name: "flagon-io/g1t" }, emailProven: true });
122 assert.equal(repo.intro, "You get flagon-io/g1t as soon as you create it.");
123});
124
Download ZIP reads blobs eight batches at a time and deflates files together125test("a pasted link or code is tidied to the code", () => {
126 assert.equal(cleanCode(CODE), CODE);
127 assert.equal(cleanCode(` ${CODE} `), CODE);
128 assert.equal(cleanCode(`https://g1t.sh/invite/${CODE}`), CODE);
129 assert.equal(cleanCode(`https://g1t.sh/register?invite=${CODE}&next=%2F`), CODE);
130 assert.equal(cleanCode("g1t-k7m2 q9xd"), "g1t-k7m2q9xd");
131 assert.equal(cleanCode(null), "");
132 assert.equal(cleanCode("x".repeat(500)).length, 80);
133});
134
135test("each invite says where it stands and whom it is for", () => {
136 const base = { redeemedBy: null, email: null, workspace: null };
137 assert.deepEqual(inviteState({ ...base, status: "pending" }), { label: "Pending", tone: "pending" });
138 assert.deepEqual(inviteState({ ...base, status: "redeemed", redeemedBy: "ada" }), { label: "Joined as @ada", tone: "done" });
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)139 assert.deepEqual(inviteState({ ...base, status: "awaiting_confirmation", redeemedBy: "ada" }), {
140 label: "@ada is confirming their email",
141 tone: "pending",
142 });
Download ZIP reads blobs eight batches at a time and deflates files together143 assert.deepEqual(inviteState({ ...base, status: "expired" }), { label: "Expired", tone: "dead" });
144 assert.deepEqual(inviteState({ ...base, status: "revoked" }), { label: "Revoked", tone: "dead" });
145 assert.equal(inviteFor({ ...base, status: "pending" }), "Anyone with the link");
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)146 assert.equal(inviteFor({ ...base, status: "pending", email: "ada@example.com", workspace: "acme" }), "ada@example.com");
147 assert.equal(inviteFor({ ...base, status: "pending", invitee: "daweazl", workspace: "flagon-io" }), "@daweazl");
148 // Which kind each is, beside whom it is for.
149 assert.deepEqual(inviteKind({ workspace: null }), { kind: "g1t", label: "Invite to g1t" });
150 assert.deepEqual(inviteKind({ workspace: "flagon-io" }), { kind: "workspace", label: "Invite to join flagon-io" });
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)151 assert.deepEqual(inviteState({ ...base, status: "awaiting_answer", redeemedBy: "daweazl" }), { label: "Waiting for @daweazl to accept", tone: "pending" });
152 assert.deepEqual(inviteState({ ...base, status: "declined", invitee: "daweazl" }), { label: "@daweazl declined", tone: "dead" });
Download ZIP reads blobs eight batches at a time and deflates files together153});
154
155test("bots that fill the hidden field or answer instantly are turned away", () => {
156 const form = (fields: Record<string, string>) => ({ get: (name: string) => fields[name] ?? null });
157 const now = 1_000_000;
158 assert.equal(looksAutomated(form({ website: "http://spam.example" }), now), true);
159 assert.equal(looksAutomated(form({ started: String(now - 200) }), now), true);
160 assert.equal(looksAutomated(form({ started: String(now - 10_000) }), now), false);
161 assert.equal(looksAutomated(form({}), now), false);
162 assert.equal(looksAutomated(form({ website: " " }), now), false);
163});
164
165test("a username is suggested from the invited address", () => {
166 assert.equal(suggestUsername("ada.lovelace@example.com"), "ada-lovelace");
167 assert.equal(suggestUsername("Margaret_Hamilton+g1t@example.com"), "margaret-hamilton");
168 assert.equal(suggestUsername("--x--@example.com"), "x");
169 assert.equal(suggestUsername(`${"a".repeat(38)}.b@example.com`), "a".repeat(38));
170 assert.equal(suggestUsername("...@example.com"), "");
171 assert.equal(suggestUsername(null), "");
172});
173
174test("an invite lands in its workspace, else its repository", () => {
175 assert.equal(landingFor({ workspace: { slug: "Flagon-IO" }, repository: null }), "flagon-io");
176 assert.equal(landingFor({ workspace: null, repository: { name: "flagon-io/g1t" } }), "flagon-io/g1t");
177 assert.equal(landingFor({ workspace: null, repository: null }), null);
178});
179
180test("the welcome is for one place, and ends", () => {
181 const set = welcomeCookie("flagon-io/g1t", true);
182 assert.match(set, /^g1t_welcome=flagon-io%2Fg1t; Path=\/; Max-Age=300; HttpOnly; SameSite=Lax; Secure$/);
183 const header = `a=1; ${set.split(";")[0]}; b=2`;
184 assert.equal(welcomes(header, "flagon-io/g1t"), true);
185 assert.equal(welcomes(header, "flagon-io"), false);
186 assert.equal(welcomes("g1t_welcome=flagon-io", "Flagon-IO"), true);
187 assert.equal(welcomes("g1t_welcome=%E0%A4%A", "flagon-io"), false);
188 assert.equal(welcomes("g1t_welcome=..%2F..%2Fx", "../../x"), false);
189 assert.equal(welcomes(null, "flagon-io"), false);
190 assert.match(clearWelcome(false), /^g1t_welcome=; Path=\/; Max-Age=0; HttpOnly; SameSite=Lax$/);
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)191});
192
193test("the two invites say which they are", () => {
194 // A workspace's People page: an invitation to accept or decline, which signs up whoever has no account.
Merge branch 'open-signups'195 const copy = workspaceInviteCopy("Flagon, Inc.");
196 assert.equal(copy.heading, "Invite to Flagon, Inc.");
197 assert.match(copy.hint, /invitation to join Flagon, Inc\..*join only if they accept/);
198 assert.match(copy.hint, /If they do not have a g1t account yet, they sign up from the invitation first/);
199 assert.doesNotMatch(copy.hint, /one of yours|shared invites/);
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)200});
201
202test("an invite's page names the invite it is", () => {
203 const base = { kind: "account" as const, invitedBy: { username: "syntaqx" }, workspace: null, repository: null, hasAccount: false };
204 const g1t = invitePageCopy(base, false);
205 assert.equal(`${g1t.before}${g1t.place ?? ""}${g1t.after}`, "@syntaqx invited you to g1t");
206 assert.match(g1t.about, /lets you make an account\. It does not add you to anyone's workspace/);
207 const join = invitePageCopy({ ...base, workspace: { name: "Flagon, Inc." } }, false);
208 assert.equal(`${join.before}${join.place}${join.after}`, "@syntaqx invited you to join Flagon, Inc. on g1t");
209 assert.equal(join.place, "Flagon, Inc.");
210 assert.match(join.about, /invitation to join Flagon, Inc\., which you accept or decline/);
211 assert.match(join.about, /You do not have a g1t account yet, so it also lets you make one/);
212 // Someone with an account, or signed in, just accepts.
213 assert.match(invitePageCopy({ ...base, kind: "workspace", workspace: { name: "Flagon, Inc." }, hasAccount: true }, false).about, /Accepting joins you to Flagon, Inc\./);
214 assert.equal(invitePageCopy({ ...base, invitedBy: null }, false).before, "The g1t team invited you to g1t");
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)215});

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