Release 0.8.0: open beta-access request flow (first/last/why)

Replaces the v0.3.0 / v0.7.0 allowed_emails admission gate with an
admin-grant flow (roadmap item #6, SPEC §6.1 / §6.2 / §14.1 / §17).
Any valid email can sign in via OTC; a fresh user lands in
permission_state='pending' with a captured first/last/why profile,
and an admin grant flips them to 'granted' before write endpoints
accept them. Grandfathered users pass through the migration with
the column default 'granted' so existing contributors are unaffected.
The allowed_emails table stays in the schema as a fast-path bypass
pending v0.9.0's admin user-management page (item #7).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-05-28 02:25:59 -07:00
parent 8aa65014b4
commit ca8ba69acb
17 changed files with 1327 additions and 145 deletions
+186
View File
@@ -23,6 +23,192 @@ skip versions are the composition of each intervening adjacent
release's steps in order — no A-to-B path is pre-computed beyond
that.
## 0.8.0 — 2026-05-28
**Minor — schema migration required; admission semantics shift.**
This release replaces the v0.3.0 / v0.7.0 `allowed_emails` admission
gate with an admin-grant flow (roadmap item #6, SPEC §6.1 / §6.2 /
§14.1 / §17). Anyone with a valid email can sign in via the v0.7.0
OTC flow; the OTC request endpoint no longer consults the
allowlist. A fresh user lands in `permission_state='pending'` until
an admin grants access. The first-OTC sign-in captures first name,
last name, and a free-text "why I should be included in the beta"
via a new `POST /api/auth/me/beta-request` endpoint; the captured
fields populate the same `users` row alongside the OAuth-era
columns.
A pending user has the same read access an anonymous viewer has —
the catalog, RFC bodies, the philosophy page, and every public
conversation are reachable. Every write-shaped endpoint
(`auth.require_contributor` floor) refuses pending users with 403.
The frontend renders a thin "Your beta access request is in
review" banner on every page and re-purposes the v0.3.0
`/beta-pending` page as the post-capture landing surface.
Grandfathered behavior: every `users` row at migration time
carries `permission_state='granted'` via the column default, so
existing contributors are unaffected. The OAuth fallback at
`/auth/callback` still consults the v0.3.0 allowlist (legacy
path); the OTC flow does not.
The `allowed_emails` table stays in the schema as a fast-path
bypass — the v0.3.0 admin UI continues to manage it, but the OTC
request handler no longer reads it. v0.9.0 (roadmap item #7) is
expected to ship the admin user-management page that replaces the
allowlist surface entirely; until then, admin grants are done by
direct DB `UPDATE`.
### Upgrade steps (from 0.13.0)
1. **MUST** restart the backend so migration `014_beta_access.sql`
runs. The migration adds `permission_state` (default `'granted'`,
so existing rows pass through unaffected), `first_name`,
`last_name`, `beta_request_reason`, `permission_decided_by`,
and `permission_decided_at` to the `users` table, plus an index
on `permission_state` for the pending queue. The migration is
ALTER-TABLE-based (no table rebuild) — every foreign key and
existing row passes through untouched.
2. **MUST** rebuild the frontend. The `Login.jsx` surface now
runs a conditional third step (the capture form) on fresh OTC
sign-ins; `BetaPending.jsx` carries the new "your request is
in review" copy; `App.jsx` renders a thin pending-access
banner. The build embeds the new `/api/auth/me/beta-request`
client call.
3. **SHOULD** announce the new admission flow to existing users.
Wording suggestion: "We've replaced our email-allowlist gate
with an admin-review flow. Existing users are unaffected;
new visitors sign in with their email, tell us a bit about
themselves, and an admin reviews their request before
discussion and contribution unlock." Existing sessions
remain valid.
4. **SHOULD** plan the admin grant mechanism. v0.8.0 does not
ship a UI for the grant — v0.9.0 (roadmap item #7) will. For
the v0.8.0 window, an admin grants access via direct DB
gesture:
```sql
UPDATE users
SET permission_state = 'granted',
permission_decided_by = <admin_user_id>,
permission_decided_at = datetime('now')
WHERE email = '<approved>';
```
The pending queue lives in `SELECT * FROM users WHERE
permission_state = 'pending' ORDER BY created_at`.
5. **MUST** decide whether to drain the `allowed_emails` table.
The OTC request handler no longer consults it; populated
rows are inert at the request surface. Three operator
choices, all valid:
* **Leave as-is** (the framework's default behavior — the
v0.3.0 admin UI continues to work, the rows stay as a
fast-path bypass record). Recommended if you anticipate
v0.9.0's user-management page folding the allowlist UI
into its surface.
* **Drain via the existing admin UI** (`/admin/allowlist`)
— one row at a time, no data loss elsewhere.
* **Bulk-drain via DB** — `DELETE FROM allowed_emails;`
drops every row; the table stays.
6. **MAY** announce write access individually to grandfathered
users you want to keep at `'granted'`. The default-`'granted'`
migration means no action is required for them; this step
exists only if you want to send a "you're still in" message.
### Added
- **`backend/migrations/014_beta_access.sql`** — adds
`permission_state` (CHECK in `('pending', 'granted', 'revoked')`,
default `'granted'`), `first_name`, `last_name`,
`beta_request_reason`, `permission_decided_by` (FK to users,
ON DELETE SET NULL), `permission_decided_at` to the `users`
table. Plus `idx_users_permission_state` for the pending
queue.
- **`POST /api/auth/me/beta-request`** — body
`{first_name, last_name, beta_request_reason}` (all required;
bounds 120 / 120 / 4000). Writes the fields to the signed-in
user's row and leaves `permission_state='pending'`. Refuses
HTTP 409 for already-granted / revoked users; refuses HTTP
401 for anonymous callers.
- **`needs_profile` flag** on the `/auth/otc/verify` response.
`true` iff the user is `permission_state='pending'` AND
carries no profile fields yet (a fresh OTC sign-in). The
Login.jsx surface uses the flag to gate the capture step.
- **`permission_state` field** on the `/api/auth/me` response,
plus `first_name`, `last_name`, `beta_request_reason`, and
the same `needs_profile` flag.
- **First-OTC profile capture step** in `Login.jsx`. Third
step in the sign-in surface, gated by the verify response's
`needs_profile` flag.
- **`/beta-pending` repurpose** in `BetaPending.jsx`. The
page now reads as "your request is in review" when the
viewer is pending; the v0.3.0 "private beta" framing
remains as the anonymous-viewer fallback.
- **Thin pending-access banner** at the top of every page
for `permission_state='pending'` viewers (other than
`/beta-pending` itself).
- **SPEC `§6` opening / `§6.1` / `§6.2` / `§14.1` / `§17` /
`§19.2`** corrections per §19.3 rule-2 — the admission
shift, the orthogonality of permission_state vs role / muted
/ notification-mutes, the new endpoints, and the
newly-surfaced §19.2 candidates (admin user-management page,
allowlist deprecation, admin notification on new request).
- **`backend/tests/test_beta_access_vertical.py`** — 9 new
tests covering: a fresh OTC user lands pending with empty
profile; the capture endpoint populates the fields and
keeps state pending; the capture endpoint refuses
anonymous / granted / revoked callers; a pending user is
refused write endpoints; an admin grant promotes pending →
granted; a grandfathered user is unaffected by the
migration; the OTC request endpoint accepts any email
regardless of allowlist state; the `allowed_emails` table
is still present in the schema.
### Changed
- **`backend/app/auth.py#require_contributor`** widens its
gate to refuse `permission_state != 'granted'` with HTTP
403. The §6.1 contributor capabilities (propose, branch,
PR, chat, claim) all funnel through this dependency, so
the widening covers them transitively. `SessionUser` now
carries `permission_state` (default `'granted'` for the
dataclass-default fallback path).
- **`backend/app/otc.py#request_code`** drops the allowlist
check from the OTC request flow. The `RequestOutcome`
shape loses the `'allowlist'` reason (replaced by
`'sent'` / `'cooldown'` / `'invalid'`).
- **`backend/app/otc.py#provision_or_link_user`** sets
`permission_state='pending'` explicitly on a fresh row.
Grandfathered (link-by-email) users pass through with
their existing column value.
- **`backend/app/auth.py#provision_user`** (OAuth fallback)
now sets `permission_state='granted'` explicitly on a
fresh row. The OAuth callback still consults the
`is_allowed_sign_in` allowlist check (the legacy fallback
path retains its v0.3.0 admission shape during the OAuth
migration window).
- **`backend/tests/test_otc_vertical.py`** — the
`test_otc_request_silently_drops_when_email_not_on_allowlist`
test (asserted the v0.7.0 allowlist gate) is replaced by
`test_otc_request_admits_emails_regardless_of_allowlist_population`
which asserts the v0.8.0 open-request contract. The
on-list test stays as a regression net for the
rate-limit / outbound-buffer plumbing.
- **`SPEC.md`** §6 opening, §6.1, §6.2, §14.1, §17, §19.2
per §19.3 rule-2.
- **`VERSION`** → `0.8.0`. `frontend/package.json#version` and
the lockfile mirror.
### Deferred to later releases
- **Admin user-management page** at `/admin/users` (item #7,
v0.9.0) — replaces the manual DB `UPDATE` gesture.
- **Allowlist UI deprecation** (also v0.9.0) — once the
admin user-management page lands, the `/admin/allowlist`
surface and the `allowed_emails` table both retire.
- **Admin email notification on new beta request** (item #7
again, v0.9.0).
- **Revoke gesture in the UI** — the `permission_state='revoked'`
state is wired in the schema and the auth gate; v0.9.0 ships
the admin UI that flips the column.
## 0.13.0 — 2026-05-28
**Minor — schema migration required; new optional env vars.** This