@cosmicdrift/kumiko-bundled-features 0.361.0 → 0.363.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 (323) hide show
  1. package/dist/admin-shell/feature.d.ts +3 -0
  2. package/dist/admin-shell/feature.js +6 -1
  3. package/dist/agent-tools/agent-manifest.js +2 -1
  4. package/dist/agent-tools/filter-ops.d.ts +1 -1
  5. package/dist/agent-tools/filter-ops.js +1 -3
  6. package/dist/agent-tools/tool-dispatch.js +2 -1
  7. package/dist/auth-email-password/email-templates.d.ts +4 -26
  8. package/dist/auth-email-password/email-templates.js +9 -101
  9. package/dist/auth-email-password/feature.js +24 -6
  10. package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
  11. package/dist/auth-email-password/handlers/request-account-unlock.write.js +2 -4
  12. package/dist/auth-email-password/handlers/request-email-verification.write.js +2 -4
  13. package/dist/auth-email-password/handlers/request-password-reset.write.js +2 -4
  14. package/dist/auth-email-password/handlers/signup-confirm.write.js +3 -4
  15. package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -2
  16. package/dist/auth-email-password/handlers/token-request-handler.js +0 -1
  17. package/dist/auth-email-password/index.d.ts +2 -2
  18. package/dist/auth-email-password/index.js +1 -1
  19. package/dist/auth-email-password/magic-link-mail.d.ts +3 -2
  20. package/dist/auth-email-password/magic-link-mail.js +19 -14
  21. package/dist/auth-email-password/mail-notifications.d.ts +32 -0
  22. package/dist/auth-email-password/mail-notifications.js +116 -0
  23. package/dist/auth-email-password/signup-activation.d.ts +1 -1
  24. package/dist/auth-email-password/signup-activation.js +2 -3
  25. package/dist/auth-foundation/types.js +2 -8
  26. package/dist/auth-mfa/feature.js +4 -4
  27. package/dist/auth-mfa/hmac-token-codec.js +4 -4
  28. package/dist/auth-mfa/mfa-challenge-token.js +3 -3
  29. package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -3
  30. package/dist/auth-mfa/mfa-setup-token.js +8 -8
  31. package/dist/billing-foundation/constants.d.ts +3 -3
  32. package/dist/billing-foundation/constants.js +5 -4
  33. package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +32 -14
  34. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +27 -32
  35. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +3 -3
  36. package/dist/billing-foundation/consumer-protection/mail-notifications.d.ts +8 -0
  37. package/dist/billing-foundation/consumer-protection/mail-notifications.js +95 -0
  38. package/dist/billing-foundation/consumer-protection/register-consent.js +2 -0
  39. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +20 -10
  40. package/dist/billing-foundation/consumer-protection/termination-mail.js +10 -21
  41. package/dist/billing-foundation/consumer-protection/termination-notify.js +2 -2
  42. package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -3
  43. package/dist/billing-foundation/consumer-protection/termination-request.js +2 -2
  44. package/dist/billing-foundation/handlers/process-event.write.js +2 -2
  45. package/dist/billing-foundation/handlers/process-payment-event.write.js +1 -1
  46. package/dist/billing-foundation/subscription-tier-sync.js +3 -4
  47. package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -1
  48. package/dist/cap-overview/web/cap-usage-cell.js +5 -4
  49. package/dist/channel-email/email-channel.d.ts +2 -2
  50. package/dist/channel-email/email-channel.js +89 -26
  51. package/dist/channel-email/feature.js +3 -3
  52. package/dist/channel-email/index.d.ts +1 -1
  53. package/dist/channel-email/index.js +1 -1
  54. package/dist/channel-email/pii-guard.js +10 -0
  55. package/dist/channel-email/smtp-transport.js +13 -0
  56. package/dist/channel-email/types.d.ts +8 -0
  57. package/dist/config/handlers/set.write.js +1 -2
  58. package/dist/custom-fields/lib/value-schema.js +10 -11
  59. package/dist/custom-fields/wire-user-data-rights.js +7 -6
  60. package/dist/data-retention/index.d.ts +1 -0
  61. package/dist/data-retention/index.js +1 -0
  62. package/dist/data-retention/run-retention-cleanup.js +6 -6
  63. package/dist/delivery/attachment-ref.d.ts +39 -0
  64. package/dist/delivery/attachment-ref.js +64 -0
  65. package/dist/delivery/channel-context.d.ts +8 -4
  66. package/dist/delivery/channel-context.js +76 -2
  67. package/dist/delivery/constants.d.ts +1 -0
  68. package/dist/delivery/constants.js +1 -0
  69. package/dist/delivery/delivery-service.d.ts +4 -1
  70. package/dist/delivery/delivery-service.js +38 -32
  71. package/dist/delivery/feature.js +1 -1
  72. package/dist/delivery/index.d.ts +5 -2
  73. package/dist/delivery/index.js +4 -1
  74. package/dist/delivery/jobs.js +25 -5
  75. package/dist/delivery/notify-factory.js +4 -1
  76. package/dist/delivery/testing.d.ts +4 -1
  77. package/dist/delivery/types.d.ts +20 -14
  78. package/dist/delivery/unsubscribe.js +4 -1
  79. package/dist/derivatives-sharp/render.js +1 -18
  80. package/dist/document-ingest-foundation/aggregate-id.d.ts +1 -0
  81. package/dist/document-ingest-foundation/aggregate-id.js +8 -0
  82. package/dist/document-ingest-foundation/events.js +3 -1
  83. package/dist/document-ingest-foundation/feature.js +4 -2
  84. package/dist/document-ingest-foundation/index.d.ts +1 -0
  85. package/dist/document-ingest-foundation/index.js +1 -0
  86. package/dist/document-ingest-foundation/pages.js +4 -4
  87. package/dist/feature-toggles/toggle-sync-signal.js +4 -4
  88. package/dist/file-foundation/index.d.ts +1 -0
  89. package/dist/file-foundation/index.js +1 -0
  90. package/dist/file-foundation/read-file-ref-content.d.ts +27 -0
  91. package/dist/file-foundation/read-file-ref-content.js +49 -0
  92. package/dist/folders/web/folder-manager.js +3 -2
  93. package/dist/form-draft/db/queries/cleanup.js +4 -4
  94. package/dist/form-draft/db/queries/draft-count.js +2 -2
  95. package/dist/form-draft/db/queries/owned-file-refs.js +2 -2
  96. package/dist/form-draft/release-file-refs.js +3 -2
  97. package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
  98. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +6 -6
  99. package/dist/inbound-mail-foundation/handlers/scope-visibility.d.ts +1 -1
  100. package/dist/inbound-mail-foundation/oauth-state.js +7 -7
  101. package/dist/inbound-mail-foundation/types.js +4 -4
  102. package/dist/inbound-mail-foundation/watch-supervisor.js +22 -13
  103. package/dist/inbound-provider-imap/imap-client.js +4 -3
  104. package/dist/ledger/recurring.js +1 -3
  105. package/dist/managed-pages/branding.d.ts +2 -0
  106. package/dist/managed-pages/branding.js +24 -2
  107. package/dist/managed-pages/feature.js +1 -0
  108. package/dist/managed-pages/i18n.js +1 -0
  109. package/dist/managed-pages/screens/branding-screen.js +8 -1
  110. package/dist/page-render/branding.d.ts +1 -0
  111. package/dist/page-render/branding.js +1 -0
  112. package/dist/page-render/markdown.js +1 -1
  113. package/dist/personal-access-tokens/feature.js +1 -1
  114. package/dist/personal-access-tokens/handlers/list.query.js +2 -1
  115. package/dist/personal-access-tokens/revoke-for-user.js +2 -1
  116. package/dist/renderer-foundation/api.d.ts +13 -13
  117. package/dist/renderer-foundation/api.js +1 -1
  118. package/dist/renderer-foundation/constants.js +2 -4
  119. package/dist/renderer-foundation/feature.js +7 -7
  120. package/dist/renderer-foundation/index.d.ts +2 -1
  121. package/dist/renderer-foundation/index.js +1 -0
  122. package/dist/renderer-foundation/types.d.ts +92 -32
  123. package/dist/renderer-pdf/feature.js +7 -1
  124. package/dist/sessions/db/queries/cleanup.js +2 -2
  125. package/dist/sessions/feature.js +1 -1
  126. package/dist/sessions/handlers/list.query.js +2 -1
  127. package/dist/shared/attachment-file-name.d.ts +3 -0
  128. package/dist/shared/attachment-file-name.js +35 -0
  129. package/dist/shared/index.d.ts +3 -0
  130. package/dist/shared/index.js +3 -0
  131. package/dist/shared/is-tenant-db.js +1 -1
  132. package/dist/shared/mail-template-body.d.ts +4 -0
  133. package/dist/shared/mail-template-body.js +17 -0
  134. package/dist/shared/sharp-input-limits.d.ts +6 -0
  135. package/dist/shared/sharp-input-limits.js +22 -0
  136. package/dist/shared/signup-handover.js +2 -2
  137. package/dist/tags/handlers/assign-tag.write.js +3 -1
  138. package/dist/tags/web/tag-chip.js +2 -0
  139. package/dist/template-resolver/api.d.ts +30 -2
  140. package/dist/template-resolver/api.js +40 -5
  141. package/dist/template-resolver/compile-document-body.d.ts +8 -0
  142. package/dist/template-resolver/compile-document-body.js +163 -0
  143. package/dist/template-resolver/constants.d.ts +13 -4
  144. package/dist/template-resolver/constants.js +30 -10
  145. package/dist/template-resolver/document-sanitizer.d.ts +32 -0
  146. package/dist/template-resolver/document-sanitizer.js +201 -0
  147. package/dist/template-resolver/document-schema.d.ts +21 -0
  148. package/dist/template-resolver/document-schema.js +296 -0
  149. package/dist/template-resolver/feature.d.ts +18 -0
  150. package/dist/template-resolver/feature.js +50 -6
  151. package/dist/template-resolver/handlers/bundle-import.d.ts +20 -0
  152. package/dist/template-resolver/handlers/bundle-import.js +147 -0
  153. package/dist/template-resolver/handlers/collection-set.write.js +25 -2
  154. package/dist/template-resolver/handlers/copy-locale.write.d.ts +19 -0
  155. package/dist/template-resolver/handlers/copy-locale.write.js +116 -0
  156. package/dist/template-resolver/handlers/diff-to-system.query.d.ts +22 -0
  157. package/dist/template-resolver/handlers/diff-to-system.query.js +64 -0
  158. package/dist/template-resolver/handlers/export-bundle.query.d.ts +35 -0
  159. package/dist/template-resolver/handlers/export-bundle.query.js +68 -0
  160. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
  161. package/dist/template-resolver/handlers/find-by-id.query.js +3 -3
  162. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +39 -0
  163. package/dist/template-resolver/handlers/import-bundle-system.write.js +36 -0
  164. package/dist/template-resolver/handlers/import-bundle.write.d.ts +40 -0
  165. package/dist/template-resolver/handlers/import-bundle.write.js +19 -0
  166. package/dist/template-resolver/handlers/layout-source.query.d.ts +26 -0
  167. package/dist/template-resolver/handlers/layout-source.query.js +64 -0
  168. package/dist/template-resolver/handlers/list.query.d.ts +4 -1
  169. package/dist/template-resolver/handlers/list.query.js +5 -7
  170. package/dist/template-resolver/handlers/overview.query.d.ts +59 -0
  171. package/dist/template-resolver/handlers/overview.query.js +165 -0
  172. package/dist/template-resolver/handlers/preview.query.d.ts +12 -0
  173. package/dist/template-resolver/handlers/preview.query.js +24 -0
  174. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +8 -0
  175. package/dist/template-resolver/handlers/reset-to-system.write.js +50 -0
  176. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +8 -0
  177. package/dist/template-resolver/handlers/resource-link-pdf.write.js +32 -0
  178. package/dist/template-resolver/handlers/resource-remove.write.d.ts +9 -0
  179. package/dist/template-resolver/handlers/resource-remove.write.js +44 -0
  180. package/dist/template-resolver/handlers/resource-shared.d.ts +9 -0
  181. package/dist/template-resolver/handlers/resource-shared.js +14 -0
  182. package/dist/template-resolver/handlers/resource-upload.write.d.ts +13 -0
  183. package/dist/template-resolver/handlers/resource-upload.write.js +98 -0
  184. package/dist/template-resolver/handlers/send-test.write.d.ts +11 -0
  185. package/dist/template-resolver/handlers/send-test.write.js +81 -0
  186. package/dist/template-resolver/handlers/set.write.d.ts +4 -1
  187. package/dist/template-resolver/handlers/set.write.js +25 -3
  188. package/dist/template-resolver/handlers/shared.d.ts +9 -5
  189. package/dist/template-resolver/handlers/shared.js +6 -2
  190. package/dist/template-resolver/handlers/template-gates.d.ts +36 -0
  191. package/dist/template-resolver/handlers/template-gates.js +131 -0
  192. package/dist/template-resolver/handlers/template-write.d.ts +45 -0
  193. package/dist/template-resolver/handlers/template-write.js +109 -0
  194. package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +17 -0
  195. package/dist/template-resolver/handlers/tenant-overrides.query.js +48 -0
  196. package/dist/template-resolver/handlers/toggle-status.write.d.ts +12 -0
  197. package/dist/template-resolver/handlers/toggle-status.write.js +27 -11
  198. package/dist/template-resolver/handlers/upsert-system.write.d.ts +5 -8
  199. package/dist/template-resolver/handlers/upsert-system.write.js +14 -59
  200. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +5 -8
  201. package/dist/template-resolver/handlers/upsert-tenant.write.js +10 -61
  202. package/dist/template-resolver/handlers/usage.query.d.ts +17 -0
  203. package/dist/template-resolver/handlers/usage.query.js +15 -0
  204. package/dist/template-resolver/handlers/validate.query.d.ts +74 -0
  205. package/dist/template-resolver/handlers/validate.query.js +80 -0
  206. package/dist/template-resolver/index.d.ts +18 -1
  207. package/dist/template-resolver/index.js +16 -1
  208. package/dist/template-resolver/layout-regions.d.ts +23 -0
  209. package/dist/template-resolver/layout-regions.js +125 -0
  210. package/dist/template-resolver/linked-resources.d.ts +17 -0
  211. package/dist/template-resolver/linked-resources.js +75 -0
  212. package/dist/template-resolver/preview-display-html.d.ts +7 -0
  213. package/dist/template-resolver/preview-display-html.js +27 -0
  214. package/dist/template-resolver/print-document.d.ts +1 -0
  215. package/dist/template-resolver/print-document.js +55 -0
  216. package/dist/template-resolver/qualified-names.d.ts +30 -0
  217. package/dist/template-resolver/qualified-names.js +30 -0
  218. package/dist/template-resolver/record-mapping.d.ts +2 -0
  219. package/dist/template-resolver/record-mapping.js +21 -0
  220. package/dist/template-resolver/resource-image.d.ts +15 -0
  221. package/dist/template-resolver/resource-image.js +34 -0
  222. package/dist/template-resolver/seeding.d.ts +14 -1
  223. package/dist/template-resolver/seeding.js +105 -15
  224. package/dist/template-resolver/store-template-resource.d.ts +64 -0
  225. package/dist/template-resolver/store-template-resource.js +264 -0
  226. package/dist/template-resolver/table.d.ts +47 -8
  227. package/dist/template-resolver/table.js +11 -0
  228. package/dist/template-resolver/template-bundle.d.ts +36 -0
  229. package/dist/template-resolver/template-bundle.js +60 -0
  230. package/dist/template-resolver/template-diff.d.ts +7 -0
  231. package/dist/template-resolver/template-diff.js +68 -0
  232. package/dist/template-resolver/template-meta.d.ts +60 -0
  233. package/dist/template-resolver/template-meta.js +101 -0
  234. package/dist/template-resolver/template-preview.d.ts +86 -0
  235. package/dist/template-resolver/template-preview.js +374 -0
  236. package/dist/template-resolver/template-resources.d.ts +14 -0
  237. package/dist/template-resolver/template-resources.js +42 -0
  238. package/dist/template-resolver/template-usage.d.ts +12 -0
  239. package/dist/template-resolver/template-usage.js +30 -0
  240. package/dist/template-resolver/template-validation.d.ts +22 -0
  241. package/dist/template-resolver/template-validation.js +172 -0
  242. package/dist/template-resolver/testing.js +5 -5
  243. package/dist/template-resolver/user-content-table.d.ts +14 -8
  244. package/dist/template-resolver/web/client-plugin.d.ts +1 -0
  245. package/dist/template-resolver/web/client-plugin.js +9 -6
  246. package/dist/template-resolver/web/document-editor-extensions.d.ts +15 -0
  247. package/dist/template-resolver/web/document-editor-extensions.js +107 -0
  248. package/dist/template-resolver/web/document-editor.d.ts +3 -0
  249. package/dist/template-resolver/web/document-editor.js +11 -0
  250. package/dist/template-resolver/web/document-tiptap-editor.d.ts +3 -0
  251. package/dist/template-resolver/web/document-tiptap-editor.js +376 -0
  252. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +33 -0
  253. package/dist/template-resolver/web/document-toolbar-blocks.js +15 -0
  254. package/dist/template-resolver/web/editor-handle.d.ts +3 -0
  255. package/dist/template-resolver/web/editor-handle.js +24 -0
  256. package/dist/template-resolver/web/i18n.js +76 -0
  257. package/dist/template-resolver/web/index.d.ts +4 -0
  258. package/dist/template-resolver/web/index.js +4 -0
  259. package/dist/template-resolver/web/placeholder-line-editor-lazy.d.ts +4 -0
  260. package/dist/template-resolver/web/placeholder-line-editor-lazy.js +16 -0
  261. package/dist/template-resolver/web/placeholder-line-editor.d.ts +15 -0
  262. package/dist/template-resolver/web/placeholder-line-editor.js +94 -0
  263. package/dist/template-resolver/web/placeholder-text.d.ts +4 -0
  264. package/dist/template-resolver/web/placeholder-text.js +34 -0
  265. package/dist/template-resolver/web/use-stable-by-key.d.ts +1 -0
  266. package/dist/template-resolver/web/use-stable-by-key.js +13 -0
  267. package/dist/tenant/handlers/team-list.query.js +3 -2
  268. package/dist/tenant/seeding.js +31 -6
  269. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
  270. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +2 -1
  271. package/dist/user/db/queries/stream-tenant-backfill.js +4 -4
  272. package/dist/user/handlers/detail.query.js +2 -1
  273. package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
  274. package/dist/user-data-rights/email-templates.d.ts +1 -29
  275. package/dist/user-data-rights/email-templates.js +5 -106
  276. package/dist/user-data-rights/feature.js +53 -35
  277. package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
  278. package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
  279. package/dist/user-data-rights/handlers/download-by-token.query.js +5 -1
  280. package/dist/user-data-rights/handlers/list-download-attempts.query.d.ts +1 -1
  281. package/dist/user-data-rights/handlers/list-download-attempts.query.js +10 -1
  282. package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
  283. package/dist/user-data-rights/index.d.ts +1 -0
  284. package/dist/user-data-rights/index.js +1 -0
  285. package/dist/user-data-rights/lib/default-mailers.d.ts +15 -7
  286. package/dist/user-data-rights/lib/default-mailers.js +100 -48
  287. package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
  288. package/dist/user-data-rights/mail-notifications.d.ts +13 -0
  289. package/dist/user-data-rights/mail-notifications.js +118 -0
  290. package/dist/user-data-rights/schema/download-attempt.js +6 -12
  291. package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
  292. package/dist/user-data-rights-defaults/feature.js +6 -1
  293. package/dist/user-data-rights-defaults/hooks/download-attempt.userdata-hook.d.ts +3 -0
  294. package/dist/user-data-rights-defaults/hooks/download-attempt.userdata-hook.js +48 -0
  295. package/dist/user-profile/web/profile-screen.js +2 -1
  296. package/dist/waitlist/feature.js +10 -0
  297. package/dist/waitlist/handlers/submit.write.js +3 -4
  298. package/dist/waitlist/mail.d.ts +19 -6
  299. package/dist/waitlist/mail.js +50 -14
  300. package/package.json +28 -17
  301. package/src/admin-shell/changes.json +6 -0
  302. package/src/auth-email-password/changes.json +14 -0
  303. package/src/billing-foundation/changes.json +7 -0
  304. package/src/channel-email/changes.json +19 -0
  305. package/src/data-retention/changes.json +6 -0
  306. package/src/delivery/changes.json +48 -0
  307. package/src/document-ingest-foundation/changes.json +6 -0
  308. package/src/managed-pages/changes.json +6 -0
  309. package/src/renderer-foundation/changes.json +39 -0
  310. package/src/template-resolver/changes.json +118 -0
  311. package/src/user-data-rights/changes.json +14 -0
  312. package/src/waitlist/changes.json +7 -0
  313. package/dist/renderer-simple/feature.d.ts +0 -10
  314. package/dist/renderer-simple/feature.js +0 -42
  315. package/dist/renderer-simple/index.d.ts +0 -3
  316. package/dist/renderer-simple/index.js +0 -2
  317. package/dist/renderer-simple/mail-blocks.d.ts +0 -21
  318. package/dist/renderer-simple/mail-blocks.js +0 -107
  319. package/dist/renderer-simple/resolve-variables.d.ts +0 -7
  320. package/dist/renderer-simple/resolve-variables.js +0 -47
  321. package/dist/renderer-simple/simple-renderer.d.ts +0 -24
  322. package/dist/renderer-simple/simple-renderer.js +0 -200
  323. package/src/renderer-simple/changes.json +0 -27
