@cosmicdrift/kumiko-framework 0.363.0 → 0.365.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 (281) hide show
  1. package/AGENTS.md +338 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +446 -0
  4. package/agents/examples/anonymous-access.md +288 -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 +319 -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 +315 -0
  20. package/agents/examples/custom-handlers.md +444 -0
  21. package/agents/examples/dashboard-stats.md +229 -0
  22. package/agents/examples/delivery-notifications.md +432 -0
  23. package/agents/examples/designer-demo.md +86 -0
  24. package/agents/examples/e2e-generator.md +211 -0
  25. package/agents/examples/embedded-entity-form.md +598 -0
  26. package/agents/examples/embedded.md +649 -0
  27. package/agents/examples/encrypted-tenant-config.md +414 -0
  28. package/agents/examples/error-contract.md +535 -0
  29. package/agents/examples/event-sourcing.md +1287 -0
  30. package/agents/examples/feature-toggles.md +274 -0
  31. package/agents/examples/field-access.md +271 -0
  32. package/agents/examples/files-post-processing.md +433 -0
  33. package/agents/examples/i18n.md +166 -0
  34. package/agents/examples/idempotency.md +179 -0
  35. package/agents/examples/inbound-mail-triage.md +315 -0
  36. package/agents/examples/lane-routing.md +246 -0
  37. package/agents/examples/ledger-banking.md +215 -0
  38. package/agents/examples/ledger-invoicing.md +236 -0
  39. package/agents/examples/legal-pages.md +393 -0
  40. package/agents/examples/lifecycle-hooks.md +368 -0
  41. package/agents/examples/managed-config.md +428 -0
  42. package/agents/examples/managed-pages.md +353 -0
  43. package/agents/examples/notes-history-basic.md +346 -0
  44. package/agents/examples/ownership.md +404 -0
  45. package/agents/examples/pdf-audit-export.md +306 -0
  46. package/agents/examples/pipeline-basics.md +792 -0
  47. package/agents/examples/public-share-token.md +351 -0
  48. package/agents/examples/rate-limiting.md +211 -0
  49. package/agents/examples/realtime-sse.md +187 -0
  50. package/agents/examples/record-detail-layout.md +297 -0
  51. package/agents/examples/reference-data.md +151 -0
  52. package/agents/examples/relations.md +280 -0
  53. package/agents/examples/screens-nav.md +353 -0
  54. package/agents/examples/search.md +233 -0
  55. package/agents/examples/secrets-demo.md +417 -0
  56. package/agents/examples/security-baseline.md +187 -0
  57. package/agents/examples/seed-migration.md +99 -0
  58. package/agents/examples/session-revocation.md +250 -0
  59. package/agents/examples/state-machine.md +920 -0
  60. package/agents/examples/tags-basic.md +401 -0
  61. package/agents/examples/tenant-isolation.md +173 -0
  62. package/agents/examples/tenant-settings.md +275 -0
  63. package/agents/examples/tier-admin.md +347 -0
  64. package/agents/examples/timezones.md +366 -0
  65. package/agents/examples/user-data-rights.md +418 -0
  66. package/agents/examples/user-profile.md +226 -0
  67. package/agents/examples/webhook-step.md +597 -0
  68. package/agents/examples/wizard-form.md +379 -0
  69. package/agents/examples/workflow-engine.md +1731 -0
  70. package/agents/examples/writeform-section.md +300 -0
  71. package/agents/examples.md +75 -0
  72. package/agents/features.md +768 -0
  73. package/agents/widgets.md +181 -0
  74. package/dist/api/auth-middleware.d.ts +2 -0
  75. package/dist/api/auth-middleware.js +7 -0
  76. package/dist/api/auth-routes.d.ts +0 -1
  77. package/dist/api/auth-routes.js +49 -46
  78. package/dist/api/extra-route.d.ts +3 -0
  79. package/dist/api/index.d.ts +2 -2
  80. package/dist/api/index.js +1 -1
  81. package/dist/api/routes.js +46 -17
  82. package/dist/api/server.d.ts +4 -3
  83. package/dist/api/server.js +39 -10
  84. package/dist/api/session-cookies.d.ts +1 -0
  85. package/dist/api/session-cookies.js +20 -5
  86. package/dist/api/sse-route.js +16 -1
  87. package/dist/bun-db/connection.js +3 -16
  88. package/dist/bun-db/index.d.ts +1 -1
  89. package/dist/bun-db/index.js +1 -1
  90. package/dist/bun-db/query.d.ts +15 -3
  91. package/dist/bun-db/query.js +76 -24
  92. package/dist/crypto/owner-transition-rekey.d.ts +1 -0
  93. package/dist/crypto/owner-transition-rekey.js +7 -3
  94. package/dist/crypto/pii-field-encryption.d.ts +1 -0
  95. package/dist/crypto/pii-field-encryption.js +9 -2
  96. package/dist/crypto/subject-resolver.d.ts +1 -0
  97. package/dist/crypto/subject-resolver.js +7 -0
  98. package/dist/db/api.d.ts +4 -3
  99. package/dist/db/apply-entity-event.js +26 -2
  100. package/dist/db/blind-index-cleanup.js +2 -1
  101. package/dist/db/bun-provider.js +9 -18
  102. package/dist/db/collect-table-metas.js +11 -7
  103. package/dist/db/dialect.d.ts +1 -1
  104. package/dist/db/dialect.js +2 -0
  105. package/dist/db/eagerload.js +4 -3
  106. package/dist/db/event-store-executor-context.d.ts +6 -6
  107. package/dist/db/event-store-executor-context.js +25 -22
  108. package/dist/db/event-store-executor-read.js +31 -16
  109. package/dist/db/event-store-executor-write.d.ts +1 -1
  110. package/dist/db/event-store-executor-write.js +84 -63
  111. package/dist/db/index.d.ts +2 -1
  112. package/dist/db/index.js +2 -1
  113. package/dist/db/lock-guard.d.ts +4 -0
  114. package/dist/db/lock-guard.js +14 -0
  115. package/dist/db/migrate-generator.js +1 -1
  116. package/dist/db/migrate-runner.d.ts +1 -0
  117. package/dist/db/migrate-runner.js +64 -38
  118. package/dist/db/queries/backfill-pii.js +63 -25
  119. package/dist/db/queries/event-consumer.d.ts +6 -6
  120. package/dist/db/queries/event-consumer.js +2 -1
  121. package/dist/db/queries/event-store-transfer.js +15 -4
  122. package/dist/db/queries/event-store.d.ts +1 -1
  123. package/dist/db/queries/event-store.js +11 -10
  124. package/dist/db/queries/pii-backfill-state.js +3 -2
  125. package/dist/db/queries/postgres-array-literal.d.ts +2 -0
  126. package/dist/db/queries/postgres-array-literal.js +6 -0
  127. package/dist/db/queries/projection-rebuild.d.ts +1 -1
  128. package/dist/db/queries/projection-rebuild.js +3 -2
  129. package/dist/db/queries/shadow-swap.d.ts +1 -1
  130. package/dist/db/queries/shadow-swap.js +12 -8
  131. package/dist/db/query.d.ts +1 -1
  132. package/dist/db/query.js +1 -1
  133. package/dist/db/retired-framework-tables.js +22 -4
  134. package/dist/db/table-builder.js +6 -2
  135. package/dist/db/tenant-db-runner.d.ts +4 -0
  136. package/dist/db/tenant-db-runner.js +17 -0
  137. package/dist/db/tenant-db.d.ts +4 -0
  138. package/dist/db/tenant-db.js +50 -5
  139. package/dist/derivatives/delete-stored-file.d.ts +4 -0
  140. package/dist/derivatives/delete-stored-file.js +9 -0
  141. package/dist/derivatives/derivatives-context.js +1 -1
  142. package/dist/derivatives/index.d.ts +1 -1
  143. package/dist/derivatives/index.js +1 -1
  144. package/dist/engine/boot-validator/access-declarations.js +2 -2
  145. package/dist/engine/boot-validator/api-ext.js +3 -3
  146. package/dist/engine/boot-validator/boot-check.js +1 -1
  147. package/dist/engine/boot-validator/config-deps.js +13 -13
  148. package/dist/engine/boot-validator/detail-screens.js +1 -1
  149. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  150. package/dist/engine/boot-validator/entity-handler.js +87 -35
  151. package/dist/engine/boot-validator/entity-list-screens.js +2 -2
  152. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  153. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  154. package/dist/engine/boot-validator/nav.js +1 -1
  155. package/dist/engine/boot-validator/pii-retention.js +8 -8
  156. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  157. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  158. package/dist/engine/boot-validator/query-refs.js +14 -2
  159. package/dist/engine/boot-validator/screens.d.ts +2 -1
  160. package/dist/engine/boot-validator/screens.js +153 -95
  161. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  162. package/dist/engine/dedupe-features.js +31 -4
  163. package/dist/engine/define-handler.d.ts +2 -2
  164. package/dist/engine/define-handler.js +8 -1
  165. package/dist/engine/entity-handlers.js +14 -7
  166. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  167. package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
  168. package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
  169. package/dist/engine/feature-ast/render.js +10 -2
  170. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  171. package/dist/engine/feature-entity-handlers.js +12 -3
  172. package/dist/engine/handler-field-references.d.ts +21 -2
  173. package/dist/engine/handler-field-references.js +55 -17
  174. package/dist/engine/index.d.ts +3 -3
  175. package/dist/engine/index.js +1 -1
  176. package/dist/engine/schema-builder.js +2 -1
  177. package/dist/engine/screen-helpers.d.ts +1 -0
  178. package/dist/engine/screen-helpers.js +2 -2
  179. package/dist/engine/soft-delete-cleanup.js +10 -3
  180. package/dist/engine/steps/_read-source.js +5 -2
  181. package/dist/engine/steps/retry.js +2 -1
  182. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  183. package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
  184. package/dist/engine/template-variables.d.ts +3 -0
  185. package/dist/engine/template-variables.js +3 -0
  186. package/dist/engine/tier-resolver-extension.d.ts +1 -0
  187. package/dist/engine/tier-resolver-extension.js +6 -0
  188. package/dist/engine/types/index.d.ts +1 -1
  189. package/dist/entrypoint/index.d.ts +2 -2
  190. package/dist/env/_zod-introspect.d.ts +1 -0
  191. package/dist/errors/classes.js +1 -0
  192. package/dist/errors/docs-url.js +6 -6
  193. package/dist/errors/index.d.ts +1 -1
  194. package/dist/errors/index.js +1 -1
  195. package/dist/errors/kumiko-error.d.ts +2 -0
  196. package/dist/errors/kumiko-error.js +2 -0
  197. package/dist/errors/subject-erased.d.ts +1 -0
  198. package/dist/errors/subject-erased.js +6 -0
  199. package/dist/errors/write-error-info.js +2 -0
  200. package/dist/event-store/admin-api.js +3 -2
  201. package/dist/event-store/errors.d.ts +9 -0
  202. package/dist/event-store/errors.js +20 -0
  203. package/dist/event-store/event-store.d.ts +6 -0
  204. package/dist/event-store/event-store.js +28 -6
  205. package/dist/event-store/index.d.ts +1 -1
  206. package/dist/event-store/index.js +1 -1
  207. package/dist/event-store/tenant-db-append.js +2 -2
  208. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  209. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  210. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  211. package/dist/files/bind-pending-file-refs.js +3 -3
  212. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  213. package/dist/files/bind-referenced-files.write.js +22 -13
  214. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  215. package/dist/files/create-file-ref-from-bytes.js +67 -4
  216. package/dist/files/create-file-ref.d.ts +24 -1
  217. package/dist/files/create-file-ref.js +67 -25
  218. package/dist/files/dispose-removed-file-refs.js +21 -6
  219. package/dist/files/feature.d.ts +1 -0
  220. package/dist/files/feature.js +2 -1
  221. package/dist/files/file-handle.d.ts +1 -1
  222. package/dist/files/file-handle.js +12 -0
  223. package/dist/files/file-routes.js +10 -6
  224. package/dist/files/index.d.ts +3 -3
  225. package/dist/files/index.js +2 -2
  226. package/dist/files/load-save-file-refs.d.ts +10 -0
  227. package/dist/files/load-save-file-refs.js +47 -0
  228. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  229. package/dist/files/purge-expired-file-ref-bytes.js +46 -21
  230. package/dist/files/resolve-content-type.js +40 -4
  231. package/dist/files/storage-tracking.js +5 -4
  232. package/dist/files/types.js +5 -8
  233. package/dist/files/zip-entry-names.d.ts +1 -0
  234. package/dist/files/zip-entry-names.js +46 -0
  235. package/dist/jobs/index.d.ts +1 -1
  236. package/dist/jobs/job-runner.d.ts +5 -1
  237. package/dist/jobs/job-runner.js +124 -37
  238. package/dist/observability/escape-hatch-report.d.ts +2 -1
  239. package/dist/observability/escape-hatch-report.js +48 -7
  240. package/dist/observability/index.d.ts +1 -1
  241. package/dist/observability/index.js +1 -1
  242. package/dist/observability/metrics-handle.d.ts +1 -0
  243. package/dist/observability/metrics-handle.js +7 -0
  244. package/dist/observability/standard-metrics.js +4 -4
  245. package/dist/pipeline/cascade-handler.js +5 -2
  246. package/dist/pipeline/dispatch-batch.js +79 -31
  247. package/dist/pipeline/dispatch-query.d.ts +2 -1
  248. package/dist/pipeline/dispatch-query.js +6 -6
  249. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  250. package/dist/pipeline/dispatch-shared.js +136 -48
  251. package/dist/pipeline/dispatch-stream.js +10 -4
  252. package/dist/pipeline/dispatch-write.js +34 -22
  253. package/dist/pipeline/dispatcher.js +3 -1
  254. package/dist/pipeline/event-dispatcher.d.ts +2 -2
  255. package/dist/pipeline/event-dispatcher.js +11 -9
  256. package/dist/pipeline/event-retention.d.ts +1 -0
  257. package/dist/pipeline/event-retention.js +7 -1
  258. package/dist/pipeline/index.d.ts +1 -0
  259. package/dist/pipeline/index.js +1 -0
  260. package/dist/pipeline/notify-scope.js +3 -1
  261. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  262. package/dist/pipeline/pre-transaction-reservations.js +29 -9
  263. package/dist/rate-limit/bucket.d.ts +3 -3
  264. package/dist/rate-limit/bucket.js +14 -4
  265. package/dist/redis/cache-sync-bus.js +9 -3
  266. package/dist/schema-cli.js +12 -1
  267. package/dist/search/meilisearch-adapter.js +36 -9
  268. package/dist/stack/db.d.ts +2 -2
  269. package/dist/stack/test-stack.js +2 -1
  270. package/dist/testing/index.d.ts +1 -0
  271. package/dist/testing/index.js +1 -0
  272. package/dist/testing/multipart-helper.d.ts +6 -0
  273. package/dist/testing/multipart-helper.js +1 -1
  274. package/dist/testing/zip-fixture.d.ts +1 -0
  275. package/dist/testing/zip-fixture.js +33 -0
  276. package/dist/ui-types/card-column-roles.js +4 -2
  277. package/dist/ui-types/index.d.ts +1 -1
  278. package/dist/ui-types/index.js +1 -1
  279. package/dist/upgrade-cli.js +46 -13
  280. package/package.json +7 -5
  281. package/src/changes.json +512 -8
