@cosmicdrift/kumiko-bundled-features 0.364.0 → 0.365.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 (161) hide show
  1. package/dist/audit/feature.js +1 -1
  2. package/dist/audit/run-escape-hatch-retention.d.ts +1 -0
  3. package/dist/audit/run-escape-hatch-retention.js +39 -12
  4. package/dist/auth-email-password/bootstrap.d.ts +1 -3
  5. package/dist/auth-email-password/feature.js +2 -0
  6. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -3
  7. package/dist/auth-email-password/handlers/invite-create.write.d.ts +2 -3
  8. package/dist/auth-email-password/handlers/invite-create.write.js +4 -1
  9. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.d.ts +1 -0
  10. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +1 -1
  11. package/dist/auth-email-password/magic-link-mail.js +6 -3
  12. package/dist/auth-email-password/mail-i18n.d.ts +3 -0
  13. package/dist/auth-email-password/mail-i18n.js +10 -0
  14. package/dist/billing-foundation/consumer-protection/termination-record.js +8 -1
  15. package/dist/billing-foundation/i18n.js +6 -0
  16. package/dist/billing-foundation/subscription-tier-sync.js +8 -2
  17. package/dist/cap-counter/book-cap-usage.js +8 -0
  18. package/dist/cap-counter/cap-reservation.js +7 -3
  19. package/dist/cap-counter/handlers/increment-rolling.write.d.ts +1 -0
  20. package/dist/cap-counter/handlers/increment-rolling.write.js +2 -0
  21. package/dist/cap-overview/handlers/tenant-caps-list.query.js +3 -1
  22. package/dist/cap-overview/handlers/tenant-options.query.js +3 -1
  23. package/dist/channel-discord/discord-channel.js +3 -2
  24. package/dist/channel-email/email-channel.js +24 -19
  25. package/dist/channel-email/smtp-transport.js +6 -1
  26. package/dist/channel-slack/slack-channel.js +3 -2
  27. package/dist/channel-teams/teams-channel.js +3 -2
  28. package/dist/crypto-shredding/handlers/forget-subject.write.js +2 -2
  29. package/dist/custom-fields/db/queries/retention.d.ts +1 -1
  30. package/dist/custom-fields/db/queries/retention.js +2 -1
  31. package/dist/data-retention/run-retention-cleanup.js +4 -5
  32. package/dist/delivery/attachment-ref.js +16 -1
  33. package/dist/delivery/delivery-service.js +71 -82
  34. package/dist/feature-toggles/toggle-runtime.d.ts +5 -3
  35. package/dist/feature-toggles/toggle-runtime.js +25 -12
  36. package/dist/files-tenant-data/index.d.ts +1 -0
  37. package/dist/files-tenant-data/index.js +2 -1
  38. package/dist/form-draft/db/queries/owned-file-refs.d.ts +1 -1
  39. package/dist/form-draft/db/queries/owned-file-refs.js +9 -2
  40. package/dist/form-draft/handlers/discard.write.js +1 -1
  41. package/dist/jobs/job-run-logger.js +5 -2
  42. package/dist/metrics/filters.d.ts +16 -1
  43. package/dist/metrics/filters.js +27 -11
  44. package/dist/personal-access-tokens/revoke-for-user.js +3 -7
  45. package/dist/secrets/handlers/delete.write.js +3 -1
  46. package/dist/secrets/write-gate.d.ts +3 -1
  47. package/dist/secrets/write-gate.js +6 -3
  48. package/dist/shared/index.d.ts +1 -0
  49. package/dist/shared/index.js +1 -0
  50. package/dist/shared/resource-image-formats.d.ts +23 -0
  51. package/dist/shared/resource-image-formats.js +21 -0
  52. package/dist/shared/sharp-input-limits.js +7 -8
  53. package/dist/step-dispatcher/webhook-runner.js +2 -2
  54. package/dist/template-resolver/compile-document-body.d.ts +3 -0
  55. package/dist/template-resolver/compile-document-body.js +63 -12
  56. package/dist/template-resolver/compile-steps-block.d.ts +10 -0
  57. package/dist/template-resolver/compile-steps-block.js +45 -0
  58. package/dist/template-resolver/constants.d.ts +4 -1
  59. package/dist/template-resolver/constants.js +10 -1
  60. package/dist/template-resolver/document-sanitizer.js +3 -0
  61. package/dist/template-resolver/document-schema.d.ts +2 -1
  62. package/dist/template-resolver/document-schema.js +6 -1
  63. package/dist/template-resolver/document-steps.d.ts +17 -0
  64. package/dist/template-resolver/document-steps.js +93 -0
  65. package/dist/template-resolver/feature.js +9 -0
  66. package/dist/template-resolver/handlers/bundle-import.d.ts +7 -1
  67. package/dist/template-resolver/handlers/bundle-import.js +63 -38
  68. package/dist/template-resolver/handlers/collection-set.write.js +4 -3
  69. package/dist/template-resolver/handlers/copy-locale.write.d.ts +1 -1
  70. package/dist/template-resolver/handlers/copy-locale.write.js +83 -55
  71. package/dist/template-resolver/handlers/diff-to-system.query.js +1 -0
  72. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +1 -1
  73. package/dist/template-resolver/handlers/import-bundle-system.write.js +16 -5
  74. package/dist/template-resolver/handlers/import-bundle.write.d.ts +1 -1
  75. package/dist/template-resolver/handlers/import-bundle.write.js +11 -1
  76. package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
  77. package/dist/template-resolver/handlers/overview.query.js +9 -1
  78. package/dist/template-resolver/handlers/resource-upload.write.d.ts +2 -2
  79. package/dist/template-resolver/handlers/resource-upload.write.js +35 -5
  80. package/dist/template-resolver/handlers/validate.query.js +4 -1
  81. package/dist/template-resolver/index.d.ts +3 -3
  82. package/dist/template-resolver/index.js +3 -3
  83. package/dist/template-resolver/layout-appearance.d.ts +1 -1
  84. package/dist/template-resolver/layout-appearance.js +19 -9
  85. package/dist/template-resolver/mail-constants.d.ts +2 -0
  86. package/dist/template-resolver/mail-constants.js +3 -0
  87. package/dist/template-resolver/resource-image.d.ts +2 -9
  88. package/dist/template-resolver/resource-image.js +2 -11
  89. package/dist/template-resolver/seeding.js +2 -0
  90. package/dist/template-resolver/store-template-resource.d.ts +53 -9
  91. package/dist/template-resolver/store-template-resource.js +250 -97
  92. package/dist/template-resolver/template-bundle.js +4 -0
  93. package/dist/template-resolver/template-draft.d.ts +3 -0
  94. package/dist/template-resolver/template-draft.js +1 -1
  95. package/dist/template-resolver/template-meta.d.ts +5 -1
  96. package/dist/template-resolver/template-meta.js +7 -0
  97. package/dist/template-resolver/template-preview.js +1 -2
  98. package/dist/template-resolver/template-resources.d.ts +0 -5
  99. package/dist/template-resolver/template-resources.js +2 -34
  100. package/dist/template-resolver/template-validation.js +12 -9
  101. package/dist/template-resolver/web/document-editor-extensions.d.ts +1 -0
  102. package/dist/template-resolver/web/document-editor-extensions.js +26 -1
  103. package/dist/template-resolver/web/document-editor.js +5 -1
  104. package/dist/template-resolver/web/document-tiptap-editor.js +30 -7
  105. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +5 -5
  106. package/dist/template-resolver/web/document-toolbar-blocks.js +2 -0
  107. package/dist/template-resolver/web/i18n.js +5 -0
  108. package/dist/template-resolver/web/index.d.ts +1 -0
  109. package/dist/template-resolver/web/index.js +1 -0
  110. package/dist/template-resolver/web/placeholder-line-editor.d.ts +4 -2
  111. package/dist/template-resolver/web/placeholder-line-editor.js +8 -6
  112. package/dist/tenant/handlers/update-member-roles.write.js +3 -6
  113. package/dist/tenant/seeding.js +23 -24
  114. package/dist/tenant-handover/move-entity-graph.js +14 -4
  115. package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +1 -0
  116. package/dist/tenant-lifecycle/run-tenant-destroy.js +8 -1
  117. package/dist/tenant-lifecycle/stages.js +3 -2
  118. package/dist/tier-engine/constants.d.ts +2 -0
  119. package/dist/tier-engine/constants.js +4 -0
  120. package/dist/tier-engine/entity.d.ts +15 -14
  121. package/dist/tier-engine/entity.js +4 -3
  122. package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +5 -0
  123. package/dist/tier-engine/handlers/set-tenant-tier.write.js +7 -4
  124. package/dist/tier-engine/index.d.ts +1 -1
  125. package/dist/tier-engine/index.js +1 -1
  126. package/dist/tier-engine/tier-resolver.d.ts +13 -13
  127. package/dist/user/constants.d.ts +1 -0
  128. package/dist/user/invited-global-roles.d.ts +2 -4
  129. package/dist/user-data-rights/email-templates.js +0 -1
  130. package/dist/user-data-rights/i18n.js +5 -0
  131. package/dist/user-data-rights/lib/update-user-lifecycle.d.ts +2 -0
  132. package/dist/user-data-rights/lib/update-user-lifecycle.js +6 -2
  133. package/dist/user-data-rights/run-forget-cleanup.js +2 -2
  134. package/dist/waitlist/feature.js +1 -1
  135. package/dist/waitlist/handlers/invite.write.js +21 -3
  136. package/dist/waitlist/handlers/reject.write.js +3 -16
  137. package/dist/waitlist/handlers/submit.write.js +29 -7
  138. package/dist/waitlist/i18n.js +9 -0
  139. package/dist/waitlist/lib.d.ts +2 -1
  140. package/dist/waitlist/lib.js +15 -0
  141. package/dist/workflow-runner/feature.js +1 -1
  142. package/package.json +10 -10
  143. package/src/audit/changes.json +12 -0
  144. package/src/auth-email-password/changes.json +18 -0
  145. package/src/billing-foundation/changes.json +12 -0
  146. package/src/cap-counter/changes.json +13 -0
  147. package/src/cap-overview/changes.json +6 -0
  148. package/src/channel-email/changes.json +18 -0
  149. package/src/delivery/changes.json +12 -0
  150. package/src/document-ingest-foundation/changes.json +6 -0
  151. package/src/feature-toggles/changes.json +7 -0
  152. package/src/jobs/changes.json +12 -0
  153. package/src/metrics/changes.json +6 -0
  154. package/src/personal-access-tokens/changes.json +6 -0
  155. package/src/secrets/changes.json +6 -0
  156. package/src/template-resolver/changes.json +81 -0
  157. package/src/tenant/changes.json +6 -0
  158. package/src/tenant-lifecycle/changes.json +6 -0
  159. package/src/tier-engine/changes.json +12 -0
  160. package/src/user-data-rights/changes.json +6 -0
  161. package/src/waitlist/changes.json +18 -0
