@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
@@ -1,15 +1,8 @@
1
- import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import {
3
- defineWriteHandler,
4
- SYSTEM_TENANT_ID,
5
- type TenantId,
6
- } from "@cosmicdrift/kumiko-framework/engine";
7
- import { AccessDeniedError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
1
+ import { defineWriteHandler, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
8
2
  import * as z from "zod";
9
- import { requireForTenant } from "../../shared/index.js";
10
- import type { TemplateResourceRow } from "../table.js";
11
- import { templateResourcesTable } from "../table.js";
12
- import { executor, upsertPayloadSchema } from "./shared.js";
3
+ import { resolveTemplateVariables } from "../template-validation.js";
4
+ import { upsertPayloadSchema } from "./shared.js";
5
+ import { writeTemplateRow } from "./template-write.js";
13
6
 
14
7
  // Tenant-Override anlegen/updaten. Liegt unter event.user.tenantId,
15
8
  // scope='tenant'. Default-Status='draft' — User publisht explizit
@@ -24,78 +17,24 @@ export const upsertTenantWrite = defineWriteHandler({
24
17
  }),
25
18
  access: { roles: ["TenantAdmin", "SystemAdmin"] },
26
19
  description:
27
- "Creates or overwrites a tenant-scoped template override with its variable schema, landing as a draft that only publish makes live; it refuses a system-tenant target, which is what upsert-system is for.",
20
+ "Creates or overwrites a tenant-scoped template override with its variable schema, landing as a draft that only publish makes live (over an active override the draft is stored beside it, the active version keeps being served until publish); it refuses a system-tenant target, which is what upsert-system is for.",
28
21
  handler: async (event, ctx) => {
29
- const override = event.payload.tenantIdOverride;
30
- // upsertTenant erzeugt scope='tenant'. SYSTEM_TENANT_ID-Override würde
31
- // scope='tenant' unter SYSTEM_TENANT_ID schreiben → inkonsistenter Zustand
32
- // (Resolver-Logik trennt sauber zwischen system+tenant). SystemAdmin muss
33
- // upsertSystem für System-Defaults nutzen.
34
- if (override === SYSTEM_TENANT_ID) {
35
- return writeFailure(
36
- new AccessDeniedError({
37
- i18nKey: "templateResolver.errors.useUpsertSystemForSystemTenant",
38
- details: { reason: "system_tenant_override_not_allowed_use_upsert_system" },
39
- }),
40
- );
41
- }
42
22
  // @cast-boundary engine-payload — override aus Zod-parsed string,
43
23
  // event.user.tenantId schon TenantId-branded; union als TenantId casten
44
24
  // ist legit (override ist UUID-Format-validiert in schema).
45
- const tenantId = (override ?? event.user.tenantId) as TenantId;
46
- const { db, streamTenantId } = requireForTenant(ctx, tenantId);
47
-
48
- const existing = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
25
+ const tenantId = (event.payload.tenantIdOverride ?? event.user.tenantId) as TenantId;
26
+ return writeTemplateRow({
27
+ ctx,
28
+ user: event.user,
49
29
  tenantId,
50
- slug: event.payload.slug,
51
- kind: event.payload.kind,
52
- locale: event.payload.locale,
53
- });
54
-
55
- const fields = {
56
- slug: event.payload.slug,
57
- kind: event.payload.kind,
58
- locale: event.payload.locale,
59
- content: event.payload.content,
60
- contentFormat: event.payload.contentFormat,
61
- variableSchema: JSON.stringify(event.payload.variableSchema),
62
- linkedResources: JSON.stringify(event.payload.linkedResources),
63
- title: event.payload.title ?? null,
64
- folder: event.payload.folder ?? null,
65
- scope: "tenant" as const,
66
- parentTemplateId: event.payload.parentTemplateId ?? null,
30
+ scope: "tenant",
67
31
  status: event.payload.status,
68
- };
69
-
70
- if (existing) {
71
- const result = await executor.update(
72
- { id: existing.id, version: existing.version, changes: fields },
73
- event.user,
74
- db,
75
- { streamTenantId },
76
- );
77
- if (!result.isSuccess) return result;
78
- return {
79
- isSuccess: true as const,
80
- data: { id: String(existing.id), slug: event.payload.slug, isNew: false },
81
- };
82
- }
83
-
84
- const result = await executor.create({ ...fields, tenantId }, event.user, db, {
85
- streamTenantId,
32
+ variables: resolveTemplateVariables(
33
+ ctx.registry,
34
+ event.payload.slug,
35
+ event.payload.variableSchema,
36
+ ),
37
+ payload: event.payload,
86
38
  });
87
- if (!result.isSuccess) return result;
88
- // @cast-boundary db-row — executor.create returnt Record-row aus
89
- // INSERT RETURNING; shape { id } ist garantiert weil PK in der
90
- // Returning-Klausel ist.
91
- const createdRow = result.data as { id: string | number };
92
- return {
93
- isSuccess: true as const,
94
- data: {
95
- id: String(createdRow.id),
96
- slug: event.payload.slug,
97
- isNew: true,
98
- },
99
- };
100
39
  },
101
40
  });
