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
18- [x] Domain: `PersonalAccessToken`, `TokenId`, `TokenHash`, and the repository port
19- [x] Infrastructure: in-memory + SQLite implementations, migration
20- [x] Application: `issue_token`, `list_tokens`, `revoke_token`
21- [x] 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
31A token issued through the UI lets `git push` succeed against a repository its owner may
32write, and lets `git clone` succeed against a private repository its owner may read.
33Revoking the token stops both. An anonymous clone of a public repository still works
34exactly as it does today.
35
36### Settled
37
38All 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,
40no scopes, and a **uniform 401** on any git path not anonymously readable — including
41repositories 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- **Tokens do not expire.** A deliberate absence, not an oversight: a credential pasted
48 into a machine and forgotten is worth less if it stops working silently, and revocation
49 is the control that matters. Written down so it does not read as a missing feature.
50- **Revoking someone else's token is `NotFound`, not `Forbidden`** — that a token id
51 exists but belongs to another user is not a fact a caller should be able to learn.
52- **`TokenRepository` looks up by hash**, because that is the lookup authentication
53 actually performs — a client presents a token, never an id.
54
55### Open
56
57Nothing open.
58
59### Carried over — small, unblocked
60
61- **A client that disappears mid-request leaves the body-copy task waiting.** The copy
62 into git's stdin runs in its own task and nothing cancels it if the connection drops.
63 Bounded by the backend exiting and closing the pipe, but not by anything deliberate.
64- **A subprocess per git request.** Unlike Milestone 3's once-per-creation, this is on a
65 hot path and has not been measured. Milestone 5 is where that bill comes due.
66- **Streaming is by construction, not by measurement.** The response body is never
67 collected, but no clone large enough to prove it has been run.
68- **An orphaned repo directory is possible** if the process dies between the record
69 write and the filesystem write, and it then blocks re-creating that name. The durable
70 fix is a reconciliation sweep on boot
71 ([architecture.md]architecture.md#db-plus-filesystem-writes); clearing one is a
72 manual `rm` today, since repo deletion does not exist.
73- **The duplicate-name check races.** The loser is caught by `init_bare` or the unique
74 constraint, but surfaces as an opaque storage error rather than "name taken".
75- **Bare repos created on macOS carry `ignorecase = true`.** A migration gotcha if the
76 data directory ever moves to Linux.
77- **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; both fall back
78 today. Topcoat's `font-fontsource` feature handles it.
79- **Light mode is untested.** The palette defines it; nobody has looked at it.
80- **No rate limiting** on `/auth/login` or `/auth/setup`.
81- **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is
82 enforced on read, so this is tidiness, not a hole.
83- **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable
84 rather than hypothetical.
85
86## Backlog
87
88Ordered. Pull from the top.
89
901. **Milestone 5 — Repo browsing.** Tree, blob, commit log. **Start with domain value
91 objects**`ObjectId`, `RefName`, `TreeEntry` — before any adapter. A query port
92 returning `String`s is an anaemic pass-through that pushes validation into the page.
93 Also the point to measure fork/exec cost per page view, and to reconsider `gix` for
94 the read path ([0006]decisions/0006-git-binary-behind-narrow-ports.md).
952. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Still open
96 whether writing or projects/showcases is the better first portfolio feature.
97
98## Open questions
99
100- **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected
101 by its own authors). Expect churn that isn't feature work.
102- Topcoat ships Tailwind without Node, which reopens the design system attempt #1
103 dropped purely to avoid an npm build step — see [ui.md]ui.md.
104
105## Routing findings (Milestone 0)
106
107- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat`
108 silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local
109 stable is now 1.97.1.
110- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**,
111 so pages can live in any module. Layering is our choice, not the framework's.
112- `module_router!` derives each URL from the module tree rather than a path string.
113 Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but
114 handles sit at the root ([0004]decisions/0004-root-handles-grouped-routes.md), so a
115 parameterised root segment still has to coexist with static ones. Worth checking how
116 `module_router!` handles that before committing to it.
117- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not
118 injected as handler arguments. Parses are memoized per request.
119- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's
120 `NotFoundError` to render a branded 404.
121- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone.
122- `Body` is a boxed `http_body::Body` used for both requests and responses, with
123 `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can
124 stream both directions without buffering. This is what makes Milestone 4 viable.