Skip to content

g1t/apps/sudo/README.md

201 lines11,554 bytesCodeBlame
1# sudo
2
3g1t's staff console, at <https://sudo.g1t.sh>: the back office g1t is
4building for itself, for sales, support and finance. It is organised the
5way customers know g1t: by **workspace**.
6
7The sidebar (`app/lib/nav.ts`) has Overview and Reach out at the top, then
8sections that fold open to their pages: Customers, Revenue, Platform,
9Support and Team. Each fold is a `<details>`, drawn open for the section
10holding the current page; on a phone the same menu sits behind a "Menu"
11button in the top bar. Pages not built yet are marked **Soon**: each is a
12real page (`routes/soon.tsx`, made from its entry in `nav.ts`) saying what
13it will do, why, and what it will have, so the sidebar doubles as the
14roadmap.
15
16- **Overview** (`/`): this month charged, cost and margin; the last six
17 months as a chart; this month by kind of usage; paying workspaces; how
18 many are stopped, near their limit or declined (each a link into Reach
19 out); open invoices; follow-ups due; and the five most urgent signals.
20 From billing's `admin_overview` and `admin_signals`.
21- **Reach out** (`/reach-out`): every workspace worth a word, most urgent
22 first (at limit, declined, near limit, high spend, growing, established,
23 first payment), with its owners, the reason in a sentence, the figure,
24 and its sales stage and owner at g1t. Filter by why and by whose
25 (everyone's, unassigned, mine), or show only **follow-ups due** (a next
26 step due today or earlier, on a deal not won or lost; one per
27 workspace). Rows show the next step and its day. Each row opens the
28 workspace's Sales.
29- **Invoices** (`/invoices`): every invoice g1t has sent, workspaces' and
30 enterprises', newest first (`admin_invoices`, at most 200). Filter by
31 status and month; totals for what is listed (amount, paid, outstanding);
32 each links to its workspace or enterprise and to Stripe's page (https
33 only).
34- **Audit log** (`/audit`): every change made in sudo, and what Stripe told
35 billing, newest first, 100 a page with "Older" (`admin_audit`). Filter by
36 staff email and kind of change; each line links to its workspace or
37 enterprise.
38- **Workspaces** (`/workspaces`): every workspace, newest first, 50 to a
39 page, with its owners, members, who it is billed to, its terms, this
40 month's usage against its limit, what it was charged and what it cost
41 g1t. Search by workspace, owner, email or enterprise (across the whole
42 list); filter to stopped or warning, comped or custom, or on an
43 enterprise. Billing's figures are fetched for exactly the page shown, so
44 the filters, and the totals over the list, cover that page; the page
45 says so when there is more than one. A workspace's page shows its members;
46 **Sales** (its stage, the staff member who has it, the next step and its
47 date, and notes, newest first; `admin_sales`, `admin_set_sales`,
48 `admin_add_note`); and under **Billing** its limit in words (trust, the
49 owners' own spend limit or the default, the most they may set, how it
50 grows), its last six months as a chart, its invoices (`admin_workspace_invoices`,
51 with Stripe's page and PDF), its terms, who it is billed to (move it onto or off an
52 enterprise), a credit form, a Stripe billing link, its ledger and its
53 audit log. If billing does not answer for sales or invoices, the page
54 still opens and says so in those sections. A protected workspace (one
55 nobody can ever delete: identity's `PROTECTED_WORKSPACES`, and
56 flagon-io always) says so beside its name.
57- **Deleted workspaces** (`/workspaces/deleted`, linked from Workspaces):
58 workspaces their owners deleted, newest first (`admin_deleted_workspaces`),
59 each with who deleted it and when, when it is purged, what went with it
60 (repositories, projects, members, counted at the deletion) and the days
61 left. An owner deletes a workspace with everything in it in one step, and
62 identity keeps it 30 days (`WORKSPACE_RESTORE_DAYS`) so support can undo a
63 deletion that was a mistake or not theirs to make. **Restore**
64 (`admin_restore_workspace`) brings it back with its members and tokens,
65 and its repositories, projects and apps with `workspace.restored`; its
66 plan stays ended, so its owners start it again from Billing. Check that
67 whoever asks is an owner of it before restoring. **Purge now**
68 (`admin_purge_workspace`, the slug typed to confirm) removes it at once,
69 as the sweep does every 15 minutes once its 30 days are up; never for a
70 protected workspace. Both go in the workspace's audit log, as g1t, and in
71 sudo's (`workspace_restored`, `workspace_purged`), naming the staff
72 member.
73- **Aliases** (`/aliases`, under Customers): names that lead to a
74 workspace, set by staff only; there is no way for a customer to make
75 one, and nothing user-facing mentions them. `g1t`, the product's name,
76 leads to `flagon-io`, Flagon, Inc. (seeded by identity's migration
77 `0029_workspace_aliases.sql`), so nobody mistakes the trading name for
78 the organization. Every address under an alias leads to the workspace:
79 pages answer with a 301 to the same page (`/g1t/g1t/issues` to
80 `/flagon-io/g1t/issues`), git over HTTPS is answered in place as the
81 workspace's repository (pushes do not follow redirects), the API and MCP
82 run the call again under the workspace's slug, and the package
83 registries answer a 301 (308 for a publish). An alias points at the
84 workspace's id, so it follows a rename; it goes when the workspace is
85 purged. Each row shows the workspace, why the alias exists, and who added
86 it and when. **Add** (`admin_set_alias`) takes the alias, the
87 workspace's slug and why: identity refuses the site's own routes
88 (`settings`, `api`…), anyone's username, a workspace's slug (deleted, or
89 held after a rename for another workspace) and an existing alias.
90 Reserved names such as `g1t` can be aliases, and an alias is nobody's to
91 register or rename a workspace to while it exists. **Remove**
92 (`admin_remove_alias`) needs a reason. Both go in sudo's audit log
93 (`alias_added`, `alias_removed`), naming the staff member. `@g1t` in
94 text still means g1t's agent: it links to how the agent works, never to
95 `/g1t`.
96- **Enterprises**: customers that pay for several workspaces with one
97 bill, one limit and one set of terms. Each has its workspaces (add or
98 remove them), combined usage, terms, credits, ledger and audit log, and
99 **Invoices**: where they go (the billing email, which also makes its
100 Stripe customer), a "Send invoice now" button, and every invoice with
101 its status (open, paid, overdue, void), a line per workspace, and a link
102 to Stripe's hosted invoice page. An invoice also goes out on its own as
103 each month closes: one Stripe invoice, a line per workspace for what it
104 owes, net 30, emailed by Stripe.
105- **Stripe**: whether billing's key is in test or live mode (or off), the
106 webhook Stripe calls (URL, endpoint id, events, who registered it and
107 when), and the events Stripe sent lately with what billing did with
108 each. "Register webhook" (or "Replace") has billing delete the endpoint
109 it made before, create a new one and keep its signing secret, which no
110 one sees. Do it once per mode, and again after switching to live keys.
111
112Sales changes are not money, so they have no confirmation step; they are
113still POSTs from sudo's own pages, recorded with who made them.
114
115Billing's internal account ids (`ws_<slug>` for a workspace's own,
116`ent_…` for an enterprise) are never shown as names; an enterprise's id
117appears only as small "Billing account id" text. Old `/accounts/…` links
118redirect to the workspace or enterprise they meant.
119
120**Cards stay on Stripe.** sudo never shows a card field. To help a customer
121update their card or see invoices, staff make a Stripe billing link on the
122workspace's page (it is recorded) and send it to the owner.
123
124It holds no data. Workspaces, owners and members come from identity's
125staff methods (`admin_workspaces`, `admin_workspace`; `IdentityAdminApi` in
126`packages/contracts/src/identity.ts`); everything about money goes to the
127billing service's (`admin_*`, `BillingAdminApi` in
128`packages/contracts/src/billing.ts`), where each change is recorded with
129the staff member's email. Both are reached over service bindings only, and
130nothing but sudo binds to them.
131
132## How it is locked
133
1341. **Cloudflare Access** sits in front of `sudo.g1t.sh` and signs people in.
1352. **The worker checks Access's work** on every request, the stylesheet
136 included (`run_worker_first`): it verifies the `Cf-Access-Jwt-Assertion`
137 JWT itself (RS256 against the team's published keys, audience, issuer,
138 expiry), then requires its email to be in `STAFF_EMAILS`. That email is
139 who every change is recorded as. See `app/lib/access.ts`.
1403. **It fails closed.** Until `ACCESS_TEAM_DOMAIN`, `ACCESS_AUD` and
141 `STAFF_EMAILS` are all set, every request gets a 403 saying sudo is not
142 configured.
1434. **Changes** are POSTs only, and only from sudo's own pages (`Origin`, or
144 `Referer`, must be `https://sudo.g1t.sh`). Terms, enterprise moves, new
145 enterprises, Stripe billing links, invoice emails, invoices and the
146 webhook show a confirmation step first; a credit needs the workspace's
147 slug typed out.
1485. **The pages ship no JavaScript.** The content security policy forbids
149 every script and inline style; responses are `no-store`, `noindex` and
150 cannot be framed. The worker has no `workers.dev` address or preview URLs.
151
152## Setting up Access (once, in the Cloudflare dashboard)
153
1541. **Zero Trust → Access → Applications → Add an application → Self-hosted.**
155 - Application name: `sudo`.
156 - Session duration: short, such as 8 hours.
157 - Public hostname: `sudo.g1t.sh` (path empty, so it covers everything).
1582. **Add a policy** (`g1t staff`): action *Allow*, include *Emails* → the
159 owner's address, and *Emails ending in* → `g1t.sh` for everyone with a
160 g1t address (the same entries as `STAFF_EMAILS`, where a domain is
161 written `@g1t.sh`). Add more staff here *and* in `STAFF_EMAILS`; either
162 one alone is not enough.
1633. Save, then open the application's **Overview** (or *Basic information*)
164 and copy the **Application Audience (AUD) tag**.
1654. Find the **team domain** under **Zero Trust → Settings → Custom pages**
166 (or *Team name and domain*): it looks like `<team>.cloudflareaccess.com`.
1675. Put both into `wrangler.jsonc`:
168
169 ```jsonc
170 "vars": {
171 "ACCESS_TEAM_DOMAIN": "<team>.cloudflareaccess.com",
172 "ACCESS_AUD": "<the AUD tag>",
173 "STAFF_EMAILS": "syntaqx@gmail.com, @g1t.sh"
174 }
175 ```
176
1776. Deploy: `scripts/deploy.sh sudo` (after `billing` and `identity`, whose
178 `admin_*` methods it calls).
179
180Visit <https://sudo.g1t.sh>: Access asks you to sign in, then the workspaces
181list opens. Anyone else gets Access's own refusal; anyone Access lets in who
182is not in `STAFF_EMAILS` gets a 403 from the worker.
183
184## Signing in through WARP
185
186Staff signed in to the Zero Trust org in the Cloudflare One agent (WARP)
187reach sudo without the login page: the org allows WARP sessions as Access
188sign-ins (8 hours), the sudo app accepts them, and the `g1t staff` policy
189is also on the WARP enrollment app, so staff can enroll their devices.
190The same two checks still apply: the Access policy, and `STAFF_EMAILS`.
191
192## Working on it
193
194```sh
195npm run typecheck -w @g1t/sudo
196npm test -w @g1t/sudo # JWT verification, forms, money, the workspace join, paging, nav, charts, signals
197npm run build -w @g1t/sudo
198```
199
200`npm run dev` serves the pages, but every request is refused without a real
201Access token, by design.