| | @@ -4,83 +4,67 @@ |
| 4 | 4 | > [progress.md](progress.md). If this file starts reading like a changelog, it has |
| 5 | 5 | > drifted — that's exactly what went wrong last time. |
| 6 | 6 | |
| 7 | | −## Active: Milestone 1 — Identity, thin |
| 7 | +## Active: Milestone 2 — Profile page |
| 8 | 8 | |
| 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 |
| 11 | | −page 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. |
| 12 | 12 | |
| 13 | | −**Explicitly out of scope** — these are Milestone 7: multi-user registration, |
| 14 | | −invite 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. |
| 15 | 16 | |
| 16 | 17 | ### Steps |
| 17 | 18 | |
| 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 |
| 35 | 25 | |
| 36 | 26 | ### Done when |
| 37 | 27 | |
| 38 | | −A fresh database prints a setup token at boot; `/setup` with that token creates the |
| 39 | | −owner and signs them in; logging out and back in works; `/api/me` returns that |
| 40 | | −identity. |
| 41 | | − |
| 42 | | −Everything 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. |
| 53 | 30 | |
| 54 | 31 | ### Watch for |
| 55 | 32 | |
| 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). |
| 70 | 56 | |
| 71 | 57 | ## Backlog |
| 72 | 58 | |
| 73 | 59 | Ordered. Pull from the top. |
| 74 | 60 | |
| 75 | | −1. **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. |
| 77 | | −2. **Milestone 3 — Writing.** Posts, markdown rendering, `/{owner}/{slug}`. |
| 61 | +1. **Milestone 3 — Writing.** Posts, markdown rendering, `/{owner}/{slug}`. |
| 78 | 62 | *Open question: is writing actually the first portfolio feature, or is it |
| 79 | 63 | projects/showcases?* |
| 80 | | −3. **Milestone 4 — Repo model.** `Repository` entity, `Visibility`, `create_repo`, |
| 64 | +2. **Milestone 4 — Repo model.** `Repository` entity, `Visibility`, `create_repo`, |
| 81 | 65 | bare repo on disk at `{data_dir}/{org}/{repo}.git`. Watch the DB-plus-filesystem |
| 82 | 66 | atomicity problem — see [architecture.md](architecture.md#db-plus-filesystem-writes). |
| 83 | | −4. **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 |
| 84 | 68 | Basic. See [0001](decisions/0001-git-over-http-not-ssh.md). |
| 85 | 69 | |
| 86 | 70 | ## Open questions |