@cosmicdrift/kumiko-samples 0.362.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 (252) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +17 -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/auth-email-password/changes.json +14 -0
  10. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  11. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  12. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  13. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  14. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  15. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  16. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  17. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +3 -12
  18. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  19. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  20. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  21. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  22. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  23. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  24. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  25. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  26. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  27. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  28. package/packages/bundled-features/src/billing-foundation/changes.json +7 -0
  29. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  30. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +38 -44
  31. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  32. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +115 -0
  33. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  34. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +30 -30
  35. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  36. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  37. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +2 -2
  38. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  39. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  40. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  41. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +2 -2
  42. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  43. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +3 -6
  44. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  45. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  46. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  47. package/packages/bundled-features/src/channel-email/email-channel.ts +122 -29
  48. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  49. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  50. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  51. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  52. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  53. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  54. package/packages/bundled-features/src/config/handlers/set.write.ts +1 -5
  55. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  56. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  57. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  58. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  59. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  60. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  61. package/packages/bundled-features/src/delivery/attachment-ref.ts +84 -0
  62. package/packages/bundled-features/src/delivery/changes.json +48 -0
  63. package/packages/bundled-features/src/delivery/channel-context.ts +129 -4
  64. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  65. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  66. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  67. package/packages/bundled-features/src/delivery/index.ts +17 -2
  68. package/packages/bundled-features/src/delivery/jobs.ts +48 -5
  69. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  70. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  71. package/packages/bundled-features/src/delivery/types.ts +28 -18
  72. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  73. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  74. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  75. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  76. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  77. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -0
  78. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  79. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  80. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  81. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  82. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  83. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  84. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  85. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  86. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  87. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  88. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  89. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  90. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  91. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  92. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +40 -25
  93. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  94. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  95. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  96. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  97. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  98. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  99. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  100. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  101. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  102. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  103. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  104. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  105. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  106. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  107. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  108. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  109. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  110. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  111. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  112. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  113. package/packages/bundled-features/src/renderer-foundation/types.ts +132 -35
  114. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  115. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  116. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  117. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  118. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  119. package/packages/bundled-features/src/shared/index.ts +6 -0
  120. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  121. package/packages/bundled-features/src/shared/mail-template-body.ts +25 -0
  122. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  123. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  124. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  125. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  126. package/packages/bundled-features/src/template-resolver/README.md +63 -7
  127. package/packages/bundled-features/src/template-resolver/api.ts +83 -8
  128. package/packages/bundled-features/src/template-resolver/changes.json +118 -0
  129. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
  130. package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
  131. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  132. package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
  133. package/packages/bundled-features/src/template-resolver/feature.ts +56 -6
  134. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  135. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  136. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +142 -0
  137. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
  138. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
  139. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
  140. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  141. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  142. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +108 -0
  143. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
  144. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -0
  145. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  146. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  147. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  148. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  149. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  150. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  151. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +106 -0
  152. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  153. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  154. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +184 -0
  155. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
  156. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
  157. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
  158. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  159. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +15 -76
  160. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
  161. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  162. package/packages/bundled-features/src/template-resolver/index.ts +100 -0
  163. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  164. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  165. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  166. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  167. package/packages/bundled-features/src/template-resolver/qualified-names.ts +30 -0
  168. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  169. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  170. package/packages/bundled-features/src/template-resolver/seeding.ts +147 -17
  171. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  172. package/packages/bundled-features/src/template-resolver/table.ts +14 -0
  173. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  174. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  175. package/packages/bundled-features/src/template-resolver/template-meta.ts +129 -0
  176. package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
  177. package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
  178. package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
  179. package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
  180. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  181. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
  182. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
  183. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
  184. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
  185. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
  186. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  187. package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
  188. package/packages/bundled-features/src/template-resolver/web/index.ts +19 -0
  189. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  190. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +169 -0
  191. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  192. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  193. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  194. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  195. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  196. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  197. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  198. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  199. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  200. package/packages/bundled-features/src/user-data-rights/changes.json +7 -0
  201. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  202. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  203. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  204. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  205. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  206. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  207. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  208. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  209. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  210. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  211. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  212. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  213. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  214. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  215. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  216. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +1 -2
  217. package/samples/apps/ui-walkthrough/package.json +2 -0
  218. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  219. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  220. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +154 -0
  221. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  222. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  223. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +354 -0
  224. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -0
  225. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  226. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  227. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  228. package/samples/apps/ui-walkthrough/src/run-config.ts +37 -1
  229. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  230. package/samples/apps/use-all-bundled/feature-manifest.json +115 -20
  231. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  232. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  233. package/samples/apps/use-all-bundled/package.json +2 -0
  234. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  235. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  236. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  237. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  238. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  239. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  240. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  241. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  242. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  243. package/samples/recipes/delivery-notifications/README.md +26 -25
  244. package/samples/recipes/delivery-notifications/package.json +2 -1
  245. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  246. package/samples/recipes/seed-migration/README.md +1 -1
  247. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  248. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  249. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  250. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  251. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  252. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -4,8 +4,11 @@ import {
4
4
  defineWriteHandler,
5
5
  type TenantId,
6
6
  } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
