Files
Danijel Martinek a3505f2e69 docs(compliance): add DSR guide, consent guide, subject-linkage example, glossary terms
- docs/guides/dsr.md: GDPR Art. 15/16/17/18/20 interface mapping, tRPC
  router wiring, multi-subject handling, soft vs cascade-hard semantics,
  DeletionCertificate format and storage requirements
- docs/guides/consent.md: requiresConsent manifest field, withConsent DI
  wiring, runtime isGranted pattern, IConsent audit trail, anonymous→
  authenticated migration, cookie _v versioning, SSR-safe banner loading,
  CNIL/EDPB equal-prominence requirement
- docs/compliance/subject-linkage.example.md: SubjectLink kind discriminator
  with worked support-ticket example (owner submitter + reference assignee)
- docs/glossary.md: SubjectLink, DeletionCertificate, UserConsentState,
  ConsentChecked entries; Manifest definition updated with requiresConsent
- CLAUDE.md: lint comment 8→12 conformance rules; conformance section notes
  requiresConsent; brand composition order updated to full 5-wrapper chain
- docs/guides/conformance-quickref.md: requiresConsent field added to
  manifest table; component-must-have-story, component-must-have-test,
  atomic-tier-import-direction added to ESLint rules table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 22:07:50 +00:00

190 lines
8.4 KiB
Markdown

