steid

@jamesgill /

steid/plans/current.md
6.9 KBCode·Blame·Raw
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
10is clonable by someone who may read it. Authentication is personal access tokens over
11HTTP 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,
14and anything to do with browsing a tree (Milestone 5).
15
16### Steps
17
18Provisional 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
33A token issued through the UI lets `git push` succeed against a repository its owner may
34write, and lets `git clone` succeed against a private repository its owner may read.
35Revoking the token stops both. An anonymous clone of a public repository still works
36exactly as it does today.
37
38### Open
39
40These are decisions, not unknowns — each needs an answer before the step that depends on
41it.
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.
57
58### Carried over — small, unblocked
59
60- **A client that disappears mid-request leaves the body-copy task waiting.** The copy
61 into git's stdin runs in its own task and nothing cancels it if the connection drops.
62 Bounded by the backend exiting and closing the pipe, but not by anything deliberate.
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.
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.
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.
79- **No rate limiting** on `/auth/login` or `/auth/setup`.
80- **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is
81 enforced on read, so this is tidiness, not a hole.
82- **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable
83 rather than hypothetical.
84
85## Backlog
86
87Ordered. Pull from the top.
88
891. **Milestone 5 — Repo browsing.** Tree, blob, commit log. **Start with domain value
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).
942. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Still open
95 whether writing or projects/showcases is the better first portfolio feature.
96
97## Open questions
98
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.
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.
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
123 stream both directions without buffering. This is what makes Milestone 4 viable.