7
8
  import * as z from "zod";
8
9
  import { requireForTenant } from "../../shared/index.js";
10
+ import { COLLECTION_REFUSED_KINDS } from "../constants.js";
11
+ import { documentViolationsToFieldIssues, sanitizeTemplateBody } from "../document-sanitizer.js";
9
12
  import {
10
13
  type CollectionEntryRow,
11
14
  collectionStore,
@@ -40,6 +43,20 @@ export function makeCollectionSetWrite(collection: ContentCollectionDefinition)
40
43
  ? `Creates or overwrites the caller's own "${collection.id}" entry at a slug and locale, live on save with no draft stage; it always writes the acting user's entry, never another user's.`
41
44
  : `Creates or overwrites the tenant-wide "${collection.id}" entry at a slug and locale, live on save with no draft stage; the change is visible to everyone who may reach this shared collection.`,
42
45
  handler: async (event, ctx) => {
46
+ if (COLLECTION_REFUSED_KINDS.includes(collection.kind)) {
47
+ return writeFailure(
48
+ new ValidationError({
49
+ fields: [
50
+ {
51
+ path: "kind",
52
+ code: "kind_not_allowed",
53
+ i18nKey: "templateResolver.errors.collection.kindNotAllowed",
54
+ params: { detail: collection.kind },
55
+ },
56
+ ],
57
+ }),
58
+ );
59
+ }
43
60
  const override = event.payload.tenantIdOverride;
44
61
  // @cast-boundary engine-payload — override is a zod-validated string, the
45
62
  // user's own tenantId is already TenantId-branded.
@@ -50,6 +67,17 @@ export function makeCollectionSetWrite(collection: ContentCollectionDefinition)
50
67
  // someone else's signature.
51
68
  const owner = store.scopeOf(event.user);
52
69
 
70
+ const body = sanitizeTemplateBody({
71
+ kind: collection.kind,
72
+ contentFormat: event.payload.contentFormat,
73
+ content: event.payload.content,
74
+ });
75
+ if (!body.ok) {
76
+ return writeFailure(
77
+ new ValidationError({ fields: documentViolationsToFieldIssues(body.violations) }),
78
+ );
79
+ }
80
+
53
81
  const existing = await fetchOne<CollectionEntryRow>(db, store.table, {
54
82
  tenantId,
55
83
  slug: event.payload.slug,
@@ -68,7 +96,7 @@ export function makeCollectionSetWrite(collection: ContentCollectionDefinition)
68
96
  // the template upserts put on the row.
69
97
  changes: {
70
98
  title: event.payload.title,
71
- content: event.payload.content,
99
+ content: body.content,
72
100
  contentFormat: event.payload.contentFormat,
73
101
  folder: event.payload.folder ?? null,
74
102
  },
@@ -91,7 +119,7 @@ export function makeCollectionSetWrite(collection: ContentCollectionDefinition)
91
119
  kind: collection.kind,
92
120
  locale: event.payload.locale,
93
121
  title: event.payload.title,
94
- content: event.payload.content,
122
+ content: body.content,
95
123
  contentFormat: event.payload.contentFormat,
96
124
  folder: event.payload.folder ?? null,
97
125
  ...store.createDefaults(tenantId),
@@ -0,0 +1,142 @@
1
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import {
3
+ defineWriteHandler,
4
+ type HandlerContext,
5
+ SYSTEM_TENANT_ID,
6
+ } from "@cosmicdrift/kumiko-framework/engine";
7
+ import {
8
+ AccessDeniedError,
9
+ NotFoundError,
10
+ ValidationError,
11
+ writeFailure,
12
+ } from "@cosmicdrift/kumiko-framework/errors";
13
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
14
+ import * as z from "zod";
15
+ import { requireForTenant } from "../../shared/index.js";
16
+ import { CONTENT_FORMATS } from "../constants.js";
17
+ import { parseLinkedResourceEntries } from "../linked-resources.js";
18
+ import { TemplateResolverErrors } from "../qualified-names.js";
19
+ import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
20
+ import { attachTemplateResources } from "../template-resources.js";
21
+ import { resolveTemplateVariables } from "../template-validation.js";
22
+ import { targetExistsIssue } from "./bundle-import.js";
23
+ import { kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
24
+ import { parseStoredJson, storedRecord } from "./template-gates.js";
25
+ import { refuseTenantScopeUnderSystemTenant, writeTemplateRow } from "./template-write.js";
26
+
27
+ type CopyScope = "tenant" | "system";
28
+ type WriteCtx = HandlerContext;
29
+
30
+ function readDbFor(ctx: WriteCtx, scope: CopyScope) {
31
+ return scope === "system" ? requireForTenant(ctx, SYSTEM_TENANT_ID).db : ctx.db;
32
+ }
33
+
34
+ // A tenant copy may start from the system default; a system copy only from system rows.
35
+ async function findCopySource(
36
+ ctx: WriteCtx,
37
+ where: {
38
+ scope: CopyScope;
39
+ tenantId: string;
40
+ slug: string;
41
+ kind: string;
42
+ locale: string;
43
+ },
44
+ ) {
45
+ const { scope, tenantId, slug, kind, locale } = where;
46
+ const readDb = readDbFor(ctx, scope);
47
+ const own = await fetchOne<TemplateResourceRow>(readDb, templateResourcesTable, {
48
+ tenantId,
49
+ slug,
50
+ kind,
51
+ locale,
52
+ });
53
+ if (own || scope === "system") return own;
54
+ return fetchOne<TemplateResourceRow>(readDb, templateResourcesTable, {
55
+ tenantId: SYSTEM_TENANT_ID,
56
+ slug,
57
+ kind,
58
+ locale,
59
+ status: "active",
60
+ });
61
+ }
62
+
63
+ export const copyLocaleWrite = defineWriteHandler({
64
+ name: "copy-locale",
65
+ schema: z.object({
66
+ slug: templateSlugSchema,
67
+ kind: kindSchema,
68
+ fromLocale: localeSchema,
69
+ toLocale: localeSchema,
70
+ overwrite: z.boolean().default(false),
71
+ /** "system" copies between system-default rows; SystemAdmin only. */
72
+ scope: z.enum(["tenant", "system"]).default("tenant"),
73
+ }),
74
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
75
+ description:
76
+ "Copies the text of one locale of a template into another locale as a draft override of the caller's tenant, taking the tenant's own row or else the system default as the source; use it as the starting point for a translation. It refuses an existing target unless overwrite is set. With scope system (SystemAdmin only) both source and target are system-default rows and the target lands as a system draft; any other caller gets a 403.",
77
+ handler: async (event, ctx) => {
78
+ const { slug, kind, fromLocale, toLocale, overwrite, scope } = event.payload;
79
+ if (scope === "system" && !event.user.roles.includes("SystemAdmin")) {
80
+ return writeFailure(
81
+ new AccessDeniedError({
82
+ i18nKey: "errors.access.denied",
83
+ details: { reason: TemplateResolverErrors.systemAdminRequired },
84
+ }),
85
+ );
86
+ }
87
+ const tenantId = scope === "system" ? SYSTEM_TENANT_ID : event.user.tenantId;
88
+ const refused = refuseTenantScopeUnderSystemTenant(scope, tenantId);
89
+ if (refused !== null) return refused;
90
+
91
+ const readDb = readDbFor(ctx, scope);
92
+ const source = await findCopySource(ctx, { scope, tenantId, slug, kind, locale: fromLocale });
93
+ if (!source || !isOneOf(CONTENT_FORMATS, source.contentFormat)) {
94
+ return writeFailure(new NotFoundError("template-resource", `${slug}/${fromLocale}`));
95
+ }
96
+
97
+ if (!overwrite) {
98
+ const target = await fetchOne<TemplateResourceRow>(readDb, templateResourcesTable, {
99
+ tenantId,
100
+ slug,
101
+ kind,
102
+ locale: toLocale,
103
+ });
104
+ if (target) {
105
+ return writeFailure(new ValidationError({ fields: [targetExistsIssue(toLocale)] }));
106
+ }
107
+ }
108
+
109
+ const variableSchema = storedRecord(source.variableSchema);
110
+ const written = await writeTemplateRow({
111
+ ctx,
112
+ user: event.user,
113
+ tenantId,
114
+ scope,
115
+ status: "draft",
116
+ variables: resolveTemplateVariables(ctx.registry, slug, variableSchema),
117
+ payload: {
118
+ slug,
119
+ kind,
120
+ locale: toLocale,
121
+ content: source.content,
122
+ contentFormat: source.contentFormat,
123
+ variableSchema,
124
+ meta: parseStoredJson(source.meta),
125
+ title: source.title,
126
+ folder: source.folder,
127
+ },
128
+ });
129
+ if (!written.isSuccess) return written;
130
+ // A system source carries no files of its own: its resource keys are known to the tenant row anyway.
131
+ if (source.tenantId === tenantId) {
132
+ const attached = await attachTemplateResources(ctx, {
133
+ user: event.user,
134
+ tenantId,
135
+ templateId: written.data.id,
136
+ resources: parseLinkedResourceEntries(source.linkedResources),
137
+ });
138
+ if (!attached.isSuccess) return attached;
139
+ }
140
+ return written;
141
+ },
142
+ });
@@ -0,0 +1,72 @@
1
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { NotFoundError } from "@cosmicdrift/kumiko-framework/errors";
4
+ import * as z from "zod";
5
+ import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
6
+ import { diffTemplateBlocks, splitTemplateBlocks } from "../template-diff.js";
7
+
8
+ function parseStoredMeta(raw: string | null): Readonly<Record<string, unknown>> {
9
+ if (raw === null) return {};
10
+ try {
11
+ const parsed: unknown = JSON.parse(raw);
12
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
13
+ ? Object.fromEntries(Object.entries(parsed))
14
+ : {};
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+
20
+ function diffMeta(
21
+ system: Readonly<Record<string, unknown>>,
22
+ tenant: Readonly<Record<string, unknown>>,
23
+ ) {
24
+ const keys = [...new Set([...Object.keys(system), ...Object.keys(tenant)])].sort();
25
+ return keys
26
+ .filter((key) => JSON.stringify(system[key]) !== JSON.stringify(tenant[key]))
27
+ .map((key) => ({ key, tenant: tenant[key], system: system[key] }));
28
+ }
29
+
30
+ export const diffToSystemQuery = defineQueryHandler({
31
+ name: "diff-to-system",
32
+ schema: z.object({ id: z.string().min(1) }),
33
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
34
+ description:
35
+ "Compares one tenant override with the system template of the same slug, kind and locale block by block and lists the changed meta settings; use it to show what an override changes before resetting it to the system version.",
36
+ handler: async (query, ctx) => {
37
+ const tenantRow = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
38
+ id: query.payload.id,
39
+ });
40
+ if (!tenantRow || tenantRow.tenantId !== query.user.tenantId || tenantRow.scope !== "tenant") {
41
+ throw new NotFoundError("template-resource", query.payload.id);
42
+ }
43
+ const systemRow = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
44
+ tenantId: SYSTEM_TENANT_ID,
45
+ slug: tenantRow.slug,
46
+ kind: tenantRow.kind,
47
+ locale: tenantRow.locale,
48
+ });
49
+ if (!systemRow) throw new NotFoundError("template-resource", tenantRow.slug);
50
+
51
+ const blocks = diffTemplateBlocks(
52
+ splitTemplateBlocks(systemRow.content ?? "", systemRow.contentFormat),
53
+ splitTemplateBlocks(tenantRow.content ?? "", tenantRow.contentFormat),
54
+ );
55
+ const meta = diffMeta(parseStoredMeta(systemRow.meta), parseStoredMeta(tenantRow.meta));
56
+ return {
57
+ tenant: {
58
+ templateId: String(tenantRow.id),
59
+ templateVersion: tenantRow.version,
60
+ contentFormat: tenantRow.contentFormat,
61
+ },
62
+ system: {
63
+ templateId: String(systemRow.id),
64
+ templateVersion: systemRow.version,
65
+ contentFormat: systemRow.contentFormat,
66
+ },
67
+ identical: meta.length === 0 && blocks.every((block) => block.change === "same"),
68
+ blocks,
69
+ meta,
70
+ };
71
+ },
72
+ });
@@ -0,0 +1,83 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { NotFoundError } from "@cosmicdrift/kumiko-framework/errors";
4
+ import * as z from "zod";
5
+ import {
6
+ isFileAttachmentEntry,
7
+ isPdfAttachmentLink,
8
+ type LinkedResourceEntry,
9
+ parseLinkedResourceEntries,
10
+ } from "../linked-resources.js";
11
+ import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
12
+ import type { TemplateBundle } from "../template-bundle.js";
13
+ import { readTemplateResource } from "../template-resources.js";
14
+ import { kindSchema, templateSlugSchema } from "./shared.js";
15
+ import { parseStoredJson, storedRecord } from "./template-gates.js";
16
+
17
+ export const exportBundleQuery = defineQueryHandler({
18
+ name: "export-bundle",
19
+ schema: z.object({
20
+ slug: templateSlugSchema,
21
+ kind: kindSchema,
22
+ scope: z.enum(["tenant", "system"]).default("tenant"),
23
+ }),
24
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
25
+ description:
26
+ "Exports every locale of one template (the caller's own rows, or the active system rows) together with its linked resource files as a self-contained bundle that import-bundle can read back, for example to move a template between tenants or environments.",
27
+ handler: async (query, ctx) => {
28
+ const { slug, kind, scope } = query.payload;
29
+ const found = await selectMany<TemplateResourceRow>(
30
+ ctx.db,
31
+ templateResourcesTable,
32
+ scope === "system"
33
+ ? { tenantId: SYSTEM_TENANT_ID, slug, kind, scope: "system", status: "active" }
34
+ : { tenantId: query.user.tenantId, slug, kind, scope: "tenant" },
35
+ );
36
+ const rows = found
37
+ .filter((row) => row.status !== "archived")
38
+ .sort((a, b) => a.locale.localeCompare(b.locale));
39
+ const first = rows[0];
40
+ if (!first) throw new NotFoundError("template-resource", slug);
41
+
42
+ const locales: Record<string, { body: string; meta?: unknown }> = {};
43
+ const entryByKey = new Map<string, LinkedResourceEntry>();
44
+ for (const row of rows) {
45
+ const meta = parseStoredJson(row.meta);
46
+ locales[row.locale] = { body: row.content ?? "", ...(meta === undefined ? {} : { meta }) };
47
+ for (const [key, entry] of Object.entries(parseLinkedResourceEntries(row.linkedResources))) {
48
+ if (!entryByKey.has(key)) entryByKey.set(key, entry);
49
+ }
50
+ }
51
+
52
+ const resources: TemplateBundle["resources"] = {};
53
+ const pdfAttachments: TemplateBundle["pdfAttachments"] = {};
54
+ for (const [key, entry] of entryByKey) {
55
+ if (isPdfAttachmentLink(entry)) {
56
+ pdfAttachments[key] = { templateSlug: entry.templateSlug, fileName: entry.fileName };
57
+ continue;
58
+ }
59
+ const attachment = isFileAttachmentEntry(entry);
60
+ const file = await readTemplateResource(
61
+ { db: ctx.db, files: ctx.files, user: query.user },
62
+ attachment ? entry.fileRefId : entry,
63
+ );
64
+ if (!file) throw new NotFoundError("template-resource-file", key);
65
+ resources[key] = {
66
+ fileName: attachment ? entry.fileName : file.fileName,
67
+ contentType: file.contentType,
68
+ base64: Buffer.from(file.bytes).toString("base64"),
69
+ ...(attachment && { purpose: "attachment" as const }),
70
+ };
71
+ }
72
+
73
+ return {
74
+ slug,
75
+ kind,
76
+ contentFormat: first.contentFormat,
77
+ variableSchema: storedRecord(first.variableSchema),
78
+ locales,
79
+ resources,
80
+ pdfAttachments,
81
+ };
82
+ },
83
+ });
@@ -6,7 +6,7 @@ import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
6
6
  // Admin-Lookup für UI-Edit-Flow. Returnt das raw Template inkl. draft/
7
7
  // archived Status. Tenant-Isolation: User sieht nur Templates des
8
8
  // eigenen Tenants oder SYSTEM_TENANT (system-defaults sind public-
9
- // readable). SystemAdmin sieht alle. Anonymous bleibt aus — Public-
9
+ // readable); ctx.db is tenant-scoped for every role, so the check below is defense in depth. Anonymous bleibt aus — Public-
10
10
  // Read geht über resolveTemplate aus der API, nicht hier.
11
11
  export const findByIdQuery = defineQueryHandler({
12
12
  name: "find-by-id",
@@ -19,10 +19,9 @@ export const findByIdQuery = defineQueryHandler({
19
19
  id: query.payload.id,
20
20
  });
21
21
  if (!row) return null;
22
- const isSystemAdmin = query.user.roles.includes("SystemAdmin");
23
22
  const isOwnTenant = row.tenantId === query.user.tenantId;
24
23
  const isSystemTemplate = row.scope === "system";
25
- if (!isSystemAdmin && !isOwnTenant && !isSystemTemplate) return null;
24
+ if (!isOwnTenant && !isSystemTemplate) return null;
26
25
 
27
26
  return {
28
27
  id: String(row.id),
@@ -35,6 +34,7 @@ export const findByIdQuery = defineQueryHandler({
35
34
  contentFormat: row.contentFormat,
36
35
  variableSchema: row.variableSchema,
37
36
  linkedResources: row.linkedResources,
37
+ meta: row.meta,
38
38
  scope: row.scope,
39
39
  parentTemplateId: row.parentTemplateId,
40
40
  status: row.status,
@@ -0,0 +1,42 @@
1
+ import { defineWriteHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
+ import * as z from "zod";
4
+ import { templateBundleSchema } from "../template-bundle.js";
5
+ import { importTemplateBundle } from "./bundle-import.js";
6
+
7
+ export const importBundleSystemWrite = defineWriteHandler({
8
+ name: "import-bundle-system",
9
+ schema: z.object({ bundle: templateBundleSchema }),
10
+ access: { roles: ["SystemAdmin"] },
11
+ // A system template change reaches every tenant without an override.
12
+ agent: { risk: "high" as const },
13
+ description:
14
+ "Imports a template bundle as system-default templates, active straight away and overwriting existing system rows; SystemAdmin only. Bundles with resource files need a caller in the system tenant, because the files are stored in the caller's file space.",
15
+ handler: (event, ctx) => {
16
+ const { bundle } = event.payload;
17
+ if (Object.keys(bundle.resources).length > 0 && event.user.tenantId !== SYSTEM_TENANT_ID) {
18
+ return Promise.resolve(
19
+ writeFailure(
20
+ new ValidationError({
21
+ fields: [
22
+ {
23
+ path: "bundle.resources",
24
+ code: "system_resources_need_system_tenant",
25
+ i18nKey: "templateResolver.errors.template.systemResourcesNeedSystemTenant",
26
+ },
27
+ ],
28
+ }),
29
+ ),
30
+ );
31
+ }
32
+ return importTemplateBundle({
33
+ ctx,
34
+ user: event.user,
35
+ tenantId: SYSTEM_TENANT_ID,
36
+ scope: "system",
37
+ status: "active",
38
+ bundle,
39
+ overwrite: true,
40
+ });
41
+ },
42
+ });
@@ -0,0 +1,22 @@
1
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import * as z from "zod";
3
+ import { templateBundleSchema } from "../template-bundle.js";
4
+ import { importTemplateBundle } from "./bundle-import.js";
5
+
6
+ export const importBundleWrite = defineWriteHandler({
7
+ name: "import-bundle",
8
+ schema: z.object({ bundle: templateBundleSchema, overwrite: z.boolean().default(false) }),
9
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
10
+ description:
11
+ "Imports a template bundle (all locales plus resource files, as produced by export-bundle) into the caller's tenant as drafts; it refuses a locale that already has an override unless overwrite is set, and publish makes the drafts live.",
12
+ handler: (event, ctx) =>
13
+ importTemplateBundle({
14
+ ctx,
15
+ user: event.user,
16
+ tenantId: event.user.tenantId,
17
+ scope: "tenant",
18
+ status: "draft",
19
+ bundle: event.payload.bundle,
20
+ overwrite: event.payload.overwrite,
21
+ }),
22
+ });
@@ -0,0 +1,108 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import * as z from "zod";
4
+ import { FALLBACK_LOCALE } from "../constants.js";
5
+ import { type LayoutRegions, readLayoutRegions } from "../layout-regions.js";
6
+ import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
7
+ import { localeSchema, templateSlugSchema } from "./shared.js";
8
+
9
+ export type LayoutSourceRow = {
10
+ readonly id: string;
11
+ readonly version: number;
12
+ readonly status: string;
13
+ readonly locale: string;
14
+ };
15
+
16
+ export type LayoutSourceResponse = {
17
+ readonly tenant: LayoutSourceRow | null;
18
+ readonly system: (LayoutSourceRow & { readonly contentFormat: string }) | null;
19
+ readonly regions: LayoutRegions;
20
+ readonly effectiveFormat: "html" | "mjml" | null;
21
+ readonly mjmlSource: string | null;
22
+ readonly locale: string;
23
+ };
24
+
25
+ const EMPTY_REGIONS: LayoutRegions = { header: "", signature: "", footer: "" };
26
+
27
+ function pickByLocale(
28
+ rows: readonly TemplateResourceRow[],
29
+ requested: string | undefined,
30
+ ): TemplateResourceRow | null {
31
+ const live = rows.filter((row) => row.status !== "archived");
32
+ const candidates = live.length > 0 ? live : rows;
33
+ return (
34
+ candidates.find((row) => row.locale === requested) ??
35
+ candidates.find((row) => row.locale === FALLBACK_LOCALE) ??
36
+ candidates[0] ??
37
+ null
38
+ );
39
+ }
40
+
41
+ function toSourceRow(row: TemplateResourceRow): LayoutSourceRow {
42
+ return { id: String(row.id), version: row.version, status: row.status, locale: row.locale };
43
+ }
44
+
45
+ function layoutFormat(row: TemplateResourceRow): "html" | "mjml" | null {
46
+ return row.contentFormat === "html" || row.contentFormat === "mjml" ? row.contentFormat : null;
47
+ }
48
+
49
+ function pickEditedRows(
50
+ rows: readonly TemplateResourceRow[],
51
+ isSystemAdmin: boolean,
52
+ locale: string | undefined,
53
+ ): {
54
+ readonly tenantRow: TemplateResourceRow | null;
55
+ readonly systemRow: TemplateResourceRow | null;
56
+ } {
57
+ const tenantRow = isSystemAdmin
58
+ ? null
59
+ : pickByLocale(
60
+ rows.filter((row) => row.scope === "tenant"),
61
+ locale,
62
+ );
63
+ const systemRow = pickByLocale(
64
+ rows.filter((row) => row.scope === "system"),
65
+ locale,
66
+ );
67
+ return { tenantRow, systemRow };
68
+ }
69
+
70
+ export const layoutSourceQuery = defineQueryHandler({
71
+ name: "layout-source",
72
+ schema: z.object({
73
+ kind: z.enum(["mail-layout", "pdf-layout"]),
74
+ slug: templateSlugSchema.default("default"),
75
+ locale: localeSchema.optional(),
76
+ }),
77
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
78
+ description:
79
+ "Reads the layout the caller edits: the tenant's own version and the system version of one layout slug, the header, signature and footer regions of the effective html version, and the locale a save has to use; a SystemAdmin edits the system version and also receives its mjml source. Use it to load the layout editor.",
80
+ handler: async (query, ctx): Promise<LayoutSourceResponse> => {
81
+ const { kind, slug, locale } = query.payload;
82
+ const isSystemAdmin = query.user.roles.includes("SystemAdmin");
83
+ const rows = await selectMany<TemplateResourceRow>(ctx.db, templateResourcesTable, {
84
+ kind,
85
+ slug,
86
+ });
87
+ const { tenantRow, systemRow } = pickEditedRows(rows, isSystemAdmin, locale);
88
+ const effective = tenantRow ?? systemRow;
89
+ const effectiveFormat = effective === null ? null : layoutFormat(effective);
90
+ return {
91
+ tenant: tenantRow === null ? null : toSourceRow(tenantRow),
92
+ system:
93
+ systemRow === null
94
+ ? null
95
+ : { ...toSourceRow(systemRow), contentFormat: systemRow.contentFormat },
96
+ regions:
97
+ effective !== null && effectiveFormat === "html"
98
+ ? readLayoutRegions(effective.content ?? "")
99
+ : EMPTY_REGIONS,
100
+ effectiveFormat,
101
+ mjmlSource:
102
+ isSystemAdmin && systemRow !== null && layoutFormat(systemRow) === "mjml"
103
+ ? (systemRow.content ?? "")
104
+ : null,
105
+ locale: effective?.locale ?? locale ?? FALLBACK_LOCALE,
106
+ };
107
+ },
108
+ });
@@ -4,9 +4,8 @@ import * as z from "zod";
4
4
  import { TEMPLATE_KINDS, TEMPLATE_STATUSES } from "../constants.js";
5
5
  import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
6
6
 
7
- // List für Admin-UI: filterbar nach kind / locale / status. Liefert
8
- // system-templates + tenant's eigene zusammen — Admin-UI rendert beide
9
- // Spalten mit scope-marker. SystemAdmin sieht alle (auch andere Tenants).
7
+ // ctx.db is tenant-scoped even for SystemAdmin: every caller sees its own
8
+ // tenant's rows plus the system defaults, never another tenant's rows.
10
9
  export const listQuery = defineQueryHandler({
11
10
  name: "list",
12
11
  schema: z.object({
@@ -21,10 +20,9 @@ export const listQuery = defineQueryHandler({
21
20
  handler: async (query, ctx) => {
22
21
  const where: Record<string, unknown> = {};
23
22
 
24
- // includeSystem=false narrows to own tenant AT THE DB — for non-SystemAdmin
25
- // TenantDb permits narrowing within its enforced [own, SYSTEM] scope, for
26
- // SystemAdmin (system-scoped db) the where applies verbatim. Filtering at
27
- // the DB keeps the limit meaningful (no post-filter starvation).
23
+ // includeSystem=false narrows to own tenant AT THE DB — TenantDb permits
24
+ // narrowing within its enforced [own, SYSTEM] scope. Filtering at the DB
25
+ // keeps the limit meaningful (no post-filter starvation).
28
26
  if (!query.payload.includeSystem) {
29
27
  where["tenantId"] = query.user.tenantId;
30
28
  }