@cosmicdrift/kumiko-samples 0.362.0 → 0.364.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +23 -6
  3. package/packages/bundled-features/src/admin-shell/changes.json +6 -0
  4. package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
  5. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
  6. package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
  7. package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
  8. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
  9. package/packages/bundled-features/src/audit/changes.json +6 -0
  10. package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
  11. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
  12. package/packages/bundled-features/src/auth-email-password/changes.json +51 -0
  13. package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
  14. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  15. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  16. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
  17. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  18. package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
  19. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  20. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  21. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  22. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  23. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
  24. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +5 -13
  25. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  26. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  27. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  28. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  29. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
  30. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  31. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  32. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  33. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  34. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  35. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  36. package/packages/bundled-features/src/billing-foundation/changes.json +43 -0
  37. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  38. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +90 -44
  39. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
  40. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  41. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +118 -0
  42. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  43. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
  44. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +39 -33
  45. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  46. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  47. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
  48. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
  49. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +36 -20
  50. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
  51. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
  52. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  53. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
  54. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  55. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  56. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +12 -6
  57. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  58. package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
  59. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +20 -11
  60. package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
  61. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
  62. package/packages/bundled-features/src/cap-counter/changes.json +6 -0
  63. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
  64. package/packages/bundled-features/src/cap-counter/index.ts +1 -0
  65. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
  66. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  67. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  68. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  69. package/packages/bundled-features/src/channel-email/email-channel.ts +135 -29
  70. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  71. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  72. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  73. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  74. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  75. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  76. package/packages/bundled-features/src/config/changes.json +6 -0
  77. package/packages/bundled-features/src/config/handlers/set.write.ts +3 -7
  78. package/packages/bundled-features/src/config/resolver.ts +15 -12
  79. package/packages/bundled-features/src/config/write-helpers.ts +23 -0
  80. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  81. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
  82. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  83. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  84. package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
  85. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  86. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  87. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  88. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  89. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  90. package/packages/bundled-features/src/delivery/attachment-ref.ts +105 -0
  91. package/packages/bundled-features/src/delivery/changes.json +66 -0
  92. package/packages/bundled-features/src/delivery/channel-context.ts +152 -4
  93. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  94. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  95. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  96. package/packages/bundled-features/src/delivery/index.ts +19 -2
  97. package/packages/bundled-features/src/delivery/jobs.ts +97 -17
  98. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  99. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  100. package/packages/bundled-features/src/delivery/types.ts +34 -18
  101. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  102. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  103. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  104. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  105. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  106. package/packages/bundled-features/src/file-foundation/changes.json +6 -0
  107. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  108. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +89 -0
  109. package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
  110. package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
  111. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  112. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  113. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  114. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  115. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  116. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
  117. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  118. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  119. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  120. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  121. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  122. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
  123. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  124. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  125. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  126. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  127. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +42 -25
  128. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  129. package/packages/bundled-features/src/jobs/changes.json +19 -0
  130. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
  131. package/packages/bundled-features/src/jobs/feature.ts +7 -4
  132. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  133. package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
  134. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
  135. package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
  136. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
  137. package/packages/bundled-features/src/ledger/changes.json +7 -0
  138. package/packages/bundled-features/src/ledger/entity.ts +3 -1
  139. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  140. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  141. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  142. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  143. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  144. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  145. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  146. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  147. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  148. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  149. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  150. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  151. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  152. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  153. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  154. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  155. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  156. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  157. package/packages/bundled-features/src/renderer-foundation/types.ts +134 -35
  158. package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
  159. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  160. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
  161. package/packages/bundled-features/src/secrets/changes.json +6 -0
  162. package/packages/bundled-features/src/secrets/feature.ts +4 -3
  163. package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
  164. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  165. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  166. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  167. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  168. package/packages/bundled-features/src/shared/changes.json +6 -0
  169. package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
  170. package/packages/bundled-features/src/shared/index.ts +6 -0
  171. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  172. package/packages/bundled-features/src/shared/mail-template-body.ts +42 -0
  173. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  174. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  175. package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
  176. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
  177. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
  178. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  179. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  180. package/packages/bundled-features/src/template-resolver/README.md +66 -8
  181. package/packages/bundled-features/src/template-resolver/api.ts +85 -9
  182. package/packages/bundled-features/src/template-resolver/changes.json +186 -0
  183. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +278 -0
  184. package/packages/bundled-features/src/template-resolver/constants.ts +49 -11
  185. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  186. package/packages/bundled-features/src/template-resolver/document-schema.ts +345 -0
  187. package/packages/bundled-features/src/template-resolver/feature.ts +59 -6
  188. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  189. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  190. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +160 -0
  191. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +33 -0
  192. package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
  193. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +138 -0
  194. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -6
  195. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  196. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  197. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +151 -0
  198. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +19 -18
  199. package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
  200. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +323 -0
  201. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  202. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  203. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  204. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  205. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  206. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  207. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +135 -0
  208. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  209. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  210. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +196 -0
  211. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +233 -0
  212. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +137 -0
  213. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +91 -17
  214. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  215. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +16 -77
  216. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +31 -0
  217. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  218. package/packages/bundled-features/src/template-resolver/index.ts +111 -0
  219. package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
  220. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  221. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  222. package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
  223. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  224. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  225. package/packages/bundled-features/src/template-resolver/qualified-names.ts +34 -0
  226. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  227. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  228. package/packages/bundled-features/src/template-resolver/seeding.ts +171 -20
  229. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  230. package/packages/bundled-features/src/template-resolver/table.ts +27 -1
  231. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  232. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  233. package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
  234. package/packages/bundled-features/src/template-resolver/template-meta.ts +192 -0
  235. package/packages/bundled-features/src/template-resolver/template-preview.ts +676 -0
  236. package/packages/bundled-features/src/template-resolver/template-resources.ts +114 -0
  237. package/packages/bundled-features/src/template-resolver/template-usage.ts +68 -0
  238. package/packages/bundled-features/src/template-resolver/template-validation.ts +230 -0
  239. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  240. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +20 -13
  241. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +215 -0
  242. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +37 -0
  243. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1065 -0
  244. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +72 -0
  245. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  246. package/packages/bundled-features/src/template-resolver/web/i18n.ts +121 -0
  247. package/packages/bundled-features/src/template-resolver/web/index.ts +26 -0
  248. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  249. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +176 -0
  250. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  251. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  252. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  253. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  254. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  255. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  256. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  257. package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
  258. package/packages/bundled-features/src/user/changes.json +6 -0
  259. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  260. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  261. package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
  262. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  263. package/packages/bundled-features/src/user/seeding.ts +19 -5
  264. package/packages/bundled-features/src/user-data-rights/changes.json +13 -0
  265. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  266. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  267. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  268. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  269. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  270. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  271. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  272. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  273. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  274. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  275. package/packages/bundled-features/src/user-profile/changes.json +1 -1
  276. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  277. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  278. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  279. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  280. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  281. package/packages/bundled-features/src/waitlist/options.ts +1 -1
  282. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  283. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +3 -3
  284. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
  285. package/samples/apps/ui-walkthrough/package.json +11 -0
  286. package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
  287. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  288. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  289. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +239 -0
  290. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  291. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  292. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +570 -0
  293. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +72 -0
  294. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  295. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  296. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  297. package/samples/apps/ui-walkthrough/src/run-config.ts +41 -1
  298. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  299. package/samples/apps/use-all-bundled/feature-manifest.json +118 -21
  300. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  301. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  302. package/samples/apps/use-all-bundled/package.json +2 -0
  303. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  304. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  305. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  306. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  307. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  308. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  309. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  310. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  311. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  312. package/samples/recipes/delivery-notifications/README.md +26 -25
  313. package/samples/recipes/delivery-notifications/package.json +2 -1
  314. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  315. package/samples/recipes/seed-migration/README.md +1 -1
  316. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  317. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  318. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  319. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  320. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  321. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -0,0 +1,416 @@
