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
+28 -4
View File
@@ -87,11 +87,15 @@ export default function App() {
// The deployment is in private beta: anonymous visitors get the full
// app in read-only mode (viewer = null is passed through to every
// component), and write affordances are hidden at the component
// level. /beta-pending is the post-OAuth-rejection page reachable by
// anyone. The original §14.1 Landing surface is retained for the
// `/welcome` URL only, in case a deployment wants to link to it.
// level. v0.8.0 (§6.1 / item #6): authenticated users with
// `permission_state='pending'` also pass through as `viewer` with
// their state attached — every write-gated affordance reads the
// state and treats pending the same as anonymous, while reads
// remain open. The /beta-pending page is the home root for a
// pending user.
const viewer = me?.authenticated ? me.user : null
const isAdmin = viewer && (viewer.role === 'owner' || viewer.role === 'admin')
const isPending = viewer && viewer.permission_state === 'pending'
return (
<div className="app">
@@ -142,11 +146,12 @@ export default function App() {
)}
</div>
</header>
{isPending && <PendingAccessBanner />}
<div className="app-body">
<Routes>
<Route path="/welcome" element={<Landing />} />
<Route path="/login" element={<Login />} />
<Route path="/beta-pending" element={<BetaPending />} />
<Route path="/beta-pending" element={<BetaPending viewer={viewer} />} />
<Route path="/philosophy" element={<PhilosophyWithSidebar viewer={viewer} />} />
{/* §14.5 / §14.6: cookie-consent companions to /philosophy.
Available to anonymous and authenticated viewers alike. */}
@@ -232,7 +237,26 @@ function AdminWithSidebar({ viewer }) {
)
}
function PendingAccessBanner() {
// v0.8.0 — thin banner shown on every page (other than /beta-pending
// itself, which carries the same message in larger form) when the
// signed-in user's `permission_state='pending'`. Sign-out works
// normally via the header affordance.
return (
<div className="pending-access-banner">
Your beta access request is in review.{' '}
<Link to="/beta-pending">Learn more </Link>
</div>
)
}
function Welcome({ viewer }) {
// v0.8.0 — a pending user landing on "/" gets the same page they'd
// see at /beta-pending, inline. This is the post-OTC home root for
// a user awaiting admin grant.
if (viewer && viewer.permission_state === 'pending') {
return <BetaPending viewer={viewer} />
}
if (!viewer) {
return (
<div className="welcome">