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
62 linesCodeBlameRaw
1/**
2 * The Invites page: every invite, to find and revoke one, with invites to
3 * g1t from Invite a friend on their own tab, and more of those for a
4 * person. Anyone may register, so there is no waitlist and no shared link
5 * to manage; an invite is never needed to sign up. No Workers imports, so
6 * it can be tested under Node. Identity checks everything again.
7 */
8
9/** The page's tabs: every invite, or only invites to g1t (Invite a friend). */
10export type InvitesTab = "all" | "g1t";
11
12export function invitesTab(raw: string | null | undefined): InvitesTab {
13 return raw === "g1t" ? "g1t" : "all";
14}
15
16/** The page's address, with its search. */
17export function invitesHref(params: Record<string, string | null | undefined> = {}): string {
18 const search = new URLSearchParams();
19 for (const [key, value] of Object.entries(params)) if (value) search.set(key, value);
20 const query = search.toString();
21 return query ? `/invites?${query}` : "/invites";
22}
23
24/** The flash messages the page redirects back with (`?done=`). */
25export const INVITES_DONE: Record<string, string> = {
26 revoked: "Invite revoked. It stops working at once.",
27 granted: "Granted. It applies at once.",
28};
29
30/** The flash for `?done=`. */
31export function doneMessage(done: string | null): string | null {
32 return done ? (INVITES_DONE[done] ?? null) : null;
33}
34
35/** What an account made with a shared link, before registration opened, says about where it came from. */
36export function joinedThrough(label: string): string {
37 return `Joined through ${label}`;
38}
39
40/** The most invites one grant gives or takes back (`MAX_INVITE_GRANT` in the contracts). */
41export const MAX_GRANT = 1000;
42
43const USERNAME = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/;
44
45export type Grant = { username: string; amount: number; note: string };
46
47/** A grant of invites to g1t, as typed: who, how many (negative takes back), and why. */
48export function parseGrant(form: { get(name: string): unknown }): { ok: true; value: Grant } | { ok: false; error: string } {
49 const read = (name: string) => {
50 const value = form.get(name);
51 return typeof value === "string" ? value.trim() : "";
52 };
53 const username = read("username").replace(/^@/, "").toLowerCase();
54 if (!USERNAME.test(username)) return { ok: false, error: `“${read("username")}” is not a username.` };
55 const raw = read("amount");
56 if (!/^-?\d+$/.test(raw)) return { ok: false, error: "Give a whole number of invites, such as 10, or -5 to take some back." };
57 const amount = Number(raw);
58 if (amount === 0 || Math.abs(amount) > MAX_GRANT) return { ok: false, error: `Grant between 1 and ${MAX_GRANT} invites.` };
59 const note = read("note");
60 if (note.length < 3) return { ok: false, error: "Say why, for the record." };
61 return { ok: true, value: { username, amount, note: note.slice(0, 500) } };
62}