@@ -0,0 +1,393 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/legal-pages. Do not edit. -->
2
+
3
+ # legal-pages
4
+
5
+ Auto-rendered Impressum + Datenschutzerklärung pages — template-resolver + legal-pages bundled features wired up with anonymousAccess for DACH-compliance.
6
+
7
+ ## README
8
+
9
+ # Recipe: legal-pages + template-resolver
10
+
11
+ DACH compliance stack: two opt-in bundled features wired up to
12
+ auto-rendered Imprint + Privacy-Policy pages, with Markdown authoring
13
+ and a boot check for required content.
14
+
15
+ **What the recipe demonstrates:**
16
+ - Activate both features in `runProdApp({ features: [...] })`
17
+ - Required wirings: `anonymousAccess` + `extraContext.templateResolver`
18
+ - Initial seed of the DE required blocks (Imprint + Privacy) for SYSTEM_TENANT
19
+ - 5 integration tests proving end-to-end behavior
20
+
21
+ ## Layout
22
+
23
+ ```
24
+ samples/recipes/legal-pages/
25
+ ├── package.json # workspace deps
26
+ ├── README.md # this file
27
+ └── src/
28
+ ├── feature.ts # the two features re-exported for tests
29
+ └── __tests__/
30
+ └── feature.integration.test.ts # 5 tests: routes + boot check
31
+ ```
32
+
33
+ `feature.ts` is intentionally thin (re-export). In a real app this
34
+ lives in `bin/main.ts` (see "Integration into a real app" below).
35
+
36
+ ## Run tests
37
+
38
+ ```bash
39
+ # From the repo root:
40
+ bun kumiko test all samples/recipes/legal-pages/
41
+ ```
42
+
43
+ Expected output:
44
+ ```
45
+ Test Files 1 passed (1)
46
+ Tests 5 passed (5)
47
+ ```
48
+
49
+ When the tests are green:
50
+ - template-resolver + legal-pages are compatible
51
+ - Table schema is clean via `unsafeCreateEntityTable(stack.db, textBlockEntity)`
52
+ - Routes are reachable via `stack.app.request("/legal/impressum")`
53
+ - Markdown rendering produces valid HTML
54
+ - Boot check correctly detects missing blocks
55
+
56
+ ## Integration into a real app
57
+
58
+ Step-by-step for an existing Kumiko app (e.g.
59
+ `samples/showcases/your-app/`):
60
+
61
+ ### 1. Activate features in `runProdApp`
62
+
63
+ ```typescript illustration
64
+ // bin/main.ts
65
+ import { runProdApp } from "@cosmicdrift/kumiko-server-runtime";
66
+ import {
67
+ createTemplateResolverApi,
68
+ createTemplateResolverFeature,
69
+ } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
70
+ import { createLegalPagesFeature } from "@cosmicdrift/kumiko-bundled-features/legal-pages";
71
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
72
+
73
+ await runProdApp({
74
+ features: [
75
+ createTemplateResolverFeature(),
76
+ createLegalPagesFeature(),
77
+ /* ... your other features */
78
+ ],
79
+ // Required (1): routes run anonymous, need tenant resolution
80
+ anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID },
81
+ // Required (2): boot check + internal lookup use ctx.templateResolver
82
+ extraContext: ({ db }) => ({
83
+ templateResolver: createTemplateResolverApi(db),
84
+ }),
85
+ });
86
+ ```
87
+
88
+ → For host-based multi-tenant apps (like publicstatus.eu),
89
+ `SYSTEM_TENANT_ID` always stays correct for legal-pages — the routes
90
+ internally set `X-Tenant: SYSTEM_TENANT_ID` and override any
91
+ host-based `tenantResolver`. This is the "1 app = X tenants = 1
92
+ imprint" decision.
93
+
94
+ ### 2. Create the table
95
+
96
+ ```bash
97
+ # In the app workspace:
98
+ bunx kumiko-schema generate <name> # detects text-block entity → SQL migration
99
+ bunx kumiko-schema apply # one-time (pre-deploy step in prod)
100
+ ```
101
+
102
+ ### 3. Initial seed of the required blocks
103
+
104
+ A one-shot setup routine that runs on first boot or via the CLI:
105
+
106
+ ```typescript illustration
107
+ // bin/seed-legal.ts
108
+ import { seedTextBlock } from "@cosmicdrift/kumiko-bundled-features/template-resolver/seeding";
109
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
110
+ import { createDb } from "@cosmicdrift/kumiko-framework/db";
111
+
112
+ const db = createDb(process.env.DATABASE_URL!);
113
+
114
+ await seedTextBlock(db, {
115
+ tenantId: SYSTEM_TENANT_ID,
116
+ slug: "imprint",
117
+ locale: "de",
118
+ title: "Impressum",
119
+ content: `## Angaben gemäß § 5 TMG
120
+
121
+ **[Dein Name / GmbH]**
122
+
123
+ [Strasse + Nr]
124
+ [PLZ Ort]
125
+ Deutschland
126
+
127
+ ## Kontakt
128
+
129
+ E-Mail: [hello@example.com](mailto:hello@example.com)
130
+
131
+ ## Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV
132
+
133
+ [Dein Name, Adresse]`,
134
+ });
135
+
136
+ await seedTextBlock(db, {
137
+ tenantId: SYSTEM_TENANT_ID,
138
+ slug: "privacy",
139
+ locale: "de",
140
+ title: "Datenschutzerklärung",
141
+ content: `## 1. Verantwortlicher
142
+
143
+ [Dein Name + Anschrift]
144
+
145
+ ## 2. Erhobene Daten
146
+
147
+ [...]`,
148
+ });
149
+ ```
150
+
151
+ → Templates for full legally-sound texts: e-recht24.de
152
+ or datenschutz-generator.de by Dr. Schwenke.
153
+
154
+ ### 4. Visit the pages
155
+
156
+ After the seed these URLs are immediately live:
157
+
158
+ | URL | Content |
159
+ |---|---|
160
+ | `https://your-app.example/legal/impressum` | Impressum (DE) |
161
+ | `https://your-app.example/legal/datenschutz` | Datenschutzerklärung (DE) |
162
+ | `https://your-app.example/legal/imprint` | Imprint (EN, if seeded) |
163
+ | `https://your-app.example/legal/privacy` | Privacy Policy (EN, if seeded) |
164
+
165
+ → Footer links are set per app (legal-pages does not ship a footer
166
+ component — deliberately, every app has its own layout).
167
+
168
+ ### 5. Editing texts later (TenantAdmin maintenance)
169
+
170
+ Through the standard write API:
171
+
172
+ ```typescript illustration
173
+ await fetch("/api/write", {
174
+ method: "POST",
175
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${jwt}` },
176
+ body: JSON.stringify({
177
+ type: "template-resolver:write:set",
178
+ payload: { slug: "imprint", locale: "de", title: "Impressum", content: "..." },
179
+ }),
180
+ });
181
+ ```
182
+
183
+ ACL: `["TenantAdmin", "SystemAdmin"]`. Cache header `public, max-age=300`
184
+ — visitors see updates within 5 minutes at most.
185
+
186
+ ## Boot-check behavior
187
+
188
+ When the required blocks (`imprint/de` + `privacy/de`) are missing
189
+ from SYSTEM_TENANT:
190
+
191
+ | Mode | Behavior |
192
+ |---|---|
193
+ | `NODE_ENV=production` | App boot throws an error with a slug list — container exits |
194
+ | otherwise (dev/test) | `console.warn` with a slug list, app starts anyway |
195
+
196
+ → Safety net: no production deploy without populated legal pages.
197
+
198
+ ## Troubleshooting
199
+
200
+ | Symptom | Cause | Fix |
201
+ |---|---|---|
202
+ | Route returns `503 legal page unavailable` | `anonymousAccess` not configured in `runProdApp` | Set `anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID }` |
203
+ | Boot check throws `ctx.templateResolver missing` | `extraContext.templateResolver` not wired | Set `extraContext: ({ db }) => ({ templateResolver: createTemplateResolverApi(db) })` |
204
+ | Route returns `404 not configured` | Required block doesn't exist or has `content=null` | `seedTextBlock` with a content string |
205
+ | Multi-tenant app: tenant subdomain shows an empty page | (Bug regression?) Routes should ALWAYS show SYSTEM_TENANT texts | The `legal-pages.integration.test.ts` test "SYSTEM_TENANT routing" covers this — should be green |
206
+ | `<script>` tags in a Markdown body land 1:1 in the HTML | Deliberately accepted right now — see [legal-pages/README.md XSS section](../../../packages/bundled-features/src/legal-pages/README.md#xss--currently-not-secured-by-design) | DOMPurify is a Phase 2 once a multi-author setup arrives |
207
+
208
+ ## Cross-refs
209
+
210
+ - [packages/bundled-features/src/template-resolver/README.md](../../../packages/bundled-features/src/template-resolver/README.md) — content store
211
+ - [packages/bundled-features/src/legal-pages/README.md](../../../packages/bundled-features/src/legal-pages/README.md) — DACH compliance wrapper
212
+
213
+ ## Source files
214
+
215
+ ### src/__tests__/feature.integration.test.ts
216
+
217
+ ```ts
218
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
219
+ import {
220
+ createTemplateResolverApi,
221
+ templateResourceEntity,
222
+ } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
223
+ import { seedTextBlock } from "@cosmicdrift/kumiko-bundled-features/template-resolver/seeding";
224
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
225
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
226
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
227
+ import {
228
+ setupTestStack,
229
+ type TestStack,
230
+ unsafeCreateEntityTable,
231
+ } from "@cosmicdrift/kumiko-framework/stack";
232
+ import { LEGAL_ROUTES, legalPagesFeature, templateResolverFeature } from "../feature";
233
+
234
+ let stack: TestStack;
235
+ let db: DbConnection;
236
+
237
+ beforeAll(async () => {
238
+ stack = await setupTestStack({
239
+ features: [templateResolverFeature, legalPagesFeature],
240
+ anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID },
241
+ extraContext: ({ db }) => ({
242
+ templateResolver: createTemplateResolverApi(db),
243
+ }),
244
+ });
245
+ db = stack.db;
246
+ await unsafeCreateEntityTable(db, templateResourceEntity);
247
+ await createEventsTable(db);
248
+
249
+ // Seed Pflicht-Blocks für SYSTEM_TENANT (DE Impressum + DSE)
250
+ await seedTextBlock(db, {
251
+ tenantId: SYSTEM_TENANT_ID,
252
+ slug: "imprint",
253
+ locale: "de",
254
+ title: "Impressum",
255
+ content: [
256
+ "## Angaben gemäß § 5 TMG",
257
+ "",
258
+ "**Alex Morgan**",
259
+ "Musterstraße 1",
260
+ "12345 Musterstadt",
261
+ "Deutschland",
262
+ "",
263
+ "## Kontakt",
264
+ "",
265
+ "E-Mail: [hello@example.com](mailto:hello@example.com)",
266
+ ].join("\n"),
267
+ });
268
+
269
+ await seedTextBlock(db, {
270
+ tenantId: SYSTEM_TENANT_ID,
271
+ slug: "privacy",
272
+ locale: "de",
273
+ title: "Datenschutzerklärung",
274
+ content: [
275
+ "## 1. Verantwortlicher",
276
+ "",
277
+ "Alex Morgan, Musterstraße 1, 12345 Musterstadt.",
278
+ "",
279
+ "## 2. Erhobene Daten",
280
+ "",
281
+ "Diese App setzt **keine Tracking-Cookies** und kein Drittanbieter-Tracking ein.",
282
+ "",
283
+ "## 3. Deine Rechte (Art. 15-22 DSGVO)",
284
+ "",
285
+ "Auskunft, Berichtigung, Löschung, Einschränkung, Datenübertragbarkeit, Widerspruch.",
286
+ ].join("\n"),
287
+ });
288
+ });
289
+
290
+ afterAll(async () => {
291
+ await stack.cleanup();
292
+ });
293
+
294
+ describe("legal-pages sample", () => {
295
+ test("alle LEGAL_ROUTES sind über stack.app erreichbar", async () => {
296
+ // Smoke-Test: jede Route gibt entweder 200 (gerendert) oder 404
297
+ // (Block fehlt). Nie 5xx — die Server-Pipeline läuft sauber durch.
298
+ for (const route of LEGAL_ROUTES) {
299
+ const res = await stack.app.request(route.path);
300
+ expect([200, 404]).toContain(res.status);
301
+ }
302
+ });
303
+
304
+ test("DE-Pflicht-Pages rendern HTML", async () => {
305
+ const imprint = await stack.app.request("/legal/impressum");
306
+ expect(imprint.status).toBe(200);
307
+ const imprintBody = await imprint.text();
308
+ expect(imprintBody).toContain("<!doctype html>");
309
+ expect(imprintBody).toContain("Alex Morgan");
310
+ expect(imprintBody).toContain("Musterstadt");
311
+
312
+ const privacy = await stack.app.request("/legal/datenschutz");
313
+ expect(privacy.status).toBe(200);
314
+ const privacyBody = await privacy.text();
315
+ expect(privacyBody).toContain("Tracking-Cookies");
316
+ expect(privacyBody).toContain("Datenschutzerklärung");
317
+ });
318
+
319
+ test("EN-Variante 404 (nicht geseedet) zeigt hilfreiche Meldung", async () => {
320
+ const res = await stack.app.request("/legal/imprint");
321
+ expect(res.status).toBe(404);
322
+ const body = await res.text();
323
+ expect(body).toContain("Tenant-Admin");
324
+ });
325
+
326
+ test("Markdown-Headings werden zu HTML-h2-Tags", async () => {
327
+ const res = await stack.app.request("/legal/impressum");
328
+ const body = await res.text();
329
+ expect(body).toContain("<h2>Angaben gemäß § 5 TMG</h2>");
330
+ expect(body).toContain("<strong>Alex Morgan</strong>");
331
+ });
332
+
333
+ test("Cache-Header zeigt 60s public-cache", async () => {
334
+ const res = await stack.app.request("/legal/impressum");
335
+ expect(res.headers.get("cache-control")).toBe("public, max-age=60, must-revalidate");
336
+ });
337
+ });
338
+ ```
339
+
340
+ ### src/feature.ts
341
+
342
+ ```ts
343
+ // Legal-Pages Sample
344
+ //
345
+ // DACH-Apps (DE/AT/CH) sind verpflichtet ein Impressum (TMG/DDG §5) und
346
+ // eine Datenschutzerklärung (DSGVO Art. 13) öffentlich zugänglich zu
347
+ // haben. Das ist 1) für jede App identisch und 2) nervig manuell pro
348
+ // App neu zu basteln.
349
+ //
350
+ // Lösung: zwei opt-in bundled-features kombinieren:
351
+ //
352
+ // - `template-resolver` — der Content-Store (Entity `template-resource`,
353
+ // kind `text-block` mit slug+locale+title+content, scoped per Tenant).
354
+ // Auch nutzbar für FAQ, About, ToS, Marketing-Snippets — nicht
355
+ // legal-spezifisch.
356
+ //
357
+ // - `legal-pages` — opt-in-Wrapper darauf, der vier feste Public-
358
+ // Routes (`/legal/impressum`, `/legal/datenschutz`, `/legal/imprint`,
359
+ // `/legal/privacy`) registriert und Markdown→HTML rendered. Plus
360
+ // Boot-Check der in Production hart fehlt wenn die DE-Pflicht-Blocks
361
+ // fehlen.
362
+ //
363
+ // Tenant-Modell: 1 App = X Tenants = 1 Impressum. Alle Subdomains
364
+ // teilen sich die SYSTEM_TENANT_ID-Version. Wer pro-Tenant-Impressums
365
+ // braucht, muss sein eigenes Routing davorsetzen oder die by-slug-
366
+ // query mit tenant-specific tenantId nutzen.
367
+ //
368
+ // Voraussetzungen für Production:
369
+ // - `anonymousAccess` muss in runProdApp/runDevApp konfiguriert sein
370
+ // (defaultTenantId = SYSTEM_TENANT_ID), sonst antworten die
371
+ // legal-pages-Routes mit 503
372
+ // - `extraContext.templateResolver = createTemplateResolverApi(db)` muss
373
+ // gewired sein, sonst wirft der Boot-Check mit Wiring-Hinweis
374
+ // - Beim ersten Boot müssen die TextBlocks geseedet sein —
375
+ // template-resolver/seeding `seedTextBlock` oder via API
376
+ // `template-resolver:write:set` mit TenantAdmin-Token
377
+
378
+ import {
379
+ createLegalPagesFeature,
380
+ LEGAL_REQUIRED_BLOCKS,
381
+ LEGAL_ROUTES,
382
+ } from "@cosmicdrift/kumiko-bundled-features/legal-pages";
383
+ import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
384
+
385
+ // Beide Features aktivieren — template-resolver ist Foundation, legal-pages
386
+ // requires sie. r.requires("template-resolver") greift automatisch im
387
+ // legal-pages-Feature.
388
+ export const templateResolverFeature = createTemplateResolverFeature();
389
+ export const legalPagesFeature = createLegalPagesFeature();
390
+
391
+ // Re-exports für Tests + andere Demos
392
+ export { LEGAL_REQUIRED_BLOCKS, LEGAL_ROUTES };
393
+ ```