@cosmicdrift/kumiko-samples 0.362.0 → 0.364.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +23 -6
  3. package/packages/bundled-features/src/admin-shell/changes.json +6 -0
  4. package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
  5. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
  6. package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
  7. package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
  8. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
  9. package/packages/bundled-features/src/audit/changes.json +6 -0
  10. package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
  11. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
  12. package/packages/bundled-features/src/auth-email-password/changes.json +51 -0
  13. package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
  14. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  15. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  16. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
  17. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  18. package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
  19. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  20. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  21. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  22. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  23. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
  24. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +5 -13
  25. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  26. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  27. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  28. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  29. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
  30. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  31. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  32. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  33. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  34. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  35. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  36. package/packages/bundled-features/src/billing-foundation/changes.json +43 -0
  37. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  38. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +90 -44
  39. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
  40. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  41. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +118 -0
  42. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  43. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
  44. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +39 -33
  45. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  46. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  47. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
  48. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
  49. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +36 -20
  50. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
  51. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
  52. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  53. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
  54. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  55. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  56. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +12 -6
  57. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  58. package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
  59. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +20 -11
  60. package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
  61. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
  62. package/packages/bundled-features/src/cap-counter/changes.json +6 -0
  63. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
  64. package/packages/bundled-features/src/cap-counter/index.ts +1 -0
  65. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
  66. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  67. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  68. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  69. package/packages/bundled-features/src/channel-email/email-channel.ts +135 -29
  70. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  71. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  72. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  73. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  74. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  75. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  76. package/packages/bundled-features/src/config/changes.json +6 -0
  77. package/packages/bundled-features/src/config/handlers/set.write.ts +3 -7
  78. package/packages/bundled-features/src/config/resolver.ts +15 -12
  79. package/packages/bundled-features/src/config/write-helpers.ts +23 -0
  80. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  81. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
  82. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  83. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  84. package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
  85. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  86. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  87. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  88. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  89. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  90. package/packages/bundled-features/src/delivery/attachment-ref.ts +105 -0
  91. package/packages/bundled-features/src/delivery/changes.json +66 -0
  92. package/packages/bundled-features/src/delivery/channel-context.ts +152 -4
  93. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  94. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  95. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  96. package/packages/bundled-features/src/delivery/index.ts +19 -2
  97. package/packages/bundled-features/src/delivery/jobs.ts +97 -17
  98. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  99. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  100. package/packages/bundled-features/src/delivery/types.ts +34 -18
  101. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  102. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  103. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  104. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  105. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  106. package/packages/bundled-features/src/file-foundation/changes.json +6 -0
  107. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  108. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +89 -0
  109. package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
  110. package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
  111. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  112. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  113. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  114. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  115. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  116. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
  117. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  118. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  119. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  120. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  121. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  122. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
  123. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  124. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  125. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  126. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  127. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +42 -25
  128. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  129. package/packages/bundled-features/src/jobs/changes.json +19 -0
  130. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
  131. package/packages/bundled-features/src/jobs/feature.ts +7 -4
  132. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  133. package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
  134. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
  135. package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
  136. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
  137. package/packages/bundled-features/src/ledger/changes.json +7 -0
  138. package/packages/bundled-features/src/ledger/entity.ts +3 -1
  139. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  140. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  141. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  142. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  143. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  144. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  145. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  146. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  147. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  148. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  149. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  150. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  151. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  152. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  153. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  154. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  155. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  156. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  157. package/packages/bundled-features/src/renderer-foundation/types.ts +134 -35
  158. package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
  159. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  160. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
  161. package/packages/bundled-features/src/secrets/changes.json +6 -0
  162. package/packages/bundled-features/src/secrets/feature.ts +4 -3
  163. package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
  164. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  165. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  166. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  167. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  168. package/packages/bundled-features/src/shared/changes.json +6 -0
  169. package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
  170. package/packages/bundled-features/src/shared/index.ts +6 -0
  171. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  172. package/packages/bundled-features/src/shared/mail-template-body.ts +42 -0
  173. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  174. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  175. package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
  176. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
  177. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
  178. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  179. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  180. package/packages/bundled-features/src/template-resolver/README.md +66 -8
  181. package/packages/bundled-features/src/template-resolver/api.ts +85 -9
  182. package/packages/bundled-features/src/template-resolver/changes.json +186 -0
  183. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +278 -0
  184. package/packages/bundled-features/src/template-resolver/constants.ts +49 -11
  185. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  186. package/packages/bundled-features/src/template-resolver/document-schema.ts +345 -0
  187. package/packages/bundled-features/src/template-resolver/feature.ts +59 -6
  188. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  189. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  190. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +160 -0
  191. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +33 -0
  192. package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
  193. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +138 -0
  194. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -6
  195. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  196. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  197. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +151 -0
  198. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +19 -18
  199. package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
  200. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +323 -0
  201. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  202. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  203. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  204. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  205. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  206. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  207. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +135 -0
  208. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  209. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  210. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +196 -0
  211. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +233 -0
  212. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +137 -0
  213. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +91 -17
  214. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  215. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +16 -77
  216. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +31 -0
  217. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  218. package/packages/bundled-features/src/template-resolver/index.ts +111 -0
  219. package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
  220. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  221. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  222. package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
  223. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  224. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  225. package/packages/bundled-features/src/template-resolver/qualified-names.ts +34 -0
  226. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  227. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  228. package/packages/bundled-features/src/template-resolver/seeding.ts +171 -20
  229. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  230. package/packages/bundled-features/src/template-resolver/table.ts +27 -1
  231. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  232. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  233. package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
  234. package/packages/bundled-features/src/template-resolver/template-meta.ts +192 -0
  235. package/packages/bundled-features/src/template-resolver/template-preview.ts +676 -0
  236. package/packages/bundled-features/src/template-resolver/template-resources.ts +114 -0
  237. package/packages/bundled-features/src/template-resolver/template-usage.ts +68 -0
  238. package/packages/bundled-features/src/template-resolver/template-validation.ts +230 -0
  239. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  240. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +20 -13
  241. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +215 -0
  242. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +37 -0
  243. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1065 -0
  244. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +72 -0
  245. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  246. package/packages/bundled-features/src/template-resolver/web/i18n.ts +121 -0
  247. package/packages/bundled-features/src/template-resolver/web/index.ts +26 -0
  248. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  249. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +176 -0
  250. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  251. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  252. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  253. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  254. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  255. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  256. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  257. package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
  258. package/packages/bundled-features/src/user/changes.json +6 -0
  259. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  260. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  261. package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
  262. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  263. package/packages/bundled-features/src/user/seeding.ts +19 -5
  264. package/packages/bundled-features/src/user-data-rights/changes.json +13 -0
  265. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  266. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  267. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  268. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  269. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  270. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  271. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  272. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  273. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  274. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  275. package/packages/bundled-features/src/user-profile/changes.json +1 -1
  276. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  277. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  278. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  279. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  280. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  281. package/packages/bundled-features/src/waitlist/options.ts +1 -1
  282. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  283. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +3 -3
  284. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
  285. package/samples/apps/ui-walkthrough/package.json +11 -0
  286. package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
  287. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  288. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  289. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +239 -0
  290. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  291. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  292. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +570 -0
  293. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +72 -0
  294. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  295. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  296. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  297. package/samples/apps/ui-walkthrough/src/run-config.ts +41 -1
  298. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  299. package/samples/apps/use-all-bundled/feature-manifest.json +118 -21
  300. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  301. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  302. package/samples/apps/use-all-bundled/package.json +2 -0
  303. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  304. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  305. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  306. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  307. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  308. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  309. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  310. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  311. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  312. package/samples/recipes/delivery-notifications/README.md +26 -25
  313. package/samples/recipes/delivery-notifications/package.json +2 -1
  314. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  315. package/samples/recipes/seed-migration/README.md +1 -1
  316. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  317. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  318. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  319. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  320. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  321. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -0,0 +1,114 @@
