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.
| Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides | 1 | # sudo |
| 2 | ||
| 3 | g1t's staff console, at <https://sudo.g1t.sh>: the back office g1t is | |
| 4 | building for itself, for sales, support and finance. It is organised the | |
| 5 | way customers know g1t: by **workspace**. | |
| 6 | ||
| 7 | The sidebar (`app/lib/nav.ts`) has Overview and Reach out at the top, then | |
| 8 | sections that fold open to their pages: Customers, Revenue, Platform, | |
| 9 | Support and Team. Each fold is a `<details>`, drawn open for the section | |
| 10 | holding the current page; on a phone the same menu sits behind a "Menu" | |
| 11 | button in the top bar. Pages not built yet are marked **Soon**: each is a | |
| 12 | real page (`routes/soon.tsx`, made from its entry in `nav.ts`) saying what | |
| 13 | it will do, why, and what it will have, so the sidebar doubles as the | |
| 14 | roadmap. | |
| 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. | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 57 | - **Suspend and clean up** (`/workspaces/<slug>/abuse`, linked from a |
| 58 | workspace's page): suspend the workspace, with a reason (identity's | |
| 59 | `admin_suspend_workspace`): its own tokens stop, and a billing hold | |
| 60 | (`Suspended: <reason>`) stops its runs, checks, deploys, new | |
| 61 | self-hosted runners and telemetry. Lifting it lifts that hold, never one | |
| 62 | staff set otherwise. Remove its self-hosted runners one at a time or all | |
| 63 | at once, which also ends its registration tokens (actions' | |
| 64 | `admin_runners`, `admin_remove_runners`), and revoke its projects' | |
| 65 | telemetry ingest tokens. Every change is in sudo's audit log. | |
| Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides | 66 | - **Deleted workspaces** (`/workspaces/deleted`, linked from Workspaces): |
| 67 | workspaces their owners deleted, newest first (`admin_deleted_workspaces`), | |
| 68 | each with who deleted it and when, when it is purged, what went with it | |
| 69 | (repositories, projects, members, counted at the deletion) and the days | |
| 70 | left. An owner deletes a workspace with everything in it in one step, and | |
| 71 | identity keeps it 30 days (`WORKSPACE_RESTORE_DAYS`) so support can undo a | |
| 72 | deletion that was a mistake or not theirs to make. **Restore** | |
| 73 | (`admin_restore_workspace`) brings it back with its members and tokens, | |
| 74 | and its repositories, projects and apps with `workspace.restored`; its | |
| 75 | plan stays ended, so its owners start it again from Billing. Check that | |
| 76 | whoever asks is an owner of it before restoring. **Purge now** | |
| 77 | (`admin_purge_workspace`, the slug typed to confirm) removes it at once, | |
| 78 | as the sweep does every 15 minutes once its 30 days are up; never for a | |
| 79 | protected workspace. Both go in the workspace's audit log, as g1t, and in | |
| 80 | sudo's (`workspace_restored`, `workspace_purged`), naming the staff | |
| 81 | member. | |
| Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037) | 82 | - **A person's page** (`/users/<username>`, linked from a workspace's |
| 83 | members): their addresses (remove one, with a reason they see), their | |
| Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038) | 84 | security log, and **Delete account**. An account made with a shared |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 85 | invite link, before registration opened, says **Joined through |
| 86 | <label>** under its name. Delete only when the person asks | |
| Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037) | 87 | (from one of the account's confirmed addresses) or for abuse: give the |
| 88 | reason, which goes in sudo's audit log (`account_deleted`), and type the | |
| 89 | username (`admin_delete_account`). It does what deleting their own | |
| 90 | account from Settings does: signs them out everywhere, ends their tokens, | |
| 91 | SSH keys, deploy keys they added and applications, takes them out of | |
| 92 | every workspace, team and repository, and emails their addresses that | |
| Merge sudo: delete an account with the workspaces it alone owns, purge each | 93 | staff deleted it. While the account is the **only owner of a live |
| 94 | workspace**, the page lists those workspaces, each linking to its page, | |
| 95 | and the form becomes **Delete account and the workspaces it alone | |
| 96 | owns**: the reason, the username typed, and a box ticked to say the | |
| 97 | named workspaces go too (`admin_delete_account` with | |
| 98 | `withSoleWorkspaces`). Use it for an account g1t no longer needs, such | |
| 99 | as a retired test account and its personal workspace; for a customer, | |
| 100 | prefer another owner first (an owner makes one under People). Identity | |
| 101 | checks every one of those workspaces before anything is deleted: one | |
| 102 | that is protected, or whose billing cannot settle (`close_workspace`: | |
| 103 | an unpaid invoice, prepaid credit, usage still metering, an enterprise | |
| 104 | account), refuses the whole deletion, and the page says which and why | |
| 105 | beside each, instead of the form. Then it deletes each workspace exactly | |
| 106 | as its owner would (billing closes it, `workspace.deleting`, its | |
| 107 | repositories and apps go with it, kept 30 days), with the staff member | |
| 108 | as who deleted it, and the account last. Each workspace is recorded in | |
| 109 | its own audit log as g1t (rule `staff`) and in sudo's | |
| 110 | (`workspace_deleted`, with the reason); the account in sudo's | |
| 111 | (`account_deleted`, naming the workspaces). Should one fail on the way | |
| 112 | (a card declined that moment), the account is not deleted and the | |
| 113 | error names the workspace and any that went before; restore those from | |
| 114 | Deleted workspaces, or try again. Accounts that can never be deleted (`g1t`, `g1t-agent`, `ghost`, | |
| Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037) | 115 | and whatever identity's `PROTECTED_ACCOUNTS` names, by username or id) |
| 116 | are marked **Protected** and offer no form. A deleted account's page | |
| 117 | says so, with who deleted it, why, when it is purged, and **Restore** and | |
| Merge sudo: delete an account with the workspaces it alone owns, purge each | 118 | **Purge now**, as on Deleted accounts; both work at once, with no wait. |
| 119 | When workspaces were deleted with it, it lists them too, each with its | |
| 120 | own **Purge now** (`admin_purge_workspace`, the slug typed; identity | |
| 121 | purges only a workspace that is still deleted, never a protected one), | |
| 122 | so staff can remove everything straight away. Purge the workspaces | |
| 123 | first: once the account is purged its page is gone (they stay on | |
| 124 | Deleted workspaces). To undo it all, restore the account first, then | |
| 125 | each workspace, so it comes back with its owner. | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 126 | - **Suspending an account**, on a person's page: with a reason, it cannot |
| 127 | sign in (with a password or GitHub), is signed out everywhere, and its | |
| 128 | tokens, SSH keys and git passwords stop until the suspension is lifted | |
| 129 | (identity's `admin_suspend_user`). Nothing is deleted. | |
| 130 | - **Blocked domains** (`/blocked-domains`, under Platform): email domains, | |
| 131 | and their subdomains, no new account may sign up with or add, each with | |
| 132 | a reason (`admin_blocked_domains`, `admin_block_domain`). Accounts | |
| 133 | already there are not touched. | |
| Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037) | 134 | - **Deleted accounts** (`/users/deleted`, linked from Workspaces): |
| 135 | accounts deleted by the person or by staff, newest first | |
| 136 | (`admin_deleted_accounts`), each with who deleted it (the person, or the | |
| 137 | staff member and why), when it is purged, the days left and what it | |
| 138 | left (workspaces, teams, repositories, tokens, SSH keys). Identity keeps | |
| 139 | each 30 days (`ACCOUNT_RESTORE_DAYS`). **Restore** | |
| 140 | (`admin_restore_account`) clears the deletion and puts back the | |
| 141 | memberships, teams and repository roles it left where they still exist; | |
| 142 | its sessions, tokens and keys stay ended, and the person signs in with | |
| 143 | their password. Check that whoever asks owns one of its addresses first. | |
| 144 | **Purge now** (`admin_purge_account`, the username typed) removes it at | |
| 145 | once, as the sweep does every 15 minutes once its 30 days are up: its | |
| 146 | row, addresses, keys, two-factor secret, GitHub link, profile and | |
| 147 | security log go; its username is kept in `deleted_users` and never given | |
| 148 | out again; what it wrote shows as `ghost`. Both go in sudo's audit log | |
| 149 | (`account_restored`, `account_purged`), naming the staff member. There | |
| 150 | is no API route for deleting an account; only the site and sudo can. | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 151 | - **Invites** (`/invites`): anyone may register on g1t.sh, so there is |
| 152 | no waitlist, allowance or shared link to manage. The page lists every | |
| 153 | invite code sent with an invitation into a workspace or onto a | |
| 154 | repository, searchable by a code's start, an email or a username, and | |
| 155 | revokes a pending one (`admin_invites`, `admin_revoke_invite`). The | |
| 156 | old waitlist, grants and shared links stay in identity's database, | |
| 157 | untouched. | |
| Merge branch 'worktree-agent-a8385d293d42c913a' | 158 | - **Aliases** (`/aliases`, under Customers): names that lead to a |
| 159 | workspace, set by staff only; there is no way for a customer to make | |
| 160 | one, and nothing user-facing mentions them. `g1t`, the product's name, | |
| 161 | leads to `flagon-io`, Flagon, Inc. (seeded by identity's migration | |
| 162 | `0029_workspace_aliases.sql`), so nobody mistakes the trading name for | |
| 163 | the organization. Every address under an alias leads to the workspace: | |
| 164 | pages answer with a 301 to the same page (`/g1t/g1t/issues` to | |
| 165 | `/flagon-io/g1t/issues`), git over HTTPS is answered in place as the | |
| 166 | workspace's repository (pushes do not follow redirects), the API and MCP | |
| 167 | run the call again under the workspace's slug, and the package | |
| 168 | registries answer a 301 (308 for a publish). An alias points at the | |
| 169 | workspace's id, so it follows a rename; it goes when the workspace is | |
| 170 | purged. Each row shows the workspace, why the alias exists, and who added | |
| 171 | it and when. **Add** (`admin_set_alias`) takes the alias, the | |
| 172 | workspace's slug and why: identity refuses the site's own routes | |
| 173 | (`settings`, `api`…), anyone's username, a workspace's slug (deleted, or | |
| 174 | held after a rename for another workspace) and an existing alias. | |
| 175 | Reserved names such as `g1t` can be aliases, and an alias is nobody's to | |
| 176 | register or rename a workspace to while it exists. **Remove** | |
| 177 | (`admin_remove_alias`) needs a reason. Both go in sudo's audit log | |
| 178 | (`alias_added`, `alias_removed`), naming the staff member. `@g1t` in | |
| 179 | text still means g1t's agent: it links to how the agent works, never to | |
| 180 | `/g1t`. | |
| Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides | 181 | - **Enterprises**: customers that pay for several workspaces with one |
| 182 | bill, one limit and one set of terms. Each has its workspaces (add or | |
| 183 | remove them), combined usage, terms, credits, ledger and audit log, and | |
| 184 | **Invoices**: where they go (the billing email, which also makes its | |
| 185 | Stripe customer), a "Send invoice now" button, and every invoice with | |
| 186 | its status (open, paid, overdue, void), a line per workspace, and a link | |
| 187 | to Stripe's hosted invoice page. An invoice also goes out on its own as | |
| 188 | each month closes: one Stripe invoice, a line per workspace for what it | |
| 189 | owes, net 30, emailed by Stripe. | |
| 190 | - **Stripe**: whether billing's key is in test or live mode (or off), the | |
| 191 | webhook Stripe calls (URL, endpoint id, events, who registered it and | |
| 192 | when), and the events Stripe sent lately with what billing did with | |
| 193 | each. "Register webhook" (or "Replace") has billing delete the endpoint | |
| 194 | it made before, create a new one and keep its signing secret, which no | |
| 195 | one sees. Do it once per mode, and again after switching to live keys. | |
| 196 | ||
| 197 | Sales changes are not money, so they have no confirmation step; they are | |
| 198 | still POSTs from sudo's own pages, recorded with who made them. | |
| 199 | ||
| 200 | Billing's internal account ids (`ws_<slug>` for a workspace's own, | |
| 201 | `ent_…` for an enterprise) are never shown as names; an enterprise's id | |
| 202 | appears only as small "Billing account id" text. Old `/accounts/…` links | |
| 203 | redirect to the workspace or enterprise they meant. | |
| 204 | ||
| 205 | **Cards stay on Stripe.** sudo never shows a card field. To help a customer | |
| 206 | update their card or see invoices, staff make a Stripe billing link on the | |
| 207 | workspace's page (it is recorded) and send it to the owner. | |
| 208 | ||
| 209 | It holds no data. Workspaces, owners and members come from identity's | |
| 210 | staff methods (`admin_workspaces`, `admin_workspace`; `IdentityAdminApi` in | |
| 211 | `packages/contracts/src/identity.ts`); everything about money goes to the | |
| 212 | billing service's (`admin_*`, `BillingAdminApi` in | |
| 213 | `packages/contracts/src/billing.ts`), where each change is recorded with | |
| 214 | the staff member's email. Both are reached over service bindings only, and | |
| 215 | nothing but sudo binds to them. | |
| 216 | ||
| 217 | ## How it is locked | |
| 218 | ||
| 219 | 1. **Cloudflare Access** sits in front of `sudo.g1t.sh` and signs people in. | |
| 220 | 2. **The worker checks Access's work** on every request, the stylesheet | |
| 221 | included (`run_worker_first`): it verifies the `Cf-Access-Jwt-Assertion` | |
| 222 | JWT itself (RS256 against the team's published keys, audience, issuer, | |
| 223 | expiry), then requires its email to be in `STAFF_EMAILS`. That email is | |
| 224 | who every change is recorded as. See `app/lib/access.ts`. | |
| sudo: Access service tokens listed in STAFF_SERVICE_TOKENS are staff, recorded as <name>@service.g1t.sh | 225 | **Service tokens.** A program (Claude, working without a browser) signs |
| 226 | in with an Access service token instead: it sends `CF-Access-Client-Id` | |
| 227 | and `CF-Access-Client-Secret`, the Access application has a policy with | |
| 228 | the **Service Auth** action that includes the token, and Access sends | |
| 229 | sudo a JWT with no email whose `common_name` is the token's client id. | |
| 230 | sudo lets it in only if that client id is in `STAFF_SERVICE_TOKENS` | |
| 231 | (`<client id>=<name>`, comma separated, in `wrangler.jsonc`), after the | |
| 232 | same signature, audience, issuer and expiry checks, and records it as | |
| 233 | `<name>@service.g1t.sh`: `claude@service.g1t.sh` for | |
| 234 | `claude-sudo`. Its secret lives in `.credentials/sudo-service-token.json` | |
| 235 | and is used by `scripts/ops/sudo.mjs`, which sends sudo's own `Origin` | |
| 236 | with each POST so the same-origin check applies to it as to a browser. | |
| 237 | To take its access away, remove its entry here, or delete or revoke the | |
| 238 | token in Zero Trust. | |
| Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides | 239 | 3. **It fails closed.** Until `ACCESS_TEAM_DOMAIN`, `ACCESS_AUD` and |
| 240 | `STAFF_EMAILS` are all set, every request gets a 403 saying sudo is not | |
| 241 | configured. | |
| 242 | 4. **Changes** are POSTs only, and only from sudo's own pages (`Origin`, or | |
| 243 | `Referer`, must be `https://sudo.g1t.sh`). Terms, enterprise moves, new | |
| 244 | enterprises, Stripe billing links, invoice emails, invoices and the | |
| 245 | webhook show a confirmation step first; a credit needs the workspace's | |
| 246 | slug typed out. | |
| 247 | 5. **The pages ship no JavaScript.** The content security policy forbids | |
| 248 | every script and inline style; responses are `no-store`, `noindex` and | |
| 249 | cannot be framed. The worker has no `workers.dev` address or preview URLs. | |
| 250 | ||
| 251 | ## Setting up Access (once, in the Cloudflare dashboard) | |
| 252 | ||
| 253 | 1. **Zero Trust → Access → Applications → Add an application → Self-hosted.** | |
| 254 | - Application name: `sudo`. | |
| 255 | - Session duration: short, such as 8 hours. | |
| 256 | - Public hostname: `sudo.g1t.sh` (path empty, so it covers everything). | |
| 257 | 2. **Add a policy** (`g1t staff`): action *Allow*, include *Emails* → the | |
| 258 | owner's address, and *Emails ending in* → `g1t.sh` for everyone with a | |
| 259 | g1t address (the same entries as `STAFF_EMAILS`, where a domain is | |
| 260 | written `@g1t.sh`). Add more staff here *and* in `STAFF_EMAILS`; either | |
| 261 | one alone is not enough. | |
| 262 | 3. Save, then open the application's **Overview** (or *Basic information*) | |
| 263 | and copy the **Application Audience (AUD) tag**. | |
| 264 | 4. Find the **team domain** under **Zero Trust → Settings → Custom pages** | |
| 265 | (or *Team name and domain*): it looks like `<team>.cloudflareaccess.com`. | |
| 266 | 5. Put both into `wrangler.jsonc`: | |
| 267 | ||
| 268 | ```jsonc | |
| 269 | "vars": { | |
| 270 | "ACCESS_TEAM_DOMAIN": "<team>.cloudflareaccess.com", | |
| 271 | "ACCESS_AUD": "<the AUD tag>", | |
| 272 | "STAFF_EMAILS": "syntaqx@gmail.com, @g1t.sh" | |
| 273 | } | |
| 274 | ``` | |
| 275 | ||
| 276 | 6. Deploy: `scripts/deploy.sh sudo` (after `billing` and `identity`, whose | |
| 277 | `admin_*` methods it calls). | |
| 278 | ||
| 279 | Visit <https://sudo.g1t.sh>: Access asks you to sign in, then the workspaces | |
| 280 | list opens. Anyone else gets Access's own refusal; anyone Access lets in who | |
| 281 | is not in `STAFF_EMAILS` gets a 403 from the worker. | |
| 282 | ||
| 283 | ## Signing in through WARP | |
| 284 | ||
| 285 | Staff signed in to the Zero Trust org in the Cloudflare One agent (WARP) | |
| 286 | reach sudo without the login page: the org allows WARP sessions as Access | |
| 287 | sign-ins (8 hours), the sudo app accepts them, and the `g1t staff` policy | |
| 288 | is also on the WARP enrollment app, so staff can enroll their devices. | |
| 289 | The same two checks still apply: the Access policy, and `STAFF_EMAILS`. | |
| 290 | ||
| 291 | ## Working on it | |
| 292 | ||
| 293 | ```sh | |
| 294 | npm run typecheck -w @g1t/sudo | |
| Merge branch 'main' into actions-toolkit-oidc-artifacts | 295 | npm test -w @g1t/sudo # JWT verification, forms, money, the workspace join, paging, nav, charts, signals, models |
| Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides | 296 | npm run build -w @g1t/sudo |
| 297 | ``` | |
| 298 | ||
| 299 | `npm run dev` serves the pages, but every request is refused without a real | |
| 300 | Access token, by design. |
This file's history is long; its oldest lines are credited to the oldest commit read.