@cosmicdrift/kumiko-bundled-features 0.363.0 → 0.364.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/dist/audit/handlers/list.query.js +9 -5
  2. package/dist/auth-email-password/bootstrap.d.ts +8 -5
  3. package/dist/auth-email-password/bootstrap.js +12 -4
  4. package/dist/auth-email-password/email-rate-limits.d.ts +4 -0
  5. package/dist/auth-email-password/email-rate-limits.js +13 -0
  6. package/dist/auth-email-password/handlers/change-password.write.d.ts +1 -1
  7. package/dist/auth-email-password/handlers/change-password.write.js +6 -2
  8. package/dist/auth-email-password/handlers/confirm-account-unlock.write.d.ts +1 -1
  9. package/dist/auth-email-password/handlers/invite-accept-with-login.write.d.ts +1 -1
  10. package/dist/auth-email-password/handlers/invite-accept.write.d.ts +1 -1
  11. package/dist/auth-email-password/handlers/invite-create.write.d.ts +2 -2
  12. package/dist/auth-email-password/handlers/invite-signup-complete.write.d.ts +1 -1
  13. package/dist/auth-email-password/handlers/login.write.d.ts +1 -1
  14. package/dist/auth-email-password/handlers/logout.write.d.ts +1 -1
  15. package/dist/auth-email-password/handlers/logout.write.js +6 -2
  16. package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -1
  17. package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -1
  18. package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -1
  19. package/dist/auth-email-password/handlers/reset-password.write.d.ts +1 -1
  20. package/dist/auth-email-password/handlers/signup-confirm.write.d.ts +1 -1
  21. package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -1
  22. package/dist/auth-email-password/handlers/signup-request.write.js +2 -1
  23. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.d.ts +1 -1
  24. package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -1
  25. package/dist/auth-email-password/handlers/token-request-handler.js +2 -1
  26. package/dist/auth-email-password/handlers/verify-email.write.d.ts +1 -1
  27. package/dist/auth-email-password/web/auth-gate.js +16 -4
  28. package/dist/auth-mfa/handlers/disable.write.d.ts +1 -1
  29. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.d.ts +1 -1
  30. package/dist/auth-mfa/handlers/enable-confirm.write.d.ts +1 -1
  31. package/dist/auth-mfa/handlers/enable-start-preauth.write.d.ts +1 -1
  32. package/dist/auth-mfa/handlers/enable-start.write.d.ts +1 -1
  33. package/dist/auth-mfa/handlers/regenerate-recovery.write.d.ts +1 -1
  34. package/dist/auth-mfa/handlers/verify.write.d.ts +1 -1
  35. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +52 -5
  36. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +3 -0
  37. package/dist/billing-foundation/consumer-protection/consent-gate.js +21 -2
  38. package/dist/billing-foundation/consumer-protection/mail-notifications.js +1 -1
  39. package/dist/billing-foundation/consumer-protection/register-termination.js +11 -1
  40. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +5 -3
  41. package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -2
  42. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +6 -0
  43. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +34 -0
  44. package/dist/billing-foundation/consumer-protection/termination-record.d.ts +2 -0
  45. package/dist/billing-foundation/consumer-protection/termination-record.js +7 -1
  46. package/dist/billing-foundation/consumer-protection/termination-request.js +30 -20
  47. package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
  48. package/dist/billing-foundation/consumer-protection/termination-texts.js +6 -0
  49. package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +10 -0
  50. package/dist/billing-foundation/consumer-protection/withdrawal-period.js +26 -0
  51. package/dist/billing-foundation/handlers/create-checkout-session.write.js +11 -5
  52. package/dist/billing-foundation/handlers/process-event.write.js +10 -6
  53. package/dist/billing-foundation/i18n.js +3 -0
  54. package/dist/billing-foundation/subscription-tier-sync.d.ts +7 -1
  55. package/dist/billing-foundation/subscription-tier-sync.js +13 -5
  56. package/dist/billing-foundation/types.d.ts +7 -2
  57. package/dist/cap-counter/calendar-cap-usage-reservation.js +2 -0
  58. package/dist/cap-counter/enforce-cap.d.ts +18 -1
  59. package/dist/cap-counter/enforce-cap.js +49 -6
  60. package/dist/cap-counter/index.d.ts +1 -1
  61. package/dist/cap-counter/index.js +1 -1
  62. package/dist/cap-counter/with-cap-enforcement.d.ts +2 -0
  63. package/dist/channel-email/email-channel.js +16 -4
  64. package/dist/channel-in-app/handlers/mark-all-read.write.d.ts +1 -1
  65. package/dist/channel-in-app/handlers/mark-read.write.d.ts +1 -1
  66. package/dist/compliance-profiles/handlers/set-profile.write.d.ts +1 -1
  67. package/dist/config/handlers/reset.write.d.ts +1 -1
  68. package/dist/config/handlers/set.write.d.ts +1 -1
  69. package/dist/config/handlers/set.write.js +2 -2
  70. package/dist/config/resolver.d.ts +1 -0
  71. package/dist/config/resolver.js +11 -14
  72. package/dist/config/write-helpers.d.ts +2 -0
  73. package/dist/config/write-helpers.js +19 -0
  74. package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
  75. package/dist/crypto-shredding/handlers/forget-subject.write.js +29 -19
  76. package/dist/custom-fields/handlers/update-tenant-field.write.js +0 -1
  77. package/dist/delivery/attachment-ref.d.ts +2 -0
  78. package/dist/delivery/attachment-ref.js +14 -0
  79. package/dist/delivery/channel-context.js +19 -1
  80. package/dist/delivery/handlers/resubscribe-address.write.d.ts +1 -1
  81. package/dist/delivery/handlers/resubscribe-user.write.d.ts +1 -1
  82. package/dist/delivery/handlers/set-preference.write.d.ts +1 -1
  83. package/dist/delivery/handlers/unsubscribe-address.write.d.ts +1 -1
  84. package/dist/delivery/handlers/unsubscribe-user.write.d.ts +1 -1
  85. package/dist/delivery/index.d.ts +1 -1
  86. package/dist/delivery/index.js +1 -1
  87. package/dist/delivery/jobs.js +43 -11
  88. package/dist/delivery/types.d.ts +7 -1
  89. package/dist/feature-toggles/handlers/set.write.d.ts +1 -1
  90. package/dist/file-derivatives/handlers/public-variant.query.js +1 -1
  91. package/dist/file-foundation/read-file-ref-content.d.ts +1 -1
  92. package/dist/file-foundation/read-file-ref-content.js +6 -4
  93. package/dist/files-tenant-data/hooks.js +4 -0
  94. package/dist/form-draft/handlers/discard.write.d.ts +1 -1
  95. package/dist/form-draft/handlers/save.write.d.ts +1 -1
  96. package/dist/inbound-mail-foundation/oauth-access-token.js +8 -3
  97. package/dist/inbound-mail-foundation/watch-supervisor.js +3 -1
  98. package/dist/jobs/db/queries/stale-run-sweep.d.ts +1 -1
  99. package/dist/jobs/db/queries/stale-run-sweep.js +12 -5
  100. package/dist/jobs/feature.js +5 -3
  101. package/dist/jobs/handlers/list.query.js +3 -1
  102. package/dist/jobs/handlers/retry.write.d.ts +1 -1
  103. package/dist/jobs/handlers/stale-run-sweep.job.d.ts +1 -1
  104. package/dist/jobs/handlers/stale-run-sweep.job.js +2 -2
  105. package/dist/jobs/handlers/trigger.write.d.ts +1 -1
  106. package/dist/jobs/job-run-logger.js +32 -10
  107. package/dist/jobs/tenant-job-run-table.js +3 -2
  108. package/dist/ledger/entity.js +3 -1
  109. package/dist/managed-pages/handlers/set.write.d.ts +1 -1
  110. package/dist/personal-access-tokens/handlers/create.write.d.ts +1 -1
  111. package/dist/personal-access-tokens/handlers/revoke.write.d.ts +1 -1
  112. package/dist/renderer-foundation/types.d.ts +2 -0
  113. package/dist/renderer-pdf/render-table-pdf.d.ts +27 -33
  114. package/dist/renderer-pdf/render-table-pdf.js +95 -34
  115. package/dist/secrets/feature.js +4 -3
  116. package/dist/secrets/handlers/delete.write.d.ts +1 -1
  117. package/dist/secrets/handlers/set.write.d.ts +1 -1
  118. package/dist/secrets/secrets-context.js +15 -3
  119. package/dist/sessions/handlers/revoke-all-for-user.write.d.ts +1 -1
  120. package/dist/sessions/handlers/revoke-all-others.write.d.ts +1 -1
  121. package/dist/sessions/handlers/revoke.write.d.ts +1 -1
  122. package/dist/shared/chunked-entity-migration.d.ts +1 -0
  123. package/dist/shared/chunked-entity-migration.js +1 -1
  124. package/dist/shared/mail-template-body.d.ts +3 -0
  125. package/dist/shared/mail-template-body.js +10 -0
  126. package/dist/subscription-stripe/plugin-methods.js +18 -8
  127. package/dist/subscription-stripe/verify-webhook.d.ts +1 -1
  128. package/dist/template-resolver/api.js +3 -2
  129. package/dist/template-resolver/compile-document-body.d.ts +4 -0
  130. package/dist/template-resolver/compile-document-body.js +58 -16
  131. package/dist/template-resolver/constants.d.ts +2 -2
  132. package/dist/template-resolver/constants.js +3 -1
  133. package/dist/template-resolver/document-schema.d.ts +1 -1
  134. package/dist/template-resolver/document-schema.js +23 -3
  135. package/dist/template-resolver/feature.d.ts +2 -0
  136. package/dist/template-resolver/feature.js +4 -1
  137. package/dist/template-resolver/handlers/collection-set.write.d.ts +1 -1
  138. package/dist/template-resolver/handlers/copy-locale.write.d.ts +1 -1
  139. package/dist/template-resolver/handlers/copy-locale.write.js +29 -11
  140. package/dist/template-resolver/handlers/diff-to-system.query.js +4 -38
  141. package/dist/template-resolver/handlers/discard-draft.write.d.ts +13 -0
  142. package/dist/template-resolver/handlers/discard-draft.write.js +37 -0
  143. package/dist/template-resolver/handlers/export-bundle.query.js +51 -1
  144. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
  145. package/dist/template-resolver/handlers/find-by-id.query.js +6 -3
  146. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +1 -1
  147. package/dist/template-resolver/handlers/import-bundle.write.d.ts +1 -1
  148. package/dist/template-resolver/handlers/layout-source.query.d.ts +5 -0
  149. package/dist/template-resolver/handlers/layout-source.query.js +46 -23
  150. package/dist/template-resolver/handlers/list.query.js +4 -1
  151. package/dist/template-resolver/handlers/override-diff.d.ts +20 -0
  152. package/dist/template-resolver/handlers/override-diff.js +41 -0
  153. package/dist/template-resolver/handlers/overview.query.d.ts +6 -0
  154. package/dist/template-resolver/handlers/overview.query.js +51 -11
  155. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +1 -1
  156. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +1 -1
  157. package/dist/template-resolver/handlers/resource-remove.write.d.ts +1 -1
  158. package/dist/template-resolver/handlers/resource-upload.write.d.ts +1 -1
  159. package/dist/template-resolver/handlers/resource-upload.write.js +1 -1
  160. package/dist/template-resolver/handlers/send-test.write.d.ts +3 -1
  161. package/dist/template-resolver/handlers/send-test.write.js +37 -10
  162. package/dist/template-resolver/handlers/set.write.d.ts +1 -1
  163. package/dist/template-resolver/handlers/template-gates.js +13 -2
  164. package/dist/template-resolver/handlers/template-write.js +36 -4
  165. package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +32 -0
  166. package/dist/template-resolver/handlers/tenant-overrides.query.js +80 -16
  167. package/dist/template-resolver/handlers/toggle-status.write.d.ts +17 -4
  168. package/dist/template-resolver/handlers/toggle-status.write.js +35 -19
  169. package/dist/template-resolver/handlers/upsert-system.write.d.ts +1 -1
  170. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +1 -1
  171. package/dist/template-resolver/handlers/upsert-tenant.write.js +1 -1
  172. package/dist/template-resolver/handlers/usage.query.js +11 -2
  173. package/dist/template-resolver/index.d.ts +4 -3
  174. package/dist/template-resolver/index.js +4 -3
  175. package/dist/template-resolver/layout-appearance.d.ts +10 -0
  176. package/dist/template-resolver/layout-appearance.js +26 -0
  177. package/dist/template-resolver/pdf-constants.d.ts +7 -0
  178. package/dist/template-resolver/pdf-constants.js +8 -0
  179. package/dist/template-resolver/qualified-names.d.ts +4 -0
  180. package/dist/template-resolver/qualified-names.js +4 -0
  181. package/dist/template-resolver/seeding.js +21 -4
  182. package/dist/template-resolver/store-template-resource.js +2 -2
  183. package/dist/template-resolver/table.d.ts +103 -0
  184. package/dist/template-resolver/table.js +9 -2
  185. package/dist/template-resolver/template-draft.d.ts +51 -0
  186. package/dist/template-resolver/template-draft.js +72 -0
  187. package/dist/template-resolver/template-meta.d.ts +30 -5
  188. package/dist/template-resolver/template-meta.js +60 -16
  189. package/dist/template-resolver/template-preview.d.ts +19 -2
  190. package/dist/template-resolver/template-preview.js +98 -54
  191. package/dist/template-resolver/template-resources.d.ts +5 -0
  192. package/dist/template-resolver/template-resources.js +35 -4
  193. package/dist/template-resolver/template-usage.d.ts +5 -1
  194. package/dist/template-resolver/template-usage.js +14 -9
  195. package/dist/template-resolver/template-validation.js +1 -0
  196. package/dist/template-resolver/web/client-plugin.js +4 -2
  197. package/dist/template-resolver/web/document-editor-extensions.d.ts +4 -1
  198. package/dist/template-resolver/web/document-editor-extensions.js +50 -12
  199. package/dist/template-resolver/web/document-editor.d.ts +2 -1
  200. package/dist/template-resolver/web/document-editor.js +8 -2
  201. package/dist/template-resolver/web/document-tiptap-editor.d.ts +1 -1
  202. package/dist/template-resolver/web/document-tiptap-editor.js +144 -28
  203. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +8 -0
  204. package/dist/template-resolver/web/document-toolbar-blocks.js +4 -0
  205. package/dist/template-resolver/web/i18n.js +19 -4
  206. package/dist/template-resolver/web/index.d.ts +1 -0
  207. package/dist/template-resolver/web/index.js +1 -0
  208. package/dist/template-resolver/web/placeholder-line-editor.d.ts +3 -1
  209. package/dist/template-resolver/web/placeholder-line-editor.js +6 -6
  210. package/dist/tenant/handlers/add-member.write.d.ts +1 -1
  211. package/dist/tenant/handlers/cancel-invitation.write.d.ts +1 -1
  212. package/dist/tenant/handlers/create.write.d.ts +1 -1
  213. package/dist/tenant/handlers/remove-member.write.d.ts +1 -1
  214. package/dist/tenant/handlers/toggle-enabled.write.d.ts +2 -2
  215. package/dist/tenant/handlers/update-member-roles.write.d.ts +1 -1
  216. package/dist/tenant/handlers/update.write.d.ts +1 -1
  217. package/dist/tenant-handover/handlers/claim.write.d.ts +1 -1
  218. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
  219. package/dist/tenant-lifecycle/handlers/request-destruction.write.d.ts +1 -1
  220. package/dist/tier-engine/feature.js +9 -3
  221. package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +1 -1
  222. package/dist/user/handlers/create.write.d.ts +1 -1
  223. package/dist/user/handlers/update.write.d.ts +1 -1
  224. package/dist/user/resolve-display-names.js +5 -9
  225. package/dist/user/seeding.js +20 -1
  226. package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
  227. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.d.ts +1 -1
  228. package/dist/user-data-rights/handlers/lift-restriction.write.d.ts +1 -1
  229. package/dist/user-data-rights/handlers/request-deletion-by-email.write.d.ts +1 -1
  230. package/dist/user-data-rights/handlers/request-deletion.write.d.ts +1 -1
  231. package/dist/user-data-rights/handlers/request-export.write.d.ts +1 -1
  232. package/dist/user-data-rights/handlers/restrict-account.write.d.ts +1 -1
  233. package/dist/user-data-rights/handlers/run-forget-cleanup.write.d.ts +1 -1
  234. package/dist/user-profile/handlers/change-email.write.d.ts +1 -1
  235. package/dist/waitlist/handlers/invite.write.d.ts +1 -1
  236. package/dist/waitlist/handlers/reject.write.d.ts +1 -1
  237. package/dist/waitlist/handlers/submit.write.d.ts +1 -1
  238. package/dist/waitlist/options.js +1 -1
  239. package/dist/workflow-runner/event-trigger.js +2 -2
  240. package/dist/workflow-runner/handlers/resume-run.write.js +7 -2
  241. package/package.json +19 -10
  242. package/src/audit/changes.json +6 -0
  243. package/src/auth-email-password/changes.json +37 -0
  244. package/src/billing-foundation/changes.json +36 -0
  245. package/src/cap-counter/changes.json +6 -0
  246. package/src/config/changes.json +6 -0
  247. package/src/crypto-shredding/changes.json +6 -0
  248. package/src/custom-fields/changes.json +6 -0
  249. package/src/delivery/changes.json +18 -0
  250. package/src/file-foundation/changes.json +6 -0
  251. package/src/files-tenant-data/changes.json +6 -0
  252. package/src/inbound-mail-foundation/changes.json +6 -0
  253. package/src/jobs/changes.json +19 -0
  254. package/src/ledger/changes.json +7 -0
  255. package/src/renderer-pdf/changes.json +12 -0
  256. package/src/secrets/changes.json +6 -0
  257. package/src/shared/changes.json +6 -0
  258. package/src/subscription-stripe/changes.json +6 -0
  259. package/src/template-resolver/changes.json +68 -0
  260. package/src/tier-engine/changes.json +6 -0
  261. package/src/user/changes.json +6 -0
  262. package/src/user-data-rights/changes.json +6 -0
  263. package/src/user-profile/changes.json +1 -1
  264. package/src/workflow-runner/changes.json +6 -0
