| 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 | |
e0856ebfeat: clone a public repository over HTTP8d | 7 | ## Active: Milestone 4b — Push and tokens |
2eb8681docs: put git next, plan the repo model24d | 8 | |
e0856ebfeat: clone a public repository over HTTP8d | 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). |
2eb8681docs: put git next, plan the repo model24d | 12 | |
e0856ebfeat: clone a public repository over HTTP8d | 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). |
2eb8681docs: put git next, plan the repo model24d | 15 | |
| 16 | ### Steps |
| 17 | |
e0856ebfeat: clone a public repository over HTTP8d | 18 | Provisional below the first two — the rest depend on the Open decisions. |
| 19 | |
| 20 | - [ ] Domain: `PersonalAccessToken`, `TokenId`, `TokenHash`, and the repository port |
| 21 | - [ ] Infrastructure: in-memory + SQLite implementations, migration |
| 22 | - [ ] Application: `issue_token`, `list_tokens`, `revoke_token` |
| 23 | - [ ] Application: `authenticate_token` — resolves a Basic credential into an `Actor` |
| 24 | - [ ] Web: HTTP Basic on the git routes, and the 401 challenge that makes a client |
| 25 | send credentials at all |
| 26 | - [ ] Application: let `serve_git` authorize writes rather than refusing them |
| 27 | - [ ] Web: token management UI under `/{handle}/settings` |
| 28 | - [ ] Verify: push to a public repo, clone a private one, and check a revoked token |
| 29 | stops working |
| 30 | |
| 31 | ### Done when |
| 32 | |
e0856ebfeat: clone a public repository over HTTP8d | 33 | A token issued through the UI lets `git push` succeed against a repository its owner may |
| 34 | write, and lets `git clone` succeed against a private repository its owner may read. |
| 35 | Revoking the token stops both. An anonymous clone of a public repository still works |
| 36 | exactly as it does today. |
3954b45docs: record milestone 2 phase 125d | 37 | |
be4fac5docs: correct the Topcoat guidance in CLAUDE.md24d | 38 | ### Open |
| 39 | |
e0856ebfeat: clone a public repository over HTTP8d | 40 | These are decisions, not unknowns — each needs an answer before the step that depends on |
| 41 | it. |
| 42 | |
| 43 | - **How tokens are hashed.** Sessions already hash their token with SHA-256 |
| 44 | (`SessionTokenHash`), which suits a high-entropy random value; Argon2 would be the |
| 45 | password answer and is far too slow for something presented on every git request, of |
| 46 | which a single clone makes several. Recommendation: copy the session approach, store a |
| 47 | display prefix alongside so the UI can name a token without holding it. |
| 48 | - **Whether tokens carry scopes.** Personal-first says no: a token acts as its user. |
| 49 | Scopes are the kind of thing that is cheap to add later behind an unchanged port and |
| 50 | expensive to design against no requirement. |
| 51 | - **401 versus 404 for a private repository.** Carried from 4a and now decidable. A git |
| 52 | client only sends credentials *after* a 401, so answering 404 to an anonymous request |
| 53 | for a private repository — which is what 4a does, and what `view_repo` does — makes |
| 54 | authenticated private clone impossible. Answering 401 leaks that the repository |
| 55 | exists. Gitea and GitHub both accept that leak. This is the one with a real cost |
| 56 | either way. |
be4fac5docs: correct the Topcoat guidance in CLAUDE.md24d | 57 | |
e0856ebfeat: clone a public repository over HTTP8d | 58 | ### Carried over — small, unblocked |
d7b99d9docs: record milestone 0 progress and routing findings1mo | 59 | |
47db238feat: serve the git protocol through http-backend, behind a port8d | 60 | - **A client that disappears mid-request leaves the body-copy task waiting.** The copy |
e0856ebfeat: clone a public repository over HTTP8d | 61 | into git's stdin runs in its own task and nothing cancels it if the connection drops. |
47db238feat: serve the git protocol through http-backend, behind a port8d | 62 | Bounded by the backend exiting and closing the pipe, but not by anything deliberate. |
e0856ebfeat: clone a public repository over HTTP8d | 63 | - **A subprocess per git request.** Unlike Milestone 3's once-per-creation, this is on a |
| 64 | hot path and has not been measured. Milestone 5 is where that bill comes due. |
| 65 | - **Streaming is by construction, not by measurement.** The response body is never |
| 66 | collected, but no clone large enough to prove it has been run. |
6a2a925docs: close Milestone 3, plan Milestone 4a8d | 67 | - **An orphaned repo directory is possible** if the process dies between the record |
| 68 | write and the filesystem write, and it then blocks re-creating that name. The durable |
| 69 | fix is a reconciliation sweep on boot |
| 70 | ([architecture.md](architecture.md#db-plus-filesystem-writes)); clearing one is a |
| 71 | manual `rm` today, since repo deletion does not exist. |
| 72 | - **The duplicate-name check races.** The loser is caught by `init_bare` or the unique |
| 73 | constraint, but surfaces as an opaque storage error rather than "name taken". |
| 74 | - **Bare repos created on macOS carry `ignorecase = true`.** A migration gotcha if the |
| 75 | data directory ever moves to Linux. |
2eb8681docs: put git next, plan the repo model24d | 76 | - **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; both fall back |
| 77 | today. Topcoat's `font-fontsource` feature handles it. |
| 78 | - **Light mode is untested.** The palette defines it; nobody has looked at it. |
f0444b7docs: plan milestone 2 in two phases1mo | 79 | - **No rate limiting** on `/auth/login` or `/auth/setup`. |
076dbc9docs: close milestone 1, open milestone 21mo | 80 | - **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is |
| 81 | enforced on read, so this is tidiness, not a hole. |
2eb8681docs: put git next, plan the repo model24d | 82 | - **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable |
| 83 | rather than hypothetical. |
| 84 | |
| 85 | ## Backlog |
| 86 | |
| 87 | Ordered. Pull from the top. |
| 88 | |
e0856ebfeat: clone a public repository over HTTP8d | 89 | 1. **Milestone 5 — Repo browsing.** Tree, blob, commit log. **Start with domain value |
02eb2e4feat: GitStorage port and DiskGitStorage24d | 90 | objects** — `ObjectId`, `RefName`, `TreeEntry` — before any adapter. A query port |
| 91 | returning `String`s is an anaemic pass-through that pushes validation into the page. |
| 92 | Also the point to measure fork/exec cost per page view, and to reconsider `gix` for |
| 93 | the read path ([0006](decisions/0006-git-binary-behind-narrow-ports.md)). |
e0856ebfeat: clone a public repository over HTTP8d | 94 | 2. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Still open |
2eb8681docs: put git next, plan the repo model24d | 95 | whether writing or projects/showcases is the better first portfolio feature. |
| 96 | |
| 97 | ## Open questions |
| 98 | |
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo | 99 | - **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected |
| 100 | by its own authors). Expect churn that isn't feature work. |
| 101 | - Topcoat ships Tailwind without Node, which reopens the design system attempt #1 |
| 102 | dropped purely to avoid an npm build step — see [ui.md](ui.md). |
| 103 | |
| 104 | ## Routing findings (Milestone 0) |
| 105 | |
| 106 | - **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat` |
| 107 | silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local |
| 108 | stable is now 1.97.1. |
| 109 | - `Router::builder().discover()` collects `#[page]`-annotated items **at link time**, |
| 110 | so pages can live in any module. Layering is our choice, not the framework's. |
| 111 | - `module_router!` derives each URL from the module tree rather than a path string. |
aaefaabfeat: root handles, grouped routes, reserved-handle denylist1mo | 112 | Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but |
| 113 | handles sit at the root ([0004](decisions/0004-root-handles-grouped-routes.md)), so a |
| 114 | parameterised root segment still has to coexist with static ones. Worth checking how |
| 115 | `module_router!` handles that before committing to it. |
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo | 116 | - Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not |
| 117 | injected as handler arguments. Parses are memoized per request. |
| 118 | - Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's |
| 119 | `NotFoundError` to render a branded 404. |
| 120 | - `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone. |
| 121 | - `Body` is a boxed `http_body::Body` used for both requests and responses, with |
| 122 | `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can |
ca76e1bdocs: fix milestone cross-references after the reorder24d | 123 | stream both directions without buffering. This is what makes Milestone 4 viable. |