Skip to content
File

Blob: src/shared/entitlements/member-management.ts

typescript45 lines
1import { isWorkspaceAdminOrOwnerRole, type ResolvedWorkspaceRole } from "@/shared/entitlements/common";
2import type { InviteRole } from "@/shared/types";
3 
4// Role-change policy. Mirrors the invariants enforced at
5// src/worker/routes/workspaces.ts for PATCH /workspaces/:id/members/:uid:
6// - only admin or owner can reach this action
7// - owner cannot have their role changed
8// - only the owner can promote anyone to admin
9// Applied symmetrically on the worker (authoritative) and the client (for
10// affordance gating on the settings members list).
11export function canChangeMemberRole(
12 viewerRole: ResolvedWorkspaceRole,
13 targetRole: ResolvedWorkspaceRole,
14 newRole: InviteRole,
15): boolean {
16 if (!isWorkspaceAdminOrOwnerRole(viewerRole)) return false;
17 if (targetRole === "owner") return false;
18 if (newRole === "admin" && viewerRole !== "owner") return false;
19 return true;
20}
21 
22// Removal policy. Mirrors DELETE /workspaces/:id/members/:uid:
23// - self-removal is allowed for anyone except owner
24// - otherwise caller must be admin or owner
25// - owner cannot be removed
26// - admin cannot remove another admin
27export function canRemoveMember(
28 viewerRole: ResolvedWorkspaceRole,
29 targetRole: ResolvedWorkspaceRole,
30 isSelf: boolean,
31): boolean {
32 if (isSelf) return viewerRole !== "owner" && viewerRole !== "none";
33 if (!isWorkspaceAdminOrOwnerRole(viewerRole)) return false;
34 if (targetRole === "owner") return false;
35 if (targetRole === "admin" && viewerRole !== "owner") return false;
36 return true;
37}
38 
39// Self-leave affordance shorthand. Owners cannot leave; non-members have
40// nothing to leave. Everyone else (admin, member, guest) can self-exit via the
41// same DELETE route that powers removal.
42export function canLeaveWorkspace(viewerRole: ResolvedWorkspaceRole): boolean {
43 return viewerRole !== "owner" && viewerRole !== "none";
44}