1
+ import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import type {
3
+ ConfigAccessor,
4
+ HandlerContext,
5
+ Registry,
6
+ SessionUser,
7
+ TenantId,
8
+ WriteResult,
9
+ } from "@cosmicdrift/kumiko-framework/engine";
10
+ import {
11
+ NotFoundError,
12
+ UnprocessableError,
13
+ writeFailure,
14
+ } from "@cosmicdrift/kumiko-framework/errors";
15
+ import { createFileRefFromBytes } from "@cosmicdrift/kumiko-framework/files";
16
+ import { fileContextForTenant } from "../file-foundation/index.js";
17
+ import type { FileAttachmentRef } from "../renderer-foundation/index.js";
18
+ import { requireForTenant } from "../shared/index.js";
19
+ import { SYSTEM_TENANT_ID } from "./constants.js";
20
+ import {
21
+ DEFAULT_RESOURCE_MAX_BYTES,
22
+ MAX_FILE_ATTACHMENTS_PER_TEMPLATE,
23
+ RESOURCE_MAX_BYTES_CONFIG_KEY,
24
+ } from "./handlers/resource-shared.js";
25
+ import { executor } from "./handlers/shared.js";
26
+ import {
27
+ attachmentEntriesOf,
28
+ type LinkedResourceEntry,
29
+ parseLinkedResourceEntries,
30
+ templatePdfAttachmentLinkSchema,
31
+ } from "./linked-resources.js";
32
+ import { TemplateResolverErrors } from "./qualified-names.js";
33
+ import { normalizeResourceImage } from "./resource-image.js";
34
+ import { type TemplateResourceRow, templateResourcesTable } from "./table.js";
35
+
36
+ const EXTENSION_BY_CONTENT_TYPE: Readonly<Record<string, string>> = {
37
+ "image/png": "png",
38
+ "image/jpeg": "jpg",
39
+ "image/gif": "gif",
40
+ "image/webp": "webp",
41
+ };
42
+
43
+ export async function readTemplateResourceMaxBytes(
44
+ config: ConfigAccessor | undefined,
45
+ registry: Registry,
46
+ ): Promise<number> {
47
+ if (config === undefined || registry.getConfigKey(RESOURCE_MAX_BYTES_CONFIG_KEY) === undefined) {
48
+ return DEFAULT_RESOURCE_MAX_BYTES;
49
+ }
50
+ const value = await config(RESOURCE_MAX_BYTES_CONFIG_KEY);
51
+ return typeof value === "number" ? value : DEFAULT_RESOURCE_MAX_BYTES;
52
+ }
53
+
54
+ export type StoredTemplateResource = {
55
+ readonly fileRefId: string;
56
+ readonly key?: string;
57
+ readonly contentType: string;
58
+ readonly width: number;
59
+ readonly height: number;
60
+ readonly size: number;
61
+ };
62
+
63
+ // Normalizes the image, stores it as a file ref of the target tenant and, with templateId + key,
64
+ // links it into that tenant's template. The caller is responsible for authorization; the function trusts tenantId.
65
+ export async function storeTemplateResource(args: {
66
+ readonly ctx: HandlerContext;
67
+ readonly user: SessionUser;
68
+ readonly tenantId: TenantId;
69
+ readonly bytes: Uint8Array;
70
+ readonly maxBytes: number;
71
+ readonly templateId?: string;
72
+ readonly key?: string;
73
+ }): Promise<WriteResult<StoredTemplateResource>> {
74
+ const { ctx, user, tenantId, bytes, maxBytes, templateId, key } = args;
75
+ if (bytes.length > maxBytes) {
76
+ return writeFailure(
77
+ new UnprocessableError(TemplateResolverErrors.resourceTooLarge, {
78
+ i18nKey: "templateResolver.errors.resource.tooLarge",
79
+ details: { maxBytes, size: bytes.length },
80
+ }),
81
+ );
82
+ }
83
+ let image: Awaited<ReturnType<typeof normalizeResourceImage>>;
84
+ try {
85
+ image = await normalizeResourceImage(Buffer.from(bytes));
86
+ } catch (cause) {
87
+ return writeFailure(
88
+ new UnprocessableError(TemplateResolverErrors.resourceUnsupportedImage, {
89
+ i18nKey: "templateResolver.errors.resource.unsupportedImage",
90
+ cause: cause instanceof Error ? cause : undefined,
91
+ }),
92
+ );
93
+ }
94
+
95
+ const { db, streamTenantId } = requireForTenant(ctx, tenantId);
96
+ let row: TemplateResourceRow | undefined;
97
+ if (templateId !== undefined) {
98
+ row = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
99
+ tenantId,
100
+ id: templateId,
101
+ });
102
+ if (row === undefined) return writeFailure(new NotFoundError("template-resource", templateId));
103
+ }
104
+
105
+ const created = await createFileRefFromBytes(
106
+ {
107
+ db,
108
+ files: fileContextForTenant(ctx, ctx.user.tenantId, tenantId),
109
+ registry: ctx.registry,
110
+ user: { ...user, tenantId },
111
+ },
112
+ {
113
+ // Neutral name: the source's file name may be personal data.
114
+ fileName: `${key ?? "resource"}.${EXTENSION_BY_CONTENT_TYPE[image.contentType] ?? "bin"}`,
115
+ mimeType: image.contentType,
116
+ data: image.data,
117
+ },
118
+ );
119
+ if (!created.isSuccess) return created;
120
+
121
+ if (row !== undefined && key !== undefined) {
122
+ // The replaced FileRef stays: copy-locale and tenant overrides can share ids and
123
+ // there is no reference count to tell when the last user is gone.
124
+ const linkedResources = {
125
+ ...parseLinkedResourceEntries(row.linkedResources),
126
+ [key]: created.data.id,
127
+ };
128
+ const updated = await executor.update(
129
+ {
130
+ id: row.id,
131
+ version: row.version,
132
+ changes: { linkedResources: JSON.stringify(linkedResources) },
133
+ },
134
+ user,
135
+ db,
136
+ { streamTenantId },
137
+ );
138
+ if (!updated.isSuccess) return updated;
139
+ }
140
+
141
+ return {
142
+ isSuccess: true as const,
143
+ data: {
144
+ fileRefId: created.data.id,
145
+ ...(key !== undefined && { key }),
146
+ contentType: image.contentType,
147
+ width: image.width,
148
+ height: image.height,
149
+ size: image.data.length,
150
+ },
151
+ };
152
+ }
153
+
154
+ const PDF_MAGIC = [0x25, 0x50, 0x44, 0x46, 0x2d] as const;
155
+ const PNG_MAGIC = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] as const;
156
+ const JPEG_MAGIC = [0xff, 0xd8, 0xff] as const;
157
+
158
+ const ATTACHMENT_TYPES = [
159
+ { contentType: "application/pdf", extension: "pdf", magic: PDF_MAGIC },
160
+ { contentType: "image/png", extension: "png", magic: PNG_MAGIC },
161
+ { contentType: "image/jpeg", extension: "jpg", magic: JPEG_MAGIC },
162
+ ] as const;
163
+
164
+ function detectAttachmentType(bytes: Uint8Array) {
165
+ return ATTACHMENT_TYPES.find(({ magic }) => magic.every((byte, index) => bytes[index] === byte));
166
+ }
167
+
168
+ export type StoredTemplateAttachment = {
169
+ readonly fileRefId: string;
170
+ readonly key: string;
171
+ readonly fileName: string;
172
+ readonly contentType: string;
173
+ readonly size: number;
174
+ };
175
+
176
+ type AttachmentType = NonNullable<ReturnType<typeof detectAttachmentType>>;
177
+
178
+ export function attachmentNeedsMailTemplate(): WriteResult<never> {
179
+ return writeFailure(
180
+ new UnprocessableError(TemplateResolverErrors.attachmentInvalid, {
181
+ i18nKey: "templateResolver.errors.attachment.invalid",
182
+ details: { code: "mail_only" },
183
+ }),
184
+ );
185
+ }
186
+
187
+ export function attachmentLimitFailure(): WriteResult<never> {
188
+ return writeFailure(
189
+ new UnprocessableError(TemplateResolverErrors.attachmentLimit, {
190
+ i18nKey: "templateResolver.errors.attachment.limit",
191
+ i18nParams: { max: MAX_FILE_ATTACHMENTS_PER_TEMPLATE },
192
+ details: { max: MAX_FILE_ATTACHMENTS_PER_TEMPLATE },
193
+ }),
194
+ );
195
+ }
196
+
197
+ // Files and rendered PDFs share one cap; the entry being replaced under `key` does not count.
198
+ export function otherAttachmentCount(
199
+ linked: Readonly<Record<string, LinkedResourceEntry>>,
200
+ key: string,
201
+ ): number {
202
+ return attachmentEntriesOf(linked).filter(([linkedKey]) => linkedKey !== key).length;
203
+ }
204
+
205
+ function inspectAttachmentBytes(
206
+ bytes: Uint8Array,
207
+ maxBytes: number,
208
+ ): { readonly failure: WriteResult<never> } | { readonly type: AttachmentType } {
209
+ if (bytes.length > maxBytes) {
210
+ return {
211
+ failure: writeFailure(
212
+ new UnprocessableError(TemplateResolverErrors.resourceTooLarge, {
213
+ details: { maxBytes, size: bytes.length },
214
+ }),
215
+ ),
216
+ };
217
+ }
218
+ const type = detectAttachmentType(bytes);
219
+ if (type === undefined) {
220
+ return {
221
+ failure: writeFailure(
222
+ new UnprocessableError(TemplateResolverErrors.attachmentUnsupportedType, {
223
+ i18nKey: "templateResolver.errors.attachment.unsupportedType",
224
+ }),
225
+ ),
226
+ };
227
+ }
228
+ return { type };
229
+ }
230
+
231
+ // Stores the bytes unchanged as a file of the target tenant, without linking it anywhere.
232
+ async function createAttachmentFileRef(args: {
233
+ readonly ctx: HandlerContext;
234
+ readonly user: SessionUser;
235
+ readonly tenantId: TenantId;
236
+ readonly bytes: Uint8Array;
237
+ readonly key: string;
238
+ readonly type: AttachmentType;
239
+ }): Promise<WriteResult<{ readonly id: string }>> {
240
+ const { ctx, user, tenantId, bytes, key, type } = args;
241
+ const { db } = requireForTenant(ctx, tenantId);
242
+ const created = await createFileRefFromBytes(
243
+ {
244
+ db,
245
+ files: fileContextForTenant(ctx, ctx.user.tenantId, tenantId),
246
+ registry: ctx.registry,
247
+ user: { ...user, tenantId },
248
+ },
249
+ {
250
+ // Neutral stored name: the admin's file name may be personal data and lives in the link entry.
251
+ fileName: `${key}.${type.extension}`,
252
+ mimeType: type.contentType,
253
+ data: bytes,
254
+ },
255
+ );
256
+ return created.isSuccess ? { isSuccess: true, data: { id: created.data.id } } : created;
257
+ }
258
+
259
+ // Stores a PDF/PNG/JPEG for a bundle import: validated like an upload, linked later by the importer.
260
+ export async function storeBundleAttachment(args: {
261
+ readonly ctx: HandlerContext;
262
+ readonly user: SessionUser;
263
+ readonly tenantId: TenantId;
264
+ readonly bytes: Uint8Array;
265
+ readonly maxBytes: number;
266
+ readonly key: string;
267
+ readonly fileName: string;
268
+ }): Promise<WriteResult<FileAttachmentRef>> {
269
+ const inspected = inspectAttachmentBytes(args.bytes, args.maxBytes);
270
+ if ("failure" in inspected) return inspected.failure;
271
+ const created = await createAttachmentFileRef({ ...args, type: inspected.type });
272
+ if (!created.isSuccess) return created;
273
+ return {
274
+ isSuccess: true,
275
+ data: { kind: "file", fileRefId: created.data.id, fileName: args.fileName },
276
+ };
277
+ }
278
+
279
+ // Stores a PDF/PNG/JPEG byte-for-byte (no re-encoding, the recipient gets what the admin uploaded)
280
+ // and links it under `key` in a mail template of the target tenant. The caller authorizes tenantId.
281
+ export async function storeTemplateAttachment(args: {
282
+ readonly ctx: HandlerContext;
283
+ readonly user: SessionUser;
284
+ readonly tenantId: TenantId;
285
+ readonly bytes: Uint8Array;
286
+ readonly maxBytes: number;
287
+ readonly templateId: string;
288
+ readonly key: string;
289
+ readonly fileName: string;
290
+ }): Promise<WriteResult<StoredTemplateAttachment>> {
291
+ const { ctx, user, tenantId, bytes, maxBytes, templateId, key, fileName } = args;
292
+ const inspected = inspectAttachmentBytes(bytes, maxBytes);
293
+ if ("failure" in inspected) return inspected.failure;
294
+ const { type } = inspected;
295
+
296
+ const { db, streamTenantId } = requireForTenant(ctx, tenantId);
297
+ const row = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
298
+ tenantId,
299
+ id: templateId,
300
+ });
301
+ if (row === undefined) return writeFailure(new NotFoundError("template-resource", templateId));
302
+ if (row.kind !== "mail") return attachmentNeedsMailTemplate();
303
+
304
+ const linked = parseLinkedResourceEntries(row.linkedResources);
305
+ if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
306
+ return attachmentLimitFailure();
307
+ }
308
+
309
+ const created = await createAttachmentFileRef({ ctx, user, tenantId, bytes, key, type });
310
+ if (!created.isSuccess) return created;
311
+
312
+ const entry: LinkedResourceEntry = { kind: "file", fileRefId: created.data.id, fileName };
313
+ const updated = await executor.update(
314
+ {
315
+ id: row.id,
316
+ version: row.version,
317
+ changes: { linkedResources: JSON.stringify({ ...linked, [key]: entry }) },
318
+ },
319
+ user,
320
+ db,
321
+ { streamTenantId },
322
+ );
323
+ if (!updated.isSuccess) return updated;
324
+
325
+ return {
326
+ isSuccess: true as const,
327
+ data: {
328
+ fileRefId: created.data.id,
329
+ key,
330
+ fileName,
331
+ contentType: type.contentType,
332
+ size: bytes.length,
333
+ },
334
+ };
335
+ }
336
+
337
+ export type LinkedTemplatePdfAttachment = {
338
+ readonly key: string;
339
+ readonly templateSlug: string;
340
+ readonly fileName: string;
341
+ };
342
+
343
+ function pdfAttachmentInvalid(code: string): WriteResult<never> {
344
+ return writeFailure(
345
+ new UnprocessableError(TemplateResolverErrors.attachmentInvalid, {
346
+ i18nKey: "templateResolver.errors.attachment.invalid",
347
+ details: { code },
348
+ }),
349
+ );
350
+ }
351
+
352
+ // Links a PDF that is rendered from a document template at send time into a mail template of the
353
+ // target tenant. Nothing is stored besides the link entry. The caller authorizes tenantId.
354
+ export async function linkTemplatePdfAttachment(args: {
355
+ readonly ctx: HandlerContext;
356
+ readonly user: SessionUser;
357
+ readonly tenantId: TenantId;
358
+ readonly templateId: string;
359
+ readonly key: string;
360
+ readonly templateSlug: string;
361
+ readonly fileName: string;
362
+ }): Promise<WriteResult<LinkedTemplatePdfAttachment>> {
363
+ const { ctx, user, tenantId, templateId, key } = args;
364
+ const link = templatePdfAttachmentLinkSchema.safeParse({
365
+ kind: "rendered-pdf",
366
+ templateSlug: args.templateSlug,
367
+ fileName: args.fileName,
368
+ });
369
+ if (!link.success) return pdfAttachmentInvalid("invalid_pdf_link");
370
+
371
+ const { db, streamTenantId } = requireForTenant(ctx, tenantId);
372
+ const row = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
373
+ tenantId,
374
+ id: templateId,
375
+ });
376
+ if (row === undefined) return writeFailure(new NotFoundError("template-resource", templateId));
377
+ if (row.kind !== "mail") return attachmentNeedsMailTemplate();
378
+
379
+ const linked = parseLinkedResourceEntries(row.linkedResources);
380
+ if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
381
+ return attachmentLimitFailure();
382
+ }
383
+
384
+ const visibleTemplates = await selectMany<TemplateResourceRow>(
385
+ db,
386
+ templateResourcesTable,
387
+ { tenantId: [tenantId, SYSTEM_TENANT_ID], slug: link.data.templateSlug, kind: "document-pdf" },
388
+ { limit: 1 },
389
+ );
390
+ if (visibleTemplates.length === 0) {
391
+ return writeFailure(
392
+ new UnprocessableError(TemplateResolverErrors.attachmentPdfTemplateMissing, {
393
+ i18nKey: "templateResolver.errors.attachment.pdfTemplateMissing",
394
+ i18nParams: { slug: link.data.templateSlug },
395
+ details: { templateSlug: link.data.templateSlug },
396
+ }),
397
+ );
398
+ }
399
+
400
+ const updated = await executor.update(
401
+ {
402
+ id: row.id,
403
+ version: row.version,
404
+ changes: { linkedResources: JSON.stringify({ ...linked, [key]: link.data }) },
405
+ },
406
+ user,
407
+ db,
408
+ { streamTenantId },
409
+ );
410
+ if (!updated.isSuccess) return updated;
411
+
412
+ return {
413
+ isSuccess: true as const,
414
+ data: { key, templateSlug: link.data.templateSlug, fileName: link.data.fileName },
415
+ };
416
+ }
@@ -2,9 +2,12 @@ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
2
2
  import {
3
3
  createEntity,
4
4
  createLongTextField,
5
+ createNumberField,
5
6
  createSelectField,
6
7
  createTextField,
8
+ createTimestampField,
7
9
  } from "@cosmicdrift/kumiko-framework/engine";