@@ -1,7 +1,7 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import { requireForTenant } from "../../shared/index.js";
4
- import { attachmentLimitFailure, attachmentNeedsMailTemplate, readTemplateResourceMaxBytes, storeBundleAttachment, storeTemplateResource, } from "../store-template-resource.js";
4
+ import { attachmentLimitFailure, attachmentNeedsMailTemplate, createPreparedBundleResourceEntry, prepareTemplateAttachmentBeforeTransaction, prepareTemplateResourceBeforeTransaction, readTemplateResourceMaxBytes, releaseAll, } from "../store-template-resource.js";
5
5
  import { templateResourcesTable } from "../table.js";
6
6
  import { attachTemplateResources } from "../template-resources.js";
7
7
  import { resolveTemplateVariables } from "../template-validation.js";
@@ -49,56 +49,81 @@ async function collectBundleIssues(args, status) {
49
49
  }
50
50
  return issues;
51
51
  }
52
- async function storeBundleResources(args) {
53
- const linked = {};
54
- const pdfLinks = Object.entries(args.bundle.pdfAttachments);
55
- const attachmentCount = Object.values(args.bundle.resources).filter((resource) => resource.purpose === "attachment")
56
- .length + pdfLinks.length;
57
- if (attachmentCount > 0 && args.bundle.kind !== "mail")
52
+ function checkBundleAttachments(bundle) {
53
+ const attachmentCount = Object.values(bundle.resources).filter((resource) => resource.purpose === "attachment").length +
54
+ Object.keys(bundle.pdfAttachments).length;
55
+ if (attachmentCount > 0 && bundle.kind !== "mail")
58
56
  return attachmentNeedsMailTemplate();
59
57
  if (attachmentCount > MAX_FILE_ATTACHMENTS_PER_TEMPLATE)
60
58
  return attachmentLimitFailure();
61
- // The document template is not looked up: a bundle may carry it later than the mail.
62
- for (const [key, link] of pdfLinks)
63
- linked[key] = { kind: "rendered-pdf", ...link };
59
+ return { isSuccess: true, data: null };
60
+ }
61
+ // beforeTransaction half: validates the bundle and uploads every resource file to the target
62
+ // tenant's storage. A failed or throwing upload releases the ones before it.
63
+ export async function prepareTemplateBundleBeforeTransaction(args) {
64
+ const refused = refuseTenantScopeUnderSystemTenant(args.scope, args.tenantId);
65
+ if (refused !== null)
66
+ return refused;
67
+ const issues = await collectBundleIssues(args, args.status);
68
+ if (issues.length > 0)
69
+ return writeFailure(new ValidationError({ fields: [...issues] }));
70
+ const attachmentCheck = checkBundleAttachments(args.bundle);
71
+ if (!attachmentCheck.isSuccess)
72
+ return attachmentCheck;
64
73
  const maxBytes = await readTemplateResourceMaxBytes(args.ctx.config, args.ctx.registry);
65
- // A bundle is untrusted input like an upload: the same re-encoding decides the stored
66
- // content type, so a declared image/png can't smuggle SVG or HTML into a mail or PDF.
67
- for (const [key, resource] of Object.entries(args.bundle.resources)) {
68
- const bytes = Uint8Array.from(Buffer.from(resource.base64, "base64"));
69
- if (resource.purpose === "attachment") {
70
- const attachment = await storeBundleAttachment({
74
+ const prepared = new Map();
75
+ const releases = [];
76
+ try {
77
+ // A bundle is untrusted input like an upload: the same re-encoding decides the stored
78
+ // content type, so a declared image/png can't smuggle SVG or HTML into a mail or PDF.
79
+ for (const [key, resource] of Object.entries(args.bundle.resources)) {
80
+ const bytes = Uint8Array.from(Buffer.from(resource.base64, "base64"));
81
+ const common = {
71
82
  ctx: args.ctx,
72
83
  user: args.user,
73
84
  tenantId: args.tenantId,
74
85
  bytes,
75
86
  maxBytes,
76
87
  key,
77
- fileName: resource.fileName,
78
- });
79
- if (!attachment.isSuccess)
80
- return attachment;
81
- linked[key] = attachment.data;
82
- continue;
88
+ };
89
+ const result = resource.purpose === "attachment"
90
+ ? await prepareTemplateAttachmentBeforeTransaction({
91
+ ...common,
92
+ fileName: resource.fileName,
93
+ })
94
+ : await prepareTemplateResourceBeforeTransaction(common);
95
+ if (!result.isSuccess) {
96
+ await releaseAll(releases);
97
+ return result;
98
+ }
99
+ if (result.release)
100
+ releases.push(result.release);
101
+ prepared.set(key, result.data);
83
102
  }
84
- const stored = await storeTemplateResource({
85
- ctx: args.ctx,
86
- user: args.user,
87
- tenantId: args.tenantId,
88
- bytes,
89
- maxBytes,
90
- key,
91
- });
92
- if (!stored.isSuccess)
93
- return stored;
94
- linked[key] = stored.data.fileRefId;
103
+ }
104
+ catch (cause) {
105
+ await releaseAll(releases);
106
+ throw cause;
107
+ }
108
+ return { isSuccess: true, data: prepared, release: () => releaseAll(releases) };
109
+ }
110
+ async function linkPreparedBundleResources(args, prepared) {
111
+ const linked = {};
112
+ // The document template is not looked up: a bundle may carry it later than the mail.
113
+ for (const [key, link] of Object.entries(args.bundle.pdfAttachments)) {
114
+ linked[key] = { kind: "rendered-pdf", ...link };
115
+ }
116
+ for (const [key, file] of prepared) {
117
+ const entry = await createPreparedBundleResourceEntry({ ctx: args.ctx, user: args.user, tenantId: args.tenantId }, file);
118
+ if (!entry.isSuccess)
119
+ return entry;
120
+ linked[key] = entry.data;
95
121
  }
96
122
  return { isSuccess: true, data: linked };
97
123
  }
98
- // Validates every locale first, stores the resource files, then writes one row
99
- // per locale, so a bad locale never leaves files or half a bundle behind. An image
100
- // that fails re-encoding stops the import before any row is written; files stored
101
- // for the resources before it stay as unlinked FileRefs, like a replaced upload.
124
+ // Validates every locale first, so a bad locale is refused before anything is written.
125
+ // The resource files are uploaded in the beforeTransaction phase; a rolled-back transaction
126
+ // releases them there. A crash between the upload and the commit leaves the binaries unreferenced.
102
127
  export async function importTemplateBundle(args) {
103
128
  const refused = refuseTenantScopeUnderSystemTenant(args.scope, args.tenantId);
104
129
  if (refused !== null)
@@ -106,7 +131,7 @@ export async function importTemplateBundle(args) {
106
131
  const issues = await collectBundleIssues(args, args.status);
107
132
  if (issues.length > 0)
108
133
  return writeFailure(new ValidationError({ fields: [...issues] }));
109
- const linked = await storeBundleResources(args);
134
+ const linked = await linkPreparedBundleResources(args, args.prepared);
110
135
  if (!linked.isSuccess)
111
136
  return linked;
112
137
  const variableSchema = args.bundle.variableSchema ?? {};
@@ -3,7 +3,7 @@ import { defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
4
  import * as z from "zod";
5
5
  import { requireForTenant } from "../../shared/index.js";
6
- import { COLLECTION_REFUSED_KINDS } from "../constants.js";
6
+ import { isCollectionRefusedKind } from "../constants.js";
7
7
  import { documentViolationsToFieldIssues, sanitizeTemplateBody } from "../document-sanitizer.js";
8
8
  import { collectionStore, DEFAULT_COLLECTION_ACCESS, } from "./collection-shared.js";
9
9
  import { contentFormatSchema, folderSchema, localeSchema, slugSchema } from "./shared.js";
@@ -33,11 +33,12 @@ export function makeCollectionSetWrite(collection) {
33
33
  ? `Creates or overwrites the caller's own "${collection.id}" entry at a slug and locale, live on save with no draft stage; it always writes the acting user's entry, never another user's.`
34
34
  : `Creates or overwrites the tenant-wide "${collection.id}" entry at a slug and locale, live on save with no draft stage; the change is visible to everyone who may reach this shared collection.`,
35
35
  handler: async (event, ctx) => {
36
- if (COLLECTION_REFUSED_KINDS.includes(collection.kind)) {
36
+ // Defense in depth: createTemplateResolverFeature already refuses these kinds at mount.
37
+ if (isCollectionRefusedKind(collection.kind)) {
37
38
  return writeFailure(new ValidationError({
38
39
  fields: [
39
40
  {
40
- path: "kind",
41
+ path: "",
41
42
  code: "kind_not_allowed",
42
43
  i18nKey: "templateResolver.errors.collection.kindNotAllowed",
43
44
  params: { detail: collection.kind },
@@ -16,4 +16,4 @@ export declare const copyLocaleWrite: import("@cosmicdrift/kumiko-types/define-h
16
16
  system: "system";
17
17
  tenant: "tenant";
18
18
  }>>;
19
- }, z.core.$strip>, import("./template-write.js").WrittenTemplateRow, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
19
+ }, z.core.$strip>, import("./template-write.js").WrittenTemplateRow, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, readonly import("../store-template-resource.js").PreparedLinkedResourceCopy[]>;
@@ -7,9 +7,10 @@ import { requireForTenant } from "../../shared/index.js";
7
7
  import { CONTENT_FORMATS } from "../constants.js";
8
8
  import { parseLinkedResourceEntries } from "../linked-resources.js";
9
9
  import { TemplateResolverErrors } from "../qualified-names.js";
10
+ import { createLinkedResourceCopies, prepareLinkedResourceCopiesBeforeTransaction, } from "../store-template-resource.js";
10
11
  import { templateResourcesTable } from "../table.js";
11
12
  import { toWorkingRow } from "../template-draft.js";
12
- import { attachTemplateResources, duplicateLinkedResourceFiles } from "../template-resources.js";
13
+ import { attachTemplateResources } from "../template-resources.js";
13
14
  import { resolveTemplateVariables } from "../template-validation.js";
14
15
  import { targetExistsIssue } from "./bundle-import.js";
15
16
  import { kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
@@ -39,54 +40,88 @@ async function findCopySource(ctx, where) {
39
40
  status: "active",
40
41
  });
41
42
  }
42
- export const copyLocaleWrite = defineWriteHandler({
43
- name: "copy-locale",
44
- schema: z.object({
45
- slug: templateSlugSchema,
46
- kind: kindSchema,
47
- fromLocale: localeSchema,
48
- toLocale: localeSchema,
49
- overwrite: z.boolean().default(false),
50
- /** "system" copies between system-default rows; SystemAdmin only. */
51
- scope: z.enum(["tenant", "system"]).default("tenant"),
52
- }),
53
- access: { roles: ["TenantAdmin", "SystemAdmin"] },
54
- description: "Copies the text of one locale of a template into another locale as a draft override of the caller's tenant, taking the tenant's own row or else the system default as the source; use it as the starting point for a translation. It refuses an existing target unless overwrite is set. With scope system (SystemAdmin only) both source and target are system-default rows and the target lands as a system draft; any other caller gets a 403.",
55
- handler: async (event, ctx) => {
56
- const { slug, kind, fromLocale, toLocale, overwrite, scope } = event.payload;
57
- if (scope === "system" && !event.user.roles.includes("SystemAdmin")) {
58
- return writeFailure(new AccessDeniedError({
59
- i18nKey: "errors.access.denied",
60
- details: { reason: TemplateResolverErrors.systemAdminRequired },
61
- }));
62
- }
63
- const tenantId = scope === "system" ? SYSTEM_TENANT_ID : event.user.tenantId;
64
- const refused = refuseTenantScopeUnderSystemTenant(scope, tenantId);
65
- if (refused !== null)
66
- return refused;
67
- const readDb = readDbFor(ctx, scope);
68
- const storedSource = await findCopySource(ctx, {
69
- scope,
43
+ const copyLocaleSchema = z.object({
44
+ slug: templateSlugSchema,
45
+ kind: kindSchema,
46
+ fromLocale: localeSchema,
47
+ toLocale: localeSchema,
48
+ overwrite: z.boolean().default(false),
49
+ /** "system" copies between system-default rows; SystemAdmin only. */
50
+ scope: z.enum(["tenant", "system"]).default("tenant"),
51
+ });
52
+ // Runs before the upload (reject early, store nothing) and again in the transaction on the fresh rows.
53
+ async function checkCopyRequest(ctx, event) {
54
+ const { slug, kind, fromLocale, toLocale, overwrite, scope } = event.payload;
55
+ if (scope === "system" && !event.user.roles.includes("SystemAdmin")) {
56
+ return writeFailure(new AccessDeniedError({
57
+ i18nKey: "errors.access.denied",
58
+ details: { reason: TemplateResolverErrors.systemAdminRequired },
59
+ }));
60
+ }
61
+ const tenantId = scope === "system" ? SYSTEM_TENANT_ID : event.user.tenantId;
62
+ const refused = refuseTenantScopeUnderSystemTenant(scope, tenantId);
63
+ if (refused !== null)
64
+ return refused;
65
+ const readDb = readDbFor(ctx, scope);
66
+ const storedSource = await findCopySource(ctx, {
67
+ scope,
68
+ tenantId,
69
+ slug,
70
+ kind,
71
+ locale: fromLocale,
72
+ });
73
+ const source = storedSource && toWorkingRow(storedSource, tenantId);
74
+ const contentFormat = source?.contentFormat;
75
+ if (!source || !isOneOf(CONTENT_FORMATS, contentFormat)) {
76
+ return writeFailure(new NotFoundError("template-resource", `${slug}/${fromLocale}`));
77
+ }
78
+ if (!overwrite) {
79
+ const target = await fetchOne(readDb, templateResourcesTable, {
70
80
  tenantId,
71
81
  slug,
72
82
  kind,
73
- locale: fromLocale,
83
+ locale: toLocale,
74
84
  });
75
- const source = storedSource && toWorkingRow(storedSource, tenantId);
76
- if (!source || !isOneOf(CONTENT_FORMATS, source.contentFormat)) {
77
- return writeFailure(new NotFoundError("template-resource", `${slug}/${fromLocale}`));
78
- }
79
- if (!overwrite) {
80
- const target = await fetchOne(readDb, templateResourcesTable, {
81
- tenantId,
82
- slug,
83
- kind,
84
- locale: toLocale,
85
- });
86
- if (target) {
87
- return writeFailure(new ValidationError({ fields: [targetExistsIssue(toLocale)] }));
88
- }
85
+ if (target) {
86
+ return writeFailure(new ValidationError({ fields: [targetExistsIssue(toLocale)] }));
89
87
  }
88
+ }
89
+ return {
90
+ isSuccess: true,
91
+ data: {
92
+ scope,
93
+ tenantId,
94
+ source,
95
+ contentFormat,
96
+ sourceResources: parseLinkedResourceEntries(source.linkedResources),
97
+ sourceIsForeign: source.tenantId !== tenantId,
98
+ },
99
+ };
100
+ }
101
+ export const copyLocaleWrite = defineWriteHandler({
102
+ name: "copy-locale",
103
+ schema: copyLocaleSchema,
104
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
105
+ description: "Copies the text of one locale of a template into another locale as a draft override of the caller's tenant, taking the tenant's own row or else the system default as the source; use it as the starting point for a translation. It refuses an existing target unless overwrite is set. With scope system (SystemAdmin only) both source and target are system-default rows and the target lands as a system draft; any other caller gets a 403.",
106
+ beforeTransaction: async (event, ctx) => {
107
+ const request = await checkCopyRequest(ctx, event);
108
+ if (!request.isSuccess)
109
+ return request;
110
+ if (!request.data.sourceIsForeign)
111
+ return { isSuccess: true, data: [] };
112
+ return prepareLinkedResourceCopiesBeforeTransaction({
113
+ ctx,
114
+ user: event.user,
115
+ tenantId: request.data.tenantId,
116
+ resources: request.data.sourceResources,
117
+ });
118
+ },
119
+ handler: async (event, ctx) => {
120
+ const { slug, kind, toLocale } = event.payload;
121
+ const request = await checkCopyRequest(ctx, event);
122
+ if (!request.isSuccess)
123
+ return request;
124
+ const { scope, tenantId, source, contentFormat, sourceResources, sourceIsForeign } = request.data;
90
125
  const variableSchema = storedRecord(source.variableSchema);
91
126
  const written = await writeTemplateRow({
92
127
  ctx,
@@ -100,7 +135,7 @@ export const copyLocaleWrite = defineWriteHandler({
100
135
  kind,
101
136
  locale: toLocale,
102
137
  content: source.content,
103
- contentFormat: source.contentFormat,
138
+ contentFormat,
104
139
  variableSchema,
105
140
  meta: parseStoredJson(source.meta),
106
141
  title: source.title,
@@ -109,16 +144,9 @@ export const copyLocaleWrite = defineWriteHandler({
109
144
  });
110
145
  if (!written.isSuccess)
111
146
  return written;
112
- const sourceResources = parseLinkedResourceEntries(source.linkedResources);
113
- // A system source's files belong to the system tenant: the tenant gets its own copies so
114
- // previews, exports and the tenant's override keep working independent of the system row.
115
- const resources = source.tenantId === tenantId
116
- ? { isSuccess: true, data: sourceResources }
117
- : await duplicateLinkedResourceFiles(ctx, {
118
- user: event.user,
119
- tenantId,
120
- resources: sourceResources,
121
- });
147
+ const resources = sourceIsForeign
148
+ ? await createLinkedResourceCopies({ ctx, user: event.user, tenantId }, event.prepared)
149
+ : { isSuccess: true, data: sourceResources };
122
150
  if (!resources.isSuccess)
123
151
  return resources;
124
152
  const attached = await attachTemplateResources(ctx, {
@@ -22,6 +22,7 @@ export const diffToSystemQuery = defineQueryHandler({
22
22
  slug: tenantRow.slug,
23
23
  kind: tenantRow.kind,
24
24
  locale: tenantRow.locale,
25
+ status: "active",
25
26
  });
26
27
  if (!systemRow || isHiddenSystemDraft(query.user, systemRow))
27
28
  throw new NotFoundError("template-resource", tenantRow.slug);
@@ -36,4 +36,4 @@ export declare const importBundleSystemWrite: import("@cosmicdrift/kumiko-types/
36
36
  }, z.core.$strict>;
37
37
  }, z.core.$strip>, {
38
38
  ids: Record<string, string>;
39
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
39
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, import("./bundle-import.js").PreparedBundleResources>;
@@ -2,7 +2,7 @@ import { defineWriteHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framew
2
2
  import { ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import * as z from "zod";
4
4
  import { templateBundleSchema } from "../template-bundle.js";
5
- import { importTemplateBundle } from "./bundle-import.js";
5
+ import { importTemplateBundle, prepareTemplateBundleBeforeTransaction } from "./bundle-import.js";
6
6
  export const importBundleSystemWrite = defineWriteHandler({
7
7
  name: "import-bundle-system",
8
8
  schema: z.object({ bundle: templateBundleSchema }),
@@ -10,10 +10,11 @@ export const importBundleSystemWrite = defineWriteHandler({
10
10
  // A system template change reaches every tenant without an override.
11
11
  agent: { risk: "high" },
12
12
  description: "Imports a template bundle as system-default templates, active straight away and overwriting existing system rows; SystemAdmin only. Bundles with resource files need a caller in the system tenant, because the files are stored in the caller's file space.",
13
- handler: (event, ctx) => {
13
+ beforeTransaction: async (event, ctx) => {
14
14
  const { bundle } = event.payload;
15
+ // Must run before any upload.
15
16
  if (Object.keys(bundle.resources).length > 0 && event.user.tenantId !== SYSTEM_TENANT_ID) {
16
- return Promise.resolve(writeFailure(new ValidationError({
17
+ return writeFailure(new ValidationError({
17
18
  fields: [
18
19
  {
19
20
  path: "bundle.resources",
@@ -21,9 +22,9 @@ export const importBundleSystemWrite = defineWriteHandler({
21
22
  i18nKey: "templateResolver.errors.template.systemResourcesNeedSystemTenant",
22
23
  },
23
24
  ],
24
- })));
25
+ }));
25
26
  }
26
- return importTemplateBundle({
27
+ return prepareTemplateBundleBeforeTransaction({
27
28
  ctx,
28
29
  user: event.user,
29
30
  tenantId: SYSTEM_TENANT_ID,
@@ -33,4 +34,14 @@ export const importBundleSystemWrite = defineWriteHandler({
33
34
  overwrite: true,
34
35
  });
35
36
  },
37
+ handler: (event, ctx) => importTemplateBundle({
38
+ ctx,
39
+ user: event.user,
40
+ tenantId: SYSTEM_TENANT_ID,
41
+ scope: "system",
42
+ status: "active",
43
+ bundle: event.payload.bundle,
44
+ overwrite: true,
45
+ prepared: event.prepared,
46
+ }),
36
47
  });
@@ -37,4 +37,4 @@ export declare const importBundleWrite: import("@cosmicdrift/kumiko-types/define
37
37
  overwrite: z.ZodDefault<z.ZodBoolean>;
38
38
  }, z.core.$strip>, {
39
39
  ids: Record<string, string>;
40
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
40
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, import("./bundle-import.js").PreparedBundleResources>;
@@ -1,12 +1,21 @@
1
1
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import * as z from "zod";
3
3
  import { templateBundleSchema } from "../template-bundle.js";
4
- import { importTemplateBundle } from "./bundle-import.js";
4
+ import { importTemplateBundle, prepareTemplateBundleBeforeTransaction } from "./bundle-import.js";
5
5
  export const importBundleWrite = defineWriteHandler({
6
6
  name: "import-bundle",
7
7
  schema: z.object({ bundle: templateBundleSchema, overwrite: z.boolean().default(false) }),
8
8
  access: { roles: ["TenantAdmin", "SystemAdmin"] },
9
9
  description: "Imports a template bundle (all locales plus resource files, as produced by export-bundle) into the caller's tenant as drafts; it refuses a locale that already has an override unless overwrite is set, and publish makes the drafts live.",
10
+ beforeTransaction: (event, ctx) => prepareTemplateBundleBeforeTransaction({
11
+ ctx,
12
+ user: event.user,
13
+ tenantId: event.user.tenantId,
14
+ scope: "tenant",
15
+ status: "draft",
16
+ bundle: event.payload.bundle,
17
+ overwrite: event.payload.overwrite,
18
+ }),
10
19
  handler: (event, ctx) => importTemplateBundle({
11
20
  ctx,
12
21
  user: event.user,
@@ -15,5 +24,6 @@ export const importBundleWrite = defineWriteHandler({
15
24
  status: "draft",
16
25
  bundle: event.payload.bundle,
17
26
  overwrite: event.payload.overwrite,
27
+ prepared: event.prepared,
18
28
  }),
19
29
  });
@@ -34,6 +34,8 @@ export type OverviewResponse = {
34
34
  readonly rows: readonly OverviewRow[];
35
35
  readonly nextCursor: null;
36
36
  readonly total: number;
37
+ /** True when the tenant has more template rows than the overview reads; rows and total are then incomplete. */
38
+ readonly truncated: boolean;
37
39
  };
38
40
  export declare function effectiveMailLayoutSlugs(rows: readonly TemplateResourceRow[]): EffectiveMailLayoutSlugs;
39
41
  export declare const overviewQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"overview", z.ZodObject<{
@@ -61,5 +63,6 @@ export declare const overviewQuery: import("@cosmicdrift/kumiko-types/define-han
61
63
  rows: OverviewRow[];
62
64
  nextCursor: null;
63
65
  total: number;
66
+ truncated: boolean;
64
67
  }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
65
68
  export {};
@@ -186,7 +186,14 @@ export const overviewQuery = defineQueryHandler({
186
186
  description: "Lists mail, PDF and layout templates as one row per slug and kind for the caller's tenant (optionally only the mails and PDFs using one layout slug): where the effective version comes from (own override or system), its locales, status and version (the served one; `draftVersion` is set when an unpublished draft waits beside it, `activeSince` says since when the served version applies), the system version it overrides, the layout it uses and which notifications or templates use it; use it to browse and filter all templates at a glance.",
187
187
  handler: async (query, ctx) => {
188
188
  const payload = query.payload;
189
- const rows = await selectMany(ctx.db, templateResourcesTable, { kind: { in: [...OVERVIEW_KINDS] } }, { limit: OVERVIEW_ROW_LIMIT });
189
+ const fetchedRows = await selectMany(ctx.db, templateResourcesTable, { kind: { in: [...OVERVIEW_KINDS] } }, { orderBy: { col: "id", direction: "asc" }, limit: OVERVIEW_ROW_LIMIT + 1 });
190
+ const truncated = fetchedRows.length > OVERVIEW_ROW_LIMIT;
191
+ const rows = truncated ? fetchedRows.slice(0, OVERVIEW_ROW_LIMIT) : fetchedRows;
192
+ if (truncated) {
193
+ ctx.log?.warn("template overview: row limit reached, result is incomplete", {
194
+ limit: OVERVIEW_ROW_LIMIT,
195
+ });
196
+ }
190
197
  const visibleRows = rows.filter((row) => !isHiddenSystemDraft(query.user, row));
191
198
  const { drafts, layoutSlugByDocument } = buildDrafts(groupBySlugAndKind(visibleRows), query.user);
192
199
  const effectiveLayouts = effectiveMailLayoutSlugs(visibleRows);
@@ -200,6 +207,7 @@ export const overviewQuery = defineQueryHandler({
200
207
  rows: filtered.slice(offset, offset + payload.limit),
201
208
  nextCursor: null,
202
209
  total: filtered.length,
210
+ truncated,
203
211
  };
204
212
  },
205
213
  });
@@ -1,5 +1,5 @@
1
1
  import * as z from "zod";
2
- import { type StoredTemplateAttachment, type StoredTemplateResource } from "../store-template-resource.js";
2
+ import { type PreparedTemplateFile, type StoredTemplateAttachment, type StoredTemplateResource } from "../store-template-resource.js";
3
3
  export declare const resourceUploadWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"resource-upload", z.ZodObject<{
4
4
  templateId: z.ZodOptional<z.ZodString>;
5
5
  key: z.ZodOptional<z.ZodString>;
@@ -10,4 +10,4 @@ export declare const resourceUploadWrite: import("@cosmicdrift/kumiko-types/defi
10
10
  image: "image";
11
11
  }>>;
12
12
  fileName: z.ZodOptional<z.ZodString>;
13
- }, z.core.$strip>, StoredTemplateAttachment | StoredTemplateResource, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
13
+ }, z.core.$strip>, StoredTemplateAttachment | StoredTemplateResource, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, PreparedTemplateFile>;
@@ -1,10 +1,10 @@
1
- import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { NotFoundError, UnprocessableError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import * as z from "zod";
4
4
  import { FileRefTooLargeError, fileContextForTenant, readFileRefContent, } from "../../file-foundation/index.js";
5
5
  import { attachmentFileNameSchema } from "../../shared/index.js";
6
6
  import { TemplateResolverErrors } from "../qualified-names.js";
7
- import { readTemplateResourceMaxBytes, storeTemplateAttachment, storeTemplateResource, } from "../store-template-resource.js";
7
+ import { prepareTemplateAttachmentBeforeTransaction, prepareTemplateResourceBeforeTransaction, readTemplateResourceMaxBytes, storePreparedTemplateAttachment, storePreparedTemplateResource, } from "../store-template-resource.js";
8
8
  import { RESOURCE_PURPOSES, resourceKeySchema, resourceTargetTenantId } from "./resource-shared.js";
9
9
  function attachmentTargetOf(input) {
10
10
  const { templateId, key, fileName } = input;
@@ -37,7 +37,7 @@ export const resourceUploadWrite = defineWriteHandler({
37
37
  }),
38
38
  access: { roles: ["TenantAdmin", "SystemAdmin"] },
39
39
  description: "Turns a file uploaded via POST /api/files (multipart, no entityType) into a template resource. With purpose image (default) the upload is checked against the resource-max-bytes cap, re-encoded to a supported image format and stored as a new file; with templateId and key it is linked into that template under the key, without them only the file is created. With purpose attachment, templateId, key and fileName are required: a PDF, PNG or JPEG is stored unchanged and linked into a mail template as a file attached to every mail sent from it, at most 10 per template. A cap above the files route's maxUploadSize has no effect.",
40
- handler: async (event, ctx) => {
40
+ beforeTransaction: async (event, ctx) => {
41
41
  const { templateId, key, sourceFileRefId, purpose, fileName } = event.payload;
42
42
  const attachmentTarget = purpose === "attachment" ? attachmentTargetOf({ templateId, key, fileName }) : null;
43
43
  if (purpose === "attachment" && attachmentTarget === null) {
@@ -76,7 +76,7 @@ export const resourceUploadWrite = defineWriteHandler({
76
76
  // The source upload stays as an unclaimed file: bundled-features cannot reach the fileRef executor to delete it.
77
77
  const tenantId = resourceTargetTenantId(event.payload.tenantIdOverride, event.user.tenantId);
78
78
  if (attachmentTarget !== null) {
79
- return storeTemplateAttachment({
79
+ return prepareTemplateAttachmentBeforeTransaction({
80
80
  ctx,
81
81
  user: event.user,
82
82
  tenantId,
@@ -85,7 +85,7 @@ export const resourceUploadWrite = defineWriteHandler({
85
85
  ...attachmentTarget,
86
86
  });
87
87
  }
88
- return storeTemplateResource({
88
+ return prepareTemplateResourceBeforeTransaction({
89
89
  ctx,
90
90
  user: event.user,
91
91
  tenantId,
@@ -95,4 +95,34 @@ export const resourceUploadWrite = defineWriteHandler({
95
95
  ...(key !== undefined && { key }),
96
96
  });
97
97
  },
98
+ handler: async (event, ctx) => {
99
+ const { templateId, key } = event.payload;
100
+ const tenantId = resourceTargetTenantId(event.payload.tenantIdOverride, event.user.tenantId);
101
+ const { prepared } = event;
102
+ if (prepared.kind === "attachment") {
103
+ // The phase accepts attachments only with a complete target, so these are set.
104
+ if (templateId === undefined || key === undefined) {
105
+ return writeFailure(new UnprocessableError(TemplateResolverErrors.attachmentInvalid, {
106
+ i18nKey: "templateResolver.errors.attachment.invalid",
107
+ details: { code: "target_required" },
108
+ }));
109
+ }
110
+ return storePreparedTemplateAttachment({
111
+ ctx,
112
+ user: event.user,
113
+ tenantId,
114
+ prepared,
115
+ templateId,
116
+ key,
117
+ });
118
+ }
119
+ return storePreparedTemplateResource({
120
+ ctx,
121
+ user: event.user,
122
+ tenantId,
123
+ prepared,
124
+ ...(templateId !== undefined && { templateId }),
125
+ ...(key !== undefined && { key }),
126
+ });
127
+ },
98
128
  });
@@ -3,6 +3,7 @@ import { defineQueryHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-frame
3
3
  import * as z from "zod";
4
4
  import { imageResourceKeysOf } from "../linked-resources.js";
5
5
  import { templateResourcesTable } from "../table.js";
6
+ import { isSystemAdmin } from "../template-draft.js";
6
7
  import { resolveTemplateVariables, systemPlaceholdersFor } from "../template-validation.js";
7
8
  import { contentFormatSchema, kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
8
9
  import { evaluateTemplateWrite } from "./template-gates.js";
@@ -20,7 +21,9 @@ async function knownResourceKeys(db, where) {
20
21
  // The dry run of the write pipeline; preview feeds on the evaluation, validate returns the response.
21
22
  export async function validateTemplateDraft(ctx, user, payload) {
22
23
  const variables = resolveTemplateVariables(ctx.registry, payload.slug, payload.variableSchema);
23
- const { slug, kind, scope, locale } = payload;
24
+ const { slug, kind, locale } = payload;
25
+ // The dry run must not promise what the caller's write handlers would refuse.
26
+ const scope = isSystemAdmin(user) ? payload.scope : "tenant";
24
27
  const known = [
25
28
  ...(await knownResourceKeys(ctx.db, {
26
29
  tenantId: SYSTEM_TENANT_ID,
@@ -1,6 +1,6 @@
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_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";
3
+ export { CONTENT_FORMATS, type ContentFormat, DEFAULT_MAIL_LAYOUT_SLUG, DEFAULT_PDF_LAYOUT_SLUG, DOCUMENT_HTML_KINDS, type DocumentHtmlKind, FALLBACK_LOCALE, NON_RENDER_KINDS, type NonRenderKind, PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, type PdfOrientation, type PdfPageFormat, RENDER_KINDS, type RenderKind, SYSTEM_TENANT_ID, TEMPLATE_KINDS, TEMPLATE_SCOPES, TEMPLATE_STATUSES, TEXT_BLOCK_KIND, type TemplateKind, type TemplateScope, type TemplateStatus, UPSERT_KINDS, type UpsertKind, } from "./constants.js";
4
4
  export { type DocumentViolation, type DocumentViolationCode, documentViolationsToFieldIssues, type SanitizeBodyResult, type SanitizeHtmlResult, sanitizeDocumentHtml, sanitizeTemplateBody, } from "./document-sanitizer.js";
5
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";
@@ -13,10 +13,10 @@ export { fileAttachmentsOf, imageLinksOf, type LinkedResourceEntry, parseLinkedR
13
13
  export { prepareDocumentForPrint } from "./print-document.js";
14
14
  export { TEMPLATE_RESOLVER_FEATURE, TemplateResolverErrors, TemplateResolverHandlers, TemplateResolverQueries, } from "./qualified-names.js";
15
15
  export { mapRecordToVariables } from "./record-mapping.js";
16
- export { readTemplateResourceMaxBytes, type StoredTemplateResource, storeTemplateResource, } from "./store-template-resource.js";
16
+ export { type PreparedTemplateImage, prepareTemplateResourceBeforeTransaction, readTemplateResourceMaxBytes, type StoredTemplateResource, storePreparedTemplateResource, } from "./store-template-resource.js";
17
17
  export { type TemplateResourceRow, templateResourceEntity, templateResourcesTable, } from "./table.js";
18
18
  export { type TemplateBundle, templateBundleSchema } from "./template-bundle.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
+ export { isMailTemplateMeta, type LayoutTemplateMeta, MAIL_CATEGORIES, MAIL_TONES, type MailCategory, type MailLayoutMeta, type MailTemplateMeta, type MailTone, type PdfTemplateMeta, parseTemplateMeta, pdfAttachmentFileNamePatternSchema, readMailLayoutMeta, readPdfTemplateMeta, type TemplateMeta, type TemplateMetaIssue, type TemplateMetaResult, templateSlugSchema, } from "./template-meta.js";
20
20
  export { type DocumentDraftOutcome, evaluateDocumentDraft } from "./template-preview.js";
21
21
  export { readTemplateResource, type TemplateResourceFile } from "./template-resources.js";
22
22
  export { collectTemplateUsage, type TemplateUsageEntry } from "./template-usage.js";