import type { PrismaClient, PrincipalType } from "@prisma/client"; export interface PrincipalRef { readonly type: PrincipalType; readonly id: string; } export interface ActorInput { readonly feishuOpenId: string; readonly chatId?: string | undefined; } export type PrincipalSource = | "actor-user" | "context-chat" | "team-membership" | "external-membership" | "team-external-binding"; export interface ResolvedPrincipal { readonly principal: PrincipalRef; readonly source: PrincipalSource; readonly via?: PrincipalRef | undefined; } export interface PrincipalResolution { readonly actor: ActorInput; readonly userId?: string | undefined; readonly principals: readonly PrincipalRef[]; readonly resolved: readonly ResolvedPrincipal[]; } export interface PrincipalResolver { resolveActor(actor: ActorInput): Promise; } export class PrismaPrincipalResolver implements PrincipalResolver { constructor(private readonly prisma: PrismaClient) {} async resolveActor(actor: ActorInput): Promise { const resolved = new PrincipalCollector(); resolved.add({ type: "USER", id: actor.feishuOpenId }, "actor-user"); if (actor.chatId !== undefined && actor.chatId !== "") { resolved.add({ type: "FEISHU_CHAT", id: actor.chatId }, "context-chat"); } const user = await this.prisma.user.findUnique({ where: { feishuOpenId: actor.feishuOpenId }, select: { id: true }, }); if (user !== null) { const memberships = await this.prisma.teamMembership.findMany({ where: { userId: user.id, revokedAt: null, team: { archivedAt: null } }, select: { teamId: true }, }); for (const membership of memberships) { resolved.add({ type: "TEAM", id: membership.teamId }, "team-membership"); } const externalMemberships = await this.prisma.externalPrincipalMembership.findMany({ where: { userId: user.id, revokedAt: null }, select: { principalType: true, principalId: true }, }); for (const membership of externalMemberships) { resolved.add( { type: membership.principalType, id: membership.principalId }, "external-membership", ); } } const externalPrincipals = resolved.principals().filter((principal) => isExternalPrincipal(principal.type)); if (externalPrincipals.length > 0) { const bindings = await this.prisma.teamExternalBinding.findMany({ where: { revokedAt: null, team: { archivedAt: null }, OR: externalPrincipals.map((principal) => ({ principalType: principal.type, principalId: principal.id, })), }, select: { teamId: true, principalType: true, principalId: true }, }); for (const binding of bindings) { resolved.add( { type: "TEAM", id: binding.teamId }, "team-external-binding", { type: binding.principalType, id: binding.principalId }, ); } } return { actor, ...(user !== null ? { userId: user.id } : {}), principals: resolved.principals(), resolved: resolved.entries(), }; } } export function principalKey(principal: PrincipalRef): string { return `${principal.type}:${principal.id}`; } function isExternalPrincipal(type: PrincipalType): boolean { return type === "FEISHU_CHAT" || type === "FEISHU_DEPARTMENT" || type === "FEISHU_USER_GROUP"; } class PrincipalCollector { private readonly refs = new Map(); private readonly provenance: ResolvedPrincipal[] = []; add(principal: PrincipalRef, source: PrincipalSource, via?: PrincipalRef): void { const key = principalKey(principal); if (!this.refs.has(key)) { this.refs.set(key, principal); } this.provenance.push({ principal, source, ...(via !== undefined ? { via } : {}), }); } principals(): readonly PrincipalRef[] { return [...this.refs.values()]; } entries(): readonly ResolvedPrincipal[] { return this.provenance; } }