feat(iios): DSR right-to-erasure (redact-in-place) + compliance hold (P9)

Adds IiosComplianceHold + IiosSourceHandle.redactedAt and a DsrService whose
eraseSubject redacts the caller's PII in place (message bodies, identity
display, raw payloads, support/inbox/transcript text) while preserving
externalId (source preservation), structure, and the audit trail. An active
compliance hold blocks erasure until released; every op is audited and
tenant-fenced. Erasure is idempotent (KG-03).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 03:24:43 +05:30
parent 67da42a103
commit 611ca74cea
9 changed files with 322 additions and 1 deletions
@@ -0,0 +1,21 @@
-- AlterTable
ALTER TABLE "IiosSourceHandle" ADD COLUMN "redactedAt" TIMESTAMP(3);
-- CreateTable
CREATE TABLE "IiosComplianceHold" (
"id" TEXT NOT NULL,
"scopeId" TEXT NOT NULL,
"targetType" TEXT NOT NULL,
"targetId" TEXT NOT NULL,
"holdReason" TEXT NOT NULL,
"appliedByActorId" TEXT,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMP(3),
"releasedAt" TIMESTAMP(3),
CONSTRAINT "IiosComplianceHold_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "IiosComplianceHold_scopeId_targetType_targetId_status_idx" ON "IiosComplianceHold"("scopeId", "targetType", "targetId", "status");
@@ -326,6 +326,7 @@ model IiosSourceHandle {
firstSeenAt DateTime @default(now())
lastSeenAt DateTime @default(now())
metadata Json?
redactedAt DateTime? // set when this subject has been erased (DSR, P9)
actors IiosActorRef[]
@@ -492,6 +493,23 @@ model IiosProcessedEvent {
@@id([consumerName, eventId])
}
/// Legal/compliance hold (P9 DSR). An ACTIVE, unexpired hold over a target BLOCKS
/// erasure/retention deletion until an authorised release. Never auto-released.
model IiosComplianceHold {
id String @id @default(cuid())
scopeId String
targetType String // 'data_subject' | 'message' | 'media'
targetId String
holdReason String
appliedByActorId String?
status String @default("ACTIVE") // ACTIVE | RELEASED
createdAt DateTime @default(now())
expiresAt DateTime?
releasedAt DateTime?
@@index([scopeId, targetType, targetId, status])
}
/// Projection replay cursor (P9, KG-06). Ordered high-water-mark + rolling checksum per
/// (projection, topic, partition) proving replay reproduces the same projection outcome.
model IiosProjectionCursor {