@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,16 +1,13 @@
1
1
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import * as z from "zod";
4
- import { collectChannels, createEmailAttachmentReader, EMAIL_CHANNEL_NAME, } from "../../delivery/index.js";
5
- import { fileContextForTenant } from "../../file-foundation/index.js";
6
4
  import { TEST_MAIL_SUBJECT_PREFIX } from "../constants.js";
7
5
  import { TemplateResolverErrors } from "../qualified-names.js";
6
+ import { refuseTemplateEdit, templateEditAccess, } from "../template-edit-policy.js";
8
7
  import { renderTemplatePreview } from "../template-preview.js";
8
+ import { emailChannelOf, resolveTestMailAddress, testMailChannelContext, } from "./send-test-shared.js";
9
9
  import { localeSchema, templateSlugSchema } from "./shared.js";
10
10
  const TEST_MAIL_RATE_LIMIT = { limit: 10, windowSeconds: 3600 };
11
- function emailChannelOf(registry) {
12
- return collectChannels(registry).find((channel) => channel.name === EMAIL_CHANNEL_NAME);
13
- }
14
11
  // Transport errors can carry provider responses or the address, so the client only gets a fixed code.
15
12
  function testMailFailed(code) {
16
13
  return writeFailure(new UnprocessableError(TemplateResolverErrors.testMailFailed, {
@@ -23,88 +20,90 @@ function errorCodeOf(error) {
23
20
  return typeof code === "string" ? code : undefined;
24
21
  }
25
22
  // The only recipient is the caller's own address, resolved server-side, so the handler can never mail a third party.
26
- export const sendTestWrite = defineWriteHandler({
27
- name: "send-test",
28
- schema: z.object({
29
- slug: templateSlugSchema,
30
- locale: localeSchema,
31
- content: z.string().max(200_000).optional(),
32
- meta: z.unknown().optional(),
33
- variableSchema: z.record(z.string(), z.unknown()).default({}),
34
- includeAttachments: z.boolean().default(true),
35
- prefixSubject: z.boolean().default(true),
36
- }),
37
- access: { roles: ["TenantAdmin", "SystemAdmin"] },
38
- rateLimit: { per: "user+handler", ...TEST_MAIL_RATE_LIMIT },
39
- description: "Sends the rendered preview of a mail template draft, or of the stored template when no content is given, to the calling user's own address with the subject prefixed by [TEST] unless prefixSubject is false, carrying the inline images and, unless includeAttachments is false, the files attached to the template; there is no recipient input, the sample values come from the declared variable examples, and a draft that fails the write rules is rejected without sending.",
40
- handler: async (event, ctx) => {
41
- const { includeAttachments, prefixSubject, ...previewPayload } = event.payload;
42
- const outcome = await renderTemplatePreview(ctx, event.user, {
43
- ...previewPayload,
44
- kind: "mail",
45
- });
46
- if (!outcome.ok)
47
- return writeFailure(outcome.error);
48
- const { preview, sender } = outcome;
49
- const mailAttachments = includeAttachments
50
- ? outcome.mailAttachments
51
- : outcome.mailAttachments.filter((ref) => ref.kind === "inline");
52
- const channel = emailChannelOf(ctx.registry);
53
- if (channel === undefined) {
54
- return writeFailure(new UnprocessableError(TemplateResolverErrors.testMailNoChannel, {
55
- i18nKey: "templateResolver.errors.testMail.noChannel",
56
- }));
57
- }
58
- const channelContext = {
59
- db: ctx.db,
60
- registry: ctx.registry,
61
- sseBroker: undefined,
62
- tenantId: event.user.tenantId,
63
- secrets: ctx.secrets,
64
- config: ctx.config,
65
- readAttachment: createEmailAttachmentReader(ctx.db, ctx.registry, event.user.tenantId, ctx.config, (tenantId) => fileContextForTenant(ctx, ctx.user.tenantId, tenantId)),
66
- };
67
- const address = (await channel.resolve?.(event.user.id, channelContext)) ?? null;
68
- if (address === null) {
69
- return writeFailure(new UnprocessableError(TemplateResolverErrors.testMailNoAddress, {
70
- i18nKey: "templateResolver.errors.testMail.noAddress",
71
- }));
72
- }
73
- const subject = `${prefixSubject ? TEST_MAIL_SUBJECT_PREFIX : ""}${preview.subject ?? ""}`;
74
- let sentStatus;
75
- try {
76
- const result = await channel.send(address, {
77
- // The template slug is the notification name, so the test mail takes the transport the real mail takes.
78
- notificationType: event.payload.slug,
79
- title: subject,
80
- body: undefined,
81
- data: undefined,
82
- locale: event.payload.locale,
83
- }, channelContext, {
84
- subject,
85
- html: preview.html,
86
- text: preview.text ?? "",
87
- ...sender,
88
- attachments: mailAttachments,
89
- });
90
- sentStatus = result.status;
91
- }
92
- catch (error) {
93
- // Class and code only: the message can carry the address or a provider response body.
94
- ctx.log?.warn("template test mail: channel threw", {
95
- tenantId: event.user.tenantId,
96
- errorClass: error instanceof Error ? error.name : typeof error,
97
- errorCode: errorCodeOf(error),
23
+ export function makeSendTestWrite(policy) {
24
+ return defineWriteHandler({
25
+ name: "send-test",
26
+ schema: z.object({
27
+ slug: templateSlugSchema,
28
+ locale: localeSchema,
29
+ content: z.string().max(200_000).optional(),
30
+ meta: z.unknown().optional(),
31
+ variableSchema: z.record(z.string(), z.unknown()).default({}),
32
+ variables: z.record(z.string(), z.unknown()).optional(),
33
+ includeAttachments: z.boolean().default(true),
34
+ prefixSubject: z.boolean().default(true),
35
+ }),
36
+ access: templateEditAccess(policy.tenantEditable),
37
+ rateLimit: { per: "user+handler", ...TEST_MAIL_RATE_LIMIT },
38
+ description: "Sends the rendered preview of a mail template draft, or of the stored template when no content is given, to the calling user's own address with the subject prefixed by [TEST] unless prefixSubject is false, carrying the inline images and, unless includeAttachments is false, the files attached to the template; there is no recipient input, the sample values come from the declared variable examples unless variables replaces them (checked against the variable schema), and a draft that fails the write rules is rejected without sending.",
39
+ handler: async (event, ctx) => {
40
+ // Sending a test is part of editing, so it follows the same edit policy as the editor writes.
41
+ const refused = refuseTemplateEdit(policy, event.user, {
42
+ slug: event.payload.slug,
43
+ kind: "mail",
98
44
  });
99
- return testMailFailed("channel_error");
100
- }
101
- if (sentStatus !== "sent") {
102
- ctx.log?.warn("template test mail: channel did not send", {
103
- tenantId: event.user.tenantId,
104
- status: sentStatus,
45
+ if (refused !== null)
46
+ return refused;
47
+ const { includeAttachments, prefixSubject, ...previewPayload } = event.payload;
48
+ const outcome = await renderTemplatePreview(ctx, event.user, {
49
+ ...previewPayload,
50
+ kind: "mail",
105
51
  });
106
- return testMailFailed("not_sent");
107
- }
108
- return { isSuccess: true, data: { subject, checks: preview.checks } };
109
- },
110
- });
52
+ if (!outcome.ok)
53
+ return writeFailure(outcome.error);
54
+ const { preview, sender } = outcome;
55
+ const mailAttachments = includeAttachments
56
+ ? outcome.mailAttachments
57
+ : outcome.mailAttachments.filter((ref) => ref.kind === "inline");
58
+ const channel = emailChannelOf(ctx.registry);
59
+ if (channel === undefined) {
60
+ return writeFailure(new UnprocessableError(TemplateResolverErrors.testMailNoChannel, {
61
+ i18nKey: "templateResolver.errors.testMail.noChannel",
62
+ }));
63
+ }
64
+ const channelContext = testMailChannelContext(ctx, event.user);
65
+ const address = await resolveTestMailAddress(channel, channelContext, event.user);
66
+ if (address === null) {
67
+ return writeFailure(new UnprocessableError(TemplateResolverErrors.testMailNoAddress, {
68
+ i18nKey: "templateResolver.errors.testMail.noAddress",
69
+ }));
70
+ }
71
+ const subject = `${prefixSubject ? TEST_MAIL_SUBJECT_PREFIX : ""}${preview.subject ?? ""}`;
72
+ let sentStatus;
73
+ try {
74
+ const result = await channel.send(address, {
75
+ // The template slug is the notification name, so the test mail takes the transport the real mail takes.
76
+ notificationType: event.payload.slug,
77
+ title: subject,
78
+ body: undefined,
79
+ data: undefined,
80
+ locale: event.payload.locale,
81
+ }, channelContext, {
82
+ subject,
83
+ html: preview.html,
84
+ text: preview.text ?? "",
85
+ ...sender,
86
+ attachments: mailAttachments,
87
+ });
88
+ sentStatus = result.status;
89
+ }
90
+ catch (error) {
91
+ // Class and code only: the message can carry the address or a provider response body.
92
+ ctx.log?.warn("template test mail: channel threw", {
93
+ tenantId: event.user.tenantId,
94
+ errorClass: error instanceof Error ? error.name : typeof error,
95
+ errorCode: errorCodeOf(error),
96
+ });
97
+ return testMailFailed("channel_error");
98
+ }
99
+ if (sentStatus !== "sent") {
100
+ ctx.log?.warn("template test mail: channel did not send", {
101
+ tenantId: event.user.tenantId,
102
+ status: sentStatus,
103
+ });
104
+ return testMailFailed("not_sent");
105
+ }
106
+ return { isSuccess: true, data: { subject, checks: preview.checks } };
107
+ },
108
+ });
109
+ }
@@ -1,5 +1,6 @@
1
1
  import * as z from "zod";
2
- export declare const setWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"set", z.ZodObject<{
2
+ import { type TemplateEditPolicy } from "../template-edit-policy.ts";
3
+ export declare function makeSetWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"set", z.ZodObject<{
3
4
  slug: z.ZodString;
4
5
  locale: z.ZodString;
5
6
  kind: z.ZodOptional<z.ZodEnum<{
@@ -5,6 +5,7 @@ import * as z from "zod";
5
5
  import { requireForTenant } from "../../shared/index.js";
6
6
  import { TEMPLATE_KINDS, TEXT_BLOCK_KIND, UPSERT_KINDS } from "../constants.js";
7
7
  import { templateResourcesTable } from "../table.js";
8
+ import { refuseTemplateEdit, templateEditAccess, } from "../template-edit-policy.js";
8
9
  import { resolveTemplateVariables } from "../template-validation.js";
9
10
  import { contentFormatSchema, executor, folderSchema, localeSchema, slugSchema } from "./shared.js";
10
11
  import { parseStoredJson, storedRecord } from "./template-gates.js";
@@ -28,100 +29,103 @@ import { gateTemplateWrite } from "./template-write.js";
28
29
  //
29
30
  // Kinds that upsertSystem/upsertTenant also write run the same validation
30
31
  // pipeline here, and because saving publishes, publish issues reject too.
31
- export const setWrite = defineWriteHandler({
32
- name: "set",
33
- schema: z.object({
34
- slug: slugSchema,
35
- locale: localeSchema,
36
- /** Which kind to author. Defaults to text-block. */
37
- kind: z.enum(TEMPLATE_KINDS).optional(),
38
- title: z.string().min(1).max(200),
39
- content: z.string().max(200_000).nullable(),
40
- contentFormat: contentFormatSchema.default("markdown"),
41
- /** Folder path for the content tree. Optional + null = root node. */
42
- folder: folderSchema.nullable().optional(),
43
- /** Optional cross-tenant write — SystemAdmin only. Typical use: the
44
- * editor lets a SystemAdmin write onto SYSTEM_TENANT_ID, otherwise the
45
- * text lands on their own platform tenant and the legal routes miss it. */
46
- tenantIdOverride: z.string().min(1).optional(),
47
- }),
48
- // SystemAdmin is a global role, TenantAdmin is per tenant-membership — both
49
- // paths are needed explicitly, otherwise nobody can set SYSTEM_TENANT texts.
50
- access: { roles: ["TenantAdmin", "SystemAdmin"] },
51
- description: "Creates or overwrites one content-tree resource addressed by slug, kind and locale, carrying a title and folder and going live on save with no draft stage; a SystemAdmin may target the system tenant, where shared legal and marketing copy lives.",
52
- handler: async (event, ctx) => {
53
- const override = event.payload.tenantIdOverride;
54
- // @cast-boundary engine-payload — override is a zod-validated string, the
55
- // user's own tenantId is already TenantId-branded.
56
- const tenantId = (override ?? event.user.tenantId);
57
- const { db, streamTenantId } = requireForTenant(ctx, tenantId);
58
- const kind = event.payload.kind ?? TEXT_BLOCK_KIND;
59
- const existing = await fetchOne(db, templateResourcesTable, {
60
- tenantId,
61
- slug: event.payload.slug,
62
- kind,
63
- locale: event.payload.locale,
64
- });
65
- const scope = tenantId === SYSTEM_TENANT_ID ? "system" : "tenant";
66
- let content = event.payload.content;
67
- if (isOneOf(UPSERT_KINDS, kind)) {
68
- const gated = gateTemplateWrite({
32
+ export function makeSetWrite(policy) {
33
+ return defineWriteHandler({
34
+ name: "set",
35
+ schema: z.object({
36
+ slug: slugSchema,
37
+ locale: localeSchema,
38
+ /** Which kind to author. Defaults to text-block. */
39
+ kind: z.enum(TEMPLATE_KINDS).optional(),
40
+ title: z.string().min(1).max(200),
41
+ content: z.string().max(200_000).nullable(),
42
+ contentFormat: contentFormatSchema.default("markdown"),
43
+ /** Folder path for the content tree. Optional + null = root node. */
44
+ folder: folderSchema.nullable().optional(),
45
+ /** Optional cross-tenant write — SystemAdmin only. Typical use: the
46
+ * editor lets a SystemAdmin write onto SYSTEM_TENANT_ID, otherwise the
47
+ * text lands on their own platform tenant and the legal routes miss it. */
48
+ tenantIdOverride: z.string().min(1).optional(),
49
+ }),
50
+ access: templateEditAccess(policy.tenantEditable),
51
+ description: "Creates or overwrites one content-tree resource addressed by slug, kind and locale, carrying a title and folder and going live on save with no draft stage; a SystemAdmin may target the system tenant, where shared legal and marketing copy lives. A TenantAdmin may write only the slug and kind pairs the app lists in tenantEditable, otherwise SystemAdmin only.",
52
+ handler: async (event, ctx) => {
53
+ const kind = event.payload.kind ?? TEXT_BLOCK_KIND;
54
+ const refused = refuseTemplateEdit(policy, event.user, { slug: event.payload.slug, kind });
55
+ if (refused !== null)
56
+ return refused;
57
+ const override = event.payload.tenantIdOverride;
58
+ // @cast-boundary engine-payload — override is a zod-validated string, the
59
+ // user's own tenantId is already TenantId-branded.
60
+ const tenantId = (override ?? event.user.tenantId);
61
+ const { db, streamTenantId } = requireForTenant(ctx, tenantId);
62
+ const existing = await fetchOne(db, templateResourcesTable, {
63
+ tenantId,
64
+ slug: event.payload.slug,
69
65
  kind,
70
- scope,
71
- contentFormat: event.payload.contentFormat,
66
+ locale: event.payload.locale,
67
+ });
68
+ const scope = tenantId === SYSTEM_TENANT_ID ? "system" : "tenant";
69
+ let content = event.payload.content;
70
+ if (isOneOf(UPSERT_KINDS, kind)) {
71
+ const gated = gateTemplateWrite({
72
+ kind,
73
+ scope,
74
+ contentFormat: event.payload.contentFormat,
75
+ content,
76
+ rawMeta: parseStoredJson(existing?.meta ?? null),
77
+ variables: resolveTemplateVariables(ctx.registry, event.payload.slug, storedRecord(existing?.variableSchema ?? null)),
78
+ }, "active");
79
+ if (!gated.ok)
80
+ return gated.failure;
81
+ content = gated.evaluated.content;
82
+ }
83
+ const fields = {
84
+ slug: event.payload.slug,
85
+ kind,
86
+ locale: event.payload.locale,
87
+ title: event.payload.title,
72
88
  content,
73
- rawMeta: parseStoredJson(existing?.meta ?? null),
74
- variables: resolveTemplateVariables(ctx.registry, event.payload.slug, storedRecord(existing?.variableSchema ?? null)),
75
- }, "active");
76
- if (!gated.ok)
77
- return gated.failure;
78
- content = gated.evaluated.content;
79
- }
80
- const fields = {
81
- slug: event.payload.slug,
82
- kind,
83
- locale: event.payload.locale,
84
- title: event.payload.title,
85
- content,
86
- contentFormat: event.payload.contentFormat,
87
- folder: event.payload.folder ?? null,
88
- variableSchema: "{}",
89
- linkedResources: "{}",
90
- scope,
91
- parentTemplateId: null,
92
- // Text blocks have no draft stage — saving publishes, so resolveTemplate
93
- // (which only returns active rows) finds them.
94
- status: "active",
95
- };
96
- if (existing) {
97
- const result = await executor.update({
98
- id: existing.id,
99
- version: existing.version,
100
- // Only the editable columns — slug/kind/locale are the unique key,
101
- // and rewriting scope/status/variableSchema here would reset whatever
102
- // the template upserts put on the row.
103
- changes: {
104
- title: fields.title,
105
- content: fields.content,
106
- contentFormat: fields.contentFormat,
107
- folder: fields.folder,
108
- },
109
- }, event.user, db, { streamTenantId });
89
+ contentFormat: event.payload.contentFormat,
90
+ folder: event.payload.folder ?? null,
91
+ variableSchema: "{}",
92
+ linkedResources: "{}",
93
+ scope,
94
+ parentTemplateId: null,
95
+ // Text blocks have no draft stage — saving publishes, so resolveTemplate
96
+ // (which only returns active rows) finds them.
97
+ status: "active",
98
+ };
99
+ if (existing) {
100
+ const result = await executor.update({
101
+ id: existing.id,
102
+ version: existing.version,
103
+ // Only the editable columns — slug/kind/locale are the unique key,
104
+ // and rewriting scope/status/variableSchema here would reset whatever
105
+ // the template upserts put on the row.
106
+ changes: {
107
+ title: fields.title,
108
+ content: fields.content,
109
+ contentFormat: fields.contentFormat,
110
+ folder: fields.folder,
111
+ },
112
+ }, event.user, db, { streamTenantId });
113
+ if (!result.isSuccess)
114
+ return result;
115
+ return {
116
+ isSuccess: true,
117
+ data: { slug: event.payload.slug, locale: event.payload.locale, isNew: false },
118
+ };
119
+ }
120
+ const result = await executor.create({ ...fields, tenantId }, event.user, db, {
121
+ streamTenantId,
122
+ });
110
123
  if (!result.isSuccess)
111
124
  return result;
112
125
  return {
113
126
  isSuccess: true,
114
- data: { slug: event.payload.slug, locale: event.payload.locale, isNew: false },
127
+ data: { slug: event.payload.slug, locale: event.payload.locale, isNew: true },
115
128
  };
116
- }
117
- const result = await executor.create({ ...fields, tenantId }, event.user, db, {
118
- streamTenantId,
119
- });
120
- if (!result.isSuccess)
121
- return result;
122
- return {
123
- isSuccess: true,
124
- data: { slug: event.payload.slug, locale: event.payload.locale, isNew: true },
125
- };
126
- },
127
- });
129
+ },
130
+ });
131
+ }
@@ -30,6 +30,8 @@ export type TemplateRowWrite = {
30
30
  readonly status: "draft" | "active";
31
31
  readonly variables: TemplateVariables;
32
32
  readonly payload: TemplateRowPayload;
33
+ /** The row version the edit was made against; null means no own row may exist yet. */
34
+ readonly expectedVersion?: number | null;
33
35
  };
34
36
  export declare function refuseTenantScopeUnderSystemTenant(scope: TemplateWriteScope, tenantId: string): WriteResult<never> | null;
35
37
  export declare function refuseSystemOnlyTenantOverride(registry: Registry, scope: TemplateWriteScope, slug: string, kind: string): WriteFailure | null;
@@ -1,6 +1,6 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
3
- import { AccessDeniedError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { AccessDeniedError, ValidationError, VersionConflictError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
4
4
  import { requireForTenant } from "../../shared/index.js";
5
5
  import { TemplateResolverErrors } from "../qualified-names.js";
6
6
  import { templateResourcesTable } from "../table.js";
@@ -67,6 +67,15 @@ function updateChanges(existing, status, fields) {
67
67
  }
68
68
  : { draft };
69
69
  }
70
+ function versionConflictFor(expectedVersion, existing, slug) {
71
+ if (expectedVersion === undefined || (existing?.version ?? null) === expectedVersion)
72
+ return null;
73
+ return writeFailure(new VersionConflictError({
74
+ entityId: existing?.id ?? slug,
75
+ expectedVersion: expectedVersion ?? 0,
76
+ currentVersion: existing?.version ?? 0,
77
+ }));
78
+ }
70
79
  // Shared by every template write: validate through the one pipeline, then
71
80
  // create or update the (tenant, slug, kind, locale) row via the event-store
72
81
  // executor. linkedResources is never part of an update; resources are attached
@@ -109,10 +118,13 @@ export async function writeTemplateRow(args) {
109
118
  parentTemplateId: payload.parentTemplateId ?? null,
110
119
  status,
111
120
  };
121
+ const conflict = versionConflictFor(args.expectedVersion, existing, payload.slug);
122
+ if (conflict !== null)
123
+ return conflict;
112
124
  if (existing) {
113
125
  const result = await executor.update({
114
126
  id: existing.id,
115
- version: existing.version,
127
+ version: args.expectedVersion ?? existing.version,
116
128
  changes: updateChanges(existing, status, fields),
117
129
  }, user, db, { streamTenantId });
118
130
  if (!result.isSuccess)
@@ -1,11 +1,10 @@
1
1
  import { type HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import * as z from "zod";
3
3
  import type { TemplateResourceRow } from "../table.ts";
4
+ import { type TemplateEditPolicy } from "../template-edit-policy.ts";
4
5
  type TemplateStatus = "active" | "archived";
5
6
  export type StatusTarget = "own" | "system";
6
- export declare function writeAccessFor(target: StatusTarget): {
7
- roles: string[];
8
- };
7
+ export declare function writeAccessFor(target: StatusTarget, policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/handlers").RoleAccessRule;
9
8
  export declare function loadTargetRow(ctx: HandlerContext, target: StatusTarget, user: {
10
9
  readonly tenantId: string;
11
10
  }, id: string): Promise<{
@@ -13,25 +12,25 @@ export declare function loadTargetRow(ctx: HandlerContext, target: StatusTarget,
13
12
  scoped: import("@cosmicdrift/kumiko-types/handlers").TenantWriteTarget | null;
14
13
  row: TemplateResourceRow | null;
15
14
  }>;
16
- export declare const archiveWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
15
+ export declare function makeArchiveWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
17
16
  id: z.ZodString;
18
17
  }, z.core.$strip>, {
19
18
  id: string;
20
19
  status: TemplateStatus;
21
20
  }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
22
- export declare const publishWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
21
+ export declare function makePublishWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
23
22
  id: z.ZodString;
24
23
  }, z.core.$strip>, {
25
24
  id: string;
26
25
  status: TemplateStatus;
27
26
  }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
28
- export declare const archiveSystemWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
27
+ export declare function makeArchiveSystemWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
29
28
  id: z.ZodString;
30
29
  }, z.core.$strip>, {
31
30
  id: string;
32
31
  status: TemplateStatus;
33
32
  }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
34
- export declare const publishSystemWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
33
+ export declare function makePublishSystemWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
35
34
  id: z.ZodString;
36
35
  }, z.core.$strip>, {
37
36
  id: string;
@@ -5,6 +5,7 @@ import * as z from "zod";
5
5
  import { requireForTenant } from "../../shared/index.js";
6
6
  import { templateResourcesTable } from "../table.js";
7
7
  import { activationChanges, parseTemplateDraft, toWorkingRow } from "../template-draft.js";
8
+ import { refuseMissingTemplate, refuseTemplateEdit, templateEditAccess, } from "../template-edit-policy.js";
8
9
  import { executor } from "./shared.js";
9
10
  import { publishGateIssues } from "./template-gates.js";
10
11
  function isRowOfTarget(row, target, callerTenantId) {
@@ -12,8 +13,10 @@ function isRowOfTarget(row, target, callerTenantId) {
12
13
  ? row.tenantId === SYSTEM_TENANT_ID && row.scope === "system"
13
14
  : row.tenantId === callerTenantId && row.scope === "tenant";
14
15
  }
15
- export function writeAccessFor(target) {
16
- return { roles: target === "system" ? ["SystemAdmin"] : ["TenantAdmin", "SystemAdmin"] };
16
+ export function writeAccessFor(target, policy) {
17
+ return target === "system"
18
+ ? { roles: ["SystemAdmin"] }
19
+ : templateEditAccess(policy.tenantEditable);
17
20
  }
18
21
  // The tenant db also reads system rows; the row's own tenant must match the
19
22
  // target or the executor would append to a foreign event stream.
@@ -34,18 +37,25 @@ function planStatusChange(ctx, row, status) {
34
37
  }
35
38
  return { ok: true, changes: { ...parseTemplateDraft(row.draft), ...activationChanges(row) } };
36
39
  }
37
- function createStatusUpdateHandler(name, target, status, description) {
40
+ function createStatusUpdateHandler(policy, name, target, status, description) {
38
41
  return defineWriteHandler({
39
42
  name,
40
43
  schema: z.object({ id: z.string().min(1) }),
41
- access: writeAccessFor(target),
44
+ access: writeAccessFor(target, policy),
42
45
  // A system template change reaches every tenant without an override.
43
46
  ...(target === "system" ? { agent: { risk: "high" } } : {}),
44
47
  description,
45
48
  handler: async (event, ctx) => {
46
49
  const { db, scoped, row } = await loadTargetRow(ctx, target, event.user, event.payload.id);
47
- if (!row)
48
- return writeFailure(new NotFoundError("template-resource", event.payload.id));
50
+ if (!row) {
51
+ const refused = target === "own" ? refuseMissingTemplate(policy, event.user) : null;
52
+ return refused ?? writeFailure(new NotFoundError("template-resource", event.payload.id));
53
+ }
54
+ if (target === "own") {
55
+ const refused = refuseTemplateEdit(policy, event.user, row);
56
+ if (refused !== null)
57
+ return refused;
58
+ }
49
59
  const plan = planStatusChange(ctx, row, status);
50
60
  if (!plan.ok)
51
61
  return plan.failure;
@@ -56,7 +66,15 @@ function createStatusUpdateHandler(name, target, status, description) {
56
66
  },
57
67
  });
58
68
  }
59
- export const archiveWrite = createStatusUpdateHandler("archive", "own", "archived", "Sets one template resource of the caller's tenant to archived so the resolver stops serving it while the row and its content survive; use it to retire a template reversibly instead of deleting it. An open draft stays.");
60
- export const publishWrite = createStatusUpdateHandler("publish", "own", "active", "Sets one template resource of the caller's tenant to active so the resolver starts serving it; use it to release a draft written by upsert-tenant (a draft saved beside an active version replaces it), or to bring an archived template back; a version with unknown placeholders or unused required variables is refused and the served version stays.");
61
- export const archiveSystemWrite = createStatusUpdateHandler("archive-system", "system", "archived", "Sets one system-default template to archived so the resolver stops serving it to every tenant that has no override; SystemAdmin only, reversible with publish-system.");
62
- export const publishSystemWrite = createStatusUpdateHandler("publish-system", "system", "active", "Sets one system-default template to active so the resolver serves it again to every tenant that has no override; an open draft beside the active version replaces it; SystemAdmin only, a version with unknown placeholders or unused required variables is refused.");
69
+ export function makeArchiveWrite(policy) {
70
+ return createStatusUpdateHandler(policy, "archive", "own", "archived", "Sets one template resource of the caller's tenant to archived so the resolver stops serving it while the row and its content survive; use it to retire a template reversibly instead of deleting it. An open draft stays. Outside the templates the app lists in tenantEditable only a SystemAdmin may use it.");
71
+ }
72
+ export function makePublishWrite(policy) {
73
+ return createStatusUpdateHandler(policy, "publish", "own", "active", "Sets one template resource of the caller's tenant to active so the resolver starts serving it; use it to release a draft written by upsert-tenant (a draft saved beside an active version replaces it), or to bring an archived template back; a version with unknown placeholders or unused required variables is refused and the served version stays. Outside the templates the app lists in tenantEditable only a SystemAdmin may use it.");
74
+ }
75
+ export function makeArchiveSystemWrite(policy) {
76
+ return createStatusUpdateHandler(policy, "archive-system", "system", "archived", "Sets one system-default template to archived so the resolver stops serving it to every tenant that has no override; SystemAdmin only, reversible with publish-system.");
77
+ }
78
+ export function makePublishSystemWrite(policy) {
79
+ return createStatusUpdateHandler(policy, "publish-system", "system", "active", "Sets one system-default template to active so the resolver serves it again to every tenant that has no override; an open draft beside the active version replaces it; SystemAdmin only, a version with unknown placeholders or unused required variables is refused.");
80
+ }
@@ -1,5 +1,6 @@
1
1
  import * as z from "zod";
2
- export declare const upsertTenantWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"upsert-tenant", z.ZodObject<{
2
+ import { type TemplateEditPolicy } from "../template-edit-policy.ts";
3
+ export declare function makeUpsertTenantWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"upsert-tenant", z.ZodObject<{
3
4
  slug: z.ZodString;
4
5
  kind: z.ZodEnum<{
5
6
  "ai-prompt": "ai-prompt";