@cosmicdrift/kumiko-samples 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 (237) hide show
  1. package/LICENSE +1 -1
  2. package/package.json +1 -1
  3. package/packages/bundled-features/package.json +7 -1
  4. package/packages/bundled-features/src/audit/changes.json +18 -0
  5. package/packages/bundled-features/src/audit/feature.ts +1 -1
  6. package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
  7. package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +63 -11
  8. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +30 -11
  9. package/packages/bundled-features/src/auth-email-password/changes.json +55 -0
  10. package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
  11. package/packages/bundled-features/src/auth-email-password/feature.ts +4 -7
  12. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
  13. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -3
  14. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +6 -4
  15. package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
  16. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
  17. package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +2 -2
  18. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +2 -1
  19. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +7 -3
  20. package/packages/bundled-features/src/auth-email-password/mail-i18n.ts +10 -0
  21. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
  22. package/packages/bundled-features/src/billing-foundation/changes.json +48 -0
  23. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +57 -5
  24. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
  25. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +4 -1
  26. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
  27. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +9 -3
  28. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
  29. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +18 -2
  30. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +35 -19
  31. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
  32. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
  33. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
  34. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +10 -4
  35. package/packages/bundled-features/src/billing-foundation/i18n.ts +9 -0
  36. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +27 -7
  37. package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
  38. package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +20 -8
  39. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
  40. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +7 -2
  41. package/packages/bundled-features/src/cap-counter/changes.json +19 -0
  42. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
  43. package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +2 -0
  44. package/packages/bundled-features/src/cap-counter/index.ts +1 -0
  45. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
  46. package/packages/bundled-features/src/cap-overview/changes.json +6 -0
  47. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  48. package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +2 -1
  49. package/packages/bundled-features/src/channel-discord/discord-channel.ts +4 -2
  50. package/packages/bundled-features/src/channel-email/changes.json +18 -0
  51. package/packages/bundled-features/src/channel-email/email-channel.ts +47 -24
  52. package/packages/bundled-features/src/channel-email/smtp-transport.ts +7 -1
  53. package/packages/bundled-features/src/channel-slack/slack-channel.ts +4 -2
  54. package/packages/bundled-features/src/channel-teams/teams-channel.ts +4 -2
  55. package/packages/bundled-features/src/config/changes.json +6 -0
  56. package/packages/bundled-features/src/config/handlers/set.write.ts +2 -2
  57. package/packages/bundled-features/src/config/resolver.ts +15 -12
  58. package/packages/bundled-features/src/config/write-helpers.ts +23 -0
  59. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  60. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +54 -27
  61. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  62. package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +2 -2
  63. package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
  64. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +4 -5
  65. package/packages/bundled-features/src/delivery/attachment-ref.ts +39 -1
  66. package/packages/bundled-features/src/delivery/changes.json +30 -0
  67. package/packages/bundled-features/src/delivery/channel-context.ts +31 -8
  68. package/packages/bundled-features/src/delivery/delivery-service.ts +93 -92
  69. package/packages/bundled-features/src/delivery/index.ts +2 -0
  70. package/packages/bundled-features/src/delivery/jobs.ts +51 -14
  71. package/packages/bundled-features/src/delivery/types.ts +7 -1
  72. package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
  73. package/packages/bundled-features/src/feature-toggles/changes.json +7 -0
  74. package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +26 -12
  75. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  76. package/packages/bundled-features/src/file-foundation/changes.json +6 -0
  77. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +6 -3
  78. package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
  79. package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
  80. package/packages/bundled-features/src/files-tenant-data/index.ts +3 -1
  81. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +12 -3
  82. package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
  83. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
  84. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
  85. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
  86. package/packages/bundled-features/src/jobs/changes.json +31 -0
  87. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
  88. package/packages/bundled-features/src/jobs/feature.ts +7 -4
  89. package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
  90. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
  91. package/packages/bundled-features/src/jobs/job-run-logger.ts +56 -18
  92. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
  93. package/packages/bundled-features/src/ledger/changes.json +7 -0
  94. package/packages/bundled-features/src/ledger/entity.ts +3 -1
  95. package/packages/bundled-features/src/metrics/changes.json +6 -0
  96. package/packages/bundled-features/src/metrics/filters.ts +28 -12
  97. package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
  98. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +7 -8
  99. package/packages/bundled-features/src/renderer-foundation/types.ts +2 -0
  100. package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
  101. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
  102. package/packages/bundled-features/src/secrets/changes.json +12 -0
  103. package/packages/bundled-features/src/secrets/feature.ts +4 -3
  104. package/packages/bundled-features/src/secrets/handlers/delete.write.ts +3 -1
  105. package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
  106. package/packages/bundled-features/src/secrets/write-gate.ts +8 -2
  107. package/packages/bundled-features/src/shared/changes.json +6 -0
  108. package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
  109. package/packages/bundled-features/src/shared/index.ts +8 -0
  110. package/packages/bundled-features/src/shared/mail-template-body.ts +17 -0
  111. package/packages/bundled-features/src/shared/resource-image-formats.ts +31 -0
  112. package/packages/bundled-features/src/shared/sharp-input-limits.ts +7 -8
  113. package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +2 -2
  114. package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
  115. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
  116. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
  117. package/packages/bundled-features/src/template-resolver/README.md +4 -2
  118. package/packages/bundled-features/src/template-resolver/api.ts +3 -2
  119. package/packages/bundled-features/src/template-resolver/changes.json +149 -0
  120. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +148 -18
  121. package/packages/bundled-features/src/template-resolver/compile-steps-block.ts +68 -0
  122. package/packages/bundled-features/src/template-resolver/constants.ts +23 -3
  123. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -0
  124. package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -5
  125. package/packages/bundled-features/src/template-resolver/document-steps.ts +114 -0
  126. package/packages/bundled-features/src/template-resolver/feature.ts +16 -1
  127. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +81 -39
  128. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +4 -3
  129. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +121 -53
  130. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +6 -44
  131. package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
  132. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +56 -1
  133. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +6 -3
  134. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +25 -15
  135. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +12 -1
  136. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +73 -30
  137. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +14 -11
  138. package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
  139. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +102 -19
  140. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +49 -10
  141. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +39 -10
  142. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +14 -2
  143. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +54 -4
  144. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +96 -14
  145. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +51 -24
  146. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
  147. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +16 -2
  148. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +4 -1
  149. package/packages/bundled-features/src/template-resolver/index.ts +18 -2
  150. package/packages/bundled-features/src/template-resolver/layout-appearance.ts +56 -0
  151. package/packages/bundled-features/src/template-resolver/mail-constants.ts +4 -0
  152. package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
  153. package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
  154. package/packages/bundled-features/src/template-resolver/resource-image.ts +9 -16
  155. package/packages/bundled-features/src/template-resolver/seeding.ts +27 -4
  156. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +390 -124
  157. package/packages/bundled-features/src/template-resolver/table.ts +13 -1
  158. package/packages/bundled-features/src/template-resolver/template-bundle.ts +4 -0
  159. package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
  160. package/packages/bundled-features/src/template-resolver/template-meta.ts +95 -24
  161. package/packages/bundled-features/src/template-resolver/template-preview.ts +160 -57
  162. package/packages/bundled-features/src/template-resolver/template-resources.ts +3 -4
  163. package/packages/bundled-features/src/template-resolver/template-usage.ts +31 -11
  164. package/packages/bundled-features/src/template-resolver/template-validation.ts +16 -9
  165. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +12 -8
  166. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +97 -10
  167. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +15 -5
  168. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +405 -145
  169. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +17 -3
  170. package/packages/bundled-features/src/template-resolver/web/i18n.ts +32 -4
  171. package/packages/bundled-features/src/template-resolver/web/index.ts +8 -0
  172. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +23 -7
  173. package/packages/bundled-features/src/tenant/changes.json +6 -0
  174. package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +3 -8
  175. package/packages/bundled-features/src/tenant/seeding.ts +33 -27
  176. package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +14 -3
  177. package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
  178. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +9 -1
  179. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +3 -2
  180. package/packages/bundled-features/src/tier-engine/changes.json +18 -0
  181. package/packages/bundled-features/src/tier-engine/constants.ts +9 -0
  182. package/packages/bundled-features/src/tier-engine/entity.ts +9 -4
  183. package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
  184. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +8 -5
  185. package/packages/bundled-features/src/tier-engine/index.ts +2 -0
  186. package/packages/bundled-features/src/user/changes.json +6 -0
  187. package/packages/bundled-features/src/user/constants.ts +1 -0
  188. package/packages/bundled-features/src/user/invited-global-roles.ts +2 -4
  189. package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
  190. package/packages/bundled-features/src/user/seeding.ts +19 -5
  191. package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
  192. package/packages/bundled-features/src/user-data-rights/email-templates.ts +0 -1
  193. package/packages/bundled-features/src/user-data-rights/i18n.ts +5 -0
  194. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +13 -2
  195. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +7 -1
  196. package/packages/bundled-features/src/user-profile/changes.json +1 -1
  197. package/packages/bundled-features/src/waitlist/changes.json +18 -0
  198. package/packages/bundled-features/src/waitlist/feature.ts +1 -1
  199. package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +35 -3
  200. package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +2 -17
  201. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +33 -9
  202. package/packages/bundled-features/src/waitlist/i18n.ts +9 -0
  203. package/packages/bundled-features/src/waitlist/lib.ts +20 -0
  204. package/packages/bundled-features/src/waitlist/options.ts +1 -1
  205. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  206. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
  207. package/packages/bundled-features/src/workflow-runner/feature.ts +1 -1
  208. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
  209. package/samples/apps/ui-walkthrough/package.json +9 -0
  210. package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
  211. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +87 -2
  212. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +246 -30
  213. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +4 -0
  214. package/samples/apps/ui-walkthrough/src/run-config.ts +18 -1
  215. package/samples/apps/use-all-bundled/README.md +3 -13
  216. package/samples/apps/use-all-bundled/feature-manifest.json +7 -5
  217. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +34 -118
  218. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +1 -1
  219. package/samples/apps/use-all-bundled/src/run-config.ts +0 -3
  220. package/samples/recipes/auth-mfa/README.md +2 -1
  221. package/samples/recipes/auth-mfa/src/feature.ts +1 -0
  222. package/samples/recipes/custom-handlers/README.md +4 -3
  223. package/samples/recipes/custom-handlers/src/feature.ts +21 -12
  224. package/samples/recipes/dashboard-stats/README.md +38 -0
  225. package/samples/recipes/dashboard-stats/package.json +8 -0
  226. package/samples/recipes/dashboard-stats/src/feature.ts +104 -0
  227. package/samples/recipes/legal-pages/README.md +2 -2
  228. package/samples/recipes/managed-pages/README.md +2 -2
  229. package/samples/recipes/pdf-audit-export/README.md +2 -1
  230. package/samples/recipes/pdf-audit-export/package.json +1 -1
  231. package/samples/recipes/pdf-audit-export/src/feature.ts +68 -48
  232. package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +2 -2
  233. package/samples/apps/use-all-bundled/schema/generate.ts +0 -617
  234. package/samples/apps/use-all-bundled/schema/migrations/meta/0000_snapshot.json +0 -3055
  235. package/samples/apps/use-all-bundled/schema/migrations/meta/_journal.json +0 -13
  236. package/samples/apps/use-all-bundled/schema/schema.generated.ts +0 -94
  237. package/samples/apps/use-all-bundled/schema/schema.ts +0 -9