@@ -1,27 +1,40 @@
1
+ import { type HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
1
2
  import * as z from "zod";
3
+ import type { TemplateResourceRow } from "../table.js";
2
4
  type TemplateStatus = "active" | "archived";
5
+ export type StatusTarget = "own" | "system";
6
+ export declare function writeAccessFor(target: StatusTarget): {
7
+ roles: string[];
8
+ };
9
+ export declare function loadTargetRow(ctx: HandlerContext, target: StatusTarget, user: {
10
+ readonly tenantId: string;
11
+ }, id: string): Promise<{
12
+ db: import("@cosmicdrift/kumiko-types/tenant-db-types").TenantDb;
13
+ scoped: import("@cosmicdrift/kumiko-types/handlers").TenantWriteTarget | null;
14
+ row: TemplateResourceRow | null;
15
+ }>;
3
16
  export declare const archiveWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
4
17
  id: z.ZodString;
5
18
  }, z.core.$strip>, {
6
19
  id: string;
7
20
  status: TemplateStatus;
8
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
21
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
9
22
  export declare const publishWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
10
23
  id: z.ZodString;
11
24
  }, z.core.$strip>, {
12
25
  id: string;
13
26
  status: TemplateStatus;
14
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
27
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
15
28
  export declare const archiveSystemWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
16
29
  id: z.ZodString;
17
30
  }, z.core.$strip>, {
18
31
  id: string;
19
32
  status: TemplateStatus;
20
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
33
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
21
34
  export declare const publishSystemWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
22
35
  id: z.ZodString;
23
36
  }, z.core.$strip>, {
24
37
  id: string;
25
38
  status: TemplateStatus;
26
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
39
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
27
40
  export {};
@@ -4,6 +4,7 @@ import { NotFoundError, ValidationError, writeFailure, } from "@cosmicdrift/kumi
4
4
  import * as z from "zod";
5
5
  import { requireForTenant } from "../../shared/index.js";
6
6
  import { templateResourcesTable } from "../table.js";
7
+ import { activationChanges, parseTemplateDraft, toWorkingRow } from "../template-draft.js";
7
8
  import { executor } from "./shared.js";
8
9
  import { publishGateIssues } from "./template-gates.js";
9
10
  function isRowOfTarget(row, target, callerTenantId) {
@@ -11,36 +12,51 @@ function isRowOfTarget(row, target, callerTenantId) {
11
12
  ? row.tenantId === SYSTEM_TENANT_ID && row.scope === "system"
12
13
  : row.tenantId === callerTenantId && row.scope === "tenant";
13
14
  }
14
- function createStatusUpdateHandler(name, target, status, description, gate) {
15
+ export function writeAccessFor(target) {
16
+ return { roles: target === "system" ? ["SystemAdmin"] : ["TenantAdmin", "SystemAdmin"] };
17
+ }
18
+ // The tenant db also reads system rows; the row's own tenant must match the
19
+ // target or the executor would append to a foreign event stream.
20
+ export async function loadTargetRow(ctx, target, user, id) {
21
+ const scoped = target === "system" ? requireForTenant(ctx, SYSTEM_TENANT_ID) : null;
22
+ const db = scoped?.db ?? ctx.db;
23
+ const row = await fetchOne(db, templateResourcesTable, { id });
24
+ return { db, scoped, row: row && isRowOfTarget(row, target, user.tenantId) ? row : null };
25
+ }
26
+ // Publishing promotes an open draft (gated as the version that goes live); a row
27
+ // without one is activated as stored. Archiving leaves the draft standing.
28
+ function planStatusChange(ctx, row, status) {
29
+ if (status === "archived")
30
+ return { ok: true, changes: { status } };
31
+ const issues = publishGateIssues(ctx.registry, toWorkingRow(row, row.tenantId));
32
+ if (issues.length > 0) {
33
+ return { ok: false, failure: writeFailure(new ValidationError({ fields: [...issues] })) };
34
+ }
35
+ return { ok: true, changes: { ...parseTemplateDraft(row.draft), ...activationChanges(row) } };
36
+ }
37
+ function createStatusUpdateHandler(name, target, status, description) {
15
38
  return defineWriteHandler({
16
39
  name,
17
40
  schema: z.object({ id: z.string().min(1) }),
18
- access: { roles: target === "system" ? ["SystemAdmin"] : ["TenantAdmin", "SystemAdmin"] },
41
+ access: writeAccessFor(target),
19
42
  // A system template change reaches every tenant without an override.
20
43
  ...(target === "system" ? { agent: { risk: "high" } } : {}),
21
44
  description,
22
45
  handler: async (event, ctx) => {
23
- const scoped = target === "system" ? requireForTenant(ctx, SYSTEM_TENANT_ID) : null;
24
- const db = scoped?.db ?? ctx.db;
25
- // The tenant db also reads system rows; the row's own tenant must match
26
- // the target or the executor would append to a foreign event stream.
27
- const existing = await fetchOne(db, templateResourcesTable, {
28
- id: event.payload.id,
29
- });
30
- if (!existing || !isRowOfTarget(existing, target, event.user.tenantId)) {
46
+ const { db, scoped, row } = await loadTargetRow(ctx, target, event.user, event.payload.id);
47
+ if (!row)
31
48
  return writeFailure(new NotFoundError("template-resource", event.payload.id));
32
- }
33
- const issues = gate?.(ctx.registry, existing) ?? [];
34
- if (issues.length > 0)
35
- return writeFailure(new ValidationError({ fields: [...issues] }));
36
- const result = await executor.update({ id: existing.id, version: existing.version, changes: { status } }, event.user, db, scoped ? { streamTenantId: scoped.streamTenantId } : undefined);
49
+ const plan = planStatusChange(ctx, row, status);
50
+ if (!plan.ok)
51
+ return plan.failure;
52
+ const result = await executor.update({ id: row.id, version: row.version, changes: plan.changes }, event.user, db, scoped ? { streamTenantId: scoped.streamTenantId } : undefined);
37
53
  if (!result.isSuccess)
38
54
  return result;
39
- return { isSuccess: true, data: { id: String(existing.id), status } };
55
+ return { isSuccess: true, data: { id: String(row.id), status } };
40
56
  },
41
57
  });
42
58
  }
43
- 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.");
44
- 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, or to bring an archived template back; a mail template with unknown placeholders or unused required variables is refused.", publishGateIssues);
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.");
45
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.");
46
- 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; SystemAdmin only, a mail template with unknown placeholders or unused required variables is refused.", publishGateIssues);
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.");
@@ -21,4 +21,4 @@ export declare const upsertSystemWrite: import("@cosmicdrift/kumiko-types/define
21
21
  parentTemplateId: import("zod").ZodOptional<import("zod").ZodString>;
22
22
  title: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
23
23
  folder: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
24
- }, import("zod/v4/core").$strip>, import("./template-write.js").WrittenTemplateRow, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
24
+ }, import("zod/v4/core").$strip>, import("./template-write.js").WrittenTemplateRow, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -27,4 +27,4 @@ export declare const upsertTenantWrite: import("@cosmicdrift/kumiko-types/define
27
27
  active: "active";
28
28
  draft: "draft";
29
29
  }>>;
30
- }, z.core.$strip>, import("./template-write.js").WrittenTemplateRow, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
30
+ }, z.core.$strip>, import("./template-write.js").WrittenTemplateRow, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -15,7 +15,7 @@ export const upsertTenantWrite = defineWriteHandler({
15
15
  status: z.enum(["draft", "active"]).default("draft"),
16
16
  }),
