// P4 Files (AK-P4a) §12 — audit + activity wiring for the Files module. // // Audit (DoD 13): every write action records a tamper-evident AuditEvent (M001) // via foundation.auditEvent with the shared db client. // // Activity (DoD 14): user-facing timeline entries are logged AGAINST the linked // entity (the file itself is not an Activity subject in v1 — see decision 4 in // the build plan). Only FileEntityType values that map to an ActivitySubjectType // (contact/company/lead/deal/project/task) produce an activity; others are // skipped (no shared @hivegrid/activity edit). import type { JsonValue } from "@toolctrl/foundation"; import { createAuditEvent } from "@toolctrl/db/audit-events.server"; import { logActivity, type ActivitySubjectType, type ActivityType } from "@hivegrid/activity"; import type { FilesRouteContext } from "./fileAccess.server"; import type { FileEntityType } from "./fileService.server"; export type FileAuditAction = "file.uploaded" | "file.linked" | "file.unlinked" | "file.archived" | "file.reactivated" | "file.deleted" | "file.updated"; export async function auditFileEvent(input: { ctx: Pick; action: FileAuditAction; fileId: string; metadata?: Record; }): Promise { try { // createAuditEvent (M001) writes the tamper-evident, hash-chained AuditEvent // and defaults its client to the shared db singleton. await createAuditEvent({ tenantId: input.ctx.tenantId, actorId: input.ctx.userId, actorType: "user", action: input.action, targetType: "file", targetId: input.fileId, metadata: input.metadata ?? null, }); } catch { // Audit must never break the user action; the failure is logged upstream. } } const ENTITY_TO_ACTIVITY_SUBJECT: Partial> = { contact: "contact", company: "company", lead: "lead", deal: "deal", project: "project", task: "task", }; /** * Log a file-related activity against the linked entity's timeline. No-op when * the entity type has no Activity subject mapping (e.g. user/form_submission). */ export async function logFileActivity(input: { ctx: Pick; entityType: FileEntityType; entityId: string; activityType: Extract; summaryText: string; fileId: string; }): Promise { const subjectType = ENTITY_TO_ACTIVITY_SUBJECT[input.entityType]; if (!subjectType) { return; } try { // logActivity defaults its write client to the shared db singleton. await logActivity({ tenantId: input.ctx.tenantId, actorUserId: input.ctx.userId, actorKind: "user", activityType: input.activityType, subjectType, subjectId: input.entityId, summaryText: input.summaryText, // The file is not an Activity subject in v1; carry its id in metadata. metadata: { fileId: input.fileId }, }); } catch { // Activity logging is best-effort; never break the user action. } }