@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,102 @@
1
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
2
+ import * as z from "zod";
3
+ import type { FileAttachmentRef } from "../renderer-foundation/index.js";
4
+ import { pdfAttachmentFileNamePatternSchema, templateSlugSchema } from "./template-meta.js";
5
+
6
+ export const templatePdfAttachmentLinkSchema = z
7
+ .object({
8
+ kind: z.literal("rendered-pdf"),
9
+ templateSlug: templateSlugSchema,
10
+ fileName: pdfAttachmentFileNamePatternSchema,
11
+ })
12
+ .strict();
13
+
14
+ // A PDF the mail renderer asks for at send time: the document template is rendered with the
15
+ // mail's variables, so nothing is stored. `fileName` is a pattern that may hold {{placeholders}}.
16
+ export type TemplatePdfAttachmentLink = z.infer<typeof templatePdfAttachmentLinkSchema>;
17
+
18
+ // A string is an image the body embeds by key (the fileRef id); an object is a file attached to
19
+ // the mail as-is or a PDF rendered from a document template. Images are the only thing a document
20
+ // body can reference.
21
+ export type LinkedResourceEntry = string | FileAttachmentRef | TemplatePdfAttachmentLink;
22
+
23
+ export function isFileAttachmentEntry(entry: LinkedResourceEntry): entry is FileAttachmentRef {
24
+ return typeof entry !== "string" && entry.kind === "file";
25
+ }
26
+
27
+ export function isPdfAttachmentLink(
28
+ entry: LinkedResourceEntry,
29
+ ): entry is TemplatePdfAttachmentLink {
30
+ return typeof entry !== "string" && entry.kind === "rendered-pdf";
31
+ }
32
+
33
+ function toLinkedResourceEntry(raw: unknown): LinkedResourceEntry | null {
34
+ if (typeof raw === "string") return raw;
35
+ if (!isPlainObject(raw)) return null;
36
+ if (raw["kind"] === "rendered-pdf") {
37
+ const link = templatePdfAttachmentLinkSchema.safeParse(raw);
38
+ return link.success ? link.data : null;
39
+ }
40
+ const { fileRefId, fileName } = raw;
41
+ if (raw["kind"] !== "file" || typeof fileRefId !== "string" || typeof fileName !== "string") {
42
+ return null;
43
+ }
44
+ return { kind: "file", fileRefId, fileName };
45
+ }
46
+
47
+ export function parseLinkedResourceEntries(
48
+ raw: string | null,
49
+ ): Record<string, LinkedResourceEntry> {
50
+ if (!raw) return {};
51
+ let parsed: unknown;
52
+ try {
53
+ parsed = JSON.parse(raw);
54
+ } catch {
55
+ return {};
56
+ }
57
+ if (!isPlainObject(parsed)) return {};
58
+ const entries: Record<string, LinkedResourceEntry> = {};
59
+ for (const [key, value] of Object.entries(parsed)) {
60
+ const entry = toLinkedResourceEntry(value);
61
+ if (entry !== null) entries[key] = entry;
62
+ }
63
+ return entries;
64
+ }
65
+
66
+ export function imageLinksOf(
67
+ entries: Readonly<Record<string, LinkedResourceEntry>>,
68
+ ): Record<string, string> {
69
+ const images: Record<string, string> = {};
70
+ for (const [key, entry] of Object.entries(entries)) {
71
+ if (typeof entry === "string") images[key] = entry;
72
+ }
73
+ return images;
74
+ }
75
+
76
+ export function fileAttachmentsOf(
77
+ entries: Readonly<Record<string, LinkedResourceEntry>>,
78
+ ): FileAttachmentRef[] {
79
+ return Object.entries(entries)
80
+ .sort(([a], [b]) => a.localeCompare(b))
81
+ .flatMap(([, entry]) => (isFileAttachmentEntry(entry) ? [entry] : []));
82
+ }
83
+
84
+ export function pdfAttachmentLinksOf(
85
+ entries: Readonly<Record<string, LinkedResourceEntry>>,
86
+ ): TemplatePdfAttachmentLink[] {
87
+ return Object.entries(entries)
88
+ .sort(([a], [b]) => a.localeCompare(b))
89
+ .flatMap(([, entry]) => (isPdfAttachmentLink(entry) ? [entry] : []));
90
+ }
91
+
92
+ export function attachmentEntriesOf(
93
+ entries: Readonly<Record<string, LinkedResourceEntry>>,
94
+ ): [string, FileAttachmentRef | TemplatePdfAttachmentLink][] {
95
+ return Object.entries(entries).flatMap(([key, entry]) =>
96
+ typeof entry === "string" ? [] : [[key, entry]],
97
+ );
98
+ }
99
+
100
+ export function imageResourceKeysOf(raw: string | null): string[] {
101
+ return Object.keys(imageLinksOf(parseLinkedResourceEntries(raw)));
102
+ }
@@ -0,0 +1,12 @@
1
+ // @runtime client
2
+ // No imports: web/ re-exports these, and constants.ts pulls in the framework engine.
3
+ export const PDF_PAGE_FORMATS = ["A4", "Letter"] as const;
4
+ export type PdfPageFormat = (typeof PDF_PAGE_FORMATS)[number];
5
+
6
+ export const PDF_ORIENTATIONS = ["portrait", "landscape"] as const;
7
+ export type PdfOrientation = (typeof PDF_ORIENTATIONS)[number];
8
+
9
+ // Page margins in mm; the header/footer values apply when the layout contributes that region and the template sets no margin itself.
10
+ export const PDF_DEFAULT_MARGIN_MM = 15;
11
+ export const PDF_HEADER_MARGIN_MM = 30;
12
+ export const PDF_FOOTER_MARGIN_MM = 20;
@@ -0,0 +1,36 @@
1
+ import type { InlineAttachmentRef } from "../renderer-foundation/index.js";
2
+
3
+ export const PREVIEW_IMAGE_BUDGET_BYTES = 5 * 1024 * 1024;
4
+
5
+ const DISPLAYABLE_IMAGE_TYPES: ReadonlySet<string> = new Set([
6
+ "image/png",
7
+ "image/jpeg",
8
+ "image/gif",
9
+ "image/webp",
10
+ ]);
11
+
12
+ export type PreviewAttachmentReader = (
13
+ ref: InlineAttachmentRef,
14
+ ) => Promise<{ readonly content: Uint8Array; readonly contentType: string } | null>;
15
+
16
+ // The preview shows images the mail client would resolve by Content-ID; a browser cannot, so each
17
+ // resolved image becomes a data URI. `html` itself stays untouched because the send path needs the cids.
18
+ export async function buildDisplayHtml(
19
+ html: string,
20
+ inline: readonly InlineAttachmentRef[],
21
+ readAttachment: PreviewAttachmentReader,
22
+ ): Promise<string> {
23
+ let displayHtml = html;
24
+ let usedBytes = 0;
25
+ for (const ref of inline) {
26
+ const needle = `src="cid:${ref.cid}"`;
27
+ if (!displayHtml.includes(needle)) continue;
28
+ const found = await readAttachment(ref);
29
+ if (found === null || !DISPLAYABLE_IMAGE_TYPES.has(found.contentType)) continue;
30
+ if (usedBytes + found.content.length > PREVIEW_IMAGE_BUDGET_BYTES) continue;
31
+ usedBytes += found.content.length;
32
+ const dataUri = `src="data:${found.contentType};base64,${Buffer.from(found.content).toString("base64")}"`;
33
+ displayHtml = displayHtml.replaceAll(needle, () => dataUri);
34
+ }
35
+ return displayHtml;
36
+ }
@@ -0,0 +1,58 @@
1
+ // Server-only (happy-dom). Prepares a rendered document body for the browser
2
+ // that prints it: repeating table headers become real <thead> rows, and
3
+ // everything that would make the browser fetch something external is removed.
4
+
5
+ import type { Element as ScanElement } from "happy-dom";
6
+ import { withParsedDocument } from "./document-sanitizer.js";
7
+
8
+ const REPEAT_HEADER_ATTRIBUTE = "data-repeat-header";
9
+ const FETCHING_ELEMENTS =
10
+ "script, link, iframe, object, embed, video, audio, source, meta, base, style";
11
+ const FETCHING_ATTRIBUTES = ["src", "srcset", "poster", "background", "data"] as const;
12
+ // Anchors only navigate; on svg image/use/feImage an href fetches.
13
+ const HREF_ATTRIBUTES = ["href", "xlink:href"] as const;
14
+ const EXTERNAL_STYLE_REFERENCE = /url\(|@import/i;
15
+
16
+ function isEmbeddedValue(value: string): boolean {
17
+ return value.trim().toLowerCase().startsWith("data:");
18
+ }
19
+
20
+ function moveRepeatHeadersIntoThead(table: ScanElement): void {
21
+ const rows = Array.from(table.querySelectorAll("tr")).filter(
22
+ (row) => row.closest("table") === table,
23
+ );
24
+ const leading: ScanElement[] = [];
25
+ for (const row of rows) {
26
+ if (!row.hasAttribute(REPEAT_HEADER_ATTRIBUTE)) break;
27
+ leading.push(row);
28
+ }
29
+ // skip: a table without leading repeat-header rows keeps its structure as written
30
+ if (leading.length === 0) return;
31
+ const thead = table.ownerDocument.createElement("thead");
32
+ for (const row of leading) {
33
+ row.removeAttribute(REPEAT_HEADER_ATTRIBUTE);
34
+ thead.appendChild(row);
35
+ }
36
+ table.insertBefore(thead, table.firstChild);
37
+ }
38
+
39
+ export function prepareDocumentForPrint(html: string): string {
40
+ return withParsedDocument(html, (body) => {
41
+ for (const table of Array.from(body.querySelectorAll("table"))) {
42
+ moveRepeatHeadersIntoThead(table);
43
+ }
44
+ for (const element of Array.from(body.querySelectorAll(FETCHING_ELEMENTS))) element.remove();
45
+ for (const element of Array.from(body.querySelectorAll("*"))) {
46
+ for (const name of FETCHING_ATTRIBUTES) {
47
+ const value = element.getAttribute(name);
48
+ if (value !== null && !isEmbeddedValue(value)) element.removeAttribute(name);
49
+ }
50
+ if (element.tagName.toLowerCase() !== "a") {
51
+ for (const name of HREF_ATTRIBUTES) element.removeAttribute(name);
52
+ }
53
+ const style = element.getAttribute("style");
54
+ if (style !== null && EXTERNAL_STYLE_REFERENCE.test(style)) element.removeAttribute("style");
55
+ }
56
+ return body.innerHTML;
57
+ });
58
+ }
@@ -7,12 +7,32 @@ export const TemplateResolverHandlers = {
7
7
  upsertTenant: "template-resolver:write:upsert-tenant",
8
8
  publish: "template-resolver:write:publish",
9
9
  archive: "template-resolver:write:archive",
10
+ publishSystem: "template-resolver:write:publish-system",
11
+ archiveSystem: "template-resolver:write:archive-system",
12
+ discardDraft: "template-resolver:write:discard-draft",
13
+ discardDraftSystem: "template-resolver:write:discard-draft-system",
14
+ resetToSystem: "template-resolver:write:reset-to-system",
10
15
  set: "template-resolver:write:set",
16
+ importBundle: "template-resolver:write:import-bundle",
17
+ importBundleSystem: "template-resolver:write:import-bundle-system",
18
+ copyLocale: "template-resolver:write:copy-locale",
19
+ resourceUpload: "template-resolver:write:resource-upload",
20
+ resourceRemove: "template-resolver:write:resource-remove",
21
+ resourceLinkPdf: "template-resolver:write:resource-link-pdf",
22
+ sendTest: "template-resolver:write:send-test",
11
23
  } as const;
12
24
 
13
25
  export const TemplateResolverQueries = {
14
26
  findById: "template-resolver:query:find-by-id",
15
27
  list: "template-resolver:query:list",
28
+ usage: "template-resolver:query:usage",
29
+ overview: "template-resolver:query:overview",
30
+ layoutSource: "template-resolver:query:layout-source",
31
+ tenantOverrides: "template-resolver:query:tenant-overrides",
32
+ diffToSystem: "template-resolver:query:diff-to-system",
33
+ validate: "template-resolver:query:validate",
34
+ preview: "template-resolver:query:preview",
35
+ exportBundle: "template-resolver:query:export-bundle",
16
36
  // Public pair — anonymous-reachable, kind pinned to text-block.
17
37
  bySlug: "template-resolver:query:by-slug",
18
38
  byTenant: "template-resolver:query:by-tenant",
@@ -35,6 +55,20 @@ export const TemplateResolverErrors = {
35
55
  invalidSlug: "invalid_slug",
36
56
  invalidLocale: "invalid_locale",
37
57
  systemAdminRequired: "system_admin_required",
58
+ noDraft: "no_draft",
38
59
  alreadyActive: "template_already_active",
39
60
  alreadyArchived: "template_already_archived",
61
+ targetExists: "target_exists",
62
+ systemResourcesNeedSystemTenant: "system_resources_need_system_tenant",
63
+ bundleLocalesDiffer: "bundle_locales_differ",
64
+ resourceTooLarge: "template_resource_too_large",
65
+ resourceUnsupportedImage: "template_resource_unsupported_image",
66
+ attachmentUnsupportedType: "template_attachment_unsupported_type",
67
+ attachmentLimit: "template_attachment_limit",
68
+ attachmentInvalid: "template_attachment_invalid",
69
+ attachmentPdfTemplateMissing: "template_attachment_pdf_template_missing",
70
+ previewRenderFailed: "template_preview_render_failed",
71
+ testMailNoChannel: "template_test_mail_no_channel",
72
+ testMailNoAddress: "template_test_mail_no_address",
73
+ testMailFailed: "template_test_mail_failed",
40
74
  } as const;
@@ -0,0 +1,26 @@
1
+ import type { NotificationRecordMapping } from "@cosmicdrift/kumiko-framework/engine";
2
+
3
+ function readPath(row: Readonly<Record<string, unknown>>, path: string): unknown {
4
+ let current: unknown = row;
5
+ for (const segment of path.split(".")) {
6
+ if (typeof current !== "object" || current === null || !Object.hasOwn(current, segment)) {
7
+ return undefined;
8
+ }
9
+ current = Object.getOwnPropertyDescriptor(current, segment)?.value;
10
+ }
11
+ return current;
12
+ }
13
+
14
+ // Variables whose path is missing on the row are left out, so the template
15
+ // renders them empty instead of printing "undefined".
16
+ export function mapRecordToVariables(
17
+ mapping: NotificationRecordMapping,
18
+ row: Readonly<Record<string, unknown>>,
19
+ ): Record<string, unknown> {
20
+ const variables: Record<string, unknown> = {};
21
+ for (const [variable, path] of Object.entries(mapping.map)) {
22
+ const value = readPath(row, path);
23
+ if (value !== undefined) variables[variable] = value;
24
+ }
25
+ return variables;
26
+ }
@@ -0,0 +1,47 @@
1
+ import { assertNotSvg, SHARP_INPUT_OPTIONS } from "../shared/index.js";
2
+
3
+ const MAX_RESOURCE_WIDTH = 1200;
4
+
5
+ const CONTENT_TYPE_BY_FORMAT = {
6
+ png: "image/png",
7
+ jpeg: "image/jpeg",
8
+ gif: "image/gif",
9
+ webp: "image/webp",
10
+ } as const;
11
+
12
+ type ResourceImageFormat = keyof typeof CONTENT_TYPE_BY_FORMAT;
13
+
14
+ export type NormalizedResourceImage = {
15
+ readonly data: Uint8Array;
16
+ readonly contentType: (typeof CONTENT_TYPE_BY_FORMAT)[ResourceImageFormat];
17
+ readonly width: number;
18
+ readonly height: number;
19
+ };
20
+
21
+ function isResourceImageFormat(format: string | undefined): format is ResourceImageFormat {
22
+ return format !== undefined && Object.hasOwn(CONTENT_TYPE_BY_FORMAT, format);
23
+ }
24
+
25
+ // Re-encodes an uploaded image: the stored content type comes from the decoded format, never
26
+ // from the client, and re-encoding drops metadata and anything appended to the file.
27
+ // sharp is imported lazily because every app loads template-resolver, few of them need an image decoder.
28
+ export async function normalizeResourceImage(data: Uint8Array): Promise<NormalizedResourceImage> {
29
+ const { default: sharp } = await import("sharp");
30
+ const metadata = await sharp(data, SHARP_INPUT_OPTIONS).metadata();
31
+ assertNotSvg(metadata.format);
32
+ if (!isResourceImageFormat(metadata.format)) {
33
+ throw new Error(`unsupported image format "${metadata.format ?? "unknown"}"`);
34
+ }
35
+ const format = metadata.format;
36
+ const pipeline = sharp(data, SHARP_INPUT_OPTIONS)
37
+ .rotate()
38
+ .resize({ width: MAX_RESOURCE_WIDTH, withoutEnlargement: true })
39
+ .toFormat(format);
40
+ const { data: output, info } = await pipeline.toBuffer({ resolveWithObject: true });
41
+ return {
42
+ data: new Uint8Array(output),
43
+ contentType: CONTENT_TYPE_BY_FORMAT[format],
44
+ width: info.width,
45
+ height: info.height,
46
+ };
47
+ }
@@ -1,13 +1,13 @@
1
- // Boot-/Seed-Helper für App-Authors. Schreibt System-Templates (tenantId=
2
- // SYSTEM_TENANT_ID, scope=system, status=active) idempotent in die DB —
3
- // gleicher Projection-Pfad wie upsertSystem-Handler, ohne Access-Check.
1
+ // Boot/seed helper for app authors. Writes system templates (tenantId=
2
+ // SYSTEM_TENANT_ID, scope=system, status=active) idempotently — same projection
3
+ // path as the upsertSystem handler, without the access check.
4
4
  //
5
- // **Wann nutzen?** runProdApp-seeds / TenantCreated-Hooks die Welcome-,
6
- // Incident- oder Feature-Mail-Slugs installieren, damit renderer-simple
7
- // (oder mail-html) sie per template-resolver auflösen kann.
5
+ // Use it from runProdApp seeds or TenantCreated hooks that install the welcome,
6
+ // incident or feature mail slugs, so the mail renderer can resolve them
7
+ // through the template-resolver.
8
8
  //
9
- // Default ifExists="skip". `createSystemUser(SYSTEM_TENANT_ID)` als Actor —
10
- // bewusst nicht TestUsers (Prod-Seeds ≠ Test-Utilities).
9
+ // Default ifExists="skip". The actor is `createSystemUser(SYSTEM_TENANT_ID)`,
10
+ // deliberately not TestUsers (prod seeds are not test utilities).
11
11
 
12
12
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
13
13
  import {
@@ -19,6 +19,7 @@ import {
19
19
  } from "@cosmicdrift/kumiko-framework/db";
20
20
  import {
21
21
  createSystemUser,
22
+ type Registry,
22
23
  type SessionUser,
23
24
  SYSTEM_TENANT_ID,
24
25
  type TenantId,
@@ -30,7 +31,13 @@ import {
30
31
  import { runEventStoreSeed, type SeedIfExists } from "@cosmicdrift/kumiko-framework/seeding";
31
32
  import { type ContentFormat, TEXT_BLOCK_KIND, type UpsertKind } from "./constants.js";
32
33
  import { executor } from "./handlers/shared.js";
34
+ import { evaluateTemplateWrite } from "./handlers/template-gates.js";
35
+ import { TEMPLATE_RESOLVER_FEATURE } from "./qualified-names.js";
33
36
  import { type TemplateResourceRow, templateResourcesTable } from "./table.js";
37
+ import type { TemplateBundle } from "./template-bundle.js";
38
+ import { activeStampChanges, servedStampChanges } from "./template-draft.js";
39
+ import { templateSlugSchema } from "./template-meta.js";
40
+ import { variablesFromSchema } from "./template-validation.js";
34
41
 
35
42
  /** Drop projection orphans (no events anywhere) so boot seeds can recreate. */
36
43
  async function resolveExistingForEventStoreSeed(
@@ -65,7 +72,10 @@ export type SeedSystemTemplateOptions = {
65
72
  readonly content: string;
66
73
  readonly contentFormat: ContentFormat;
67
74
  readonly variableSchema?: Record<string, unknown>;
75
+ /** Omitted: links on an existing row stay untouched, so a re-seed on boot keeps images a
76
+ * system admin uploaded through resource-upload. */
68
77
  readonly linkedResources?: Record<string, string>;
78
+ readonly meta?: unknown;
69
79
  readonly parentTemplateId?: string | null;
70
80
  readonly by?: SessionUser;
71
81
  readonly ifExists?: SeedIfExists;
@@ -75,10 +85,29 @@ export async function seedSystemTemplate(
75
85
  db: DbConnection,
76
86
  opts: SeedSystemTemplateOptions,
77
87
  ): Promise<{ id: string }> {
78
- const tenantId = SYSTEM_TENANT_ID as TenantId;
88
+ const tenantId = SYSTEM_TENANT_ID;
79
89
  const by = opts.by ?? createSystemUser(tenantId);
80
90
  const tdb = createTenantDb(db, tenantId, "system");
81
91
 
92
+ const label = `seedSystemTemplate ${opts.slug}/${opts.kind}/${opts.locale}`;
93
+ const slug = templateSlugSchema.safeParse(opts.slug);
94
+ if (!slug.success) throw new Error(`${label} rejected: ${slug.error.issues[0]?.message}`);
95
+ const evaluated = evaluateTemplateWrite({
96
+ kind: opts.kind,
97
+ scope: "system",
98
+ contentFormat: opts.contentFormat,
99
+ content: opts.content,
100
+ rawMeta: opts.meta,
101
+ variables: variablesFromSchema(opts.variableSchema ?? {}),
102
+ resourceKeys: Object.keys(opts.linkedResources ?? {}),
103
+ });
104
+ const issues = evaluated.ok ? evaluated.publishIssues : evaluated.issues;
105
+ if (!evaluated.ok || issues.length > 0) {
106
+ throw new Error(
107
+ `${label} rejected: ${issues.map((issue) => `${issue.code} (${issue.params?.["detail"] ?? issue.path})`).join("; ")}`,
108
+ );
109
+ }
110
+
82
111
  const existingRow = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
83
112
  tenantId,
84
113
  slug: opts.slug,
@@ -93,17 +122,20 @@ export async function seedSystemTemplate(
93
122
  );
94
123
 
95
124
  const variableSchema = JSON.stringify(opts.variableSchema ?? {});
96
- const linkedResources = JSON.stringify(opts.linkedResources ?? {});
125
+ const meta = evaluated.metaJson;
126
+ const linkedResources =
127
+ opts.linkedResources === undefined ? undefined : JSON.stringify(opts.linkedResources);
97
128
  const parentTemplateId = opts.parentTemplateId ?? null;
98
129
 
99
130
  const rowFields = {
100
131
  slug: opts.slug,
101
132
  kind: opts.kind,
102
133
  locale: opts.locale,
103
- content: opts.content,
134
+ content: evaluated.content,
104
135
  contentFormat: opts.contentFormat,
105
136
  variableSchema,
106
- linkedResources,
137
+ ...(linkedResources !== undefined && { linkedResources }),
138
+ meta,
107
139
  title: null,
108
140
  folder: null,
109
141
  scope: "system" as const,
@@ -111,9 +143,28 @@ export async function seedSystemTemplate(
111
143
  status: "active" as const,
112
144
  };
113
145
 
114
- const applyUpdate = async (row: { id: string; version: number }): Promise<{ id: string }> => {
146
+ const applyUpdate = async (
147
+ row: { id: string; version: number },
148
+ current: TemplateResourceRow | null | undefined,
149
+ ): Promise<{ id: string }> => {
150
+ // Skip no-op updates so re-seeding on every pod boot does not bump
151
+ // version/modifiedAt or grow the event store.
152
+ if (
153
+ current?.content === rowFields.content &&
154
+ current?.contentFormat === rowFields.contentFormat &&
155
+ current?.title === rowFields.title &&
156
+ current?.variableSchema === rowFields.variableSchema &&
157
+ current?.meta === rowFields.meta &&
158
+ (linkedResources === undefined || current?.linkedResources === linkedResources)
159
+ ) {
160
+ return { id: String(row.id) };
161
+ }
115
162
  const result = await executor.update(
116
- { id: row.id, version: row.version, changes: rowFields },
163
+ {
164
+ id: row.id,
165
+ version: row.version,
166
+ changes: { ...rowFields, ...activeStampChanges(row) },
167
+ },
117
168
  by,
118
169
  tdb,
119
170
  );
@@ -127,7 +178,17 @@ export async function seedSystemTemplate(
127
178
  existing,
128
179
  ifExists: opts.ifExists,
129
180
  create: async () => {
130
- const result = await executor.create({ ...rowFields, tenantId }, by, tdb);
181
+ const result = await executor.create(
182
+ {
183
+ ...rowFields,
184
+ ...activeStampChanges(null),
185
+ draft: null,
186
+ linkedResources: linkedResources ?? "{}",
187
+ tenantId,
188
+ },
189
+ by,
190
+ tdb,
191
+ );
131
192
  if (!result.isSuccess) {
132
193
  // Parallel boot pods can both delete the orphan and race on create —
133
194
  // unique on (tenantId, slug, kind, locale) → resolve winner and update.
@@ -152,7 +213,7 @@ export async function seedSystemTemplate(
152
213
  existing: resolved,
153
214
  ifExists: opts.ifExists,
154
215
  create: () => Promise.reject(new Error("seedSystemTemplate: unreachable create")),
155
- update: applyUpdate,
216
+ update: (row) => applyUpdate(row, again),
156
217
  });
157
218
  }
158
219
  throw new Error(`seedSystemTemplate create failed: ${JSON.stringify(result)}`);
@@ -163,10 +224,90 @@ export async function seedSystemTemplate(
163
224
  }
164
225
  return { id: String(data.id) };
165
226
  },
166
- update: applyUpdate,
227
+ update: (row) => applyUpdate(row, existingRow),
167
228
  });
168
229
  }
169
230
 
231
+ export type SeedSystemTemplateBundleOptions = {
232
+ readonly by?: SessionUser;
233
+ readonly ifExists?: SeedIfExists;
234
+ };
235
+
236
+ // Seeds one system row per bundle locale. Bundled resources need a file
237
+ // provider and the system tenant's file space, which a boot seed does not
238
+ // have, so those go through the import-bundle-system write instead.
239
+ export async function seedSystemTemplateBundle(
240
+ db: DbConnection,
241
+ bundle: TemplateBundle,
242
+ opts: SeedSystemTemplateBundleOptions = {},
243
+ ): Promise<{ ids: Record<string, string> }> {
244
+ if (Object.keys(bundle.resources).length > 0) {
245
+ throw new Error(
246
+ `seedSystemTemplateBundle ${bundle.slug}/${bundle.kind}: bundles with resources cannot be seeded; use the import-bundle-system write`,
247
+ );
248
+ }
249
+ const ids: Record<string, string> = {};
250
+ for (const [locale, entry] of Object.entries(bundle.locales)) {
251
+ const seeded = await seedSystemTemplate(db, {
252
+ slug: bundle.slug,
253
+ kind: bundle.kind,
254
+ locale,
255
+ content: entry.body,
256
+ contentFormat: bundle.contentFormat,
257
+ variableSchema: bundle.variableSchema ?? {},
258
+ meta: entry.meta,
259
+ ...(opts.by !== undefined ? { by: opts.by } : {}),
260
+ ...(opts.ifExists !== undefined ? { ifExists: opts.ifExists } : {}),
261
+ });
262
+ ids[locale] = seeded.id;
263
+ }
264
+ return { ids };
265
+ }
266
+
267
+ export function hasNotificationTemplates(registry: Registry): boolean {
268
+ return Array.from(registry.getAllNotifications().values()).some(
269
+ (notification) => notification.template !== undefined,
270
+ );
271
+ }
272
+
273
+ // Boot seed for notifications that ship their mail text with the code
274
+ // (`r.notification({ template, meta, variables })`). The code is authoritative,
275
+ // so existing system rows are updated; tenant overrides are untouched.
276
+ export async function seedNotificationTemplates(
277
+ db: DbConnection,
278
+ registry: Registry,
279
+ ): Promise<void> {
280
+ const hasTemplates = hasNotificationTemplates(registry);
281
+ if (hasTemplates && registry.getFeature(TEMPLATE_RESOLVER_FEATURE) === undefined) {
282
+ throw new Error(
283
+ `Notifications declare a mail template but the "${TEMPLATE_RESOLVER_FEATURE}" feature is not mounted`,
284
+ );
285
+ }
286
+ if (hasTemplates) {
287
+ // Dynamic: renderer-foundation imports this feature's barrel, a static import would cycle.
288
+ const { collectRendererPlugins } = await import("../renderer-foundation/index.js");
289
+ if (!collectRendererPlugins(registry).some((plugin) => plugin.kinds.includes("mail"))) {
290
+ throw new Error(
291
+ 'Notifications declare a mail template but no renderer plugin for kind "mail" is registered (mount renderer-foundation and @cosmicdrift/kumiko-renderer-mail-html)',
292
+ );
293
+ }
294
+ }
295
+ for (const [slug, notification] of registry.getAllNotifications()) {
296
+ for (const [locale, { body }] of Object.entries(notification.template ?? {})) {
297
+ await seedSystemTemplate(db, {
298
+ slug,
299
+ kind: "mail",
300
+ locale,
301
+ content: body,
302
+ contentFormat: "html",
303
+ meta: notification.meta?.[locale],
304
+ variableSchema: notification.variables,
305
+ ifExists: "update",
306
+ });
307
+ }
308
+ }
309
+ }
310
+
170
311
  export type SeedTextBlockOptions = {
171
312
  readonly tenantId: TenantId;
172
313
  readonly slug: string;
@@ -236,7 +377,13 @@ export async function seedTextBlock(
236
377
  {
237
378
  id: row.id,
238
379
  version: row.version,
239
- changes: { title: fields.title, content: fields.content, folder: fields.folder },
380
+ changes: {
381
+ title: fields.title,
382
+ content: fields.content,
383
+ folder: fields.folder,
384
+ // A re-seed must not revive an archived block; only a served one gets a new served version.
385
+ ...(current?.status === "active" && servedStampChanges(row)),
386
+ },
240
387
  },
241
388
  by,
242
389
  tdb,
@@ -251,7 +398,11 @@ export async function seedTextBlock(
251
398
  existing,
252
399
  ifExists: opts.ifExists,
253
400
  create: async () => {
254
- const result = await executor.create({ ...fields, tenantId: opts.tenantId }, by, tdb);
401
+ const result = await executor.create(
402
+ { ...fields, ...activeStampChanges(null), draft: null, tenantId: opts.tenantId },
403
+ by,
404
+ tdb,
405
+ );
255
406
  if (!result.isSuccess) {
256
407
  if (result.error?.code === "unique_violation") {
257
408
  const again = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
@@ -306,7 +457,7 @@ export async function seedLegalContentFromJson(
306
457
  blocks: readonly LegalContentBlock[],
307
458
  opts: { readonly tenantId?: TenantId; readonly by?: SessionUser } = {},
308
459
  ): Promise<void> {
309
- const tenantId = opts.tenantId ?? (SYSTEM_TENANT_ID as TenantId);
460
+ const tenantId = opts.tenantId ?? SYSTEM_TENANT_ID;
310
461
  const by = opts.by ?? createSystemUser(tenantId);
311
462
  for (const block of blocks) {
312
463
  await seedTextBlock(db, {