/** * @factory/permissions — permission levels and roles. * * Pure, framework-free primitives shared by the decision engine * (`resolve.ts`), the role matrix (`rules.ts`) and the API layer. * * Wire form is `snake_case` (`"full_access"`); the Prisma enums * (`prisma/schema.prisma`) are `SCREAMING_SNAKE` (`FULL_ACCESS`). The * mapping helpers at the bottom of this file are the single place where the * two forms meet — never compare DTO values against Prisma rows directly. */ /** Permission levels in ascending order of capability (index = rank). */ export const LEVELS = ["none", "view", "comment", "edit", "full_access"] as const; export type PermissionLevel = (typeof LEVELS)[number]; /** * Levels a user can hold on a resource. `"none"` is the absence of a level * (used by grants/shares to mean "no access") and therefore never requested. */ export type AccessLevel = Exclude; /** Workspace roles (prisma `WorkspaceRole`), in ascending authority order. */ export const ROLES = ["guest", "member", "admin", "owner"] as const; export type WorkspaceRole = (typeof ROLES)[number]; const LEVEL_RANK: Record = { none: 0, view: 1, comment: 2, edit: 3, full_access: 4, }; const LEVEL_TO_PRISMA: Record = { none: "NONE", view: "VIEW", comment: "COMMENT", edit: "EDIT", full_access: "FULL_ACCESS", }; const ROLE_TO_PRISMA: Record = { guest: "GUEST", member: "MEMBER", admin: "ADMIN", owner: "OWNER", }; /** * Rank of a level in {@link LEVELS} (0…4). Unknown values throw — a bad level * string is a bug, not a "none". */ export function levelRank(level: PermissionLevel): number { const rank = LEVEL_RANK[level]; if (rank === undefined) throw new Error(`unknown permission level: ${String(level)}`); return rank; } /** The higher of two levels. `maxLevel("edit", "view") === "edit"`. */ export function maxLevel(a: PermissionLevel, b: PermissionLevel): PermissionLevel { return levelRank(a) >= levelRank(b) ? a : b; } /** The lower of two levels. `minLevel("edit", "view") === "view"`. */ export function minLevel(a: PermissionLevel, b: PermissionLevel): PermissionLevel { return levelRank(a) <= levelRank(b) ? a : b; } /** * Whether `level` satisfies `required` (level ≥ required). * `levelAtLeast("edit", "comment") === true`, `levelAtLeast("view", "edit") === false`. */ export function levelAtLeast(level: PermissionLevel, required: PermissionLevel): boolean { return levelRank(level) >= levelRank(required); } /** * Convert a wire-level value to its Prisma enum string. * `levelToPrisma("full_access") === "FULL_ACCESS"`. */ export function levelToPrisma(level: PermissionLevel): string { return LEVEL_TO_PRISMA[level]; } /** * Convert a Prisma enum string (e.g. `"FULL_ACCESS"`, stored value) to the * wire-level value. Unknown strings throw. */ export function levelFromPrisma(value: string): PermissionLevel { const entry = (Object.entries(LEVEL_TO_PRISMA) as [PermissionLevel, string][]).find( ([, prisma]) => prisma === value, ); if (entry === undefined) throw new Error(`unknown PermissionLevel prisma value: ${value}`); return entry[0]; } /** Convert a role to its Prisma enum string. `roleToPrisma("owner") === "OWNER"`. */ export function roleToPrisma(role: WorkspaceRole): string { return ROLE_TO_PRISMA[role]; } /** Convert a Prisma enum string (e.g. `"MEMBER"`) to the wire role value. */ export function roleFromPrisma(value: string): WorkspaceRole { const entry = (Object.entries(ROLE_TO_PRISMA) as [WorkspaceRole, string][]).find( ([, prisma]) => prisma === value, ); if (entry === undefined) throw new Error(`unknown WorkspaceRole prisma value: ${value}`); return entry[0]; } /** The 6 resource types a grant or share can attach to (matches prisma Permission). */ export const RESOURCE_TYPES = [ "workspace", "teamspace", "page", "database", "record", "file", ] as const; export type ResourceType = (typeof RESOURCE_TYPES)[number];