feat(core-shared): grace-purge soft-deleted rows + boot registration

The retention purge job gated its whole body on activeRetention while
every collection declares only postDeletion, and no app ever called
registerRetentionPurgeJobs — retention was dead end to end (audit
findings A2 + A3). The DSR soft delete now stamps a deletedAt tombstone
on postDeletion collections (kept distinct from processingRestrictedAt
so an Art. 18 restriction never feeds the purge), the job grace-purges
tombstoned rows past postDeletion.duration with the declared action,
core-cms injects the tombstone field + Payload task definitions, and
bindAllProduction enqueues the first purge cycle at boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 18:02:43 +02:00
parent d09b3e2cdd
commit 413ac0273c
15 changed files with 755 additions and 54 deletions

View File

@@ -21,6 +21,10 @@ import {
NoopRateLimit,
type RateLimitBudget,
} from "@repo/core-shared/rate-limit";
import {
registerRetentionPurgeJobs,
type GetPayloadFn,
} from "@repo/core-shared/payload";
import { bindAudit, type IAuditLog } from "@repo/core-audit";
import {
bindProductionConsent,
@@ -192,6 +196,17 @@ export async function bindAllProduction(): Promise<void> {
bindProductionMarketingPages(ctx);
bindProductionNavigation(ctx);
bindProductionMedia(ctx);
// Kick off the retention purge cycle (audit finding A3): enqueue the first
// `retention-purge--<slug>` job for every collection declaring a
// custom.retention.purgeSchedule. The task definitions live in the Payload
// config (core-cms jobs.tasks); each run re-enqueues the next cycle.
await registerRetentionPurgeJobs({
queue,
config: resolvedConfig,
getPayload: getPayload as unknown as GetPayloadFn,
auditLog,
});
}
/**