@cosmicdrift/kumiko-bundled-features 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/package.json +22 -13
  3. package/src/auth-email-password/auth-user-row.ts +6 -0
  4. package/src/auth-email-password/constants.ts +11 -0
  5. package/src/auth-email-password/handlers/change-password.write.ts +1 -1
  6. package/src/auth-email-password/handlers/confirm-token-flow.ts +1 -1
  7. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +7 -7
  8. package/src/auth-email-password/handlers/invite-accept.write.ts +7 -6
  9. package/src/auth-email-password/handlers/invite-create.write.ts +3 -3
  10. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +4 -4
  11. package/src/auth-email-password/handlers/login.write.ts +32 -2
  12. package/src/auth-email-password/handlers/logout.write.ts +2 -2
  13. package/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
  14. package/src/auth-email-password/i18n.ts +4 -0
  15. package/src/auth-email-password/web/auth-client.ts +1 -1
  16. package/src/billing-foundation/events.ts +1 -1
  17. package/src/billing-foundation/feature.ts +44 -47
  18. package/src/billing-foundation/handlers/create-portal-session.write.ts +3 -3
  19. package/src/billing-foundation/handlers/process-event.write.ts +3 -3
  20. package/src/billing-foundation/projection.ts +1 -1
  21. package/src/billing-foundation/webhook-handler.ts +1 -1
  22. package/src/cap-counter/constants.ts +1 -1
  23. package/src/cap-counter/enforce-cap.ts +1 -1
  24. package/src/cap-counter/feature.ts +3 -7
  25. package/src/cap-counter/handlers/get-counter.query.ts +1 -1
  26. package/src/cap-counter/handlers/increment-rolling.write.ts +2 -2
  27. package/src/cap-counter/handlers/increment.write.ts +3 -3
  28. package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
  29. package/src/channel-email/email-channel.ts +1 -1
  30. package/src/channel-email/types.ts +1 -1
  31. package/src/compliance-profiles/README.md +88 -0
  32. package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +308 -0
  33. package/src/compliance-profiles/__tests__/seeding.integration.ts +93 -0
  34. package/src/compliance-profiles/feature.ts +51 -0
  35. package/src/compliance-profiles/handlers/for-tenant.query.ts +64 -0
  36. package/src/compliance-profiles/handlers/list-profiles.query.ts +44 -0
  37. package/src/compliance-profiles/handlers/needs-profile.query.ts +56 -0
  38. package/src/compliance-profiles/handlers/set-profile.write.ts +144 -0
  39. package/src/compliance-profiles/handlers/sub-processors.query.ts +43 -0
  40. package/src/compliance-profiles/index.ts +6 -0
  41. package/src/compliance-profiles/resolve-for-tenant.ts +63 -0
  42. package/src/compliance-profiles/schema/profile-selection.ts +52 -0
  43. package/src/compliance-profiles/seeding.ts +96 -0
  44. package/src/config/resolver.ts +1 -1
  45. package/src/data-retention/__tests__/data-retention.integration.ts +49 -0
  46. package/src/data-retention/__tests__/keep-for.test.ts +77 -0
  47. package/src/data-retention/__tests__/override-schema.test.ts +96 -0
  48. package/src/data-retention/__tests__/policy-for.integration.ts +172 -0
  49. package/src/data-retention/__tests__/resolver.test.ts +201 -0
  50. package/src/data-retention/_internal/parse-override.ts +34 -0
  51. package/src/data-retention/feature.ts +57 -0
  52. package/src/data-retention/handlers/policy-for.query.ts +57 -0
  53. package/src/data-retention/index.ts +18 -0
  54. package/src/data-retention/keep-for.ts +75 -0
  55. package/src/data-retention/override-schema.ts +37 -0
  56. package/src/data-retention/presets.ts +72 -0
  57. package/src/data-retention/resolve-for-tenant.ts +50 -0
  58. package/src/data-retention/resolver.ts +107 -0
  59. package/src/data-retention/schema/tenant-retention-override.ts +47 -0
  60. package/src/delivery/feature.ts +1 -1
  61. package/src/delivery/testing.ts +1 -2
  62. package/src/delivery/upsert-preference.ts +1 -1
  63. package/src/feature-toggles/feature.ts +1 -1
  64. package/src/feature-toggles/handlers/list.query.ts +1 -1
  65. package/src/feature-toggles/handlers/registered.query.ts +9 -2
  66. package/src/feature-toggles/handlers/set.write.ts +3 -3
  67. package/src/file-foundation/feature.ts +44 -4
  68. package/src/file-foundation/index.ts +1 -0
  69. package/src/file-provider-inmemory/feature.ts +6 -3
  70. package/src/file-provider-s3/feature.ts +10 -12
  71. package/src/files/README.md +50 -0
  72. package/src/files/__tests__/files.integration.ts +157 -0
  73. package/src/files/feature.ts +34 -0
  74. package/src/files/index.ts +1 -0
  75. package/src/files/schema/file-ref.ts +58 -0
  76. package/src/files-provider-s3/s3-provider.ts +90 -1
  77. package/src/jobs/handlers/list.query.ts +3 -3
  78. package/src/jobs/handlers/trigger.write.ts +1 -1
  79. package/src/legal-pages/constants.ts +1 -0
  80. package/src/legal-pages/web/client-plugin.ts +42 -0
  81. package/src/legal-pages/web/index.ts +4 -0
  82. package/src/mail-foundation/feature.ts +1 -1
  83. package/src/mail-transport-smtp/feature.ts +2 -2
  84. package/src/renderer-simple/simple-renderer.ts +1 -1
  85. package/src/secrets/__tests__/require-secrets-context.test.ts +81 -0
  86. package/src/secrets/feature.ts +10 -6
  87. package/src/secrets/handlers/rotate.job.ts +2 -2
  88. package/src/sessions/constants.ts +4 -0
  89. package/src/sessions/feature.ts +3 -0
  90. package/src/sessions/handlers/cleanup.job.ts +2 -2
  91. package/src/sessions/handlers/revoke-all-for-user.write.ts +42 -0
  92. package/src/step-dispatcher/feature.ts +62 -0
  93. package/src/step-dispatcher/index.ts +16 -0
  94. package/src/step-dispatcher/mail-runner.ts +32 -0
  95. package/src/step-dispatcher/webhook-runner.ts +67 -0
  96. package/src/subscription-mollie/plugin-methods.ts +1 -1
  97. package/src/subscription-mollie/verify-webhook.ts +9 -5
  98. package/src/subscription-stripe/verify-webhook.ts +3 -3
  99. package/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
  100. package/src/tenant/handlers/cancel-invitation.write.ts +1 -1
  101. package/src/tenant/handlers/remove-member.write.ts +1 -1
  102. package/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
  103. package/src/tenant/handlers/update-member-roles.write.ts +3 -3
  104. package/src/text-content/constants.ts +2 -0
  105. package/src/text-content/feature.ts +20 -4
  106. package/src/text-content/handlers/by-tenant.query.ts +56 -0
  107. package/src/text-content/handlers/set.write.ts +1 -1
  108. package/src/text-content/web/client-plugin.ts +113 -0
  109. package/src/text-content/web/index.ts +8 -0
  110. package/src/tier-engine/__tests__/auto-default-tier.integration.ts +118 -0
  111. package/src/tier-engine/feature.ts +23 -13
  112. package/src/user/__tests__/user-status.test.ts +39 -0
  113. package/src/user/handlers/find-for-auth.query.ts +1 -1
  114. package/src/user/index.ts +11 -1
  115. package/src/user/schema/user.ts +76 -0
  116. package/src/user/seeding.ts +2 -2
  117. package/src/user-data-rights/COMPLIANCE.md +182 -0
  118. package/src/user-data-rights/README.md +109 -0
  119. package/src/user-data-rights/__tests__/audit-log.integration.ts +199 -0
  120. package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +349 -0
  121. package/src/user-data-rights/__tests__/download.integration.ts +565 -0
  122. package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +244 -0
  123. package/src/user-data-rights/__tests__/export-job-schema.test.ts +163 -0
  124. package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +30 -0
  125. package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +370 -0
  126. package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +179 -0
  127. package/src/user-data-rights/__tests__/request-export.integration.ts +269 -0
  128. package/src/user-data-rights/__tests__/restriction-flow.integration.ts +309 -0
  129. package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +1124 -0
  130. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +703 -0
  131. package/src/user-data-rights/__tests__/run-user-export.integration.ts +291 -0
  132. package/src/user-data-rights/__tests__/token-helpers.test.ts +63 -0
  133. package/src/user-data-rights/__tests__/user-data-rights.integration.ts +57 -0
  134. package/src/user-data-rights/__tests__/zip-path.test.ts +119 -0
  135. package/src/user-data-rights/audit-download.ts +125 -0
  136. package/src/user-data-rights/feature.ts +310 -0
  137. package/src/user-data-rights/handlers/cancel-deletion.write.ts +84 -0
  138. package/src/user-data-rights/handlers/download-by-job.query.ts +206 -0
  139. package/src/user-data-rights/handlers/download-by-token.query.ts +255 -0
  140. package/src/user-data-rights/handlers/export-status.query.ts +76 -0
  141. package/src/user-data-rights/handlers/lift-restriction.write.ts +68 -0
  142. package/src/user-data-rights/handlers/list-download-attempts.query.ts +53 -0
  143. package/src/user-data-rights/handlers/my-audit-log.query.ts +63 -0
  144. package/src/user-data-rights/handlers/request-deletion.write.ts +123 -0
  145. package/src/user-data-rights/handlers/request-export.write.ts +155 -0
  146. package/src/user-data-rights/handlers/restrict-account.write.ts +81 -0
  147. package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +61 -0
  148. package/src/user-data-rights/i18n.ts +37 -0
  149. package/src/user-data-rights/index.ts +19 -0
  150. package/src/user-data-rights/run-export-jobs.ts +878 -0
  151. package/src/user-data-rights/run-forget-cleanup.ts +333 -0
  152. package/src/user-data-rights/run-user-export.ts +211 -0
  153. package/src/user-data-rights/schema/download-attempt.ts +37 -0
  154. package/src/user-data-rights/schema/download-token.ts +111 -0
  155. package/src/user-data-rights/schema/export-job.ts +166 -0
  156. package/src/user-data-rights/token-helpers.ts +67 -0
  157. package/src/user-data-rights/zip-path.ts +94 -0
  158. package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +337 -0
  159. package/src/user-data-rights-defaults/feature.ts +40 -0
  160. package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +109 -0
  161. package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +91 -0
  162. package/src/user-data-rights-defaults/index.ts +6 -0
