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/sudo/app/lib/workspaces.ts

148 lines5,378 bytesCodeBlame
1/**
2 * sudo is organised around workspaces, as customers know them. Identity
3 * says which workspaces exist and who owns them; billing says who pays for
4 * each and where it stands. This joins the two, by slug. No Workers
5 * imports, so it can be tested under Node.
6 *
7 * Billing's account ids (`ws_<slug>`, `ent_…`) are internal: they are used
8 * to look things up and never shown as a name.
9 */
10import type { AccountSummary, AdminOwner, AdminWorkspace, Limit, Terms } from "@g1t/contracts";
11
12/** Terms every workspace starts on. */
13export const STANDARD_TERMS: Terms = {
14 kind: "standard",
15 discountPercent: 0,
16 ceilingMicros: null,
17 note: "",
18 until: null,
19 setBy: null,
20 setAt: null,
21};
22
23export type Enterprise = { id: string; name: string };
24
25/** Where a workspace stands with billing. */
26export type WorkspaceBilling = {
27 /** The enterprise that pays for it, or null when it pays for itself. */
28 billedTo: Enterprise | null;
29 /** The terms it is charged on: its own, or its enterprise's. */
30 terms: Terms;
31 /**
32 * Its limit this month: its own, or for a workspace on an enterprise the
33 * enterprise's, which all its workspaces share. Null with no billing
34 * activity yet.
35 */
36 limit: Limit | null;
37 /** This workspace's own figures this month (charged, cost) and ever (paid). */
38 chargedMicros: number;
39 costMicros: number;
40 paidMicros: number;
41};
42
43export type WorkspaceRow = {
44 slug: string;
45 name: string;
46 /** Null for a workspace billing knows and identity returned nothing for. */
47 createdAt: string | null;
48 owners: AdminOwner[];
49 memberCount: number | null;
50 /** False when only billing knows the slug. */
51 known: boolean;
52 billing: WorkspaceBilling;
53};
54
55/** The billing side of every workspace billing knows, by slug. */
56export function billingBySlug(accounts: AccountSummary[]): Map<string, WorkspaceBilling> {
57 const bySlug = new Map<string, WorkspaceBilling>();
58 for (const summary of accounts) {
59 const { account } = summary;
60 const shares = new Map((summary.byWorkspace ?? []).map((share) => [share.workspace, share]));
61 if (account.kind === "enterprise") {
62 for (const slug of account.workspaces) {
63 const share = shares.get(slug);
64 bySlug.set(slug, {
65 billedTo: { id: account.id, name: account.name },
66 terms: account.terms,
67 limit: summary.limit,
68 chargedMicros: share?.chargedMicros ?? 0,
69 costMicros: share?.costMicros ?? 0,
70 paidMicros: share?.paidMicros ?? 0,
71 });
72 }
73 } else {
74 const slug = account.workspaces[0] ?? summary.limit.workspace;
75 // An enterprise's claim on a workspace wins over a stale own account.
76 if (bySlug.get(slug)?.billedTo) continue;
77 bySlug.set(slug, {
78 billedTo: null,
79 terms: account.terms,
80 limit: summary.limit,
81 chargedMicros: summary.chargedMicros,
82 costMicros: summary.costMicros,
83 paidMicros: summary.paidMicros,
84 });
85 }
86 }
87 return bySlug;
88}
89
90/** A workspace with no billing activity: standard terms, nothing charged. */
91export function noBilling(): WorkspaceBilling {
92 return { billedTo: null, terms: STANDARD_TERMS, limit: null, chargedMicros: 0, costMicros: 0, paidMicros: 0 };
93}
94
95/**
96 * Every workspace identity listed, in its order, with its billing; then any
97 * workspace only billing knows. A workspace with no billing activity is
98 * still listed, on standard terms at $0.
99 */
100export function joinWorkspaces(workspaces: AdminWorkspace[], accounts: AccountSummary[]): WorkspaceRow[] {
101 const billing = billingBySlug(accounts);
102 const rows: WorkspaceRow[] = workspaces.map((workspace) => ({
103 slug: workspace.slug,
104 name: workspace.name,
105 createdAt: workspace.createdAt,
106 owners: workspace.owners,
107 memberCount: workspace.memberCount,
108 known: true,
109 billing: billing.get(workspace.slug) ?? noBilling(),
110 }));
111 const listed = new Set(rows.map((row) => row.slug));
112 for (const [slug, entry] of billing) {
113 if (listed.has(slug)) continue;
114 rows.push({ slug, name: slug, createdAt: null, owners: [], memberCount: null, known: false, billing: entry });
115 }
116 return rows;
117}
118
119/** Whether a row matches a search: slug, name, owner, owner's email or enterprise. */
120export function matchesQuery(row: WorkspaceRow, query: string): boolean {
121 const q = query.trim().toLowerCase();
122 if (!q) return true;
123 const haystack = [
124 row.slug,
125 row.name,
126 row.billing.billedTo?.name,
127 ...row.owners.flatMap((owner) => [owner.username, owner.email]),
128 ];
129 return haystack.some((value) => value?.toLowerCase().includes(q));
130}
131
132/**
133 * Where an old `/accounts/<id>` link now lives: a workspace's own account
134 * (`ws_<slug>`, or a bare slug) at its workspace, an enterprise at its own
135 * page. Null for anything else.
136 */
137export function legacyAccountPath(id: string): string | null {
138 const value = id.trim().toLowerCase();
139 if (/^ent_[a-z0-9_-]{1,80}$/.test(value)) return `/enterprises/${encodeURIComponent(value)}`;
140 const slug = value.startsWith("ws_") ? value.slice(3) : value;
141 if (/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/.test(slug)) return `/workspaces/${encodeURIComponent(slug)}`;
142 return null;
143}
144
145/** A row's owners' usernames, as one line. */
146export function ownerNames(owners: AdminOwner[]): string {
147 return owners.map((owner) => owner.username).join(", ");
148}