Gate the publish affordance and the accessLevel='public' write path behind isCloud()
Opened by Zhu Yue ·
When MOTIR_CLOUD is unset, a project cannot be made public. Two halves, one coherent gate (defense in depth — neither half alone is the gate):
- Server —
setAccessLevelinlib/services/projectMembersService.tsrejectslevel === 'public'when!isCloud(), throwing a typed error (an "unavailable on this build" domain error) thatPATCH /api/projects/[key]/accessmaps to a non-500 response. This is the enforcement point; the write is refused even if the UI is bypassed. - UI — the access-level selector (
app/(authed)/settings/project/members/_components/ProjectMembersSettings.tsx) no longer offers thepublicoption, and the "go public" affordances (app/(authed)/_components/build-in-public/{useGoPublic.ts,BuildingInPublicHeaderLink.tsx}) are not rendered, when!isCloud().
Only the public level is gated. The open / limited / private browse-access levels — how a self-hosted team shares within its own workspace — are unchanged in both arms. The gate reads isCloud() (single-reader rule).
No new design: this suppresses an already-designed, already-shipped control (design/projects/access-members.mock.html) — the public option simply is not offered; nothing new is drawn.
Acceptance criteria
- With
MOTIR_CLOUDunset,setAccessLevel({ level: 'public' })throws a typed error; the access route maps it to a non-500 response (404 or 400), and nomadePublicAtstamp is written. - With
MOTIR_CLOUDunset, the access selector does not offerpublic, and the go-public affordances are not rendered. - With
MOTIR_CLOUD=true, behaviour is identical to today:publicremains settable andmadePublicAtis stamped on the transition intopublic. - The gate reads
isCloud()only (single-reader rule). open/limited/privateare unaffected in both arms.- ≥90% coverage on the touched files.
Context refs
lib/services/projectMembersService.ts—setAccessLevel(the enforcement point)app/api/projects/[key]/access/route.ts— thePATCHwrite routelib/projects/roles.ts—PROJECT_ACCESS_LEVELS = ['public','open','limited','private'],asAccessLevelapp/(authed)/settings/project/members/_components/ProjectMembersSettings.tsx— the access selectorapp/(authed)/_components/build-in-public/{useGoPublic.ts,BuildingInPublicHeaderLink.tsx}— the go-public affordancesdesign/projects/access-members.mock.html— the existing (already-shipped) selector design; suppressed, not redrawnlib/billing/availability.ts—isCloud()
Discussion
No comments yet.
Adding to this discussion signs you in on app.motir.co and brings you back to this request.