import prisma from '@/lib/prisma' export type BillingOwnerContext = { billingOwnerId: string isGuestActor: boolean } /** Resolve quota billing owner from an already-loaded session row (no extra query). */ export function billingOwnerFromSession( sessionUserId: string, requestingUserId: string, ): BillingOwnerContext { if (!sessionUserId) { throw new Error('Session has no owner — cannot resolve billing owner') } return { billingOwnerId: sessionUserId, isGuestActor: sessionUserId !== requestingUserId, } } /** Host-pays: all collaborative AI usage is billed to BrainstormSession.userId (Story 3.4). */ export async function getBillingOwner( sessionId: string, requestingUserId: string, ): Promise { const session = await prisma.brainstormSession.findUnique({ where: { id: sessionId }, select: { userId: true }, }) if (!session) { throw new Error('Session not found') } return billingOwnerFromSession(session.userId, requestingUserId) } export async function verifyParticipant( sessionId: string, userId: string, requiredRole?: 'host' | 'editor' | 'viewer' ): Promise<{ isParticipant: boolean; role: string }> { const participant = await prisma.brainstormParticipant.findFirst({ where: { sessionId, userId }, }) if (!participant) { return { isParticipant: false, role: 'none' } } await prisma.brainstormParticipant.update({ where: { id: participant.id }, data: { lastSeenAt: new Date() }, }) if (requiredRole === 'host' && participant.role !== 'host') { return { isParticipant: false, role: participant.role } } if (requiredRole === 'editor' && participant.role === 'viewer') { return { isParticipant: false, role: participant.role } } return { isParticipant: true, role: participant.role } } export async function logActivity( sessionId: string, action: string, userId?: string | null, details?: Record ) { await prisma.brainstormActivity.create({ data: { sessionId, userId: userId || null, action, details: details ? JSON.stringify(details) : null, }, }) } // [UPDATE - SÉCURITÉ] Résoudre l'userId et le périmètre de notes autorisé pour les appels IA. // - Hôte : accès complet à ses notes (publicNoteIds = null) // - Invité : restreint aux contextNoteIds publics de la session (publicNoteIds = string[] | []) export async function resolveAiContextUserId( sessionId: string, requestingUserId: string ): Promise<{ aiUserId: string; isGuest: boolean; publicNoteIds: string[] | null }> { const session = await prisma.brainstormSession.findUnique({ where: { id: sessionId }, select: { userId: true, contextNoteIds: true, }, }) if (!session) throw new Error('Session not found') const isHost = session.userId === requestingUserId if (isHost) { return { aiUserId: requestingUserId, isGuest: false, publicNoteIds: null } } // Invité : on restreint aux contextNoteIds déclarés publics par l'hôte const publicNoteIds: string[] = session.contextNoteIds ? (JSON.parse(session.contextNoteIds) as string[]) : [] return { aiUserId: session.userId, isGuest: true, publicNoteIds: publicNoteIds.length > 0 ? publicNoteIds : [], } } // [UPDATE - SÉCURITÉ] Sanitize les notes injectées dans un prompt IA pour un invité. // Tronque le contenu et masque les entités nommées (Prénom Nom) avec [Person]. export function sanitizeNotesForGuest( notes: { id: string; title: string | null; summary: string }[] ): { id: string; title: string; summary: string }[] { const namedEntityRe = /\b[A-ZÀ-Ü][a-zà-ü]+ [A-ZÀ-Ü][a-zà-ü]+\b/g return notes.map(n => ({ id: n.id, title: (n.title || 'Note').replace(namedEntityRe, '[Person]'), summary: n.summary.slice(0, 80).replace(namedEntityRe, '[Person]') + '…', })) } export async function captureSnapshot( sessionId: string, label: string, activityId?: string ): Promise { const ideas = await prisma.brainstormIdea.findMany({ where: { sessionId, status: 'active' }, select: { id: true, title: true, waveNumber: true, positionX: true, positionY: true, parentIdeaId: true, noveltyScore: true, createdByType: true, status: true, }, orderBy: [{ waveNumber: 'asc' }, { createdAt: 'asc' }], }) const maxStep = await prisma.brainstormSnapshot.findFirst({ where: { sessionId }, orderBy: { step: 'desc' }, select: { step: true }, }) await prisma.brainstormSnapshot.create({ data: { sessionId, activityId: activityId || null, step: (maxStep?.step || 0) + 1, label, ideaGraph: JSON.stringify(ideas), }, }) }