steid

@jamesgill /

docs: close milestone 1, open milestone 2

Roadmap and progress mark identity done at 91 tests. current.md moves to the
profile page, with the routing decision deferred from milestone 0 as its
first step -- module_router! vs explicit paths is worth settling before the
URL space grows past four routes.

Two new watch-fors that only became visible now the URL shape is real:
handle lookups must survive different casing, and nothing stops a user
claiming a handle that collides with /setup, /login, /logout or /api.

The unfinished small items -- flash messages, rate limiting, uncalled
sweep_expired, CSRF, styling -- carry forward explicitly rather than being
dropped on milestone rollover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JamesPatrickGill authored 1 month agoparent40ab5c7Browse files076dbc98f62508f9fe933c5cb90468f891877de3

3 files changed+51 −62

plans/ROADMAP.md+2 −2View file
@@ -50,8 +50,8 @@ a baseline.
5050 | # | Milestone | Status |
5151 |---|---|---|
5252 | 0 | **Skeleton** — Topcoat app boots, config, one page, SQLite wired | done |
53| 1 | **Identity, thin** — claim on first run, login, session | active (all but `/api/me`) |
54| 2 | **Profile page** — `/{owner}` as the real home page | not started |
53+| 1 | **Identity, thin** — claim on first run, login, session | done |
54+| 2 | **Profile page** — `/{owner}` as the real home page | active |
5555 | 3 | **Writing** — posts, markdown | not started |
5656 | 4 | **Repo model** — records + bare repos on disk | not started |
5757 | 5 | **Git over HTTP** — `git http-backend`, PATs for auth | not started |
plans/current.md+41 −57View file
@@ -4,83 +4,67 @@
44 > [progress.md](progress.md). If this file starts reading like a changelog, it has
55 > drifted — that's exactly what went wrong last time.
66
7## Active: Milestone 1 — Identity, thin
7+## Active: Milestone 2 — Profile page
88
9**Goal:** the app knows who you are. An unclaimed installation is claimed through
10`/setup`, and the resulting owner can log in and out. Enough identity to hang a profile
11page off, and no more.
9+**Goal:** `/{owner}` becomes the real home page, replacing the Milestone 0
10+placeholder. This is the frame the rest of the product hangs in — repos, writing, and
11+projects all appear on it later.
1212
13**Explicitly out of scope** — these are Milestone 7: multi-user registration,
14invite codes, `RegistrationPolicy`, organisation management UI, roles beyond owner.
13+**Explicitly out of scope:** repos and posts don't exist yet, so the page shows
14+identity plus empty sections. Editing display name / bio is Milestone 2 only if it
15+stays small.
1516
1617 ### Steps
1718
18- [x] Domain: typed IDs, `Email`, `PasswordHash`, `User`, `Organization`,
19 `Membership`, `Role`, `Actor`, `DomainError`
20- [x] Domain: repository traits — `UserRepository`, `OrgRepository`,
21 `MembershipRepository`
22- [x] Infrastructure: in-memory implementations (these are what make use cases
23 testable without a database)
24- [x] Application: `PasswordHasher` port + Argon2 adapter, stub hasher for tests
25- [x] Domain: `SetupToken` — one-time claim secret, constant-time comparison
26- [x] Application: `claim_instance` use case — token-gated, creates org → user →
27 owner membership, returns the owner signed in
28- [x] Application: `login` use case — verifies credentials, returns an `Actor`
29- [x] Infrastructure: migrations + SQLite implementations
30- [x] Infrastructure: `sessions` table + session storage
31- [x] Boot: mint and print a `SetupToken` when unclaimed; register it in app context
32- [x] Web: `/setup` claim page; every other route redirects there while unclaimed
33- [x] Web: login page, logout, `current_actor(cx)` helper
34- [ ] `/api/me` — first `/api` route, proves the use case layer has two consumers
19+- [ ] Settle routing: `module_router!` vs explicit `#[page]` paths, and `path_param!`
20+ for `{owner}` — deferred from Milestone 0, and the URL space is about to grow
21+- [ ] `/{owner}` — public page, resolves the org by handle, 404 when unknown
22+- [ ] `/` redirects to the owner's profile once claimed
23+- [ ] Owner-only affordances visible when the viewer is the owner
24+- [ ] `/api/orgs/{owner}` — the API surface for the same read model
3525
3626 ### Done when
3727
38A fresh database prints a setup token at boot; `/setup` with that token creates the
39owner and signs them in; logging out and back in works; `/api/me` returns that
40identity.
41
42Everything but `/api/me` is done and verified in a browser.
43
44### Resolved
45
46- **`__Host-` cookies need a secure context — and it bit.** The claim succeeded, the
47 server recorded sessions, and every page still rendered signed out, because the
48 browser silently discarded a `Secure` cookie served over plain HTTP. Nothing errored
49 on either side. Fixed with `InsecureCookieTokenStore` behind
50 `STEID_INSECURE_COOKIES`, off by default — see [runbook.md](runbook.md#configuration).
51- **Claim TOCTOU** is now covered by a test that drives a real claim through the SQLite
52 repos and asserts `unique(orgs.name)` / `unique(users.email)` refuse the second.
28+`/{owner}` renders for a signed-out visitor, shows extra affordances to the owner, and
29+an unknown handle 404s rather than erroring.
5330
5431 ### Watch for
5532
56- **CSRF.** `SameSite=Lax` blocks cross-site POSTs, which covers the common case.
57 Whether forms also want tokens is an open decision, not a default to pick quietly.
58 Still undecided.
59- **No rate limiting anywhere.** `/login` and `/setup` accept unlimited attempts. The
60 setup token has 256 bits so brute force is not the worry; password guessing is.
61- **Form errors are invisible.** A wrong token or password redirects back with no
62 message — deliberate, so failures can't be used to probe, but indistinguishable from
63 a broken form. Flash messages are the fix and don't exist yet.
64- **Session sweeping is never called.** `sweep_expired` exists and is tested but
65 nothing invokes it, so expired rows accumulate. Expiry is enforced on read, so this
66 is tidiness rather than a security hole.
67- **Foreign key ordering.** The org must be saved before the user — attempt #2 had to
68 fix this in two places. Enforced now: `foreign_keys(true)` plus a test.
69- Never log or `Debug`-print a password. `PasswordHash` is opaque on purpose.
33+- **Handle lookups are case-insensitive** in storage (`collate nocase`) and lowercased
34+ by `OrgName::new`. A URL with different casing must resolve, not 404.
35+- **Reserved handles.** `/setup`, `/login`, `/logout`, `/api` are real routes. A user
36+ claiming the handle `login` would shadow or be shadowed by one. Nothing prevents
37+ this yet — decide before multi-user registration in Milestone 7, or sooner if it's
38+ cheap.
39+- **A profile is public.** It is the first page rendering for anonymous visitors by
40+ design, so anything private must be gated explicitly rather than by assuming a
41+ session exists.
42+
43+### Carried over — small, unblocked, worth doing alongside
44+
45+- **Flash messages.** A wrong password or setup token bounces with no explanation.
46+ Deliberate on the security side, but indistinguishable from a broken form. Every
47+ form added from here inherits the problem.
48+- **No rate limiting** on `/login` or `/setup`.
49+- **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is
50+ enforced on read, so this is tidiness, not a hole.
51+- **CSRF.** `SameSite=Lax` covers the common case; whether forms also want tokens is
52+ still undecided.
53+- **Styling.** Everything is unstyled HTML. Topcoat ships Tailwind without Node, and
54+ `steid-backup/AGENTS/UI.md` has a full OKLCH system to mine. Cheaper at two pages
55+ than at ten — see [ui.md](ui.md).
7056
7157 ## Backlog
7258
7359 Ordered. Pull from the top.
7460
751. **Milestone 2 — Profile page.** `/{owner}` becomes the real home page, replacing
76 the Milestone 0 placeholder. The frame the rest of the product hangs in.
772. **Milestone 3 — Writing.** Posts, markdown rendering, `/{owner}/{slug}`.
61+1. **Milestone 3 — Writing.** Posts, markdown rendering, `/{owner}/{slug}`.
7862 *Open question: is writing actually the first portfolio feature, or is it
7963 projects/showcases?*
803. **Milestone 4 — Repo model.** `Repository` entity, `Visibility`, `create_repo`,
64+2. **Milestone 4 — Repo model.** `Repository` entity, `Visibility`, `create_repo`,
8165 bare repo on disk at `{data_dir}/{org}/{repo}.git`. Watch the DB-plus-filesystem
8266 atomicity problem — see [architecture.md](architecture.md#db-plus-filesystem-writes).
834. **Milestone 5 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP
67+3. **Milestone 5 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP
8468 Basic. See [0001](decisions/0001-git-over-http-not-ssh.md).
8569
8670 ## Open questions
plans/progress.md+8 −3View file
@@ -2,7 +2,7 @@
22
33 ## This attempt (#3, Topcoat)
44
587 tests. Active milestone in [current.md](current.md).
5+91 tests. Active milestone in [current.md](current.md).
66
77 ### Milestone 0 — Skeleton · done
88
@@ -14,7 +14,7 @@ Topcoat's link-time page discovery works from a library; that was checked, not a
1414 **Requires rustc ≥ 1.95.** On older toolchains `cargo add topcoat` silently resolves to
1515 an empty `topcoat v0.0.0` placeholder instead of failing.
1616
17### Milestone 1 — Identity, thin · all but `/api/me`
17+### Milestone 1 — Identity, thin · done
1818
1919 **Domain.** Typed IDs, `Email`, `PasswordHash`, `OrgName`, `Organization`, `User`,
2020 `Membership`, `Role`, `Actor`, `Session`, `SetupToken`, `DomainError`. Value objects
@@ -24,7 +24,12 @@ pair `new()` (validates) with `from_trusted()` (skips, for rows already validate
2424 `end_session`, `sweep_expired`. `PasswordHasher` port with Argon2 and a stub.
2525
2626 **Infrastructure.** Migrations for orgs, users, memberships, sessions. In-memory and
27SQLite implementations of every port. Root layout, `/setup`, `/login`, `/logout`, home.
27+SQLite implementations of every port. Root layout, `/setup`, `/login`, `/logout`, home,
28+and `/api/me`.
29+
30+`describe_identity` is the first use case with two consumers — the home page and
31+`/api/me` both read through it. Until that existed, "the application layer is
32+transport-neutral" was an assertion with one caller behind it.
2833
2934 **Verified in a browser and by curl:** wrong token refused with nothing written;
3035 correct token creates org + user + owner membership and signs the owner in; session