@cosmicdrift/kumiko-framework 0.362.0 → 0.364.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 (259) hide show
  1. package/AGENTS.md +307 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +443 -0
  4. package/agents/examples/anonymous-access.md +300 -0
  5. package/agents/examples/apex-landing.md +528 -0
  6. package/agents/examples/apex-surface-auth.md +355 -0
  7. package/agents/examples/api-worker-split.md +434 -0
  8. package/agents/examples/auth-claims.md +351 -0
  9. package/agents/examples/auth-foundation-providers.md +155 -0
  10. package/agents/examples/auth-mfa.md +323 -0
  11. package/agents/examples/basic-entity.md +420 -0
  12. package/agents/examples/boot-check.md +122 -0
  13. package/agents/examples/chat-channels.md +254 -0
  14. package/agents/examples/compliance-profiles-demo.md +307 -0
  15. package/agents/examples/cross-feature-events.md +317 -0
  16. package/agents/examples/crypto-shredding-hr.md +314 -0
  17. package/agents/examples/currencies-global.md +609 -0
  18. package/agents/examples/currencies-per-tenant.md +453 -0
  19. package/agents/examples/custom-fields-basic.md +314 -0
  20. package/agents/examples/custom-handlers.md +397 -0
  21. package/agents/examples/delivery-notifications.md +432 -0
  22. package/agents/examples/designer-demo.md +86 -0
  23. package/agents/examples/e2e-generator.md +211 -0
  24. package/agents/examples/embedded-entity-form.md +598 -0
  25. package/agents/examples/embedded.md +649 -0
  26. package/agents/examples/encrypted-tenant-config.md +405 -0
  27. package/agents/examples/error-contract.md +539 -0
  28. package/agents/examples/event-sourcing.md +1287 -0
  29. package/agents/examples/feature-toggles.md +263 -0
  30. package/agents/examples/field-access.md +271 -0
  31. package/agents/examples/files-post-processing.md +433 -0
  32. package/agents/examples/i18n.md +166 -0
  33. package/agents/examples/idempotency.md +179 -0
  34. package/agents/examples/inbound-mail-triage.md +315 -0
  35. package/agents/examples/lane-routing.md +246 -0
  36. package/agents/examples/ledger-banking.md +214 -0
  37. package/agents/examples/ledger-invoicing.md +235 -0
  38. package/agents/examples/legal-pages.md +393 -0
  39. package/agents/examples/lifecycle-hooks.md +368 -0
  40. package/agents/examples/managed-config.md +428 -0
  41. package/agents/examples/managed-pages.md +353 -0
  42. package/agents/examples/notes-history-basic.md +345 -0
  43. package/agents/examples/ownership.md +396 -0
  44. package/agents/examples/pdf-audit-export.md +270 -0
  45. package/agents/examples/pipeline-basics.md +792 -0
  46. package/agents/examples/public-share-token.md +351 -0
  47. package/agents/examples/rate-limiting.md +211 -0
  48. package/agents/examples/realtime-sse.md +187 -0
  49. package/agents/examples/record-detail-layout.md +297 -0
  50. package/agents/examples/reference-data.md +151 -0
  51. package/agents/examples/relations.md +280 -0
  52. package/agents/examples/screens-nav.md +353 -0
  53. package/agents/examples/search.md +233 -0
  54. package/agents/examples/secrets-demo.md +417 -0
  55. package/agents/examples/security-baseline.md +187 -0
  56. package/agents/examples/seed-migration.md +99 -0
  57. package/agents/examples/session-revocation.md +259 -0
  58. package/agents/examples/state-machine.md +920 -0
  59. package/agents/examples/tags-basic.md +401 -0
  60. package/agents/examples/tenant-isolation.md +173 -0
  61. package/agents/examples/tenant-settings.md +275 -0
  62. package/agents/examples/tier-admin.md +347 -0
  63. package/agents/examples/timezones.md +366 -0
  64. package/agents/examples/user-data-rights.md +410 -0
  65. package/agents/examples/user-profile.md +226 -0
  66. package/agents/examples/webhook-step.md +597 -0
  67. package/agents/examples/wizard-form.md +379 -0
  68. package/agents/examples/workflow-engine.md +1731 -0
  69. package/agents/examples/writeform-section.md +300 -0
  70. package/agents/examples.md +74 -0
  71. package/agents/features.md +768 -0
  72. package/agents/widgets.md +181 -0
  73. package/dist/api/auth-middleware.d.ts +2 -0
  74. package/dist/api/auth-middleware.js +14 -5
  75. package/dist/api/auth-routes.js +5 -3
  76. package/dist/api/extra-route.d.ts +3 -0
  77. package/dist/api/index.d.ts +2 -2
  78. package/dist/api/index.js +1 -1
  79. package/dist/api/redis-sse-broker.js +10 -8
  80. package/dist/api/routes.js +48 -19
  81. package/dist/api/server.d.ts +2 -1
  82. package/dist/api/server.js +18 -6
  83. package/dist/api/session-cookies.d.ts +1 -0
  84. package/dist/api/session-cookies.js +20 -5
  85. package/dist/api/sse-route.js +16 -1
  86. package/dist/bun-db/index.d.ts +1 -1
  87. package/dist/bun-db/index.js +1 -1
  88. package/dist/bun-db/query.d.ts +12 -5
  89. package/dist/bun-db/query.js +88 -94
  90. package/dist/crypto/kek-source.js +5 -3
  91. package/dist/crypto/pii-field-encryption.d.ts +4 -1
  92. package/dist/crypto/pii-field-encryption.js +2 -2
  93. package/dist/db/apply-entity-event.js +2 -2
  94. package/dist/db/collect-table-metas.js +11 -7
  95. package/dist/db/cursor.js +2 -1
  96. package/dist/db/dialect.d.ts +1 -0
  97. package/dist/db/dialect.js +28 -13
  98. package/dist/db/eagerload.js +2 -2
  99. package/dist/db/entity-table-from-registry.js +1 -1
  100. package/dist/db/entity-table-meta.js +5 -13
  101. package/dist/db/event-store-executor-context.d.ts +5 -6
  102. package/dist/db/event-store-executor-context.js +29 -27
  103. package/dist/db/event-store-executor-read.js +40 -25
  104. package/dist/db/event-store-executor-write.js +18 -20
  105. package/dist/db/index.d.ts +1 -1
  106. package/dist/db/index.js +1 -1
  107. package/dist/db/lock-guard.d.ts +4 -0
  108. package/dist/db/lock-guard.js +14 -0
  109. package/dist/db/migrate-generator.js +6 -4
  110. package/dist/db/migrate-runner.js +32 -35
  111. package/dist/db/parent-ref-clause.js +2 -2
  112. package/dist/db/queries/backfill-pii.js +11 -9
  113. package/dist/db/queries/es-ops.js +1 -1
  114. package/dist/db/queries/event-consumer.js +17 -17
  115. package/dist/db/queries/event-store-admin.js +6 -6
  116. package/dist/db/queries/event-store-transfer.js +2 -2
  117. package/dist/db/queries/event-store.js +2 -2
  118. package/dist/db/queries/pii-backfill-state.js +2 -2
  119. package/dist/db/queries/raw-sql.js +1 -1
  120. package/dist/db/queries/test-stack.js +1 -1
  121. package/dist/db/reference-data.js +3 -5
  122. package/dist/db/table-builder.js +6 -4
  123. package/dist/db/table-introspection.d.ts +16 -0
  124. package/dist/db/table-introspection.js +76 -0
  125. package/dist/db/tenant-db-runner.d.ts +4 -0
  126. package/dist/db/tenant-db-runner.js +17 -0
  127. package/dist/db/tenant-db.d.ts +5 -2
  128. package/dist/db/tenant-db.js +34 -14
  129. package/dist/derivatives/derivatives-context.js +1 -1
  130. package/dist/derivatives/variant-key.js +2 -1
  131. package/dist/engine/boot-validator/access-declarations.js +2 -2
  132. package/dist/engine/boot-validator/api-ext.js +3 -3
  133. package/dist/engine/boot-validator/boot-check.js +1 -1
  134. package/dist/engine/boot-validator/config-deps.js +13 -13
  135. package/dist/engine/boot-validator/detail-screens.js +1 -1
  136. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  137. package/dist/engine/boot-validator/entity-handler.js +84 -35
  138. package/dist/engine/boot-validator/entity-list-screens.js +3 -3
  139. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  140. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  141. package/dist/engine/boot-validator/nav.js +1 -1
  142. package/dist/engine/boot-validator/pii-retention.js +9 -9
  143. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  144. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  145. package/dist/engine/boot-validator/screens.d.ts +2 -1
  146. package/dist/engine/boot-validator/screens.js +155 -96
  147. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  148. package/dist/engine/changeset-changes.js +3 -3
  149. package/dist/engine/define-handler.d.ts +2 -2
  150. package/dist/engine/define-handler.js +11 -7
  151. package/dist/engine/embedded-derived.js +10 -6
  152. package/dist/engine/extensions/escape-hatch-usage.js +2 -1
  153. package/dist/engine/factories.js +5 -6
  154. package/dist/engine/feature-ast/extractors/events.js +43 -33
  155. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  156. package/dist/engine/feature-ast/patcher.d.ts +3 -3
  157. package/dist/engine/feature-ast/patcher.js +3 -3
  158. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
  159. package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
  160. package/dist/engine/feature-ast/patterns.d.ts +3 -3
  161. package/dist/engine/feature-ast/render.js +16 -5
  162. package/dist/engine/feature-changelog.d.ts +2 -1
  163. package/dist/engine/feature-changelog.js +8 -6
  164. package/dist/engine/feature-config-events-jobs.d.ts +17 -7
  165. package/dist/engine/feature-config-events-jobs.js +20 -5
  166. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  167. package/dist/engine/feature-entity-handlers.js +12 -3
  168. package/dist/engine/feature-ui-extensions.js +2 -3
  169. package/dist/engine/handler-field-references.d.ts +21 -2
  170. package/dist/engine/handler-field-references.js +55 -17
  171. package/dist/engine/index.d.ts +3 -2
  172. package/dist/engine/index.js +1 -0
  173. package/dist/engine/ownership.js +7 -20
  174. package/dist/engine/pattern-library/mixed-schemas.js +0 -1
  175. package/dist/engine/registry-ingest.d.ts +2 -1
  176. package/dist/engine/registry-ingest.js +85 -2
  177. package/dist/engine/registry-validate.d.ts +1 -0
  178. package/dist/engine/registry-validate.js +38 -8
  179. package/dist/engine/registry.js +2 -1
  180. package/dist/engine/schema-builder.js +3 -4
  181. package/dist/engine/soft-delete-cleanup.js +12 -4
  182. package/dist/engine/steps/call-feature.js +1 -3
  183. package/dist/engine/steps/retry.js +2 -1
  184. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  185. package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
  186. package/dist/engine/template-variables.d.ts +32 -0
  187. package/dist/engine/template-variables.js +80 -0
  188. package/dist/engine/tier-resolver-extension.js +2 -3
  189. package/dist/engine/types/index.d.ts +2 -2
  190. package/dist/engine/validate-projection-allowlist.js +12 -13
  191. package/dist/engine/where-rule-lint.js +8 -7
  192. package/dist/env/_zod-introspect.d.ts +1 -0
  193. package/dist/errors/classes.js +1 -0
  194. package/dist/errors/kumiko-error.d.ts +2 -0
  195. package/dist/errors/kumiko-error.js +2 -0
  196. package/dist/errors/reasons.d.ts +1 -0
  197. package/dist/errors/reasons.js +2 -0
  198. package/dist/errors/write-error-info.js +2 -0
  199. package/dist/event-store/tenant-db-append.js +2 -2
  200. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  201. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  202. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  203. package/dist/files/bind-pending-file-refs.js +3 -3
  204. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  205. package/dist/files/bind-referenced-files.write.js +22 -13
  206. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  207. package/dist/files/create-file-ref-from-bytes.js +67 -4
  208. package/dist/files/create-file-ref.d.ts +22 -0
  209. package/dist/files/create-file-ref.js +53 -25
  210. package/dist/files/file-handle.d.ts +1 -1
  211. package/dist/files/file-handle.js +8 -0
  212. package/dist/files/file-routes.js +2 -2
  213. package/dist/files/index.d.ts +2 -2
  214. package/dist/files/index.js +1 -1
  215. package/dist/files/load-save-file-refs.d.ts +10 -0
  216. package/dist/files/load-save-file-refs.js +47 -0
  217. package/dist/files/local-provider.js +2 -1
  218. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  219. package/dist/files/purge-expired-file-ref-bytes.js +45 -19
  220. package/dist/i18n/index.d.ts +1 -1
  221. package/dist/i18n/index.js +1 -1
  222. package/dist/i18n/mail-registry.d.ts +2 -0
  223. package/dist/i18n/mail-registry.js +7 -0
  224. package/dist/jobs/index.d.ts +1 -1
  225. package/dist/jobs/job-runner.d.ts +4 -1
  226. package/dist/jobs/job-runner.js +13 -9
  227. package/dist/observability/escape-hatch-report.d.ts +1 -1
  228. package/dist/observability/escape-hatch-report.js +17 -5
  229. package/dist/pipeline/cascade-handler.js +5 -2
  230. package/dist/pipeline/dispatch-batch.js +79 -31
  231. package/dist/pipeline/dispatch-query.js +2 -3
  232. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  233. package/dist/pipeline/dispatch-shared.js +142 -34
  234. package/dist/pipeline/dispatch-write.js +34 -22
  235. package/dist/pipeline/event-dispatcher-delivery.js +2 -3
  236. package/dist/pipeline/index.d.ts +2 -0
  237. package/dist/pipeline/index.js +2 -0
  238. package/dist/pipeline/member-read-only-transaction.js +1 -1
  239. package/dist/pipeline/notify-scope.d.ts +13 -0
  240. package/dist/pipeline/notify-scope.js +32 -0
  241. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  242. package/dist/pipeline/pre-transaction-reservations.js +28 -9
  243. package/dist/pipeline/system-hooks.js +2 -1
  244. package/dist/pipeline/system-identity-switch.js +2 -2
  245. package/dist/rate-limit/bucket.d.ts +3 -3
  246. package/dist/rate-limit/bucket.js +14 -4
  247. package/dist/redis/cache-sync-bus.js +9 -3
  248. package/dist/search/meilisearch-adapter.js +39 -13
  249. package/dist/secrets/stored-envelope.js +7 -7
  250. package/dist/stack/push-entity-projection-tables.js +2 -3
  251. package/dist/stack/table-helpers.js +10 -26
  252. package/dist/stack/test-stack.js +2 -5
  253. package/dist/testing/multipart-helper.d.ts +0 -6
  254. package/dist/testing/multipart-helper.js +5 -5
  255. package/dist/testing/without-ambient-temporal.js +4 -4
  256. package/dist/utils/is-plain-object.d.ts +1 -2
  257. package/dist/utils/is-plain-object.js +1 -4
  258. package/package.json +7 -5
  259. package/src/changes.json +251 -8
