Release 0.16.0: owner-only invite for per-RFC contribution + discussion
Lands roadmap item #12: the RFC's owner can invite specific users by email to one of two per-RFC roles — contributor (open PRs and join discussion) or discussant (join discussion only). Non-invited users keep the v0.6.0 anonymous-read contract: they can read but cannot write/discuss that RFC. The platform-level grant (v0.8.0 / item #6) is unchanged; this release adds a per-RFC membership layer beneath it. Migration 018_rfc_invitations.sql adds rfc_invitations (the lifecycle row with the email token and 30-day expiry) and rfc_collaborators (the accepted-invitation substrate the write gate consults). FK-cascaded against cached_rfcs and users per §5. New endpoints (in backend/app/api_invitations.py): POST /api/rfcs/{slug}/invitations (owner) GET /api/rfcs/{slug}/invitations (owner) POST /api/rfcs/{slug}/invitations/{id}/revoke (owner) GET /api/invitations/accept?token=… (signed-in) POST /api/invitations/accept (signed-in) The discussion / branch / open-pr write surfaces compose new auth.can_discuss_rfc and auth.can_contribute_to_rfc predicates after the existing require_contributor check. A super-draft with no frontmatter owners yet falls through to the platform-granted contract — the gate engages only once an owner exists. The email path reuses the existing SMTP plumbing (EmailConfig.from_env) the v0.7.0 OTC and v0.5.0 notification mailers share — transactional envelope, no preferences honored, no unsubscribe footer. The §17 admin user-management surface (item #7, v0.9.0) is extended additively: GET /api/admin/users carries a new per-user rfc_invitations array naming each accepted per-RFC collaboration with inviter / RFC / role / timestamp. The platform-grant decision keeps its context without restructuring the existing shape. Frontend additions: InvitationsModal.jsx (owner-only RFC view header strip affordance), AcceptInvitation.jsx (the /invitations/accept landing page), five api.js helpers. 237 backend tests pass (18 new in test_rfc_invitations_vertical.py, three older tests updated to opt into the per-RFC contributor contract via the new grant_rfc_collaborator test seam). Frontend build clean. No new env vars. No new overlay keys. Migration auto-applied on backend start by the existing db.run_migrations() sweep. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -279,6 +279,15 @@ def make_router(
|
||||
@router.post("/api/rfcs/{slug}/branches/main/promote-to-branch")
|
||||
async def promote_to_branch(slug: str, body: PromoteToBranchBody, request: Request) -> dict[str, Any]:
|
||||
viewer = auth.require_contributor(request)
|
||||
# v0.16.0 (item #12): cutting a contribute branch is the
|
||||
# PR-shaped write surface gate. A platform-granted user who is
|
||||
# not invited as a per-RFC contributor cannot start work that
|
||||
# only exists to land in a PR.
|
||||
if not auth.can_contribute_to_rfc(viewer, slug):
|
||||
raise HTTPException(
|
||||
403,
|
||||
"This RFC's owner has not invited you to contribute PRs",
|
||||
)
|
||||
rfc = _require_active_rfc(slug)
|
||||
owner, repo = _repo_for(rfc)
|
||||
new_branch = (body.branch_name or "").strip()
|
||||
@@ -331,6 +340,14 @@ def make_router(
|
||||
@router.post("/api/rfcs/{slug}/start-edit-branch")
|
||||
async def start_edit_branch(slug: str, body: StartEditBranchBody, request: Request) -> dict[str, Any]:
|
||||
viewer = auth.require_contributor(request)
|
||||
# v0.16.0 (item #12): same per-RFC contribute gate as
|
||||
# promote-to-branch — kicking off a super-draft edit branch is
|
||||
# also PR-shaped work.
|
||||
if not auth.can_contribute_to_rfc(viewer, slug):
|
||||
raise HTTPException(
|
||||
403,
|
||||
"This RFC's owner has not invited you to contribute PRs",
|
||||
)
|
||||
rfc = _require_super_draft(slug)
|
||||
owner, repo = _repo_for(rfc)
|
||||
new_branch = (body.branch_name or "").strip()
|
||||
|
||||
Reference in New Issue
Block a user