@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
@@ -3,13 +3,23 @@
3
3
  // ctx.templateResolver at runtime.
4
4
 
5
5
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
6
- import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
6
+ import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
7
7
  import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
8
8
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
9
9
  import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
10
+ import type { FileAttachmentRef } from "../renderer-foundation/index.js";
10
11
  import type { ContentFormat, TemplateKind } from "./constants.js";
11
12
  import { FALLBACK_LOCALE, SYSTEM_TENANT_ID } from "./constants.js";
13
+ import {
14
+ fileAttachmentsOf,
15
+ imageLinksOf,
16
+ parseLinkedResourceEntries,
17
+ pdfAttachmentLinksOf,
18
+ type TemplatePdfAttachmentLink,
19
+ } from "./linked-resources.js";
12
20
  import { type TemplateResourceRow, templateResourcesTable } from "./table.js";
21
+ import { activeVersionOf, servedVersionOf } from "./template-draft.js";
22
+ import { parseTemplateMeta, type TemplateMeta } from "./template-meta.js";
13
23
 
14
24
  // Public TemplateResource: what consumers see. Hides DB-internal columns
15
25
  // (insertedById, internal id-type) but keeps the fields needed for rendering.
@@ -25,7 +35,15 @@ export type TemplateResource = {
25
35
  readonly content: string;
26
36
  readonly contentFormat: ContentFormat;
27
37
  readonly variableSchema: Record<string, unknown>;
38
+ /** Embeddable images: resource key -> fileRef id. */
28
39
  readonly linkedResources: Record<string, string>;
40
+ /** Files attached to every mail rendered from this template, ordered by key. */
41
+ readonly fileAttachments?: readonly FileAttachmentRef[];
42
+ /** PDFs rendered from document templates and attached at send time, ordered by key. */
43
+ readonly pdfAttachmentLinks?: readonly TemplatePdfAttachmentLink[];
44
+ /** Parsed per kind; null when absent or when the stored JSON no longer
45
+ * matches the kind's schema (the renderer then fails with a clear error). */
46
+ readonly meta: TemplateMeta | null;
29
47
  readonly scope: "system" | "tenant";
30
48
  readonly parentTemplateId: string | null;
31
49
  readonly status: "draft" | "active" | "archived";
@@ -39,6 +57,15 @@ export type ResolveRequest = {
39
57
  readonly locale: string;
40
58
  };
41
59
 
60
+ export type LayoutKind = "mail-layout" | "pdf-layout";
61
+
62
+ export type ResolveLayoutRequest = {
63
+ readonly tenantId: TenantId;
64
+ readonly slug: string;
65
+ readonly kind: LayoutKind;
66
+ readonly locale: string;
67
+ };
68
+
42
69
  export type TemplateResolverApi = {
43
70
  /**
44
71
  * Findet ein konkretes Template by (tenantId, slug, kind, locale).
@@ -74,9 +101,20 @@ export type TemplateResolverApi = {
74
101
  * User cross-tenant Templates abfragen (Tenant-Isolation gebrochen).
75
102
  */
76
103
  readonly resolveTemplate: (args: ResolveRequest) => Promise<TemplateResource>;
104
+
105
+ /**
106
+ * Layouts are locale-neutral in the product (the layout editor saves under the locale of the
107
+ * row it loaded), so the locale chain is wider than resolveTemplate: tenant (requested,
108
+ * FALLBACK_LOCALE, any active locale), then system (same). Only active rows count; among
109
+ * "any" the highest version wins, then the alphabetically first locale, so the pick is stable.
110
+ * Throws TemplateNotFoundError when neither scope has an active row.
111
+ *
112
+ * **Caller invariant:** `tenantId` must come from the server, as for `resolveTemplate`.
113
+ */
114
+ readonly resolveLayout: (args: ResolveLayoutRequest) => Promise<TemplateResource>;
77
115
  };
78
116
 
79
- export function createTemplateResolverApi(db: DbRunner): TemplateResolverApi {
117
+ export function createTemplateResolverApi(db: DbRunner | TenantDb): TemplateResolverApi {
80
118
  return {
81
119
  findExact: async ({ tenantId, slug, kind, locale, scope }) => {
82
120
  const effectiveTenantId = scope === "system" ? SYSTEM_TENANT_ID : tenantId;
@@ -102,11 +140,39 @@ export function createTemplateResolverApi(db: DbRunner): TemplateResolverApi {
102
140
  }
103
141
  throw new TemplateNotFoundError({ slug, kind, locale });
104
142
  },
143
+
144
+ resolveLayout: async ({ tenantId, slug, kind, locale }) => {
145
+ for (const tid of [tenantId, SYSTEM_TENANT_ID]) {
146
+ const rows = await selectMany<TemplateResourceRow>(db, templateResourcesTable, {
147
+ tenantId: tid,
148
+ slug,
149
+ kind,
150
+ });
151
+ const row = pickActiveLayoutRow(rows, locale);
152
+ if (row !== null) return toPublic(row);
153
+ }
154
+ throw new TemplateNotFoundError({ slug, kind, locale });
155
+ },
105
156
  };
106
157
  }
107
158
 
159
+ function pickActiveLayoutRow(
160
+ rows: readonly TemplateResourceRow[],
161
+ locale: string,
162
+ ): TemplateResourceRow | null {
163
+ const active = rows
164
+ .filter((row) => row.status === "active")
165
+ .sort((a, b) => activeVersionOf(b) - activeVersionOf(a) || a.locale.localeCompare(b.locale));
166
+ return (
167
+ active.find((row) => row.locale === locale) ??
168
+ active.find((row) => row.locale === FALLBACK_LOCALE) ??
169
+ active[0] ??
170
+ null
171
+ );
172
+ }
173
+
108
174
  async function fetchTemplate(
109
- db: DbRunner,
175
+ db: DbRunner | TenantDb,
110
176
  tenantId: string,
111
177
  slug: string,
112
178
  kind: TemplateKind,
@@ -127,7 +193,6 @@ function toPublic(row: TemplateResourceRow): TemplateResource {
127
193
  // @cast-boundary db-row — Drizzle-Schema typisiert kind/contentFormat/
128
194
  // scope/status als generic text. CHECK-Constraints in der DB schränken
129
195
  // sie auf die Union-Types ein; Cast assertet das Schema-Wissen.
130
- // linkedResources ist ein text-column mit JSON-payload (string→string map).
131
196
  const kind = row.kind as TemplateKind;
132
197
  // @cast-boundary db-row — siehe kind.
133
198
  const contentFormat = row.contentFormat as ContentFormat;
@@ -135,12 +200,10 @@ function toPublic(row: TemplateResourceRow): TemplateResource {
135
200
  const scope = row.scope as "system" | "tenant";
136
201
  // @cast-boundary db-row — siehe kind.
137
202
  const status = row.status as "draft" | "active" | "archived";
138
- // @cast-boundary db-row — parseJson returnt Record<string, unknown>;
139
- // linkedResources-Spalte enthält per Schema {key: signedUrl}-Map.
140
- const linkedResources = parseJson(row.linkedResources) as Record<string, string>;
203
+ const linkedEntries = parseLinkedResourceEntries(row.linkedResources);
141
204
  return {
142
205
  id: String(row.id),
143
- version: row.version,
206
+ version: servedVersionOf(row),
144
207
  tenantId: row.tenantId,
145
208
  slug: row.slug,
146
209
  kind,
@@ -150,7 +213,10 @@ function toPublic(row: TemplateResourceRow): TemplateResource {
150
213
  content: row.content ?? "",
151
214
  contentFormat,
152
215
  variableSchema: parseJson(row.variableSchema),
153
- linkedResources,
216
+ linkedResources: imageLinksOf(linkedEntries),
217
+ fileAttachments: fileAttachmentsOf(linkedEntries),
218
+ pdfAttachmentLinks: pdfAttachmentLinksOf(linkedEntries),
219
+ meta: parseStoredMeta(kind, row.meta),
154
220
  scope,
155
221
  parentTemplateId: row.parentTemplateId,
156
222
  status,
@@ -158,6 +224,16 @@ function toPublic(row: TemplateResourceRow): TemplateResource {
158
224
  };
159
225
  }
160
226
 
227
+ function parseStoredMeta(kind: TemplateKind, raw: string | null): TemplateMeta | null {
228
+ if (!raw) return null;
229
+ try {
230
+ const result = parseTemplateMeta(kind, JSON.parse(raw));
231
+ return result.ok ? result.meta : null;
232
+ } catch {
233
+ return null;
234
+ }
235
+ }
236
+
161
237
  function parseJson(raw: string | null): Record<string, unknown> {
162
238
  if (!raw) return {};
163
239
  try {
@@ -1,4 +1,190 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "PDF collections show Page break instead of Button, and placeholder links no longer insert {{name}} text",
6
+ "detail": "The content editor for a `document-pdf` collection now offers the PDF block set (including Page break) instead of the mail set, chosen from the collection's kind. A link to a placeholder href such as `{{invoice.url}}` without a selection now links a variable node instead of inserting the placeholder as plain text."
7
+ },
8
+ {
9
+ "version": "0.364.0",
10
+ "type": "fix",
11
+ "title": "Document sanitizer rejects prototype-style placeholder hrefs",
12
+ "detail": "`{{__proto__}}` and `{{constructor.prototype}}` are no longer accepted as link or button placeholder hrefs; only valid reference names pass."
13
+ },
14
+ {
15
+ "version": "0.364.0",
16
+ "type": "breaking",
17
+ "title": "DocumentRepeatRow is removed, use DocumentTableRow",
18
+ "detail": "Removed the DocumentRepeatRow alias",
19
+ "migration": "Replace `DocumentRepeatRow` with `DocumentTableRow` (identical shape)."
20
+ },
21
+ {
22
+ "version": "0.364.0",
23
+ "type": "fix",
24
+ "title": "export-bundle for a system template with resources returns a validation error outside the system tenant",
25
+ "detail": "A TenantAdmin, or a SystemAdmin outside the system tenant, exporting a system template that links resource files now gets the `system_resources_need_system_tenant` validation error, the same code import-bundle-system uses, instead of a NotFound for the resource file."
26
+ },
27
+ {
28
+ "version": "0.364.0",
29
+ "type": "fix",
30
+ "title": "send-test logs failed test mails with error class and tenant",
31
+ "detail": "A transport that throws or reports not sent is logged with the error class/code and tenant id (no address, no provider body) and still answers `template_test_mail_failed`."
32
+ },
33
+ {
34
+ "version": "0.364.0",
35
+ "type": "fix",
36
+ "title": "copy-locale ignores archived tenant sources and duplicates linked files of system sources; export-bundle refuses locales with differing format",
37
+ "detail": "copy-locale no longer copies from an archived tenant template, and copying a system template duplicates its linked resource files into the target tenant. export-bundle rejects a bundle whose locales differ in content format or variable schema with `bundleLocalesDiffer`."
38
+ },
39
+ {
40
+ "version": "0.364.0",
41
+ "type": "fix",
42
+ "title": "Template preview counts encoding overhead and reports missing attachments",
43
+ "detail": "The mail size estimate counts base64 size plus a fixed per-part overhead, an attachment whose file reference is missing fails the resources_resolved check, and the document draft scope is derived once per request."
44
+ },
45
+ {
46
+ "version": "0.364.0",
47
+ "type": "breaking",
48
+ "title": "A draft no longer replaces the active template",
49
+ "detail": "Saving a draft (upsert-tenant default, copy-locale, import-bundle with status draft) over an active template stores it beside the active version in the new column draft instead of overwriting content, meta and status; resolveTemplate, resolveLayout, preview without content and sends keep using the active version. publish and publish-system promote an open draft (the publish gate checks the draft) and record activeVersion and activeSince; the new handlers discard-draft and discard-draft-system drop it and refuse with no_draft when none is open. find-by-id and layout-source return the working version (the open draft with status draft, else the stored one) plus active { version, since }, null when nothing is served. overview keeps status and version of the served version and adds draftVersion and activeSince. A draft over a row that is not active (new, draft or archived) behaves as before. A tenant never sees system draft content: find-by-id, layout-source, overview, copy-locale and diff-to-system apply a system draft only for a SystemAdmin and hide system rows that were never served; list leaves those rows out too. Version numbers shown for an active row are the served version (activeVersion): resolveTemplate / resolveLayout results, tenant-overrides rows and the override diff no longer show the save counter that drafts raise.",
50
+ "migration": "Run kumiko schema generate and apply the migration before deploying: it adds the nullable columns draft, active_version and active_since to read_template_resources. No rebuild needed."
51
+ },
52
+ {
53
+ "version": "0.364.0",
54
+ "type": "fix",
55
+ "title": "template-resolver:query:overview accepts layoutSlug to list only the mails and PDFs using that layout",
56
+ "detail": "The optional layoutSlug payload field keeps only documents whose layout slug matches; total counts the filtered rows. Without it the response is unchanged."
57
+ },
58
+ {
59
+ "version": "0.364.0",
60
+ "type": "fix",
61
+ "title": "Layout usage follows the effective template per notification and locale",
62
+ "detail": "Mail layout usage counts a notification only when it declares a template and resolves the effective row per notification and locale, so tenant overrides are reflected."
63
+ },
64
+ {
65
+ "version": "0.364.0",
66
+ "type": "improvement",
67
+ "title": "templateVariable.richText for notification variables that carry formatted HTML",
68
+ "detail": "Notification variables are escaped, so a value holding HTML (for example a document written in a rich text editor) showed up as literal tags in the mail. A variable declared with templateVariable.richText({ required }) is run through the document sanitizer at render time and inserted as HTML; bold, italic, lists and safe links stay, script, event handlers and javascript links are not emitted as markup. If the sanitizer rejects a value, it is rendered as escaped text and a warning with the violation codes is logged. All other variable types stay escaped."
69
+ },
70
+ {
71
+ "version": "0.363.0",
72
+ "type": "improvement",
73
+ "title": "Overview and layout-source queries for the template designer",
74
+ "detail": "template-resolver:query:overview lists the mail, PDF and layout templates of the caller's tenant grouped by slug and kind (effective origin, locales, status, versions, layout slug, last change and the notifications that use it) and template-resolver:query:layout-source returns the header, signature and footer regions of a mail or PDF layout plus the locale a save has to use; a SystemAdmin also receives the MJML source of the system layout. The usage lookup is the exported collectTemplateUsage."
75
+ },
76
+ {
77
+ "version": "0.363.0",
78
+ "type": "improvement",
79
+ "title": "contentFormat \"document\" with a toolbar editor for mail and PDF templates",
80
+ "detail": "ContentEditorFormat gains \"document\". template-resolver registers DocumentContentEditor for it (tiptap, lazy-loaded) and stores the body as html. The toolbar offers paragraph style, bold, italic, lists, links, placeholders and the blocks image, table, button, divider, repeat and page break; the blocks prop picks them (MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS). Placeholders are inserted only as variable nodes (data-variable), never as {{name}} text. ContentPreview does not substitute example values for \"document\" content, because substituting a button or link href would make the schema drop that node. New exports from template-resolver/web: DocumentContentEditor, DocumentEditorProps, DocumentToolbarBlock, MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS."
81
+ },
82
+ {
83
+ "version": "0.363.0",
84
+ "type": "fix",
85
+ "title": "Preview uses the examples of a stored or draft variable schema",
86
+ "detail": "A template's variableSchema kept only type and required, label and example were dropped, so the preview showed [name] instead of the declared example. The schema now keeps label and example, for mail and document previews."
87
+ },
88
+ {
89
+ "version": "0.363.0",
90
+ "type": "improvement",
91
+ "title": "PDF meta gets orientation, printBackground and layoutHeaderFooter; paragraphs and headings get data-align",
92
+ "detail": "The document-pdf meta accepts orientation (portrait or landscape), printBackground (default true) and layoutHeaderFooter (false prints without the layout header and footer). Paragraphs and headings carry textAlign (left, center, right, justify), serialized as data-align and kept by the sanitizer only with these four values. New exports are evaluateDocumentDraft, localeSchema, DOCUMENT_TEXT_ALIGNMENTS and DocumentTextAlign; compileDocumentBodyToHandlebars accepts alignmentAsInlineStyle."
93
+ },
94
+ {
95
+ "version": "0.363.0",
96
+ "type": "breaking",
97
+ "title": "document-pdf templates require contentFormat html, and html bodies are sanitized on write",
98
+ "detail": "upsertSystem, upsertTenant, the collection-set handlers and seedSystemTemplate rebuild html bodies of kind mail-html and document-pdf through the new document schema (template-resolver/document-schema). Script elements, inline event handlers, embedded content (iframe, frame, object, embed, applet, portal), links and url attributes (svg href, xlink:href, form action, base) outside https/http/mailto/tel/{{placeholder}}, external image sources, alt text containing < or >, and invalid or prototype-chain variable names are rejected with a validation_error (or a thrown Error in seedSystemTemplate); anything the schema cannot express is dropped silently, and links keep only href (no target, rel, class or title). New exports: documentExtensions, DocumentLink and the other Document* nodes, isAllowedDocumentLinkHref, isDocumentReferenceName, isDocumentResourceKey, isDocumentPlaceholderHref, sanitizeDocumentHtml, sanitizeTemplateBody. kind document-pdf with another contentFormat is rejected with document_requires_html_format.",
99
+ "migration": "Store document-pdf bodies as html and reference images via data-resource instead of src. mail-html with markdown or mjml is unchanged."
100
+ },
101
+ {
102
+ "version": "0.363.0",
103
+ "type": "improvement",
104
+ "title": "Mail templates carry file attachments (PDF, PNG, JPEG)",
105
+ "detail": "linkedResources stays image only; file attachments of a mail template are listed in the optional TemplateResource.fileAttachments (kind file, fileRefId, fileName), sorted by key. template-resolver:write:resource-upload takes purpose \"attachment\" with a fileName; it checks the file by content (PDF, PNG, JPEG), keeps the original bytes, allows at most 10 per mail template and stores the entry next to the inline images. resource-remove, copy-locale and the bundle export/import carry attachments (bundle resource entries get purpose \"attachment\"). The preview lists attachments with kind \"inline\" or \"file\" (PreviewAttachment is now a union), send-test sends them, and the mail renderer attaches them after the inline images within the delivery attachment limit."
106
+ },
107
+ {
108
+ "version": "0.363.0",
109
+ "type": "improvement",
110
+ "title": "copy-locale can copy system templates (scope \"system\", system admins only)",
111
+ "detail": "template-resolver:write:copy-locale takes scope \"tenant\" (default) or \"system\". With \"system\" a system admin copies a system template to another locale as a draft; other callers get access denied."
112
+ },
113
+ {
114
+ "version": "0.363.0",
115
+ "type": "improvement",
116
+ "title": "Mail preview returns displayHtml for the editor",
117
+ "detail": "The preview of a mail template carries displayHtml, the body with cid images replaced by data URIs (png, jpeg, gif, webp, 5 MiB budget in total) so an editor can show it without cid support. The sent mail is unchanged."
118
+ },
119
+ {
120
+ "version": "0.363.0",
121
+ "type": "improvement",
122
+ "title": "Document editor gets variable labels, a handle, resource previews, canvas chrome, underline and PlaceholderLineEditor",
123
+ "detail": "DocumentContentEditor accepts variableLabels, systemVariables, resourcePreviews, chrome (\"default\" or \"canvas\") and onEditorReady (handle with insertPlaceholder, insertResource, focus). The new PlaceholderLineEditor edits plain text with {{name}} placeholders as chips on one line. Underline (u) is part of the document schema and survives the sanitizer."
124
+ },
125
+ {
126
+ "version": "0.363.0",
127
+ "type": "improvement",
128
+ "title": "Mail templates link a PDF rendered from a document template (resource-link-pdf)",
129
+ "detail": "New write template-resolver:write:resource-link-pdf takes templateId, key, templateSlug and fileName and stores a linkedResources entry { kind \"rendered-pdf\", templateSlug, fileName } in a mail template; nothing is uploaded. fileName is a one-line pattern that may use the mail's {{placeholders}} and must end with .pdf (no / \\ \" ; or ..). The document-pdf template must exist for the tenant or the system, otherwise the write fails with template_attachment_pdf_template_missing; an invalid pattern or slug fails with template_attachment_invalid. Uploaded files and linked PDFs share the limit of 10 attachments per template. resource-remove, copy-locale and the bundle export/import carry the entry (the bundle gets an optional pdfAttachments map of key to { templateSlug, fileName }; import does not check that the document template exists). TemplateResource gains pdfAttachmentLinks, LinkedResourceEntry gains the TemplatePdfAttachmentLink variant."
130
+ },
131
+ {
132
+ "version": "0.363.0",
133
+ "type": "improvement",
134
+ "title": "Preview and test mail use the images and attachments of the edited row, also while it is a draft",
135
+ "detail": "When preview or send-test get content and the caller has a row for (slug, kind, locale), that row supplies linkedResources whatever its status (also when it has none, so nothing is borrowed from the system template), so a tenant whose customization is saved as a draft sees its logo and attachments. MailDraft has an optional linkedResources; without it the stored active template's resources are used as before. PreviewAttachment gains the variant { kind \"rendered-pdf\", name, templateSlug } without sizeBytes, because the PDF is not rendered in the preview; size_under_cap does not count it."
136
+ },
137
+ {
138
+ "version": "0.363.0",
139
+ "type": "improvement",
140
+ "title": "Preview query and send-test write for mail and PDF templates",
141
+ "detail": "template-resolver:query:preview renders a draft, or the stored template when no content is given, with sample values from the declared variable examples (list variables get two rows). The draft passes the same write gates as a save and a mail goes through the real mail renderer, so subject, html, plain text and inline images match a real send. The response carries checks (images resolved, plain text present, size under 10 MiB, required placeholders used); a PDF template returns its sanitized body and the layout regions. template-resolver:write:send-test mails the rendered mail to the calling user's own address only, subject prefixed with [TEST], limited to 10 per user and hour. It needs delivery, channel-email and renderer-mail-html."
142
+ },
143
+ {
144
+ "version": "0.363.0",
145
+ "type": "improvement",
146
+ "title": "DocumentEditorProps toolbar option for an icon-only toolbar",
147
+ "detail": "DocumentEditorProps gains toolbar (\"full\" or \"compact\", default \"full\"). Compact renders the placeholder and block buttons icon-only with a tooltip and a narrower paragraph select, for narrow columns such as the layout editor regions. The formatting buttons now carry a tooltip with their label in every mode."
148
+ },
149
+ {
150
+ "version": "0.363.0",
151
+ "type": "breaking",
152
+ "title": "kind mail-html is replaced by mail, mail-layout and pdf-layout; templates carry meta",
153
+ "detail": "The kind mail-html is removed; the new non-render kind snippet holds collection content (reply snippets, signatures; any contentFormat, html is sanitized like a mail body). Mail bodies are kind mail (contentFormat html only, rebuilt through the document schema, meta.subject required), mail frames are kind mail-layout (contentFormat mjml), and pdf-layout (html) is new. The table gains a nullable meta column (JSON per kind, validated by parseTemplateMeta: mail takes subject, previewText, layoutSlug, category, sender; layouts take none; other kinds take no meta) and the template-resource entity moves to eventVersion 2 with an upcaster that reads stored mail-html events as mail. upsertSystem, upsertTenant (active), publish and seedSystemTemplate reject unknown placeholders and, for the same writes, bodies that miss a required variable of the notification with the same qualified name; a tenant draft may be incomplete and returns validation. Slugs must be kebab segments joined by a colon. seedSystemTemplate now validates the slug (templateSlugSchema) and the per-kind meta for every kind. A content collection of kind mail, mail-layout or pdf-layout is refused on collection-set (field issue on kind, templateResolver.errors.collection.kindNotAllowed), because such a tenant row would shadow the system template without the meta and placeholder gates. TemplateResource gains meta. New exports: parseTemplateMeta, isMailTemplateMeta, templateSlugSchema, validateTemplatePlaceholders, resolveTemplateVariables, compileDocumentBodyToHandlebars, MAIL_CATEGORIES and their types; seedNotificationTemplates and hasNotificationTemplates on template-resolver/seeding, which applyBootSeeds now calls. RENDER_KINDS contains mail instead of mail-html.",
154
+ "migration": "Rename kind mail-html to mail in app code, collections and seeds; mail bodies must be contentFormat html (markdown or mjml mail bodies are rejected) and carry meta.subject, mail layouts use kind mail-layout with contentFormat mjml and slug default. Slugs of every kind must now be kebab segments joined by a colon (upsert handlers and seedSystemTemplate reject dotted, underscored or camelCase slugs). Collections that used kind mail-html move to the new kind snippet (stored mail-html rows are upcast to mail, which collections refuse, so re-create those entries under snippet); collection-set refuses mail, mail-layout and pdf-layout collections. seedSystemTemplate callers must pass a kebab slug and valid meta for every kind, not only mail. Generate and apply the migration that adds the nullable meta column to read_template_resources; it ships a rebuild marker for read_template_resources so existing mail-html rows are read as mail after the rebuild."
155
+ },
156
+ {
157
+ "version": "0.363.0",
158
+ "type": "improvement",
159
+ "title": "resolveLayout finds a layout in any locale",
160
+ "detail": "TemplateResolverApi gains resolveLayout({ tenantId, slug, kind, locale }) for mail-layout and pdf-layout. It looks at the tenant first (requested locale, then the fallback locale, then any active locale) and then at the system layouts the same way, ignoring draft and archived rows. A layout saved by the layout editor under one locale is therefore found for every locale. The template preview uses it for the PDF layout."
161
+ },
162
+ {
163
+ "version": "0.363.0",
164
+ "type": "improvement",
165
+ "title": "PDF template meta, render provenance, system template lifecycle and override queries",
166
+ "detail": "kind document-pdf takes a strict meta (layoutSlug, pageFormat A4/Letter, margins in mm, fileName without / \\ \" ; or .., metadata title/author); new exports PDF_PAGE_FORMATS, PdfPageFormat, PdfTemplateMeta, readPdfTemplateMeta, DEFAULT_MAIL_LAYOUT_SLUG. RenderResponse (renderer-foundation) gains optional template and layout refs ({ templateId, templateVersion, scope }, new types RenderedTemplateRef and RenderProvenance); renderer-mail-html fills them. New handlers template-resolver:write:publish-system and archive-system (SystemAdmin), reset-to-system (TenantAdmin, SystemAdmin), queries usage, tenant-overrides (SystemAdmin, counts only) and diff-to-system. publish and archive answer not_found for rows outside the caller's tenant instead of a version_conflict; find-by-id also returns meta."
167
+ },
168
+ {
169
+ "version": "0.363.0",
170
+ "type": "breaking",
171
+ "title": "linkedResources is no longer a payload field of upsertSystem/upsertTenant; resourceUpload and resourceRemove manage it",
172
+ "detail": "Two steps - upload the image with POST /api/files (multipart, no entityType), then call template-resolver:write:resource-upload with { sourceFileRefId, templateId?, key? }. The handler re-encodes the caller's own unclaimed upload into a new file ref (width at most 1200 px) and links it into the template; resource-remove unlinks a key. upsertSystem and upsertTenant no longer accept linkedResources and leave the stored value untouched on update. The size cap is the system config key resource-max-bytes (default 2 MiB); a cap above the files route's maxUploadSize has no effect because the upload is rejected first. The source upload stays as an unclaimed file.",
173
+ "migration": "Whoever sent linkedResources with upsertSystem or upsertTenant now uploads the image via POST /api/files (multipart, no entityType) and links it with template-resolver:write:resource-upload { sourceFileRefId, templateId, key }. Server-side seeding (seedSystemTemplate linkedResources) stays."
174
+ },
175
+ {
176
+ "version": "0.363.0",
177
+ "type": "improvement",
178
+ "title": "Bundle import re-encodes its images like resource-upload; re-seeding keeps uploaded system resources",
179
+ "detail": "importBundle and importBundleSystem store each bundle resource through the same normalization as resource-upload (decoded format decides the content type, SVG and non-images are rejected, width at most 1200 px, resource-max-bytes applies), so an exported bundle carries the re-encoded bytes. seedSystemTemplate without linkedResources no longer resets the links of an existing row, so images a system admin uploaded survive the next boot. storeTemplateResource from the template-resolver barrel is the normalizing store; the raw byte variant is gone."
180
+ },
181
+ {
182
+ "version": "0.363.0",
183
+ "type": "breaking",
184
+ "title": "Tenant mail-layouts are structured html, pdf-layout and document-pdf are placeholder-validated",
185
+ "detail": "A new validate query dry-runs the write pipeline (used, unknown and missing placeholders, canSave, canPublish). document-pdf, pdf-layout and mail-layout now go through the same placeholder gate as mail; document-pdf and pdf-layout know the system placeholders page and pages. A tenant mail-layout is html with a data-content slot plus header, signature and footer regions that renderer-mail-html wraps in a fixed MJML frame; raw mjml (with {{{content}}} exactly once) is system-only. New handlers export-bundle, import-bundle, import-bundle-system and copy-locale move a template with its resource files between tenants and locales. r.notification accepts a record mapping from an entity row to the declared variables, validated at boot. publish and archive no longer touch system rows through a tenant-scoped SystemAdmin.",
186
+ "migration": "Tenant mail-layout rows stored as mjml are ignored by the renderer, which falls back to the system layout; re-create them as html with a data-content slot. upsert-system answers with an extra validation field. seedSystemTemplate errors now read `<label> rejected: <code> (<detail>); ...`. Layout bodies without the content slot are rejected on write. `set` now runs the same gate for mail, document-pdf, pdf-layout and mail-layout rows and rejects unsanitized markup or publish issues, since saving publishes; the mail renderer also re-sanitizes stored tenant html layouts and falls back to the system layout when they fail. upsert-system and upsert-tenant no longer accept linkedResources (resources go through the resource upload; bundle import and copy-locale attach them). Unknown data-resource keys are a validate warning (resource_not_uploaded), not a write error."
187
+ },
2
188
  {
3
189
  "version": "0.354.0",
4
190
  "type": "fix",
@@ -0,0 +1,278 @@
1
+ import type { TemplateVariableDef, TemplateVariables } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
3
+ import type { Element as ScanElement } from "happy-dom";
4
+ import { withParsedDocument } from "./document-sanitizer.js";
5
+ import {
6
+ DOCUMENT_TEXT_ALIGNMENTS,
7
+ isAllowedDocumentLinkHref,
8
+ isDocumentPlaceholderHref,
9
+ isDocumentReferenceName,
10
+ isDocumentResourceKey,
11
+ } from "./document-schema.js";
12
+
13
+ // Private-use characters mark where a Handlebars expression goes. Author text
14
+ // is scrubbed of them first and every `{`/`}` is entity-encoded before the
15
+ // markers are swapped for real expressions, so the only Handlebars syntax in
16
+ // the output is the syntax this module wrote.
17
+ const EXPRESSION_OPEN = "\uE000";
18
+ const EXPRESSION_CLOSE = "\uE001";
19
+ const MARKER_PATTERN = /\uE000(\d+)\uE001/g;
20
+
21
+ type RowScope = { readonly listName: string; readonly itemFields: TemplateVariables } | null;
22
+
23
+ type CompileState = {
24
+ readonly variables: TemplateVariables;
25
+ readonly expressions: string[];
26
+ };
27
+
28
+ function marker(state: CompileState, expression: string): string {
29
+ state.expressions.push(expression);
30
+ return `${EXPRESSION_OPEN}${state.expressions.length - 1}${EXPRESSION_CLOSE}`;
31
+ }
32
+
33
+ function ownVariable(variables: TemplateVariables, name: string): TemplateVariableDef | undefined {
34
+ return Object.hasOwn(variables, name) ? variables[name] : undefined;
35
+ }
36
+
37
+ function itemFieldsOf(variables: TemplateVariables, listName: string): TemplateVariables | null {
38
+ const def = ownVariable(variables, listName);
39
+ return def?.type === "list" ? def.item : null;
40
+ }
41
+
42
+ function rowScopeOf(element: ScanElement, state: CompileState): RowScope {
43
+ const listName = element.closest("tr[data-each]")?.getAttribute("data-each") ?? null;
44
+ if (listName === null) return null;
45
+ const itemFields = itemFieldsOf(state.variables, listName);
46
+ return itemFields === null ? null : { listName, itemFields };
47
+ }
48
+
49
+ function resolveReference(
50
+ name: string,
51
+ scope: RowScope,
52
+ variables: TemplateVariables,
53
+ ): { readonly path: string; readonly def: TemplateVariableDef | undefined } {
54
+ if (scope !== null && ownVariable(scope.itemFields, name) !== undefined) {
55
+ return { path: name, def: ownVariable(scope.itemFields, name) };
56
+ }
57
+ return { path: scope === null ? name : `@root.${name}`, def: ownVariable(variables, name) };
58
+ }
59
+
60
+ function valueExpression(path: string, def: TemplateVariableDef | undefined): string {
61
+ if (def?.type === "money") return `{{money ${path}}}`;
62
+ if (def?.type === "date") return `{{date ${path}}}`;
63
+ if (def?.type === "richText") return `{{richText ${path}}}`;
64
+ return `{{${path}}}`;
65
+ }
66
+
67
+ function rewriteVariable(element: ScanElement, state: CompileState): void {
68
+ const name = element.getAttribute("data-variable") ?? "";
69
+ const replacement = element.ownerDocument.createTextNode(
70
+ isDocumentReferenceName(name)
71
+ ? marker(state, valueExpression(...pathAndDef(name, element, state)))
72
+ : "",
73
+ );
74
+ element.replaceWith(replacement);
75
+ }
76
+
77
+ function pathAndDef(
78
+ name: string,
79
+ element: ScanElement,
80
+ state: CompileState,
81
+ ): [string, TemplateVariableDef | undefined] {
82
+ const { path, def } = resolveReference(name, rowScopeOf(element, state), state.variables);
83
+ return [path, def];
84
+ }
85
+
86
+ function rewriteLink(element: ScanElement, state: CompileState): void {
87
+ const href = element.getAttribute("href") ?? "";
88
+ if (isDocumentPlaceholderHref(href)) {
89
+ const name = href.slice(2, -2);
90
+ if (isDocumentReferenceName(name)) {
91
+ const [path] = pathAndDef(name, element, state);
92
+ element.setAttribute("href", marker(state, `{{link ${path}}}`));
93
+ } else {
94
+ element.setAttribute("href", "#");
95
+ }
96
+ } else if (!isAllowedDocumentLinkHref(href)) {
97
+ element.setAttribute("href", "#");
98
+ }
99
+ }
100
+
101
+ function rewriteRepeatRow(row: ScanElement, state: CompileState): void {
102
+ const listName = row.getAttribute("data-each") ?? "";
103
+ const document = row.ownerDocument;
104
+ if (itemFieldsOf(state.variables, listName) === null || !isDocumentReferenceName(listName)) {
105
+ row.remove();
106
+ } else {
107
+ row.before(document.createTextNode(marker(state, `{{#each ${listName}}}`)));
108
+ row.after(document.createTextNode(marker(state, "{{/each}}")));
109
+ row.removeAttribute("data-each");
110
+ row.removeAttribute("data-repeat-header");
111
+ }
112
+ }
113
+
114
+ function stripActiveContent(element: ScanElement): void {
115
+ for (const attribute of Array.from(element.attributes)) {
116
+ if (/^on/i.test(attribute.name)) element.removeAttribute(attribute.name);
117
+ }
118
+ }
119
+
120
+ function neutralizeBraces(html: string): string {
121
+ return html.replace(/[{}]/g, (brace) => (brace === "{" ? "&#123;" : "&#125;"));
122
+ }
123
+
124
+ export type CompileDocumentBodyOptions = {
125
+ // Returns the src for an `img[data-resource]` key; undefined drops the image.
126
+ readonly resourceSrc?: (key: string) => string | undefined;
127
+ // Mail clients ignore document CSS, so a full-width logo needs an inline width cap.
128
+ readonly resourceImageMaxWidthPx?: number;
129
+ // Mail clients ignore document CSS, so mail turns data-align into an inline style.
130
+ readonly alignmentAsInlineStyle?: boolean;
131
+ // Same reason: a[data-button] only looks like a button through editor/print CSS.
132
+ readonly buttonAsInlineStyle?: boolean;
133
+ // Same reason: cell padding and table width only exist in editor/print CSS, so without
134
+ // inline styles a mail table renders its cells flush against each other.
135
+ readonly tableAsInlineStyle?: boolean;
136
+ // Replaces the default button look; only values from the layout appearance belong here.
137
+ readonly buttonInlineStyle?: string;
138
+ };
139
+
140
+ const MAIL_BUTTON_STYLE =
141
+ "display:inline-block;padding:10px 20px;border-radius:6px;background-color:#1f5468;color:#ffffff;font-weight:600;text-decoration:none";
142
+
143
+ const MAIL_TABLE_STYLE = "width:100%;border-collapse:collapse";
144
+ const MAIL_TABLE_CELL_STYLE = "padding:6px 12px;vertical-align:top;border-bottom:1px solid #e5e5e5";
145
+ const MAIL_TABLE_HEAD_STYLE = "font-size:12px;font-weight:600;color:#6b7280;text-align:left";
146
+ const MAIL_SMALL_STYLE = "font-size:12px;color:#6b7280";
147
+
148
+ // Appended after an existing style (data-align) so the alignment survives.
149
+ function appendStyle(element: ScanElement, style: string): void {
150
+ const existing = element.getAttribute("style");
151
+ element.setAttribute("style", existing === null ? style : `${existing};${style}`);
152
+ }
153
+
154
+ const RESOURCE_IMAGE_CONTENT_TYPES: ReadonlySet<string> = new Set([
155
+ "image/png",
156
+ "image/jpeg",
157
+ "image/gif",
158
+ "image/webp",
159
+ ]);
160
+
161
+ // Unique valid resource keys in document order.
162
+ export function collectDocumentResourceKeys(html: string): readonly string[] {
163
+ return withParsedDocument(html, (root) => {
164
+ const keys = new Set<string>();
165
+ for (const element of Array.from(root.querySelectorAll("img[data-resource]"))) {
166
+ const key = element.getAttribute("data-resource") ?? "";
167
+ if (isDocumentResourceKey(key)) keys.add(key);
168
+ }
169
+ return [...keys];
170
+ });
171
+ }
172
+
173
+ export function toResourceDataUri(contentType: string, bytes: Uint8Array): string {
174
+ if (!RESOURCE_IMAGE_CONTENT_TYPES.has(contentType)) {
175
+ throw new Error(`resource content type "${contentType}" cannot be embedded as an image`);
176
+ }
177
+ return `data:${contentType};base64,${Buffer.from(bytes).toString("base64")}`;
178
+ }
179
+
180
+ function rewriteAlignment(element: ScanElement): void {
181
+ const align = element.getAttribute("data-align");
182
+ element.removeAttribute("data-align");
183
+ if (isOneOf(DOCUMENT_TEXT_ALIGNMENTS, align))
184
+ element.setAttribute("style", `text-align:${align}`);
185
+ }
186
+
187
+ function rewriteResourceImage(
188
+ element: ScanElement,
189
+ resourceSrc: CompileDocumentBodyOptions["resourceSrc"],
190
+ maxWidthPx: number | undefined,
191
+ ): void {
192
+ const key = element.getAttribute("data-resource") ?? "";
193
+ const src = isDocumentResourceKey(key) ? resourceSrc?.(key) : undefined;
194
+ if (src === undefined) {
195
+ element.remove();
196
+ // skip: an unresolved resource key drops the image
197
+ return;
198
+ }
199
+ element.setAttribute("src", src);
200
+ element.removeAttribute("data-resource");
201
+ if (maxWidthPx !== undefined)
202
+ element.setAttribute("style", `max-width:${maxWidthPx}px;height:auto`);
203
+ }
204
+
205
+ function stripScriptsAndActiveContent(
206
+ root: ScanElement,
207
+ options: CompileDocumentBodyOptions,
208
+ ): void {
209
+ for (const element of Array.from(root.querySelectorAll("script"))) element.remove();
210
+ for (const element of Array.from(root.querySelectorAll("img[data-resource]"))) {
211
+ rewriteResourceImage(element, options.resourceSrc, options.resourceImageMaxWidthPx);
212
+ }
213
+ for (const element of Array.from(root.querySelectorAll("*"))) stripActiveContent(element);
214
+ }
215
+
216
+ function applyAlignmentInlineStyles(root: ScanElement): void {
217
+ for (const element of Array.from(root.querySelectorAll("[data-align]"))) {
218
+ rewriteAlignment(element);
219
+ }
220
+ for (const element of Array.from(root.querySelectorAll("[data-size]"))) {
221
+ element.removeAttribute("data-size");
222
+ appendStyle(element, MAIL_SMALL_STYLE);
223
+ }
224
+ }
225
+
226
+ function applyTableInlineStyles(root: ScanElement): void {
227
+ for (const table of Array.from(root.querySelectorAll("table"))) {
228
+ appendStyle(table, MAIL_TABLE_STYLE);
229
+ }
230
+ for (const cell of Array.from(root.querySelectorAll("td, th"))) {
231
+ appendStyle(cell, MAIL_TABLE_CELL_STYLE);
232
+ }
233
+ for (const head of Array.from(root.querySelectorAll("th"))) {
234
+ appendStyle(head, MAIL_TABLE_HEAD_STYLE);
235
+ }
236
+ }
237
+
238
+ function rewriteLinks(
239
+ root: ScanElement,
240
+ state: CompileState,
241
+ options: CompileDocumentBodyOptions,
242
+ ): void {
243
+ for (const element of Array.from(root.querySelectorAll("a[href]"))) {
244
+ rewriteLink(element, state);
245
+ if (options.buttonAsInlineStyle === true && element.hasAttribute("data-button")) {
246
+ element.setAttribute("style", options.buttonInlineStyle ?? MAIL_BUTTON_STYLE);
247
+ }
248
+ }
249
+ }
250
+
251
+ // Turns a sanitized document body into Handlebars source. Rows and links are
252
+ // rewritten in document order; list rows last so their placeholders still see
253
+ // the `tr[data-each]` ancestor.
254
+ export function compileDocumentBodyToHandlebars(
255
+ html: string,
256
+ variables: TemplateVariables,
257
+ options: CompileDocumentBodyOptions = {},
258
+ ): string {
259
+ const state: CompileState = { variables, expressions: [] };
260
+ const scrubbed = html.replaceAll(EXPRESSION_OPEN, "").replaceAll(EXPRESSION_CLOSE, "");
261
+ const compiled = withParsedDocument(scrubbed, (root) => {
262
+ stripScriptsAndActiveContent(root, options);
263
+ if (options.alignmentAsInlineStyle === true) applyAlignmentInlineStyles(root);
264
+ if (options.tableAsInlineStyle === true) applyTableInlineStyles(root);
265
+ for (const element of Array.from(root.querySelectorAll("span[data-variable]"))) {
266
+ rewriteVariable(element, state);
267
+ }
268
+ rewriteLinks(root, state, options);
269
+ for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
270
+ rewriteRepeatRow(element, state);
271
+ }
272
+ return root.innerHTML;
273
+ });
274
+ return neutralizeBraces(compiled).replace(
275
+ MARKER_PATTERN,
276
+ (_match, index: string) => state.expressions[Number(index)] ?? "",
277
+ );
278
+ }