| 1 | # Current |
| 2 | |
| 3 | > Keep this file short. One active step, one ordered backlog. Completed work moves to |
| 4 | > [progress.md](progress.md). If this file starts reading like a changelog, it has |
| 5 | > drifted — that's exactly what went wrong last time. |
| 6 | |
| 7 | ## Active: Milestone 4b — Push and tokens |
| 8 | |
| 9 | **Goal:** `git push` works over HTTP for someone who may write, and a private repository |
| 10 | is clonable by someone who may read it. Authentication is personal access tokens over |
| 11 | HTTP Basic, per [0001](decisions/0001-git-over-http-not-ssh.md). |
| 12 | |
| 13 | **Out of scope:** SSH, token scopes beyond whatever the Open questions settle, OAuth, |
| 14 | and anything to do with browsing a tree (Milestone 5). |
| 15 | |
| 16 | ### Steps |
| 17 | |
| 18 | - [x] Domain: `PersonalAccessToken`, `TokenId`, `TokenHash`, and the repository port |
| 19 | - [ ] Infrastructure: in-memory + SQLite implementations, migration |
| 20 | - [ ] Application: `issue_token`, `list_tokens`, `revoke_token` |
| 21 | - [ ] Application: `authenticate_token` — resolves a Basic credential into an `Actor` |
| 22 | - [ ] Web: HTTP Basic on the git routes, and the 401 challenge that makes a client |
| 23 | send credentials at all |
| 24 | - [ ] Application: let `serve_git` authorize writes rather than refusing them |
| 25 | - [ ] Web: token management UI under `/{handle}/settings` |
| 26 | - [ ] Verify: push to a public repo, clone a private one, and check a revoked token |
| 27 | stops working |
| 28 | |
| 29 | ### Done when |
| 30 | |
| 31 | A token issued through the UI lets `git push` succeed against a repository its owner may |
| 32 | write, and lets `git clone` succeed against a private repository its owner may read. |
| 33 | Revoking the token stops both. An anonymous clone of a public repository still works |
| 34 | exactly as it does today. |
| 35 | |
| 36 | ### Settled |
| 37 | |
| 38 | All three of this milestone's open decisions are answered in |
| 39 | [0007](decisions/0007-tokens-over-http-basic.md): SHA-256 with a stored display prefix, |
| 40 | no scopes, and a **uniform 401** on any git path not anonymously readable — including |
| 41 | repositories that do not exist, so nothing distinguishes "private" from "absent". |
| 42 | |
| 43 | - **Revocation is a delete, not a flag.** A revoked row that lingers is a credential |
| 44 | that stops working only as long as every read remembers to check the flag. |
| 45 | - **Tokens authenticate; they do not authorize.** A token widens who the actor is; |
| 46 | `serve_git` still decides what that actor may do. |
| 47 | - **`TokenRepository` looks up by hash**, because that is the lookup authentication |
| 48 | actually performs — a client presents a token, never an id. |
| 49 | |
| 50 | ### Open |
| 51 | |
| 52 | Nothing open. |
| 53 | |
| 54 | ### Carried over — small, unblocked |
| 55 | |
| 56 | - **A client that disappears mid-request leaves the body-copy task waiting.** The copy |
| 57 | into git's stdin runs in its own task and nothing cancels it if the connection drops. |
| 58 | Bounded by the backend exiting and closing the pipe, but not by anything deliberate. |
| 59 | - **A subprocess per git request.** Unlike Milestone 3's once-per-creation, this is on a |
| 60 | hot path and has not been measured. Milestone 5 is where that bill comes due. |
| 61 | - **Streaming is by construction, not by measurement.** The response body is never |
| 62 | collected, but no clone large enough to prove it has been run. |
| 63 | - **An orphaned repo directory is possible** if the process dies between the record |
| 64 | write and the filesystem write, and it then blocks re-creating that name. The durable |
| 65 | fix is a reconciliation sweep on boot |
| 66 | ([architecture.md](architecture.md#db-plus-filesystem-writes)); clearing one is a |
| 67 | manual `rm` today, since repo deletion does not exist. |
| 68 | - **The duplicate-name check races.** The loser is caught by `init_bare` or the unique |
| 69 | constraint, but surfaces as an opaque storage error rather than "name taken". |
| 70 | - **Bare repos created on macOS carry `ignorecase = true`.** A migration gotcha if the |
| 71 | data directory ever moves to Linux. |
| 72 | - **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; both fall back |
| 73 | today. Topcoat's `font-fontsource` feature handles it. |
| 74 | - **Light mode is untested.** The palette defines it; nobody has looked at it. |
| 75 | - **No rate limiting** on `/auth/login` or `/auth/setup`. |
| 76 | - **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is |
| 77 | enforced on read, so this is tidiness, not a hole. |
| 78 | - **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable |
| 79 | rather than hypothetical. |
| 80 | |
| 81 | ## Backlog |
| 82 | |
| 83 | Ordered. Pull from the top. |
| 84 | |
| 85 | 1. **Milestone 5 — Repo browsing.** Tree, blob, commit log. **Start with domain value |
| 86 | objects** — `ObjectId`, `RefName`, `TreeEntry` — before any adapter. A query port |
| 87 | returning `String`s is an anaemic pass-through that pushes validation into the page. |
| 88 | Also the point to measure fork/exec cost per page view, and to reconsider `gix` for |
| 89 | the read path ([0006](decisions/0006-git-binary-behind-narrow-ports.md)). |
| 90 | 2. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Still open |
| 91 | whether writing or projects/showcases is the better first portfolio feature. |
| 92 | |
| 93 | ## Open questions |
| 94 | |
| 95 | - **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected |
| 96 | by its own authors). Expect churn that isn't feature work. |
| 97 | - Topcoat ships Tailwind without Node, which reopens the design system attempt #1 |
| 98 | dropped purely to avoid an npm build step — see [ui.md](ui.md). |
| 99 | |
| 100 | ## Routing findings (Milestone 0) |
| 101 | |
| 102 | - **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat` |
| 103 | silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local |
| 104 | stable is now 1.97.1. |
| 105 | - `Router::builder().discover()` collects `#[page]`-annotated items **at link time**, |
| 106 | so pages can live in any module. Layering is our choice, not the framework's. |
| 107 | - `module_router!` derives each URL from the module tree rather than a path string. |
| 108 | Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but |
| 109 | handles sit at the root ([0004](decisions/0004-root-handles-grouped-routes.md)), so a |
| 110 | parameterised root segment still has to coexist with static ones. Worth checking how |
| 111 | `module_router!` handles that before committing to it. |
| 112 | - Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not |
| 113 | injected as handler arguments. Parses are memoized per request. |
| 114 | - Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's |
| 115 | `NotFoundError` to render a branded 404. |
| 116 | - `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone. |
| 117 | - `Body` is a boxed `http_body::Body` used for both requests and responses, with |
| 118 | `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can |
| 119 | stream both directions without buffering. This is what makes Milestone 4 viable. |