@cosmicdrift/kumiko-bundled-features 0.374.0 → 0.378.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 (260) hide show
  1. package/dist/account-security/constants.d.ts +1 -0
  2. package/dist/account-security/constants.js +1 -0
  3. package/dist/account-security/index.d.ts +1 -1
  4. package/dist/account-security/index.js +1 -1
  5. package/dist/admin-shell/web/client-plugin.d.ts +1 -1
  6. package/dist/audit/web/client-plugin.d.ts +1 -1
  7. package/dist/auth-email-password/mail-notifications.js +1 -1
  8. package/dist/auth-email-password/web/auth-client.js +1 -1
  9. package/dist/auth-email-password/web/auth-form-primitives.js +1 -1
  10. package/dist/auth-email-password/web/default-topbar-actions.js +1 -1
  11. package/dist/auth-email-password/web/index.js +1 -1
  12. package/dist/auth-email-password/web/session.js +1 -1
  13. package/dist/auth-email-password/web/tenant-menu-items.js +1 -1
  14. package/dist/auth-email-password/web/tenant-switcher.js +1 -1
  15. package/dist/auth-email-password/web/user-menu.js +1 -1
  16. package/dist/auth-foundation/env.d.ts +9 -0
  17. package/dist/auth-foundation/env.js +21 -0
  18. package/dist/auth-foundation/index.d.ts +1 -0
  19. package/dist/auth-foundation/index.js +1 -0
  20. package/dist/auth-mfa/schema/user-mfa.d.ts +6 -6
  21. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +1 -1
  22. package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -1
  23. package/dist/billing-foundation/contact-card.d.ts +2 -0
  24. package/dist/billing-foundation/contact-card.js +16 -0
  25. package/dist/billing-foundation/entities.d.ts +8 -8
  26. package/dist/billing-foundation/handlers/billing-plans.query.js +1 -1
  27. package/dist/billing-foundation/index.d.ts +1 -1
  28. package/dist/billing-foundation/plan-catalog.js +1 -0
  29. package/dist/billing-foundation/projection.d.ts +8 -8
  30. package/dist/billing-foundation/types.d.ts +11 -0
  31. package/dist/billing-foundation/validate-options.js +13 -0
  32. package/dist/billing-foundation/web/billing-contact-plan.d.ts +5 -0
  33. package/dist/billing-foundation/web/billing-contact-plan.js +17 -0
  34. package/dist/billing-foundation/web/billing-plans-panel.js +5 -13
  35. package/dist/billing-foundation/web/cancel-contract-dialog.js +2 -2
  36. package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +1 -1
  37. package/dist/billing-foundation/web/checkout-consent-dialog.js +1 -0
  38. package/dist/billing-foundation/web/client-plugin.d.ts +1 -1
  39. package/dist/billing-foundation/web/localized-params.d.ts +1 -0
  40. package/dist/billing-foundation/web/localized-params.js +11 -0
  41. package/dist/cap-counter/entity.d.ts +1 -1
  42. package/dist/cap-counter/stock-cap-guard.js +8 -0
  43. package/dist/cap-overview/web/cap-cards-panel.js +3 -3
  44. package/dist/cap-overview/web/client-plugin.d.ts +1 -1
  45. package/dist/channel-email/email-channel.d.ts +2 -1
  46. package/dist/compliance-profiles/web/client-plugin.d.ts +1 -1
  47. package/dist/config/feature.js +2 -2
  48. package/dist/config/table.d.ts +6 -6
  49. package/dist/config/web/i18n.js +1 -0
  50. package/dist/custom-fields/entity.d.ts +4 -4
  51. package/dist/custom-fields/web/client-plugin.d.ts +1 -1
  52. package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -4
  53. package/dist/delivery/tables.d.ts +6 -6
  54. package/dist/delivery/unsubscribe-page.js +1 -1
  55. package/dist/delivery/web/client-plugin.d.ts +1 -1
  56. package/dist/delivery/web/delivery-status-cell.js +1 -1
  57. package/dist/document-ingest-foundation/entity.d.ts +4 -4
  58. package/dist/feature-toggles/web/client-plugin.d.ts +1 -1
  59. package/dist/file-provider-s3/env-named-feature.d.ts +17 -0
  60. package/dist/file-provider-s3/env-named-feature.js +23 -0
  61. package/dist/file-provider-s3/env-schema.d.ts +7 -0
  62. package/dist/file-provider-s3/env-schema.js +15 -10
  63. package/dist/file-provider-s3/index.d.ts +1 -0
  64. package/dist/file-provider-s3/index.js +1 -0
  65. package/dist/folders/entity.d.ts +8 -8
  66. package/dist/folders/web/client-plugin.d.ts +1 -1
  67. package/dist/form-draft/entity.d.ts +2 -2
  68. package/dist/host-routing/constants.d.ts +2 -0
  69. package/dist/host-routing/constants.js +3 -0
  70. package/dist/host-routing/dispatch.d.ts +1 -1
  71. package/dist/host-routing/dispatch.js +35 -25
  72. package/dist/host-routing/feature.js +100 -27
  73. package/dist/host-routing/host.d.ts +9 -0
  74. package/dist/host-routing/host.js +24 -2
  75. package/dist/host-routing/index.d.ts +2 -1
  76. package/dist/host-routing/index.js +1 -0
  77. package/dist/host-routing/options.d.ts +14 -0
  78. package/dist/host-routing/redirects.d.ts +6 -0
  79. package/dist/host-routing/redirects.js +27 -0
  80. package/dist/host-routing/tenant-lookup.d.ts +7 -0
  81. package/dist/host-routing/tenant-lookup.js +39 -0
  82. package/dist/host-routing/validate-options.js +34 -1
  83. package/dist/inbound-mail-foundation/entities.d.ts +25 -25
  84. package/dist/inbound-mail-foundation/feature.js +7 -0
  85. package/dist/inbound-mail-foundation/handlers/list-accounts.query.d.ts +1 -1
  86. package/dist/inbound-mail-foundation/handlers/list-accounts.query.js +11 -3
  87. package/dist/inbound-mail-foundation/i18n.d.ts +3 -0
  88. package/dist/inbound-mail-foundation/i18n.js +35 -0
  89. package/dist/inbound-mail-foundation/index.d.ts +1 -0
  90. package/dist/inbound-mail-foundation/index.js +1 -0
  91. package/dist/inbound-mail-foundation/projection.d.ts +20 -20
  92. package/dist/inbound-mail-foundation/screens.d.ts +7 -0
  93. package/dist/inbound-mail-foundation/screens.js +86 -0
  94. package/dist/inbound-mail-foundation/types.d.ts +2 -2
  95. package/dist/jobs/job-run-table.d.ts +6 -6
  96. package/dist/jobs/web/client-plugin.d.ts +1 -1
  97. package/dist/ledger/entity.d.ts +13 -13
  98. package/dist/legal-pages/feature.js +11 -1
  99. package/dist/legal-pages/layout.d.ts +2 -2
  100. package/dist/legal-pages/layout.js +4 -4
  101. package/dist/legal-pages/web/client-plugin.d.ts +1 -1
  102. package/dist/mail-foundation/feature.d.ts +20 -21
  103. package/dist/mail-foundation/feature.js +1 -1
  104. package/dist/mail-foundation/i18n.d.ts +5 -2
  105. package/dist/mail-foundation/i18n.js +3 -2
  106. package/dist/mail-transport-smtp/i18n.d.ts +1 -1
  107. package/dist/mail-transport-smtp/i18n.js +1 -1
  108. package/dist/managed-pages/table.d.ts +12 -12
  109. package/dist/managed-pages/web/client-plugin.d.ts +1 -1
  110. package/dist/notes-history/entity.d.ts +12 -12
  111. package/dist/notes-history/web/client-plugin.d.ts +1 -1
  112. package/dist/page-render/branding.js +1 -1
  113. package/dist/page-render/layout.d.ts +1 -1
  114. package/dist/page-render/layout.js +2 -2
  115. package/dist/page-render/markdown.js +1 -1
  116. package/dist/personal-access-tokens/i18n.js +1 -1
  117. package/dist/personal-access-tokens/schema/api-token.d.ts +6 -6
  118. package/dist/personal-access-tokens/screens.js +1 -1
  119. package/dist/secrets/feature.d.ts +1 -1
  120. package/dist/secrets/feature.js +3 -3
  121. package/dist/secrets/table.d.ts +1 -1
  122. package/dist/seo/sitemap.js +1 -1
  123. package/dist/sessions/schema/user-session.d.ts +4 -4
  124. package/dist/shared/mail-template-body.js +1 -1
  125. package/dist/shared/purge-files-reaching-subject.js +98 -3
  126. package/dist/tags/entity.d.ts +9 -9
  127. package/dist/tags/web/client-plugin.d.ts +1 -1
  128. package/dist/template-resolver/compile-document-body.d.ts +3 -0
  129. package/dist/template-resolver/compile-document-body.js +50 -3
  130. package/dist/template-resolver/constants.d.ts +3 -0
  131. package/dist/template-resolver/constants.js +2 -0
  132. package/dist/template-resolver/document-blocks.d.ts +5 -0
  133. package/dist/template-resolver/document-blocks.js +22 -0
  134. package/dist/template-resolver/document-images.d.ts +2 -0
  135. package/dist/template-resolver/document-images.js +28 -0
  136. package/dist/template-resolver/document-ops.d.ts +167 -0
  137. package/dist/template-resolver/document-ops.js +390 -0
  138. package/dist/template-resolver/document-sanitizer.d.ts +4 -1
  139. package/dist/template-resolver/document-sanitizer.js +26 -0
  140. package/dist/template-resolver/document-schema.d.ts +2 -1
  141. package/dist/template-resolver/document-schema.js +4 -0
  142. package/dist/template-resolver/feature.d.ts +11 -0
  143. package/dist/template-resolver/feature.js +60 -31
  144. package/dist/template-resolver/handlers/bundle-import.js +1 -0
  145. package/dist/template-resolver/handlers/collection-item.query.js +2 -1
  146. package/dist/template-resolver/handlers/collection-list.query.js +2 -1
  147. package/dist/template-resolver/handlers/collection-set.write.js +2 -2
  148. package/dist/template-resolver/handlers/collection-shared.d.ts +1 -2
  149. package/dist/template-resolver/handlers/collection-shared.js +0 -4
  150. package/dist/template-resolver/handlers/copy-locale.write.d.ts +2 -1
  151. package/dist/template-resolver/handlers/copy-locale.write.js +67 -63
  152. package/dist/template-resolver/handlers/discard-draft.write.d.ts +3 -2
  153. package/dist/template-resolver/handlers/discard-draft.write.js +18 -6
  154. package/dist/template-resolver/handlers/document-base.d.ts +23 -0
  155. package/dist/template-resolver/handlers/document-base.js +55 -0
  156. package/dist/template-resolver/handlers/document-blocks.query.d.ts +22 -0
  157. package/dist/template-resolver/handlers/document-blocks.query.js +46 -0
  158. package/dist/template-resolver/handlers/export-bundle.query.d.ts +1 -0
  159. package/dist/template-resolver/handlers/export-bundle.query.js +9 -2
  160. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +1 -0
  161. package/dist/template-resolver/handlers/import-bundle.write.d.ts +3 -1
  162. package/dist/template-resolver/handlers/import-bundle.write.js +30 -25
  163. package/dist/template-resolver/handlers/overview.query.d.ts +2 -1
  164. package/dist/template-resolver/handlers/overview.query.js +46 -42
  165. package/dist/template-resolver/handlers/patch-draft.write.d.ts +67 -0
  166. package/dist/template-resolver/handlers/patch-draft.write.js +111 -0
  167. package/dist/template-resolver/handlers/preview.query.d.ts +1 -0
  168. package/dist/template-resolver/handlers/preview.query.js +2 -1
  169. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +2 -1
  170. package/dist/template-resolver/handlers/reset-to-system.write.js +51 -44
  171. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +2 -1
  172. package/dist/template-resolver/handlers/resource-link-pdf.write.js +34 -28
  173. package/dist/template-resolver/handlers/resource-remove.write.d.ts +2 -1
  174. package/dist/template-resolver/handlers/resource-remove.write.js +39 -33
  175. package/dist/template-resolver/handlers/resource-upload.write.d.ts +2 -1
  176. package/dist/template-resolver/handlers/resource-upload.write.js +104 -96
  177. package/dist/template-resolver/handlers/send-test-shared.d.ts +5 -0
  178. package/dist/template-resolver/handlers/send-test-shared.js +19 -0
  179. package/dist/template-resolver/handlers/send-test-status.query.d.ts +9 -0
  180. package/dist/template-resolver/handlers/send-test-status.query.js +18 -0
  181. package/dist/template-resolver/handlers/send-test.write.d.ts +3 -1
  182. package/dist/template-resolver/handlers/send-test.write.js +87 -88
  183. package/dist/template-resolver/handlers/set.write.d.ts +2 -1
  184. package/dist/template-resolver/handlers/set.write.js +94 -90
  185. package/dist/template-resolver/handlers/template-write.d.ts +2 -0
  186. package/dist/template-resolver/handlers/template-write.js +14 -2
  187. package/dist/template-resolver/handlers/toggle-status.write.d.ts +6 -7
  188. package/dist/template-resolver/handlers/toggle-status.write.js +28 -10
  189. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +2 -1
  190. package/dist/template-resolver/handlers/upsert-tenant.write.js +30 -24
  191. package/dist/template-resolver/handlers/validate.query.d.ts +10 -4
  192. package/dist/template-resolver/handlers/validate.query.js +12 -7
  193. package/dist/template-resolver/index.d.ts +4 -2
  194. package/dist/template-resolver/index.js +3 -1
  195. package/dist/template-resolver/{web/placeholder-text.js → placeholder-text.js} +1 -1
  196. package/dist/template-resolver/qualified-names.d.ts +4 -0
  197. package/dist/template-resolver/qualified-names.js +4 -0
  198. package/dist/template-resolver/seeding.d.ts +1 -0
  199. package/dist/template-resolver/seeding.js +2 -1
  200. package/dist/template-resolver/table.d.ts +10 -10
  201. package/dist/template-resolver/template-bundle.d.ts +1 -0
  202. package/dist/template-resolver/template-bundle.js +6 -1
  203. package/dist/template-resolver/template-edit-policy.d.ts +22 -0
  204. package/dist/template-resolver/template-edit-policy.js +45 -0
  205. package/dist/template-resolver/template-preview.d.ts +4 -1
  206. package/dist/template-resolver/template-preview.js +23 -5
  207. package/dist/template-resolver/template-validation.d.ts +2 -1
  208. package/dist/template-resolver/template-validation.js +34 -14
  209. package/dist/template-resolver/user-content-table.d.ts +10 -10
  210. package/dist/template-resolver/web/client-plugin.d.ts +1 -1
  211. package/dist/template-resolver/web/client-plugin.js +3 -3
  212. package/dist/template-resolver/web/document-editor-extensions.d.ts +1 -0
  213. package/dist/template-resolver/web/document-editor-extensions.js +21 -1
  214. package/dist/template-resolver/web/document-tiptap-editor.d.ts +1 -1
  215. package/dist/template-resolver/web/document-tiptap-editor.js +23 -7
  216. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +4 -5
  217. package/dist/template-resolver/web/document-toolbar-blocks.js +1 -21
  218. package/dist/template-resolver/web/i18n.js +18 -0
  219. package/dist/template-resolver/web/placeholder-line-editor.js +2 -2
  220. package/dist/tenant/invitation-table.d.ts +6 -6
  221. package/dist/tenant/membership-table.d.ts +4 -4
  222. package/dist/tenant/schema/tenant.d.ts +6 -6
  223. package/dist/tenant/web/client-plugin.d.ts +1 -1
  224. package/dist/tenant/web/member-status-cell.js +1 -1
  225. package/dist/tier-engine/entity.d.ts +1 -1
  226. package/dist/tier-engine/tier-resolver.d.ts +1 -1
  227. package/dist/tier-engine/web/client-plugin.d.ts +1 -1
  228. package/dist/user/schema/user.d.ts +12 -12
  229. package/dist/user-data-rights/env.d.ts +5 -0
  230. package/dist/user-data-rights/env.js +11 -0
  231. package/dist/user-data-rights/feature.js +4 -2
  232. package/dist/user-data-rights/index.d.ts +1 -0
  233. package/dist/user-data-rights/index.js +1 -0
  234. package/dist/user-data-rights/lib/mail-transport-resolver.js +1 -1
  235. package/dist/user-data-rights/lib/storage-provider-resolver.js +1 -1
  236. package/dist/user-data-rights/mail-notifications.js +1 -1
  237. package/dist/user-data-rights/schema/download-attempt.d.ts +14 -14
  238. package/dist/user-data-rights/schema/download-token.d.ts +8 -8
  239. package/dist/user-data-rights/schema/export-job.d.ts +6 -6
  240. package/dist/user-data-rights/web/client-plugin.d.ts +1 -1
  241. package/dist/user-data-rights/web/privacy-center-screen.js +2 -2
  242. package/dist/waitlist/entity.d.ts +14 -14
  243. package/dist/waitlist/legacy-migrate.d.ts +2 -0
  244. package/dist/waitlist/legacy-migrate.js +38 -3
  245. package/package.json +16 -11
  246. package/src/account-security/changes.json +6 -0
  247. package/src/auth-foundation/changes.json +6 -0
  248. package/src/billing-foundation/changes.json +6 -0
  249. package/src/cap-counter/changes.json +12 -0
  250. package/src/channel-email/changes.json +6 -0
  251. package/src/crypto-shredding/changes.json +6 -0
  252. package/src/file-provider-s3/changes.json +6 -0
  253. package/src/host-routing/changes.json +6 -0
  254. package/src/inbound-mail-foundation/changes.json +13 -0
  255. package/src/legal-pages/changes.json +6 -0
  256. package/src/mail-foundation/changes.json +6 -0
  257. package/src/template-resolver/changes.json +38 -0
  258. package/src/user-data-rights/changes.json +6 -0
  259. package/src/waitlist/changes.json +6 -0
  260. /package/dist/template-resolver/{web/placeholder-text.d.ts → placeholder-text.d.ts} +0 -0
