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
223 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.

Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure1/**
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)2 * The Invites page's forms and tabs: the waitlist, every invite, shared
3 * invite links, grants of more invites, and a person's invite tree. No
4 * Workers imports, so it can be tested under Node. Identity checks
5 * everything again.
Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure6 */
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)7import type { NewSharedInvite, SharedInvite, SharedInviteStatus, WaitlistStatus } from "@g1t/contracts";
Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure8
9import type { Parsed } from "./forms.ts";
10
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)11export const INVITE_TABS = ["waitlist", "invites", "shared", "grant", "tree"] as const;
Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure12export type InviteTab = (typeof INVITE_TABS)[number];
13
14export const TAB_LABEL: Record<InviteTab, string> = {
15 waitlist: "Waitlist",
16 invites: "Invites",
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)17 shared: "Shared links",
Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure18 grant: "Grant & mint",
19 tree: "Invite tree",
20};
21
22export function parseTab(raw: string | null): InviteTab {
23 return (INVITE_TABS as readonly string[]).includes(raw ?? "") ? (raw as InviteTab) : "waitlist";
24}
25
26const STATUSES: (WaitlistStatus | "all")[] = ["waiting", "invited", "dismissed", "all"];
27
28export function parseWaitlistStatus(raw: string | null): WaitlistStatus | "all" {
29 return STATUSES.includes(raw as WaitlistStatus) ? (raw as WaitlistStatus | "all") : "waiting";
30}
31
32/** Where a tab lives, with its search. */
33export function invitesHref(tab: InviteTab, params: Record<string, string | null | undefined> = {}): string {
34 const search = new URLSearchParams({ tab });
35 for (const [key, value] of Object.entries(params)) if (value) search.set(key, value);
36 return `/invites?${search}`;
37}
38
39/** The most one grant gives or takes back; identity holds the same line. */
40export const MAX_GRANT = 1000;
41
42const NAME = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/;
43
44export type Grant = { target: "user" | "workspace"; name: string; amount: number; note: string };
45
46/** A grant of invites, as typed: who, how many (negative takes back), and why. */
47export function parseGrant(form: { get(name: string): unknown }): Parsed<Grant> {
48 const read = (name: string) => {
49 const value = form.get(name);
50 return typeof value === "string" ? value.trim() : "";
51 };
52 const target = read("target");
53 if (target !== "user" && target !== "workspace") return { ok: false, error: "Choose a person or a workspace." };
54 const name = read("name").replace(/^@/, "").toLowerCase();
55 if (!NAME.test(name)) return { ok: false, error: `“${read("name")}” is not a ${target === "user" ? "username" : "workspace slug"}.` };
56 const raw = read("amount");
57 if (!/^-?\d+$/.test(raw)) return { ok: false, error: "Give a whole number of invites, such as 10, or -5 to take some back." };
58 const amount = Number(raw);
59 if (amount === 0 || Math.abs(amount) > MAX_GRANT) return { ok: false, error: `Grant between 1 and ${MAX_GRANT} invites.` };
60 const note = read("note");
61 if (note.length < 3) return { ok: false, error: "Say why, for the record." };
62 return { ok: true, value: { target, name, amount, note: note.slice(0, 500) } };
63}
64
65/** An address to mint an invite for, or none: anyone with the code. */
66export function parseMintEmail(raw: string): Parsed<string | null> {
67 const email = raw.trim().toLowerCase();
68 if (!email) return { ok: true, value: null };
69 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? { ok: true, value: email } : { ok: false, error: `“${raw}” is not an email address.` };
70}
71
72/** The flash messages the page redirects back with (`?done=`). */
73export const INVITES_DONE: Record<string, string> = {
74 approved: "Approved. The invite is on its way to that address.",
75 dismissed: "Dismissed.",
76 revoked: "Invite revoked. It comes back to whoever it was charged to.",
77 granted: "Granted. It applies at once.",
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)78 "shared-created": "Shared link made. Copy it below and hand it to the group.",
79 "shared-revoked": "Shared link revoked. It makes no more accounts; the ones it made stay.",
Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure80};
81
82/** The flash for `?done=` and, after deciding several at once, `?n=` of them. */
83export function doneMessage(done: string | null, n: string | null): string | null {
84 if (!done) return null;
85 const count = Number(n);
86 if (Number.isInteger(count) && count > 1) {
87 if (done === "approved") return `Approved ${count} requests. Each invite is on its way.`;
88 if (done === "dismissed") return `Dismissed ${count} requests.`;
89 }
90 return INVITES_DONE[done] ?? null;
91}
92
93/** The most requests one bulk decision takes: each one is its own call to identity. */
94export const MAX_BULK = 50;
95
96/** The waitlist entries ticked on a bulk form, each once, in order. */
97export function parseIds(form: { getAll(name: string): unknown[] }): Parsed<string[]> {
98 const ids = [
99 ...new Set(
100 form
101 .getAll("ids")
102 .filter((id): id is string => typeof id === "string")
103 .map((id) => id.trim())
104 .filter((id) => /^wl_[0-9a-z]{1,40}$/.test(id)),
105 ),
106 ];
107 if (ids.length === 0) return { ok: false, error: "Tick the requests to decide on first." };
108 if (ids.length > MAX_BULK) return { ok: false, error: `Decide on at most ${MAX_BULK} at a time.` };
109 return { ok: true, value: ids };
110}
111
112/** The most characters an approval's note keeps; identity holds the same line. */
113export const MAX_NOTE = 500;
114
115/** A note for the invite email: trimmed, or none. */
116export function parseNote(raw: string | null | undefined): Parsed<string | null> {
117 const note = (raw ?? "").trim();
118 if (!note) return { ok: true, value: null };
119 if (note.length > MAX_NOTE) return { ok: false, error: `Keep the note to ${MAX_NOTE} characters; it is ${note.length}.` };
120 return { ok: true, value: note };
121}
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)122
123// --- Shared invite links ------------------------------------------------------------
124
125/** The most accounts one shared link makes; identity holds the same line. */
126export const MAX_SHARED_USES = 1000;
127/** The most characters a shared link's label keeps; identity holds the same line. */
128export const MAX_SHARED_LABEL = 80;
129/** The most email domains a shared link may be limited to; identity holds the same line. */
130export const MAX_SHARED_DOMAINS = 10;
131/** How long a shared link works unless staff choose a day. */
132export const SHARED_TTL_DAYS = 14;
133/** The furthest ahead its last day may be. */
134export const SHARED_MAX_DAYS = 365;
135
136const DAY_MS = 24 * 60 * 60 * 1000;
137const DATE = /^\d{4}-\d{2}-\d{2}$/;
138const DOMAIN_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
139
140/** The day `days` after `now`, as a date input holds it (UTC). */
141export function dayAfter(now: number, days: number): string {
142 return new Date(now + days * DAY_MS).toISOString().slice(0, 10);
143}
144
145/** The address a shared link has: sign-up with its code filled in. */
146export function sharedInviteLink(code: string, origin = "https://g1t.sh"): string {
147 return `${origin.replace(/\/+$/, "")}/register?invite=${encodeURIComponent(code)}`;
148}
149
150/** One email domain as staff typed it (`@Cloudflare.com` reads as `cloudflare.com`), if it is one. */
151export function normalizeDomain(raw: string): string | null {
152 const domain = raw.trim().replace(/^@+/, "").replace(/\.+$/, "").toLowerCase();
153 if (domain.length < 3 || domain.length > 253 || !domain.includes(".")) return null;
154 return domain.split(".").every((label) => DOMAIN_LABEL.test(label)) ? domain : null;
155}
156
157/**
158 * A new shared link, as typed: a label, 1 to 1000 uses, a last day from
159 * today to a year ahead (none: 14 days), and up to 10 domains separated by
160 * commas or spaces.
161 */
162export function parseSharedInvite(form: { get(name: string): unknown }, now = Date.now()): Parsed<NewSharedInvite> {
163 const read = (name: string) => {
164 const value = form.get(name);
165 return typeof value === "string" ? value.trim() : "";
166 };
167 const label = read("label").replace(/\s+/g, " ");
168 if (!label) return { ok: false, error: "Give the link a label, such as Cloudflare judges." };
169 if (label.length > MAX_SHARED_LABEL) return { ok: false, error: `Keep the label to ${MAX_SHARED_LABEL} characters.` };
170 const uses = read("max_uses");
171 if (!/^\d+$/.test(uses) || Number(uses) < 1 || Number(uses) > MAX_SHARED_USES) {
172 return { ok: false, error: `A shared link makes between 1 and ${MAX_SHARED_USES} accounts.` };
173 }
174 const expires = read("expires_on");
175 let expiresOn: string | null = null;
176 if (expires) {
177 const day = DATE.test(expires) ? new Date(`${expires}T00:00:00Z`) : null;
178 if (!day || Number.isNaN(day.getTime()) || day.toISOString().slice(0, 10) !== expires) {
179 return { ok: false, error: "Give the expiry as a date, such as 2026-10-28." };
180 }
181 if (expires < dayAfter(now, 0)) return { ok: false, error: "The expiry has passed. Choose today or a later day." };
182 if (expires > dayAfter(now, SHARED_MAX_DAYS)) return { ok: false, error: `A shared link works for at most ${SHARED_MAX_DAYS} days.` };
183 expiresOn = expires;
184 }
185 const domains: string[] = [];
186 for (const typed of read("domains").split(/[\s,]+/).filter(Boolean)) {
187 const domain = normalizeDomain(typed);
188 if (!domain) return { ok: false, error: `${typed} is not an email domain. Write domains such as cloudflare.com.` };
189 if (!domains.includes(domain)) domains.push(domain);
190 }
191 if (domains.length > MAX_SHARED_DOMAINS) return { ok: false, error: `Limit a link to at most ${MAX_SHARED_DOMAINS} domains.` };
192 return { ok: true, value: { label, maxUses: Number(uses), expiresOn, domains } };
193}
194
195/** How a shared link's state reads, and its badge's tone. */
196export function sharedStatus(status: SharedInviteStatus): { label: string; tone: "lavender" | "mint" | "danger" | "plain" } {
197 switch (status) {
198 case "live":
199 return { label: "Live", tone: "lavender" };
200 case "used_up":
201 return { label: "Used up", tone: "mint" };
202 case "expired":
203 return { label: "Expired", tone: "plain" };
204 case "revoked":
205 return { label: "Revoked", tone: "danger" };
206 }
207}
208
209/** How many of its uses are taken, in words. */
210export function usesLine(link: Pick<SharedInvite, "uses" | "maxUses">): string {
211 return `${link.uses} of ${link.maxUses} used`;
212}
213
214/** Whom a shared link is for, by address. */
215export function domainsLine(domains: string[]): string {
216 if (domains.length === 0) return "Any email address";
217 return `Only addresses at ${domains.join(", ")}`;
218}
219
220/** What an account made with a shared link says about where it came from. */
221export function joinedThrough(label: string): string {
222 return `Joined through ${label}`;
223}

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