Files
agentic-dev/packages/core-dsr/src/payload-data-rectify.ts

106 lines
3.0 KiB
TypeScript

import { getPayload as _getPayload } from "payload";
import type { SanitizedConfig } from "payload";
import type { AuditLogProtocol } from "@repo/core-shared/di";
import type { IDataRectify } from "./data-rectify.interface";
import type { DsrCollectionCustom } from "./dsr-collection-custom";
type PayloadDoc = Record<string, unknown>;
type PayloadAPI = {
find(args: {
collection: string;
where: Record<string, unknown>;
overrideAccess: true;
limit: number;
}): Promise<{ docs: PayloadDoc[] }>;
update(args: {
collection: string;
id: string;
data: Record<string, unknown>;
overrideAccess: true;
}): Promise<PayloadDoc>;
};
type GetPayload = (args: { config: SanitizedConfig }) => Promise<PayloadAPI>;
/**
* Payload-backed IDataRectify. Verifies the target field is PII-tagged before
* updating and emits a RESTRICT audit entry with `reason: "art-16-request"` as
* the tamper-evident record of the Art. 16 correction.
*/
export class PayloadDataRectify implements IDataRectify {
constructor(
private readonly config: SanitizedConfig,
private readonly auditLog: AuditLogProtocol,
private readonly getPayloadFn: GetPayload = _getPayload as unknown as GetPayload,
) {}
async updateSubjectField(
subjectId: string,
collectionSlug: string,
field: string,
value: unknown,
): Promise<void> {
const collectionCfg = this.config.collections.find(
(c) => c.slug === collectionSlug,
);
if (!collectionCfg) {
throw new Error(`Collection "${collectionSlug}" not found in config`);
}
const custom = (collectionCfg.custom ?? {}) as DsrCollectionCustom;
if (!custom.subject) {
throw new Error(
`Collection "${collectionSlug}" has no DSR subject linkage`,
);
}
if (!custom.pii?.[field]) {
throw new Error(
`Field "${field}" in "${collectionSlug}" is not tagged as PII`,
);
}
const { field: subjectField } = custom.subject;
const where =
subjectField === "id"
? { id: { equals: subjectId } }
: { [subjectField]: { equals: subjectId } };
const payload = await this.getPayloadFn({ config: this.config });
const result = await payload.find({
collection: collectionSlug,
where,
overrideAccess: true,
limit: 1000,
});
for (const doc of result.docs) {
await payload.update({
collection: collectionSlug,
id: String(doc["id"]),
data: { [field]: value },
overrideAccess: true,
});
}
await this.auditLog.record({
actorId: subjectId,
actorType: "user",
actorRoles: [],
action: "RESTRICT",
resource: { type: collectionSlug },
changedFields: [field],
at: new Date(),
scope: {
feature: "core-dsr",
environment: process.env["NODE_ENV"] ?? "development",
tenant: "default",
},
reason: "art-16-request",
from: { ipTruncated: "system", userAgent: "system" },
containsPii: false,
outcome: "success",
});
}
}