@@ -0,0 +1,410 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/user-data-rights. Do not edit. -->
2
+
3
+ # user-data-rights
4
+
5
+ DSGVO-compliant in 5 LOC: r.useExtension(EXT_USER_DATA, ...) haengt eine Domain an Forget+Export-Pipeline. Forget-Cron, Export-ZIP, Magic-Link kommen alle aus user-data-rights.
6
+
7
+ ## README
8
+
9
+ # user-data-rights
10
+
11
+ GDPR-compliant domain wiring in ~5 lines: `r.useExtension(EXT_USER_DATA, …)`
12
+ hooks any entity into the forget cron, export ZIP, and magic-link pipeline.
13
+ You write two hooks per entity — the framework runs the rest.
14
+
15
+ ## What it shows
16
+
17
+ A minimal notes domain with GDPR Art. 15+17+20 integrated without hand-written
18
+ forget logic:
19
+
20
+ ```ts illustration
21
+ defineFeature("notes", (r) => {
22
+ r.requires("user-data-rights");
23
+ r.entity("note", noteEntity);
24
+
25
+ r.useExtension(EXT_USER_DATA, "note", {
26
+ export: async (ctx) => ({
27
+ entity: "note",
28
+ rows: await ctx.db.select().from(notesTable)
29
+ .where(and(eq(notesTable.tenantId, ctx.tenantId),
30
+ eq(notesTable.authorId, ctx.userId))),
31
+ }),
32
+ delete: async (ctx, strategy) => {
33
+ const where = and(eq(notesTable.tenantId, ctx.tenantId),
34
+ eq(notesTable.authorId, ctx.userId));
35
+ if (strategy === "anonymize") {
36
+ await ctx.db.update(notesTable).set({ authorId: null }).where(where);
37
+ } else {
38
+ await ctx.db.delete(notesTable).where(where);
39
+ }
40
+ },
41
+ });
42
+ });
43
+ ```
44
+
45
+ ## What runs automatically
46
+
47
+ | Article | What happens | Trigger |
48
+ |---|---|---|
49
+ | Art. 15 + 20 | Notes snippet in export ZIP + magic-link email | `runUserExport` (via `request-export`) |
50
+ | Art. 17 | Notes deleted or anonymized after grace | `runForgetCleanup` cron |
51
+ | Art. 18 | Restriction blocks login while active | Auth middleware |
52
+ | Operator | Brute-force detection on magic-link endpoint | Edge rate-limit + audit log |
53
+
54
+ ## Strategy-aware delete
55
+
56
+ `retention.policyFor("note")` resolves per entity:
57
+
58
+ | Profile | Strategy | Effect |
59
+ |---|---|---|
60
+ | `eu-dsgvo` (default) | `delete` | Notes hard-deleted |
61
+ | `de-hr-dsgvo-hgb` | `anonymize` for HR entities | `authorId=null`, row kept for multi-user refs |
62
+
63
+ The integration test pins both paths.
64
+
65
+ ## Flow
66
+
67
+ 1. User creates notes → rows stored with `authorId`.
68
+ 2. `request-export` queues job → `runUserExport` bundles user + note rows.
69
+ 3. `request-deletion` → grace period from compliance profile.
70
+ 4. `runForgetCleanup` calls your `delete` hook with resolved strategy.
71
+
72
+ ## Tests
73
+
74
+ Needs a running Postgres and `TEST_DATABASE_URL` set (e.g. `postgres://postgres:postgres@127.0.0.1:5432/postgres`, see `demo/.env.example`).
75
+
76
+ ```bash
77
+ bun test src/__tests__/feature.integration.test.ts
78
+ ```
79
+
80
+ Step-by-step proof:
81
+
82
+ 1. Alice creates 2 notes → export bundle includes note snippet
83
+ 2. Deletion + expired grace → `runForgetCleanup` removes all notes
84
+ 3. Strategy `anonymize` → `authorId=null`, title/body remain
85
+
86
+ ## Full app vs this recipe
87
+
88
+ | | This recipe | [user-data-rights-demo](/en/samples/apps-user-data-rights-demo/) |
89
+ |---|---|---|
90
+ | Scope | One hook pattern | Runnable app + todos + files |
91
+ | Boot | Integration test only | `bun dev` on port 4291 |
92
+
93
+ ## Dependencies
94
+
95
+ ```ts illustration
96
+ import { createUserDataRightsFeature } from "@cosmicdrift/kumiko-bundled-features/user-data-rights";
97
+ import { createUserDataRightsDefaultsFeature } from "@cosmicdrift/kumiko-bundled-features/user-data-rights-defaults";
98
+ import { createDataRetentionFeature } from "@cosmicdrift/kumiko-bundled-features/data-retention";
99
+ import { createComplianceProfilesFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
100
+
101
+ export const features = [
102
+ createDataRetentionFeature(),
103
+ createComplianceProfilesFeature(),
104
+ createUserDataRightsFeature(),
105
+ createUserDataRightsDefaultsFeature(),
106
+ notesFeature,
107
+ ];
108
+ ```
109
+
110
+ ## Source files
111
+
112
+ ### src/__tests__/feature.integration.test.ts
113
+
114
+ ```ts
115
+ // User-Data-Rights Recipe — Integration-Test.
116
+ //
117
+ // Pinst dass die zwei Hooks aus feature.ts genau das tun was die Recipe-
118
+ // README verspricht:
119
+ // 1. Notes des Users landen via export-Hook im Bundle-Snippet.
120
+ // 2. Forget-Cleanup-Cron entfernt die Notes via delete-Hook (strategy
121
+ // "delete", default).
122
+ // 3. Strategy "anonymize" (HR-Override) setzt authorId=null statt zu
123
+ // löschen — Row bleibt für Multi-User-Refs intakt.
124
+
125
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
126
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
127
+ import {
128
+ createComplianceProfilesFeature,
129
+ tenantComplianceProfileEntity,
130
+ } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
131
+ import {
132
+ createDataRetentionFeature,
133
+ tenantRetentionOverrideEntity,
134
+ } from "@cosmicdrift/kumiko-bundled-features/data-retention";
135
+ import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
136
+ import {
137
+ createUserFeature,
138
+ USER_STATUS,
139
+ userEntity,
140
+ userTable,
141
+ } from "@cosmicdrift/kumiko-bundled-features/user";
142
+ import {
143
+ createUserDataRightsFeature,
144
+ runForgetCleanup,
145
+ runUserExport,
146
+ } from "@cosmicdrift/kumiko-bundled-features/user-data-rights";
147
+ import { asRawClient, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
148
+ import { EXT_USER_DATA } from "@cosmicdrift/kumiko-framework/engine";
149
+ import {
150
+ setupTestStack,
151
+ type TestStack,
152
+ unsafeCreateEntityTable,
153
+ } from "@cosmicdrift/kumiko-framework/stack";
154
+ import { seedRow } from "@cosmicdrift/kumiko-framework/testing";
155
+ import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
156
+ import { noteEntity, notesFeature, notesTable } from "../feature";
157
+
158
+ let stack: TestStack;
159
+
160
+ const TENANT_A = "00000000-0000-4000-8000-00000000000a";
161
+ const TENANT_SYSTEM = "00000000-0000-4000-8000-000000000001";
162
+ const ALICE_ID = "11111111-1111-4111-8111-000000000001";
163
+
164
+ type Instant = InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
165
+ const NOW = (): Instant => getTemporal().Now.instant();
166
+ const PAST = (): Instant => getTemporal().Instant.fromEpochMilliseconds(Date.now() - 60_000);
167
+
168
+ beforeAll(async () => {
169
+ stack = await setupTestStack({
170
+ features: [
171
+ createUserFeature(),
172
+ authFoundationFeature,
173
+ createSessionsFeature(),
174
+ createDataRetentionFeature(),
175
+ createComplianceProfilesFeature(),
176
+ createUserDataRightsFeature(),
177
+ notesFeature,
178
+ ],
179
+ });
180
+ await unsafeCreateEntityTable(stack.db, userEntity);
181
+ await unsafeCreateEntityTable(stack.db, noteEntity);
182
+ await unsafeCreateEntityTable(stack.db, tenantRetentionOverrideEntity);
183
+ await unsafeCreateEntityTable(stack.db, tenantComplianceProfileEntity);
184
+ await asRawClient(stack.db).unsafe(`
185
+ CREATE TABLE IF NOT EXISTS read_tenant_memberships (
186
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
187
+ tenant_id UUID NOT NULL,
188
+ user_id TEXT NOT NULL,
189
+ version INTEGER NOT NULL DEFAULT 0,
190
+ inserted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
191
+ modified_at TIMESTAMPTZ NOT NULL DEFAULT now(),
192
+ inserted_by_id TEXT,
193
+ modified_by_id TEXT,
194
+ is_deleted BOOLEAN NOT NULL DEFAULT false,
195
+ deleted_at TIMESTAMPTZ,
196
+ deleted_by_id TEXT,
197
+ roles TEXT NOT NULL DEFAULT '[]',
198
+ UNIQUE(user_id, tenant_id)
199
+ )
200
+ `);
201
+ });
202
+
203
+ afterAll(async () => {
204
+ await stack.cleanup();
205
+ });
206
+
207
+ beforeEach(async () => {
208
+ await asRawClient(stack.db).unsafe(`DELETE FROM store_notes`);
209
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
210
+ await asRawClient(stack.db).unsafe(`DELETE FROM read_tenant_memberships`);
211
+ });
212
+
213
+ async function seedAlice(opts: { gracePeriodEnd?: Instant; status?: string } = {}): Promise<void> {
214
+ await seedRow(stack.db, userTable, {
215
+ id: ALICE_ID,
216
+ tenantId: TENANT_SYSTEM,
217
+ email: "alice@recipe.test",
218
+ passwordHash: "h",
219
+ displayName: "Alice",
220
+ locale: "de",
221
+ emailVerified: true,
222
+ roles: '["Member"]',
223
+ status: opts.status ?? USER_STATUS.Active,
224
+ gracePeriodEnd: opts.gracePeriodEnd ?? null,
225
+ });
226
+ await asRawClient(stack.db).unsafe(
227
+ `
228
+ INSERT INTO read_tenant_memberships (tenant_id, user_id, roles)
229
+ VALUES ($1, $2, '["Member"]')
230
+ ON CONFLICT DO NOTHING
231
+ `,
232
+ [TENANT_A, ALICE_ID],
233
+ );
234
+ }
235
+
236
+ async function seedNote(id: string, title: string): Promise<void> {
237
+ await insertOne(stack.db, notesTable, {
238
+ id,
239
+ tenantId: TENANT_A,
240
+ authorId: ALICE_ID,
241
+ title,
242
+ body: `body for ${title}`,
243
+ });
244
+ }
245
+
246
+ describe("user-data-rights recipe :: EXT_USER_DATA-Hooks integrieren Notes-Domain", () => {
247
+ test("Export-Bundle enthaelt Note-Snippet via Hook", async () => {
248
+ await seedAlice();
249
+ await seedNote("aaaaaaaa-aaaa-4aaa-8aaa-000000000001", "first");
250
+ await seedNote("aaaaaaaa-aaaa-4aaa-8aaa-000000000002", "second");
251
+
252
+ const bundle = await runUserExport({
253
+ db: stack.db,
254
+ registry: stack.registry,
255
+ userId: ALICE_ID,
256
+ now: NOW(),
257
+ });
258
+
259
+ const tenantA = bundle.tenants.find((t) => t.tenantId === TENANT_A);
260
+ const noteSnippet = tenantA?.entities.find((e) => e.entity === "note");
261
+ expect(noteSnippet?.rows).toHaveLength(2);
262
+ expect((noteSnippet?.rows ?? []).map((r) => String(r["title"])).sort()).toEqual([
263
+ "first",
264
+ "second",
265
+ ]);
266
+ });
267
+
268
+ test("Forget-Cron strategy=delete entfernt Notes (Default-Pfad)", async () => {
269
+ await seedAlice({ status: USER_STATUS.DeletionRequested, gracePeriodEnd: PAST() });
270
+ await seedNote("aaaaaaaa-aaaa-4aaa-8aaa-000000000003", "to-delete");
271
+
272
+ const result = await runForgetCleanup({
273
+ db: stack.db,
274
+ registry: stack.registry,
275
+ now: NOW(),
276
+ });
277
+ expect(result.processedUserIds).toContain(ALICE_ID);
278
+
279
+ const remaining = await asRawClient(stack.db).unsafe(`SELECT id FROM store_notes`);
280
+ expect(remaining.length).toBe(0);
281
+ });
282
+
283
+ test("Strategy=anonymize: authorId=null, Row bleibt (HR-Compliance-Pfad)", async () => {
284
+ await seedAlice();
285
+ await seedNote("aaaaaaaa-aaaa-4aaa-8aaa-000000000004", "stays-anonymous");
286
+
287
+ const usage = stack.registry
288
+ .getExtensionUsages(EXT_USER_DATA)
289
+ .find((u) => u.entityName === "note");
290
+ const hooks = usage?.options as
291
+ | {
292
+ delete?: (
293
+ ctx: { db: typeof stack.db; tenantId: string; userId: string },
294
+ strategy: "delete" | "anonymize",
295
+ ) => Promise<void>;
296
+ }
297
+ | undefined;
298
+ await hooks?.delete?.({ db: stack.db, tenantId: TENANT_A, userId: ALICE_ID }, "anonymize");
299
+
300
+ const after = (await asRawClient(stack.db).unsafe(
301
+ `SELECT title, author_id FROM store_notes`,
302
+ )) as Array<{ title: string; author_id: string | null }>;
303
+ expect(after).toHaveLength(1);
304
+ expect(after[0]?.author_id).toBeNull();
305
+ expect(after[0]?.title).toBe("stays-anonymous");
306
+ });
307
+ });
308
+ ```
309
+
310
+ ### src/feature.ts
311
+
312
+ ```ts
313
+ // User-Data-Rights — Recipe
314
+ //
315
+ // Wie eine App-Domain DSGVO-Pipeline integriert: pro Entity einen
316
+ // (export, delete)-Hook ueber EXT_USER_DATA. Forget-Cron, Export-ZIP-Bau
317
+ // und Magic-Link-Versand kommen vollstaendig aus user-data-rights —
318
+ // App-Author schreibt nur die zwei Hooks pro Entity.
319
+ //
320
+ // Demo-Domain: minimaler Note-Service. Note hat (id, tenantId, authorId,
321
+ // title, body). Pinst:
322
+ // 1. Hook export liefert Note-Rows als JSON-Snippet ins Export-Bundle.
323
+ // 2. Hook delete entfernt Note-Rows beim Forget-Cleanup-Cron.
324
+ //
325
+ // Was nicht im Recipe ist (siehe samples/apps/user-data-rights-demo
326
+ // fuer eine vollstaendige App):
327
+ // - Strategy-aware delete (anonymize vs hardDelete)
328
+ // - HTTP-Endpoints fuer create/list
329
+ // - Compliance-Profile-Wiring + Cron-Scheduling
330
+
331
+ import { deleteMany, selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
332
+ import { deriveEntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
333
+ import {
334
+ createEntity,
335
+ createTextField,
336
+ defineFeature,
337
+ EXT_USER_DATA,
338
+ type UserDataDeleteHook,
339
+ type UserDataExportHook,
340
+ } from "@cosmicdrift/kumiko-framework/engine";
341
+
342
+ export const noteEntity = createEntity({
343
+ table: "store_notes",
344
+ idType: "uuid",
345
+ fields: {
346
+ authorId: createTextField({ personal: "ref" }),
347
+ title: createTextField({
348
+ personal: false,
349
+ reason: "is_business_data",
350
+ required: true,
351
+ maxLength: 200,
352
+ }),
353
+ // Plaintext on purpose: store_notes is an unmanaged direct-write store, so the
354
+ // PII executor never encrypts it. Art. 17 runs through the EXT_USER_DATA
355
+ // hook below instead (deletes the author's rows, or unlinks them on anonymize).
356
+ body: createTextField({
357
+ personal: false,
358
+ reason: "covered_by_user_data_hook",
359
+ maxLength: 4000,
360
+ }),
361
+ },
362
+ });
363
+
364
+ // Plain EntityTableMeta, NOT a branded EntityTable: store_notes is a deliberate
365
+ // unmanaged direct-write store (r.storeTable below), so the forget hook may
366
+ // updateMany/deleteMany it directly — the meta carries no executor-only brand.
367
+ export const notesTable = deriveEntityTableMeta("note", noteEntity, { source: "unmanaged" });
368
+
369
+ export const notesFeature = defineFeature("notes", (r) => {
370
+ r.requires("user-data-rights");
371
+ // store_notes is a direct-write store: the forget hook below `updateMany`/
372
+ // `deleteMany`s rows WITHOUT emitting lifecycle events. r.entity would make
373
+ // it a rebuildable implicit projection whose replay finds zero note events
374
+ // and wipes the table (or un-forgets anonymized rows) on the next rebuild
375
+ // (#498). r.storeTable keeps the DDL, opts out of implicit rebuild.
376
+ r.storeTable(notesTable, {
377
+ reason: "read_side.notes_direct_write",
378
+ });
379
+
380
+ const exportNotes: UserDataExportHook = async (ctx) => {
381
+ const rows = await selectMany(ctx.db, notesTable, {
382
+ tenantId: ctx.tenantId,
383
+ authorId: ctx.userId,
384
+ });
385
+ if (rows.length === 0) return null;
386
+ return {
387
+ entity: "note",
388
+ rows: rows.map((row) => ({
389
+ id: String(row.id),
390
+ title: row.title ?? "",
391
+ body: row.body ?? "",
392
+ })),
393
+ };
394
+ };
395
+
396
+ const deleteNotes: UserDataDeleteHook = async (ctx, strategy) => {
397
+ const where = { tenantId: ctx.tenantId, authorId: ctx.userId };
398
+ if (strategy === "anonymize") {
399
+ await updateMany(ctx.db, notesTable, { authorId: null }, where);
400
+ } else {
401
+ await deleteMany(ctx.db, notesTable, where);
402
+ }
403
+ };
404
+
405
+ r.useExtension(EXT_USER_DATA, "note", {
406
+ export: exportNotes,
407
+ delete: deleteNotes,
408
+ });
409
+ });
410
+ ```
@@ -0,0 +1,226 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/user-profile. Do not edit. -->
2
+
3
+ # user-profile
4
+
5
+ user-profile bundled feature — Self-Service-Kontoseite (Passwort/E-Mail ändern, Konto löschen) als App-Screen verdrahten.
6
+
7
+ ## README
8
+
9
+ # user-profile
10
+
11
+ Self-service account page as a bundled feature: change password, change email
12
+ (with re-auth + verification reset), and request or cancel account deletion
13
+ (with grace period from `user-data-rights`).
14
+
15
+ The bundled feature ships the `profile` screen itself (a declarative
16
+ `projectionDetail`, fw#2312) plus handlers and i18n. **Your app** only points
17
+ a nav entry at it and registers the two extension-section components
18
+ (change-password, change-email — re-auth flows a declarative action can't
19
+ express) — this recipe shows the wiring.
20
+
21
+ ## What it shows
22
+
23
+ - **`user-profile:write:change-email`** — re-authenticated email change;
24
+ resets `emailVerified` until the user confirms the new address.
25
+ - **`auth-email-password:write:change-password`** — used from the same
26
+ screen; lives on `auth-email-password`, not duplicated in user-profile.
27
+ - **Account deletion** — `user-data-rights` handlers for request / cancel
28
+ deletion; grace period comes from the tenant's compliance profile.
29
+ - **App-side nav wiring only** — `r.nav({ screen: "user-profile:screen:profile", ... })`;
30
+ no `r.screen()` registration needed, the bundled feature owns the screen.
31
+ - **Full require chain** — `user` → `tenant` → `auth-email-password` →
32
+ `user-data-rights` (which pulls `data-retention`, `compliance-profiles`,
33
+ `sessions`). `composeAccountApp()` mounts everything boot needs.
34
+
35
+ ## Feature composition
36
+
37
+ ```
38
+ user → cross-tenant identity (email, roles, status)
39
+ tenant → memberships + tenant-scoped roles
40
+ auth-email-password → login + change-password handlers
41
+ data-retention → retention policies (via user-data-rights)
42
+ compliance-profiles → region profiles for grace periods
43
+ sessions → revocable JWTs (via user-data-rights chain)
44
+ user-data-rights → deletion request / export pipeline
45
+ user-profile → declarative `profile` screen + change-email handler
46
+ account → this recipe: nav wiring only
47
+ ```
48
+
49
+ ## Client wiring
50
+
51
+ Register the two extension-section components once at app boot (the screen
52
+ itself needs no client registration):
53
+
54
+ ```tsx illustration
55
+ import { userProfileClient } from "@cosmicdrift/kumiko-bundled-features/user-profile/web";
56
+ import { emailPasswordClient } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";
57
+ import { createKumikoApp } from "@cosmicdrift/kumiko-renderer-web";
58
+
59
+ createKumikoApp({
60
+ clientFeatures: [emailPasswordClient(), userProfileClient()],
61
+ // ... schema, nav from server features
62
+ });
63
+ ```
64
+
65
+ ## Flow
66
+
67
+ 1. User opens the profile nav entry → renderer loads the declarative `profile`
68
+ screen.
69
+ 2. Password change dispatches `auth-email-password:write:change-password`.
70
+ 3. Email change dispatches `user-profile:write:change-email` (re-auth required).
71
+ 4. Delete account dispatches `user-data-rights:write:request-deletion`; cancel
72
+ uses the matching cancel handler during the grace window.
73
+
74
+ ## Tests
75
+
76
+ Boot validation only — no DB/HTTP in this recipe (HTTP proof lives in the
77
+ bundled-feature integration tests):
78
+
79
+ ```bash
80
+ bun test src/__tests__/feature.test.ts
81
+ ```
82
+
83
+ The test asserts `validateBoot(composeAccountApp())` passes, the profile
84
+ screen/nav are registered, and the documented change-email qualified name
85
+ matches `UserProfileHandlers.changeEmail`.
86
+
87
+ ## Related samples
88
+
89
+ - [apex-surface-auth](/en/samples/recipes-apex-surface-auth/) — public login/
90
+ signup on the marketing apex (no admin nav).
91
+ - [user-data-rights](/en/samples/recipes-user-data-rights/) — `EXT_USER_DATA`
92
+ hooks for export/forget on your domain entities.
93
+ - [apps-user-data-rights-demo](/en/samples/apps-user-data-rights-demo/) —
94
+ full runnable app with todos + export ZIP.
95
+
96
+ ## Source files
97
+
98
+ ### src/__tests__/feature.test.ts
99
+
100
+ ```ts
101
+ // Boot-Validation der vollen user-profile-Komposition: Require-Kette
102
+ // erfüllt, Screen + Nav registriert, change-email-Handler unter der
103
+ // dokumentierten QN. Kein DB/HTTP nötig — der HTTP-Beweis lebt im
104
+ // bundled-feature-Test (change-email.integration.test.ts).
105
+
106
+ import { describe, expect, test } from "bun:test";
107
+ import { UserProfileHandlers } from "@cosmicdrift/kumiko-bundled-features/user-profile";
108
+ import {
109
+ createRegistry,
110
+ validateBoot as validateBootRaw,
111
+ } from "@cosmicdrift/kumiko-framework/engine";
112
+ import { withBootValidatorFixture } from "@cosmicdrift/kumiko-framework/testing";
113
+ import { composeAccountApp } from "../feature";
114
+
115
+ function validateBoot(features: Parameters<typeof validateBootRaw>[0]): void {
116
+ validateBootRaw(withBootValidatorFixture(features));
117
+ }
118
+
119
+ const features = composeAccountApp();
120
+
121
+ describe("user-profile recipe — boot validation", () => {
122
+ test("validateBoot accepts the full composition (require chain satisfied)", () => {
123
+ expect(() => validateBoot(features)).not.toThrow();
124
+ });
125
+
126
+ test("profile screen + nav registered with qualified ids", () => {
127
+ const registry = createRegistry(features);
128
+ expect(registry.getScreen("user-profile:screen:profile")?.id).toBe(
129
+ "user-profile:screen:profile",
130
+ );
131
+ });
132
+
133
+ test("user-profile deklariert seine Require-Kette (Manifest-Quelle)", () => {
134
+ const userProfile = features.find((f) => f.name === "user-profile");
135
+ if (!userProfile) throw new Error("user-profile feature missing in composition");
136
+ expect(userProfile.requires).toEqual([
137
+ "user",
138
+ "auth-email-password",
139
+ "user-data-rights",
140
+ "user-data-rights-defaults",
141
+ ]);
142
+ });
143
+
144
+ test("change-email QN entspricht der dokumentierten Konstante", () => {
145
+ expect(UserProfileHandlers.changeEmail).toBe("user-profile:write:change-email");
146
+ });
147
+ });
148
+ ```
149
+
150
+ ### src/feature.ts
151
+
152
+ ```ts
153
+ // user-profile Recipe — shows the app-side wiring for the self-service
154
+ // account page: the bundled feature ships the screen itself (fw#2312:
155
+ // declarative `projectionDetail`, id "profile") including handler + i18n;
156
+ // the app only has to navigate to it, no own `r.screen()` registration
157
+ // anymore (unlike before #2312 — see user-data-rights' recipe/demo pattern:
158
+ // `r.nav({ screen: "<feature>:screen:<id>", ... })`).
159
+ //
160
+ // Client-side, the app only registers the two extension-section components
161
+ // (change-password/change-email stay React, re-auth):
162
+ // import { userProfileClient } from
163
+ // "@cosmicdrift/kumiko-bundled-features/user-profile/web";
164
+ // createKumikoApp({
165
+ // clientFeatures: [emailPasswordClient(), userProfileClient()],
166
+ // })
167
+
168
+ import { createAuthEmailPasswordFeature } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
169
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
170
+ import { createComplianceProfilesFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
171
+ import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
172
+ import { createDataRetentionFeature } from "@cosmicdrift/kumiko-bundled-features/data-retention";
173
+ import { createFilesFeature } from "@cosmicdrift/kumiko-bundled-features/files";
174
+ import { createPersonalAccessTokensFeature } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
175
+ import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
176
+ import { createTenantFeature } from "@cosmicdrift/kumiko-bundled-features/tenant";
177
+ import { createUserFeature } from "@cosmicdrift/kumiko-bundled-features/user";
178
+ import { createUserDataRightsFeature } from "@cosmicdrift/kumiko-bundled-features/user-data-rights";
179
+ import { createUserDataRightsDefaultsFeature } from "@cosmicdrift/kumiko-bundled-features/user-data-rights-defaults";
180
+ import { createUserProfileFeature } from "@cosmicdrift/kumiko-bundled-features/user-profile";
181
+ import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
182
+
183
+ export function createAccountFeature(): FeatureDefinition {
184
+ return defineFeature("account", (r) => {
185
+ r.describe(
186
+ "App-side wiring for the user-profile bundled feature: just the nav " +
187
+ "entry — user-profile registers the `profile` screen itself " +
188
+ "(fw#2312 declarative projectionDetail).",
189
+ );
190
+ r.requires("user-profile");
191
+
192
+ r.nav({
193
+ id: "profile",
194
+ label: "account:nav.profile",
195
+ icon: "user",
196
+ screen: "user-profile:screen:profile",
197
+ order: 90,
198
+ });
199
+
200
+ return {};
201
+ });
202
+ }
203
+
204
+ /** Volle Feature-Komposition inkl. der user-profile-Require-Kette
205
+ * (user-data-rights → data-retention + compliance-profiles + sessions). */
206
+ export function composeAccountApp(): FeatureDefinition[] {
207
+ return [
208
+ createConfigFeature(),
209
+ createUserFeature(),
210
+ createTenantFeature(),
211
+ createAuthEmailPasswordFeature(),
212
+ createDataRetentionFeature(),
213
+ createComplianceProfilesFeature(),
214
+ authFoundationFeature,
215
+ createPersonalAccessTokensFeature({ scopes: {} }),
216
+ createSessionsFeature(),
217
+ createFilesFeature(),
218
+ createUserDataRightsFeature(),
219
+ // registers the default export/erase hooks for core PII entities (user,
220
+ // fileRef, folder) so the GDPR boot gate (V3) is satisfied for the stack.
221
+ createUserDataRightsDefaultsFeature(),
222
+ createUserProfileFeature(),
223
+ createAccountFeature(),
224
+ ];
225
+ }
226
+ ```