10
+ import type { EntityEventMigration } from "@cosmicdrift/kumiko-types/fields";
8
11
  import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
9
12
  import {
10
13
  CONTENT_FORMATS,
@@ -13,10 +16,19 @@ import {
13
16
  TEMPLATE_STATUSES,
14
17
  } from "./constants.js";
15
18
 
19
+ // Stored v1 events name the kind "mail-html", which is now "mail".
20
+ const renameMailHtmlKind: EntityEventMigration = {
21
+ fromVersion: 1,
22
+ toVersion: 2,
23
+ transform: (fields) => (fields["kind"] === "mail-html" ? { ...fields, kind: "mail" } : fields),
24
+ };
25
+
16
26
  // TemplateResource — strukturierte Template-Definition mit Tenant-
17
27
  // Override-Hierarchie, Locale-Fallback und Resource-Linking via
18
28
  // file-foundation. Pro (tenantId, slug, kind, locale) genau eine Row;
19
- // scope/status/version differenzieren Lifecycle.
29
+ // scope/status/version differenzieren Lifecycle. Der Entwurf neben einer
30
+ // aktiven Fassung liegt in `draft`; die Hauptfelder bleiben die ausgelieferte
31
+ // Fassung.
20
32
  //
21
33
  // `variableSchema` + `linkedResources` sind JSON-Strings in longText
22
34
  // (Pattern aus compliance-profiles — kein dedizierter jsonbField im
@@ -44,9 +56,17 @@ export const templateResourceEntity = createEntity({
44
56
  contentFormat: createSelectField({ required: true, options: [...CONTENT_FORMATS] }),
45
57
  variableSchema: createLongTextField({ personal: false, reason: "technical_reference" }),
46
58
  linkedResources: createLongTextField({ personal: false, reason: "technical_reference" }),
59
+ // Per-kind JSON (mail: subject, preview text, layout slug, ...), validated
60
+ // against template-meta.ts on write. Authored by TenantAdmin/Operator.
61
+ meta: createLongTextField({ personal: false, reason: "is_business_data" }),
47
62
  scope: createSelectField({ required: true, options: [...TEMPLATE_SCOPES] }),
48
63
  parentTemplateId: createTextField({ personal: false, reason: "technical_reference" }),
49
64
  status: createSelectField({ required: true, options: [...TEMPLATE_STATUSES] }),
65
+ // JSON of the unpublished edit beside the active version (see template-draft.ts).
66
+ draft: createLongTextField({ personal: false, reason: "is_business_data" }),
67
+ // `version` counts every save including drafts; these two describe the served version.
68
+ activeVersion: createNumberField({ integer: true, min: 1 }),
69
+ activeSince: createTimestampField({}),
50
70
  },
51
71
  indexes: [
52
72
  {
@@ -55,6 +75,8 @@ export const templateResourceEntity = createEntity({
55
75
  name: "read_template_resources_unique",
56
76
  },
57
77
  ],
78
+ eventVersion: 2,
79
+ eventMigrations: [renameMailHtmlKind],
58
80
  });
59
81
 
60
82
  export const templateResourcesTable = buildEntityTable("template-resource", templateResourceEntity);
@@ -84,7 +106,11 @@ export type TemplateResourceRow = {
84
106
  readonly contentFormat: string;
85
107
  readonly variableSchema: string | null;
86
108
  readonly linkedResources: string | null;
109
+ readonly meta: string | null;
87
110
  readonly scope: string;
88
111
  readonly parentTemplateId: string | null;
89
112
  readonly status: string;
113
+ readonly draft: string | null;
114
+ readonly activeVersion: number | null;
115
+ readonly activeSince: Temporal.Instant | null;
90
116
  } & EntityAuditColumns;
@@ -0,0 +1,79 @@
1
+ import * as z from "zod";
2
+ import { CONTENT_FORMATS, UPSERT_KINDS } from "./constants.js";
3
+ import { isDocumentResourceKey } from "./document-schema.js";
4
+ import { templatePdfAttachmentLinkSchema } from "./linked-resources.js";
5
+ import { templateSlugSchema } from "./template-meta.js";
6
+
7
+ const MAX_RESOURCES = 20;
8
+ const MAX_RESOURCE_BYTES = 2 * 1024 * 1024;
9
+ const MAX_BASE64_LENGTH = Math.ceil(MAX_RESOURCE_BYTES / 3) * 4;
10
+
11
+ const STRICT_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
12
+ const CONTENT_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i;
13
+ const UNSAFE_FILE_NAME_CHARACTERS = /[/\\";]|\p{Cc}/u;
14
+
15
+ const bundleLocaleSchema = z
16
+ .string()
17
+ .min(2)
18
+ .max(8)
19
+ .regex(/^[a-z]{2}(-[a-z]{2})?$/i, "locale must be ISO 639-1 (e.g. de, en, en-us)");
20
+
21
+ const bundleFileNameSchema = z
22
+ .string()
23
+ .min(1)
24
+ .max(200)
25
+ .refine((name) => !UNSAFE_FILE_NAME_CHARACTERS.test(name) && !name.includes(".."), {
26
+ message: 'file name must not contain / \\ " ; control characters or ".."',
27
+ });
28
+
29
+ const bundleResourceSchema = z
30
+ .object({
31
+ fileName: bundleFileNameSchema,
32
+ contentType: z.string().max(100).regex(CONTENT_TYPE, "content type must be type/subtype"),
33
+ base64: z.string().max(MAX_BASE64_LENGTH).regex(STRICT_BASE64, "base64 is not valid"),
34
+ // Absent means an image the body embeds; "attachment" is a file attached to the mail.
35
+ purpose: z.enum(["attachment"]).optional(),
36
+ })
37
+ .strict();
38
+
39
+ const pdfAttachmentLinkBodySchema = templatePdfAttachmentLinkSchema.omit({ kind: true });
40
+
41
+ const bundleLocaleEntrySchema = z
42
+ .object({ body: z.string().max(200_000), meta: z.unknown().optional() })
43
+ .strict();
44
+
45
+ export const templateBundleSchema = z
46
+ .object({
47
+ slug: templateSlugSchema,
48
+ kind: z.enum(UPSERT_KINDS),
49
+ contentFormat: z.enum(CONTENT_FORMATS),
50
+ variableSchema: z.record(z.string(), z.unknown()).optional(),
51
+ locales: z
52
+ .record(bundleLocaleSchema, bundleLocaleEntrySchema)
53
+ .refine((locales) => Object.keys(locales).length >= 1, {
54
+ message: "a bundle needs at least one locale",
55
+ }),
56
+ resources: z
57
+ .record(
58
+ z.string().refine(isDocumentResourceKey, { message: "not a valid resource key" }),
59
+ bundleResourceSchema,
60
+ )
61
+ .refine((resources) => Object.keys(resources).length <= MAX_RESOURCES, {
62
+ message: `a bundle holds at most ${MAX_RESOURCES} resources`,
63
+ })
64
+ .default({}),
65
+ // PDFs rendered from document templates at send time; nothing to store, so no bytes.
66
+ pdfAttachments: z
67
+ .record(
68
+ z.string().refine(isDocumentResourceKey, { message: "not a valid resource key" }),
69
+ pdfAttachmentLinkBodySchema,
70
+ )
71
+ .default({}),
72
+ })
73
+ .strict()
74
+ .refine(
75
+ (bundle) => Object.keys(bundle.pdfAttachments).every((key) => !(key in bundle.resources)),
76
+ { message: "a key is used by a resource and a PDF attachment", path: ["pdfAttachments"] },
77
+ );
78
+
79
+ export type TemplateBundle = z.infer<typeof templateBundleSchema>;
@@ -0,0 +1,78 @@
1
+ import { withParsedDocument } from "./document-sanitizer.js";
2
+
3
+ export type TemplateDiffChange = "same" | "added" | "removed";
4
+
5
+ export type TemplateDiffBlock = { readonly change: TemplateDiffChange; readonly content: string };
6
+
7
+ // Beyond this the quadratic LCS table is not worth it for a preview.
8
+ const MAX_LCS_CELLS = 1_000_000;
9
+
10
+ function splitHtmlBlocks(content: string): string[] {
11
+ return withParsedDocument(content, (body) => {
12
+ const blocks: string[] = [];
13
+ for (const node of Array.from(body.childNodes)) {
14
+ const markup =
15
+ "outerHTML" in node && typeof node.outerHTML === "string" ? node.outerHTML : "";
16
+ const text = markup === "" ? (node.textContent ?? "").trim() : markup;
17
+ if (text !== "") blocks.push(text);
18
+ }
19
+ return blocks;
20
+ });
21
+ }
22
+
23
+ export function splitTemplateBlocks(content: string, contentFormat: string): string[] {
24
+ if (contentFormat === "html") return splitHtmlBlocks(content);
25
+ return content
26
+ .split(/\r?\n[ \t]*\r?\n/)
27
+ .map((block) => block.trim())
28
+ .filter((block) => block !== "");
29
+ }
30
+
31
+ // "removed" = only in the system template, "added" = only in the tenant version.
32
+ export function diffTemplateBlocks(
33
+ system: readonly string[],
34
+ tenant: readonly string[],
35
+ ): TemplateDiffBlock[] {
36
+ if (system.length * tenant.length > MAX_LCS_CELLS) {
37
+ return [
38
+ ...system.map((content) => ({ change: "removed" as const, content })),
39
+ ...tenant.map((content) => ({ change: "added" as const, content })),
40
+ ];
41
+ }
42
+ const rows = system.length;
43
+ const cols = tenant.length;
44
+ const width = cols + 1;
45
+ // lengths[i * width + j] = LCS length of system[i..] and tenant[j..]
46
+ const lengths = new Uint32Array((rows + 1) * width);
47
+ const lengthAt = (i: number, j: number): number => lengths[i * width + j] ?? 0;
48
+ for (let i = rows - 1; i >= 0; i--) {
49
+ for (let j = cols - 1; j >= 0; j--) {
50
+ lengths[i * width + j] =
51
+ system[i] === tenant[j]
52
+ ? lengthAt(i + 1, j + 1) + 1
53
+ : Math.max(lengthAt(i + 1, j), lengthAt(i, j + 1));
54
+ }
55
+ }
56
+ const result: TemplateDiffBlock[] = [];
57
+ let i = 0;
58
+ let j = 0;
59
+ while (i < rows || j < cols) {
60
+ const systemBlock = system[i];
61
+ const tenantBlock = tenant[j];
62
+ if (systemBlock !== undefined && systemBlock === tenantBlock) {
63
+ result.push({ change: "same", content: systemBlock });
64
+ i++;
65
+ j++;
66
+ } else if (
67
+ systemBlock !== undefined &&
68
+ (tenantBlock === undefined || lengthAt(i + 1, j) >= lengthAt(i, j + 1))
69
+ ) {
70
+ result.push({ change: "removed", content: systemBlock });
71
+ i++;
72
+ } else if (tenantBlock !== undefined) {
73
+ result.push({ change: "added", content: tenantBlock });
74
+ j++;
75
+ }
76
+ }
77
+ return result;
78
+ }