import type { CollectionAfterDeleteHook } from "payload"; import type { IAuditLog } from "../audit-log.interface"; import { PayloadAuditLog } from "../payload-audit-log"; export type AuditErasureHookOpts = { /** The audit log impl that will perform the erasure. */ auditLog: IAuditLog; /** * Erasure mode. Defaults to "pseudonymize" — the softer option that * retains the audit trail shape while removing PII linkage. Use * "delete" only when the data-subject specifically requests hard removal. */ mode?: "pseudonymize" | "delete"; }; /** * Payload `afterDelete` hook factory for GDPR erasure. * * Wire this on any collection whose `id` doubles as an audit subject * (e.g., the users collection). When Payload deletes a document, the * hook calls `auditLog.eraseSubject(String(doc.id), mode)`, removing * or pseudonymizing all audit entries recorded for that actor. * * The hook has no schema-specific knowledge — it works on any collection * that stores the subject identifier as its document `id`. * * Non-string, non-numeric ids are silently skipped (safe guard against * undefined/null that Payload may produce in edge cases). */ export function createAuditErasureHook( opts: AuditErasureHookOpts, ): CollectionAfterDeleteHook { const mode = opts.mode ?? "pseudonymize"; return async ({ doc }) => { if (typeof doc.id === "string" || typeof doc.id === "number") { await opts.auditLog.eraseSubject(String(doc.id), mode); } }; } export type ReqScopedAuditErasureHookOpts = { /** Erasure mode — see AuditErasureHookOpts. Defaults to "pseudonymize". */ mode?: "pseudonymize" | "delete"; }; /** * Variant of `createAuditErasureHook` for config-composition time (audit * finding A6): a Payload collection config is built before any `IAuditLog` * can exist (binding the audit log needs the built config), so this hook * constructs a `PayloadAuditLog` lazily from the running instance on * `req.payload` when the delete fires. No-ops when the `audit-logs` * collection is not registered. */ export function createReqScopedAuditErasureHook( opts: ReqScopedAuditErasureHookOpts = {}, ): CollectionAfterDeleteHook { const mode = opts.mode ?? "pseudonymize"; return async ({ doc, req }) => { if (typeof doc.id !== "string" && typeof doc.id !== "number") return; const payload = req.payload; const hasAuditCollection = payload.config.collections?.some( (c) => c.slug === "audit-logs", ); if (!hasAuditCollection) return; const auditLog = new PayloadAuditLog( payload.config, async () => payload as never, ); await auditLog.eraseSubject(String(doc.id), mode); }; }