1
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import type {
3
+ HandlerContext,
4
+ SessionUser,
5
+ TenantId,
6
+ WriteResult,
7
+ } from "@cosmicdrift/kumiko-framework/engine";
8
+ import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
9
+ import { createFileRefFromBytes, fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
10
+ import { fileContextForTenant } from "../file-foundation/index.js";
11
+ import { requireForTenant } from "../shared/index.js";
12
+ import { executor } from "./handlers/shared.js";
13
+ import type { LinkedResourceEntry } from "./linked-resources.js";
14
+ import { type TemplateResourceRow, templateResourcesTable } from "./table.js";
15
+
16
+ export type TemplateResourceFile = {
17
+ readonly fileName: string;
18
+ readonly contentType: string;
19
+ readonly bytes: Uint8Array;
20
+ };
21
+
22
+ type FileRefRow = {
23
+ readonly storageKey: string;
24
+ readonly fileName: string;
25
+ readonly mimeType: string;
26
+ };
27
+
28
+ // Reads the file behind a template resource. Null when the fileRef is unknown
29
+ // to the caller's tenant or has no bytes.
30
+ export async function readTemplateResource(
31
+ ctx: Pick<HandlerContext, "db" | "files" | "user">,
32
+ fileRefId: string,
33
+ ): Promise<TemplateResourceFile | null> {
34
+ if (!ctx.files) return null;
35
+ const row = await fetchOne<FileRefRow>(ctx.db, fileRefsTable, {
36
+ id: fileRefId,
37
+ isDeleted: false,
38
+ });
39
+ if (!row) return null;
40
+ const handle = ctx.files.refFromFileRef(row);
41
+ if (!(await handle.exists())) return null;
42
+ return { fileName: row.fileName, contentType: row.mimeType, bytes: await handle.read() };
43
+ }
44
+
45
+ // Gives the target tenant its own copy of every file behind the entries, so a template that was
46
+ // copied from the system default keeps working once the tenant exports it or measures its
47
+ // attachments. Rendered-PDF links carry no file and pass through. An entry whose source file is
48
+ // gone is left out: it could not be resolved at render time either.
49
+ export async function duplicateLinkedResourceFiles(
50
+ ctx: HandlerContext,
51
+ args: {
52
+ readonly user: SessionUser;
53
+ readonly tenantId: TenantId;
54
+ readonly resources: Readonly<Record<string, LinkedResourceEntry>>;
55
+ },
56
+ ): Promise<WriteResult<Record<string, LinkedResourceEntry>>> {
57
+ const { user, tenantId } = args;
58
+ const { db } = requireForTenant(ctx, tenantId);
59
+ const copies: Record<string, LinkedResourceEntry> = {};
60
+ for (const [key, entry] of Object.entries(args.resources)) {
61
+ if (typeof entry !== "string" && entry.kind === "rendered-pdf") {
62
+ copies[key] = entry;
63
+ continue;
64
+ }
65
+ const sourceFileRefId = typeof entry === "string" ? entry : entry.fileRefId;
66
+ const file = await readTemplateResource(ctx, sourceFileRefId);
67
+ // skip: the source file is gone, there is nothing to copy
68
+ if (file === null) continue;
69
+ const created = await createFileRefFromBytes(
70
+ {
71
+ db,
72
+ files: fileContextForTenant(ctx, ctx.user.tenantId, tenantId),
73
+ registry: ctx.registry,
74
+ user: { ...user, tenantId },
75
+ },
76
+ { fileName: file.fileName, mimeType: file.contentType, data: file.bytes },
77
+ );
78
+ if (!created.isSuccess) return created;
79
+ copies[key] =
80
+ typeof entry === "string" ? created.data.id : { ...entry, fileRefId: created.data.id };
81
+ }
82
+ return { isSuccess: true, data: copies };
83
+ }
84
+
85
+ // Replaces the whole key map of one row with files that are already stored (bundle import,
86
+ // copy-locale). Single keys go through storeTemplateResource and resource-remove.
87
+ export async function attachTemplateResources(
88
+ ctx: Pick<HandlerContext, "forTenant">,
89
+ args: {
90
+ readonly user: SessionUser;
91
+ readonly tenantId: TenantId;
92
+ readonly templateId: string;
93
+ readonly resources: Readonly<Record<string, LinkedResourceEntry>>;
94
+ },
95
+ ): Promise<WriteResult<null>> {
96
+ if (Object.keys(args.resources).length === 0) return { isSuccess: true, data: null };
97
+ const { db, streamTenantId } = requireForTenant(ctx, args.tenantId);
98
+ const row = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
99
+ id: args.templateId,
100
+ tenantId: args.tenantId,
101
+ });
102
+ if (!row) return writeFailure(new NotFoundError("template-resource", args.templateId));
103
+ const result = await executor.update(
104
+ {
105
+ id: row.id,
106
+ version: row.version,
107
+ changes: { linkedResources: JSON.stringify(args.resources) },
108
+ },
109
+ args.user,
110
+ db,
111
+ { streamTenantId },
112
+ );
113
+ return result.isSuccess ? { isSuccess: true, data: null } : result;
114
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ type NotificationDefinition,
3
+ parseQn,
4
+ type Registry,
5
+ } from "@cosmicdrift/kumiko-framework/engine";
6
+ import { DEFAULT_MAIL_LAYOUT_SLUG, type TemplateKind } from "./constants.js";
7
+
8
+ type UsageRole = "template" | "layout";
9
+
10
+ export type TemplateUsageEntry = {
11
+ readonly qualifiedName: string;
12
+ readonly feature: string;
13
+ readonly notification: string;
14
+ readonly trigger: string | null;
15
+ readonly role: UsageRole;
16
+ };
17
+
18
+ /** Layout slug of the effective stored mail template, by notification qualified
19
+ * name and locale. The renderer reads the layout from the stored row's meta,
20
+ * so it wins over the code-registered notification meta. */
21
+ export type EffectiveMailLayoutSlugs = ReadonlyMap<string, ReadonlyMap<string, string>>;
22
+
23
+ function layoutSlugsOf(
24
+ qualifiedName: string,
25
+ notification: NotificationDefinition,
26
+ effectiveLayouts: EffectiveMailLayoutSlugs,
27
+ ): string[] {
28
+ const stored = effectiveLayouts.get(qualifiedName);
29
+ const locales = new Set([
30
+ ...Object.keys(notification.template ?? {}),
31
+ ...Object.keys(notification.meta ?? {}),
32
+ ...(stored?.keys() ?? []),
33
+ ]);
34
+ return [...locales].map(
35
+ (locale) =>
36
+ stored?.get(locale) ?? notification.meta?.[locale]?.layoutSlug ?? DEFAULT_MAIL_LAYOUT_SLUG,
37
+ );
38
+ }
39
+
40
+ export function collectTemplateUsage(
41
+ registry: Registry,
42
+ slug: string,
43
+ kind: TemplateKind,
44
+ effectiveLayouts: EffectiveMailLayoutSlugs = new Map(),
45
+ ): TemplateUsageEntry[] {
46
+ const usedBy: TemplateUsageEntry[] = [];
47
+ if (kind === "mail" || kind === "mail-layout") {
48
+ for (const [qualifiedName, notification] of registry.getAllNotifications()) {
49
+ const role: UsageRole = kind === "mail" ? "template" : "layout";
50
+ // Notifications without a code template are not rendered through the resolver.
51
+ const isUsed =
52
+ kind === "mail"
53
+ ? qualifiedName === slug && notification.template !== undefined
54
+ : layoutSlugsOf(qualifiedName, notification, effectiveLayouts).includes(slug);
55
+ if (!isUsed) continue;
56
+ const parsed = parseQn(qualifiedName);
57
+ usedBy.push({
58
+ qualifiedName,
59
+ feature: parsed.scope,
60
+ notification: parsed.name,
61
+ trigger: notification.trigger?.on ?? null,
62
+ role,
63
+ });
64
+ }
65
+ }
66
+ usedBy.sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName));
67
+ return usedBy;
68
+ }
@@ -0,0 +1,230 @@
1
+ import type {
2
+ Registry,
3
+ TemplateScalarVariableDef,
4
+ TemplateVariableDef,
5
+ TemplateVariables,
6
+ } from "@cosmicdrift/kumiko-framework/engine";
7
+ import type { Element as ScanElement } from "happy-dom";
8
+ import * as z from "zod";
9
+ import { withParsedDocument } from "./document-sanitizer.js";
10
+ import { isDocumentPlaceholderHref, isDocumentReferenceName } from "./document-schema.js";
11
+ import type { TemplateMeta } from "./template-meta.js";
12
+
13
+ export const SYSTEM_PLACEHOLDERS: ReadonlySet<string> = new Set([
14
+ "tenantName",
15
+ "signature",
16
+ "imprint",
17
+ "unsubscribeUrl",
18
+ ]);
19
+
20
+ const PAGE_PLACEHOLDERS = ["page", "pages"] as const;
21
+ const PAGED_KINDS: ReadonlySet<string> = new Set(["document-pdf", "pdf-layout"]);
22
+
23
+ export function systemPlaceholdersFor(kind: string): ReadonlySet<string> {
24
+ return PAGED_KINDS.has(kind)
25
+ ? new Set([...SYSTEM_PLACEHOLDERS, ...PAGE_PLACEHOLDERS])
26
+ : SYSTEM_PLACEHOLDERS;
27
+ }
28
+
29
+ const BASE_RESOURCE_KEYS = ["logo"] as const;
30
+
31
+ export type TemplateValidation = {
32
+ readonly unknown: readonly string[];
33
+ readonly missingRequired: readonly string[];
34
+ readonly used: readonly string[];
35
+ /** data-resource keys with no uploaded file; a warning, never a blocker. */
36
+ readonly unknownResources: readonly string[];
37
+ };
38
+
39
+ // Mirrors TemplateVariableDef: a stored or draft schema keeps its label and example, which the
40
+ // preview uses as sample values.
41
+ const variableCommon = {
42
+ label: z.string().optional(),
43
+ required: z.boolean().optional(),
44
+ };
45
+ const textLikeExample = z.string().optional();
46
+
47
+ const scalarVariableSchema = z.discriminatedUnion("type", [
48
+ z.object({ ...variableCommon, type: z.literal("text"), example: textLikeExample }),
49
+ z.object({ ...variableCommon, type: z.literal("link"), example: textLikeExample }),
50
+ z.object({ ...variableCommon, type: z.literal("richText"), example: textLikeExample }),
51
+ z.object({
52
+ ...variableCommon,
53
+ type: z.literal("money"),
54
+ example: z
55
+ .union([z.number(), z.object({ amount: z.number(), currency: z.string() })])
56
+ .optional(),
57
+ }),
58
+ z.object({ ...variableCommon, type: z.literal("date"), example: textLikeExample }),
59
+ ]);
60
+
61
+ const variableDefSchema = z.union([
62
+ scalarVariableSchema,
63
+ z.object({
64
+ ...variableCommon,
65
+ type: z.literal("list"),
66
+ item: z.record(z.string(), scalarVariableSchema),
67
+ example: z.array(z.record(z.string(), z.unknown())).optional(),
68
+ }),
69
+ ]);
70
+
71
+ // A stored variableSchema value is either a bare string (legacy "this is a text
72
+ // variable") or a TemplateVariableDef object; anything else is not declared and
73
+ // therefore shows up as an unknown placeholder instead of being guessed at.
74
+ function toVariableDef(raw: unknown): TemplateVariableDef | null {
75
+ if (typeof raw === "string") return { type: "text" };
76
+ const parsed = variableDefSchema.safeParse(raw);
77
+ return parsed.success ? parsed.data : null;
78
+ }
79
+
80
+ export function resolveTemplateVariables(
81
+ registry: Registry,
82
+ slug: string,
83
+ variableSchema: Readonly<Record<string, unknown>>,
84
+ ): TemplateVariables {
85
+ const declared = registry.getAllNotifications().get(slug)?.variables;
86
+ return declared ?? variablesFromSchema(variableSchema);
87
+ }
88
+
89
+ export function variablesFromSchema(
90
+ variableSchema: Readonly<Record<string, unknown>>,
91
+ ): TemplateVariables {
92
+ const variables: Record<string, TemplateVariableDef> = {};
93
+ for (const [name, raw] of Object.entries(variableSchema)) {
94
+ const def = toVariableDef(raw);
95
+ if (def !== null) variables[name] = def;
96
+ }
97
+ return variables;
98
+ }
99
+
100
+ type Resolution = { readonly known: boolean; readonly used: string };
101
+
102
+ function rootName(name: string): string {
103
+ return name.split(".")[0] ?? name;
104
+ }
105
+
106
+ type PlaceholderScope = {
107
+ readonly variables: TemplateVariables;
108
+ readonly systemPlaceholders: ReadonlySet<string>;
109
+ readonly resourceKeys: ReadonlySet<string>;
110
+ readonly used: Set<string>;
111
+ readonly unknown: Set<string>;
112
+ readonly unknownResources: Set<string>;
113
+ };
114
+
115
+ function listItemFields(
116
+ variables: TemplateVariables,
117
+ listName: string | null,
118
+ ): Readonly<Record<string, TemplateScalarVariableDef>> {
119
+ const def =
120
+ listName !== null && Object.hasOwn(variables, listName) ? variables[listName] : undefined;
121
+ return def?.type === "list" ? def.item : {};
122
+ }
123
+
124
+ function isKnownName(
125
+ name: string,
126
+ scope: PlaceholderScope,
127
+ itemFields: Readonly<Record<string, unknown>>,
128
+ ): Resolution {
129
+ const { variables } = scope;
130
+ const root = rootName(name);
131
+ if (Object.hasOwn(itemFields, name) || Object.hasOwn(itemFields, root)) {
132
+ return { known: true, used: "" };
133
+ }
134
+ if (scope.systemPlaceholders.has(root)) return { known: true, used: "" };
135
+ if (Object.hasOwn(variables, name)) return { known: true, used: name };
136
+ if (Object.hasOwn(variables, root)) return { known: true, used: root };
137
+ return { known: false, used: "" };
138
+ }
139
+
140
+ function recordName(
141
+ name: string,
142
+ itemFields: Readonly<Record<string, unknown>>,
143
+ scope: PlaceholderScope,
144
+ ) {
145
+ const resolution = isKnownName(name, scope, itemFields);
146
+ if (!resolution.known) scope.unknown.add(name);
147
+ else if (resolution.used !== "") scope.used.add(resolution.used);
148
+ }
149
+
150
+ function enclosingEachName(element: ScanElement): string | null {
151
+ return element.closest("tr[data-each]")?.getAttribute("data-each") ?? null;
152
+ }
153
+
154
+ function scanBodyPlaceholders(body: string, scope: PlaceholderScope): void {
155
+ withParsedDocument(body, (root) => {
156
+ for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
157
+ const listName = element.getAttribute("data-each") ?? "";
158
+ const def = Object.hasOwn(scope.variables, listName) ? scope.variables[listName] : undefined;
159
+ if (def?.type === "list") scope.used.add(listName);
160
+ else scope.unknown.add(listName);
161
+ }
162
+ for (const element of Array.from(
163
+ root.querySelectorAll("span[data-variable], a[href], img[data-resource]"),
164
+ )) {
165
+ const itemFields = listItemFields(scope.variables, enclosingEachName(element));
166
+ const tag = element.tagName.toLowerCase();
167
+ if (tag === "span") {
168
+ recordName(element.getAttribute("data-variable") ?? "", itemFields, scope);
169
+ } else if (tag === "a") {
170
+ const href = element.getAttribute("href") ?? "";
171
+ if (isDocumentPlaceholderHref(href)) recordName(href.slice(2, -2), itemFields, scope);
172
+ } else {
173
+ const key = element.getAttribute("data-resource") ?? "";
174
+ if (!scope.resourceKeys.has(key)) scope.unknownResources.add(key);
175
+ }
176
+ }
177
+ });
178
+ }
179
+
180
+ const HANDLEBARS_EXPRESSION = /\{\{([^{}]*)\}\}/g;
181
+
182
+ // Meta text supports only plain `{{name}}` / `{{name.field}}`; every other
183
+ // Handlebars construct is reported as unknown so it can never reach the compiler.
184
+ function scanMetaPlaceholders(text: string, scope: PlaceholderScope): void {
185
+ const leftover = text.replace(HANDLEBARS_EXPRESSION, (_match, inner: string) => {
186
+ const name = inner.trim();
187
+ if (isDocumentReferenceName(name)) recordName(name, {}, scope);
188
+ else scope.unknown.add(`{{${inner}}}`);
189
+ return "";
190
+ });
191
+ if (/[{}]/.test(leftover)) scope.unknown.add("{");
192
+ }
193
+
194
+ function metaTexts(meta: TemplateMeta | null): readonly string[] {
195
+ if (meta === null || !("subject" in meta)) return [];
196
+ return [meta.subject, ...(meta.previewText === undefined ? [] : [meta.previewText])];
197
+ }
198
+
199
+ export function validateTemplatePlaceholders(input: {
200
+ readonly kind: string;
201
+ readonly body: string;
202
+ readonly meta: TemplateMeta | null;
203
+ readonly variables: TemplateVariables;
204
+ /** data-resource keys with an uploaded file besides the built-in `logo`. */
205
+ readonly resourceKeys?: readonly string[];
206
+ }): TemplateValidation {
207
+ const scope: PlaceholderScope = {
208
+ variables: input.variables,
209
+ systemPlaceholders: systemPlaceholdersFor(input.kind),
210
+ resourceKeys: new Set([...BASE_RESOURCE_KEYS, ...(input.resourceKeys ?? [])]),
211
+ used: new Set(),
212
+ unknown: new Set(),
213
+ unknownResources: new Set(),
214
+ };
215
+ scanBodyPlaceholders(input.body, scope);
216
+ for (const text of metaTexts(input.meta)) scanMetaPlaceholders(text, scope);
217
+ const missingRequired = Object.entries(input.variables)
218
+ .filter(([name, def]) => def.required === true && !scope.used.has(name))
219
+ .map(([name]) => name);
220
+ return {
221
+ unknown: [...scope.unknown].sort(),
222
+ missingRequired: missingRequired.sort(),
223
+ used: [...scope.used].sort(),
224
+ unknownResources: [...scope.unknownResources].sort(),
225
+ };
226
+ }
227
+
228
+ export function hasPlaceholderProblems(validation: TemplateValidation): boolean {
229
+ return validation.unknown.length > 0 || validation.missingRequired.length > 0;
230
+ }
@@ -79,7 +79,7 @@ export async function assertConsumerHandlesNotFound(
79
79
  await consumer.resolve({
80
80
  tenantId,
81
81
  slug,
82
- kind: "mail-html",
82
+ kind: "mail",
83
83
  locale: "de",
84
84
  });
85
85
  } catch (e) {
@@ -105,7 +105,7 @@ export async function assertConsumerRespectsLocaleFallback(
105
105
  await seedTemplate(db, {
106
106
  tenantId: SYSTEM_TENANT_ID,
107
107
  slug,
108
- kind: "notification",
108
+ kind: "document-pdf",
109
109
  locale: FALLBACK_LOCALE,
110
110
  scope: "system",
111
111
  content: "conformance-fallback-content",
@@ -113,7 +113,7 @@ export async function assertConsumerRespectsLocaleFallback(
113
113
  const result = await consumer.resolve({
114
114
  tenantId,
115
115
  slug,
116
- kind: "notification",
116
+ kind: "document-pdf",
117
117
  locale: "tr",
118
118
  });
119
119
  if (result.locale !== FALLBACK_LOCALE) {
@@ -143,7 +143,7 @@ export async function assertConsumerHandlesMissingResourceKeys(
143
143
  await seedTemplate(db, {
144
144
  tenantId: SYSTEM_TENANT_ID,
145
145
  slug,
146
- kind: "mail-html",
146
+ kind: "mail",
147
147
  locale: "de",
148
148
  scope: "system",
149
149
  linkedResources: { logo: "file_missing" },
@@ -151,7 +151,7 @@ export async function assertConsumerHandlesMissingResourceKeys(
151
151
  const template = await consumer.resolve({
152
152
  tenantId,
153
153
  slug,
154
- kind: "mail-html",
154
+ kind: "mail",
155
155
  locale: "de",
156
156
  });
157
157
 
@@ -40,6 +40,8 @@ import {
40
40
  TemplateResolverHandlers,
41
41
  TemplateResolverQueries,
42
42
  } from "../qualified-names.js";
43
+ import { DocumentBlocksContext, DocumentContentEditor } from "./document-editor.js";
44
+ import { documentBlocksForKind } from "./document-toolbar-blocks.js";
43
45
  import { defaultTranslations } from "./i18n.js";
44
46
 
45
47
  // Exported for the unit test — groupBlocksByFolder is a pure function.
@@ -256,21 +258,22 @@ type TextBlock = {
256
258
  readonly modifiedAt: string | null;
257
259
  };
258
260
 
259
- // The collection's declared contentFormat ("plain" | "rich" | "markdown",
260
- // see ContentCollectionDefinition.contentFormat) uses a different vocabulary
261
+ // The collection's declared contentFormat ("plain" | "rich" | "markdown" |
262
+ // "document", see ContentCollectionDefinition.contentFormat) uses a different vocabulary
261
263
  // than the stored entry's contentFormat ("plain" | "html" | "markdown" |
262
- // "mjml", see CONTENT_FORMATS) — "rich" always stores HTML. Without this,
264
+ // "mjml", see CONTENT_FORMATS) — "rich" and "document" always store HTML. Without this,
263
265
  // handleSave omitting contentFormat let the write schema's
264
266
  // contentFormatSchema.default("markdown") silently stamp every save as
265
267
  // markdown, clobbering e.g. a "plain" entry. Undefined (no collection, or
266
268
  // the collection didn't declare a format) keeps whatever was already saved
267
269
  // instead of guessing.
268
- function resolveWriteContentFormat(
270
+ // Exported for the unit test — resolveWriteContentFormat is a pure function.
271
+ export function resolveWriteContentFormat(
269
272
  collectionContentFormat: string | undefined,
270
273
  loadedContentFormat: string | undefined,
271
274
  ): string | undefined {
272
275
  if (collectionContentFormat === "plain") return "plain";
273
- if (collectionContentFormat === "rich") return "html";
276
+ if (collectionContentFormat === "rich" || collectionContentFormat === "document") return "html";
274
277
  if (collectionContentFormat === "markdown") return "markdown";
275
278
  return loadedContentFormat;
276
279
  }
@@ -309,6 +312,7 @@ function TextBlockEditor({
309
312
  const contentFormat = findContentFormat(features, collectionId);
310
313
  const variableExamples = findVariableExamples(features, collectionId);
311
314
  const variables = Object.keys(variableExamples);
315
+ const documentBlocks = documentBlocksForKind(findCollection(features, collectionId)?.kind);
312
316
  const ContentEditor = useContentEditor(contentFormat);
313
317
  const canWrite =
314
318
  user?.roles.includes("TenantAdmin") === true || user?.roles.includes("SystemAdmin") === true;
@@ -446,13 +450,15 @@ function TextBlockEditor({
446
450
  contentFormat={contentFormat}
447
451
  />
448
452
  ) : (
449
- <ContentEditor
450
- id={contentEditorId}
451
- value={content}
452
- onChange={setContent}
453
- variables={variables}
454
- readOnly={disabled}
455
- />
453
+ <DocumentBlocksContext.Provider value={documentBlocks}>
454
+ <ContentEditor
455
+ id={contentEditorId}
456
+ value={content}
457
+ onChange={setContent}
458
+ variables={variables}
459
+ readOnly={disabled}
460
+ />
461
+ </DocumentBlocksContext.Provider>
456
462
  )}
457
463
  </Field>
458
464
  {saveError !== null && <Banner variant="error">{saveError}</Banner>}
@@ -499,12 +505,13 @@ export function textBlocksClient(opts?: {
499
505
  resolvers: {
500
506
  "template-resolver:edit": TextBlockEditor,
501
507
  },
502
- // The default for every "plain" collection (ai-prompt, mail-html without
508
+ // The default for every "plain" collection (ai-prompt, mail without
503
509
  // an explicit rich override) — an app-registered "plain" editor on
504
510
  // another clientFeature still wins, last-wins same as columnRenderers.
505
511
  contentEditors: {
506
512
  plain: PlainContentEditor,
507
513
  rich: RichContentEditor,
514
+ document: DocumentContentEditor,
508
515
  // ponytail: same textarea+chips as "plain" — markdown is stored as
509
516
  // text either way. Shares PlainContentEditor's fixed-DOM-id ceiling
510
517
  // (see its own file comment); swap for a live-preview widget or a