Skip to content
216 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.

sudo: Invoices, Audit log, and follow-ups due1/**
2 * The Invoices and Audit log pages' arithmetic and wording: filters read
3 * from the address, totals, links from billing's account ids to sudo's
4 * pages, and paging. No Workers imports, so it can be tested under Node.
5 */
6import type { AdminAction, InvoiceSummary } from "@g1t/contracts";
7
8// --- Invoices -----------------------------------------------------------------
9
10export const INVOICE_STATUSES = ["open", "overdue", "failed", "paid", "void"] as const;
11export type InvoiceStatusFilter = (typeof INVOICE_STATUSES)[number];
12
13export function parseInvoiceStatus(value: string | null): InvoiceStatusFilter | null {
14 return INVOICE_STATUSES.find((status) => status === value) ?? null;
15}
16
17/** A month as `<input type="month">` sends it: `2026-10`. */
18export function parseMonth(value: string | null): string | null {
19 if (!value || !/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) return null;
20 return value;
21}
22
23/** Not paid and not void: what is still owed. */
24export function isOutstanding(status: string): boolean {
25 return status === "open" || status === "overdue" || status === "failed";
26}
27
28export type InvoiceTotals = { count: number; amountMicros: number; paidMicros: number; outstandingMicros: number; outstanding: number };
29
30export function invoiceTotals(invoices: Pick<InvoiceSummary, "amountMicros" | "status">[]): InvoiceTotals {
31 const totals: InvoiceTotals = { count: invoices.length, amountMicros: 0, paidMicros: 0, outstandingMicros: 0, outstanding: 0 };
32 for (const invoice of invoices) {
33 if (invoice.status === "void") continue;
34 totals.amountMicros += invoice.amountMicros;
35 if (invoice.status === "paid") totals.paidMicros += invoice.amountMicros;
36 if (isOutstanding(invoice.status)) {
37 totals.outstandingMicros += invoice.amountMicros;
38 totals.outstanding += 1;
39 }
40 }
41 return totals;
42}
43
44/** A link to the invoices list with these filters; the defaults are left out. */
45export function invoicesHref({ status, month }: { status?: string | null; month?: string | null }): string {
46 const params = new URLSearchParams();
47 if (status) params.set("status", status);
48 if (month) params.set("month", month);
49 const query = params.toString();
50 return query ? `/invoices?${query}` : "/invoices";
51}
52
53/** Only https links to Stripe are followed. */
54export function safeUrl(url: string | null | undefined): string | null {
55 return url && url.startsWith("https://") ? url : null;
56}
57
58// --- Accounts -------------------------------------------------------------------
59
60const SLUG = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/;
61const ENTERPRISE = /^ent_[a-z0-9_-]{1,80}$/;
62
63/**
64 * The sudo page for one of billing's account ids: `ws_<slug>` is the
65 * workspace's, `ent_…` the enterprise's. Null for anything else, such as
66 * Stripe's own lines.
67 */
68export function accountPath(account: string | null | undefined): string | null {
69 const id = (account ?? "").trim().toLowerCase();
Merge branch 'main' into actions-toolkit-oidc-artifacts70 // The model catalogue's changes (billing's catalogue.rs).
71 if (id === "models") return "/agents";
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)72 // Shared invite links (identity's shared_invites.rs): `invites` is a reserved name, never a workspace's.
73 if (id === SHARED_INVITES_ACCOUNT) return "/invites?tab=shared";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas74 const status = /^(incident|maintenance):([a-z0-9-]{1,64})$/.exec(id);
75 if (status) return status[1] === "incident" ? `/incidents/${status[2]}` : `/incidents/maintenance/${status[2]}`;
sudo: Invoices, Audit log, and follow-ups due76 if (ENTERPRISE.test(id)) return `/enterprises/${encodeURIComponent(id)}`;
77 if (id.startsWith("ws_") && SLUG.test(id.slice(3))) return `/workspaces/${encodeURIComponent(id.slice(3))}`;
78 return null;
79}
80
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)81/** Where sudo's audit log files shared invite links. */
82export const SHARED_INVITES_ACCOUNT = "ws_invites";
83
sudo: Invoices, Audit log, and follow-ups due84/** What to call an account: a workspace by its slug, an enterprise by its name when known. */
85export function accountName(account: string, names: Map<string, string> = new Map()): string {
86 if (names.has(account)) return names.get(account) as string;
Merge branch 'main' into actions-toolkit-oidc-artifacts87 if (account === "models") return "Agents & models";
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)88 if (account === SHARED_INVITES_ACCOUNT) return "Shared invite links";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas89 if (account.startsWith("incident:")) return "Incident";
90 if (account.startsWith("maintenance:")) return "Maintenance";
sudo: Invoices, Audit log, and follow-ups due91 if (account.startsWith("ws_")) return account.slice(3);
92 if (ENTERPRISE.test(account)) return "an enterprise";
93 return account;
94}
95
96// --- Audit ----------------------------------------------------------------------
97
98/** Each kind of change, as staff read it. */
99export const AUDIT_ACTIONS: Record<string, string> = {
100 terms: "Terms changed",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look101 allowances: "Plan, pools and caps changed",
102 payment: "Bank transfer recorded",
103 goodwill: "Goodwill credit",
104 request: "Request answered",
sudo: Invoices, Audit log, and follow-ups due105 create: "Enterprise created",
106 attach: "Workspace added",
107 detach: "Workspace removed",
108 credit: "Credit issued",
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's109 reset: "Billing reset (testing)",
sudo: Invoices, Audit log, and follow-ups due110 billing_link: "Billing link made",
111 billing_email: "Invoice email set",
112 invoice: "Invoice sent",
113 dispute: "Payment disputed",
114 sales: "Sales record changed",
115 note: "Sales note added",
116 stripe: "From Stripe",
117 webhook: "Stripe webhook registered",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member118 close: "Billing closed",
119 // From identity: deleted workspaces staff restored or purged.
120 workspace_restored: "Workspace restored",
121 workspace_purged: "Workspace purged",
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)122 // From identity: accounts staff deleted, restored or purged.
123 account_deleted: "Account deleted",
124 account_restored: "Account restored",
125 account_purged: "Account purged",
Merge branch 'worktree-agent-a8385d293d42c913a'126 // From identity: workspace aliases staff set or removed.
127 alias_added: "Alias added",
128 alias_removed: "Alias removed",
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)129 // From identity: shared invite links staff made or revoked, filed under `ws_invites`.
130 shared_invite_created: "Shared invite link made",
131 shared_invite_revoked: "Shared invite link revoked",
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas132 // From the status page (apps/status), merged in by the Audit log page.
133 incident_declared: "Incident declared",
134 incident_detected: "Incident detected",
135 incident_update: "Incident update",
136 incident_note: "Incident note",
137 incident_resolved: "Incident resolved",
138 incident_roles: "Incident roles",
139 incident_published: "Incident published",
140 incident_dismissed: "Draft dismissed",
141 incident_followup: "Incident follow-up",
142 postmortem_saved: "Postmortem saved",
143 postmortem_published: "Postmortem published",
144 postmortem_unpublished: "Postmortem taken down",
145 maintenance_scheduled: "Maintenance scheduled",
146 maintenance_update: "Maintenance update",
147 maintenance_started: "Maintenance started",
148 maintenance_completed: "Maintenance completed",
149 maintenance_cancelled: "Maintenance cancelled",
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging150 credit_revoked: "Credit revoked",
151 credit_expired: "Credit expired",
Merge branch 'main' into actions-toolkit-oidc-artifacts152 // The model catalogue (Agents & models).
153 models_discovered: "Models found or gone",
154 model_approved: "Model approved",
155 model_retired: "Model retired",
156 model_restored: "Model restored",
157 model_default: "Model default changed",
sudo: Invoices, Audit log, and follow-ups due158};
159
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas160/**
161 * The status page's audit lines as the Audit log lists billing's: the
162 * account is `incident:<id>` or `maintenance:<id>`, which link to their
163 * pages in sudo.
164 */
165export function statusAuditActions(entries: { id: string; at: string; by: string; action: string; target: string; detail: string }[]): AdminAction[] {
166 return entries.map((e) => ({
167 id: `status-${e.id}`,
168 account: `${e.action.startsWith("maintenance_") ? "maintenance" : "incident"}:${e.target}`,
169 action: e.action,
170 detail: e.detail,
171 by: e.by,
172 createdAt: e.at,
173 }));
174}
175
176/** Billing's lines and the status page's, newest first, one page's worth. */
177export function mergeAudit(a: AdminAction[], b: AdminAction[], page = AUDIT_PAGE): AdminAction[] {
178 return [...a, ...b].sort((x, y) => y.createdAt.localeCompare(x.createdAt)).slice(0, page);
179}
180
sudo: Invoices, Audit log, and follow-ups due181export function actionLabel(action: string): string {
182 return AUDIT_ACTIONS[action] ?? action.replace(/_/g, " ").replace(/^./, (char) => char.toUpperCase());
183}
184
185export const AUDIT_PAGE = 100;
186
187/** Where the next, older page starts: after the last line, if this page was full. */
188export function olderBefore(actions: Pick<AdminAction, "createdAt">[], page = AUDIT_PAGE): string | null {
189 return actions.length >= page ? (actions.at(-1)?.createdAt ?? null) : null;
190}
191
192/** A staff email filter: lowercased, at most one address's length. */
193export function parseBy(value: string | null): string | null {
194 const by = (value ?? "").trim().toLowerCase();
195 return by && by.length <= 254 && !/\s/.test(by) ? by : null;
196}
197
198export function parseAction(value: string | null): string | null {
199 const action = (value ?? "").trim();
200 return /^[a-z_]{1,40}$/.test(action) ? action : null;
201}
202
203/** An RFC 3339 time to page from. */
204export function parseBefore(value: string | null): string | null {
205 if (!value || value.length > 40 || Number.isNaN(new Date(value).getTime())) return null;
206 return value;
207}
208
209export function auditHref({ by, action, before }: { by?: string | null; action?: string | null; before?: string | null }): string {
210 const params = new URLSearchParams();
211 if (by) params.set("by", by);
212 if (action) params.set("action", action);
213 if (before) params.set("before", before);
214 const query = params.toString();
215 return query ? `/audit?${query}` : "/audit";
216}

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