# Data Subject Rights (DSR) guide
Consumer-facing reference for the `@repo/core-dsr` optional core package. Covers the four GDPR interfaces, tRPC procedure wiring, multi-subject collection handling, deletion semantics, `DeletionCertificate` format, and per-article compliance notes.
**Prerequisite:** scaffold the package if it isn't already present:
```bash
pnpm turbo gen core-package dsr
```
---
## Interfaces and GDPR article mapping
`@repo/core-dsr` exposes four vendor-neutral interfaces:
| Interface | tRPC procedure | GDPR article(s) | Mutation? |
| ------------------------ | -------------- | ----------------------------------------- | ---------- |
| `IDataExport` | `dsr.export` | Art. 15 (access) + Art. 20 (portability) | No (query) |
| `IDataDelete` | `dsr.delete` | Art. 17 (erasure / right to be forgotten) | Yes |
| `IDataRectify` | `dsr.rectify` | Art. 16 (rectification) | Yes |
| `IProcessingRestriction` | `dsr.restrict` | Art. 18 (restriction of processing) | Yes |
---
## Wiring the DSR router
`core-dsr` ships a pre-built tRPC router. Mount it once in your app router:
```ts
// apps/web-next/src/server/router.ts
import { createDsrRouter } from "@repo/core-dsr";
import { bindProductionDsr } from "@repo/core-dsr/di/bind-production";
const dsrBinding = bindProductionDsr({ config: payloadConfig, auditLog });
export const appRouter = t.router({
// ... other feature routers
dsr: createDsrRouter(dsrBinding),
});
```
All four procedures require an authenticated user in `ctx.user`. `cascade-hard` deletion additionally requires `ctx.user.roles` to include `"admin"`.
### Input schemas
| Procedure | Input fields |
| -------------- | ---------------------------------------------------------------------------- |
| `dsr.export` | `subjectId: string`, `format: "json" \| "json-ld"` |
| `dsr.delete` | `subjectId: string`, `mode: "soft" \| "cascade-hard"` |
| `dsr.rectify` | `subjectId: string`, `collection: string`, `field: string`, `value: unknown` |
| `dsr.restrict` | `subjectId: string`, `granted: boolean` |
---
## Multi-subject handling
A Payload collection can reference **multiple data subjects** — for example, a support ticket has both a submitter and an assignee. Declare subject relationships via `custom.subject` on the collection config:
```ts
// packages/<feature>/src/integrations/cms/support-tickets.collection.ts
{
slug: "support-tickets",
custom: {
subject: [
{ field: "submittedBy", kind: "self", target: "users" },
{ field: "assignedTo", kind: "reference", target: "users", role: "assignee" },
],
},
fields: [
{ name: "submittedBy", type: "relationship", relationTo: "users" },
{ name: "assignedTo", type: "relationship", relationTo: "users" },
],
}
```
The DSR cascade walks each `SubjectLink` at runtime to determine scope:
| `kind` | Meaning | Export behaviour | Delete behaviour |
| ------------- | ------------------------------------------------------------------------------------------ | ------------------------------------- | ---------------------------------- |
| `"self"` | The subject **is** this row (e.g., the Users row itself) | Full row in `asSelf` | Row deleted / pseudonymized |
| `"owner"` | The subject **created or owns** this row (e.g., posts authored by the user) | Full row in `asSelf` | Row deleted / pseudonymized |
| `"reference"` | The subject is **referenced** in this row but does not own it (e.g., assignee on a ticket) | Row ID + link coords in `asReference` | Linked field NULLed; row preserved |
See `docs/compliance/subject-linkage.example.md` for a full annotated example.
---
## Deletion modes
### `soft` (default, no admin role required)
Redacts or pseudonymizes PII fields in rows the subject owns (`kind: "self" | "owner"`) while preserving foreign-key integrity. Reference rows (`kind: "reference"`) have the linking field NULLed. The row structure remains intact — useful for preserving order history, audit trails, and referential integrity.
### `cascade-hard` (admin role required)
Hard-deletes all rows the subject owns (where `postDeletion.action === "hard-delete"` in the collection's retention config), then NULLs reference fields. Use only when the subject's right to erasure overrides your referential-integrity requirements or when retention policy mandates hard deletion.
Retention-policy overrides apply: a collection with `postDeletion.action = "pseudonymize"` will pseudonymize rather than hard-delete even in `cascade-hard` mode.
---
## `DeletionCertificate` format
`IDataDelete.deleteSubjectData(subjectId, mode)` resolves to a `DeletionCertificate`:
```ts
type DeletionCertificate = {
subjectId: string; // or "erased-{hash}" if the ID itself was purged
mode: "soft" | "cascade-hard";
timestamp: string; // ISO 8601
reason: "art-17-request" | "admin-expunge" | "retention-policy";
affected: Array<{
collection: string;
rowsAffected: number;
action: "deleted" | "redacted" | "pseudonymized";
fields?: string[]; // PII field names NULLed when action === "redacted"
}>;
auditEntryId: string; // links to the immutable audit log entry
};
```
**Storage requirements:** persist the `DeletionCertificate` permanently — it is your Art. 17 compliance evidence for regulatory inspection. The `auditEntryId` forms a tamper-evident chain back to the `core-audit` log. Never mutate a certificate after creation.
---
## `UserDataBundle` format (export)
`IDataExport.exportSubjectData(subjectId, format)` resolves to a `UserDataBundle`:
```ts
type UserDataBundle = {
subjectId: string;
exportedAt: string; // ISO 8601
format: "json" | "json-ld";
data: Record<
string,
{
// keyed by collection slug
asSelf?: Array<Record<string, unknown>>; // owned rows (PII-filtered)
asReference?: Array<{
rowId: string;
linkedField: string;
linkedThrough: string;
}>;
}
>;
auditLog?: AuditEntry[]; // subject-scoped audit history
"@context"?: string | Record<string, unknown>; // JSON-LD only
};
```
Only fields marked `exportable: true` in their `custom.pii` block are included in `asSelf` rows.
---
## Compliance notes
### Art. 15 — Right of access
`dsr.export` with `format: "json"` satisfies the right of access. Return the `UserDataBundle` directly in the API response or as a downloadable JSON file. Response time: ≤ 30 days under GDPR.
### Art. 16 — Right to rectification
`dsr.rectify` targets a single field. For bulk updates, call it once per field. The implementation calls `IDataRectify.rectifySubjectData(subjectId, collection, field, value)` and records an audit entry.
### Art. 17 — Right to erasure
`dsr.delete` with `mode: "soft"` is the default erasure path. Use `mode: "cascade-hard"` only when data minimisation obligations outweigh referential integrity needs. Both paths produce a `DeletionCertificate`.
**Exemptions:** Art. 17(3) allows retention for legal obligations (e.g., invoices, tax records). Implement exemptions via a `postDeletion.action = "pseudonymize"` retention override on the affected collection — the DSR cascade respects this automatically.
### Art. 18 — Restriction of processing
`dsr.restrict` with `granted: true` flags the subject's data for restricted processing. Your application code must check `IProcessingRestriction.isRestricted(subjectId)` before performing processing operations on restricted subjects.
### Art. 20 — Right to data portability
`dsr.export` with `format: "json-ld"` produces a machine-readable JSON-LD export suitable for portability. The `@context` field is populated with the schema URI for downstream consumption.
---
## Cross-references
- Subject linkage patterns: `docs/compliance/subject-linkage.example.md`
- PII field tagging: `docs/compliance/README.md` → "Annotating PII fields"
- Retention policy: `docs/compliance/retention-policy.example.yml`
- Audit log: `docs/guides/audit-and-compliance.md`
- Glossary: `SubjectLink`, `DeletionCertificate` in `docs/glossary.md`