@@ -0,0 +1,31 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import * as z from "zod";
4
+ import { TEMPLATE_KINDS } from "../constants.js";
5
+ import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
6
+ import { templateSlugSchema } from "../template-meta.js";
7
+ import { collectTemplateUsage } from "../template-usage.js";
8
+ import { effectiveMailLayoutSlugs } from "./overview.query.js";
9
+
10
+ const MAIL_ROW_LIMIT = 5000;
11
+
12
+ export const usageQuery = defineQueryHandler({
13
+ name: "usage",
14
+ schema: z.object({ slug: templateSlugSchema, kind: z.enum(TEMPLATE_KINDS) }),
15
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
16
+ description:
17
+ "Lists the code-registered notifications that use one template: a mail template is used by the notification whose qualified name is its slug and that has a code template, a mail layout by every mail notification whose effective template (own override, else system) names it, or whose code meta does when no stored template exists, or that falls back to the default layout; use it to show what a change to a template affects.",
18
+ handler: async (query, ctx) => {
19
+ const { slug, kind } = query.payload;
20
+ if (kind !== "mail-layout") return { usedBy: collectTemplateUsage(ctx.registry, slug, kind) };
21
+ const mailRows = await selectMany<TemplateResourceRow>(
22
+ ctx.db,
23
+ templateResourcesTable,
24
+ { kind: "mail" },
25
+ { limit: MAIL_ROW_LIMIT },
26
+ );
27
+ return {
28
+ usedBy: collectTemplateUsage(ctx.registry, slug, kind, effectiveMailLayoutSlugs(mailRows)),
29
+ };
30
+ },
31
+ });
@@ -0,0 +1,116 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import {
3
+ defineQueryHandler,
4
+ type HandlerContext,
5
+ type SessionUser,
6
+ SYSTEM_TENANT_ID,
7
+ } from "@cosmicdrift/kumiko-framework/engine";
8
+ import * as z from "zod";
9
+ import type { ContentFormat, UpsertKind } from "../constants.js";
10
+ import type { TemplateWriteScope } from "../layout-regions.js";
11
+ import { imageResourceKeysOf } from "../linked-resources.js";
12
+ import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
13
+ import { resolveTemplateVariables, systemPlaceholdersFor } from "../template-validation.js";
14
+ import { contentFormatSchema, kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
15
+ import { evaluateTemplateWrite } from "./template-gates.js";
16
+
17
+ async function knownResourceKeys(
18
+ db: Parameters<typeof selectMany>[0],
19
+ where: {
20
+ readonly tenantId: string;
21
+ readonly slug: string;
22
+ readonly kind: string;
23
+ readonly scope: "tenant" | "system";
24
+ readonly locale: string | undefined;
25
+ },
26
+ ): Promise<string[]> {
27
+ const rows = await selectMany<TemplateResourceRow>(db, templateResourcesTable, {
28
+ tenantId: where.tenantId,
29
+ slug: where.slug,
30
+ kind: where.kind,
31
+ scope: where.scope,
32
+ ...(where.locale === undefined ? {} : { locale: where.locale }),
33
+ ...(where.scope === "system" ? { status: "active" } : {}),
34
+ });
35
+ return rows.flatMap((row) => imageResourceKeysOf(row.linkedResources));
36
+ }
37
+
38
+ type TemplateDraftInput = {
39
+ readonly slug: string;
40
+ readonly kind: UpsertKind;
41
+ readonly contentFormat: ContentFormat;
42
+ readonly content: string;
43
+ readonly meta?: unknown;
44
+ readonly variableSchema: Readonly<Record<string, unknown>>;
45
+ readonly locale?: string | undefined;
46
+ readonly scope: TemplateWriteScope;
47
+ };
48
+
49
+ // The dry run of the write pipeline; preview feeds on the evaluation, validate returns the response.
50
+ export async function validateTemplateDraft(
51
+ ctx: HandlerContext,
52
+ user: SessionUser,
53
+ payload: TemplateDraftInput,
54
+ ) {
55
+ const variables = resolveTemplateVariables(ctx.registry, payload.slug, payload.variableSchema);
56
+ const { slug, kind, scope, locale } = payload;
57
+ const known = [
58
+ ...(await knownResourceKeys(ctx.db, {
59
+ tenantId: SYSTEM_TENANT_ID,
60
+ slug,
61
+ kind,
62
+ scope: "system",
63
+ locale,
64
+ })),
65
+ ...(scope === "tenant"
66
+ ? await knownResourceKeys(ctx.db, { tenantId: user.tenantId, slug, kind, scope, locale })
67
+ : []),
68
+ ];
69
+ const evaluation = evaluateTemplateWrite({
70
+ kind,
71
+ scope,
72
+ contentFormat: payload.contentFormat,
73
+ content: payload.content,
74
+ rawMeta: payload.meta,
75
+ variables,
76
+ resourceKeys: known,
77
+ });
78
+ const validation = evaluation.validation;
79
+ const response = {
80
+ canSave: evaluation.ok,
81
+ canPublish: evaluation.ok && evaluation.publishIssues.length === 0,
82
+ issues: evaluation.ok ? [] : [...evaluation.issues],
83
+ publishIssues: evaluation.ok ? [...evaluation.publishIssues] : [],
84
+ used: [...(validation?.used ?? [])],
85
+ unknown: [...(validation?.unknown ?? [])],
86
+ missingRequired: [...(validation?.missingRequired ?? [])],
87
+ warnings: (validation?.unknownResources ?? []).map((key) => ({
88
+ code: "resource_not_uploaded" as const,
89
+ detail: key,
90
+ i18nKey: "templateResolver.warnings.resourceNotUploaded" as const,
91
+ })),
92
+ variables,
93
+ systemPlaceholders: [...systemPlaceholdersFor(kind)].sort(),
94
+ };
95
+ return { response, evaluation };
96
+ }
97
+
98
+ export const validateQuery = defineQueryHandler({
99
+ name: "validate",
100
+ schema: z.object({
101
+ slug: templateSlugSchema,
102
+ kind: kindSchema,
103
+ contentFormat: contentFormatSchema,
104
+ content: z.string().max(200_000),
105
+ meta: z.unknown().optional(),
106
+ variableSchema: z.record(z.string(), z.unknown()).default({}),
107
+ /** Narrows the known resource keys to one locale's rows. */
108
+ locale: localeSchema.optional(),
109
+ scope: z.enum(["tenant", "system"]).default("tenant"),
110
+ }),
111
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
112
+ description:
113
+ "Dry-runs the write pipeline for a template draft without saving: reports whether it can be saved or published, the sanitizer and placeholder problems, which placeholders it uses, which are unknown, which required variables are missing, and the variables and system placeholders available for the kind; use it to give an editor live feedback.",
114
+ handler: async (query, ctx) =>
115
+ (await validateTemplateDraft(ctx, query.user, query.payload)).response,
116
+ });
@@ -6,12 +6,28 @@ export {
6
6
  type TemplateResolverApi,
7
7
  type TemplateResource,
8
8
  } from "./api.js";
9
+ export {
10
+ type CompileDocumentBodyOptions,
11
+ collectDocumentResourceKeys,
12
+ compileDocumentBodyToHandlebars,
13
+ toResourceDataUri,
14
+ } from "./compile-document-body.js";
9
15
  export {
10
16
  CONTENT_FORMATS,
11
17
  type ContentFormat,
18
+ DEFAULT_MAIL_LAYOUT_SLUG,
19
+ DOCUMENT_HTML_KINDS,
20
+ type DocumentHtmlKind,
12
21
  FALLBACK_LOCALE,
13
22
  NON_RENDER_KINDS,
14
23
  type NonRenderKind,
24
+ PDF_DEFAULT_MARGIN_MM,
25
+ PDF_FOOTER_MARGIN_MM,
26
+ PDF_HEADER_MARGIN_MM,
27
+ PDF_ORIENTATIONS,
28
+ PDF_PAGE_FORMATS,
29
+ type PdfOrientation,
30
+ type PdfPageFormat,
15
31
  RENDER_KINDS,
16
32
  type RenderKind,
17
33
  SYSTEM_TENANT_ID,
@@ -25,18 +41,113 @@ export {
25
41
  UPSERT_KINDS,
26
42
  type UpsertKind,
27
43
  } from "./constants.js";
44
+ export {
45
+ type DocumentViolation,
46
+ type DocumentViolationCode,
47
+ documentViolationsToFieldIssues,
48
+ type SanitizeBodyResult,
49
+ type SanitizeHtmlResult,
50
+ sanitizeDocumentHtml,
51
+ sanitizeTemplateBody,
52
+ } from "./document-sanitizer.js";
53
+ export {
54
+ DOCUMENT_NODE_NAMES,
55
+ DOCUMENT_TEXT_ALIGNMENTS,
56
+ DocumentButton,
57
+ DocumentDivider,
58
+ DocumentLink,
59
+ DocumentPageBreak,
60
+ DocumentResource,
61
+ DocumentTable,
62
+ DocumentTableCell,
63
+ DocumentTableHeader,
64
+ DocumentTableRow,
65
+ DocumentTextAlign,
66
+ type DocumentTextAlignment,
67
+ DocumentVariable,
68
+ documentExtensions,
69
+ isAllowedDocumentLinkHref,
70
+ isDocumentPlaceholderHref,
71
+ isDocumentReferenceName,
72
+ isDocumentResourceKey,
73
+ } from "./document-schema.js";
28
74
  export { createTemplateResolverFeature } from "./feature.js";
75
+ export type { LayoutSourceResponse, LayoutSourceRow } from "./handlers/layout-source.query.js";
76
+ export type {
77
+ OverviewKindGroup,
78
+ OverviewOrigin,
79
+ OverviewResponse,
80
+ OverviewRow,
81
+ OverviewUsage,
82
+ } from "./handlers/overview.query.js";
83
+ export { localeSchema } from "./handlers/shared.js";
84
+ export {
85
+ isHexColor,
86
+ type MailLayoutAppearance,
87
+ mailLayoutAppearance,
88
+ } from "./layout-appearance.js";
89
+ export {
90
+ type LayoutRegions,
91
+ readLayoutRegions,
92
+ type SanitizeLayoutResult,
93
+ sanitizeLayoutHtml,
94
+ } from "./layout-regions.js";
95
+ export {
96
+ fileAttachmentsOf,
97
+ imageLinksOf,
98
+ type LinkedResourceEntry,
99
+ parseLinkedResourceEntries,
100
+ pdfAttachmentLinksOf,
101
+ type TemplatePdfAttachmentLink,
102
+ templatePdfAttachmentLinkSchema,
103
+ } from "./linked-resources.js";
104
+ export { prepareDocumentForPrint } from "./print-document.js";
29
105
  export {
30
106
  TEMPLATE_RESOLVER_FEATURE,
31
107
  TemplateResolverErrors,
32
108
  TemplateResolverHandlers,
33
109
  TemplateResolverQueries,
34
110
  } from "./qualified-names.js";
111
+ export { mapRecordToVariables } from "./record-mapping.js";
112
+ export {
113
+ readTemplateResourceMaxBytes,
114
+ type StoredTemplateResource,
115
+ storeTemplateResource,
116
+ } from "./store-template-resource.js";
35
117
  export {
36
118
  type TemplateResourceRow,
37
119
  templateResourceEntity,
38
120
  templateResourcesTable,
39
121
  } from "./table.js";
122
+ export { type TemplateBundle, templateBundleSchema } from "./template-bundle.js";
123
+ export {
124
+ isMailTemplateMeta,
125
+ type LayoutTemplateMeta,
126
+ MAIL_CATEGORIES,
127
+ type MailCategory,
128
+ type MailLayoutMeta,
129
+ type MailTemplateMeta,
130
+ type PdfTemplateMeta,
131
+ parseTemplateMeta,
132
+ pdfAttachmentFileNamePatternSchema,
133
+ readMailLayoutMeta,
134
+ readPdfTemplateMeta,
135
+ type TemplateMeta,
136
+ type TemplateMetaIssue,
137
+ type TemplateMetaResult,
138
+ templateSlugSchema,
139
+ } from "./template-meta.js";
140
+ export { type DocumentDraftOutcome, evaluateDocumentDraft } from "./template-preview.js";
141
+ export { readTemplateResource, type TemplateResourceFile } from "./template-resources.js";
142
+ export { collectTemplateUsage, type TemplateUsageEntry } from "./template-usage.js";
143
+ export {
144
+ hasPlaceholderProblems,
145
+ resolveTemplateVariables,
146
+ SYSTEM_PLACEHOLDERS,
147
+ systemPlaceholdersFor,
148
+ type TemplateValidation,
149
+ validateTemplatePlaceholders,
150
+ } from "./template-validation.js";
40
151
  export {
41
152
  type UserContentEntryRow,
42
153
  userContentEntriesTable,
@@ -0,0 +1,40 @@
1
+ import { HEX_COLOR_PATTERN } from "./constants.js";
2
+ import { type MailLayoutMeta, readMailLayoutMeta } from "./template-meta.js";
3
+
4
+ // Every value is a member of a closed set, so it can be written into MJML and inline styles as is.
5
+ export type MailLayoutAppearance = {
6
+ readonly width: number;
7
+ readonly fontFamily: string;
8
+ readonly outerBackground: string;
9
+ readonly accentLine: boolean;
10
+ readonly buttonStyle: string;
11
+ };
12
+
13
+ const FONT_FAMILIES = {
14
+ sans: "Arial, Helvetica, sans-serif",
15
+ serif: "Georgia, 'Times New Roman', serif",
16
+ } as const;
17
+
18
+ const BUTTON_COLOR = "#1f5468";
19
+ const BUTTON_BASE = "display:inline-block;padding:10px 20px;font-weight:600;text-decoration:none";
20
+
21
+ const BUTTON_STYLES = {
22
+ "filled-rounded": `${BUTTON_BASE};border-radius:6px;background-color:${BUTTON_COLOR};color:#ffffff`,
23
+ "filled-square": `${BUTTON_BASE};background-color:${BUTTON_COLOR};color:#ffffff`,
24
+ outline: `${BUTTON_BASE};border:2px solid ${BUTTON_COLOR};border-radius:6px;color:${BUTTON_COLOR}`,
25
+ } as const;
26
+
27
+ export function isHexColor(value: string): boolean {
28
+ return HEX_COLOR_PATTERN.test(value);
29
+ }
30
+
31
+ export function mailLayoutAppearance(meta: MailLayoutMeta | unknown): MailLayoutAppearance {
32
+ const parsed = readMailLayoutMeta(meta);
33
+ return {
34
+ width: parsed.width ?? 600,
35
+ fontFamily: FONT_FAMILIES[parsed.font ?? "sans"],
36
+ outerBackground: parsed.outerBackground ?? "#f5f5f5",
37
+ accentLine: (parsed.accentLine ?? "none") === "branding",
38
+ buttonStyle: BUTTON_STYLES[parsed.buttonStyle ?? "filled-rounded"],
39
+ };
40
+ }
@@ -0,0 +1,206 @@
1
+ // Server-only (happy-dom). A structured layout is the editable frame around a
2
+ // mail or PDF body: header, signature and footer regions plus one empty
3
+ // content slot. Everything else is rejected so a layout can never carry markup
4
+ // the renderer did not plan for.
5
+
6
+ import type { Element as ScanElement } from "happy-dom";
7
+ import type { ContentFormat } from "./constants.js";
8
+ import {
9
+ type DocumentViolation,
10
+ type SanitizeBodyResult,
11
+ sanitizeDocumentHtml,
12
+ sanitizeTemplateBody,
13
+ violation,
14
+ withParsedDocument,
15
+ } from "./document-sanitizer.js";
16
+
17
+ const REGION_ATTRIBUTES = ["data-header", "data-content", "data-signature", "data-footer"] as const;
18
+ type RegionAttribute = (typeof REGION_ATTRIBUTES)[number];
19
+
20
+ const MAIL_LAYOUT_CONTENT_SLOT = /\{\{\{\s*content\s*\}\}\}/g;
21
+
22
+ export type LayoutRegions = {
23
+ readonly header: string;
24
+ readonly signature: string;
25
+ readonly footer: string;
26
+ };
27
+
28
+ export type SanitizeLayoutResult =
29
+ | { readonly ok: true; readonly html: string }
30
+ | { readonly ok: false; readonly violations: readonly DocumentViolation[] };
31
+
32
+ type ParsedRegions = {
33
+ readonly innerHtml: Partial<Record<RegionAttribute, string>>;
34
+ readonly contentIsEmpty: boolean;
35
+ readonly violations: DocumentViolation[];
36
+ };
37
+
38
+ const ELEMENT_NODE = 1;
39
+ const TEXT_NODE = 3;
40
+
41
+ function regionAttributeOf(element: ScanElement): RegionAttribute | null {
42
+ if (element.tagName.toLowerCase() !== "div") return null;
43
+ const present = REGION_ATTRIBUTES.filter((name) => element.hasAttribute(name));
44
+ return present.length === 1 ? (present[0] ?? null) : null;
45
+ }
46
+
47
+ function parseRegions(html: string): ParsedRegions {
48
+ return withParsedDocument(html, (body) => {
49
+ const innerHtml: Partial<Record<RegionAttribute, string>> = {};
50
+ const violations: DocumentViolation[] = [];
51
+ let contentIsEmpty = true;
52
+ const invalid = (detail: string, message: string) =>
53
+ violations.push(violation("layout_invalid_structure", detail, message));
54
+ for (const node of Array.from(body.childNodes)) {
55
+ if (node.nodeType === ELEMENT_NODE) continue;
56
+ if (node.nodeType !== TEXT_NODE || (node.textContent ?? "").trim() !== "") {
57
+ invalid("text", "Text and comments outside the layout regions are not allowed");
58
+ }
59
+ }
60
+ for (const element of Array.from(body.children)) {
61
+ const region = regionAttributeOf(element);
62
+ if (region === null) {
63
+ invalid(
64
+ element.tagName.toLowerCase(),
65
+ "Only <div data-header>, <div data-content>, <div data-signature> and <div data-footer> are allowed at the top level",
66
+ );
67
+ continue;
68
+ }
69
+ if (innerHtml[region] !== undefined) {
70
+ invalid(region, `Layout region ${region} may appear only once`);
71
+ continue;
72
+ }
73
+ innerHtml[region] = element.innerHTML;
74
+ if (region === "data-content") {
75
+ contentIsEmpty = element.children.length === 0 && (element.textContent ?? "").trim() === "";
76
+ }
77
+ }
78
+ return { innerHtml, contentIsEmpty, violations };
79
+ });
80
+ }
81
+
82
+ export function sanitizeLayoutHtml(html: string): SanitizeLayoutResult {
83
+ const parsed = parseRegions(html);
84
+ const violations = [...parsed.violations];
85
+ if (parsed.innerHtml["data-content"] === undefined) {
86
+ violations.push(
87
+ violation(
88
+ "layout_missing_content_slot",
89
+ "data-content",
90
+ "A layout needs exactly one empty <div data-content></div> where the body is placed",
91
+ ),
92
+ );
93
+ } else if (!parsed.contentIsEmpty) {
94
+ violations.push(
95
+ violation(
96
+ "layout_invalid_structure",
97
+ "data-content",
98
+ "The data-content region must be empty",
99
+ ),
100
+ );
101
+ }
102
+ const cleanedHtml: Partial<Record<RegionAttribute, string>> = {};
103
+ for (const name of ["data-header", "data-signature", "data-footer"] as const) {
104
+ const inner = parsed.innerHtml[name];
105
+ if (inner === undefined || inner.trim() === "") continue;
106
+ const result = sanitizeDocumentHtml(inner);
107
+ if (result.ok) cleanedHtml[name] = result.html;
108
+ else violations.push(...result.violations);
109
+ }
110
+ if (violations.length > 0) return { ok: false, violations };
111
+ const part = (name: RegionAttribute) => {
112
+ const regionHtml = cleanedHtml[name];
113
+ return regionHtml === undefined ? "" : `<div ${name}>${regionHtml}</div>`;
114
+ };
115
+ return {
116
+ ok: true,
117
+ html: [
118
+ part("data-header"),
119
+ "<div data-content></div>",
120
+ part("data-signature"),
121
+ part("data-footer"),
122
+ ].join(""),
123
+ };
124
+ }
125
+
126
+ export function readLayoutRegions(html: string): LayoutRegions {
127
+ const { innerHtml } = parseRegions(html);
128
+ return {
129
+ header: innerHtml["data-header"] ?? "",
130
+ signature: innerHtml["data-signature"] ?? "",
131
+ footer: innerHtml["data-footer"] ?? "",
132
+ };
133
+ }
134
+
135
+ export type TemplateWriteScope = "system" | "tenant";
136
+
137
+ function rejected(violationItem: DocumentViolation): SanitizeBodyResult {
138
+ return { ok: false, violations: [violationItem] };
139
+ }
140
+
141
+ function sanitizeMailLayout(
142
+ scope: TemplateWriteScope,
143
+ contentFormat: ContentFormat,
144
+ content: string,
145
+ ): SanitizeBodyResult {
146
+ if (contentFormat === "html") return fromLayoutResult(sanitizeLayoutHtml(content));
147
+ if (contentFormat !== "mjml") {
148
+ return rejected(
149
+ violation(
150
+ "layout_requires_mjml_format",
151
+ contentFormat,
152
+ `Templates of kind mail-layout require contentFormat mjml or html, got "${contentFormat}"`,
153
+ ),
154
+ );
155
+ }
156
+ if (scope !== "system") {
157
+ return rejected(
158
+ violation(
159
+ "layout_source_system_only",
160
+ "mjml",
161
+ "Only system layouts may use mjml source; tenant layouts use the structured html format",
162
+ ),
163
+ );
164
+ }
165
+ if ((content.match(MAIL_LAYOUT_CONTENT_SLOT) ?? []).length !== 1) {
166
+ return rejected(
167
+ violation(
168
+ "layout_missing_content_slot",
169
+ "{{{content}}}",
170
+ "A mjml layout must contain {{{content}}} exactly once",
171
+ ),
172
+ );
173
+ }
174
+ return { ok: true, content };
175
+ }
176
+
177
+ function fromLayoutResult(result: SanitizeLayoutResult): SanitizeBodyResult {
178
+ return result.ok ? { ok: true, content: result.html } : result;
179
+ }
180
+
181
+ // One entry for every write path: layout kinds follow the scope-dependent
182
+ // layout rules, everything else the plain document sanitizer.
183
+ export function sanitizeTemplateWriteBody(input: {
184
+ readonly kind: string;
185
+ readonly scope: TemplateWriteScope;
186
+ readonly contentFormat: ContentFormat;
187
+ readonly content: string | null;
188
+ }): SanitizeBodyResult {
189
+ const { kind, scope, contentFormat, content } = input;
190
+ if (kind === "mail-layout" && content !== null) {
191
+ return sanitizeMailLayout(scope, contentFormat, content);
192
+ }
193
+ if (kind === "pdf-layout" && content !== null) {
194
+ if (contentFormat !== "html") {
195
+ return rejected(
196
+ violation(
197
+ "document_requires_html_format",
198
+ contentFormat,
199
+ `Templates of kind pdf-layout require contentFormat html, got "${contentFormat}"`,
200
+ ),
201
+ );
202
+ }
203
+ return fromLayoutResult(sanitizeLayoutHtml(content));
204
+ }
205
+ return sanitizeTemplateBody({ kind, contentFormat, content });
206
+ }