17
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; it refuses a system-tenant target, which is what upsert-system is for.",
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
19
  handler: async (event, ctx) => {
20
20
  // @cast-boundary engine-payload — override aus Zod-parsed string,
21
21
  // event.user.tenantId schon TenantId-branded; union als TenantId casten
@@ -1,15 +1,24 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import * as z from "zod";
3
4
  import { TEMPLATE_KINDS } from "../constants.js";
5
+ import { templateResourcesTable } from "../table.js";
4
6
  import { templateSlugSchema } from "../template-meta.js";
5
7
  import { collectTemplateUsage } from "../template-usage.js";
8
+ import { effectiveMailLayoutSlugs } from "./overview.query.js";
9
+ const MAIL_ROW_LIMIT = 5000;
6
10
  export const usageQuery = defineQueryHandler({
7
11
  name: "usage",
8
12
  schema: z.object({ slug: templateSlugSchema, kind: z.enum(TEMPLATE_KINDS) }),
9
13
  access: { roles: ["TenantAdmin", "SystemAdmin"] },
10
- description: "Lists the code-registered notifications that use one template: a mail template is used by the notification whose qualified name is its slug, a mail layout by every mail notification that names it in its meta or falls back to the default layout; use it to show what a change to a template affects.",
14
+ description: "Lists the code-registered notifications that use one template: a mail template is used by the notification whose qualified name is its slug and that has a code template, a mail layout by every mail notification whose effective template (own override, else system) names it, or whose code meta does when no stored template exists, or that falls back to the default layout; use it to show what a change to a template affects.",
11
15
  handler: async (query, ctx) => {
12
16
  const { slug, kind } = query.payload;
13
- return { usedBy: collectTemplateUsage(ctx.registry, slug, kind) };
17
+ if (kind !== "mail-layout")
18
+ return { usedBy: collectTemplateUsage(ctx.registry, slug, kind) };
19
+ const mailRows = await selectMany(ctx.db, templateResourcesTable, { kind: "mail" }, { limit: MAIL_ROW_LIMIT });
20
+ return {
21
+ usedBy: collectTemplateUsage(ctx.registry, slug, kind, effectiveMailLayoutSlugs(mailRows)),
22
+ };
14
23
  },
15
24
  });
@@ -1,12 +1,13 @@
1
1
  export { createTemplateResolverApi, type ResolveRequest, requireTemplateResolver, TemplateNotFoundError, type TemplateResolverApi, type TemplateResource, } from "./api.js";
2
2
  export { type CompileDocumentBodyOptions, collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.js";
3
- export { CONTENT_FORMATS, type ContentFormat, DEFAULT_MAIL_LAYOUT_SLUG, DOCUMENT_HTML_KINDS, type DocumentHtmlKind, FALLBACK_LOCALE, NON_RENDER_KINDS, type NonRenderKind, PDF_PAGE_FORMATS, 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.js";
3
+ export { CONTENT_FORMATS, type ContentFormat, DEFAULT_MAIL_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.js";
4
4
  export { type DocumentViolation, type DocumentViolationCode, documentViolationsToFieldIssues, type SanitizeBodyResult, type SanitizeHtmlResult, sanitizeDocumentHtml, sanitizeTemplateBody, } from "./document-sanitizer.js";
5
- export { DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentRepeatRow, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, type DocumentTextAlignment, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
5
+ export { DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, type DocumentTextAlignment, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
6
6
  export { createTemplateResolverFeature } from "./feature.js";
7
7
  export type { LayoutSourceResponse, LayoutSourceRow } from "./handlers/layout-source.query.js";
8
8
  export type { OverviewKindGroup, OverviewOrigin, OverviewResponse, OverviewRow, OverviewUsage, } from "./handlers/overview.query.js";
9
9
  export { localeSchema } from "./handlers/shared.js";
10
+ export { isHexColor, type MailLayoutAppearance, mailLayoutAppearance, } from "./layout-appearance.js";
10
11
  export { type LayoutRegions, readLayoutRegions, type SanitizeLayoutResult, sanitizeLayoutHtml, } from "./layout-regions.js";
11
12
  export { fileAttachmentsOf, imageLinksOf, type LinkedResourceEntry, parseLinkedResourceEntries, pdfAttachmentLinksOf, type TemplatePdfAttachmentLink, templatePdfAttachmentLinkSchema, } from "./linked-resources.js";
12
13
  export { prepareDocumentForPrint } from "./print-document.js";
@@ -15,7 +16,7 @@ export { mapRecordToVariables } from "./record-mapping.js";
15
16
  export { readTemplateResourceMaxBytes, type StoredTemplateResource, storeTemplateResource, } from "./store-template-resource.js";
16
17
  export { type TemplateResourceRow, templateResourceEntity, templateResourcesTable, } from "./table.js";
17
18
  export { type TemplateBundle, templateBundleSchema } from "./template-bundle.js";
18
- export { isMailTemplateMeta, type LayoutTemplateMeta, MAIL_CATEGORIES, type MailCategory, type MailTemplateMeta, type PdfTemplateMeta, parseTemplateMeta, pdfAttachmentFileNamePatternSchema, readPdfTemplateMeta, type TemplateMeta, type TemplateMetaIssue, type TemplateMetaResult, templateSlugSchema, } from "./template-meta.js";
19
+ export { isMailTemplateMeta, type LayoutTemplateMeta, MAIL_CATEGORIES, type MailCategory, type MailLayoutMeta, type MailTemplateMeta, type PdfTemplateMeta, parseTemplateMeta, pdfAttachmentFileNamePatternSchema, readMailLayoutMeta, readPdfTemplateMeta, type TemplateMeta, type TemplateMetaIssue, type TemplateMetaResult, templateSlugSchema, } from "./template-meta.js";
19
20
  export { type DocumentDraftOutcome, evaluateDocumentDraft } from "./template-preview.js";
20
21
  export { readTemplateResource, type TemplateResourceFile } from "./template-resources.js";
21
22
  export { collectTemplateUsage, type TemplateUsageEntry } from "./template-usage.js";
@@ -1,10 +1,11 @@
1
1
  export { createTemplateResolverApi, requireTemplateResolver, TemplateNotFoundError, } from "./api.js";
2
2
  export { collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.js";
3
- export { CONTENT_FORMATS, DEFAULT_MAIL_LAYOUT_SLUG, DOCUMENT_HTML_KINDS, FALLBACK_LOCALE, NON_RENDER_KINDS, PDF_PAGE_FORMATS, RENDER_KINDS, SYSTEM_TENANT_ID, TEMPLATE_KINDS, TEMPLATE_SCOPES, TEMPLATE_STATUSES, TEXT_BLOCK_KIND, UPSERT_KINDS, } from "./constants.js";
3
+ export { CONTENT_FORMATS, DEFAULT_MAIL_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
4
  export { documentViolationsToFieldIssues, sanitizeDocumentHtml, sanitizeTemplateBody, } from "./document-sanitizer.js";
5
- export { DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentRepeatRow, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
5
+ export { DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
6
6
  export { createTemplateResolverFeature } from "./feature.js";
7
7
  export { localeSchema } from "./handlers/shared.js";
8
+ export { isHexColor, mailLayoutAppearance, } from "./layout-appearance.js";
8
9
  export { readLayoutRegions, sanitizeLayoutHtml, } from "./layout-regions.js";
9
10
  export { fileAttachmentsOf, imageLinksOf, parseLinkedResourceEntries, pdfAttachmentLinksOf, templatePdfAttachmentLinkSchema, } from "./linked-resources.js";
10
11
  export { prepareDocumentForPrint } from "./print-document.js";
@@ -13,7 +14,7 @@ export { mapRecordToVariables } from "./record-mapping.js";
13
14
  export { readTemplateResourceMaxBytes, storeTemplateResource, } from "./store-template-resource.js";
14
15
  export { templateResourceEntity, templateResourcesTable, } from "./table.js";
15
16
  export { templateBundleSchema } from "./template-bundle.js";
16
- export { isMailTemplateMeta, MAIL_CATEGORIES, parseTemplateMeta, pdfAttachmentFileNamePatternSchema, readPdfTemplateMeta, templateSlugSchema, } from "./template-meta.js";
17
+ export { isMailTemplateMeta, MAIL_CATEGORIES, parseTemplateMeta, pdfAttachmentFileNamePatternSchema, readMailLayoutMeta, readPdfTemplateMeta, templateSlugSchema, } from "./template-meta.js";
17
18
  export { evaluateDocumentDraft } from "./template-preview.js";
18
19
  export { readTemplateResource } from "./template-resources.js";
19
20
  export { collectTemplateUsage } from "./template-usage.js";
@@ -0,0 +1,10 @@
1
+ import { type MailLayoutMeta } from "./template-meta.js";
2
+ export type MailLayoutAppearance = {
3
+ readonly width: number;
4
+ readonly fontFamily: string;
5
+ readonly outerBackground: string;
6
+ readonly accentLine: boolean;
7
+ readonly buttonStyle: string;
8
+ };
9
+ export declare function isHexColor(value: string): boolean;
10
+ export declare function mailLayoutAppearance(meta: MailLayoutMeta | unknown): MailLayoutAppearance;
@@ -0,0 +1,26 @@
1
+ import { HEX_COLOR_PATTERN } from "./constants.js";
2
+ import { readMailLayoutMeta } from "./template-meta.js";
3
+ const FONT_FAMILIES = {
4
+ sans: "Arial, Helvetica, sans-serif",
5
+ serif: "Georgia, 'Times New Roman', serif",
6
+ };
7
+ const BUTTON_COLOR = "#1f5468";
8
+ const BUTTON_BASE = "display:inline-block;padding:10px 20px;font-weight:600;text-decoration:none";
9
+ const BUTTON_STYLES = {
10
+ "filled-rounded": `${BUTTON_BASE};border-radius:6px;background-color:${BUTTON_COLOR};color:#ffffff`,
11
+ "filled-square": `${BUTTON_BASE};background-color:${BUTTON_COLOR};color:#ffffff`,
12
+ outline: `${BUTTON_BASE};border:2px solid ${BUTTON_COLOR};border-radius:6px;color:${BUTTON_COLOR}`,
13
+ };
14
+ export function isHexColor(value) {
15
+ return HEX_COLOR_PATTERN.test(value);
16
+ }
17
+ export function mailLayoutAppearance(meta) {
18
+ const parsed = readMailLayoutMeta(meta);
19
+ return {
20
+ width: parsed.width ?? 600,
21
+ fontFamily: FONT_FAMILIES[parsed.font ?? "sans"],
22
+ outerBackground: parsed.outerBackground ?? "#f5f5f5",
23
+ accentLine: (parsed.accentLine ?? "none") === "branding",
24
+ buttonStyle: BUTTON_STYLES[parsed.buttonStyle ?? "filled-rounded"],
25
+ };
26
+ }
@@ -0,0 +1,7 @@
1
+ export declare const PDF_PAGE_FORMATS: readonly ["A4", "Letter"];
2
+ export type PdfPageFormat = (typeof PDF_PAGE_FORMATS)[number];
3
+ export declare const PDF_ORIENTATIONS: readonly ["portrait", "landscape"];
4
+ export type PdfOrientation = (typeof PDF_ORIENTATIONS)[number];
5
+ export declare const PDF_DEFAULT_MARGIN_MM = 15;
6
+ export declare const PDF_HEADER_MARGIN_MM = 30;
7
+ export declare const PDF_FOOTER_MARGIN_MM = 20;
@@ -0,0 +1,8 @@
1
+ // @runtime client
2
+ // No imports: web/ re-exports these, and constants.ts pulls in the framework engine.
3
+ export const PDF_PAGE_FORMATS = ["A4", "Letter"];
4
+ export const PDF_ORIENTATIONS = ["portrait", "landscape"];
5
+ // Page margins in mm; the header/footer values apply when the layout contributes that region and the template sets no margin itself.
6
+ export const PDF_DEFAULT_MARGIN_MM = 15;
7
+ export const PDF_HEADER_MARGIN_MM = 30;
8
+ export const PDF_FOOTER_MARGIN_MM = 20;
@@ -6,6 +6,8 @@ export declare const TemplateResolverHandlers: {
6
6
  readonly archive: "template-resolver:write:archive";
7
7
  readonly publishSystem: "template-resolver:write:publish-system";
8
8
  readonly archiveSystem: "template-resolver:write:archive-system";
9
+ readonly discardDraft: "template-resolver:write:discard-draft";
10
+ readonly discardDraftSystem: "template-resolver:write:discard-draft-system";
9
11
  readonly resetToSystem: "template-resolver:write:reset-to-system";
10
12
  readonly set: "template-resolver:write:set";
11
13
  readonly importBundle: "template-resolver:write:import-bundle";
@@ -37,10 +39,12 @@ export declare const TemplateResolverErrors: {
37
39
  readonly invalidSlug: "invalid_slug";
38
40
  readonly invalidLocale: "invalid_locale";
39
41
  readonly systemAdminRequired: "system_admin_required";
42
+ readonly noDraft: "no_draft";
40
43
  readonly alreadyActive: "template_already_active";
41
44
  readonly alreadyArchived: "template_already_archived";
42
45
  readonly targetExists: "target_exists";
43
46
  readonly systemResourcesNeedSystemTenant: "system_resources_need_system_tenant";
47
+ readonly bundleLocalesDiffer: "bundle_locales_differ";
44
48
  readonly resourceTooLarge: "template_resource_too_large";
45
49
  readonly resourceUnsupportedImage: "template_resource_unsupported_image";
46
50
  readonly attachmentUnsupportedType: "template_attachment_unsupported_type";
@@ -8,6 +8,8 @@ export const TemplateResolverHandlers = {
8
8
  archive: "template-resolver:write:archive",
9
9
  publishSystem: "template-resolver:write:publish-system",
10
10
  archiveSystem: "template-resolver:write:archive-system",
11
+ discardDraft: "template-resolver:write:discard-draft",
12
+ discardDraftSystem: "template-resolver:write:discard-draft-system",
11
13
  resetToSystem: "template-resolver:write:reset-to-system",
12
14
  set: "template-resolver:write:set",
13
15
  importBundle: "template-resolver:write:import-bundle",
@@ -48,10 +50,12 @@ export const TemplateResolverErrors = {
48
50
  invalidSlug: "invalid_slug",
49
51
  invalidLocale: "invalid_locale",
50
52
  systemAdminRequired: "system_admin_required",
53
+ noDraft: "no_draft",
51
54
  alreadyActive: "template_already_active",
52
55
  alreadyArchived: "template_already_archived",
53
56
  targetExists: "target_exists",
54
57
  systemResourcesNeedSystemTenant: "system_resources_need_system_tenant",
58
+ bundleLocalesDiffer: "bundle_locales_differ",
55
59
  resourceTooLarge: "template_resource_too_large",
56
60
  resourceUnsupportedImage: "template_resource_unsupported_image",
57
61
  attachmentUnsupportedType: "template_attachment_unsupported_type",
@@ -18,6 +18,7 @@ import { executor } from "./handlers/shared.js";
18
18
  import { evaluateTemplateWrite } from "./handlers/template-gates.js";
19
19
  import { TEMPLATE_RESOLVER_FEATURE } from "./qualified-names.js";
20
20
  import { templateResourcesTable } from "./table.js";
21
+ import { activeStampChanges, servedStampChanges } from "./template-draft.js";
21
22
  import { templateSlugSchema } from "./template-meta.js";
22
23
  import { variablesFromSchema } from "./template-validation.js";
23
24
  /** Drop projection orphans (no events anywhere) so boot seeds can recreate. */
@@ -97,7 +98,11 @@ export async function seedSystemTemplate(db, opts) {
97
98
  (linkedResources === undefined || current?.linkedResources === linkedResources)) {
98
99
  return { id: String(row.id) };
99
100
  }
100
- const result = await executor.update({ id: row.id, version: row.version, changes: rowFields }, by, tdb);
101
+ const result = await executor.update({
102
+ id: row.id,
103
+ version: row.version,
104
+ changes: { ...rowFields, ...activeStampChanges(row) },
105
+ }, by, tdb);
101
106
  if (!result.isSuccess) {
102
107
  throw new Error(`seedSystemTemplate update failed: ${JSON.stringify(result)}`);
103
108
  }
@@ -107,7 +112,13 @@ export async function seedSystemTemplate(db, opts) {
107
112
  existing,
108
113
  ifExists: opts.ifExists,
109
114
  create: async () => {
110
- const result = await executor.create({ ...rowFields, linkedResources: linkedResources ?? "{}", tenantId }, by, tdb);
115
+ const result = await executor.create({
116
+ ...rowFields,
117
+ ...activeStampChanges(null),
118
+ draft: null,
119
+ linkedResources: linkedResources ?? "{}",
120
+ tenantId,
121
+ }, by, tdb);
111
122
  if (!result.isSuccess) {
112
123
  // Parallel boot pods can both delete the orphan and race on create —
113
124
  // unique on (tenantId, slug, kind, locale) → resolve winner and update.
@@ -235,7 +246,13 @@ export async function seedTextBlock(db, opts) {
235
246
  const result = await executor.update({
236
247
  id: row.id,
237
248
  version: row.version,
238
- changes: { title: fields.title, content: fields.content, folder: fields.folder },
249
+ changes: {
250
+ title: fields.title,
251
+ content: fields.content,
252
+ folder: fields.folder,
253
+ // A re-seed must not revive an archived block; only a served one gets a new served version.
254
+ ...(current?.status === "active" && servedStampChanges(row)),
255
+ },
239
256
  }, by, tdb);
240
257
  if (!result.isSuccess) {
241
258
  throw new Error(`seedTextBlock update failed: ${JSON.stringify(result)}`);
@@ -246,7 +263,7 @@ export async function seedTextBlock(db, opts) {
246
263
  existing,
247
264
  ifExists: opts.ifExists,
248
265
  create: async () => {
249
- const result = await executor.create({ ...fields, tenantId: opts.tenantId }, by, tdb);
266
+ const result = await executor.create({ ...fields, ...activeStampChanges(null), draft: null, tenantId: opts.tenantId }, by, tdb);
250
267
  if (!result.isSuccess) {
251
268
  if (result.error?.code === "unique_violation") {
252
269
  const again = await fetchOne(db, templateResourcesTable, {
@@ -55,7 +55,7 @@ export async function storeTemplateResource(args) {
55
55
  }
56
56
  const created = await createFileRefFromBytes({
57
57
  db,
58
- files: fileContextForTenant(ctx, tenantId),
58
+ files: fileContextForTenant(ctx, ctx.user.tenantId, tenantId),
59
59
  registry: ctx.registry,
60
60
  user: { ...user, tenantId },
61
61
  }, {
@@ -145,7 +145,7 @@ async function createAttachmentFileRef(args) {
145
145
  const { db } = requireForTenant(ctx, tenantId);
146
146
  const created = await createFileRefFromBytes({
147
147
  db,
148
- files: fileContextForTenant(ctx, tenantId),
148
+ files: fileContextForTenant(ctx, ctx.user.tenantId, tenantId),
149
149
  registry: ctx.registry,
150
150
  user: { ...user, tenantId },
151
151
  }, {
@@ -277,6 +277,56 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
277
277
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
278
278
  required: true;
279
279
  };
280
+ draft: {
281
+ readonly type: "longText";
282
+ readonly description?: string;
283
+ readonly maxLength?: number;
284
+ readonly required?: boolean;
285
+ readonly encrypted?: boolean;
286
+ readonly sensitive?: boolean;
287
+ readonly readAsInstruction?: boolean;
288
+ readonly default?: string;
289
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
290
+ readonly multiline?: boolean | {
291
+ readonly rows?: number;
292
+ };
293
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
294
+ required: false;
295
+ };
296
+ activeVersion: {
297
+ readonly type: "number";
298
+ readonly description?: string;
299
+ readonly required?: boolean;
300
+ readonly sortable?: boolean;
301
+ readonly filterable?: boolean;
302
+ readonly sensitive?: boolean;
303
+ readonly default?: number;
304
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
305
+ readonly min?: number;
306
+ readonly max?: number;
307
+ readonly integer?: boolean;
308
+ readonly unit?: string | {
309
+ readonly field: string;
310
+ };
311
+ readonly grouping?: boolean;
312
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
313
+ required: false;
314
+ };
315
+ activeSince: {
316
+ readonly type: "timestamp";
317
+ readonly description?: string;
318
+ readonly required?: boolean;
319
+ readonly sortable?: boolean;
320
+ readonly filterable?: boolean;
321
+ readonly sensitive?: boolean;
322
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
323
+ readonly locatedBy?: string;
324
+ readonly min?: string;
325
+ readonly max?: string;
326
+ readonly locale?: string;
327
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
328
+ required: false;
329
+ };
280
330
  }, "tenant">;
281
331
  export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framework/db").EntityTable<import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
282
332
  slug: {
@@ -556,6 +606,56 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
556
606
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
557
607
  required: true;
558
608
  };
609
+ draft: {
610
+ readonly type: "longText";
611
+ readonly description?: string;
612
+ readonly maxLength?: number;
613
+ readonly required?: boolean;
614
+ readonly encrypted?: boolean;
615
+ readonly sensitive?: boolean;
616
+ readonly readAsInstruction?: boolean;
617
+ readonly default?: string;
618
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
619
+ readonly multiline?: boolean | {
620
+ readonly rows?: number;
621
+ };
622
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
623
+ required: false;
624
+ };
625
+ activeVersion: {
626
+ readonly type: "number";
627
+ readonly description?: string;
628
+ readonly required?: boolean;
629
+ readonly sortable?: boolean;
630
+ readonly filterable?: boolean;
631
+ readonly sensitive?: boolean;
632
+ readonly default?: number;
633
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
634
+ readonly min?: number;
635
+ readonly max?: number;
636
+ readonly integer?: boolean;
637
+ readonly unit?: string | {
638
+ readonly field: string;
639
+ };
640
+ readonly grouping?: boolean;
641
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
642
+ required: false;
643
+ };
644
+ activeSince: {
645
+ readonly type: "timestamp";
646
+ readonly description?: string;
647
+ readonly required?: boolean;
648
+ readonly sortable?: boolean;
649
+ readonly filterable?: boolean;
650
+ readonly sensitive?: boolean;
651
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
652
+ readonly locatedBy?: string;
653
+ readonly min?: string;
654
+ readonly max?: string;
655
+ readonly locale?: string;
656
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
657
+ required: false;
658
+ };
559
659
  }, "tenant">>;
560
660
  export type EntityAuditColumns = {
561
661
  readonly insertedAt: Temporal.Instant;
@@ -580,4 +680,7 @@ export type TemplateResourceRow = {
580
680
  readonly scope: string;
581
681
  readonly parentTemplateId: string | null;
582
682
  readonly status: string;
683
+ readonly draft: string | null;
684
+ readonly activeVersion: number | null;
685
+ readonly activeSince: Temporal.Instant | null;
583
686
  } & EntityAuditColumns;
@@ -1,5 +1,5 @@
1
1
  import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
2
- import { createEntity, createLongTextField, createSelectField, createTextField, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { createEntity, createLongTextField, createNumberField, createSelectField, createTextField, createTimestampField, } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { CONTENT_FORMATS, TEMPLATE_KINDS, TEMPLATE_SCOPES, TEMPLATE_STATUSES, } from "./constants.js";
4
4
  // Stored v1 events name the kind "mail-html", which is now "mail".
5
5
  const renameMailHtmlKind = {
@@ -10,7 +10,9 @@ const renameMailHtmlKind = {
10
10
  // TemplateResource — strukturierte Template-Definition mit Tenant-
11
11
  // Override-Hierarchie, Locale-Fallback und Resource-Linking via
12
12
  // file-foundation. Pro (tenantId, slug, kind, locale) genau eine Row;
13
- // scope/status/version differenzieren Lifecycle.
13
+ // scope/status/version differenzieren Lifecycle. Der Entwurf neben einer
14
+ // aktiven Fassung liegt in `draft`; die Hauptfelder bleiben die ausgelieferte
15
+ // Fassung.
14
16
  //
15
17
  // `variableSchema` + `linkedResources` sind JSON-Strings in longText
16
18
  // (Pattern aus compliance-profiles — kein dedizierter jsonbField im
@@ -44,6 +46,11 @@ export const templateResourceEntity = createEntity({
44
46
  scope: createSelectField({ required: true, options: [...TEMPLATE_SCOPES] }),
45
47
  parentTemplateId: createTextField({ personal: false, reason: "technical_reference" }),
46
48
  status: createSelectField({ required: true, options: [...TEMPLATE_STATUSES] }),
49
+ // JSON of the unpublished edit beside the active version (see template-draft.ts).
50
+ draft: createLongTextField({ personal: false, reason: "is_business_data" }),
51
+ // `version` counts every save including drafts; these two describe the served version.
52
+ activeVersion: createNumberField({ integer: true, min: 1 }),
53
+ activeSince: createTimestampField({}),
47
54
  },
48
55
  indexes: [
49
56
  {
@@ -0,0 +1,51 @@
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
+ import { type ContentFormat } from "./constants.js";
3
+ import type { TemplateResourceRow } from "./table.js";
4
+ export type TemplateDraft = {
5
+ readonly content: string | null;
6
+ readonly contentFormat: ContentFormat;
7
+ readonly variableSchema: string;
8
+ readonly meta: string | null;
9
+ readonly title: string | null;
10
+ readonly folder: string | null;
11
+ readonly parentTemplateId: string | null;
12
+ };
13
+ export declare function parseTemplateDraft(raw: string | null): TemplateDraft | null;
14
+ export declare function serializeTemplateDraft(draft: TemplateDraft): string;
15
+ export declare function activeVersionOf(row: TemplateResourceRow): number;
16
+ export declare function servedVersionOf(row: TemplateResourceRow): number;
17
+ export declare function activeSinceOf(row: TemplateResourceRow): Temporal.Instant;
18
+ export type ActiveInfo = {
19
+ readonly version: number;
20
+ readonly since: Temporal.Instant;
21
+ };
22
+ export type WorkingRow = TemplateResourceRow & {
23
+ readonly active: ActiveInfo | null;
24
+ };
25
+ export declare function isHiddenSystemDraft(user: {
26
+ readonly roles: readonly string[];
27
+ }, row: Pick<TemplateResourceRow, "scope" | "status">): boolean;
28
+ export declare function draftViewerTenantId(user: {
29
+ readonly tenantId: string;
30
+ readonly roles: readonly string[];
31
+ }, row: TemplateResourceRow): string;
32
+ export declare function toWorkingRow(row: TemplateResourceRow, viewerTenantId: string): WorkingRow;
33
+ export declare function servedStampChanges(row: {
34
+ readonly version: number;
35
+ } | null): {
36
+ activeVersion: number;
37
+ activeSince: globalThis.Temporal.Instant;
38
+ };
39
+ export declare function activeStampChanges(row: {
40
+ readonly version: number;
41
+ } | null): {
42
+ activeVersion: number;
43
+ activeSince: globalThis.Temporal.Instant;
44
+ status: string;
45
+ };
46
+ export declare function activationChanges(row: TemplateResourceRow | null): {
47
+ activeVersion: number;
48
+ activeSince: globalThis.Temporal.Instant;
49
+ status: string;
50
+ draft: null;
51
+ };