Files
claudemesh/apps/broker/src/permissions.ts
Alejandro Gutiérrez bb1310167e
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
feat: granular mesh permissions + mesh delete + share picker
- Drizzle schema: mesh.permission table with 11 boolean flags
- Default permissions by role (owner > admin > member)
- Broker: GET/POST /cli/mesh/:slug/permissions
- Broker: DELETE /cli/mesh/:slug (owner only, soft delete)
- Broker: permission check module (getPermissions, checkPermission, setPermissions)
- CLI: mesh share with interactive mesh picker
- CLI: mesh delete with server-side delete + confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:03:28 +01:00

113 lines
3.2 KiB
TypeScript

/**
* Granular permission checks for mesh operations.
*
* If a meshPermission row exists for the member, use it.
* Otherwise, derive defaults from the member's role.
*/
import { and, eq } from "drizzle-orm";
import { db } from "./db";
import { meshPermission, meshMember, mesh, DEFAULT_PERMISSIONS } from "@turbostarter/db/schema/mesh";
import type { PermissionKey } from "@turbostarter/db/schema/mesh";
export interface ResolvedPermissions {
canInvite: boolean;
canDeployMcp: boolean;
canManageFiles: boolean;
canManageVault: boolean;
canManageWatches: boolean;
canManageWebhooks: boolean;
canWriteState: boolean;
canSend: boolean;
canUseTools: boolean;
canDeleteMesh: boolean;
canManagePermissions: boolean;
}
/**
* Get effective permissions for a member in a mesh.
* Checks for explicit permission row, falls back to role defaults.
*/
export async function getPermissions(meshId: string, memberId: string): Promise<ResolvedPermissions> {
// Get the explicit permission row if it exists
const [perm] = await db.select().from(meshPermission)
.where(and(eq(meshPermission.meshId, meshId), eq(meshPermission.memberId, memberId)))
.limit(1);
if (perm) {
return {
canInvite: perm.canInvite,
canDeployMcp: perm.canDeployMcp,
canManageFiles: perm.canManageFiles,
canManageVault: perm.canManageVault,
canManageWatches: perm.canManageWatches,
canManageWebhooks: perm.canManageWebhooks,
canWriteState: perm.canWriteState,
canSend: perm.canSend,
canUseTools: perm.canUseTools,
canDeleteMesh: perm.canDeleteMesh,
canManagePermissions: perm.canManagePermissions,
};
}
// Fall back to role-based defaults
const [member] = await db.select().from(meshMember)
.where(eq(meshMember.id, memberId))
.limit(1);
if (!member) return DEFAULT_PERMISSIONS.member;
// Check if member is mesh owner
const [m] = await db.select().from(mesh)
.where(eq(mesh.id, meshId))
.limit(1);
if (m && m.ownerUserId && member.userId === m.ownerUserId) {
return DEFAULT_PERMISSIONS.owner;
}
return DEFAULT_PERMISSIONS[member.role] ?? DEFAULT_PERMISSIONS.member;
}
/**
* Check a single permission for a member.
* Returns true if allowed, false if denied.
*/
export async function checkPermission(
meshId: string,
memberId: string,
permission: PermissionKey,
): Promise<boolean> {
const perms = await getPermissions(meshId, memberId);
return perms[permission];
}
/**
* Set explicit permissions for a member (partial update).
* Creates the row if it doesn't exist.
*/
export async function setPermissions(
meshId: string,
memberId: string,
updates: Partial<ResolvedPermissions>,
): Promise<void> {
const [existing] = await db.select().from(meshPermission)
.where(and(eq(meshPermission.meshId, meshId), eq(meshPermission.memberId, memberId)))
.limit(1);
if (existing) {
await db.update(meshPermission)
.set({ ...updates, updatedAt: new Date() })
.where(eq(meshPermission.id, existing.id));
} else {
// Get role defaults first, then overlay updates
const defaults = await getPermissions(meshId, memberId);
await db.insert(meshPermission).values({
meshId,
memberId,
...defaults,
...updates,
});
}
}