@@ -1,5 +1,6 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { access, definePagedQueryHandler, MAX_LIST_LIMIT, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
3
4
  import * as z from "zod";
4
5
  import { decryptStoredPii } from "../../shared/index.js";
5
6
  import { userSessionTable } from "../schema/user-session.js";
@@ -10,7 +11,7 @@ import { userSessionTable } from "../schema/user-session.js";
10
11
  // non-exposed column.
11
12
  const SORTABLE_COLUMNS = ["id", "userId", "createdAt", "expiresAt", "revokedAt"];
12
13
  function isSortableColumn(value) {
13
- return SORTABLE_COLUMNS.includes(value);
14
+ return isOneOf(SORTABLE_COLUMNS, value);
14
15
  }
15
16
  // Admin view of every session in the active tenant. ctx.db (TenantDb)
16
17
  // applies tenant-scoping automatically on selects from tables with a
@@ -0,0 +1,3 @@
1
+ import * as z from "zod";
2
+ export declare const attachmentFileNameSchema: z.ZodString;
3
+ export declare function sanitizePdfAttachmentFileName(raw: string, templateSlug: string): string;
@@ -0,0 +1,35 @@
1
+ import * as z from "zod";
2
+ const MAX_FILE_NAME_LENGTH = 255;
3
+ function hasControlCharacter(value) {
4
+ return value.split("").some((char) => {
5
+ const code = char.charCodeAt(0);
6
+ return code <= 0x1f || code === 0x7f;
7
+ });
8
+ }
9
+ export const attachmentFileNameSchema = z
10
+ .string()
11
+ .min(1)
12
+ .max(MAX_FILE_NAME_LENGTH)
13
+ .refine((name) => !name.includes("/") && !name.includes("\\") && !hasControlCharacter(name), {
14
+ message: "file name must not contain path separators or control characters",
15
+ });
16
+ const PDF_FILE_NAME_MAX_LENGTH = 200;
17
+ const PDF_EXTENSION = ".pdf";
18
+ // Shared by everything that names a rendered PDF, so a mail attachment and a download of the same
19
+ // template get the same name. The cut leaves room for the extension: the attachment schema caps
20
+ // rendered-pdf names, and a name that only fits without ".pdf" would fail it.
21
+ export function sanitizePdfAttachmentFileName(raw, templateSlug) {
22
+ const cleaned = raw
23
+ .split("")
24
+ .filter((char) => !hasControlCharacter(char))
25
+ .join("")
26
+ .replace(/[/\\";]/g, "-")
27
+ .replaceAll("..", "-")
28
+ .trim()
29
+ .slice(0, PDF_FILE_NAME_MAX_LENGTH - PDF_EXTENSION.length)
30
+ .trim();
31
+ const lastSegment = templateSlug.split(":").pop() ?? templateSlug;
32
+ if (cleaned === "")
33
+ return `${lastSegment}${PDF_EXTENSION}`;
34
+ return cleaned.toLowerCase().endsWith(PDF_EXTENSION) ? cleaned : `${cleaned}${PDF_EXTENSION}`;
35
+ }
@@ -1,5 +1,6 @@
1
1
  export { collectErasureFailure, throwIfErasureFailed } from "./assert-erased.js";
2
2
  export { assertNoWhereRuleInOwnershipWrite } from "./assert-no-where-rule-in-ownership-write.js";
3
+ export { attachmentFileNameSchema, sanitizePdfAttachmentFileName, } from "./attachment-file-name.js";
3
4
  export { type ChunkedMigrationOptions, type ChunkedMigrationResult, type ChunkedMigrationStopReason, type MigrationRowOutcome, runChunkedMigration, } from "./chunked-entity-migration.js";
4
5
  export { classifyStoredEnvelope, type StoredEnvelopeClassification, } from "./classify-stored-envelope.js";
5
6
  export { decryptStoredPii } from "./decrypt-stored-pii.js";
@@ -9,6 +10,7 @@ export { isWithinGracePeriod } from "./grace-period.js";
9
10
  export { isIdentityV3Hash, verifyIdentityV3Hash } from "./identity-v3-hash.js";
10
11
  export { isTenantDb } from "./is-tenant-db.js";
11
12
  export { createLockoutCounter, type LockoutCounterState } from "./lockout-counter.js";
13
+ export { mailHeading, mailParagraph, mailRepeatedRows } from "./mail-template-body.js";
12
14
  export { mapWithConcurrency } from "./map-with-concurrency.js";
13
15
  export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
14
16
  export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
@@ -20,6 +22,7 @@ export { type RowBoundGrantResult, redeemRowBoundGrant, signRowBoundGrant, } fro
20
22
  export { runInSubTransaction } from "./run-in-sub-transaction.js";
21
23
  export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
22
24
  export { sessionTimezoneField } from "./session-timezone-field.js";
25
+ export { assertNotSvg, MAX_INPUT_PIXELS, SHARP_INPUT_OPTIONS } from "./sharp-input-limits.js";
23
26
  export { peekTokenSubject, signToken, TokenPurpose, type VerifyResult, verifyToken, } from "./signed-token.js";
24
27
  export { EXT_SIGNUP_HANDOVER, findSignupHandoverProvider, isSignupHandoverProvider, SIGNUP_HANDOVER_BENIGN_CLAIM_REJECTION_CODE, type SignupHandoverBinding, type SignupHandoverProvider, } from "./signup-handover.js";
25
28
  export { createSingleUseTokenStore, hashSingleUseToken } from "./single-use-token-store.js";
@@ -1,5 +1,6 @@
1
1
  export { collectErasureFailure, throwIfErasureFailed } from "./assert-erased.js";
2
2
  export { assertNoWhereRuleInOwnershipWrite } from "./assert-no-where-rule-in-ownership-write.js";
3
+ export { attachmentFileNameSchema, sanitizePdfAttachmentFileName, } from "./attachment-file-name.js";
3
4
  export { runChunkedMigration, } from "./chunked-entity-migration.js";
4
5
  export { classifyStoredEnvelope, } from "./classify-stored-envelope.js";
5
6
  export { decryptStoredPii } from "./decrypt-stored-pii.js";
@@ -9,6 +10,7 @@ export { isWithinGracePeriod } from "./grace-period.js";
9
10
  export { isIdentityV3Hash, verifyIdentityV3Hash } from "./identity-v3-hash.js";
10
11
  export { isTenantDb } from "./is-tenant-db.js";
11
12
  export { createLockoutCounter } from "./lockout-counter.js";
13
+ export { mailHeading, mailParagraph, mailRepeatedRows } from "./mail-template-body.js";
12
14
  export { mapWithConcurrency } from "./map-with-concurrency.js";
13
15
  export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
14
16
  export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
@@ -20,6 +22,7 @@ export { redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
20
22
  export { runInSubTransaction } from "./run-in-sub-transaction.js";
21
23
  export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
22
24
  export { sessionTimezoneField } from "./session-timezone-field.js";
25
+ export { assertNotSvg, MAX_INPUT_PIXELS, SHARP_INPUT_OPTIONS } from "./sharp-input-limits.js";
23
26
  export { peekTokenSubject, signToken, TokenPurpose, verifyToken, } from "./signed-token.js";
24
27
  export { EXT_SIGNUP_HANDOVER, findSignupHandoverProvider, isSignupHandoverProvider, SIGNUP_HANDOVER_BENIGN_CLAIM_REJECTION_CODE, } from "./signup-handover.js";
25
28
  export { createSingleUseTokenStore, hashSingleUseToken } from "./single-use-token-store.js";
@@ -3,5 +3,5 @@
3
3
  // Shared so retention/compliance-profile resolvers that accept either (bulk cron
4
4
  // vs. per-hook TenantDb) branch through one predicate instead of duplicating it.
5
5
  export function isTenantDb(db) {
6
- return typeof db.unsafeRaw === "function";
6
+ return "unsafeRaw" in db && typeof db.unsafeRaw === "function";
7
7
  }
@@ -0,0 +1,4 @@
1
+ export declare function mailTextWithSlotsHtml(text: string): string;
2
+ export declare function mailParagraph(text: string): string;
3
+ export declare function mailHeading(text: string): string;
4
+ export declare function mailRepeatedRows(listName: string, field: string): string;
@@ -0,0 +1,17 @@
1
+ import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
2
+ const SLOT_PATTERN = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
3
+ // Author text uses `{name}` slots. The text is escaped first and the slots are
4
+ // swapped for variable nodes afterwards, so a slot can never smuggle markup.
5
+ export function mailTextWithSlotsHtml(text) {
6
+ return escapeHtml(text).replace(SLOT_PATTERN, (_, name) => `<span data-variable="${escapeHtmlAttr(name)}"></span>`);
7
+ }
8
+ export function mailParagraph(text) {
9
+ return `<p>${mailTextWithSlotsHtml(text)}</p>`;
10
+ }
11
+ export function mailHeading(text) {
12
+ return `<h2>${mailTextWithSlotsHtml(text)}</h2>`;
13
+ }
14
+ // One table row per list entry; the cell shows the entry's `field`.
15
+ export function mailRepeatedRows(listName, field) {
16
+ return `<table><tbody><tr data-each="${escapeHtmlAttr(listName)}"><td><p><span data-variable="${escapeHtmlAttr(field)}"></span></p></td></tr></tbody></table>`;
17
+ }
@@ -0,0 +1,6 @@
1
+ export declare const MAX_INPUT_PIXELS = 100000000;
2
+ export declare const SHARP_INPUT_OPTIONS: {
3
+ readonly limitInputPixels: 100000000;
4
+ readonly sequentialRead: true;
5
+ };
6
+ export declare function assertNotSvg(format: string | undefined): void;
@@ -0,0 +1,22 @@
1
+ // Sharp-free so callers can import the limits without loading the native binding.
2
+ // Above this, a decoded pixel buffer runs into hundreds of MB before any
3
+ // resize even happens — sharp's own default (~268MP) is still large enough
4
+ // to be an OOM primitive for a single request.
5
+ export const MAX_INPUT_PIXELS = 100_000_000;
6
+ // sequentialRead trades random pixel access for lower peak memory on
7
+ // streamed formats — applied at every decode entry point below since none
8
+ // of them need non-sequential access to the *original* bytes (extract runs
9
+ // against a single already-buffered region per call).
10
+ export const SHARP_INPUT_OPTIONS = {
11
+ limitInputPixels: MAX_INPUT_PIXELS,
12
+ sequentialRead: true,
13
+ };
14
+ // Shared trust boundary for every image byte source this renderer decodes —
15
+ // the original upload (renderImage) and an overlay layer's bytes (applyOverlays)
16
+ // alike: a base64 string in a field declaration isn't more trustworthy than
17
+ // an upload just because it came from app code.
18
+ export function assertNotSvg(format) {
19
+ if (format === "svg") {
20
+ throw new Error("derivatives-sharp: SVG is not supported — rendering untrusted SVG through libvips is a trust boundary this renderer doesn't need to cross.");
21
+ }
22
+ }
@@ -10,6 +10,7 @@
10
10
  // The grant itself never crosses this boundary: verifyGrant returns only
11
11
  // the { entityType, rowId, sourceTenantId } binding, and mintClaim returns a
12
12
  // freshly signed short-lived token rather than handing back the original.
13
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
13
14
  export const EXT_SIGNUP_HANDOVER = "signupHandover";
14
15
  // Part of the contract: a provider's minted claim write may reject two
15
16
  // different ways. Benign — this code, nothing was written (forged,
@@ -20,8 +21,7 @@ export const EXT_SIGNUP_HANDOVER = "signupHandover";
20
21
  // together with it, never treat that as a skippable no-op.
21
22
  export const SIGNUP_HANDOVER_BENIGN_CLAIM_REJECTION_CODE = "invalid_or_expired_grant";
22
23
  export function isSignupHandoverProvider(options) {
23
- return (typeof options === "object" &&
24
- options !== null &&
24
+ return (isPlainObject(options) &&
25
25
  typeof options["verifyGrant"] === "function" &&
26
26
  typeof options["mintClaim"] === "function");
27
27
  }
@@ -1,4 +1,5 @@
1
1
  import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
2
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
2
3
  import { denyUnlessJoinRowParentVisible } from "../../shared/index.js";
3
4
  import { tagAssignmentAggregateId } from "../aggregate-id.js";
4
5
  import { DEFAULT_TAG_ACCESS } from "../constants.js";
@@ -49,7 +50,8 @@ export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS, assignmentEx
49
50
  // above and this restore() call, so restore() correctly refuses with
50
51
  // "not_deleted" — but the desired end state is already true. Converge
51
52
  // like the create() race below instead of surfacing a spurious error.
52
- if (restored.error.details?.reason === "not_deleted") {
53
+ if (isPlainObject(restored.error.details) &&
54
+ restored.error.details["reason"] === "not_deleted") {
53
55
  return { isSuccess: true, data: { id } };
54
56
  }
55
57
  if (restored.error.code !== "not_found")
@@ -14,6 +14,8 @@ function hexToRgb(color) {
14
14
  if (match === null)
15
15
  return null;
16
16
  const short = match[1];
17
+ if (short === undefined)
18
+ return null;
17
19
  const hex = short.length === 3
18
20
  ? `${short[0]}${short[0]}${short[1]}${short[1]}${short[2]}${short[2]}`
19
21
  : short;
@@ -1,7 +1,10 @@
1
- import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
1
+ import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
2
  import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
4
+ import type { FileAttachmentRef } from "../renderer-foundation/index.js";
4
5
  import type { ContentFormat, TemplateKind } from "./constants.js";
6
+ import { type TemplatePdfAttachmentLink } from "./linked-resources.js";
7
+ import { type TemplateMeta } from "./template-meta.js";
5
8
  export type TemplateResource = {
6
9
  readonly id: string;
7
10
  readonly version: number;
@@ -14,7 +17,15 @@ export type TemplateResource = {
14
17
  readonly content: string;
15
18
  readonly contentFormat: ContentFormat;
16
19
  readonly variableSchema: Record<string, unknown>;
20
+ /** Embeddable images: resource key -> fileRef id. */
17
21
  readonly linkedResources: Record<string, string>;
22
+ /** Files attached to every mail rendered from this template, ordered by key. */
23
+ readonly fileAttachments?: readonly FileAttachmentRef[];
24
+ /** PDFs rendered from document templates and attached at send time, ordered by key. */
25
+ readonly pdfAttachmentLinks?: readonly TemplatePdfAttachmentLink[];
26
+ /** Parsed per kind; null when absent or when the stored JSON no longer
27
+ * matches the kind's schema (the renderer then fails with a clear error). */
28
+ readonly meta: TemplateMeta | null;
18
29
  readonly scope: "system" | "tenant";
19
30
  readonly parentTemplateId: string | null;
20
31
  readonly status: "draft" | "active" | "archived";
@@ -26,6 +37,13 @@ export type ResolveRequest = {
26
37
  readonly kind: TemplateKind;
27
38
  readonly locale: string;
28
39
  };
40
+ export type LayoutKind = "mail-layout" | "pdf-layout";
41
+ export type ResolveLayoutRequest = {
42
+ readonly tenantId: TenantId;
43
+ readonly slug: string;
44
+ readonly kind: LayoutKind;
45
+ readonly locale: string;
46
+ };
29
47
  export type TemplateResolverApi = {
30
48
  /**
31
49
  * Findet ein konkretes Template by (tenantId, slug, kind, locale).
@@ -60,8 +78,18 @@ export type TemplateResolverApi = {
60
78
  * User cross-tenant Templates abfragen (Tenant-Isolation gebrochen).
61
79
  */
62
80
  readonly resolveTemplate: (args: ResolveRequest) => Promise<TemplateResource>;
81
+ /**
82
+ * Layouts are locale-neutral in the product (the layout editor saves under the locale of the
83
+ * row it loaded), so the locale chain is wider than resolveTemplate: tenant (requested,
84
+ * FALLBACK_LOCALE, any active locale), then system (same). Only active rows count; among
85
+ * "any" the highest version wins, then the alphabetically first locale, so the pick is stable.
86
+ * Throws TemplateNotFoundError when neither scope has an active row.
87
+ *
88
+ * **Caller invariant:** `tenantId` must come from the server, as for `resolveTemplate`.
89
+ */
90
+ readonly resolveLayout: (args: ResolveLayoutRequest) => Promise<TemplateResource>;
63
91
  };
64
- export declare function createTemplateResolverApi(db: DbRunner): TemplateResolverApi;
92
+ export declare function createTemplateResolverApi(db: DbRunner | TenantDb): TemplateResolverApi;
65
93
  export declare class TemplateNotFoundError extends Error {
66
94
  readonly query: {
67
95
  slug: string;
@@ -4,7 +4,9 @@
4
4
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
5
5
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
6
6
  import { FALLBACK_LOCALE, SYSTEM_TENANT_ID } from "./constants.js";
7
+ import { fileAttachmentsOf, imageLinksOf, parseLinkedResourceEntries, pdfAttachmentLinksOf, } from "./linked-resources.js";
7
8
  import { templateResourcesTable } from "./table.js";
9
+ import { parseTemplateMeta } from "./template-meta.js";
8
10
  export function createTemplateResolverApi(db) {
9
11
  return {
10
12
  findExact: async ({ tenantId, slug, kind, locale, scope }) => {
@@ -31,8 +33,30 @@ export function createTemplateResolverApi(db) {
31
33
  }
32
34
  throw new TemplateNotFoundError({ slug, kind, locale });
33
35
  },
36
+ resolveLayout: async ({ tenantId, slug, kind, locale }) => {
37
+ for (const tid of [tenantId, SYSTEM_TENANT_ID]) {
38
+ const rows = await selectMany(db, templateResourcesTable, {
39
+ tenantId: tid,
40
+ slug,
41
+ kind,
42
+ });
43
+ const row = pickActiveLayoutRow(rows, locale);
44
+ if (row !== null)
45
+ return toPublic(row);
46
+ }
47
+ throw new TemplateNotFoundError({ slug, kind, locale });
48
+ },
34
49
  };
35
50
  }
51
+ function pickActiveLayoutRow(rows, locale) {
52
+ const active = rows
53
+ .filter((row) => row.status === "active")
54
+ .sort((a, b) => b.version - a.version || a.locale.localeCompare(b.locale));
55
+ return (active.find((row) => row.locale === locale) ??
56
+ active.find((row) => row.locale === FALLBACK_LOCALE) ??
57
+ active[0] ??
58
+ null);
59
+ }
36
60
  async function fetchTemplate(db, tenantId, slug, kind, locale) {
37
61
  const rows = await selectMany(db, templateResourcesTable, { tenantId, slug, kind, locale }, { limit: 1 });
38
62
  // @cast-boundary db-row — db.select returnt unbenanntes unknown[],
@@ -43,7 +67,6 @@ function toPublic(row) {
43
67
  // @cast-boundary db-row — Drizzle-Schema typisiert kind/contentFormat/
44
68
  // scope/status als generic text. CHECK-Constraints in der DB schränken
45
69
  // sie auf die Union-Types ein; Cast assertet das Schema-Wissen.
46
- // linkedResources ist ein text-column mit JSON-payload (string→string map).
47
70
  const kind = row.kind;
48
71
  // @cast-boundary db-row — siehe kind.
49
72
  const contentFormat = row.contentFormat;
@@ -51,9 +74,7 @@ function toPublic(row) {
51
74
  const scope = row.scope;
52
75
  // @cast-boundary db-row — siehe kind.
53
76
  const status = row.status;
54
- // @cast-boundary db-row — parseJson returnt Record<string, unknown>;
55
- // linkedResources-Spalte enthält per Schema {key: signedUrl}-Map.
56
- const linkedResources = parseJson(row.linkedResources);
77
+ const linkedEntries = parseLinkedResourceEntries(row.linkedResources);
57
78
  return {
58
79
  id: String(row.id),
59
80
  version: row.version,
@@ -66,13 +87,27 @@ function toPublic(row) {
66
87
  content: row.content ?? "",
67
88
  contentFormat,
68
89
  variableSchema: parseJson(row.variableSchema),
69
- linkedResources,
90
+ linkedResources: imageLinksOf(linkedEntries),
91
+ fileAttachments: fileAttachmentsOf(linkedEntries),
92
+ pdfAttachmentLinks: pdfAttachmentLinksOf(linkedEntries),
93
+ meta: parseStoredMeta(kind, row.meta),
70
94
  scope,
71
95
  parentTemplateId: row.parentTemplateId,
72
96
  status,
73
97
  modifiedAt: row.modifiedAt,
74
98
  };
75
99
  }
100
+ function parseStoredMeta(kind, raw) {
101
+ if (!raw)
102
+ return null;
103
+ try {
104
+ const result = parseTemplateMeta(kind, JSON.parse(raw));
105
+ return result.ok ? result.meta : null;
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
76
111
  function parseJson(raw) {
77
112
  if (!raw)
78
113
  return {};
@@ -0,0 +1,8 @@
1
+ import type { TemplateVariables } from "@cosmicdrift/kumiko-framework/engine";
2
+ export type CompileDocumentBodyOptions = {
3
+ readonly resourceSrc?: (key: string) => string | undefined;
4
+ readonly alignmentAsInlineStyle?: boolean;
5
+ };
6
+ export declare function collectDocumentResourceKeys(html: string): readonly string[];
7
+ export declare function toResourceDataUri(contentType: string, bytes: Uint8Array): string;
8
+ export declare function compileDocumentBodyToHandlebars(html: string, variables: TemplateVariables, options?: CompileDocumentBodyOptions): string;
@@ -0,0 +1,163 @@
1
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
2
+ import { withParsedDocument } from "./document-sanitizer.js";
3
+ import { DOCUMENT_TEXT_ALIGNMENTS, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
4
+ // Private-use characters mark where a Handlebars expression goes. Author text
5
+ // is scrubbed of them first and every `{`/`}` is entity-encoded before the
6
+ // markers are swapped for real expressions, so the only Handlebars syntax in
7
+ // the output is the syntax this module wrote.
8
+ const EXPRESSION_OPEN = "\uE000";
9
+ const EXPRESSION_CLOSE = "\uE001";
10
+ const MARKER_PATTERN = /\uE000(\d+)\uE001/g;
11
+ function marker(state, expression) {
12
+ state.expressions.push(expression);
13
+ return `${EXPRESSION_OPEN}${state.expressions.length - 1}${EXPRESSION_CLOSE}`;
14
+ }
15
+ function ownVariable(variables, name) {
16
+ return Object.hasOwn(variables, name) ? variables[name] : undefined;
17
+ }
18
+ function itemFieldsOf(variables, listName) {
19
+ const def = ownVariable(variables, listName);
20
+ return def?.type === "list" ? def.item : null;
21
+ }
22
+ function rowScopeOf(element, state) {
23
+ const listName = element.closest("tr[data-each]")?.getAttribute("data-each") ?? null;
24
+ if (listName === null)
25
+ return null;
26
+ const itemFields = itemFieldsOf(state.variables, listName);
27
+ return itemFields === null ? null : { listName, itemFields };
28
+ }
29
+ function resolveReference(name, scope, variables) {
30
+ if (scope !== null && ownVariable(scope.itemFields, name) !== undefined) {
31
+ return { path: name, def: ownVariable(scope.itemFields, name) };
32
+ }
33
+ return { path: scope === null ? name : `@root.${name}`, def: ownVariable(variables, name) };
34
+ }
35
+ function valueExpression(path, def) {
36
+ if (def?.type === "money")
37
+ return `{{money ${path}}}`;
38
+ if (def?.type === "date")
39
+ return `{{date ${path}}}`;
40
+ return `{{${path}}}`;
41
+ }
42
+ function rewriteVariable(element, state) {
43
+ const name = element.getAttribute("data-variable") ?? "";
44
+ const replacement = element.ownerDocument.createTextNode(isDocumentReferenceName(name)
45
+ ? marker(state, valueExpression(...pathAndDef(name, element, state)))
46
+ : "");
47
+ element.replaceWith(replacement);
48
+ }
49
+ function pathAndDef(name, element, state) {
50
+ const { path, def } = resolveReference(name, rowScopeOf(element, state), state.variables);
51
+ return [path, def];
52
+ }
53
+ function rewriteLink(element, state) {
54
+ const href = element.getAttribute("href") ?? "";
55
+ if (isDocumentPlaceholderHref(href)) {
56
+ const name = href.slice(2, -2);
57
+ if (isDocumentReferenceName(name)) {
58
+ const [path] = pathAndDef(name, element, state);
59
+ element.setAttribute("href", marker(state, `{{link ${path}}}`));
60
+ }
61
+ else {
62
+ element.setAttribute("href", "#");
63
+ }
64
+ }
65
+ else if (!isAllowedDocumentLinkHref(href)) {
66
+ element.setAttribute("href", "#");
67
+ }
68
+ }
69
+ function rewriteRepeatRow(row, state) {
70
+ const listName = row.getAttribute("data-each") ?? "";
71
+ const document = row.ownerDocument;
72
+ if (itemFieldsOf(state.variables, listName) === null || !isDocumentReferenceName(listName)) {
73
+ row.remove();
74
+ }
75
+ else {
76
+ row.before(document.createTextNode(marker(state, `{{#each ${listName}}}`)));
77
+ row.after(document.createTextNode(marker(state, "{{/each}}")));
78
+ row.removeAttribute("data-each");
79
+ row.removeAttribute("data-repeat-header");
80
+ }
81
+ }
82
+ function stripActiveContent(element) {
83
+ for (const attribute of Array.from(element.attributes)) {
84
+ if (/^on/i.test(attribute.name))
85
+ element.removeAttribute(attribute.name);
86
+ }
87
+ }
88
+ function neutralizeBraces(html) {
89
+ return html.replace(/[{}]/g, (brace) => (brace === "{" ? "&#123;" : "&#125;"));
90
+ }
91
+ const RESOURCE_IMAGE_CONTENT_TYPES = new Set([
92
+ "image/png",
93
+ "image/jpeg",
94
+ "image/gif",
95
+ "image/webp",
96
+ ]);
97
+ // Unique valid resource keys in document order.
98
+ export function collectDocumentResourceKeys(html) {
99
+ return withParsedDocument(html, (root) => {
100
+ const keys = new Set();
101
+ for (const element of Array.from(root.querySelectorAll("img[data-resource]"))) {
102
+ const key = element.getAttribute("data-resource") ?? "";
103
+ if (isDocumentResourceKey(key))
104
+ keys.add(key);
105
+ }
106
+ return [...keys];
107
+ });
108
+ }
109
+ export function toResourceDataUri(contentType, bytes) {
110
+ if (!RESOURCE_IMAGE_CONTENT_TYPES.has(contentType)) {
111
+ throw new Error(`resource content type "${contentType}" cannot be embedded as an image`);
112
+ }
113
+ return `data:${contentType};base64,${Buffer.from(bytes).toString("base64")}`;
114
+ }
115
+ function rewriteAlignment(element) {
116
+ const align = element.getAttribute("data-align");
117
+ element.removeAttribute("data-align");
118
+ if (isOneOf(DOCUMENT_TEXT_ALIGNMENTS, align))
119
+ element.setAttribute("style", `text-align:${align}`);
120
+ }
121
+ function rewriteResourceImage(element, resourceSrc) {
122
+ const key = element.getAttribute("data-resource") ?? "";
123
+ const src = isDocumentResourceKey(key) ? resourceSrc?.(key) : undefined;
124
+ if (src === undefined) {
125
+ element.remove();
126
+ // skip: an unresolved resource key drops the image
127
+ return;
128
+ }
129
+ element.setAttribute("src", src);
130
+ element.removeAttribute("data-resource");
131
+ }
132
+ // Turns a sanitized document body into Handlebars source. Rows and links are
133
+ // rewritten in document order; list rows last so their placeholders still see
134
+ // the `tr[data-each]` ancestor.
135
+ export function compileDocumentBodyToHandlebars(html, variables, options = {}) {
136
+ const state = { variables, expressions: [] };
137
+ const scrubbed = html.replaceAll(EXPRESSION_OPEN, "").replaceAll(EXPRESSION_CLOSE, "");
138
+ const compiled = withParsedDocument(scrubbed, (root) => {
139
+ for (const element of Array.from(root.querySelectorAll("script")))
140
+ element.remove();
141
+ for (const element of Array.from(root.querySelectorAll("img[data-resource]"))) {
142
+ rewriteResourceImage(element, options.resourceSrc);
143
+ }
144
+ for (const element of Array.from(root.querySelectorAll("*")))
145
+ stripActiveContent(element);
146
+ if (options.alignmentAsInlineStyle === true) {
147
+ for (const element of Array.from(root.querySelectorAll("[data-align]"))) {
148
+ rewriteAlignment(element);
149
+ }
150
+ }
151
+ for (const element of Array.from(root.querySelectorAll("span[data-variable]"))) {
152
+ rewriteVariable(element, state);
153
+ }
154
+ for (const element of Array.from(root.querySelectorAll("a[href]"))) {
155
+ rewriteLink(element, state);
156
+ }
157
+ for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
158
+ rewriteRepeatRow(element, state);
159
+ }
160
+ return root.innerHTML;
161
+ });
162
+ return neutralizeBraces(compiled).replace(MARKER_PATTERN, (_match, index) => state.expressions[Number(index)] ?? "");
163
+ }
@@ -1,17 +1,26 @@
1
- export declare const RENDER_KINDS: readonly ["notification", "mail-html", "document-pdf", "image-snapshot"];
1
+ export declare const RENDER_KINDS: readonly ["mail", "document-pdf", "image-snapshot"];
2
2
  export type RenderKind = (typeof RENDER_KINDS)[number];
3
- export declare const NON_RENDER_KINDS: readonly ["text-block", "ai-prompt"];
3
+ export declare const DOCUMENT_HTML_KINDS: readonly ["mail", "document-pdf"];
4
+ export type DocumentHtmlKind = (typeof DOCUMENT_HTML_KINDS)[number];
5
+ export declare const NON_RENDER_KINDS: readonly ["notification", "text-block", "snippet", "ai-prompt", "mail-layout", "pdf-layout"];
4
6
  export type NonRenderKind = (typeof NON_RENDER_KINDS)[number];
5
- export declare const TEMPLATE_KINDS: readonly ["notification", "mail-html", "document-pdf", "image-snapshot", "text-block", "ai-prompt"];
7
+ export declare const TEMPLATE_KINDS: readonly ["mail", "document-pdf", "image-snapshot", "notification", "text-block", "snippet", "ai-prompt", "mail-layout", "pdf-layout"];
6
8
  export type TemplateKind = (typeof TEMPLATE_KINDS)[number];
7
- export declare const UPSERT_KINDS: readonly ["notification", "mail-html", "document-pdf", "image-snapshot", "ai-prompt"];
9
+ export declare const UPSERT_KINDS: readonly ["mail", "document-pdf", "image-snapshot", "ai-prompt", "mail-layout", "pdf-layout"];
8
10
  export type UpsertKind = (typeof UPSERT_KINDS)[number];
9
11
  export declare const TEXT_BLOCK_KIND: "text-block";
12
+ export declare const COLLECTION_REFUSED_KINDS: readonly string[];
10
13
  export declare const CONTENT_FORMATS: readonly ["markdown", "mjml", "html", "plain"];
11
14
  export type ContentFormat = (typeof CONTENT_FORMATS)[number];
12
15
  export declare const TEMPLATE_SCOPES: readonly ["system", "tenant"];
13
16
  export type TemplateScope = (typeof TEMPLATE_SCOPES)[number];
14
17
  export declare const TEMPLATE_STATUSES: readonly ["draft", "active", "archived"];
15
18
  export type TemplateStatus = (typeof TEMPLATE_STATUSES)[number];
19
+ export declare const PDF_PAGE_FORMATS: readonly ["A4", "Letter"];
20
+ export type PdfPageFormat = (typeof PDF_PAGE_FORMATS)[number];
21
+ export declare const DEFAULT_MAIL_LAYOUT_SLUG = "default";
22
+ export declare const MAIL_PREVIEW_MAX_BYTES: number;
23
+ export declare const TEST_MAIL_NOTIFICATION_TYPE = "template-resolver:test-mail";
24
+ export declare const TEST_MAIL_SUBJECT_PREFIX = "[TEST] ";
16
25
  export { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
17
26
  export declare const FALLBACK_LOCALE = "de";
@@ -6,30 +6,50 @@
6
6
  // Plugin-Renderer in `renderer-foundation` matchen auf kind; der
7
7
  // Resolver hier ist kind-agnostisch — er lädt nur, das Content-Format
8
8
  // (markdown/mjml/html) entscheidet wer's rendert.
9
- export const RENDER_KINDS = [
9
+ export const RENDER_KINDS = ["mail", "document-pdf", "image-snapshot"];
10
+ // Kinds whose html body is a rich document: the server rebuilds it through the
11
+ // document schema on every write (see document-sanitizer.ts).
12
+ export const DOCUMENT_HTML_KINDS = ["mail", "document-pdf"];
13
+ // Kinds without a renderer plugin: `notification` is the retired sections-JSON
14
+ // notification kind, kept so rows stored before mails moved to `mail` stay
15
+ // valid on replay; nothing writes or renders it any more. `text-block` is the Markdown text store
16
+ // (ex text-content feature), `snippet` is the content of app collections such
17
+ // as reply snippets and signatures (own kind so they stay out of the text-block
18
+ // content tree), `ai-prompt` holds LLM prompts, `mail-layout` and
19
+ // `pdf-layout` hold the frame a mail/pdf body is rendered into. All are read,
20
+ // not rendered on their own — keeping them out of RENDER_KINDS is what stops
21
+ // DEFAULT_PLUGIN_BY_KIND from demanding a plugin for them.
22
+ export const NON_RENDER_KINDS = [
10
23
  "notification",
11
- "mail-html",
12
- "document-pdf",
13
- "image-snapshot",
24
+ "text-block",
25
+ "snippet",
26
+ "ai-prompt",
27
+ "mail-layout",
28
+ "pdf-layout",
14
29
  ];
15
- // Kinds without a renderer plugin: `text-block` is the Markdown text store
16
- // (ex text-content feature), `ai-prompt` holds LLM prompts. Both are read,
17
- // not rendered — keeping them out of RENDER_KINDS is what stops
18
- // DEFAULT_PLUGIN_BY_KIND from demanding a plugin for them.
19
- export const NON_RENDER_KINDS = ["text-block", "ai-prompt"];
20
30
  // Full kind domain of the entity; RENDER_KINDS is the renderer-foundation subset.
21
31
  export const TEMPLATE_KINDS = [...RENDER_KINDS, ...NON_RENDER_KINDS];
22
32
  // Kinds written through upsertSystem/upsertTenant. `text-block` is absent on
23
33
  // purpose: it goes through set.write (title + folder tree); two write paths
24
34
  // onto the same row would drift apart.
25
- export const UPSERT_KINDS = [...RENDER_KINDS, "ai-prompt"];
35
+ export const UPSERT_KINDS = [...RENDER_KINDS, "ai-prompt", "mail-layout", "pdf-layout"];
26
36
  // The only kind served by the anonymous-capable by-slug/by-tenant queries and
27
37
  // by set.write — mail templates and AI prompts are operational internals and
28
38
  // must not leak through the anonymous path.
29
39
  export const TEXT_BLOCK_KIND = "text-block";
40
+ // Kinds a content collection must not write: a tenant row of these kinds would
41
+ // shadow the system notification template or layout without the meta and
42
+ // placeholder gates that upsertTenant applies.
43
+ export const COLLECTION_REFUSED_KINDS = ["mail", "mail-layout", "pdf-layout"];
30
44
  export const CONTENT_FORMATS = ["markdown", "mjml", "html", "plain"];
31
45
  export const TEMPLATE_SCOPES = ["system", "tenant"];
32
46
  export const TEMPLATE_STATUSES = ["draft", "active", "archived"];
47
+ export const PDF_PAGE_FORMATS = ["A4", "Letter"];
48
+ export const DEFAULT_MAIL_LAYOUT_SLUG = "default";
49
+ // The mail transport enforces no size limit of its own; 10 MiB is a common provider ceiling.
50
+ export const MAIL_PREVIEW_MAX_BYTES = 10 * 1024 * 1024;
51
+ export const TEST_MAIL_NOTIFICATION_TYPE = "template-resolver:test-mail";
52
+ export const TEST_MAIL_SUBJECT_PREFIX = "[TEST] ";
33
53
  // System-Templates leben unter der canonical SYSTEM_TENANT_ID-Sentinel-UUID.
34
54
  // Re-Export aus framework — single source of truth, vermeidet Drift.
35
55
  export { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";