Files
agentic-dev/packages/core-cms/src/payload.config.ts
Danijel Martinek a2be5d5488 feat(core-cms): register audit-logs + wire GDPR audit erasure
The audit-logs collection was never registered (record() would throw),
bindAudit/createAuditErasureHook were unused, and DSR cascade-hard never
touched the audit trail (audit finding A6). core-cms now registers the
collection and wires a req-scoped afterDelete erasure hook on users;
bindAllProduction binds the audit log into consent/DSR; cascade-hard
pseudonymizes the subject's audit entries; the action select accepts
the full AuditAction enum so consent/DSR entries pass validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:25:46 +02:00

71 lines
2.4 KiB
TypeScript

import { buildConfig } from "payload";
import { postgresAdapter } from "@payloadcms/db-postgres";
import { lexicalEditor } from "@payloadcms/richtext-lexical";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
withRetentionTombstone,
buildRetentionPurgeTask,
} from "@repo/core-shared/payload";
import {
auditLogsCollection,
createReqScopedAuditErasureHook,
} from "@repo/core-audit";
import { users as usersBase } from "@repo/auth/cms";
import { articles } from "@repo/blog/cms";
import { media } from "@repo/media/cms";
import { pages, siteSettings } from "@repo/marketing-pages/cms";
import { header } from "@repo/navigation/cms";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
// GDPR audit erasure (audit finding A6): when a users row is hard-deleted
// (admin expunge, DSR cascade-hard, retention purge), pseudonymize that
// subject's audit-log entries so the trail keeps its shape without PII linkage.
const users = {
...usersBase,
hooks: {
...usersBase.hooks,
afterDelete: [
...(usersBase.hooks?.afterDelete ?? []),
createReqScopedAuditErasureHook(),
],
},
};
// Collections declaring custom.retention.postDeletion get the soft-delete
// tombstone field (`deletedAt`) so the DSR soft delete can stamp rows and the
// retention purge job can grace-purge them (audit finding A2).
const collections = [
...[users, articles, pages, media].map(withRetentionTombstone),
// Local audit sink (A6) — required for PayloadAuditLog.record() to work.
auditLogsCollection,
];
export default buildConfig({
editor: lexicalEditor(),
collections,
globals: [siteSettings, header],
secret: process.env.PAYLOAD_SECRET || "default-secret-change-me",
db: postgresAdapter({
pool: {
connectionString:
process.env.DATABASE_URL ||
"postgresql://postgres:postgres@localhost:5433/template",
},
}),
jobs: {
// Task definitions for the retention purge (audit finding A3):
// registerRetentionPurgeJobs (called from bindAllProduction) enqueues
// `retention-purge--<slug>` jobs; these definitions let Payload run them.
tasks: collections
.filter((c) => Boolean(c.custom?.retention?.purgeSchedule))
.map((c) => buildRetentionPurgeTask(c.slug)) as never,
},
typescript: {
outputFile: path.resolve(dirname, "generated-types.ts"),
},
});