@cosmicdrift/kumiko-framework 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 (191) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/package.json +124 -38
  3. package/src/__tests__/full-stack.integration.ts +2 -2
  4. package/src/api/auth-routes.ts +5 -5
  5. package/src/api/jwt.ts +2 -2
  6. package/src/api/route-registrars.ts +1 -1
  7. package/src/api/routes.ts +3 -3
  8. package/src/api/server.ts +6 -7
  9. package/src/auth/__tests__/roles.test.ts +24 -0
  10. package/src/auth/index.ts +7 -0
  11. package/src/auth/roles.ts +42 -0
  12. package/src/compliance/__tests__/duration-spec.test.ts +72 -0
  13. package/src/compliance/__tests__/profiles.test.ts +308 -0
  14. package/src/compliance/__tests__/sub-processors.test.ts +139 -0
  15. package/src/compliance/duration-spec.ts +44 -0
  16. package/src/compliance/index.ts +31 -0
  17. package/src/compliance/override-schema.ts +136 -0
  18. package/src/compliance/profiles.ts +427 -0
  19. package/src/compliance/sub-processors.ts +152 -0
  20. package/src/db/__tests__/big-int-field.test.ts +131 -0
  21. package/src/db/assert-exists-in.ts +2 -2
  22. package/src/db/cursor.ts +3 -3
  23. package/src/db/event-store-executor.ts +19 -13
  24. package/src/db/located-timestamp.ts +1 -1
  25. package/src/db/money.ts +12 -2
  26. package/src/db/pg-error.ts +1 -1
  27. package/src/db/row-helpers.ts +1 -1
  28. package/src/db/table-builder.ts +20 -5
  29. package/src/db/tenant-db.ts +9 -9
  30. package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
  31. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
  32. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
  33. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -0
  34. package/src/engine/__tests__/build-target.test.ts +135 -0
  35. package/src/engine/__tests__/codemod-pipeline.test.ts +551 -0
  36. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  37. package/src/engine/__tests__/event-helpers.test.ts +4 -4
  38. package/src/engine/__tests__/pipeline-engine.test.ts +215 -0
  39. package/src/engine/__tests__/pipeline-handler.integration.ts +894 -0
  40. package/src/engine/__tests__/pipeline-observability.integration.ts +142 -0
  41. package/src/engine/__tests__/pipeline-performance.integration.ts +152 -0
  42. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +288 -0
  43. package/src/engine/__tests__/raw-table.test.ts +2 -2
  44. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +115 -0
  45. package/src/engine/__tests__/steps-aggregate-create.test.ts +92 -0
  46. package/src/engine/__tests__/steps-aggregate-update.test.ts +127 -0
  47. package/src/engine/__tests__/steps-call-feature.test.ts +123 -0
  48. package/src/engine/__tests__/steps-mail-send.test.ts +136 -0
  49. package/src/engine/__tests__/steps-read.test.ts +142 -0
  50. package/src/engine/__tests__/steps-resolver-utils.test.ts +50 -0
  51. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +69 -0
  52. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +117 -0
  53. package/src/engine/__tests__/steps-webhook-send.test.ts +135 -0
  54. package/src/engine/__tests__/steps-workflow.test.ts +198 -0
  55. package/src/engine/__tests__/validate-projection-allowlist.test.ts +491 -0
  56. package/src/engine/__tests__/visual-tree-patterns.test.ts +251 -0
  57. package/src/engine/boot-validator/api-ext.ts +77 -0
  58. package/src/engine/boot-validator/config-deps.ts +163 -0
  59. package/src/engine/boot-validator/entity-handler.ts +466 -0
  60. package/src/engine/boot-validator/index.ts +159 -0
  61. package/src/engine/boot-validator/ownership.ts +198 -0
  62. package/src/engine/boot-validator/pii-retention.ts +155 -0
  63. package/src/engine/boot-validator/screens-nav.ts +624 -0
  64. package/src/engine/boot-validator.ts +1 -1528
  65. package/src/engine/build-app-schema.ts +1 -1
  66. package/src/engine/build-target.ts +99 -0
  67. package/src/engine/codemod/index.ts +15 -0
  68. package/src/engine/codemod/pipeline-codemod.ts +641 -0
  69. package/src/engine/config-helpers.ts +9 -19
  70. package/src/engine/constants.ts +1 -1
  71. package/src/engine/define-feature.ts +127 -9
  72. package/src/engine/define-handler.ts +89 -3
  73. package/src/engine/define-roles.ts +2 -2
  74. package/src/engine/define-step.ts +28 -0
  75. package/src/engine/define-workflow.ts +110 -0
  76. package/src/engine/entity-handlers.ts +10 -9
  77. package/src/engine/event-helpers.ts +4 -4
  78. package/src/engine/extension-names.ts +105 -0
  79. package/src/engine/extensions/user-data.ts +106 -0
  80. package/src/engine/factories.ts +26 -16
  81. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +184 -0
  82. package/src/engine/feature-ast/extractors/index.ts +74 -0
  83. package/src/engine/feature-ast/extractors/round1.ts +110 -0
  84. package/src/engine/feature-ast/extractors/round2.ts +253 -0
  85. package/src/engine/feature-ast/extractors/round3.ts +471 -0
  86. package/src/engine/feature-ast/extractors/round4.ts +1365 -0
  87. package/src/engine/feature-ast/extractors/round5.ts +72 -0
  88. package/src/engine/feature-ast/extractors/round6.ts +66 -0
  89. package/src/engine/feature-ast/extractors/shared.ts +177 -0
  90. package/src/engine/feature-ast/parse.ts +13 -0
  91. package/src/engine/feature-ast/patch.ts +9 -1
  92. package/src/engine/feature-ast/patcher.ts +10 -3
  93. package/src/engine/feature-ast/patterns.ts +71 -1
  94. package/src/engine/feature-ast/render.ts +31 -1
  95. package/src/engine/index.ts +66 -2
  96. package/src/engine/pattern-library/__tests__/library.test.ts +11 -0
  97. package/src/engine/pattern-library/library.ts +78 -2
  98. package/src/engine/pipeline.ts +88 -0
  99. package/src/engine/projection-helpers.ts +1 -1
  100. package/src/engine/read-claim.ts +1 -1
  101. package/src/engine/registry.ts +30 -2
  102. package/src/engine/resolve-config-or-param.ts +4 -0
  103. package/src/engine/run-pipeline.ts +162 -0
  104. package/src/engine/schema-builder.ts +10 -4
  105. package/src/engine/state-machine.ts +1 -1
  106. package/src/engine/steps/_drizzle-boundary.ts +19 -0
  107. package/src/engine/steps/_duration-utils.ts +33 -0
  108. package/src/engine/steps/_no-return-guard.ts +21 -0
  109. package/src/engine/steps/_resolver-utils.ts +42 -0
  110. package/src/engine/steps/_step-dispatch-constants.ts +38 -0
  111. package/src/engine/steps/aggregate-append-event.ts +56 -0
  112. package/src/engine/steps/aggregate-create.ts +56 -0
  113. package/src/engine/steps/aggregate-update.ts +68 -0
  114. package/src/engine/steps/branch.ts +84 -0
  115. package/src/engine/steps/call-feature.ts +49 -0
  116. package/src/engine/steps/compute.ts +41 -0
  117. package/src/engine/steps/for-each.ts +111 -0
  118. package/src/engine/steps/mail-send.ts +44 -0
  119. package/src/engine/steps/read-find-many.ts +51 -0
  120. package/src/engine/steps/read-find-one.ts +58 -0
  121. package/src/engine/steps/retry.ts +87 -0
  122. package/src/engine/steps/return.ts +34 -0
  123. package/src/engine/steps/unsafe-projection-delete.ts +46 -0
  124. package/src/engine/steps/unsafe-projection-upsert.ts +69 -0
  125. package/src/engine/steps/wait-for-event.ts +71 -0
  126. package/src/engine/steps/wait.ts +69 -0
  127. package/src/engine/steps/webhook-send.ts +71 -0
  128. package/src/engine/system-user.ts +1 -1
  129. package/src/engine/types/feature.ts +143 -1
  130. package/src/engine/types/fields.ts +134 -10
  131. package/src/engine/types/handlers.ts +18 -10
  132. package/src/engine/types/identifiers.ts +1 -0
  133. package/src/engine/types/index.ts +15 -1
  134. package/src/engine/types/step.ts +334 -0
  135. package/src/engine/types/target-ref.ts +21 -0
  136. package/src/engine/types/tree-node.ts +130 -0
  137. package/src/engine/types/workspace.ts +7 -0
  138. package/src/engine/validate-projection-allowlist.ts +161 -0
  139. package/src/event-store/snapshot.ts +1 -1
  140. package/src/event-store/upcaster-dead-letter.ts +1 -1
  141. package/src/event-store/upcaster.ts +1 -1
  142. package/src/files/__tests__/read-stream.test.ts +105 -0
  143. package/src/files/__tests__/write-stream.test.ts +233 -0
  144. package/src/files/__tests__/zip-stream.test.ts +357 -0
  145. package/src/files/file-routes.ts +1 -1
  146. package/src/files/in-memory-provider.ts +38 -0
  147. package/src/files/index.ts +3 -0
  148. package/src/files/local-provider.ts +58 -1
  149. package/src/files/types.ts +36 -8
  150. package/src/files/zip-stream.ts +251 -0
  151. package/src/jobs/job-runner.ts +10 -10
  152. package/src/lifecycle/lifecycle.ts +0 -3
  153. package/src/logging/index.ts +1 -0
  154. package/src/logging/pino-logger.ts +11 -7
  155. package/src/logging/utils.ts +24 -0
  156. package/src/observability/prometheus-meter.ts +7 -5
  157. package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
  158. package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
  159. package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
  160. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
  161. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
  162. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
  163. package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
  164. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
  165. package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
  166. package/src/pipeline/append-event-core.ts +22 -6
  167. package/src/pipeline/dispatcher-utils.ts +188 -0
  168. package/src/pipeline/dispatcher.ts +63 -283
  169. package/src/pipeline/distributed-lock.ts +1 -1
  170. package/src/pipeline/entity-cache.ts +2 -2
  171. package/src/pipeline/event-consumer-state.ts +0 -13
  172. package/src/pipeline/event-dispatcher.ts +4 -4
  173. package/src/pipeline/index.ts +0 -2
  174. package/src/pipeline/lifecycle-pipeline.ts +6 -12
  175. package/src/pipeline/msp-rebuild.ts +5 -5
  176. package/src/pipeline/multi-stream-apply-context.ts +6 -7
  177. package/src/pipeline/projection-rebuild.ts +2 -2
  178. package/src/pipeline/projection-state.ts +0 -12
  179. package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
  180. package/src/rate-limit/resolver.ts +1 -1
  181. package/src/search/in-memory-adapter.ts +1 -1
  182. package/src/search/meilisearch-adapter.ts +3 -3
  183. package/src/search/types.ts +1 -1
  184. package/src/secrets/leak-guard.ts +2 -2
  185. package/src/stack/request-helper.ts +9 -5
  186. package/src/stack/test-stack.ts +1 -1
  187. package/src/testing/handler-context.ts +4 -4
  188. package/src/testing/http-cookies.ts +1 -1
  189. package/src/time/tz-context.ts +1 -2
  190. package/src/ui-types/index.ts +4 -0
  191. package/src/engine/feature-ast/extractors.ts +0 -2562