@@ -15,6 +15,7 @@ import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-featu
15
15
  import { createDocumentDesignerFeature } from "@cosmicdrift/kumiko-document-designer";
16
16
  import { localeDe } from "@cosmicdrift/kumiko-locale-de";
17
17
  import { createRendererMailHtmlFeature } from "@cosmicdrift/kumiko-renderer-mail-html";
18
+ import { createRendererPuppeteerClientFeature } from "@cosmicdrift/kumiko-renderer-puppeteer-client";
18
19
  import { TENANT_ADMIN_EMAIL } from "./app/auth-constants";
19
20
  import { billingFeature } from "./features/billing";
20
21
  import { rentalFeature } from "./features/rental";
@@ -34,7 +35,23 @@ export const APP_FEATURES = [
34
35
  createDocumentDesignerFeature(),
35
36
  // Mounted individually: composeRendererStack() would add a second template-resolver.
36
37
  createRendererFoundationFeature(),
37
- createRendererMailHtmlFeature(),
38
+ createRendererMailHtmlFeature({
39
+ footer: ({ locale }) =>
40
+ locale.startsWith("de")
41
+ ? {
42
+ text: "Gesendet über das Nordlicht Mieterportal",
43
+ imprint: { label: "Impressum", url: "https://nordlicht-immobilien.de/impressum" },
44
+ privacy: { label: "Datenschutz", url: "https://nordlicht-immobilien.de/datenschutz" },
45
+ }
46
+ : {
47
+ text: "Sent via the Nordlicht tenant portal",
48
+ imprint: { label: "Imprint", url: "https://nordlicht-immobilien.de/imprint" },
49
+ privacy: { label: "Privacy", url: "https://nordlicht-immobilien.de/privacy" },
50
+ },
51
+ }),
52
+ // The PDF preview of the template editor prints through the puppeteer-service
53
+ // (PUPPETEER_SERVICE_URL / PUPPETEER_AUTH_TOKEN); the e2e config starts one.
54
+ createRendererPuppeteerClientFeature(),
38
55
  createDeliveryFeature(),
39
56
  // In-memory transport: the test mail of the mail editor is rendered and
40
57
  // "sent" for real, nothing leaves the process. Every user resolves to the
@@ -33,24 +33,14 @@ Exits 0 on success; any feature-wiring fault throws and exits 1.
33
33
 
34
34
  ## Coverage status
35
35
 
36
- 36/39 bundled-feature exports mounted, 3 held-back. M0.1 reduced this from 10. Held-back nach M0.1:
37
-
38
- - `auth-email-password` — auto-mounted via `composeFeatures(authOptions)` (no need to list)
39
- - `files-provider-s3` — utility helpers (`createS3Provider`), kein `defineFeature`
40
- - `foundation-shared` — pure utilities (`requireDefined`, `requireNonEmpty`), kein `defineFeature`
41
-
42
- The 3 remaining are utilities or auto-mounted — they cannot be added to `APP_FEATURES` by design.
36
+ Every bundled-feature export is mounted except the held-back ones in `EXPECTED_HELD_BACK` (`scripts/check-coverage.ts`): utilities without `defineFeature` and scaffold-only features. They cannot be added to `APP_FEATURES` by design.
43
37
 
44
38
  ## Maintenance
45
39
 
46
40
  When you add a new feature-export to `@cosmicdrift/kumiko-bundled-features` you MUST either:
47
41
 
48
42
  1. Mount it in `src/run-config.ts`. `kumiko/schema.ts` picks up entities via
49
- `composeFeatures(APP_FEATURES)` — then regenerate if schema changed:
50
- ```sh
51
- cd samples/apps/use-all-bundled
52
- bun ../../bin/kumiko.ts schema generate feature-coverage
53
- ```
43
+ `composeFeatures(APP_FEATURES)`; run `kumiko-schema generate` if the schema changed.
54
44
  2. Or, if the feature is not yet smoke-ready, add it to `EXPECTED_HELD_BACK` in `scripts/check-coverage.ts` with a one-line reason.
55
45
 
56
- CI fails otherwise via `Coverage lint (M5)`.
46
+ CI fails otherwise via `Coverage lint`.
@@ -1423,7 +1423,7 @@
1423
1423
  },
