f8e797ab09
Replaces the Gitea OAuth gesture as the primary human-auth path (roadmap item #5, SPEC §6.2). Users sign in by entering their email, receiving a six-digit code via the existing SMTP layer, and entering the code on a two-step /login surface. The Gitea OAuth callback remains functional during migration — the new UI links to it as a fallback for users with active OAuth sessions or older invite paths — and is scheduled for removal in a future release once OTC adoption is universal. Existing users are linked by email on first OTC sign- in (gitea_id preserved); new users are provisioned with NULL gitea_id and rely on email as the identity key. The migration introduces backend/migrations/012_otc.sql (otc_codes table + users schema rebuild for nullable gitea_id and a partial unique index on email), two new endpoints (POST /auth/otc/request, POST /auth/otc/verify), bcrypt as a new backend dependency for code hashing, and 11 new tests in test_otc_vertical.py covering the happy path, expired and consumed and wrong codes, the per-email rate limit, the allowlist gate, the OAuth-era link path, fresh provisioning, and prior-code invalidation on re-request. No new secrets are required — the existing SECRET_KEY signs sessions and bcrypt's per-row salt covers the code hashes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
616 lines
30 KiB
Markdown
616 lines
30 KiB
Markdown
# Changelog
|
|
|
|
The binding policy for what each kind of version bump means and what
|
|
the changelog has to carry is in [`SPEC.md` §20](./SPEC.md). The
|
|
practical recipe downstream deployments follow when reading this file
|
|
is in [`docs/DEPLOYMENTS.md`](./docs/DEPLOYMENTS.md).
|
|
|
|
The canonical version is the `VERSION` file at the repo root;
|
|
`frontend/package.json#version` mirrors it. Deployments pin to a
|
|
specific framework version in their own `.rfc-app-version` file
|
|
(per §20.5).
|
|
|
|
While the framework is pre-1.0, minor-version bumps may introduce
|
|
breaking changes; the entry below each such bump documents the
|
|
upgrade steps a deployment must apply.
|
|
|
|
Upgrade-steps blocks use the [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119)
|
|
/ [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) normative-
|
|
language convention per `SPEC.md` §20.4. **MUST** / **SHALL** steps
|
|
are required; **SHOULD** steps are recommended (the framework's
|
|
default and tested path); **MAY** steps are optional. Upgrades that
|
|
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.7.0 — 2026-05-28
|
|
|
|
**Minor — schema migration required; new auth path is additive.**
|
|
This release lands email + one-time-code sign-in (roadmap item #5,
|
|
SPEC §6.2) as the primary human-auth gesture. Users sign in by
|
|
typing their email, receiving a six-digit code via email, and
|
|
entering it. The Gitea OAuth callback (`/auth/callback`) remains
|
|
functional during migration — the new UI no longer points at it
|
|
primarily, but a "Sign in with Gitea (fallback)" link survives on
|
|
the new login surface so users with active OAuth sessions or older
|
|
invite paths still have a way in. A future release retires the
|
|
OAuth path entirely once every active user has signed in at least
|
|
once via OTC.
|
|
|
|
The migration path for existing users: first OTC sign-in matches by
|
|
`users.email` (case-insensitive) to the OAuth-era row and reuses
|
|
that row's `id` and `gitea_id`. New users provisioned via OTC carry
|
|
`gitea_id = NULL` and `gitea_login = NULL`. The `gitea_id` linker
|
|
remains the canonical handle for grandfathered users; `email`
|
|
becomes the identity key for everything provisioned after v0.7.0.
|
|
|
|
The Gitea **bot** user + token are still required (server-side git
|
|
operations — repo reads, PR creation — still flow through it). Only
|
|
the operator-facing sign-in surface moves.
|
|
|
|
### Upgrade steps (from 0.6.0)
|
|
|
|
1. **MUST** restart the backend so migration `012_otc.sql` runs.
|
|
The migration rebuilds the `users` table (SQLite cannot ALTER
|
|
COLUMN); existing rows pass through unchanged, but the new
|
|
schema relaxes `gitea_id` / `gitea_login` to nullable (with
|
|
partial unique indexes that ignore NULL) and adds a partial
|
|
unique index on `email`. A new `otc_codes` table is created.
|
|
2. **MUST** confirm the SMTP overlay is set (`SMTP_HOST`,
|
|
`SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_STARTTLS`,
|
|
`EMAIL_FROM`, `EMAIL_FROM_NAME`). The OHM overlay already
|
|
carries these as of v0.5.0; deployments without them fall back
|
|
to logging the code to stdout (dev-only path — production
|
|
visitors will not receive their codes).
|
|
3. **SHOULD** announce the new email-based sign-in to existing
|
|
users. Wording suggestion: "You can now sign in by entering
|
|
your email and a one-time code we'll send you. Your old
|
|
account is linked automatically the first time you sign in."
|
|
4. **MAY** keep the existing OAuth callback as a fallback path.
|
|
The new login UI surfaces a small "Sign in with Gitea
|
|
(fallback)" link beneath the primary email/code form; a
|
|
deployment that prefers to hide it can override the Login
|
|
component in a future framework release that exposes the link
|
|
behind a feature flag. For v0.7.0, the link is hard-coded.
|
|
|
|
### New environment variables (all optional with defaults)
|
|
|
|
- `OTC_TTL_MINUTES` (default `10`) — how long a one-time code is
|
|
valid after issuance. Re-requesting invalidates the prior code
|
|
immediately regardless of TTL.
|
|
- `OTC_REQUEST_COOLDOWN_SECONDS` (default `60`) — per-email cooldown
|
|
between successive `/auth/otc/request` calls. The endpoint returns
|
|
HTTP 429 when the cooldown blocks a request (the loud-failure
|
|
shape; the abuse path is visible rather than swallowed).
|
|
|
|
No new secrets are required. The existing `SECRET_KEY` continues to
|
|
sign session cookies; OTC codes are bcrypt-hashed at rest using a
|
|
per-row salt the library generates.
|
|
|
|
### Added
|
|
|
|
- **`POST /auth/otc/request`** — body `{email}`. Generates a six-digit
|
|
code, stores its bcrypt hash with an expiry, and dispatches a plain
|
|
text email via the existing SMTP layer. Returns HTTP 200 (`{ok:true}`)
|
|
uniformly so allowlist state is not leaked. Returns HTTP 429 when
|
|
the per-email cooldown blocks the request.
|
|
- **`POST /auth/otc/verify`** — body `{email, code}`. Validates the
|
|
bcrypt hash against the most-recent unconsumed non-expired row, marks
|
|
the row consumed, provisions or links the `users` row by email, and
|
|
stores the session cookie. Returns HTTP 200 on success, HTTP 400 on
|
|
any failure (expired, consumed, wrong, unknown).
|
|
- **`backend/migrations/012_otc.sql`** — creates `otc_codes` and
|
|
rebuilds `users` with nullable `gitea_id` / `gitea_login` plus a
|
|
partial unique index on `email`.
|
|
- **`backend/app/otc.py`** — the OTC request/verify state machine and
|
|
the `provision_or_link_user` linker.
|
|
- **`backend/app/email_otc.py`** — outbound OTC mail composition. Reuses
|
|
the SMTP plumbing from `email.py` (`EmailConfig.from_env()`) and the
|
|
test buffer (`_SENT`) but writes its own envelope (no unsubscribe
|
|
footer, no quiet-hours hold — OTC mail carries a credential and
|
|
ignores notification preferences).
|
|
- **`frontend/src/components/Login.jsx`** — two-step sign-in surface
|
|
at `/login`. Step 1: enter email → request code. Step 2: enter
|
|
six-digit code → verify. Cmd/Ctrl+Enter on the code field submits.
|
|
The previous header "Sign in" link and the `Welcome` component's
|
|
inline link now route to `/login` instead of jumping straight to
|
|
the Gitea OAuth dance.
|
|
- **SPEC `§6.1` / `§6.2` / `§14.1` / `§17` / `§19.2`** corrections per
|
|
§19.3 rule-2 — see below.
|
|
- **`backend/tests/test_otc_vertical.py`** — 11 new tests covering
|
|
the happy path, expired/consumed/wrong codes, the per-email rate
|
|
limit (and its per-email isolation), the allowlist gate, the
|
|
migration link to OAuth-era users, fresh provisioning, and the
|
|
prior-code-invalidation behavior on re-request.
|
|
|
|
### Changed
|
|
|
|
- **`backend/app/auth.py#current_user`** — coerces NULL `gitea_id` and
|
|
NULL `gitea_login` to `0` / `""` so the `SessionUser` shape stays
|
|
stable for OTC-only users. The DB remains the source of truth for
|
|
"is this user OAuth-linked" (`gitea_id IS NOT NULL`).
|
|
- **`backend/requirements.txt`** — adds `bcrypt>=4.2` for OTC code
|
|
hashing. Pure-Python wheels are available on every platform the
|
|
deployment matrix targets; `pip install -r backend/requirements.txt`
|
|
picks it up.
|
|
- **`frontend/src/App.jsx`** — adds the `/login` route and replaces
|
|
the header "Sign in" `<a href="/auth/login">` with `<Link to="/login">`.
|
|
The `Welcome` component's inline sign-in link follows suit.
|
|
- **`frontend/src/components/Landing.jsx`** — the `/welcome` page's
|
|
primary action moves from "Sign in with Gitea" to "Sign in" pointing
|
|
at `/login`.
|
|
|
|
### Deferred to later releases
|
|
|
|
Per the v0.7.0 scope discipline (the foundation for items #6, #8, #9,
|
|
#10), several adjacent capabilities are intentionally not in this
|
|
release and surface as §19.2 candidates:
|
|
|
|
- **First-OTC profile capture** (first name, last name, "why") — item
|
|
#6, expected v0.8.0.
|
|
- **Open beta-access request flow** replacing the allowlist gate —
|
|
also item #6, v0.8.0.
|
|
- **Passcodes** (a long-term reauth token alternative) — item #8,
|
|
expected v0.10.0.
|
|
- **Device-trust 30-day skip** — item #9, expected v0.11.0.
|
|
- **Cloudflare Turnstile** on `/auth/otc/request` — item #10,
|
|
expected v0.12.0.
|
|
- **Removing the Gitea OAuth `/auth/callback` route entirely** — a
|
|
later release after every active user has signed in via OTC.
|
|
|
|
## 0.6.0 — 2026-05-28
|
|
|
|
**Minor — no operator action required.** A sweep-the-edges hardening
|
|
release (roadmap item #4, "anon discuss + contribute off-limits") that
|
|
audits every write-shaped backend endpoint and asserts each one
|
|
enforces an explicit `auth.require_contributor` (or stricter) gate
|
|
before doing any state-changing work. v0.3.0 hid write affordances
|
|
behind a sign-in CTA on the frontend; v0.5.0 added the PR-less
|
|
discussion surface with its own write gate; v0.6.0 sweeps the rest
|
|
and adds a regression test net so future endpoints can't quietly
|
|
ship without a gate. No schema migration, no env-var changes, no
|
|
new dependencies. The only behavioural change is one tightening: the
|
|
`GET /api/rfcs/<slug>/graduate/progress` SSE now requires
|
|
`auth.require_user` since it surfaces operator-visible step detail
|
|
(repo name, PR number, rollback steps) not part of the v0.3.0
|
|
anonymous-read contract for catalog/RFC bodies.
|
|
|
|
### Changed
|
|
|
|
- **`backend/app/api_graduation.py`** — `GET /graduate/progress`
|
|
now calls `auth.require_user(request)` as its first line.
|
|
Anonymous callers receive 401 instead of being able to subscribe
|
|
to a graduation's SSE. The floor is `require_user` (not
|
|
`require_contributor`) so a write-muted operator can still observe
|
|
the progress of a graduation they kicked off before being muted.
|
|
- **SPEC `§6.1`** (`SPEC.md`) — the Anonymous role's bullet now
|
|
documents the v0.6.0 audit: every write-shaped endpoint in §17
|
|
enforces an explicit gate; anonymous writes refuse 401. The list
|
|
of audited write families is recorded in-line.
|
|
- **SPEC `§10.10`** — the discussion-vs-contribution section now
|
|
records that the v0.5.0 write gates have a regression test net
|
|
(`test_anon_offlimits_vertical.py`) added in v0.6.0.
|
|
- **SPEC `§17`** — the `GET /api/rfcs/<slug>/graduate/progress`
|
|
bullet now documents the `require_user` gate added in v0.6.0,
|
|
with the rationale.
|
|
|
|
### Added (tests)
|
|
|
|
- **`backend/tests/test_anon_offlimits_vertical.py`** — twelve new
|
|
tests asserting that every write-shaped endpoint surveyed in the
|
|
v0.6.0 audit refuses anonymous callers with 401, and that the five
|
|
anonymous-read surfaces (health, philosophy, auth/me, catalog,
|
|
RFC view, discussion threads, proposals) stay reachable. Sixty-six
|
|
assertions in total, covering: propose; proposal merge / decline /
|
|
withdraw; branch promote-to-branch / start-edit-branch / metadata /
|
|
manual-flush / visibility / grants (POST + DELETE) / threads (POST)
|
|
/ messages (POST) / resolve / chat-seen / changes (accept / decline
|
|
/ reask) / chat-stream; super-draft start-edit-branch + metadata;
|
|
PR pr-draft / open / seen / review / merge / withdraw /
|
|
description / resolution-branch; discussion thread create + message
|
|
post + resolve; admin role / mute / allowlist (POST + DELETE) plus
|
|
the admin reads; notification preferences / quiet-hours / watch /
|
|
mark-read / user-mute (POST + DELETE); funder credentials (POST +
|
|
DELETE) + consent (POST + DELETE); graduation kickoff + claim +
|
|
progress SSE; PR review page anonymous-readable.
|
|
|
|
### Anonymous-writeable allowlist
|
|
|
|
Two endpoints are intentionally anonymous-by-design. They are not
|
|
audit findings; they are documented here so the contract is
|
|
explicit:
|
|
|
|
- `GET /auth/login` and `GET /auth/callback` — the OAuth
|
|
round-trip. Anonymous-by-design because they ARE the sign-in
|
|
entrypoint.
|
|
- `POST /api/webhooks/gitea` and `POST /api/webhooks/email-bounce`
|
|
— anonymous in the session sense but authenticated by HMAC shared
|
|
secret (`GITEA_WEBHOOK_SECRET` and `WEBHOOK_EMAIL_BOUNCE_SECRET`
|
|
respectively). The webhook receiver is the wrong place for an
|
|
authenticated session; the shared-secret shape is correct.
|
|
|
|
### §19.2 candidates surfaced
|
|
|
|
- None unique to this release. The two pre-existing candidates the
|
|
audit touched — anonymous-read polish for the discussion surface
|
|
(carried from v0.5.0) and the operator-visibility floor on
|
|
graduation progress — were settled here as `require_user` on
|
|
`/graduate/progress` rather than deferred.
|
|
|
|
### Upgrade steps (from 0.5.0)
|
|
|
|
1. The deployment **MUST** rebuild the frontend (`npm install &&
|
|
npm run build`) so the frontend bundle's reported version matches
|
|
the backend's. No new env vars; existing `frontend/.env` is
|
|
sufficient.
|
|
2. The deployment **MUST** restart the backend so the new
|
|
`require_user` gate on `/graduate/progress` is enforced. No
|
|
schema migration runs.
|
|
3. The deployment **MAY** announce the audit completion to
|
|
operators: every write-shaped backend endpoint now enforces an
|
|
explicit gate, and the `test_anon_offlimits_vertical.py` test
|
|
net asserts the contract on every CI run. The audited surfaces
|
|
are listed in the `Added (tests)` section above and in `SPEC.md`
|
|
§6.1.
|
|
4. The deployment **MUST NOT** assume any new envelope behaviour:
|
|
v0.6.0 is purely a hardening release. No schema, no env, no
|
|
dependency changes; the operator's role is reduced to rebuild +
|
|
restart.
|
|
|
|
## 0.5.0 — 2026-05-27
|
|
|
|
**Minor — no operator action required.** This release wires the
|
|
PR-less per-RFC discussion surface (roadmap item #3, SPEC §10.10).
|
|
An RFC's main view now carries a discussion panel distinct from PR
|
|
comments and branch chat; contribution — proposing edits the document
|
|
will land — still requires opening a PR via the §10.1 affordance.
|
|
The substrate is the existing `threads` / `thread_messages` tables;
|
|
rows with `threads.branch_name IS NULL` scope to the RFC's main view.
|
|
No schema migration is required.
|
|
|
|
### Added
|
|
|
|
- **PR-less discussion endpoints** (`backend/app/api_discussion.py`)
|
|
mounted at `/api/rfcs/<slug>/discussion/...`:
|
|
- `GET /api/rfcs/<slug>/discussion/threads` — list threads where
|
|
`threads.branch_name IS NULL`. Anonymous-readable per the v0.3.0
|
|
contract; the default whole-doc chat thread is materialized
|
|
lazily on first read.
|
|
- `POST /api/rfcs/<slug>/discussion/threads` — open a new
|
|
discussion thread (`thread_kind='chat'`, `anchor_kind='whole-doc'`,
|
|
`branch_name=NULL`). Body: optional `label`, optional first
|
|
`message`. Requires contributor role.
|
|
- `GET /api/rfcs/<slug>/discussion/threads/<thread_id>/messages`
|
|
— read messages on a discussion thread. Anonymous-readable.
|
|
- `POST /api/rfcs/<slug>/discussion/threads/<thread_id>/messages`
|
|
— post a message. Body: `text`, optional `quote`. Requires
|
|
contributor role.
|
|
- `POST /api/rfcs/<slug>/discussion/threads/<thread_id>/resolve`
|
|
— resolve a discussion thread. Permission per §10.10: thread
|
|
creator, RFC owner / arbiter, or app admin / owner.
|
|
- **`RFCDiscussionPanel.jsx`** — the right-column surface on the RFC
|
|
view when the viewer is on `main`. Composer requires sign-in;
|
|
Cmd/Ctrl+Enter sends. Multiple threads surface as pill-shaped
|
|
tabs above the message feed. A "New thread" affordance opens a
|
|
fresh thread on the same RFC.
|
|
- **SPEC `§10.10` PR-less discussion vs. contribution** — settles
|
|
the distinction between discussion (RFC-scoped, no PR, both
|
|
anonymous-readable and contributor-writeable) and contribution
|
|
(still requires a PR via §10.1). Also extends `§5`'s `threads`
|
|
table commentary so the null-branch interpretation is documented
|
|
as actively used rather than reserved scaffold.
|
|
- **SPEC `§17`** — lists the five new `discussion/...` endpoints
|
|
in the illustrative table.
|
|
|
|
### Changed
|
|
|
|
- **`backend/app/chat.py`** — `_fan_out_chat` now passes
|
|
`branch_name` straight through to the notify chokepoint instead of
|
|
coercing `None` to `"main"`. The notifications row carries the
|
|
null through, which preserves the §15.7 reconciler's keying on
|
|
`(rfc_slug, branch_name)` for the eventual discussion-side
|
|
chat-seen advance (deferred to a §19.2 candidate). Existing
|
|
branch-scoped chat continues to pass non-null branch names; the
|
|
change is invisible to that path.
|
|
- **`backend/app/notify.py`** — `fan_out_chat_message`'s
|
|
`branch_name` parameter is now typed `str | None` to match the
|
|
v0.5.0 PR-less shape. Routing rules are unchanged; the inbox prose
|
|
renders identically whether the chat lives on a branch or on the
|
|
RFC's discussion surface, which is the right honest signal.
|
|
- **`RFCView.jsx`** — the right-column panel is now conditional:
|
|
when `branchParam === 'main'`, render `RFCDiscussionPanel`
|
|
(the new PR-less surface); otherwise render the existing
|
|
`ChatPanel` (branch chat unchanged).
|
|
|
|
### §19.2 candidates surfaced
|
|
|
|
- PR-less discussion: range / paragraph anchors (the data model
|
|
permits them; the UI is the deferred part).
|
|
- PR-less discussion: distinct notification `event_kind`s
|
|
(`open_rfc_discussion_thread`, etc.) if usage shows contributors
|
|
want to filter discussion-vs-branch in the §15.2 inbox.
|
|
- PR-less discussion: chat-seen cursor closing the §15.7
|
|
reconciliation loop for the new surface.
|
|
- PR-less discussion: AI participant invocation (discussion-only,
|
|
no `<change>` block side-effects).
|
|
- PR-less discussion: anonymous-read polish to match the v0.6.0
|
|
write-gate hardening (item #4).
|
|
|
|
### Upgrade steps (from 0.4.0)
|
|
|
|
1. The deployment **MUST** rebuild the frontend (`npm install &&
|
|
npm run build`) so `RFCDiscussionPanel.jsx` ships in the bundle.
|
|
No new env vars; existing `frontend/.env` is sufficient.
|
|
2. The deployment **MUST** restart the backend so the new
|
|
`api_discussion` router mounts. No schema migration runs — the
|
|
`threads` table already supports `branch_name IS NULL` per §5,
|
|
and the v0.5.0 build is the first to write rows in that shape.
|
|
3. The deployment **MAY** announce the new surface to its
|
|
contributors: the RFC view's main page now carries a discussion
|
|
panel below the document. Existing branch chat and PR review
|
|
surfaces are unchanged.
|
|
4. The deployment **SHOULD NOT** expect a hardening of the
|
|
anonymous-write gate in v0.5.0 — that lands in v0.6.0 (item #4).
|
|
v0.5.0's write paths already refuse anonymous posts, so no
|
|
pre-emptive operator action is needed.
|
|
|
|
## 0.4.0 — 2026-05-27
|
|
|
|
**Minor — no operator action required beyond rebuild + restart.** The
|
|
proposer of a new RFC is now the implicit first owner of its super-
|
|
draft entry, set automatically at propose time from the session user.
|
|
The §13.1 claim flow remains available for *additional* owners. No
|
|
schema changes, no env-var changes, no API-shape changes; only newly
|
|
proposed RFCs receive the auto-owner — existing super-drafts whose
|
|
`owners:` is empty are unaffected and can still be claimed via §13.1
|
|
as before. This is a §19.3 rule-2 spec correction: `SPEC.md` §9.1,
|
|
§9.2, and §13.1 are updated to reflect the new shape.
|
|
|
|
### Changed
|
|
|
|
- **`POST /api/rfcs/propose`** (`backend/app/api.py`) now sets
|
|
`Entry.owners = [user.gitea_login]` when constructing the new
|
|
super-draft entry, instead of `owners=[]`. The session's
|
|
`gitea_login` is the canonical source; the endpoint never accepted
|
|
an owner field from the request payload and still doesn't.
|
|
- **`SPEC.md` §9.1** narrowed: the "no proposed-owner or working-
|
|
group fields" sentence becomes a proposer-owner-auto / working-
|
|
group-deferred split, with a §19.3 rule-2 note.
|
|
- **`SPEC.md` §9.2** frontmatter shape: `owners: []` → `owners:
|
|
[<proposer.gitea_login>]`, with a §19.3 rule-2 note.
|
|
- **`SPEC.md` §13.1** reframed: claim flow is now a graduation-time
|
|
broadening for additional owners, not a precondition for the
|
|
proposer's own RFC. The §13.1 / §13.2 / §13.3 graduation pipeline
|
|
itself is unchanged — the "at least one owner" precondition for
|
|
graduation still holds and is now satisfied by default.
|
|
|
|
### Upgrade steps (from 0.3.0)
|
|
|
|
1. The deployment **MUST** rebuild and restart per the routine
|
|
deploy steps. No `.env` changes, no schema/migration changes, no
|
|
API-shape changes.
|
|
2. Operators **SHOULD** note that newly proposed RFCs after the
|
|
upgrade carry the proposer in `owners:` automatically. Existing
|
|
super-drafts with empty `owners:` are not migrated; they remain
|
|
claimable via the §13.1 flow exactly as before. No deployment-
|
|
side data action is required.
|
|
3. Deployments **MAY** communicate the UX shift to active proposers
|
|
(the "Claim ownership" affordance no longer applies to your own
|
|
newly proposed RFC), but the affordance simply hides on RFCs the
|
|
viewer already owns, so no operator-side action is required.
|
|
|
|
## 0.3.0 — 2026-05-27
|
|
|
|
**Minor — operator action required if a deployment wants to enable the
|
|
private-beta gate; no action required to stay open.** This release adds
|
|
an email allowlist that, when populated, restricts OAuth sign-in to the
|
|
listed emails while keeping all read paths public. Anonymous visitors
|
|
now see the full app (catalog, RFC bodies, public branch conversations)
|
|
in read-only mode instead of the §14.1 landing-page wall.
|
|
|
|
### Added
|
|
|
|
- **`allowed_emails` table** (`backend/migrations/011_allowlist.sql`).
|
|
Empty list = gate off (any successful OAuth provisions a user, as
|
|
before). Any rows present = gate on (only listed emails, plus
|
|
users already grandfathered by `gitea_id`, may sign in).
|
|
- **Admin → Allowlist tab** at `/admin/allowlist`. Add/remove emails,
|
|
see who added each row and when. Status banner shows whether the
|
|
gate is currently active.
|
|
- **`/beta-pending` page** shown after a rejected OAuth callback. Free-
|
|
text invite-contact line is configurable via the new
|
|
`VITE_BETA_CONTACT` env var (optional; falls back to a generic line).
|
|
- **Beta chips** next to the Discuss/Contribute mode toggle, the Sign
|
|
in link, and the header Sign-in button so anonymous viewers see
|
|
immediately what is gated.
|
|
- **Anonymous read mode** in the React app: the §14.1 Landing page is
|
|
preserved at `/welcome` for deployments that want to link to it, but
|
|
the default route now renders the full app shell with write
|
|
affordances hidden behind a sign-in CTA.
|
|
|
|
### Changed
|
|
|
|
- **`/auth/callback`** now consults `auth.is_allowed_sign_in()` after
|
|
fetching the Gitea profile. Rejected sign-ins clear the OAuth state
|
|
and redirect to `/beta-pending`; the session is not populated.
|
|
- **`Catalog`** receives a `viewer` prop. Anonymous viewers see "Sign
|
|
in to propose (Beta)" instead of "+ Propose New RFC".
|
|
- **`PhilosophyWithSidebar`** now reads `authenticated` from the
|
|
current viewer instead of hardcoded `true`.
|
|
|
|
### Fixed
|
|
|
|
- **Single-finger scroll on the `/philosophy` page** (and any other
|
|
`.chrome-pane`-hosted view: `/admin/*`, `/settings/notifications`)
|
|
was broken on iOS Safari. The `.app` container used `height: 100vh`,
|
|
which on iOS measures the URL-bar-hidden ("largest") viewport — so
|
|
`.app` overflowed what's actually visible. Combined with the
|
|
`body { overflow: hidden }` in `index.css`, this meant single-finger
|
|
touches on the visible area were consumed by the (blocked) page-
|
|
level scroll attempt rather than reaching the nested `.chrome-pane`
|
|
scroll. Two-finger touches bypassed the page-level layer and
|
|
one-finger then worked once the URL bar had collapsed. Switched
|
|
`.app` to `height: 100dvh` (dynamic viewport — adjusts as the URL
|
|
bar shows/hides), with `100vh` retained as a fallback for browsers
|
|
predating iOS 15.4 / Chrome 108.
|
|
|
|
### Upgrade steps (from 0.2.3)
|
|
|
|
1. The deployment **MUST** rebuild the frontend with the new
|
|
`VITE_BETA_CONTACT` env var optionally set in `frontend/.env` (it
|
|
is OK to leave it blank — the `/beta-pending` page falls back to
|
|
a generic line).
|
|
2. The deployment **MUST** restart the backend so migration
|
|
`011_allowlist.sql` runs. No data loss; the new table starts
|
|
empty, which keeps the gate off and preserves existing behavior.
|
|
3. To **enable** the private-beta gate, the deployment operator
|
|
**SHOULD** sign in once (so their `users` row exists and they
|
|
grandfather in by `gitea_id`), then open `/admin/allowlist` and
|
|
add the first invited email. The first row added turns the gate
|
|
on for any user not yet in `users`.
|
|
4. To **stay open**, do nothing — leave `allowed_emails` empty and
|
|
the deployment behaves exactly as 0.2.3.
|
|
5. The deployment **MAY** customise its `/beta-pending` contact line
|
|
by setting `VITE_BETA_CONTACT` (an email, a URL, or a short
|
|
instruction) before the frontend build. Unset is fine.
|
|
|
|
## 0.2.3 — 2026-05-26
|
|
|
|
**Patch — no operator action required.** Rebuild and restart per the
|
|
routine deploy steps; no `.env` changes, no schema changes, no
|
|
behavior changes a deployment would notice in steady state beyond
|
|
the new endpoint below.
|
|
|
|
### Added
|
|
|
|
- **`GET /api/health`** — an unauthenticated probe returning JSON
|
|
`{version, status}` for ops tooling. `version` is the running
|
|
framework version (the contents of `VERSION` per §20.1, read at
|
|
process start and cached as a module-level constant in
|
|
`backend/app/health.py`); `status` is `"ok"` with HTTP 200 in
|
|
v1. The `"degraded"` / HTTP 503 path stays reserved in the
|
|
response shape so a later release can wire real degradation
|
|
conditions without breaking the contract. The version-match
|
|
check is the structural value — a deploy-control-panel polling
|
|
the endpoint after `systemctl restart` catches the failure mode
|
|
where a restart did not pick up the new code. See `SPEC.md` §17
|
|
and the §19.2 settlement.
|
|
|
|
### Upgrade steps (from 0.2.2)
|
|
|
|
1. The deployment **MAY** configure its monitoring (Pingdom,
|
|
Healthchecks.io, the flotilla deploy control panel, etc.) to
|
|
probe `/api/health` and compare the returned `version` against
|
|
the tag last deployed. The endpoint is unauthenticated by
|
|
design — no PII in the payload, no session required.
|
|
|
|
## 0.2.2 — 2026-05-26
|
|
|
|
**Patch — no operator action required.** Rebuild the frontend and
|
|
restart per the routine deploy steps; no `.env` changes, no schema
|
|
changes, no behavior changes a deployment would notice in steady
|
|
state beyond the fix below.
|
|
|
|
### Fixed
|
|
|
|
- **Mermaid blocks rendered as raw code on `/philosophy`.**
|
|
`frontend/src/components/Philosophy.jsx` parsed PHILOSOPHY.md with
|
|
the bare `marked` import and `dangerouslySetInnerHTML`, so
|
|
```` ```mermaid ```` fences fell through as `<pre>` blocks rather
|
|
than rendered diagrams. Swapped to `MarkdownPreview`, which already
|
|
carries the lazy mermaid loader + SVG render path used by the RFC
|
|
body view, so the philosophy surface now renders mermaid the same
|
|
way RFC bodies do. Pre-existing gap, surfaced when a deployment
|
|
authored a mermaid block in its `PHILOSOPHY_PATH` override.
|
|
|
|
## 0.2.1 — 2026-05-26
|
|
|
|
**Patch — no operator action required.** Rebuild the frontend and
|
|
restart per the routine deploy steps; no `.env` changes, no schema
|
|
changes, no behavior changes a deployment would notice in steady
|
|
state.
|
|
|
|
### Fixed
|
|
|
|
- **PR view blank page (React #310).** `frontend/src/components/PRView.jsx`
|
|
declared its `threadsByKind` `useMemo` *after* the early-return
|
|
guard for the loading state (`if (!pr) return …`). On first mount
|
|
`pr` was null so the early return fired and 14 hooks were called;
|
|
on the second render `pr` was populated and execution reached the
|
|
`useMemo`, calling 15 hooks — violating the Rules of Hooks and
|
|
unmounting the page subtree (blank screen). The `useMemo` is now
|
|
declared above the early returns with optional-chaining on `pr`,
|
|
keeping the hook count stable between the loading and loaded
|
|
renders. Pre-existing bug in the post-Contribute-rewrite PR view;
|
|
surfaced in production by 0.2.0's graduation merge race fix
|
|
enabling the workflow that opens this code path.
|
|
|
|
## 0.2.0 — 2026-05-26
|
|
|
|
**Breaking config change.** The frontend now requires
|
|
`VITE_APP_NAME` to be set at build time. `npm run build` fails with a
|
|
clear message if it is missing. There is no default; every deployment
|
|
names itself.
|
|
|
|
### Upgrade steps (from 0.1.0)
|
|
|
|
1. The deployment **MUST** create a `frontend/.env` file before
|
|
building. See `frontend/.env.example` for the contract.
|
|
2. The deployment **MUST** set `VITE_APP_NAME` in `frontend/.env` to
|
|
the user-visible name it wants to ship — the string used as the
|
|
browser tab title, the header brand, and the landing H1. The
|
|
build will fail loudly if this is unset or blank.
|
|
3. The deployment **MUST** rebuild the frontend (`npm install && npm
|
|
run build`) and redeploy the resulting bundle. The previous
|
|
bundle does not read `VITE_APP_NAME`.
|
|
4. The deployment **SHOULD** verify the name appears correctly in
|
|
the browser tab and the header after redeploy before bumping its
|
|
`.rfc-app-version` pin to `0.2.0`.
|
|
5. The deployment **MAY**, in the same upgrade, take the opportunity
|
|
to retire any local overrides it was using to brand the
|
|
pre-0.2.0 hardcoded strings — those overrides are now obsolete.
|
|
6. The deployment **MUST NOT** continue to expect graduation step 4
|
|
to fail on Gitea's "Please try again later" race; that path is
|
|
now handled by `wait_for_mergeable` + bounded retry. Any local
|
|
workaround retrying graduation at the deployment level is now
|
|
redundant and **SHOULD** be removed.
|
|
|
|
### Fixed
|
|
|
|
- **Graduation merge race.** §13.3 step 4 (`merge_pr`) used to call
|
|
Gitea's merge endpoint the instant step 3 returned, which races
|
|
Gitea's background mergeability computation and produces the
|
|
`405 "Please try again later"` response. Step 4 now waits for the
|
|
`mergeable` field to become non-null (bounded by a 30s timeout) and
|
|
retries the merge call up to three times on the transient response.
|
|
See `backend/app/gitea.py#wait_for_mergeable` and
|
|
`backend/app/bot.py#_merge_with_retry`.
|
|
|
|
### Changed
|
|
|
|
- `frontend/src/App.jsx` header brand reads `VITE_APP_NAME` instead of
|
|
a hardcoded string.
|
|
- `frontend/src/components/Landing.jsx` H1 reads `VITE_APP_NAME`
|
|
instead of a hardcoded string. The pitch / deck / attribution copy
|
|
in this file is still deployment-specific and tracked as a follow-up
|
|
for extraction into deployment config.
|
|
- `frontend/index.html` `<title>` is rewritten at build time via Vite's
|
|
HTML transform.
|
|
|
|
### Added
|
|
|
|
- `frontend/.env.example` documenting the new required variable.
|
|
- Top-level `VERSION` file and this `CHANGELOG.md` as the canonical
|
|
release log.
|
|
- `SPEC.md` §20 (versioning and downstream deployments) as the
|
|
binding policy for the framework/deployment relationship.
|
|
- `docs/DEPLOYMENTS.md` as the practical guide for building on
|
|
rfc-app and upgrading existing deployments to new versions.
|
|
- `CLAUDE.md` at the repo root capturing the separation-of-concerns
|
|
rule for working sessions.
|
|
|
|
## 0.1.0 — v1 build
|
|
|
|
Initial release. See `docs/DEV.md` for the slicing plan and build
|
|
history.
|