Files
agentic-dev/packages/core-audit/src/audit-logs-collection.ts
2026-05-11 16:12:48 +02:00

83 lines
2.6 KiB
TypeScript

import type { CollectionConfig } from "payload";
/**
* Append-only Payload collection for audit entries. Mounted by core-cms
* when this package is scaffolded (manual wiring step printed by generator).
*
* Access rules:
* - read: admins only
* - create: any authenticated context (filtered upstream by PayloadAuditLog)
* - update: NEVER (compliance requires append-only)
* - delete: admins only (used by the GDPR erasure path with overrideAccess)
*
* The `update: () => false` rule is the compliance backbone. The erasure
* path uses `overrideAccess: true` to bypass for pseudonymization — that's
* Payload's documented escape hatch for privileged operations.
*/
export const auditLogsCollection: CollectionConfig = {
slug: "audit-logs",
access: {
read: ({ req }) => {
const user = req.user as { roles?: string[] } | null | undefined;
return Array.isArray(user?.roles) && user.roles.includes("admin");
},
create: () => true,
update: () => false,
delete: ({ req }) => {
const user = req.user as { roles?: string[] } | null | undefined;
return Array.isArray(user?.roles) && user.roles.includes("admin");
},
},
timestamps: true,
fields: [
// WHO
{ name: "actorId", type: "text", required: true, index: true },
{
name: "actorType",
type: "select",
options: ["user", "system", "service"],
required: true,
},
{ name: "actorRoles", type: "json", required: true },
// WHAT
{
name: "action",
type: "select",
options: ["VIEW", "CREATE", "UPDATE", "DELETE", "EXPORT", "PERMISSION_CHANGE"],
required: true,
index: true,
},
{ name: "resourceType", type: "text", required: true, index: true },
{ name: "resourceId", type: "text" },
{ name: "changedFields", type: "json" },
// SCOPE
{ name: "scopeFeature", type: "text", required: true, index: true },
{ name: "scopeEnvironment", type: "text", required: true },
{ name: "scopeTenant", type: "text", required: true, index: true },
// WHY
{ name: "reason", type: "text" },
{ name: "correlationId", type: "text", index: true },
{ name: "requestId", type: "text" },
// FROM
{ name: "ipTruncated", type: "text", required: true },
{ name: "userAgent", type: "text", required: true },
// PII
{ name: "containsPii", type: "checkbox", required: true },
{ name: "piiCategories", type: "json" },
// OUTCOME
{
name: "outcome",
type: "select",
options: ["success", "denied", "error"],
required: true,
},
{ name: "errorCode", type: "text" },
],
};