@@ -0,0 +1,308 @@
1
+ // Tests fuer Compliance-Profile-Constants + extends-Resolver.
2
+ //
3
+ // Edge-Cases (advisor-pinned 2026-05-06):
4
+ // 1. Default-Fallback bei selection=undefined → minimal-no-region + warning
5
+ // 2. extends-Chain max 1 Level (Cycle-/Tiefe-Schutz)
6
+ // 3. Deep-merge-Semantik beim Override (rekursiv, Top-Level-Replace nicht)
7
+ // 4. Override darf einzelne Felder gezielt setzen ohne Required-Drops
8
+
9
+ import { describe, expect, test } from "vitest";
10
+ import { complianceProfileOverrideSchema } from "../override-schema";
11
+ import {
12
+ COMPLIANCE_PROFILES,
13
+ OVERRIDABLE_PROFILE_KEYS,
14
+ resolveComplianceProfile,
15
+ SELECTABLE_PROFILE_KEYS,
16
+ } from "../profiles";
17
+
18
+ // Identifikations-Keys die ein Override NICHT modifizieren darf — sie
19
+ // definieren die Profile-Identität. Werden vom Drift-Test ausgeschlossen.
20
+ const IDENTIFICATION_KEYS = new Set(["key", "region", "label", "extends"]);
21
+
22
+ describe("COMPLIANCE_PROFILES — Pre-baked", () => {
23
+ test("eu-dsgvo hat alle Required-Felder", () => {
24
+ const p = COMPLIANCE_PROFILES["eu-dsgvo"];
25
+ expect(p.key).toBe("eu-dsgvo");
26
+ expect(p.region).toBe("EU");
27
+ expect(p.userRights.gracePeriod).toEqual({ days: 30 });
28
+ expect(p.notifications.languages).toContain("de");
29
+ expect(p.breach.authorityContact).toBe("BlnBDI Berlin");
30
+ expect(p.tenantDestroyGracePeriod).toEqual({ days: 30 });
31
+ });
32
+
33
+ test("swiss-dsg extends eu-dsgvo — übernimmt Base-Felder", () => {
34
+ const p = COMPLIANCE_PROFILES["swiss-dsg"];
35
+ expect(p.key).toBe("swiss-dsg");
36
+ expect(p.region).toBe("CH");
37
+ // Override: andere Sprachen + andere Aufsicht
38
+ expect(p.notifications.languages).toEqual(["de", "fr", "it", "en"]);
39
+ expect(p.breach.authorityContact).toBe("EDÖB Bern");
40
+ // Geerbt von eu-dsgvo
41
+ expect(p.userRights.gracePeriod).toEqual({ days: 30 });
42
+ expect(p.userRights.restrictionAllowed).toBe(true);
43
+ expect(p.tenantDestroyGracePeriod).toEqual({ days: 30 });
44
+ });
45
+
46
+ test("de-hr-dsgvo-hgb extends eu-dsgvo — HR-Override greift, Base bleibt", () => {
47
+ const p = COMPLIANCE_PROFILES["de-hr-dsgvo-hgb"];
48
+ expect(p.region).toBe("DE");
49
+ expect(p.userRights.employeeAccessRight).toBe(true);
50
+ expect(p.breach.worksCouncilNotificationRequired).toBe(true);
51
+ expect(p.subProcessor.worksCouncilApprovalRequired).toBe(true);
52
+ expect(p.auditLog.retention).toEqual({ years: 10 });
53
+ expect(p.tenantDestroyGracePeriod).toEqual({ days: 60 });
54
+ // Geerbt: portabilityFormat aus eu-dsgvo bleibt
55
+ expect(p.userRights.portabilityFormat).toEqual(["json"]);
56
+ });
57
+
58
+ test("minimal-no-region ist eigenständig (kein extends)", () => {
59
+ const p = COMPLIANCE_PROFILES["minimal-no-region"];
60
+ expect(p.region).toBe("—");
61
+ expect(p.notifications.mandatoryBreachNotification).toBe(false);
62
+ expect(p.breach.authorityNotificationDeadline).toBe("manual");
63
+ });
64
+ });
65
+
66
+ describe("SELECTABLE_PROFILE_KEYS", () => {
67
+ test('enthält 3 Profile, ohne "minimal-no-region" (kein Production-Default)', () => {
68
+ expect(SELECTABLE_PROFILE_KEYS).toEqual(["eu-dsgvo", "swiss-dsg", "de-hr-dsgvo-hgb"]);
69
+ expect(SELECTABLE_PROFILE_KEYS).not.toContain("minimal-no-region");
70
+ });
71
+ });
72
+
73
+ describe("OVERRIDABLE_PROFILE_KEYS — Drift-Guard (S1.8 N2)", () => {
74
+ // Wenn ein neues Top-Level-Property zu ComplianceProfile kommt
75
+ // (oder eines wegfaellt), muss OVERRIDABLE_PROFILE_KEYS synchron
76
+ // gehalten werden — sonst lehnt set-profile valide Overrides ab
77
+ // ODER akzeptiert ungueltige. Dieser Test detektiert den Drift in
78
+ // beide Richtungen anhand des voll aufgeloesten eu-dsgvo-Profile.
79
+ test("Override-Whitelist matched die uebrigen Top-Level-Keys von eu-dsgvo (minus Identifikations-Felder)", () => {
80
+ const allTopLevelKeys = new Set(Object.keys(COMPLIANCE_PROFILES["eu-dsgvo"]));
81
+ for (const idKey of IDENTIFICATION_KEYS) {
82
+ allTopLevelKeys.delete(idKey);
83
+ }
84
+ const expected = [...allTopLevelKeys].sort();
85
+ const actual = [...OVERRIDABLE_PROFILE_KEYS].sort();
86
+ expect(actual).toEqual(expected);
87
+ });
88
+
89
+ test("Identifikations-Keys (key, region, label, extends) sind NICHT in OVERRIDABLE_PROFILE_KEYS", () => {
90
+ for (const idKey of IDENTIFICATION_KEYS) {
91
+ expect(OVERRIDABLE_PROFILE_KEYS.has(idKey)).toBe(false);
92
+ }
93
+ });
94
+ });
95
+
96
+ describe("complianceProfileOverrideSchema — Schema↔Profile-Konsistenz (S1.10 M2)", () => {
97
+ // Wenn Profile-Type erweitert wird (neues Sub-Property in userRights /
98
+ // breach / auditLog / etc.) ohne dass override-schema.ts mitgepflegt
99
+ // wird, fail dieser Test. Prinzip: ein voll aufgelöstes Profile ist
100
+ // ein triviales valides Override-Object — wenn Schema das ablehnt, ist
101
+ // entweder Schema oder Profile out of sync.
102
+ test("Schema akzeptiert ein vollständig aufgelöstes Profile als Override (Drift-Guard)", () => {
103
+ // Identifikations-Felder rausnehmen weil die nicht override-bar sind.
104
+ const fullProfile = COMPLIANCE_PROFILES["eu-dsgvo"];
105
+ const {
106
+ key: _key,
107
+ region: _region,
108
+ label: _label,
109
+ extends: _extends,
110
+ ...overridable
111
+ } = fullProfile;
112
+
113
+ const result = complianceProfileOverrideSchema.safeParse(overridable);
114
+ if (!result.success) {
115
+ throw new Error(
116
+ `Schema↔Profile-Drift: vollständig aufgelöstes eu-dsgvo wird vom Override-Schema abgelehnt. ` +
117
+ `Issue at "${result.error.issues[0]?.path.join(".")}": ${result.error.issues[0]?.message}. ` +
118
+ "Entweder ComplianceProfile-Type erweitert ohne Schema-Update, oder Schema strikter als Profile-Type.",
119
+ );
120
+ }
121
+ expect(result.success).toBe(true);
122
+ });
123
+
124
+ test("Schema akzeptiert auch swiss-dsg + de-hr-dsgvo-hgb (extends-Profile mit Override-Feldern)", () => {
125
+ for (const key of ["swiss-dsg", "de-hr-dsgvo-hgb"] as const) {
126
+ const fullProfile = COMPLIANCE_PROFILES[key];
127
+ const {
128
+ key: _key,
129
+ region: _region,
130
+ label: _label,
131
+ extends: _extends,
132
+ ...overridable
133
+ } = fullProfile;
134
+ const result = complianceProfileOverrideSchema.safeParse(overridable);
135
+ expect(result.success, `Schema lehnt voll aufgelöstes ${key} ab`).toBe(true);
136
+ }
137
+ });
138
+ });
139
+
140
+ describe("resolveComplianceProfile — Default-Fallback", () => {
141
+ test("selection=undefined → minimal-no-region + warning=no-profile-selected", () => {
142
+ const result = resolveComplianceProfile({});
143
+ expect(result.profile.key).toBe("minimal-no-region");
144
+ expect(result.warning).toBe("no-profile-selected");
145
+ });
146
+
147
+ test("selection=minimal-no-region (DB-Edge-Case) → kein warning, fallback aktiv", () => {
148
+ // Sprint 1.7 X1: minimal-no-region ist ueber set-profile nicht mehr
149
+ // setzbar. Wer den State trotzdem in der DB hat (Migration, Direct-
150
+ // Insert) bekommt das minimal-Profile zurueck — der needs-profile-
151
+ // Banner-Endpoint markiert ihn separat als "needsSelection=true".
152
+ // Resolver selber zieht keine warning, weil "explicit selection".
153
+ const result = resolveComplianceProfile({ selection: "minimal-no-region" });
154
+ expect(result.profile.key).toBe("minimal-no-region");
155
+ expect(result.warning).toBeUndefined();
156
+ });
157
+
158
+ test("selection=eu-dsgvo + kein override → keine warning", () => {
159
+ const result = resolveComplianceProfile({ selection: "eu-dsgvo" });
160
+ expect(result.profile.key).toBe("eu-dsgvo");
161
+ expect(result.warning).toBeUndefined();
162
+ });
163
+ });
164
+
165
+ describe("resolveComplianceProfile — Override deep-merge", () => {
166
+ test("Override gracePeriod.days überschreibt rekursiv, andere userRights bleiben", () => {
167
+ const result = resolveComplianceProfile({
168
+ selection: "eu-dsgvo",
169
+ override: {
170
+ userRights: { gracePeriod: { days: 60 } },
171
+ },
172
+ });
173
+ expect(result.profile.userRights.gracePeriod).toEqual({ days: 60 });
174
+ // Andere userRights-Felder unverändert
175
+ expect(result.profile.userRights.restrictionAllowed).toBe(true);
176
+ expect(result.profile.userRights.objectionAllowed).toBe(true);
177
+ expect(result.profile.userRights.portabilityFormat).toEqual(["json"]);
178
+ expect(result.profile.userRights.auskunftFrist).toEqual({ days: 30 });
179
+ });
180
+
181
+ test("Override breach.authorityContact ändert nur diesen einen Pfad", () => {
182
+ const result = resolveComplianceProfile({
183
+ selection: "eu-dsgvo",
184
+ override: {
185
+ breach: { authorityContact: "Hamburgischer DSB" },
186
+ },
187
+ });
188
+ expect(result.profile.breach.authorityContact).toBe("Hamburgischer DSB");
189
+ // Rest des breach-Objects unverändert
190
+ expect(result.profile.breach.authorityNotificationDeadline).toEqual({ hours: 72 });
191
+ expect(result.profile.breach.userNotificationRequired).toBe("if-high-risk");
192
+ });
193
+
194
+ test("Override mehrerer Top-Level-Pfade gleichzeitig", () => {
195
+ const result = resolveComplianceProfile({
196
+ selection: "swiss-dsg",
197
+ override: {
198
+ userRights: { gracePeriod: { days: 45 } },
199
+ tenantDestroyGracePeriod: { days: 90 },
200
+ },
201
+ });
202
+ expect(result.profile.userRights.gracePeriod).toEqual({ days: 45 });
203
+ expect(result.profile.tenantDestroyGracePeriod).toEqual({ days: 90 });
204
+ // swiss-dsg extends eu-dsgvo war drin
205
+ expect(result.profile.region).toBe("CH");
206
+ expect(result.profile.breach.authorityContact).toBe("EDÖB Bern");
207
+ });
208
+
209
+ test("Atomic path: retention {months} → {years} ersetzt komplett (kein object-merge)", () => {
210
+ // Bug-Regression: deepMerge auf Diskriminierten-Union-Objects
211
+ // wuerde sonst { months: 24, years: 10 } produzieren — semantisch
212
+ // Nonsense, retention ist EINE Wahl.
213
+ const result = resolveComplianceProfile({
214
+ selection: "eu-dsgvo",
215
+ override: {
216
+ auditLog: { retention: { years: 10 } },
217
+ },
218
+ });
219
+ expect(result.profile.auditLog.retention).toEqual({ years: 10 });
220
+ // reportFrequency darf NICHT mit ersetzt werden — auditLog ist
221
+ // nicht atomic, nur retention darunter.
222
+ expect(result.profile.auditLog.reportFrequency).toBe("quarterly");
223
+ });
224
+
225
+ test("Atomic path: gracePeriod {days} → {hours} ersetzt komplett", () => {
226
+ const result = resolveComplianceProfile({
227
+ selection: "eu-dsgvo",
228
+ override: {
229
+ userRights: { gracePeriod: { hours: 24 } },
230
+ },
231
+ });
232
+ expect(result.profile.userRights.gracePeriod).toEqual({ hours: 24 });
233
+ expect(result.profile.userRights.gracePeriod).not.toHaveProperty("days");
234
+ });
235
+
236
+ test("Array-Property im Override ersetzt komplett (Top-Level-Replace, kein concat)", () => {
237
+ const result = resolveComplianceProfile({
238
+ selection: "eu-dsgvo",
239
+ override: {
240
+ notifications: { languages: ["en"] },
241
+ },
242
+ });
243
+ // Replace, nicht append: ["de", "en"] wird zu ["en"]
244
+ expect(result.profile.notifications.languages).toEqual(["en"]);
245
+ // Andere notifications-Felder bleiben
246
+ expect(result.profile.notifications.mandatoryBreachNotification).toBe(true);
247
+ });
248
+ });
249
+
250
+ describe("Profile-Definition-Snapshots — fängt Drift ab", () => {
251
+ test("eu-dsgvo voll-resolved", () => {
252
+ expect(COMPLIANCE_PROFILES["eu-dsgvo"]).toMatchInlineSnapshot(`
253
+ {
254
+ "auditLog": {
255
+ "reportFrequency": "quarterly",
256
+ "retention": {
257
+ "months": 24,
258
+ },
259
+ },
260
+ "breach": {
261
+ "authorityContact": "BlnBDI Berlin",
262
+ "authorityNotificationDeadline": {
263
+ "hours": 72,
264
+ },
265
+ "userNotificationRequired": "if-high-risk",
266
+ },
267
+ "forgetDiscovery": {
268
+ "enabled": false,
269
+ },
270
+ "key": "eu-dsgvo",
271
+ "label": "EU — DSGVO Standard",
272
+ "notifications": {
273
+ "languages": [
274
+ "de",
275
+ "en",
276
+ ],
277
+ "mandatoryBreachNotification": true,
278
+ },
279
+ "region": "EU",
280
+ "subProcessor": {
281
+ "changeNotificationLeadDays": 30,
282
+ "consentRequired": false,
283
+ },
284
+ "tenantDestroyGracePeriod": {
285
+ "days": 30,
286
+ },
287
+ "userRights": {
288
+ "auskunftFrist": {
289
+ "days": 30,
290
+ },
291
+ "exportDownloadTtl": {
292
+ "days": 7,
293
+ },
294
+ "exportStaleTimeoutMinutes": 30,
295
+ "exportStorageCleanupGraceHours": 24,
296
+ "gracePeriod": {
297
+ "days": 30,
298
+ },
299
+ "objectionAllowed": true,
300
+ "portabilityFormat": [
301
+ "json",
302
+ ],
303
+ "restrictionAllowed": true,
304
+ },
305
+ }
306
+ `);
307
+ });
308
+ });
@@ -0,0 +1,139 @@
1
+ // Snapshot-Tests fuer KUMIKO_SUB_PROCESSORS. Fangen still-Drift ab —
2
+ // jede Aenderung an der Liste muss bewusst durch ein Snapshot-Update
3
+ // gehen, damit niemand versehentlich einen Sub-Processor entfernt
4
+ // oder hinzufuegt ohne dass die DPA/Tenant-Notification-Pipeline das
5
+ // mitbekommt.
6
+
7
+ import { describe, expect, test } from "vitest";
8
+ import {
9
+ getActiveSubProcessors,
10
+ getPlannedSubProcessors,
11
+ KUMIKO_SUB_PROCESSORS,
12
+ } from "../sub-processors";
13
+
14
+ describe("KUMIKO_SUB_PROCESSORS", () => {
15
+ test("Liste ist nicht leer (Plattform hat mindestens Hetzner+Cloudflare)", () => {
16
+ expect(KUMIKO_SUB_PROCESSORS.length).toBeGreaterThan(0);
17
+ });
18
+
19
+ test("Alle Eintraege haben Pflicht-Felder (name, purpose, region, dpa, addedAt, appliesTo)", () => {
20
+ for (const sp of KUMIKO_SUB_PROCESSORS) {
21
+ expect(sp.name).toBeTruthy();
22
+ expect(sp.purpose).toBeTruthy();
23
+ expect(sp.region).toBeTruthy();
24
+ expect(sp.dpa).toMatch(/^https?:\/\//);
25
+ expect(sp.addedAt).toMatch(/^\d{4}-\d{2}-\d{2}$/);
26
+ expect(sp.appliesTo.length).toBeGreaterThan(0);
27
+ }
28
+ });
29
+
30
+ test("US-Sub-Processors haben sccRequired: true (DSGVO Art. 44+ Drittland)", () => {
31
+ const usProcessors = KUMIKO_SUB_PROCESSORS.filter(
32
+ (sp) => sp.region.includes("US") && !sp.region.startsWith("EU"),
33
+ );
34
+ for (const sp of usProcessors) {
35
+ expect(
36
+ sp.sccRequired,
37
+ `Sub-Processor "${sp.name}" with US-region should have sccRequired: true`,
38
+ ).toBe(true);
39
+ }
40
+ });
41
+
42
+ test("Snapshot — explizit zu updaten bei jeder Liste-Aenderung", () => {
43
+ const summary = KUMIKO_SUB_PROCESSORS.map((sp) => ({
44
+ name: sp.name,
45
+ region: sp.region,
46
+ status: sp.status ?? "active",
47
+ appliesTo: sp.appliesTo,
48
+ sccRequired: sp.sccRequired ?? false,
49
+ optInOnly: sp.optInOnly ?? false,
50
+ }));
51
+ expect(summary).toMatchInlineSnapshot(`
52
+ [
53
+ {
54
+ "appliesTo": [
55
+ "all-tiers",
56
+ ],
57
+ "name": "Hetzner Online GmbH",
58
+ "optInOnly": false,
59
+ "region": "EU (Germany)",
60
+ "sccRequired": false,
61
+ "status": "active",
62
+ },
63
+ {
64
+ "appliesTo": [
65
+ "all-tiers",
66
+ ],
67
+ "name": "Cloudflare, Inc.",
68
+ "optInOnly": false,
69
+ "region": "Global (US-headquartered)",
70
+ "sccRequired": true,
71
+ "status": "active",
72
+ },
73
+ {
74
+ "appliesTo": [
75
+ "standard",
76
+ "business",
77
+ "enterprise",
78
+ ],
79
+ "name": "Sendinblue SAS (Brevo)",
80
+ "optInOnly": false,
81
+ "region": "EU (France)",
82
+ "sccRequired": false,
83
+ "status": "active",
84
+ },
85
+ {
86
+ "appliesTo": [
87
+ "all-tiers",
88
+ ],
89
+ "name": "Heinlein Hosting (Mailbox.org)",
90
+ "optInOnly": false,
91
+ "region": "EU (Germany)",
92
+ "sccRequired": false,
93
+ "status": "active",
94
+ },
95
+ {
96
+ "appliesTo": [
97
+ "business",
98
+ "enterprise",
99
+ ],
100
+ "name": "Anthropic PBC",
101
+ "optInOnly": true,
102
+ "region": "US",
103
+ "sccRequired": true,
104
+ "status": "planned",
105
+ },
106
+ {
107
+ "appliesTo": [
108
+ "all-tiers",
109
+ ],
110
+ "name": "Stripe, Inc.",
111
+ "optInOnly": false,
112
+ "region": "Global (US-headquartered)",
113
+ "sccRequired": true,
114
+ "status": "planned",
115
+ },
116
+ ]
117
+ `);
118
+ });
119
+ });
120
+
121
+ describe("getActiveSubProcessors / getPlannedSubProcessors", () => {
122
+ test("active + planned partitionieren die Gesamt-Liste vollstaendig", () => {
123
+ const active = getActiveSubProcessors();
124
+ const planned = getPlannedSubProcessors();
125
+ expect(active.length + planned.length).toBe(KUMIKO_SUB_PROCESSORS.length);
126
+ });
127
+
128
+ test("kein Sub-Processor in active hat status=planned", () => {
129
+ for (const sp of getActiveSubProcessors()) {
130
+ expect(sp.status).not.toBe("planned");
131
+ }
132
+ });
133
+
134
+ test("alle planned-Eintraege haben status=planned", () => {
135
+ for (const sp of getPlannedSubProcessors()) {
136
+ expect(sp.status).toBe("planned");
137
+ }
138
+ });
139
+ });
@@ -0,0 +1,44 @@
1
+ // DurationSpec ↔ SQL-Interval ↔ Temporal.Duration converter.
2
+ //
3
+ // Hintergrund: ComplianceProfile.userRights.gracePeriod ist
4
+ // `{ days: number } | { hours: number }` (Discriminated Union). Caller
5
+ // die das in eine Postgres-`interval`-SQL einsetzen brauchen einen
6
+ // einzigen vertrauenswuerdigen Punkt, sonst springt ein
7
+ // `{ hours: 6 }`-Override stillschweigend auf einen days-Default.
8
+ //
9
+ // Single source of truth: hier. Andere Spec-Forms (months/years) im
10
+ // retention-Pfad gehen ueber den breiteren `keep-for`-Parser, sind hier
11
+ // bewusst nicht abgedeckt — das engt den Type ein und macht die SQL-
12
+ // Renderung total.
13
+
14
+ import { getTemporal } from "../time";
15
+ import type { DurationSpec } from "./profiles";
16
+
17
+ type Instant = InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
18
+
19
+ // Spec in Millisekunden. Source of truth fuer alle Konvertierungen
20
+ // (Instant-add, optional fuer JS-Datumsrechnung).
21
+ export function durationSpecToMs(spec: DurationSpec): number {
22
+ if ("days" in spec) return spec.days * 24 * 60 * 60 * 1000;
23
+ return spec.hours * 60 * 60 * 1000;
24
+ }
25
+
26
+ // Frist berechnen ohne DB-now() — der App-Server-Clock ist
27
+ // authoritative. Fuer Forget-Grace, Token-TTLs und Frist-Setzungen
28
+ // where eine Toleranz von wenigen ms zwischen App und DB irrelevant
29
+ // ist (Grace-Periods >= 6h, Tokens >= Minuten).
30
+ //
31
+ // Schreibt direkt in `instant()`-customType-Spalten — kein interval-
32
+ // SQL-Fragment, kein Codec-Bypass.
33
+ export function addDurationSpec(now: Instant, spec: DurationSpec): Instant {
34
+ return getTemporal().Instant.fromEpochMilliseconds(
35
+ now.epochMilliseconds + durationSpecToMs(spec),
36
+ );
37
+ }
38
+
39
+ // Lesbare Beschreibung fuer Logs / Error-Messages. Nicht i18n —
40
+ // English-only-Operator-Output.
41
+ export function describeDurationSpec(spec: DurationSpec): string {
42
+ if ("days" in spec) return `${spec.days} day${spec.days === 1 ? "" : "s"}`;
43
+ return `${spec.hours} hour${spec.hours === 1 ? "" : "s"}`;
44
+ }
@@ -0,0 +1,31 @@
1
+ // `@cosmicdrift/kumiko-framework/compliance` — Datenschutz/Compliance-
2
+ // Foundation. Wird von Sprint-1+ Features genutzt (compliance-profiles,
3
+ // data-retention, user-data-rights, ...).
4
+
5
+ export {
6
+ addDurationSpec,
7
+ describeDurationSpec,
8
+ durationSpecToMs,
9
+ } from "./duration-spec";
10
+ export { complianceProfileOverrideSchema } from "./override-schema";
11
+ export type {
12
+ AuthorityNotificationDeadline,
13
+ ComplianceProfile,
14
+ ComplianceProfileKey,
15
+ ComplianceProfileOverride,
16
+ DurationSpec,
17
+ EffectiveComplianceProfile,
18
+ UserNotificationRequiredPolicy,
19
+ } from "./profiles";
20
+ export {
21
+ COMPLIANCE_PROFILES,
22
+ OVERRIDABLE_PROFILE_KEYS,
23
+ resolveComplianceProfile,
24
+ SELECTABLE_PROFILE_KEYS,
25
+ } from "./profiles";
26
+ export type { BundleTier, SubProcessor } from "./sub-processors";
27
+ export {
28
+ getActiveSubProcessors,
29
+ getPlannedSubProcessors,
30
+ KUMIKO_SUB_PROCESSORS,
31
+ } from "./sub-processors";
@@ -0,0 +1,136 @@
1
+ // Zod-Schema fuer ComplianceProfileOverride mit .strict() rekursiv —
2
+ // Sprint 1.9 Z3.
3
+ //
4
+ // Vorher: set-profile.write.ts pruefte nur Top-Level-Keys gegen
5
+ // OVERRIDABLE_PROFILE_KEYS. Sub-Level-Tippfehler (z.B.
6
+ // `{ userRights: { weeks: 3 } }` statt `{ userRights: { gracePeriod: { days: 30 } } }`)
7
+ // kamen durch — der deepMerge spliced das nonsense ins effektive
8
+ // Profile, und ein Caller der `userRights.gracePeriod.days` liest
9
+ // crashed mit `undefined`.
10
+ //
11
+ // Lösung: Zod-Schema das die ComplianceProfile-Struktur in DeepPartial-
12
+ // Form abbildet, mit .strict() auf jedem Object damit unbekannte Keys
13
+ // werfen. Single source of truth wie OVERRIDABLE_PROFILE_KEYS, plus
14
+ // Sub-Level-Coverage.
15
+ //
16
+ // Identifikations-Felder (key, region, label, extends) sind NICHT im
17
+ // Schema — wer die overriden will, würde die Profile-Identitaet
18
+ // zerstören.
19
+
20
+ import { z } from "zod";
21
+
22
+ // DurationSpec: { days } | { hours } — strict bedeutet beide Forms
23
+ // muessen exakt 1 property haben (kein "{ days: 30, hours: 1 }").
24
+ const durationSpecSchema = z.union([
25
+ z.object({ days: z.number().int().nonnegative() }).strict(),
26
+ z.object({ hours: z.number().int().nonnegative() }).strict(),
27
+ ]);
28
+
29
+ // retention.* erlaubt zusaetzlich "months" und "years". Wieder eine
30
+ // strikte Disjunktion.
31
+ const auditRetentionSchema = z.union([
32
+ durationSpecSchema,
33
+ z.object({ months: z.number().int().nonnegative() }).strict(),
34
+ z.object({ years: z.number().int().nonnegative() }).strict(),
35
+ ]);
36
+
37
+ const authorityNotificationDeadlineSchema = z.union([
38
+ durationSpecSchema,
39
+ z.literal("as-soon-as-feasible"),
40
+ z.literal("in-most-expedient-time"),
41
+ z.literal("manual"),
42
+ ]);
43
+
44
+ const userNotificationRequiredSchema = z.union([
45
+ z.literal("if-high-risk"),
46
+ z.literal("if-real-risk-of-significant-harm"),
47
+ z.literal("if-serious-risk-of-injury"),
48
+ z.literal("always-if-encrypted-data-or-pii"),
49
+ z.literal("always-without-undue-delay"),
50
+ z.literal("manual"),
51
+ ]);
52
+
53
+ const userRightsOverrideSchema = z
54
+ .object({
55
+ gracePeriod: durationSpecSchema.optional(),
56
+ restrictionAllowed: z.boolean().optional(),
57
+ objectionAllowed: z.boolean().optional(),
58
+ portabilityFormat: z.array(z.string()).optional(),
59
+ auskunftFrist: durationSpecSchema.optional(),
60
+ employeeAccessRight: z.boolean().optional(),
61
+ explicitConsentForAutomatedDecision: z.boolean().optional(),
62
+ doNotSellRequired: z.boolean().optional(),
63
+ // Async-Export-Pipeline (S2.U3+U4) — TTL Compliance-relevant,
64
+ // Stale/Cleanup Operations-Settings.
65
+ exportDownloadTtl: durationSpecSchema.optional(),
66
+ exportStaleTimeoutMinutes: z.number().int().nonnegative().optional(),
67
+ exportStorageCleanupGraceHours: z.number().int().nonnegative().optional(),
68
+ })
69
+ .strict();
70
+
71
+ const notificationsOverrideSchema = z
72
+ .object({
73
+ languages: z.array(z.string()).optional(),
74
+ languageDefault: z.string().optional(),
75
+ mandatoryBreachNotification: z.boolean().optional(),
76
+ })
77
+ .strict();
78
+
79
+ const breachOverrideSchema = z
80
+ .object({
81
+ authorityNotificationDeadline: authorityNotificationDeadlineSchema.optional(),
82
+ authorityContact: z.string().optional(),
83
+ userNotificationRequired: userNotificationRequiredSchema.optional(),
84
+ worksCouncilNotificationRequired: z.boolean().optional(),
85
+ mandatoryRegisterOfBreaches: z.boolean().optional(),
86
+ })
87
+ .strict();
88
+
89
+ const auditLogOverrideSchema = z
90
+ .object({
91
+ retention: auditRetentionSchema.optional(),
92
+ reportFrequency: z
93
+ .union([
94
+ z.literal("quarterly"),
95
+ z.literal("yearly"),
96
+ z.literal("annual-required"),
97
+ z.literal("manual"),
98
+ ])
99
+ .optional(),
100
+ })
101
+ .strict();
102
+
103
+ const subProcessorOverrideSchema = z
104
+ .object({
105
+ consentRequired: z.boolean().optional(),
106
+ changeNotificationLeadDays: z.number().int().nonnegative().optional(),
107
+ mandatoryBaaWithSubProcessors: z.boolean().optional(),
108
+ worksCouncilApprovalRequired: z.boolean().optional(),
109
+ tierFilter: z.array(z.string()).optional(),
110
+ })
111
+ .strict();
112
+
113
+ const forgetDiscoveryOverrideSchema = z
114
+ .object({
115
+ enabled: z.boolean().optional(),
116
+ mode: z.union([z.literal("manual-redact"), z.literal("auto-redact-strict")]).optional(),
117
+ })
118
+ .strict();
119
+
120
+ /**
121
+ * Komplett-Schema fuer ComplianceProfileOverride. Alle Top-Level- UND
122
+ * Sub-Level-Keys sind gewhitelisted via .strict() — Tippfehler werfen
123
+ * sofort. Set-profile-Handler (Sprint 1.9 Z3) validiert das Override
124
+ * gegen dieses Schema vor dem Persist.
125
+ */
126
+ export const complianceProfileOverrideSchema = z
127
+ .object({
128
+ userRights: userRightsOverrideSchema.optional(),
129
+ notifications: notificationsOverrideSchema.optional(),
130
+ breach: breachOverrideSchema.optional(),
131
+ auditLog: auditLogOverrideSchema.optional(),
132
+ subProcessor: subProcessorOverrideSchema.optional(),
133
+ tenantDestroyGracePeriod: durationSpecSchema.optional(),
134
+ forgetDiscovery: forgetDiscoveryOverrideSchema.optional(),
135
+ })
136
+ .strict();