import type { Actor } from "@rakazo/contracts"; import type { PrismaClient } from "./client.js"; export class IsolationError extends Error { constructor(message = "Resource not found") { super(message); this.name = "IsolationError"; } } /** Unique (spaceId, userId, name) collision when renaming a bot section. */ export class BotSectionNameConflictError extends Error { constructor(message = "Section name already used") { super(message); this.name = "BotSectionNameConflictError"; } } /** Thread clear blocked while a delegated bot run still owes its coordinator a return. */ export class ClearThreadPendingDelegatedReturnsError extends Error { constructor(message = "Delegated bot work is still finishing") { super(message); this.name = "ClearThreadPendingDelegatedReturnsError"; } } export async function requireMembership( prisma: PrismaClient, userId: string, requestedSpaceId?: string | null, ): Promise { const membership = await prisma.spaceMember.findFirst({ where: { userId, ...(requestedSpaceId ? { spaceId: requestedSpaceId } : {}), }, orderBy: [{ space: { isDefault: "desc" } }, { createdAt: "asc" }, { id: "asc" }], include: { member: { include: { user: true } } }, }); if (!membership) { throw new IsolationError("No personal space"); } const settings = await prisma.deploymentSettings.findUnique({ where: { id: "default" }, }); return { userId: membership.userId, spaceId: membership.spaceId, email: membership.member.user.email, isDeploymentOwner: settings?.ownerUserId === membership.userId, }; } export function scoped( actor: Actor, record: T | null, ): T { if (!record || record.spaceId !== actor.spaceId) { throw new IsolationError(); } if (record.userId && record.userId !== actor.userId) { throw new IsolationError(); } return record; }