/** * 文件库审计 sink(契约 C3 的入驻适配)。 * * 契约原文:本地 outbox 表(与业务同事务)→ 中继 POST 到独立审计服务。 * 入驻 hub 后的适配:审计同事 = 本库 AuditEntry,与业务写在同一 Prisma 事务 * 内落库 —— 同库同事务天然满足"操作成功则日志必存在",比 outbox+relay 更强。 * 若审计团队日后独立成服务,只换本文件的实现,action 词汇表保持不变。 */ import type { Prisma } from "@prisma/client"; /** C3 §6.3:文件库审计动作词汇表(与契约文档逐条对应,改词需升契约版本)。 */ export const FILE_LIB_AUDIT_ACTIONS = { folderCreate: "folder.create", folderRename: "folder.rename", folderMove: "folder.move", folderDelete: "folder.delete", projectCreate: "project.create", projectRename: "project.rename", projectMove: "project.move", projectDelete: "project.delete", permissionGrant: "permission.grant", permissionUpdate: "permission.update", permissionRevoke: "permission.revoke", independentEnable: "project.independent_permission.enable", independentDisable: "project.independent_permission.disable", independentChange: "project.independent_permission.change", fileUpload: "file.upload", fileRename: "file.rename", fileDelete: "file.delete", fileCommit: "file.commit", fileConflictDetected: "file.conflict_detected", exportRun: "export.run", adminForceAdjust: "admin.force_adjust", // ADR-0028:成员组内置进 hub,组动作在本地审计(契约 C3 §6.3 原委托外部 Group 服务)。 groupCreate: "group.create", groupUpdate: "group.update", groupDelete: "group.delete", groupRestore: "group.restore", groupMemberAdd: "group.member_add", groupMemberRemove: "group.member_remove", } as const; export type FileLibAuditObjectType = "folder" | "project" | "file" | "grant" | "export_job" | "group"; export interface FileLibAuditEntry { readonly action: string; readonly actorUserId: string; readonly organizationId: string; readonly objectType: FileLibAuditObjectType; readonly objectId: string; /** 节点 id 路径(pathIds)或项目内文件路径,便于按路径检索。 */ readonly objectPath: string; readonly detail?: Record | undefined; } /** * 在调用方的事务里写一条审计。刻意不吞错:写不出来整个业务操作回滚 * (需求 5.1"操作成功则日志必存在"的强保证)。 */ export async function writeFileLibAudit( tx: Prisma.TransactionClient, entry: FileLibAuditEntry, ): Promise { const metadata: Record = { objectType: entry.objectType, objectId: entry.objectId, objectPath: entry.objectPath, ...(entry.detail ?? {}), }; await tx.auditEntry.create({ data: { action: entry.action, actorUserId: entry.actorUserId, organizationId: entry.organizationId, metadata: metadata as Prisma.InputJsonValue, }, }); }