@@ -1,5 +1,6 @@
1
1
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import * as z from "zod";
3
+ import { refuseTemplateEdit, templateEditAccess, } from "../template-edit-policy.js";
3
4
  import { resolveTemplateVariables } from "../template-validation.js";
4
5
  import { upsertPayloadSchema } from "./shared.js";
5
6
  import { writeTemplateRow } from "./template-write.js";
@@ -8,27 +9,32 @@ import { writeTemplateRow } from "./template-write.js";
8
9
  // via publish-Handler. SystemAdmin kann via tenantIdOverride für
9
10
  // einen anderen Tenant schreiben (typisch: Plattform-Admin-UI das
10
11
  // Tenant-Templates kuratiert).
11
- export const upsertTenantWrite = defineWriteHandler({
12
- name: "upsert-tenant",
13
- schema: upsertPayloadSchema.extend({
14
- tenantIdOverride: z.string().min(1).optional(),
15
- status: z.enum(["draft", "active"]).default("draft"),
16
- }),
17
- access: { roles: ["TenantAdmin", "SystemAdmin"] },
18
- description: "Creates or overwrites a tenant-scoped template override with its variable schema, landing as a draft that only publish makes live (over an active override the draft is stored beside it, the active version keeps being served until publish); it refuses a system-tenant target, which is what upsert-system is for.",
19
- handler: async (event, ctx) => {
20
- // @cast-boundary engine-payload — override aus Zod-parsed string,
21
- // event.user.tenantId schon TenantId-branded; union als TenantId casten
22
- // ist legit (override ist UUID-Format-validiert in schema).
23
- const tenantId = (event.payload.tenantIdOverride ?? event.user.tenantId);
24
- return writeTemplateRow({
25
- ctx,
26
- user: event.user,
27
- tenantId,
28
- scope: "tenant",
29
- status: event.payload.status,
30
- variables: resolveTemplateVariables(ctx.registry, event.payload.slug, event.payload.variableSchema),
31
- payload: event.payload,
32
- });
33
- },
34
- });
12
+ export function makeUpsertTenantWrite(policy) {
13
+ return defineWriteHandler({
14
+ name: "upsert-tenant",
15
+ schema: upsertPayloadSchema.extend({
16
+ tenantIdOverride: z.string().min(1).optional(),
17
+ status: z.enum(["draft", "active"]).default("draft"),
18
+ }),
19
+ access: templateEditAccess(policy.tenantEditable),
20
+ description: "Creates or overwrites a tenant-scoped template override with its variable schema, landing as a draft that only publish makes live (over an active override the draft is stored beside it, the active version keeps being served until publish); it refuses a system-tenant target, which is what upsert-system is for; a TenantAdmin may write it only for templates the app lists in tenantEditable, otherwise SystemAdmin only.",
21
+ handler: async (event, ctx) => {
22
+ const refused = refuseTemplateEdit(policy, event.user, event.payload);
23
+ if (refused !== null)
24
+ return refused;
25
+ // @cast-boundary engine-payload — override is a zod-validated string and
26
+ // event.user.tenantId is already TenantId-branded; forTenant rejects the
27
+ // override for non-SystemAdmins.
28
+ const tenantId = (event.payload.tenantIdOverride ?? event.user.tenantId);
29
+ return writeTemplateRow({
30
+ ctx,
31
+ user: event.user,
32
+ tenantId,
33
+ scope: "tenant",
34
+ status: event.payload.status,
35
+ variables: resolveTemplateVariables(ctx.registry, event.payload.slug, event.payload.variableSchema),
36
+ payload: event.payload,
37
+ });
38
+ },
39
+ });
40
+ }
@@ -12,12 +12,18 @@ type TemplateDraftInput = {
12
12
  readonly locale?: string | undefined;
13
13
  readonly scope: TemplateWriteScope;
14
14
  };
