Ports the upstream compliance-core audit fixes onto the kept core-dsr, core-consent, core-audit, core-cms and core-shared packages (pristine template state here, so taken to the fixed end-state): - core-dsr: scope DSR operations to the caller's own subject (A11); include the subject's audit trail in exports; resolve the per-request binding from ctx instead of a throwing singleton proxy. - core-consent: build the consent router from the shared superjson transformer (A10); merge per-category on persist instead of replacing; validate migrated categories against an allow-list. - core-audit: keyed 128-bit pseudonyms + salted DSR certificate; add the audit-logs collection and the req-scoped GDPR audit-erasure afterDelete hook (A6). - core-shared: grace-purge soft-deleted rows via a retention-purge task + tombstone field and boot registration (A2/A3); add the require-authenticated tRPC helper; derive clientIp + resolve the session user in createTrpcContext (B2/A11). - core-cms: register audit-logs, wire the audit-erasure hook and retention-purge tasks; adapted to our collection set (users, workspaces). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
74 lines
2.8 KiB
TypeScript
74 lines
2.8 KiB
TypeScript
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;
|
|
// `slug as string`: apps with generated CollectionSlug types narrow slug
|
|
// to their registered union, which need not include "audit-logs".
|
|
const hasAuditCollection = payload.config.collections?.some(
|
|
(c) => (c.slug as string) === "audit-logs",
|
|
);
|
|
if (!hasAuditCollection) return;
|
|
const auditLog = new PayloadAuditLog(
|
|
payload.config,
|
|
async () => payload as never,
|
|
);
|
|
await auditLog.eraseSubject(String(doc.id), mode);
|
|
};
|
|
}
|