@@ -0,0 +1,182 @@
1
+ # DSGVO Compliance — Operator-Guide
2
+
3
+ Dieses Dokument bündelt die **technischen Fakten** zur DSGVO-Pipeline
4
+ (Art. 15/17/18/20) als Grundlage für:
5
+
6
+ - **Verarbeitungsverzeichnis** (Art. 30) — was wird wo wie lange gespeichert
7
+ - **Datenschutzerklärung** — welche Endpoints decken welchen Artikel ab
8
+ - **AVV mit Sub-Processors** — welche TOM sind eingebaut
9
+ - **Operator-Runbook** — was triggert wann, wer kann was sehen
10
+
11
+ > Juristische Texte (AVV-Wortlaut, Datenschutzerklärung-Bausteine,
12
+ > Verarbeitungsverzeichnis-Strukturierung) gehören zu Marc + Anwalt —
13
+ > dieses Dokument liefert nur die technischen Fakten dafür.
14
+
15
+ ---
16
+
17
+ ## 1. Endpoint → Artikel-Mapping
18
+
19
+ | Artikel | Endpoint / Runner | Wer ruft auf | Was passiert |
20
+ |---------|-------------------|--------------|--------------|
21
+ | **Art. 15 (Auskunft, light)** | `user-data-rights:query:my-audit-log` | User | Liest eigene Framework-Events aus `kumiko_events` (account-weit über alle Memberships). Filterbar nach `eventType`, `aggregateType`, `from`/`to`. Domain-Entities ohne `ctx.appendEvent` erscheinen NICHT — die kommen im Export-Bundle (Art. 20). |
22
+ | **Art. 15 + 20 (Auskunft + Portabilität)** | `user-data-rights:write:request-export` | User | Async Job → ZIP mit user-Profil + fileRefs + alle EXT_USER_DATA-Provider-Daten (cross-tenant) + signed Magic-Link per Email. Idempotent: nur 1 active Job pro User (`ACTIVE_JOB_CONSTRAINT`). |
23
+ | **Art. 15 + 20** | `GET /user-export/by-token?token=…` | Anonym (Magic-Link-Pfad) | Token-Hash-Lookup → 302-Redirect auf signed Storage-URL. Multi-use innerhalb TTL. Audit: `lastUsedAt`, `lastUsedFromIp`, `lastUsedUserAgent`. |
24
+ | **Art. 15 + 20** | `GET /user-export/by-job/:jobId` | User (Session-Auth) | UI-Klick-Pfad. Cross-tenant-same-user: User in Tenant B kann Job aus Tenant A laden wenn er der Owner ist. |
25
+ | **Art. 17 (Löschung)** | `user-data-rights:write:request-deletion` | User | Soft-Delete: `status=DeletionRequested` + `gracePeriodEnd = now + profile.gracePeriod`. Cron `run-forget-cleanup` führt nach Grace die Anonymisierung durch. |
26
+ | **Art. 17** | `user-data-rights:write:cancel-deletion` | User | Während Grace: status zurück auf `Active`. |
27
+ | **Art. 17** | `run-forget-cleanup` (Cron) | System | Findet User mit `status=DeletionRequested AND gracePeriodEnd < now`. Pro User Sub-TX über alle EXT_USER_DATA-Provider mit Strategy aus `retention.policyFor`. user-Hook anonymisiert (PII raus, Sentinel-Email). |
28
+ | **Art. 18 (Restriction)** | `user-data-rights:write:restrict-account` | Admin / SystemAdmin | Status-Flip → Auth-Middleware-Guard blockt Logins. `sessions.revokeAllForUser` killt aktive Sessions. |
29
+ | **Art. 18** | `user-data-rights:write:lift-restriction` | Admin / SystemAdmin | Restriction aufheben. |
30
+ | **Operator (DPO)** | `user-data-rights:query:list-download-attempts` | Admin / SystemAdmin | Brute-Force-Detection: zeigt invalid Download-Versuche (notFound / expired / failed / signedUrlNotSupported) gefiltert nach Result, IP, Zeitraum. |
31
+
32
+ ---
33
+
34
+ ## 2. Speicherorte (Verarbeitungsverzeichnis Art. 30)
35
+
36
+ | Tabelle | Inhalt | Personenbezug | Retention | Zweck |
37
+ |---------|--------|---------------|-----------|-------|
38
+ | `read_users` | User-Profil (email, displayName, passwordHash, locale, status, gracePeriodEnd, roles) | direkt | per Domain (typ. blockDelete bei Aufbewahrungspflicht, sonst hardDelete via Forget-Pipeline) | Authentifizierung, Nutzer-Profil |
39
+ | `read_tenant_memberships` | User↔Tenant-Verknüpfung + Rollen | direkt (via userId) | per Tenant-Lifecycle | Mehrmandant-Zuordnung |
40
+ | `kumiko_events` | Event-Store (alle write-Events) | direkt (`createdBy = userId`) | per Domain-Policy via `data-retention` | Audit-Trail (Art. 15-Selbstauskunft Quelle), Event-Sourcing |
41
+ | `read_export_jobs` | Async Export-Status (queued/running/done/failed), userId, requestedAt, doneAt, storageKey | direkt (userId) | per `compliance-profiles` Profil-Default | Idempotenz + Status-Polling |
42
+ | `read_export_download_tokens` | Magic-Link-Hash (SHA-256), TTL, useCount, lastUsedAt, lastUsedFromIp, lastUsedUserAgent | indirekt (Token→Job→User) | `compliance-profiles.userRights.exportDownloadTtl` (default 7d) | Magic-Link-Auth + Multi-Use-Audit |
43
+ | `read_download_attempts` | Invalid Download-Versuche: result, via, tokenHash, ip, userAgent, attemptedAt | indirekt (IP) | **90d hardDelete** (Entity-Default, Disk-Bomb-Schutz) | DPO-Brute-Force-Detection |
44
+ | `read_tenant_compliance_profiles` | Per-Tenant Profile-Wahl + Override | nein | unbounded (Konfiguration) | Region-/Branchen-Defaults |
45
+ | `read_tenant_retention_overrides` | Per-Tenant Retention-Override pro Entity | nein | unbounded (Konfiguration) | Aufbewahrungspflicht-Edge-Cases |
46
+ | Storage-Provider (Local / S3) | Export-ZIPs + File-Binaries | indirekt (Inhalt) | Local: per `exportDownloadTtl` Cleanup. S3: lifecycle-Policy (App-Author-Verantwortung) | Daten-Export-Auslieferung |
47
+
48
+ ---
49
+
50
+ ## 3. Compliance-Profile
51
+
52
+ | Profil | gracePeriod | exportDownloadTtl | Stale-After | Sub-Processors |
53
+ |--------|-------------|-------------------|-------------|----------------|
54
+ | `eu-dsgvo` | 30d | 7d | 30d | per Tenant konfigurierbar |
55
+ | `swiss-dsg` | 30d | 7d | 30d | + EDÖB-Meldepfad |
56
+ | `de-hr-dsgvo-hgb` | 30d | 7d | 30d | + HR-Aufbewahrung 10y für HR-Entities (anonymize statt delete) |
57
+ | `minimal-no-region` | 30d | 7d | 30d | Migration-Edge-Case ohne Region |
58
+
59
+ Tenant kann via `compliance-profiles:write:set-profile` + Override (`override.userRights.gracePeriod={ days: N }` etc.) eigene Werte setzen.
60
+
61
+ ---
62
+
63
+ ## 4. Technische und Organisatorische Maßnahmen (TOM)
64
+
65
+ Eingebaute Schutzmaßnahmen — können 1:1 in den AVV-Anhang "Technische
66
+ und Organisatorische Maßnahmen" übernommen werden:
67
+
68
+ ### Vertraulichkeit / Zugriffskontrolle
69
+
70
+ - **Magic-Link-Token-Hashing**: Plain-Token landet NIE in DB / Event-Store. SHA-256-Hash via `crypto.subtle.digest` (Web-Crypto-API). Plain-Token kommt nur ephemeral via Email-Callback an die App-Author-Implementation.
71
+ - **Download-Token Multi-Use within TTL**: kein consume-on-use (Pattern Google Takeout) — User kann ZIP mehrfach laden, aber TTL gilt absolut.
72
+ - **Audit-Felder am Token**: useCount, lastUsedAt, lastUsedFromIp, lastUsedUserAgent. Operator sieht ob Token mehrfach verwendet wurde, von welcher IP.
73
+ - **Account-weite Auskunft via `ctx.db.raw`**: `my-audit-log` umgeht TenantDb-Auto-Filter explizit (Account-weite Sicht für Art. 15 ist Pflicht); Sicherung über hard-coded `WHERE createdBy = ctx.user.id` (kein userId-Parameter, kein Cross-User-Snooping möglich).
74
+ - **Cross-User-Schutz im Download-Pfad**: `download-by-job` checkt `jobRow.userId === session.user.id` — User kann nur eigene Jobs laden.
75
+ - **Restriction killt Sessions**: `restrict-account` triggert `sessions.revokeAllForUser` — restricted User kann existierende Tabs nicht weiternutzen.
76
+
77
+ ### Integrität
78
+
79
+ - **Event-Sourcing first-class**: alle DSGVO-Schreib-Operationen (request-deletion, restrict, lift, request-export) sind Events im `kumiko_events`-Store mit `version_conflict`-Schutz.
80
+ - **Forget-Strategy aus `data-retention`**: Cleanup-Runner konsultiert `retention.policyFor` pro Entity — `blockDelete` für gesetzliche Aufbewahrungspflicht (HR/HGB), `anonymize` als Alternative zu `hardDelete`.
81
+ - **Strategy-respect-Pattern in Default-Hooks**: user-Hook anonymisiert mit Sentinel-Pattern `deleted-<id>@anonymized.invalid` — Unique-Constraint + FK-Refs bleiben intakt.
82
+
83
+ ### Verfügbarkeit / Belastbarkeit
84
+
85
+ - **Best-Effort-Audit beim Download**: Audit-INSERT in `read_download_attempts` ist `try/catch` swallowed — Audit-Failure killt nicht den User-facing 4xx.
86
+ - **Idempotenz im Export-Job**: `ACTIVE_JOB_CONSTRAINT` (UNIQUE-Index auf `(userId, status='active')`) verhindert Doppel-Jobs. Worker-Crash → Job bleibt `running`, Recovery-Pfad via Job-Run-Tracking aus `jobs`-Feature.
87
+ - **Per-User Sub-TX im Forget-Runner**: Ein User-Hook-Throw rollback'd nur diesen User; andere User im Batch laufen weiter.
88
+ - **Best-Effort-Notification-Callbacks**: send-Throw für Job A killt nicht Batch B/C (Memory: Atom 5.fix3).
89
+
90
+ ### Brute-Force-Schutz
91
+
92
+ - **Edge-Rate-Limit auf Download-Endpoint**: `rateLimit: { per: "ip", limit: 30, windowSeconds: 60 }` für `download-by-token`.
93
+ - **Download-Attempt-Audit** mit 90d hardDelete: invalid Versuche werden persistiert für DPO-Detection, Tabelle ist begrenzt → kein Disk-Bomb durch Brute-Force.
94
+ - **Token-Hash-Suchraum**: 32-Byte-Random = 256 Bit, Brute-Force über Edge-Limit praktisch nicht möglich.
95
+
96
+ ### Zweckbindung / Datenminimierung
97
+
98
+ - **Export-Bundle Default-PII-Filter**: user-Hook entfernt `passwordHash`, `roles`, `status` aus dem Bundle (App-Author kann das per Custom-Hook überschreiben).
99
+ - **fileRefs separat**: Export-Bundle enthält Datei-Metadaten (id, fileName, mimeType, size); Binaries werden via signed-URL separat ins ZIP gepackt — kein Inline-Base64-Memory-Druck.
100
+
101
+ ### Auftragskontrolle (gegenüber Sub-Processors)
102
+
103
+ - **Storage-Provider als Plugin** (`file-foundation` + `file-provider-{s3,inmemory}`): App-Author wählt Provider; AVV mit S3-Hoster (Hetzner / AWS) ist vom Provider abhängig.
104
+ - **Email-Transport als Plugin** (`mail-foundation` + `mail-transport-{smtp,inmemory}`): SMTP / SES / Resend per Plugin austauschbar.
105
+ - **`compliance-profiles:query:sub-processors`**: Per-Tenant Liste der aktiven Sub-Processors, abrufbar für AVV-Anhang.
106
+
107
+ ---
108
+
109
+ ## 5. Cron-Jobs / Operationale Trigger
110
+
111
+ | Job | Trigger | Was passiert | Operator-Sichtbarkeit |
112
+ |-----|---------|--------------|------------------------|
113
+ | `run-forget-cleanup` | Cron (per App-Author konfigurierbar, typisch täglich) | Findet User mit abgelaufener Grace, ruft `EXT_USER_DATA.delete` pro Provider, anonymisiert User, sendet `sendDeletionExecutedEmail`-Callback | `read_job_runs` (success/fail), Hook-Errors als `errors[]` im Result |
114
+ | `run-export-jobs` | Cron + Event-getriggert | Findet pending Export-Jobs, ruft `runUserExport` → ZIP-Bau → Storage-Upload → Magic-Link-Token → `sendExportReadyEmail` | `read_job_runs`, `read_export_jobs.status` |
115
+ | `data-retention-cleanup` | Cron (in `data-retention`) | Cleant abgelaufene Rows pro Entity per `retention.keepFor` | `read_job_runs` |
116
+ | Token-Cleanup (implizit) | Per `compliance-profiles.userRights.exportDownloadTtl` | Worker-Job entfernt expired Magic-Link-Tokens + Storage-Binaries | `read_export_download_tokens` |
117
+ | Download-Attempt-Cleanup | Per Entity-Default 90d | Cleant `read_download_attempts` | — |
118
+
119
+ App-Author registriert die Cron-Trigger im run-config; `jobs`-Feature
120
+ persistiert Run-Tracking + Retry-Pfad.
121
+
122
+ ---
123
+
124
+ ## 6. Doku-Snippets für Marc
125
+
126
+ ### Datenschutzerklärung — Betroffenenrechte
127
+
128
+ > **Recht auf Auskunft (Art. 15 DSGVO):** Sie können jederzeit eine
129
+ > Kopie aller bei uns über Sie gespeicherten Daten anfordern. Über die
130
+ > Funktion "Daten exportieren" in den Account-Einstellungen erhalten
131
+ > Sie ein vollständiges JSON-/ZIP-Bundle Ihrer Profildaten, hochgeladenen
132
+ > Dateien und [App-Author: Domain-Daten ergänzen] per signed Magic-Link
133
+ > auf Ihre hinterlegte E-Mail-Adresse. Der Link ist 7 Tage gültig.
134
+
135
+ > **Recht auf Löschung (Art. 17 DSGVO):** Über "Account löschen" können
136
+ > Sie Ihren Account jederzeit zur Löschung vormerken. Es gilt eine
137
+ > Karenzzeit von 30 Tagen, in der Sie den Antrag widerrufen können
138
+ > (Funktion "Löschung widerrufen"). Nach Ablauf werden Ihre Daten
139
+ > automatisch gelöscht oder anonymisiert. Daten mit gesetzlicher
140
+ > Aufbewahrungspflicht (z. B. Rechnungen nach §147 AO) bleiben bis zum
141
+ > Fristablauf gesperrt erhalten und werden danach automatisch gelöscht.
142
+
143
+ > **Recht auf Einschränkung (Art. 18 DSGVO):** Auf begründeten Antrag
144
+ > kann Ihr Account temporär gesperrt werden. Während der Sperre können
145
+ > Sie sich nicht mehr einloggen, Ihre Daten werden aber nicht gelöscht
146
+ > oder verändert.
147
+
148
+ ### AVV-TOM-Anhang
149
+
150
+ Für den AVV-Anhang "Technische und Organisatorische Maßnahmen" siehe
151
+ **Abschnitt 4** dieses Dokuments — komplette Liste mit Verweisen auf
152
+ die Code-Implementierung.
153
+
154
+ ### Verarbeitungsverzeichnis
155
+
156
+ Spalte "Speicherorte / Empfänger" → siehe **Abschnitt 2** (Tabellen-
157
+ Übersicht). Spalte "Löschfristen" → siehe **Abschnitt 3** + Spalte
158
+ "Retention" in Abschnitt 2.
159
+
160
+ ---
161
+
162
+ ## 7. Was NICHT in diesem Framework abgedeckt ist
163
+
164
+ Bewusst ausserhalb — App-Author-Verantwortung:
165
+
166
+ - **Auswahl + AVV mit konkretem Storage-Provider** (Hetzner / AWS / etc.)
167
+ - **Auswahl + AVV mit konkretem Email-Provider** (SMTP-Host / SES / Resend)
168
+ - **Datenpannen-Meldung** an Aufsichtsbehörde (organisatorisch, nicht technisch)
169
+ - **DSFA** (Datenschutz-Folgenabschätzung) — Framework liefert die TOM-Inputs, App-Author macht die Bewertung
170
+ - **Cookie-Consent-Layer** (das ist Frontend-Sache + separate consent-Feature, nicht Teil von user-data-rights)
171
+ - **Tenant-Lifecycle-Destroy** (Account-Löschung des Tenants selbst, nicht der User darin) — kommt als separates `tenant-lifecycle`-Feature in Sprint 5
172
+
173
+ ---
174
+
175
+ ## Referenzen
176
+
177
+ - Code: `packages/bundled-features/src/user-data-rights/`
178
+ - Default-Hooks: `packages/bundled-features/src/user-data-rights-defaults/`
179
+ - Compliance-Profile: `packages/bundled-features/src/compliance-profiles/`
180
+ - Retention-Engine: `packages/bundled-features/src/data-retention/`
181
+ - Sample-App: `samples/apps/user-data-rights-demo/`
182
+ - Tests: `packages/bundled-features/src/user-data-rights/__tests__/` (188 Tests)
@@ -0,0 +1,109 @@
1
+ # user-data-rights
2
+
3
+ DSGVO Art. 15 (Auskunft) + Art. 17 (Löschung) + Art. 18 (Restriction) +
4
+ Art. 20 (Portabilität) als Core-Feature.
5
+
6
+ **Status:** S2 abgeschlossen — alle Endpoints, Hooks, Default-Provider,
7
+ Cron-Pipeline, Tests + Sample wired.
8
+
9
+ ## Pattern
10
+
11
+ Statt jedes Feature seine eigene Forget-/Export-Logik schreibt, hängt
12
+ es sich via `r.useExtension(EXT_USER_DATA, "<entity>", { export, delete })`
13
+ an. user-data-rights orchestriert Export- und Forget-Pipeline:
14
+
15
+ ```ts
16
+ defineFeature("tasks", (r) => {
17
+ r.requires("user-data-rights");
18
+ r.useExtension(EXT_USER_DATA, "task", {
19
+ export: async (ctx) => ({
20
+ entity: "task",
21
+ rows: await ctx.db.select().from(tasksTable)
22
+ .where(eq(tasksTable.authorId, ctx.userId)),
23
+ }),
24
+ delete: async (ctx, strategy) => {
25
+ if (strategy === "anonymize") {
26
+ await ctx.db.update(tasksTable).set({ authorId: null })
27
+ .where(eq(tasksTable.authorId, ctx.userId));
28
+ } else {
29
+ await ctx.db.delete(tasksTable)
30
+ .where(eq(tasksTable.authorId, ctx.userId));
31
+ }
32
+ },
33
+ });
34
+ });
35
+ ```
36
+
37
+ Hook-Signaturen in `framework/src/engine/extensions/user-data.ts`:
38
+
39
+ - `UserDataExportHook(ctx) => Promise<UserDataExportSnippet | null>`
40
+ - `UserDataDeleteHook(ctx, strategy) => Promise<void>`
41
+ - `UserDataDeleteStrategy = "delete" | "anonymize"`
42
+
43
+ ## Endpoints
44
+
45
+ | Article | Handler | Zweck |
46
+ |---------|---------|-------|
47
+ | Art. 15 | `user-data-rights:query:my-audit-log` | User sieht eigene Framework-Events (account-weit über alle Memberships). Domain-Entities ohne `ctx.appendEvent` erscheinen NICHT — nur im Export-Bundle. |
48
+ | Art. 15+20 | `user-data-rights:write:request-export` | Async Job → ZIP mit user-Profil + fileRefs + alle EXT_USER_DATA-Provider-Daten + signed Magic-Link |
49
+ | Art. 17 | `user-data-rights:write:request-deletion` | Soft-Delete mit Grace-Period, anschließend Cron anonymisiert User + cleant Domain-Entities |
50
+ | Art. 17 | `user-data-rights:write:cancel-deletion` | User widerruft seinen Forget-Request während der Grace |
51
+ | Art. 18 | `user-data-rights:write:restrict-account` | Auth-Middleware blockt Logins bis Lift |
52
+ | Art. 18 | `user-data-rights:write:lift-restriction` | Admin/SystemAdmin hebt Restriction auf |
53
+ | Operator | `user-data-rights:query:list-download-attempts` | DPO-Sicht auf invalid Download-Versuche (Brute-Force-Detection, Admin/SystemAdmin only) |
54
+
55
+ Plus 2 anonyme HTTP-Routes für Export-Download (Magic-Link-Pfad +
56
+ session-auth-Pfad), siehe `handlers/download-by-{token,job}.query.ts`.
57
+
58
+ ## Cross-Feature-API
59
+
60
+ **Exposes:**
61
+ - `userDataRights.runExport` — über die public Runner-Exports
62
+ (`runUserExport`, `runForgetCleanup`)
63
+ - `userDataRights.runForget`
64
+
65
+ **Uses:**
66
+ - `compliance.forTenant` (Grace-Period aus Profile)
67
+ - `retention.policyFor` (blockDelete-Konsultation, anonymize statt delete)
68
+ - `sessions.revokeAllForUser` (Restriction killt aktive Sessions)
69
+
70
+ ## Default-Hooks (`user-data-rights-defaults`)
71
+
72
+ Optional-mountbares Sub-Feature liefert Default-Hooks für
73
+ Core-Entities `user` (anonymize: email→`deleted-<id>@anonymized.invalid`,
74
+ displayName→`(deleted)`, passwordHash=null) und `fileRef` (delete: row +
75
+ storage-binary; anonymize: insertedById=null). App-Author kann es
76
+ weglassen wenn er Custom-Hooks registrieren will.
77
+
78
+ ## Audit-Trail
79
+
80
+ | Tabelle | Zweck | Retention |
81
+ |---------|-------|-----------|
82
+ | `kumiko_events` | Framework-Event-Store, Quelle für `my-audit-log` | per Domain-Policy |
83
+ | `read_export_jobs` | Async Export-Status (queued / done / failed) | per `compliance-profiles` |
84
+ | `read_export_download_tokens` | Magic-Link-Hash + TTL + lastUsed-Audit | per `compliance-profiles` (default `exportDownloadTtl`) |
85
+ | `read_download_attempts` | Invalid-Download-Versuche für DPO-Brute-Force-Detection | **90d hardDelete** (Entity-Default — schützt vor Disk-Bomb bei aktiven Angriffen) |
86
+
87
+ ## Tests
88
+
89
+ 18 Testdateien, 188 Tests, alle grün:
90
+
91
+ | Datei | Pinst |
92
+ |-------|-------|
93
+ | `audit-log.integration.ts` | Cross-User-Isolation, Account-weite Sicht, eventType-Filter, Admin-only operator-query, download-attempt 90d-retention |
94
+ | `cross-data-matrix.integration.ts` | 3-Provider-Pipeline (user + fileRef + custom-domain), Cross-Tenant Forget mit user-anonymize, Other-User-Isolation |
95
+ | `download.integration.ts` | HTTP-e2e via `r.httpRoute`: Magic-Link, multi-use, expired, failed-job, storage-cleared, cross-tenant-same-user, malicious-filename |
96
+ | `request-export.integration.ts` | Idempotency, active-job-constraint, cross-tenant-anyMember-userId-pattern |
97
+ | `request-deletion-callback.integration.ts` + `request-cancel-deletion.integration.ts` | Grace + Cancel-Pfad + Email-Callback best-effort |
98
+ | `restriction-flow.integration.ts` | Status-Flip + Auth-Middleware-Block + Lift |
99
+ | `run-{export-jobs,forget-cleanup,user-export}.integration.ts` | Worker-Logic + Idempotency + Email-Callbacks |
100
+ | `policy-to-strategy.test.ts` | Retention.strategy → UserDataDeleteStrategy mapping |
101
+ | `user-data-rights.integration.ts` | Boot-Smoke + Feature-Meta |
102
+ | `token-helpers.test.ts` + `zip-path.test.ts` | Token-Hashing + Path-Traversal-Schutz |
103
+ | `export-job-{idempotency,schema}.test.ts` | Active-job-uniqueness + Schema-Constraints |
104
+
105
+ ## Sample
106
+
107
+ `samples/apps/user-data-rights-demo` — runnable Demo mit todos-Domain,
108
+ EXT_USER_DATA-Hook für strategy-aware delete (anonymize → authorId=null,
109
+ delete → DROP), 3 living-doc Integration-Tests.
@@ -0,0 +1,199 @@
1
+ // S2.U7 — my-audit-log + invalid-attempt-audit + list-download-attempts.
2
+
3
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
4
+ import {
5
+ createTestUser,
6
+ setupTestStack,
7
+ type TestStack,
8
+ testTenantId,
9
+ unsafeCreateEntityTable,
10
+ } from "@cosmicdrift/kumiko-framework/stack";
11
+ import { sql } from "drizzle-orm";
12
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
13
+ import {
14
+ createComplianceProfilesFeature,
15
+ tenantComplianceProfileEntity,
16
+ } from "../../compliance-profiles";
17
+ import { createDataRetentionFeature } from "../../data-retention";
18
+ import { USER_STATUS, userEntity, userTable } from "../../user";
19
+ import { createUserFeature } from "../../user/feature";
20
+ import { createUserDataRightsFeature } from "../feature";
21
+ import { downloadAttemptEntity, downloadAttemptsTable } from "../schema/download-attempt";
22
+
23
+ const MY_AUDIT = "user-data-rights:query:my-audit-log";
24
+ const LIST_ATTEMPTS = "user-data-rights:query:list-download-attempts";
25
+
26
+ let stack: TestStack;
27
+
28
+ const tenantA = testTenantId(1);
29
+ const alice = createTestUser({ id: 42, tenantId: tenantA, roles: ["Member"] });
30
+ const bob = createTestUser({ id: 43, tenantId: tenantA, roles: ["Member"] });
31
+ const admin = createTestUser({ id: 1, tenantId: tenantA, roles: ["Admin"] });
32
+
33
+ beforeAll(async () => {
34
+ stack = await setupTestStack({
35
+ features: [
36
+ createUserFeature(),
37
+ createDataRetentionFeature(),
38
+ createComplianceProfilesFeature(),
39
+ createUserDataRightsFeature(),
40
+ ],
41
+ });
42
+ await unsafeCreateEntityTable(stack.db, userEntity);
43
+ await unsafeCreateEntityTable(stack.db, tenantComplianceProfileEntity);
44
+ await unsafeCreateEntityTable(stack.db, downloadAttemptEntity);
45
+ await createEventsTable(stack.db);
46
+ });
47
+
48
+ afterAll(async () => {
49
+ await stack.cleanup();
50
+ });
51
+
52
+ beforeEach(async () => {
53
+ await stack.db.delete(userTable);
54
+ await stack.db.execute(sql`DELETE FROM read_tenant_compliance_profiles`);
55
+ await stack.db.execute(sql`DELETE FROM read_download_attempts`);
56
+ await stack.db.execute(sql`DELETE FROM kumiko_events`);
57
+ });
58
+
59
+ async function seedUser(u: typeof alice, email: string): Promise<void> {
60
+ await stack.db.insert(userTable).values({
61
+ id: u.id,
62
+ tenantId: u.tenantId,
63
+ email,
64
+ passwordHash: "h",
65
+ displayName: email,
66
+ locale: "de",
67
+ emailVerified: true,
68
+ roles: '["Member"]',
69
+ status: USER_STATUS.Active,
70
+ });
71
+ }
72
+
73
+ let _eventVersion = 0;
74
+ async function seedEvent(
75
+ createdBy: string,
76
+ tenantId: string,
77
+ type: string,
78
+ payload: object,
79
+ ): Promise<void> {
80
+ _eventVersion += 1;
81
+ await stack.db.execute(sql`
82
+ INSERT INTO kumiko_events
83
+ (tenant_id, aggregate_type, aggregate_id, version, type, payload, metadata, created_at, created_by)
84
+ VALUES (${tenantId}, ${"test-aggregate"}, ${"00000000-0000-4000-8000-00000000aaaa"},
85
+ ${_eventVersion}, ${type}, ${JSON.stringify(payload)}, ${"{}"}, now(), ${createdBy})
86
+ `);
87
+ }
88
+
89
+ describe("my-audit-log", () => {
90
+ test("user sieht nur seine eigenen events (cross-user-Isolation)", async () => {
91
+ await seedEvent(alice.id, tenantA, "user.requested-deletion", { foo: "alice" });
92
+ await seedEvent(bob.id, tenantA, "user.requested-deletion", { foo: "bob" });
93
+
94
+ const aliceLog = await stack.http.queryOk<{ rows: Array<{ payload: unknown }> }>(
95
+ MY_AUDIT,
96
+ {},
97
+ alice,
98
+ );
99
+ const bobLog = await stack.http.queryOk<{ rows: Array<{ payload: unknown }> }>(
100
+ MY_AUDIT,
101
+ {},
102
+ bob,
103
+ );
104
+ expect(aliceLog.rows.length).toBe(1);
105
+ expect(bobLog.rows.length).toBe(1);
106
+ // Payload-pinning beweist die Cross-User-Filterung: alice sieht nur
107
+ // ihre Payload, bob nur seine.
108
+ expect((aliceLog.rows[0]?.payload as { foo: string }).foo).toBe("alice");
109
+ expect((bobLog.rows[0]?.payload as { foo: string }).foo).toBe("bob");
110
+ });
111
+
112
+ test("Account-weite Sicht: User sieht events aus anderen Tenants (DSGVO Art. 15)", async () => {
113
+ const tenantB = testTenantId(2);
114
+ await seedEvent(alice.id, tenantA, "user.x", { from: "tenantA" });
115
+ await seedEvent(alice.id, tenantB, "user.y", { from: "tenantB" });
116
+
117
+ const log = await stack.http.queryOk<{
118
+ rows: Array<{ payload: { from: string } }>;
119
+ }>(MY_AUDIT, {}, alice);
120
+
121
+ expect(log.rows.length).toBe(2);
122
+ const fromTenants = log.rows.map((r) => r.payload.from).sort();
123
+ expect(fromTenants).toEqual(["tenantA", "tenantB"]);
124
+ });
125
+
126
+ test("filter eventType + payload kommt mit", async () => {
127
+ await seedEvent(alice.id, tenantA, "user.requested-deletion", { gracePeriodEnd: "2026-06-01" });
128
+ await seedEvent(alice.id, tenantA, "user.lifted-restriction", {});
129
+
130
+ const filtered = await stack.http.queryOk<{ rows: Array<{ type: string }> }>(
131
+ MY_AUDIT,
132
+ { eventType: "user.requested-deletion" },
133
+ alice,
134
+ );
135
+ expect(filtered.rows.length).toBe(1);
136
+ expect(filtered.rows[0]?.type).toBe("user.requested-deletion");
137
+ });
138
+ });
139
+
140
+ describe("download-attempt retention :: disk-bomb-Schutz bei Brute-Force", () => {
141
+ test("Entity-Default ist 90d hardDelete (kein unbounded growth)", () => {
142
+ expect(downloadAttemptEntity.retention).toBeDefined();
143
+ expect(downloadAttemptEntity.retention?.keepFor).toBe("90d");
144
+ expect(downloadAttemptEntity.retention?.strategy).toBe("hardDelete");
145
+ expect(downloadAttemptEntity.retention?.reference).toBe("attemptedAt");
146
+ });
147
+ });
148
+
149
+ describe("list-download-attempts (DPO operator-query)", () => {
150
+ test("Admin kann queryen, Member nicht", async () => {
151
+ await seedUser(alice, "alice@example.com");
152
+ // Admin allowed
153
+ const ok = await stack.http.queryOk<{ rows: unknown[] }>(LIST_ATTEMPTS, {}, admin);
154
+ expect(Array.isArray(ok.rows)).toBe(true);
155
+ // Member blocked
156
+ const res = await stack.http.query(LIST_ATTEMPTS, {}, alice);
157
+ expect([401, 403]).toContain(res.status);
158
+ });
159
+
160
+ test("filter result=notFound", async () => {
161
+ // Direct-INSERT in attempts (simuliert was die download-handler schreiben).
162
+ const T = await import("@cosmicdrift/kumiko-framework/time");
163
+ const now = T.getTemporal().Now.instant();
164
+ await stack.db.insert(downloadAttemptsTable).values([
165
+ {
166
+ id: "11111111-1111-4111-8111-111111111111",
167
+ tenantId: tenantA,
168
+ result: "notFound",
169
+ via: "token",
170
+ tokenHash: "abc",
171
+ jobId: null,
172
+ attemptedByUserId: null,
173
+ ip: "1.2.3.4",
174
+ userAgent: "test",
175
+ attemptedAt: now,
176
+ },
177
+ {
178
+ id: "22222222-2222-4222-8222-222222222222",
179
+ tenantId: tenantA,
180
+ result: "expired",
181
+ via: "token",
182
+ tokenHash: "def",
183
+ jobId: null,
184
+ attemptedByUserId: null,
185
+ ip: "1.2.3.4",
186
+ userAgent: "test",
187
+ attemptedAt: now,
188
+ },
189
+ ]);
190
+
191
+ const filtered = await stack.http.queryOk<{ rows: Array<{ result: string }> }>(
192
+ LIST_ATTEMPTS,
193
+ { result: "notFound" },
194
+ admin,
195
+ );
196
+ expect(filtered.rows.length).toBe(1);
197
+ expect(filtered.rows[0]?.result).toBe("notFound");
198
+ });
199
+ });