15
+ export declare function knownResourceKeysFor(ctx: HandlerContext, user: SessionUser, where: {
16
+ readonly slug: string;
17
+ readonly kind: string;
18
+ readonly scope: TemplateWriteScope;
19
+ readonly locale: string | undefined;
20
+ }): Promise<string[]>;
15
21
  export declare function validateTemplateDraft(ctx: HandlerContext, user: SessionUser, payload: TemplateDraftInput): Promise<{
16
22
  response: {
17
23
  canSave: boolean;
18
24
  canPublish: boolean;
19
- issues: import("@cosmicdrift/kumiko-headless").FieldIssue[];
20
- publishIssues: import("@cosmicdrift/kumiko-headless").FieldIssue[];
25
+ issues: import("@cosmicdrift/kumiko-framework/errors").FieldIssue[];
26
+ publishIssues: import("@cosmicdrift/kumiko-framework/errors").FieldIssue[];
21
27
  used: string[];
22
28
  unknown: string[];
23
29
  missingRequired: string[];
@@ -58,8 +64,8 @@ export declare const validateQuery: import("@cosmicdrift/kumiko-types/define-han
58
64
  }, z.core.$strip>, {
59
65
  canSave: boolean;
60
66
  canPublish: boolean;
61
- issues: import("@cosmicdrift/kumiko-headless").FieldIssue[];
62
- publishIssues: import("@cosmicdrift/kumiko-headless").FieldIssue[];
67
+ issues: import("@cosmicdrift/kumiko-framework/errors").FieldIssue[];
68
+ publishIssues: import("@cosmicdrift/kumiko-framework/errors").FieldIssue[];
63
69
  used: string[];
64
70
  unknown: string[];
65
71
  missingRequired: string[];
@@ -18,13 +18,10 @@ async function knownResourceKeys(db, where) {
18
18
  });
19
19
  return rows.flatMap((row) => imageResourceKeysOf(row.linkedResources));
20
20
  }
21
- // The dry run of the write pipeline; preview feeds on the evaluation, validate returns the response.
22
- export async function validateTemplateDraft(ctx, user, payload) {
23
- const variables = resolveTemplateVariables(ctx.registry, payload.slug, payload.variableSchema);
24
- const { slug, kind, locale } = payload;
25
- // The dry run must not promise what the caller's write handlers would refuse.
26
- const scope = isSystemAdmin(user) ? payload.scope : "tenant";
27
- const known = [
21
+ // Image keys the editor may reference: the system rows plus, for a tenant write, the caller's own.
22
+ export async function knownResourceKeysFor(ctx, user, where) {
23
+ const { slug, kind, scope, locale } = where;
24
+ return [
28
25
  ...(await knownResourceKeys(ctx.db, {
29
26
  tenantId: SYSTEM_TENANT_ID,
30
27
  slug,
@@ -36,6 +33,14 @@ export async function validateTemplateDraft(ctx, user, payload) {
36
33
  ? await knownResourceKeys(ctx.db, { tenantId: user.tenantId, slug, kind, scope, locale })
37
34
  : []),
38
35
  ];
36
+ }
37
+ // The dry run of the write pipeline; preview feeds on the evaluation, validate returns the response.
38
+ export async function validateTemplateDraft(ctx, user, payload) {
39
+ const variables = resolveTemplateVariables(ctx.registry, payload.slug, payload.variableSchema);
40
+ const { slug, kind, locale } = payload;
41
+ // The dry run must not promise what the caller's write handlers would refuse.
42
+ const scope = isSystemAdmin(user) ? payload.scope : "tenant";
43
+ const known = await knownResourceKeysFor(ctx, user, { slug, kind, scope, locale });
39
44
  const evaluation = evaluateTemplateWrite({
40
45
  kind,
41
46
  scope,
@@ -1,9 +1,10 @@
1
+ export { isResourceImageContentType } from "../shared/index.ts";
1
2
  export { createTemplateResolverApi, type ResolveRequest, requireTemplateResolver, TemplateNotFoundError, type TemplateResolverApi, type TemplateResource, } from "./api.ts";
2
- export { type CompileDocumentBodyOptions, collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.ts";
3
+ export { type CompileDocumentBodyOptions, collectDocumentImageVariables, collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.ts";
3
4
  export { CONTENT_FORMATS, type ContentFormat, DEFAULT_MAIL_LAYOUT_SLUG, DEFAULT_PDF_LAYOUT_SLUG, DOCUMENT_HTML_KINDS, type DocumentHtmlKind, FALLBACK_LOCALE, NON_RENDER_KINDS, type NonRenderKind, PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, type PdfOrientation, type PdfPageFormat, RENDER_KINDS, type RenderKind, SYSTEM_TENANT_ID, TEMPLATE_KINDS, TEMPLATE_SCOPES, TEMPLATE_STATUSES, TEXT_BLOCK_KIND, type TemplateKind, type TemplateScope, type TemplateStatus, UPSERT_KINDS, type UpsertKind, } from "./constants.ts";
4
5
  export { type DocumentViolation, type DocumentViolationCode, documentViolationsToFieldIssues, flattenParagraphsToLineBreaks, type SanitizeBodyResult, type SanitizeHtmlResult, sanitizeDocumentHtml, sanitizeTemplateBody, } from "./document-sanitizer.ts";
5
6
  export { DOCUMENT_CONDITION_ATTRIBUTE, DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentCondition, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, type DocumentTextAlignment, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.ts";
6
- export { createTemplateResolverFeature } from "./feature.ts";
7
+ export { createTemplateResolverFeature, type TemplateResolverOptions } from "./feature.ts";
7
8
  export type { LayoutSourceResponse, LayoutSourceRow } from "./handlers/layout-source.query.ts";
8
9
  export type { OverviewKindGroup, OverviewOrigin, OverviewResponse, OverviewRow, OverviewUsage, } from "./handlers/overview.query.ts";
9
10
  export { localeSchema } from "./handlers/shared.ts";
@@ -16,6 +17,7 @@ export { mapRecordToVariables } from "./record-mapping.ts";
16
17
  export { type PreparedTemplateImage, prepareTemplateResourceBeforeTransaction, readTemplateResourceMaxBytes, type StoredTemplateResource, storePreparedTemplateResource, } from "./store-template-resource.ts";
17
18
  export { type TemplateResourceRow, templateResourceEntity, templateResourcesTable, } from "./table.ts";
18
19
  export { type TemplateBundle, templateBundleSchema } from "./template-bundle.ts";
20
+ export { type TemplateEditPolicy, type TenantEditableTemplate, templateEditAccess, } from "./template-edit-policy.ts";
19
21
  export { isMailTemplateMeta, type LayoutTemplateMeta, MAIL_CATEGORIES, MAIL_TONES, type MailCategory, type MailLayoutMeta, type MailTemplateMeta, type MailTone, type PdfTemplateMeta, parseTemplateMeta, pdfAttachmentFileNamePatternSchema, readMailLayoutMeta, readPdfTemplateMeta, type TemplateMeta, type TemplateMetaIssue, type TemplateMetaResult, templateSlugSchema, } from "./template-meta.ts";
20
22
  export { type DocumentDraftOutcome, evaluateDocumentDraft } from "./template-preview.ts";
21
23
  export { readTemplateResource, type TemplateResourceFile } from "./template-resources.ts";
@@ -1,5 +1,6 @@
1
+ export { isResourceImageContentType } from "../shared/index.js";
1
2
  export { createTemplateResolverApi, requireTemplateResolver, TemplateNotFoundError, } from "./api.js";
2
- export { collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.js";
3
+ export { collectDocumentImageVariables, collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.js";
3
4
  export { CONTENT_FORMATS, DEFAULT_MAIL_LAYOUT_SLUG, DEFAULT_PDF_LAYOUT_SLUG, DOCUMENT_HTML_KINDS, FALLBACK_LOCALE, NON_RENDER_KINDS, PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, RENDER_KINDS, SYSTEM_TENANT_ID, TEMPLATE_KINDS, TEMPLATE_SCOPES, TEMPLATE_STATUSES, TEXT_BLOCK_KIND, UPSERT_KINDS, } from "./constants.js";
4
5
  export { documentViolationsToFieldIssues, flattenParagraphsToLineBreaks, sanitizeDocumentHtml, sanitizeTemplateBody, } from "./document-sanitizer.js";
5
6
  export { DOCUMENT_CONDITION_ATTRIBUTE, DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentCondition, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
@@ -14,6 +15,7 @@ export { mapRecordToVariables } from "./record-mapping.js";
14
15
  export { prepareTemplateResourceBeforeTransaction, readTemplateResourceMaxBytes, storePreparedTemplateResource, } from "./store-template-resource.js";
15
16
  export { templateResourceEntity, templateResourcesTable, } from "./table.js";
16
17
  export { templateBundleSchema } from "./template-bundle.js";
18
+ export { templateEditAccess, } from "./template-edit-policy.js";
17
19
  export { isMailTemplateMeta, MAIL_CATEGORIES, MAIL_TONES, parseTemplateMeta, pdfAttachmentFileNamePatternSchema, readMailLayoutMeta, readPdfTemplateMeta, templateSlugSchema, } from "./template-meta.js";
18
20
  export { evaluateDocumentDraft } from "./template-preview.js";
19
21
  export { readTemplateResource } from "./template-resources.js";
@@ -1,4 +1,4 @@
1
- import { isDocumentReferenceName } from "../document-schema.js";
1
+ import { isDocumentReferenceName } from "./document-schema.js";
2
2
  const PLACEHOLDER_TOKEN = /\{\{([^{}]*)\}\}/g;
3
3
  // Names the schema refuses stay plain text, so a typo never becomes a chip
4
4
  // that the server would reject.
@@ -17,6 +17,7 @@ export declare const TemplateResolverHandlers: {
17
17
  readonly resourceRemove: "template-resolver:write:resource-remove";
18
18
  readonly resourceLinkPdf: "template-resolver:write:resource-link-pdf";
19
19
  readonly sendTest: "template-resolver:write:send-test";
20
+ readonly patchDraft: "template-resolver:write:patch-draft";
20
21
  };
21
22
  export declare const TemplateResolverQueries: {
22
23
  readonly findById: "template-resolver:query:find-by-id";
@@ -28,7 +29,9 @@ export declare const TemplateResolverQueries: {
28
29
  readonly diffToSystem: "template-resolver:query:diff-to-system";
29
30
  readonly validate: "template-resolver:query:validate";
30
31
  readonly preview: "template-resolver:query:preview";
32
+ readonly documentBlocks: "template-resolver:query:document-blocks";
31
33
  readonly exportBundle: "template-resolver:query:export-bundle";
34
+ readonly sendTestStatus: "template-resolver:query:send-test-status";
32
35
  readonly bySlug: "template-resolver:query:by-slug";
33
36
  readonly byTenant: "template-resolver:query:by-tenant";
34
37
  };
@@ -45,6 +48,7 @@ export declare const TemplateResolverErrors: {
45
48
  readonly targetExists: "target_exists";
46
49
  readonly systemResourcesNeedSystemTenant: "system_resources_need_system_tenant";
47
50
  readonly systemOnlyTemplate: "system_only_template";
51
+ readonly templateNotTenantEditable: "template_not_tenant_editable";
48
52
  readonly bundleLocalesDiffer: "bundle_locales_differ";
49
53
  readonly resourceTooLarge: "template_resource_too_large";
50
54
  readonly resourceUnsupportedImage: "template_resource_unsupported_image";
@@ -19,6 +19,7 @@ export const TemplateResolverHandlers = {
19
19
  resourceRemove: "template-resolver:write:resource-remove",
20
20
  resourceLinkPdf: "template-resolver:write:resource-link-pdf",
21
21
  sendTest: "template-resolver:write:send-test",
22
+ patchDraft: "template-resolver:write:patch-draft",
22
23
  };
23
24
  export const TemplateResolverQueries = {
24
25
  findById: "template-resolver:query:find-by-id",
@@ -30,7 +31,9 @@ export const TemplateResolverQueries = {
30
31
  diffToSystem: "template-resolver:query:diff-to-system",
31
32
  validate: "template-resolver:query:validate",
32
33
  preview: "template-resolver:query:preview",
34
+ documentBlocks: "template-resolver:query:document-blocks",
33
35
  exportBundle: "template-resolver:query:export-bundle",
36
+ sendTestStatus: "template-resolver:query:send-test-status",
34
37
  // Public pair — anonymous-reachable, kind pinned to text-block.
35
38
  bySlug: "template-resolver:query:by-slug",
36
39
  byTenant: "template-resolver:query:by-tenant",
@@ -56,6 +59,7 @@ export const TemplateResolverErrors = {
56
59
  targetExists: "target_exists",
57
60
  systemResourcesNeedSystemTenant: "system_resources_need_system_tenant",
58
61
  systemOnlyTemplate: "system_only_template",
62
+ templateNotTenantEditable: "template_not_tenant_editable",
59
63
  bundleLocalesDiffer: "bundle_locales_differ",
60
64
  resourceTooLarge: "template_resource_too_large",
61
65
  resourceUnsupportedImage: "template_resource_unsupported_image",
@@ -15,6 +15,7 @@ export type SeedSystemTemplateOptions = {
15
15
  * system admin uploaded through resource-upload. */
16
16
  readonly linkedResources?: Record<string, string>;
17
17
  readonly meta?: unknown;
18
+ readonly title?: string;
18
19
  readonly parentTemplateId?: string | null;
19
20
  readonly by?: SessionUser;
20
21
  readonly ifExists?: SeedIfExists;
@@ -81,7 +81,7 @@ export async function seedSystemTemplate(db, opts) {
81
81
  variableSchema,
82
82
  ...(linkedResources !== undefined && { linkedResources }),
83
83
  meta,
84
- title: null,
84
+ title: opts.title ?? null,
85
85
  folder: null,
86
86
  scope: "system",
87
87
  parentTemplateId,
@@ -171,6 +171,7 @@ export async function seedSystemTemplateBundle(db, bundle, opts = {}) {
171
171
  contentFormat: bundle.contentFormat,
172
172
  variableSchema: bundle.variableSchema ?? {},
173
173
  meta: entry.meta,
174
+ ...(entry.title !== undefined ? { title: entry.title } : {}),
174
175
  ...(opts.by !== undefined ? { by: opts.by } : {}),
175
176
  ...(opts.ifExists !== undefined ? { ifExists: opts.ifExists } : {}),
176
177
  });
@@ -14,7 +14,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
14
14
  readonly sensitive?: boolean;
15
15
  readonly writeOnly?: boolean;
16
16
  readonly readAsInstruction?: boolean;
17
- readonly format?: "email" | "url" | "phone" | "password";
17
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
18
18
  readonly default?: string;
19
19
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
20
20
  readonly multiline?: boolean | {
@@ -66,7 +66,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
66
66
  readonly sensitive?: boolean;
67
67
  readonly writeOnly?: boolean;
68
68
  readonly readAsInstruction?: boolean;
69
- readonly format?: "email" | "url" | "phone" | "password";
69
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
70
70
  readonly default?: string;
71
71
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
72
72
  readonly multiline?: boolean | {
@@ -89,7 +89,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
89
89
  readonly sensitive?: boolean;
90
90
  readonly writeOnly?: boolean;
91
91
  readonly readAsInstruction?: boolean;
92
- readonly format?: "email" | "url" | "phone" | "password";
92
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
93
93
  readonly default?: string;
94
94
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
95
95
  readonly multiline?: boolean | {
@@ -112,7 +112,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
112
112
  readonly sensitive?: boolean;
113
113
  readonly writeOnly?: boolean;
114
114
  readonly readAsInstruction?: boolean;
115
- readonly format?: "email" | "url" | "phone" | "password";
115
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
116
116
  readonly default?: string;
117
117
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
118
118
  readonly multiline?: boolean | {
@@ -245,7 +245,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
245
245
  readonly sensitive?: boolean;
246
246
  readonly writeOnly?: boolean;
247
247
  readonly readAsInstruction?: boolean;
248
- readonly format?: "email" | "url" | "phone" | "password";
248
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
249
249
  readonly default?: string;
250
250
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
251
251
  readonly multiline?: boolean | {
@@ -343,7 +343,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
343
343
  readonly sensitive?: boolean;
344
344
  readonly writeOnly?: boolean;
345
345
  readonly readAsInstruction?: boolean;
346
- readonly format?: "email" | "url" | "phone" | "password";
346
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
347
347
  readonly default?: string;
348
348
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
349
349
  readonly multiline?: boolean | {
@@ -395,7 +395,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
395
395
  readonly sensitive?: boolean;
396
396
  readonly writeOnly?: boolean;
397
397
  readonly readAsInstruction?: boolean;
398
- readonly format?: "email" | "url" | "phone" | "password";
398
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
399
399
  readonly default?: string;
400
400
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
401
401
  readonly multiline?: boolean | {
@@ -418,7 +418,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
418
418
  readonly sensitive?: boolean;
419
419
  readonly writeOnly?: boolean;
420
420
  readonly readAsInstruction?: boolean;
421
- readonly format?: "email" | "url" | "phone" | "password";
421
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
422
422
  readonly default?: string;
423
423
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
424
424
  readonly multiline?: boolean | {
@@ -441,7 +441,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
441
441
  readonly sensitive?: boolean;
442
442
  readonly writeOnly?: boolean;
443
443
  readonly readAsInstruction?: boolean;
444
- readonly format?: "email" | "url" | "phone" | "password";
444
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
445
445
  readonly default?: string;
446
446
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
447
447
  readonly multiline?: boolean | {
@@ -574,7 +574,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
574
574
  readonly sensitive?: boolean;
575
575
  readonly writeOnly?: boolean;
576
576
  readonly readAsInstruction?: boolean;
577
- readonly format?: "email" | "url" | "phone" | "password";
577
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
578
578
  readonly default?: string;
579
579
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
580
580
  readonly multiline?: boolean | {
@@ -19,6 +19,7 @@ export declare const templateBundleSchema: z.ZodObject<{
19
19
  locales: z.ZodRecord<z.ZodString, z.ZodObject<{
20
20
  body: z.ZodString;
21
21
  meta: z.ZodOptional<z.ZodUnknown>;
22
+ title: z.ZodOptional<z.ZodString>;
22
23
  }, z.core.$strict>>;
23
24
  resources: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
24
25
  fileName: z.ZodString;
@@ -33,7 +33,12 @@ const bundleResourceSchema = z
33
33
  .strict();
34
34
  const pdfAttachmentLinkBodySchema = templatePdfAttachmentLinkSchema.omit({ kind: true });
35
35
  const bundleLocaleEntrySchema = z
36
- .object({ body: z.string().max(200_000), meta: z.unknown().optional() })
36
+ .object({
37
+ body: z.string().max(200_000),
38
+ meta: z.unknown().optional(),
39
+ // Shown in the template overview instead of the slug; one per locale, so it is translated.
40
+ title: z.string().min(1).max(200).optional(),
41
+ })
37
42
  .strict();
38
43
  export const templateBundleSchema = z
39
44
  .object({
@@ -0,0 +1,22 @@
1
+ import { type HandlerContext, type RoleAccessRule, type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { type WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
3
+ import type { TemplateKind } from "./constants.ts";
4
+ export type TenantEditableTemplate = {
5
+ readonly kind: TemplateKind;
6
+ readonly slug: string;
7
+ readonly access: RoleAccessRule;
8
+ };
9
+ export type TemplateEditPolicy = {
10
+ readonly tenantEditable: readonly TenantEditableTemplate[];
11
+ };
12
+ type EditTarget = {
13
+ readonly slug: string;
14
+ readonly kind: string;
15
+ };
16
+ type EditUser = Pick<SessionUser, "roles">;
17
+ export declare function templateEditAccess(entries: readonly TenantEditableTemplate[]): RoleAccessRule;
18
+ export declare function mayEditTemplate(policy: TemplateEditPolicy, user: EditUser, target: EditTarget): boolean;
19
+ export declare function refuseTemplateEdit(policy: TemplateEditPolicy, user: EditUser, target: EditTarget): WriteFailure | null;
20
+ export declare function refuseMissingTemplate(policy: TemplateEditPolicy, user: EditUser): WriteFailure | null;
21
+ export declare function refuseTemplateEditById(db: HandlerContext["db"], policy: TemplateEditPolicy, user: EditUser, tenantId: string, templateId: string): Promise<WriteFailure | null>;
22
+ export {};
@@ -0,0 +1,45 @@
1
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
2
+ import { hasAccess, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { AccessDeniedError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
4
+ import { TemplateResolverErrors } from "./qualified-names.js";
5
+ import { templateResourcesTable } from "./table.js";
6
+ import { isSystemAdmin } from "./template-draft.js";
7
+ const SYSTEM_ADMIN_ROLE = "SystemAdmin";
8
+ export function templateEditAccess(entries) {
9
+ const roles = new Set([SYSTEM_ADMIN_ROLE]);
10
+ for (const entry of entries) {
11
+ for (const role of entry.access.roles)
12
+ roles.add(role);
13
+ }
14
+ return { roles: [...roles] };
15
+ }
16
+ export function mayEditTemplate(policy, user, target) {
17
+ if (isSystemAdmin(user))
18
+ return true;
19
+ const entry = policy.tenantEditable.find((candidate) => candidate.slug === target.slug && candidate.kind === target.kind);
20
+ return entry !== undefined && hasAccess(user, entry.access);
21
+ }
22
+ export function refuseTemplateEdit(policy, user, target) {
23
+ if (mayEditTemplate(policy, user, target))
24
+ return null;
25
+ return writeFailure(new AccessDeniedError({
26
+ i18nKey: "templateResolver.errors.templateNotTenantEditable",
27
+ details: { reason: TemplateResolverErrors.templateNotTenantEditable },
28
+ }));
29
+ }
30
+ // Fail closed: a missing row gives a non-SystemAdmin the same refusal as a locked one,
31
+ // so the response never reveals whether an id exists. SystemAdmin keeps the 404.
32
+ export function refuseMissingTemplate(policy, user) {
33
+ return isSystemAdmin(user) ? null : refuseTemplateEdit(policy, user, { slug: "", kind: "" });
34
+ }
35
+ export async function refuseTemplateEditById(db, policy, user, tenantId, templateId) {
36
+ if (isSystemAdmin(user))
37
+ return null;
38
+ const row = await fetchOne(db, templateResourcesTable, {
39
+ tenantId,
40
+ id: templateId,
41
+ });
42
+ return row === undefined
43
+ ? refuseMissingTemplate(policy, user)
44
+ : refuseTemplateEdit(policy, user, row);
45
+ }
@@ -1,4 +1,4 @@
1
- import type { HandlerContext, SessionUser, TemplateVariables } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { type HandlerContext, type SessionUser, type TemplateVariables } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { type KumikoError } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import { type MailAttachmentRef } from "../renderer-foundation/index.ts";
4
4
  import { type TemplateScope } from "./constants.ts";
@@ -41,6 +41,8 @@ export type TemplatePreviewInput = {
41
41
  readonly content?: string | undefined;
42
42
  readonly meta?: unknown;
43
43
  readonly variableSchema: Readonly<Record<string, unknown>>;
44
+ /** Replaces the declared example values for the named variables. */
45
+ readonly variables?: Readonly<Record<string, unknown>> | undefined;
44
46
  };
45
47
  type ValidationResponse = Awaited<ReturnType<typeof validateTemplateDraft>>["response"];
46
48
  export type TemplatePreview = {
@@ -59,6 +61,7 @@ export type TemplatePreview = {
59
61
  readonly scope: TemplateScope;
60
62
  readonly version: number;
61
63
  } | null;
64
+ /** The values the preview was actually rendered with: examples plus any overrides. */
62
65
  readonly sampleValues: Readonly<Record<string, unknown>>;
63
66
  readonly checks: readonly PreviewCheck[];
64
67
  readonly validation: ValidationResponse;
@@ -1,8 +1,10 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/db";
2
- import { NotFoundError, UnprocessableError, ValidationError, } from "@cosmicdrift/kumiko-framework/errors";
2
+ import { templateVariablesToZod, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { NotFoundError, UnprocessableError, ValidationError, validationErrorFromZod, } from "@cosmicdrift/kumiko-framework/errors";
3
4
  import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
4
5
  import { createI18n } from "@cosmicdrift/kumiko-framework/i18n";
5
- import { escapeHtml } from "@cosmicdrift/kumiko-headless";
6
+ import { escapeHtml } from "@cosmicdrift/kumiko-renderer/headless";
7
+ import * as z from "zod";
6
8
  import { createEmailAttachmentReader, createTemplateMailRenderer } from "../delivery/index.js";
7
9
  import { fileContextForTenant } from "../file-foundation/index.js";
8
10
  import { collectRendererPlugins, createRendererFoundationApi, RendererError, } from "../renderer-foundation/index.js";
@@ -62,12 +64,17 @@ function sampleList(def, labelText) {
62
64
  rows.push(sampleRow(def.item, labelText));
63
65
  return rows;
64
66
  }
67
+ function sampleValueFor(name, def, labelText) {
68
+ // An images variable has no sample: file ids are record data, never preview content.
69
+ if (def.type === "images")
70
+ return [];
71
+ return def.type === "list" ? sampleList(def, labelText) : sampleScalar(name, def, labelText);
72
+ }
65
73
  // Declared examples win; the fallbacks are deterministic so a preview never differs between calls.
66
74
  export function sampleValuesFor(variables, labelText = (label) => label) {
67
75
  const values = {};
68
76
  for (const [name, def] of Object.entries(variables)) {
69
- values[name] =
70
- def.type === "list" ? sampleList(def, labelText) : sampleScalar(name, def, labelText);
77
+ values[name] = sampleValueFor(name, def, labelText);
71
78
  }
72
79
  return values;
73
80
  }
@@ -413,7 +420,18 @@ export async function renderTemplatePreview(ctx, user, input) {
413
420
  return failed(new ValidationError({ fields: [...evaluation.issues] }));
414
421
  const content = evaluation.content ?? "";
415
422
  const meta = parseTemplateMeta(input.kind, parseStoredJson(evaluation.metaJson));
416
- const sampleValues = sampleValuesFor(validation.variables, labelTextFor(ctx, input.locale));
423
+ const exampleValues = sampleValuesFor(validation.variables, labelTextFor(ctx, input.locale));
424
+ let sampleValues = exampleValues;
425
+ if (input.variables !== undefined) {
426
+ const overrides = z.object({
427
+ variables: templateVariablesToZod(validation.variables).partial().strict(),
428
+ });
429
+ const checked = overrides.safeParse({ variables: input.variables });
430
+ if (!checked.success)
431
+ return failed(validationErrorFromZod(checked.error));
432
+ // Original values, not the parse result: parsing would strip keys inside list rows.
433
+ sampleValues = { ...exampleValues, ...input.variables };
434
+ }
417
435
  if (input.kind === "document-pdf") {
418
436
  return renderDocumentPreview(ctx, user, {
419
437
  input,
@@ -1,4 +1,4 @@
1
- import type { Registry, TemplateVariables } from "@cosmicdrift/kumiko-framework/engine";
1
+ import type { Registry, TemplateVariableDef, TemplateVariables } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import type { TemplateMeta } from "./template-meta.ts";
3
3
  export declare const SYSTEM_PLACEHOLDERS: ReadonlySet<string>;
4
4
  export declare function systemPlaceholdersFor(kind: string): ReadonlySet<string>;
@@ -9,6 +9,7 @@ export type TemplateValidation = {
9
9
  /** data-resource keys with no uploaded file; a warning, never a blocker. */
10
10
  readonly unknownResources: readonly string[];
11
11
  };
12
+ export declare function toVariableDef(raw: unknown): TemplateVariableDef | null;
12
13
  export declare function resolveTemplateVariables(registry: Registry, slug: string, variableSchema: Readonly<Record<string, unknown>>): TemplateVariables;
13
14
  export declare function variablesFromSchema(variableSchema: Readonly<Record<string, unknown>>): TemplateVariables;
14
15
  export declare function validateTemplatePlaceholders(input: {
@@ -38,6 +38,7 @@ const scalarVariableSchema = z.discriminatedUnion("type", [
38
38
  ]);
39
39
  const variableDefSchema = z.union([
40
40
  scalarVariableSchema,
41
+ z.object({ ...variableCommon, type: z.literal("images") }),
41
42
  z.object({
42
43
  ...variableCommon,
43
44
  type: z.literal("list"),
@@ -48,7 +49,7 @@ const variableDefSchema = z.union([
48
49
  // A stored variableSchema value is either a bare string (legacy "this is a text
49
50
  // variable") or a TemplateVariableDef object; anything else is not declared and
50
51
  // therefore shows up as an unknown placeholder instead of being guessed at.
51
- function toVariableDef(raw) {
52
+ export function toVariableDef(raw) {
52
53
  if (typeof raw === "string")
53
54
  return { type: "text" };
54
55
  const parsed = variableDefSchema.safeParse(raw);
@@ -74,7 +75,9 @@ function listItemFields(variables, listName) {
74
75
  const def = listName !== null && Object.hasOwn(variables, listName) ? variables[listName] : undefined;
75
76
  return def?.type === "list" ? def.item : {};
76
77
  }
77
- function isKnownName(name, scope, itemFields) {
78
+ // An images variable holds file ids: as text it would leak them, so only an images block
79
+ // (and a condition on its presence) may name it.
80
+ function isKnownName(name, scope, itemFields, allowImages = false) {
78
81
  const { variables } = scope;
79
82
  const root = rootName(name);
80
83
  // Declared variables and item fields are scalars: a dotted path under one never renders a value.
@@ -82,12 +85,15 @@ function isKnownName(name, scope, itemFields) {
82
85
  return { known: true, used: "" };
83
86
  if (scope.systemPlaceholders.has(root))
84
87
  return { known: true, used: "" };
85
- if (Object.hasOwn(variables, name))
86
- return { known: true, used: name };
88
+ if (Object.hasOwn(variables, name)) {
89
+ return variables[name]?.type === "images" && !allowImages
90
+ ? { known: false, used: "" }
91
+ : { known: true, used: name };
92
+ }
87
93
  return { known: false, used: "" };
88
94
  }
89
- function recordName(name, itemFields, scope) {
90
- const resolution = isKnownName(name, scope, itemFields);
95
+ function recordName(name, itemFields, scope, allowImages = false) {
96
+ const resolution = isKnownName(name, scope, itemFields, allowImages);
91
97
  if (!resolution.known)
92
98
  scope.unknown.add(name);
93
99
  else if (resolution.used !== "")
@@ -96,10 +102,21 @@ function recordName(name, itemFields, scope) {
96
102
  function enclosingEachName(element) {
97
103
  return element.closest("tr[data-each]")?.getAttribute("data-each") ?? null;
98
104
  }
99
- const NAME_ATTRIBUTE_BY_TAG = {
100
- span: "data-variable",
101
- div: "data-steps",
102
- };
105
+ function nameAttributeOf(element) {
106
+ const tag = element.tagName.toLowerCase();
107
+ if (tag === "span")
108
+ return "data-variable";
109
+ if (tag !== "div")
110
+ return undefined;
111
+ return element.hasAttribute("data-images") ? "data-images" : "data-steps";
112
+ }
113
+ function recordImagesName(name, scope) {
114
+ const def = Object.hasOwn(scope.variables, name) ? scope.variables[name] : undefined;
115
+ if (def?.type === "images")
116
+ scope.used.add(name);
117
+ else
118
+ scope.unknown.add(name);
119
+ }
103
120
  function scanRepeatRows(root, scope) {
104
121
  for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
105
122
  const listName = element.getAttribute("data-each") ?? "";
@@ -113,15 +130,18 @@ function scanRepeatRows(root, scope) {
113
130
  function scanConditions(root, scope) {
114
131
  for (const element of Array.from(root.querySelectorAll(`[${DOCUMENT_CONDITION_ATTRIBUTE}]`))) {
115
132
  const itemFields = listItemFields(scope.variables, enclosingEachName(conditionScopeElement(element)));
116
- recordName(element.getAttribute(DOCUMENT_CONDITION_ATTRIBUTE) ?? "", itemFields, scope);
133
+ recordName(element.getAttribute(DOCUMENT_CONDITION_ATTRIBUTE) ?? "", itemFields, scope, true);
117
134
  }
118
135
  }
119
136
  function scanReferences(root, scope) {
120
- for (const element of Array.from(root.querySelectorAll("span[data-variable], div[data-steps], a[href], img[data-resource]"))) {
137
+ for (const element of Array.from(root.querySelectorAll("span[data-variable], div[data-steps], div[data-images], a[href], img[data-resource]"))) {
121
138
  const itemFields = listItemFields(scope.variables, enclosingEachName(element));
122
139
  const tag = element.tagName.toLowerCase();
123
- const nameAttribute = NAME_ATTRIBUTE_BY_TAG[tag];
124
- if (nameAttribute !== undefined) {
140
+ const nameAttribute = nameAttributeOf(element);
141
+ if (nameAttribute === "data-images") {
142
+ recordImagesName(element.getAttribute(nameAttribute) ?? "", scope);
143
+ }
144
+ else if (nameAttribute !== undefined) {
125
145
  recordName(element.getAttribute(nameAttribute) ?? "", itemFields, scope);
126
146
  }
127
147
  else if (tag === "a") {