1424
1424
  {
1425
1425
  "name": "jobs",
1426
- "description": "Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079). A job that declares `tenantVisibleRun` also keeps its run state per tenant and subject in `store_tenant_job_runs` (queued, running, plus the latest completed or failed run, no payload, error text or logs), which the tenant reads through `jobs:query:tenant-runs` (fw#3616); the stale-run sweep fails its stuck `running` rows and drops its stuck `queued` rows.",
1426
+ "description": "Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079). A job that declares `tenantVisibleRun` also keeps its run state per tenant and subject in `store_tenant_job_runs` (queued, running, plus the latest completed or failed run, no payload, error text or logs), which the tenant reads through `jobs:query:tenant-runs` (fw#3616); the stale-run sweep fails its stuck `running` rows and drops `queued` rows older than `retentionDays`.",
1427
1427
  "toggleableDefault": null,
1428
1428
  "requires": [],
1429
1429
  "optionalRequires": [],
@@ -2122,7 +2122,7 @@
2122
2122
  },
2123
2123
  {
2124
2124
  "name": "renderer-mail-html",
2125
- "description": "A `renderer-foundation` plugin that handles `RenderRequest { kind: \"mail\" }` and returns `{ subject, html, text, previewText? }` suitable for sending via `channel-email`. The mail template (`kind: \"mail\"`, document HTML, subject in `meta`) resolves through `template-resolver`'s DB fallback, its body is compiled to Handlebars by template-resolver and rendered with escaping, then placed into an MJML layout (`kind: \"mail-layout\"`, slug `default` unless `meta.layoutSlug` says otherwise, in-code `DEFAULT_MAIL_LAYOUT_MJML` as last resort) that `mjml()` compiles to email-safe table HTML, with `html-to-text` producing the plain-text alternative. Seed an editable system layout with `seedSystemMailLayout(db)`. Requires `renderer-foundation` + `template-resolver`.",
2125
+ "description": "A `renderer-foundation` plugin that handles `RenderRequest { kind: \"mail\" }` and returns `{ subject, html, text, previewText? }` suitable for sending via `channel-email`. The mail template (`kind: \"mail\"`, document HTML, subject in `meta`) resolves through `template-resolver`'s DB fallback, its body is compiled to Handlebars by template-resolver and rendered with escaping, then placed into an MJML layout (`kind: \"mail-layout\"`, slug `default` unless `meta.layoutSlug` says otherwise, an in-code structured layout with the tenant branding logo and accent color as last resort) that `mjml()` compiles to email-safe table HTML, with `html-to-text` producing the plain-text alternative. An optional `footer` resolver adds the platform imprint, privacy link and footer text below every structured layout, and an optional `brandSuffix` resolver adds a word after the tenant name in the brand row of the in-code layout. Mail meta `headerChip`, `tone` and `footerNote` add a chip beside the brand, the tone of the top line and chip, and a note above the site and unsubscribe links of the card. Seed an editable system layout with `seedSystemMailLayout(db)`. Requires `renderer-foundation` + `template-resolver`.",
2126
2126
  "toggleableDefault": null,
2127
2127
  "requires": [
2128
2128
  "renderer-foundation",
@@ -2170,7 +2170,7 @@
2170
2170
  },
2171
2171
  {
2172
2172
  "name": "renderer-puppeteer-client",
2173
- "description": "A `renderer-foundation` plugin that prints documents to PDF through a separately deployed `puppeteer-service`. A `document-pdf` request names a stored template (`{ slug, locale, variables }`): the plugin resolves the template and its `pdf-layout` (header, signature and footer regions), embeds every image as a data URI, turns repeating table header rows into a real table head and sends the finished HTML to the service, which performs no network fetches itself. The page format, margins, file name pattern and title come from the template meta. `renderAndStorePdf(ctx, ...)` renders and stores the PDF as a file with a `rendered-pdf` record that keeps the template and layout version, so a stored document can always be traced to the text it was created from. The `puppeteer-status` query tells a TenantAdmin whether the service answers; while it does not, rendering fails with a `puppeteer_unavailable` error (HTTP 503) and no PDF is created. `image-snapshot` requests still take ready HTML in `payload.content`. Configure the endpoint and token with `PUPPETEER_SERVICE_URL` and `PUPPETEER_AUTH_TOKEN`; the client retries 5xx and network errors with exponential backoff (default 3 retries), 4xx fails at once. The feature adds the `rendered_pdfs` table, so apps that mount it need a schema migration.",
2173
+ "description": "A `renderer-foundation` plugin that prints documents to PDF through a separately deployed `puppeteer-service`. A `document-pdf` request names a stored template (`{ slug, locale, variables }`): the plugin resolves the template and its `pdf-layout` (header, signature and footer regions), embeds every image as a data URI, turns repeating table header rows into a real table head and sends the finished HTML to the service, which performs no network fetches itself. The page format, margins, file name pattern and title come from the template meta. A write handler renders with `renderPdfBeforeTransaction(ctx, { slug, locale, variables })` as its `beforeTransaction` phase, so the print service call holds no database connection, and stores the result with `storeRenderedPdf(ctx, event.prepared)` in the handler: a file plus a `rendered-pdf` record that keeps the template and layout version, so a stored document can always be traced to the text it was created from. The `puppeteer-status` query tells a TenantAdmin whether the service answers; while it does not, rendering fails with a `puppeteer_unavailable` error (HTTP 503) and no PDF is created. `image-snapshot` requests still take ready HTML in `payload.content`, which must embed all resources as data: URIs because the service fetches no http(s) resources. Configure the endpoint and token with `PUPPETEER_SERVICE_URL` and `PUPPETEER_AUTH_TOKEN`; the client retries 5xx and network errors with exponential backoff (default 3 retries), 4xx fails at once. The feature adds the `rendered_pdfs` table, so apps that mount it need a schema migration.",
2174
2174
  "toggleableDefault": null,
2175
2175
  "requires": [
2176
2176
  "renderer-foundation"
@@ -2563,7 +2563,7 @@
2563
2563
  },
2564
2564
  {
2565
2565
  "name": "template-resolver",
2566
- "description": "Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`).\n\nReading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`.\n\nText an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation.\n\nHow a collection is edited follows from `contentFormat`: `plain` gives a text area, `rich` a small WYSIWYG (bold, italic, headings, lists, links), `markdown` a text area that stores markdown text (same insertable chips as `plain`). All three offer the collection's `variableSchema` as insertable chips and a preview rendered with sample data — an editor sees what `{{firstName}}` becomes without sending a mail. An app can register its own editor for a format; the last `clientFeature` that registers a format wins (a conflict logs a warning).\n\nA collection is tenant-wide by default. With `ownership: \"user\"` every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate `user-content-entry` entity and count as user data, so mounting one also requires the `template-resolver-user-data` feature and a migration on the app side.\n\n`validate` dry-runs the write pipeline for a draft (sanitizer, layout rules, placeholders) and reports what the template uses and misses. `export-bundle` / `import-bundle` (and `import-bundle-system`) move one template with all locales and resource files between tenants or environments; `copy-locale` starts a translation from an existing locale. Layouts are structured: a mail or PDF layout is `data-header` / `data-content` / `data-signature` / `data-footer` regions, and mjml layout source is system-only.\n\n`preview` renders a draft (or the stored template) with sample values taken from the `example` of each declared variable, through the same gates and the same mail renderer as a real send, and returns subject, html, `displayHtml` (the html with cid images as data URIs for an editor), text, attachments (inline images and files) and `checks` (images resolved, plain text present, size under 10 MiB, required placeholders used); nothing is saved or sent. `send-test` mails that rendering to the calling user's own address only, subject prefixed with `[TEST]`, at most 10 per user and hour; it needs `delivery`, `channel-email` and `renderer-mail-html` to be mounted.\n\nImages used by a template (`<img data-resource=\"logo\">`) are uploaded with `resourceUpload`, which re-encodes them, stores a file and links it under the key; `resourceRemove` unlinks it. `linkedResources` is written only through these two handlers. With `purpose: \"attachment\"` and a `fileName`, `resourceUpload` stores a PDF, PNG or JPEG as a file attachment of a mail template (original bytes, at most 10) that the mail renderer attaches after the inline images. `resourceLinkPdf` instead links a `document-pdf` template under a key: every mail sent from the template then carries that PDF, rendered at send time with the mail's own variables and locale, under a file name pattern such as `invoice-{{number}}.pdf`; files and linked PDFs share the limit of 10.\n\nReplaces the former `text-content` feature; its blocks now live here as kind `text-block`.",
2566
+ "description": "Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`).\n\nReading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`. Saving a draft over an active template keeps the active version served and stores the edit beside it (column `draft`); `publish` makes it the served version, `discardDraft` / `discardDraftSystem` throws it away.\n\nText an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation.\n\nHow a collection is edited follows from `contentFormat`: `plain` gives a text area, `rich` a small WYSIWYG (bold, italic, headings, lists, links), `markdown` a text area that stores markdown text (same insertable chips as `plain`). All three offer the collection's `variableSchema` as insertable chips and a preview rendered with sample data — an editor sees what `{{firstName}}` becomes without sending a mail. An app can register its own editor for a format; the last `clientFeature` that registers a format wins (a conflict logs a warning).\n\nA collection is tenant-wide by default. With `ownership: \"user\"` every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate `user-content-entry` entity and count as user data, so mounting one also requires the `template-resolver-user-data` feature and a migration on the app side.\n\n`validate` dry-runs the write pipeline for a draft (sanitizer, layout rules, placeholders) and reports what the template uses and misses. `export-bundle` / `import-bundle` (and `import-bundle-system`) move one template with all locales and resource files between tenants or environments; `copy-locale` starts a translation from an existing locale. Layouts are structured: a mail or PDF layout is `data-header` / `data-content` / `data-signature` / `data-footer` regions, and mjml layout source is system-only.\n\n`preview` renders a draft (or the stored template) with sample values taken from the `example` of each declared variable, through the same gates and the same mail renderer as a real send, and returns subject, html, `displayHtml` (the html with cid images as data URIs for an editor), text, attachments (inline images and files) and `checks` (images resolved, plain text present, size under 10 MiB, required placeholders used); nothing is saved or sent. `send-test` mails that rendering to the calling user's own address only, subject prefixed with `[TEST]`, at most 10 per user and hour; it needs `delivery`, `channel-email` and `renderer-mail-html` to be mounted.\n\nImages used by a template (`<img data-resource=\"logo\">`) are uploaded with `resourceUpload`, which re-encodes them, stores a file and links it under the key; `resourceRemove` unlinks it. `linkedResources` is written only through these two handlers. With `purpose: \"attachment\"` and a `fileName`, `resourceUpload` stores a PDF, PNG or JPEG as a file attachment of a mail template (original bytes, at most 10) that the mail renderer attaches after the inline images. `resourceLinkPdf` instead links a `document-pdf` template under a key: every mail sent from the template then carries that PDF, rendered at send time with the mail's own variables and locale, under a file name pattern such as `invoice-{{number}}.pdf`; files and linked PDFs share the limit of 10.\n\nReplaces the former `text-content` feature; its blocks now live here as kind `text-block`.",
2567
2567
  "toggleableDefault": null,
2568
2568
  "requires": [],
2569
2569
  "optionalRequires": [],
@@ -2601,6 +2601,8 @@
2601
2601
  "template-resolver:write:archive",
2602
2602
  "template-resolver:write:archive-system",
2603
2603
  "template-resolver:write:copy-locale",
2604
+ "template-resolver:write:discard-draft",
2605
+ "template-resolver:write:discard-draft-system",
2604
2606
  "template-resolver:write:import-bundle",
2605
2607
  "template-resolver:write:import-bundle-system",
2606
2608
  "template-resolver:write:publish",
@@ -3207,7 +3209,7 @@
3207
3209
  },
3208
3210
  {
3209
3211
  "name": "waitlist",
3210
- "description": "Public waitlist: an anonymous, rate-limited `submit` handler collects name, email and optional company, portfolio and message (a honeypot field and per-email dedupe keep bots and repeat submits out, and the response never reveals whether an address was already listed), and mails the submitter a confirmation. SystemAdmins review entries in a list screen and invite or reject them; an invite creates the entrant's own tenant (or joins one shared tenant) and mails an accept link with the configured membership role, never a global role. Entries are platform-wide, their personal data is crypto-shredding-ready, and the EXT_USER_DATA hooks export and erase entries matching a user's email.",
3212
+ "description": "Public waitlist: an anonymous, rate-limited `submit` handler collects name, email and optional company, portfolio and message (a honeypot field and per-email dedupe keep bots and repeat submits out, and the response body is identical whether or not an address was already listed), and mails the submitter a confirmation. SystemAdmins review entries in a list screen and invite or reject them; an invite creates the entrant's own tenant (or joins one shared tenant) and mails an accept link with the configured membership role, never a global role. Entries are platform-wide, their personal data is crypto-shredding-ready, and the EXT_USER_DATA hooks export and erase entries matching a user's email.",
3211
3213
  "toggleableDefault": null,
3212
3214
  "requires": [
3213
3215
  "tenant",
@@ -1,18 +1,13 @@
1
1
  #!/usr/bin/env bun
2
2
  // biome-ignore-all lint/suspicious/noConsole: CLI-Script, console ist Feature.
3
3
  //
4
- // C1 M5 coverage lint. Diffs bundled-features-Exports gegen den
5
- // FEATURE_IMPORT_REGISTRY in samples/apps/use-all-bundled/schema/
6
- // generate.ts. Fängt zwei Drifts:
4
+ // Coverage lint. Diffs die Top-Level-Exports von bundled-features gegen die Features, die
5
+ // use-all-bundled mountet (FEATURES aus kumiko/schema.ts: APP_FEATURES plus die von
6
+ // composeFeatures ergänzten Auth-/Signup-Features). Fängt drei Drifts:
7
7
  //
8
- // 1. Ein neuer feature-export ohne Aufnahme in use-all-bundled
9
- // → unreferenced exports
10
- // 2. Ein removed feature-export der noch im Registry steht
11
- // → stale registry
12
- //
13
- // Held-back features für M0.1 (subscription-stripe, channel-email, …)
14
- // stehen in EXPECTED_HELD_BACK. Sobald sie mit stub-options gemountet
15
- // werden, müssen sie aus dieser Liste raus.
8
+ // 1. Ein Export, der weder gemountet ist noch in EXPECTED_HELD_BACK steht
9
+ // 2. Ein EXPECTED_HELD_BACK-Eintrag, der kein Export mehr ist
10
+ // 3. Ein EXPECTED_HELD_BACK-Eintrag, der inzwischen gemountet ist (stale)
16
11
  //
17
12
  // Usage:
18
13
  // bun samples/apps/use-all-bundled/scripts/check-coverage.ts
@@ -20,6 +15,7 @@
20
15
 
21
16
  import { readFileSync } from "node:fs";
22
17
  import { resolve } from "node:path";
18
+ import { FEATURES } from "../kumiko/schema";
23
19
 
24
20
  const ROOT = resolve(import.meta.dir, "..");
25
21
  const FRAMEWORK_ROOT = resolve(ROOT, "..", "..", "..");
@@ -27,33 +23,13 @@ const FRAMEWORK_ROOT = resolve(ROOT, "..", "..", "..");
27
23
  // Held-back exports: existieren in bundled-features, sind aber nicht
28
24
  // als runtime-feature in use-all-bundled mountbar.
29
25
  //
30
- // Nach M0.1 nur noch utilities + auto-mounted:
31
- // - utilities: kein r.defineFeature (foundation-shared, files-provider-s3
32
- // sind pure helpers).
33
- // - auto-mounted: kommt via composeFeatures(includeBundled:true) automatisch
34
- // (auth-email-password) — wäre doppelt-mount wenn explizit gelistet.
26
+ // Nur Utilities ohne defineFeature (foundation-shared, files-provider-s3 sind pure helpers)
27
+ // und scaffold-only Features. Gemountete Features gehören nicht hierher.
35
28
  const EXPECTED_HELD_BACK = new Set([
36
- "auth-email-password", // auto-mounted via composeFeatures(authOptions)
37
- "agent-tools", // utility (buildToolCatalog: Registry -> ToolDefinition[]), kein defineFeature
38
29
  "files-provider-s3", // utility (createS3Provider helpers), kein defineFeature
39
30
  "foundation-shared", // utilities (requireDefined/requireNonEmpty), kein feature
40
31
  "page-render", // shared HTML render/cache helpers (legal/managed-pages), kein defineFeature
41
32
  "presets", // feature-bundle helpers (dsgvoSelfServiceFeatures, securityBaselineFeatures), kein defineFeature
42
- "auth-foundation", // scaffold-only (#1368): bootCheck wirft ohne gemountete auth-provider-*, die es noch nicht gibt
43
- ]);
44
-
45
- // Registry-only: mounted features that are NOT top-level package exports
46
- // (companion features living under another export path). schema-check still
47
- // needs them in FEATURE_IMPORT_REGISTRY; coverage must not flag them stale.
48
- const EXPECTED_REGISTRY_ONLY = new Set([
49
- "auth-self-registration", // from ./auth-email-password, auto-mounted with signup (#1521)
50
- "collection-labels", // app-local (src/app/), nav labels for this app's content collections
51
- "cap-overview-labels", // app-local (src/app/), labels for this app's example cap-overview caps
52
- "metrics-system", // from ./metrics, companion of createMetricsFeature (platform-wide scope)
53
- "locale-de", // @cosmicdrift/kumiko-locale-de, not a bundled-features export
54
- "renderer-mail-html", // @cosmicdrift/kumiko-renderer-mail-html, auto-mounted with auth by composeFeatures
55
- "renderer-puppeteer-client", // @cosmicdrift/kumiko-renderer-puppeteer-client, not a bundled-features export
56
- "document-designer", // @cosmicdrift/kumiko-document-designer, not a bundled-features export
57
33
  ]);
58
34
 
59
35
  // Sub-paths in bundled-features's package.json exports (./tenant/seeding,
@@ -87,120 +63,60 @@ function readBundledExports(): Set<string> {
87
63
  return out;
88
64
  }
89
65
 
90
- function extractRegistryBlock(src: string): string {
91
- // Anchor auf den FEATURE_IMPORT_REGISTRY-Object-Body. Ohne diesen Anchor
92
- // würde die kind-Regex jedes `<ident>: { kind: "factory"|"named" }` im File
93
- // matchen (z.B. eine künftige Hilfsvariable) → false positive/negative.
94
- const startMatch = /FEATURE_IMPORT_REGISTRY[^=]*=\s*\{/.exec(src);
95
- if (!startMatch) {
96
- throw new Error("check-coverage: FEATURE_IMPORT_REGISTRY block not found in generate.ts");
97
- }
98
- const bodyStart = startMatch.index + startMatch[0].length;
99
- // Top-level-Close ist das erste `};` am Zeilenanfang nach bodyStart.
100
- const closeMatch = /\n};/.exec(src.slice(bodyStart));
101
- if (!closeMatch) {
102
- throw new Error("check-coverage: FEATURE_IMPORT_REGISTRY closing `};` not found");
103
- }
104
- return src.slice(bodyStart, bodyStart + closeMatch.index);
105
- }
106
-
107
- function readRegistryFeatures(): Set<string> {
108
- const generateSrc = readFileSync(resolve(ROOT, "schema", "generate.ts"), "utf-8");
109
- const registryBlock = extractRegistryBlock(generateSrc);
110
- // Match object-keys mit Discriminator `kind: "factory" | "named"`.
111
- // Property-Keys können quoted ("billing-foundation") oder unquoted
112
- // (config, user) sein — beides ist gültig wenn kebab-segment-free.
113
- const re = /(?:"([a-z][a-z0-9-]*)"|([a-z][a-z0-9]*)):\s*\{\s*kind:\s*"(factory|named)"/g;
114
- const out = new Set<string>();
115
- for (const m of registryBlock.matchAll(re)) {
116
- const name = m[1] ?? m[2];
117
- if (name) out.add(name);
118
- }
119
- return out;
120
- }
121
-
122
66
  const exports = readBundledExports();
123
- const registry = readRegistryFeatures();
124
-
125
- const unreferenced: string[] = [];
126
- const stale: string[] = [];
127
- // Held-back-Einträge, die kein bundled-Export mehr sind → stale Konfig.
128
- // Ohne diese Prüfung hätte der Drift-Guard in seiner eigenen Konfig einen
129
- // Blind-Spot: ein umbenanntes/entferntes Feature bliebe still in der Liste.
130
- const staleHeldBack: string[] = [];
131
-
132
- for (const name of exports) {
133
- if (registry.has(name)) continue;
134
- if (EXPECTED_HELD_BACK.has(name)) continue;
135
- unreferenced.push(name);
136
- }
137
-
138
- for (const name of registry) {
139
- if (!exports.has(name) && !EXPECTED_REGISTRY_ONLY.has(name)) stale.push(name);
140
- }
67
+ const mounted = new Set<string>(FEATURES.map((feature) => feature.name));
141
68
 
142
- for (const name of EXPECTED_HELD_BACK) {
143
- if (!exports.has(name)) staleHeldBack.push(name);
144
- }
69
+ const unreferenced = [...exports].filter(
70
+ (name) => !mounted.has(name) && !EXPECTED_HELD_BACK.has(name),
71
+ );
72
+ // Ohne diese Prüfungen hätte der Guard in seiner eigenen Konfig einen Blind-Spot: ein
73
+ // umbenanntes oder entferntes Feature bliebe still in der Liste, ein gemountetes ebenso.
74
+ const staleHeldBack = [...EXPECTED_HELD_BACK].filter((name) => !exports.has(name));
75
+ const mountedHeldBack = [...EXPECTED_HELD_BACK].filter((name) => mounted.has(name));
145
76
 
146
77
  let ok = true;
147
78
 
148
79
  if (unreferenced.length > 0) {
149
80
  console.error(
150
- `\n❌ ${unreferenced.length} bundled-feature export(s) NICHT in use-all-bundled/schema/generate.ts:`,
81
+ `\n❌ ${unreferenced.length} bundled-feature export(s) nicht in use-all-bundled gemountet:`,
151
82
  );
152
83
  for (const name of unreferenced.sort()) {
153
84
  console.error(` - ${name}`);
154
85
  }
155
86
  console.error(
156
- "\n Action: in src/run-config.ts + schema/generate.ts FEATURE_IMPORT_REGISTRY aufnehmen,",
157
- );
158
- console.error(
159
- " oder (wenn mount-options noch fehlen) zu EXPECTED_HELD_BACK in dieser Datei hinzufügen.",
87
+ "\n Action: in src/run-config.ts mounten, oder (wenn mount-options noch fehlen) zu EXPECTED_HELD_BACK in dieser Datei hinzufügen.",
160
88
  );
161
89
  ok = false;
162
90
  }
163
91
 
164
- if (stale.length > 0) {
92
+ if (staleHeldBack.length > 0) {
165
93
  console.error(
166
- `\n❌ ${stale.length} FEATURE_IMPORT_REGISTRY entry/entries ohne Match in bundled-features exports:`,
94
+ `\n❌ ${staleHeldBack.length} EXPECTED_HELD_BACK entry/entries ohne Match in bundled-features exports:`,
167
95
  );
168
- for (const name of stale.sort()) {
96
+ for (const name of staleHeldBack.sort()) {
169
97
  console.error(` - ${name}`);
170
98
  }
171
99
  console.error(
172
- "\n Action: aus schema/generate.ts FEATURE_IMPORT_REGISTRY entfernen, ggf. App-eigenes Feature.",
100
+ "\n Action: aus EXPECTED_HELD_BACK in dieser Datei entfernen (Feature wurde umbenannt/entfernt).",
173
101
  );
174
102
  ok = false;
175
103
  }
176
104
 
177
- if (staleHeldBack.length > 0) {
105
+ if (mountedHeldBack.length > 0) {
178
106
  console.error(
179
- `\n❌ ${staleHeldBack.length} EXPECTED_HELD_BACK entry/entries ohne Match in bundled-features exports:`,
107
+ `\n❌ ${mountedHeldBack.length} EXPECTED_HELD_BACK entry/entries sind gemountet (stale):`,
180
108
  );
181
- for (const name of staleHeldBack.sort()) {
109
+ for (const name of mountedHeldBack.sort()) {
182
110
  console.error(` - ${name}`);
183
111
  }
184
- console.error(
185
- "\n Action: aus EXPECTED_HELD_BACK in dieser Datei entfernen (Feature wurde umbenannt/entfernt).",
186
- );
112
+ console.error("\n Action: aus EXPECTED_HELD_BACK in dieser Datei entfernen.");
187
113
  ok = false;
188
114
  }
189
115
 
190
- if (ok) {
191
- // Disjunkte Zahlen: `mountable` = exports ohne held-back; `coveredCount`
192
- // zählt nur Registry-Einträge, die ein echter mountbarer Export sind
193
- // (auth-email-password steht im Registry für schema-check-Konsistenz,
194
- // ist aber held-back — würde sonst die Ratio >1 verfälschen).
195
- const mountable = exports.size - EXPECTED_HELD_BACK.size;
196
- let coveredCount = 0;
197
- for (const name of registry) {
198
- if (exports.has(name) && !EXPECTED_HELD_BACK.has(name)) coveredCount++;
199
- }
200
- console.log(
201
- `✓ Coverage: ${coveredCount}/${mountable} mountable bundled-features im Registry, ${EXPECTED_HELD_BACK.size} held-back (M0.1).`,
202
- );
203
- process.exit(0);
204
- } else {
205
- process.exit(1);
206
- }
116
+ if (!ok) process.exit(1);
117
+
118
+ const mountable = exports.size - EXPECTED_HELD_BACK.size;
119
+ const mountedExports = [...exports].filter((name) => mounted.has(name)).length;
120
+ console.log(
121
+ `✓ Coverage: ${mountedExports}/${mountable} mountable bundled-features gemountet, ${EXPECTED_HELD_BACK.size} held-back.`,
122
+ );
@@ -6,7 +6,7 @@
6
6
  // proving the "extend any object" promise on real, bootable screens.
7
7
  //
8
8
  // Deliberately NOT in src/run-config.ts APP_FEATURES: that set is bundled-only
9
- // (schema/generate.ts FEATURE_IMPORT_REGISTRY + check-coverage gate it). Like
9
+ // (check-coverage compares it against the bundled-features exports). Like
10
10
  // appScreensFeature, this app-owned feature is mounted only in the dev server
11
11
  // (src/app/dev-app-options.ts), where runDevApp dev-pushes its table into the ephemeral
12
12
  // DB. Nothing here ships to prod / the schema pipeline.
@@ -442,6 +442,3 @@ export const AUTH_COMPOSE_OPTIONS = {
442
442
  tokenTtlMinutes: 60,
443
443
  },
444
444
  } as const;
445
-
446
- /** schema-check: auth-self-registration is mounted when signup is set. */
447
- export const HAS_SIGNUP = true;
@@ -10,7 +10,7 @@ The production wiring shape: mount `createAuthMfaFeature()` next to `createAuthE
10
10
 
11
11
  1. `createAuthMfaFeature({ setupTokenSecret, challengeTokenSecret, issuer })` builds the feature; `mfaStatusCheckerFromFeature(mfaFeature)` reads its login-status callback back off.
12
12
  2. That callback goes into `createAuthEmailPasswordFeature({ mfaStatusChecker: ... })` — `login.write.ts` calls it after the password check to decide whether to mint a JWT or a challenge.
13
- 3. `buildServer({ auth: { mfaVerifyHandler: AuthMfaHandlers.verify } })` mounts `POST /auth/mfa/verify`, a framework route (not a dispatcher write-handler) with its own rate limiter, since it runs before a JWT exists.
13
+ 3. `buildServer({ auth: { mfaVerifyHandler: AuthMfaHandlers.verify, switchTenantMfaGateHandler: AuthHandlers.switchTenantMfaGate } })` (the gate is required with `mfaVerifyHandler`, so a tenant switch asks for the second factor too) mounts `POST /auth/mfa/verify`, a framework route (not a dispatcher write-handler) with its own rate limiter, since it runs before a JWT exists.
14
14
  4. If `sessions` is mounted too, `bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(sessionCallbacks.sessionRevokeAllOthers)` wires "enable/disable/regenerate signs out every other session."
15
15
 
16
16
  ## What to copy into your app
@@ -43,6 +43,7 @@ const server = buildServer({
43
43
  membershipQuery: "tenant:query:memberships",
44
44
  loginHandler: "auth-email-password:[REDACTED:API key param]",
45
45
  mfaVerifyHandler: AuthMfaHandlers.verify,
46
+ switchTenantMfaGateHandler: AuthHandlers.switchTenantMfaGate,
46
47
  sessionCreator: sessionCallbacks.sessionCreator,
47
48
  sessionRevoker: sessionCallbacks.sessionRevoker,
48
49
  sessionChecker: sessionCallbacks.sessionChecker,
@@ -37,6 +37,7 @@
37
37
  // membershipQuery: "tenant:query:memberships",
38
38
  // loginHandler: "auth-email-password:[REDACTED:API key param]",
39
39
  // mfaVerifyHandler: AuthMfaHandlers.verify,
40
+ // switchTenantMfaGateHandler: AuthHandlers.switchTenantMfaGate,
40
41
  // sessionCreator: sessionCallbacks.sessionCreator,
41
42
  // sessionRevoker: sessionCallbacks.sessionRevoker,
42
43
  // sessionChecker: sessionCallbacks.sessionChecker,
@@ -22,9 +22,10 @@ logic is yours.
22
22
  - **`skipOptimisticLock` for last-writer-wins** — `reset` deliberately
23
23
  bypasses the version check because the operation is admin-driven and
24
24
  resetting twice is semantically the same as resetting once.
25
- - **Custom queries** — `counter:active` filters the projection in
26
- TypeScript after the executor's list returns. Useful when the filter is
27
- computed (above-threshold) rather than indexed.
25
+ - **Custom queries** — `counter:active` passes a server-side `filter`
26
+ (`eq`/`ne`/`lt`/`gt`/`lte`/`gte`/`in`) and `totalCount` to the executor's
27
+ list. Never filter a page in TypeScript: list is paginated, so rows after
28
+ the first page would be missed.
28
29
  - **`failNotFound` helper** — typed error response for the 404 case
29
30
  without a hand-built `KumikoError` subclass.
30
31
 
@@ -1,5 +1,5 @@
1
1
  // Custom Handlers Sample
2
- // Shows: writeHandler with business logic, queryHandler with custom filtering,
2
+ // Shows: writeHandler with business logic, queryHandler with a server-side filter,
3
3
  // handler that modifies payload before DB write.
4
4
 
5
5
  import {
@@ -12,6 +12,9 @@ import {
12
12
  import { failNotFound } from "@cosmicdrift/kumiko-framework/errors";
13
13
  import { z } from "zod";
14
14
 
15
+ // detail() returns an untyped row; parse the two fields the increment needs once.
16
+ const counterVersionRowSchema = z.object({ count: z.number(), version: z.number() });
17
+
15
18
  export const counterEntity = createEntity({
16
19
  table: "read_sample_counters",
17
20
  fields: {
@@ -48,13 +51,14 @@ export const counterFeature = defineFeature("counters", (r) => {
48
51
  return failNotFound("counter", event.payload.id);
49
52
  }
50
53
 
51
- const newCount = (current["count"] as number) + event.payload.amount;
54
+ const { count, version } = counterVersionRowSchema.parse(current);
55
+ const newCount = count + event.payload.amount;
52
56
  // Read-modify-write on counter: use the version we just read so two
53
57
  // concurrent increments don't clobber each other's changes.
54
58
  return counterExecutor.update(
55
59
  {
56
60
  id: event.payload.id,
57
- version: current["version"] as number,
61
+ version,
58
62
  changes: { count: newCount, lastIncrementedBy: `user:${event.user.id}` },
59
63
  },
60
64
  event.user,
@@ -80,17 +84,22 @@ export const counterFeature = defineFeature("counters", (r) => {
80
84
  { access: { roles: ["Admin"] } },
81
85
  );
82
86
 
83
- // Custom query: only counters above a threshold
87
+ // Custom query: only counters above a threshold. list() returns one page
88
+ // (default 50, max 200, MAX_LIST_LIMIT), so a JS filter would miss rows after
89
+ // page 1; filter in the query instead. totalCount counts over the whole WHERE.
84
90
  r.queryHandler(
85
91
  "counter:active",
86
- z.object({ minCount: z.number().default(1) }),
87
- async (query, ctx) => {
88
- const all = await counterExecutor.list({}, query.user, ctx.db);
89
- return {
90
- ...all,
91
- rows: all.rows.filter((r) => (r["count"] as number) >= query.payload.minCount),
92
- };
93
- },
92
+ z.object({ minCount: z.number().int().default(1), cursor: z.string().optional() }),
93
+ async (query, ctx) =>
94
+ counterExecutor.list(
95
+ {
96
+ filter: { field: "count", op: "gte", value: query.payload.minCount },
97
+ totalCount: true,
98
+ cursor: query.payload.cursor,
99
+ },
100
+ query.user,
101
+ ctx.db,
102
+ ),
94
103
  {
95
104
  access: {
96
105
  openToAll: {
@@ -0,0 +1,38 @@
1
+ # Dashboard stats
2
+
3
+ Key figures on a dashboard. This recipe ships a `ticket` entity, one query
4
+ handler that returns the figures as a flat object, and a `dashboard` screen
5
+ with a stat group (open, closed) and a single stat (total).
6
+
7
+ The point is to show how the three pieces connect: the screen only names a
8
+ query and the fields to read, the query does the counting.
9
+
10
+ ## What it shows
11
+
12
+ - **Panels reference a query by qualified name**: each `stat` carries
13
+ `query: "tickets:query:ticket:stats"`. The dashboard calls that handler and
14
+ renders the result.
15
+ - **`valueField` picks a field from a flat result**: the handler returns
16
+ `{ open, closed, total }`, and each stat names one of those keys.
17
+ - **`stat-group` and `stat`**: a group shows several stats side by side under
18
+ one title, a top-level `stat` stands alone.
19
+ - **Counting happens in the database**: the handler calls the executor's
20
+ `list` with `filter`, `limit: 1` and `totalCount: true`. The total is counted
21
+ over the whole WHERE clause, so no rows are loaded just to be counted.
22
+ - **Labels are translation keys**: declared with `r.translations`.
23
+
24
+ ## When to reach for it
25
+
26
+ You want a landing screen with a few numbers: open items, overdue invoices,
27
+ this month's revenue. Write one query per screen that returns every figure,
28
+ and let the stats pick their field.
29
+
30
+ ## Source
31
+
32
+ The whole feature lives in `src/feature.ts` (~100 lines). The integration test
33
+ checks that every stat reads a field the query returns, and that the query
34
+ counts open and closed tickets correctly.
35
+
36
+ ```bash
37
+ bun kumiko test integration samples/dashboard-stats
38
+ ```
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@cosmicdrift/kumiko-sample-dashboard-stats",
3
+ "description": "Key figures on a dashboard: stat panels backed by one counting query.",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@cosmicdrift/kumiko-framework": "workspace:*"
7
+ }
8
+ }
@@ -0,0 +1,104 @@
1
+ // Dashboard Stats Sample
2
+ // Shows: a dashboard screen with a stat-group and a single stat, backed by one
3
+ // query handler that counts in the database.
4
+
5
+ import {
6
+ createEntity,
7
+ createEntityExecutor,
8
+ createSelectField,
9
+ createTextField,
10
+ defineFeature,
11
+ } from "@cosmicdrift/kumiko-framework/engine";
12
+ import { z } from "zod";
13
+
14
+ export const TICKET_STATUSES = ["open", "closed"] as const;
15
+
16
+ export const ticketEntity = createEntity({
17
+ table: "read_sample_tickets",
18
+ fields: {
19
+ title: createTextField({ personal: false, reason: "is_business_data", required: true }),
20
+ status: createSelectField({ options: TICKET_STATUSES, default: "open", required: true }),
21
+ },
22
+ });
23
+
24
+ export const TICKET_STATS_QUERY = "tickets:query:ticket:stats";
25
+
26
+ const readerRoles = { roles: ["Admin", "User"] } as const;
27
+
28
+ export const ticketFeature = defineFeature("tickets", (r) => {
29
+ const { executor: ticketExecutor } = createEntityExecutor("ticket", ticketEntity);
30
+
31
+ r.crud("ticket", ticketEntity, {
32
+ write: { access: { roles: ["Admin"] } },
33
+ read: { access: readerRoles },
34
+ });
35
+
36
+ // Flat result: each dashboard stat reads one field via its valueField.
37
+ // limit: 1 keeps the page tiny; totalCount counts over the whole WHERE in
38
+ // the database, so no rows are loaded just to be counted.
39
+ r.queryHandler(
40
+ "ticket:stats",
41
+ z.object({}),
42
+ async (query, ctx) => {
43
+ const countWithStatus = async (status: (typeof TICKET_STATUSES)[number]): Promise<number> => {
44
+ const page = await ticketExecutor.list(
45
+ { filter: { field: "status", op: "eq", value: status }, limit: 1, totalCount: true },
46
+ query.user,
47
+ ctx.db,
48
+ );
49
+ return page.total ?? 0;
50
+ };
51
+ const [open, closed] = await Promise.all([
52
+ countWithStatus("open"),
53
+ countWithStatus("closed"),
54
+ ]);
55
+ return { open, closed, total: open + closed };
56
+ },
57
+ { access: readerRoles },
58
+ );
59
+
60
+ r.translations({
61
+ keys: {
62
+ "tickets.dashboard.group": { en: "Tickets by status" },
63
+ "tickets.dashboard.open": { en: "Open" },
64
+ "tickets.dashboard.closed": { en: "Closed" },
65
+ "tickets.dashboard.total": { en: "All tickets" },
66
+ },
67
+ });
68
+
69
+ r.screen({
70
+ id: "ticket-dashboard",
71
+ type: "dashboard",
72
+ panels: [
73
+ {
74
+ kind: "stat-group",
75
+ id: "by-status",
76
+ label: "tickets.dashboard.group",
77
+ stats: [
78
+ {
79
+ kind: "stat",
80
+ id: "open",
81
+ label: "tickets.dashboard.open",
82
+ query: TICKET_STATS_QUERY,
83
+ valueField: "open",
84
+ },
85
+ {
86
+ kind: "stat",
87
+ id: "closed",
88
+ label: "tickets.dashboard.closed",
89
+ query: TICKET_STATS_QUERY,
90
+ valueField: "closed",
91
+ },
92
+ ],
93
+ },
94
+ {
95
+ kind: "stat",
96
+ id: "total",
97
+ label: "tickets.dashboard.total",
98
+ query: TICKET_STATS_QUERY,
99
+ valueField: "total",
100
+ },
101
+ ],
102
+ access: readerRoles,
103
+ });
104
+ });