Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-4035Done

Gate the publish affordance and the accessLevel='public' write path behind isCloud()

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):

  1. ServersetAccessLevel in lib/services/projectMembersService.ts rejects level === 'public' when !isCloud(), throwing a typed error (an "unavailable on this build" domain error) that PATCH /api/projects/[key]/access maps to a non-500 response. This is the enforcement point; the write is refused even if the UI is bypassed.
  2. UI — the access-level selector (app/(authed)/settings/project/members/_components/ProjectMembersSettings.tsx) no longer offers the public option, 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_CLOUD unset, setAccessLevel({ level: 'public' }) throws a typed error; the access route maps it to a non-500 response (404 or 400), and no madePublicAt stamp is written.
  • With MOTIR_CLOUD unset, the access selector does not offer public, and the go-public affordances are not rendered.
  • With MOTIR_CLOUD=true, behaviour is identical to today: public remains settable and madePublicAt is stamped on the transition into public.
  • The gate reads isCloud() only (single-reader rule).
  • open / limited / private are unaffected in both arms.
  • ≥90% coverage on the touched files.

Context refs

  • lib/services/projectMembersService.tssetAccessLevel (the enforcement point)
  • app/api/projects/[key]/access/route.ts — the PATCH write route
  • lib/projects/roles.tsPROJECT_ACCESS_LEVELS = ['public','open','limited','private'], asAccessLevel
  • app/(authed)/settings/project/members/_components/ProjectMembersSettings.tsx — the access selector
  • app/(authed)/_components/build-in-public/{useGoPublic.ts,BuildingInPublicHeaderLink.tsx} — the go-public affordances
  • design/projects/access-members.mock.html — the existing (already-shipped) selector design; suppressed, not redrawn
  • lib/billing/availability.tsisCloud()