@cosmicdrift/kumiko-bundled-features 0.374.0 → 0.377.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 (205) hide show
  1. package/dist/account-security/constants.d.ts +1 -0
  2. package/dist/account-security/constants.js +1 -0
  3. package/dist/account-security/index.d.ts +1 -1
  4. package/dist/account-security/index.js +1 -1
  5. package/dist/admin-shell/web/client-plugin.d.ts +1 -1
  6. package/dist/audit/web/client-plugin.d.ts +1 -1
  7. package/dist/auth-email-password/mail-notifications.js +1 -1
  8. package/dist/auth-email-password/web/auth-client.js +1 -1
  9. package/dist/auth-email-password/web/auth-form-primitives.js +1 -1
  10. package/dist/auth-email-password/web/default-topbar-actions.js +1 -1
  11. package/dist/auth-email-password/web/index.js +1 -1
  12. package/dist/auth-email-password/web/session.js +1 -1
  13. package/dist/auth-email-password/web/tenant-menu-items.js +1 -1
  14. package/dist/auth-email-password/web/tenant-switcher.js +1 -1
  15. package/dist/auth-email-password/web/user-menu.js +1 -1
  16. package/dist/auth-foundation/env.d.ts +9 -0
  17. package/dist/auth-foundation/env.js +21 -0
  18. package/dist/auth-foundation/index.d.ts +1 -0
  19. package/dist/auth-foundation/index.js +1 -0
  20. package/dist/auth-mfa/schema/user-mfa.d.ts +6 -6
  21. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +1 -1
  22. package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -1
  23. package/dist/billing-foundation/contact-card.d.ts +2 -0
  24. package/dist/billing-foundation/contact-card.js +16 -0
  25. package/dist/billing-foundation/entities.d.ts +8 -8
  26. package/dist/billing-foundation/handlers/billing-plans.query.js +1 -1
  27. package/dist/billing-foundation/index.d.ts +1 -1
  28. package/dist/billing-foundation/plan-catalog.js +1 -0
  29. package/dist/billing-foundation/projection.d.ts +8 -8
  30. package/dist/billing-foundation/types.d.ts +11 -0
  31. package/dist/billing-foundation/validate-options.js +13 -0
  32. package/dist/billing-foundation/web/billing-contact-plan.d.ts +5 -0
  33. package/dist/billing-foundation/web/billing-contact-plan.js +17 -0
  34. package/dist/billing-foundation/web/billing-plans-panel.js +5 -13
  35. package/dist/billing-foundation/web/cancel-contract-dialog.js +2 -2
  36. package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +1 -1
  37. package/dist/billing-foundation/web/checkout-consent-dialog.js +1 -0
  38. package/dist/billing-foundation/web/client-plugin.d.ts +1 -1
  39. package/dist/billing-foundation/web/localized-params.d.ts +1 -0
  40. package/dist/billing-foundation/web/localized-params.js +11 -0
  41. package/dist/cap-counter/entity.d.ts +1 -1
  42. package/dist/cap-overview/web/cap-cards-panel.js +3 -3
  43. package/dist/cap-overview/web/client-plugin.d.ts +1 -1
  44. package/dist/channel-email/email-channel.d.ts +2 -1
  45. package/dist/compliance-profiles/web/client-plugin.d.ts +1 -1
  46. package/dist/config/feature.js +2 -2
  47. package/dist/config/table.d.ts +6 -6
  48. package/dist/custom-fields/entity.d.ts +4 -4
  49. package/dist/custom-fields/web/client-plugin.d.ts +1 -1
  50. package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -4
  51. package/dist/delivery/tables.d.ts +6 -6
  52. package/dist/delivery/unsubscribe-page.js +1 -1
  53. package/dist/delivery/web/client-plugin.d.ts +1 -1
  54. package/dist/delivery/web/delivery-status-cell.js +1 -1
  55. package/dist/document-ingest-foundation/entity.d.ts +4 -4
  56. package/dist/feature-toggles/web/client-plugin.d.ts +1 -1
  57. package/dist/file-provider-s3/env-named-feature.d.ts +17 -0
  58. package/dist/file-provider-s3/env-named-feature.js +23 -0
  59. package/dist/file-provider-s3/env-schema.d.ts +7 -0
  60. package/dist/file-provider-s3/env-schema.js +15 -10
  61. package/dist/file-provider-s3/index.d.ts +1 -0
  62. package/dist/file-provider-s3/index.js +1 -0
  63. package/dist/folders/entity.d.ts +8 -8
  64. package/dist/folders/web/client-plugin.d.ts +1 -1
  65. package/dist/form-draft/entity.d.ts +2 -2
  66. package/dist/host-routing/constants.d.ts +2 -0
  67. package/dist/host-routing/constants.js +3 -0
  68. package/dist/host-routing/dispatch.d.ts +1 -1
  69. package/dist/host-routing/dispatch.js +35 -25
  70. package/dist/host-routing/feature.js +100 -27
  71. package/dist/host-routing/host.d.ts +9 -0
  72. package/dist/host-routing/host.js +24 -2
  73. package/dist/host-routing/index.d.ts +2 -1
  74. package/dist/host-routing/index.js +1 -0
  75. package/dist/host-routing/options.d.ts +14 -0
  76. package/dist/host-routing/redirects.d.ts +6 -0
  77. package/dist/host-routing/redirects.js +27 -0
  78. package/dist/host-routing/tenant-lookup.d.ts +7 -0
  79. package/dist/host-routing/tenant-lookup.js +39 -0
  80. package/dist/host-routing/validate-options.js +34 -1
  81. package/dist/inbound-mail-foundation/entities.d.ts +25 -25
  82. package/dist/inbound-mail-foundation/feature.js +7 -0
  83. package/dist/inbound-mail-foundation/i18n.d.ts +3 -0
  84. package/dist/inbound-mail-foundation/i18n.js +35 -0
  85. package/dist/inbound-mail-foundation/index.d.ts +1 -0
  86. package/dist/inbound-mail-foundation/index.js +1 -0
  87. package/dist/inbound-mail-foundation/projection.d.ts +20 -20
  88. package/dist/inbound-mail-foundation/screens.d.ts +7 -0
  89. package/dist/inbound-mail-foundation/screens.js +86 -0
  90. package/dist/inbound-mail-foundation/types.d.ts +2 -2
  91. package/dist/jobs/job-run-table.d.ts +6 -6
  92. package/dist/jobs/web/client-plugin.d.ts +1 -1
  93. package/dist/ledger/entity.d.ts +13 -13
  94. package/dist/legal-pages/feature.js +11 -1
  95. package/dist/legal-pages/layout.d.ts +2 -2
  96. package/dist/legal-pages/layout.js +4 -4
  97. package/dist/legal-pages/web/client-plugin.d.ts +1 -1
  98. package/dist/mail-foundation/feature.d.ts +20 -21
  99. package/dist/mail-foundation/feature.js +1 -1
  100. package/dist/managed-pages/table.d.ts +12 -12
  101. package/dist/managed-pages/web/client-plugin.d.ts +1 -1
  102. package/dist/notes-history/entity.d.ts +12 -12
  103. package/dist/notes-history/web/client-plugin.d.ts +1 -1
  104. package/dist/page-render/branding.js +1 -1
  105. package/dist/page-render/layout.d.ts +1 -1
  106. package/dist/page-render/layout.js +2 -2
  107. package/dist/page-render/markdown.js +1 -1
  108. package/dist/personal-access-tokens/i18n.js +1 -1
  109. package/dist/personal-access-tokens/schema/api-token.d.ts +6 -6
  110. package/dist/personal-access-tokens/screens.js +1 -1
  111. package/dist/secrets/feature.d.ts +1 -1
  112. package/dist/secrets/feature.js +3 -3
  113. package/dist/secrets/table.d.ts +1 -1
  114. package/dist/seo/sitemap.js +1 -1
  115. package/dist/sessions/schema/user-session.d.ts +4 -4
  116. package/dist/shared/mail-template-body.js +1 -1
  117. package/dist/shared/purge-files-reaching-subject.js +98 -3
  118. package/dist/tags/entity.d.ts +9 -9
  119. package/dist/tags/web/client-plugin.d.ts +1 -1
  120. package/dist/template-resolver/document-blocks.d.ts +5 -0
  121. package/dist/template-resolver/document-blocks.js +21 -0
  122. package/dist/template-resolver/document-ops.d.ts +167 -0
  123. package/dist/template-resolver/document-ops.js +390 -0
  124. package/dist/template-resolver/document-sanitizer.d.ts +3 -0
  125. package/dist/template-resolver/document-sanitizer.js +6 -0
  126. package/dist/template-resolver/feature.d.ts +2 -0
  127. package/dist/template-resolver/feature.js +4 -0
  128. package/dist/template-resolver/handlers/bundle-import.js +1 -0
  129. package/dist/template-resolver/handlers/document-base.d.ts +23 -0
  130. package/dist/template-resolver/handlers/document-base.js +55 -0
  131. package/dist/template-resolver/handlers/document-blocks.query.d.ts +22 -0
  132. package/dist/template-resolver/handlers/document-blocks.query.js +46 -0
  133. package/dist/template-resolver/handlers/export-bundle.query.d.ts +1 -0
  134. package/dist/template-resolver/handlers/export-bundle.query.js +9 -2
  135. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +1 -0
  136. package/dist/template-resolver/handlers/import-bundle.write.d.ts +1 -0
  137. package/dist/template-resolver/handlers/patch-draft.write.d.ts +66 -0
  138. package/dist/template-resolver/handlers/patch-draft.write.js +105 -0
  139. package/dist/template-resolver/handlers/preview.query.d.ts +1 -0
  140. package/dist/template-resolver/handlers/preview.query.js +2 -1
  141. package/dist/template-resolver/handlers/send-test.write.d.ts +1 -0
  142. package/dist/template-resolver/handlers/send-test.write.js +2 -1
  143. package/dist/template-resolver/handlers/template-write.d.ts +2 -0
  144. package/dist/template-resolver/handlers/template-write.js +14 -2
  145. package/dist/template-resolver/handlers/validate.query.d.ts +10 -4
  146. package/dist/template-resolver/handlers/validate.query.js +12 -7
  147. package/dist/template-resolver/{web/placeholder-text.js → placeholder-text.js} +1 -1
  148. package/dist/template-resolver/qualified-names.d.ts +2 -0
  149. package/dist/template-resolver/qualified-names.js +2 -0
  150. package/dist/template-resolver/seeding.d.ts +1 -0
  151. package/dist/template-resolver/seeding.js +2 -1
  152. package/dist/template-resolver/table.d.ts +10 -10
  153. package/dist/template-resolver/template-bundle.d.ts +1 -0
  154. package/dist/template-resolver/template-bundle.js +6 -1
  155. package/dist/template-resolver/template-preview.d.ts +4 -1
  156. package/dist/template-resolver/template-preview.js +16 -3
  157. package/dist/template-resolver/template-validation.d.ts +2 -1
  158. package/dist/template-resolver/template-validation.js +1 -1
  159. package/dist/template-resolver/user-content-table.d.ts +10 -10
  160. package/dist/template-resolver/web/client-plugin.d.ts +1 -1
  161. package/dist/template-resolver/web/client-plugin.js +3 -3
  162. package/dist/template-resolver/web/document-tiptap-editor.js +1 -1
  163. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +2 -5
  164. package/dist/template-resolver/web/document-toolbar-blocks.js +1 -21
  165. package/dist/template-resolver/web/i18n.js +14 -0
  166. package/dist/template-resolver/web/placeholder-line-editor.js +2 -2
  167. package/dist/tenant/invitation-table.d.ts +6 -6
  168. package/dist/tenant/membership-table.d.ts +4 -4
  169. package/dist/tenant/schema/tenant.d.ts +6 -6
  170. package/dist/tenant/web/client-plugin.d.ts +1 -1
  171. package/dist/tenant/web/member-status-cell.js +1 -1
  172. package/dist/tier-engine/entity.d.ts +1 -1
  173. package/dist/tier-engine/tier-resolver.d.ts +1 -1
  174. package/dist/tier-engine/web/client-plugin.d.ts +1 -1
  175. package/dist/user/schema/user.d.ts +12 -12
  176. package/dist/user-data-rights/env.d.ts +5 -0
  177. package/dist/user-data-rights/env.js +11 -0
  178. package/dist/user-data-rights/feature.js +4 -2
  179. package/dist/user-data-rights/index.d.ts +1 -0
  180. package/dist/user-data-rights/index.js +1 -0
  181. package/dist/user-data-rights/lib/mail-transport-resolver.js +1 -1
  182. package/dist/user-data-rights/lib/storage-provider-resolver.js +1 -1
  183. package/dist/user-data-rights/mail-notifications.js +1 -1
  184. package/dist/user-data-rights/schema/download-attempt.d.ts +14 -14
  185. package/dist/user-data-rights/schema/download-token.d.ts +8 -8
  186. package/dist/user-data-rights/schema/export-job.d.ts +6 -6
  187. package/dist/user-data-rights/web/client-plugin.d.ts +1 -1
  188. package/dist/user-data-rights/web/privacy-center-screen.js +2 -2
  189. package/dist/waitlist/entity.d.ts +14 -14
  190. package/dist/waitlist/legacy-migrate.d.ts +2 -0
  191. package/dist/waitlist/legacy-migrate.js +38 -3
  192. package/package.json +16 -11
  193. package/src/account-security/changes.json +6 -0
  194. package/src/auth-foundation/changes.json +6 -0
  195. package/src/billing-foundation/changes.json +6 -0
  196. package/src/channel-email/changes.json +6 -0
  197. package/src/crypto-shredding/changes.json +6 -0
  198. package/src/file-provider-s3/changes.json +6 -0
  199. package/src/host-routing/changes.json +6 -0
  200. package/src/inbound-mail-foundation/changes.json +7 -0
  201. package/src/legal-pages/changes.json +6 -0
  202. package/src/template-resolver/changes.json +18 -0
  203. package/src/user-data-rights/changes.json +6 -0
  204. package/src/waitlist/changes.json +6 -0
  205. /package/dist/template-resolver/{web/placeholder-text.d.ts → placeholder-text.d.ts} +0 -0
@@ -0,0 +1,390 @@
1
+ import { getSchema } from "@tiptap/core";
2
+ import * as z from "zod";
3
+ import { documentBlocksForKind } from "./document-blocks.js";
4
+ import { DOCUMENT_TEXT_ALIGNMENTS, documentExtensions, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
5
+ import { contentToLine, lineToContent, singleLine } from "./placeholder-text.js";
6
+ import { toVariableDef } from "./template-validation.js";
7
+ export const MAX_DOCUMENT_OPS = 50;
8
+ // Assigning these keys would reach the prototype of the meta object.
9
+ const FORBIDDEN_META_KEYS = new Set(["__proto__", "prototype", "constructor"]);
10
+ const TEXT_BLOCK_TYPES = new Set(["paragraph", "heading", "button"]);
11
+ function blockTypeOf(node) {
12
+ switch (node.type) {
13
+ case "paragraph":
14
+ case "heading":
15
+ case "button":
16
+ case "divider":
17
+ case "pageBreak":
18
+ case "steps":
19
+ return node.type;
20
+ case "bulletList":
21
+ case "orderedList":
22
+ return "list";
23
+ case "table":
24
+ return eachOf(node) === undefined ? "table" : "repeat";
25
+ case "resource":
26
+ return "image";
27
+ default:
28
+ return null;
29
+ }
30
+ }
31
+ function stringAttr(node, name) {
32
+ const value = node.attrs?.[name];
33
+ return typeof value === "string" && value !== "" ? value : undefined;
34
+ }
35
+ function eachOf(table) {
36
+ for (const row of table.content ?? []) {
37
+ const each = stringAttr(row, "each");
38
+ if (each !== undefined)
39
+ return each;
40
+ }
41
+ return undefined;
42
+ }
43
+ function textLinesOf(node) {
44
+ if (TEXT_BLOCK_TYPES.has(node.type ?? ""))
45
+ return [contentToLine(node.content)];
46
+ return (node.content ?? []).flatMap(textLinesOf);
47
+ }
48
+ const NON_TEXT_BLOCK_TYPES = new Set([
49
+ "image",
50
+ "divider",
51
+ "pageBreak",
52
+ "steps",
53
+ ]);
54
+ function setOptionalAttrs(block, node) {
55
+ const align = stringAttr(node, "textAlign");
56
+ if (align !== undefined)
57
+ block.align = align;
58
+ const showIf = stringAttr(node, "showIf");
59
+ if (showIf !== undefined)
60
+ block.showIf = showIf;
61
+ }
62
+ function setTypeSpecificAttrs(block, node) {
63
+ const level = node.attrs?.["level"];
64
+ if (block.type === "heading" && typeof level === "number")
65
+ block.level = level;
66
+ const resource = stringAttr(node, "resource");
67
+ if (block.type === "image" && resource !== undefined)
68
+ block.resource = resource;
69
+ const alt = stringAttr(node, "alt");
70
+ if (block.type === "image" && alt !== undefined)
71
+ block.alt = alt;
72
+ const href = stringAttr(node, "href");
73
+ if (block.type === "button" && href !== undefined)
74
+ block.href = href;
75
+ const each = eachOf(node);
76
+ if (block.type === "repeat" && each !== undefined)
77
+ block.each = each;
78
+ }
79
+ function blockOf(node, id) {
80
+ const type = blockTypeOf(node);
81
+ if (type === null)
82
+ return null;
83
+ const block = { id, type };
84
+ if (!NON_TEXT_BLOCK_TYPES.has(type))
85
+ block.text = textLinesOf(node).join("\n");
86
+ setOptionalAttrs(block, node);
87
+ setTypeSpecificAttrs(block, node);
88
+ return block;
89
+ }
90
+ export function documentBlockId(index) {
91
+ return `b${index}`;
92
+ }
93
+ export function documentBlocksOf(doc) {
94
+ return (doc.content ?? []).flatMap((node, index) => blockOf(node, documentBlockId(index)) ?? []);
95
+ }
96
+ const blockIdSchema = z.string().min(1).max(20);
97
+ const singleLineText = z.string().max(2000);
98
+ const blockInputSchema = z.discriminatedUnion("type", [
99
+ z.object({
100
+ type: z.literal("paragraph"),
101
+ text: singleLineText,
102
+ align: z.enum(DOCUMENT_TEXT_ALIGNMENTS).optional(),
103
+ }),
104
+ z.object({
105
+ type: z.literal("heading"),
106
+ level: z.number().int().min(1).max(3),
107
+ text: singleLineText,
108
+ }),
109
+ z.object({
110
+ type: z.literal("image"),
111
+ resource: z.string().min(1).max(200),
112
+ alt: z.string().max(500).optional(),
113
+ }),
114
+ z.object({ type: z.literal("button"), text: singleLineText, href: z.string().max(300) }),
115
+ z.object({ type: z.literal("divider") }),
116
+ z.object({ type: z.literal("pageBreak") }),
117
+ ]);
118
+ export const documentOpSchema = z.discriminatedUnion("op", [
119
+ z.object({
120
+ op: z.literal("insertBlock"),
121
+ after: blockIdSchema.nullable(),
122
+ block: blockInputSchema,
123
+ }),
124
+ z.object({
125
+ op: z.literal("replaceText"),
126
+ blockId: blockIdSchema,
127
+ find: z.string().min(1).max(2000),
128
+ replace: z.string().max(2000),
129
+ }),
130
+ z.object({ op: z.literal("removeBlock"), blockId: blockIdSchema }),
131
+ z.object({
132
+ op: z.literal("setResource"),
133
+ blockId: blockIdSchema,
134
+ resource: z.string().min(1).max(200),
135
+ alt: z.string().max(500).nullable().optional(),
136
+ }),
137
+ z.object({ op: z.literal("setMeta"), meta: z.record(z.string(), z.unknown()) }),
138
+ z.object({
139
+ op: z.literal("setVariable"),
140
+ name: z.string().min(1).max(100),
141
+ definition: z.unknown().nullable(),
142
+ }),
143
+ ]);
144
+ export const documentOpsSchema = z.array(documentOpSchema).min(1).max(MAX_DOCUMENT_OPS);
145
+ function patchIssue(path, code, params) {
146
+ const camel = code.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
147
+ return {
148
+ path,
149
+ code,
150
+ i18nKey: `templateResolver.errors.patch.${camel}`,
151
+ ...(params === undefined ? {} : { params }),
152
+ };
153
+ }
154
+ function textNodesIn(nodes) {
155
+ return nodes.flatMap((node) => (node.type === "text" ? [node] : textNodesIn(node.content ?? [])));
156
+ }
157
+ function occurrencesOf(text, find) {
158
+ return text.split(find).length - 1;
159
+ }
160
+ function replacementNodes(textNode, find, replace) {
161
+ const text = textNode.text ?? "";
162
+ const at = text.indexOf(find);
163
+ const withMarks = (node) => node.type === "text" && textNode.marks !== undefined
164
+ ? { ...node, marks: textNode.marks }
165
+ : node;
166
+ const before = text.slice(0, at);
167
+ const after = text.slice(at + find.length);
168
+ return [
169
+ ...(before === "" ? [] : [{ ...textNode, text: before }]),
170
+ ...lineToContent(singleLine(replace)).map(withMarks),
171
+ ...(after === "" ? [] : [{ ...textNode, text: after }]),
172
+ ];
173
+ }
174
+ function replaceInContent(content, find, replace) {
175
+ return content.flatMap((node) => {
176
+ if (node.type === "text") {
177
+ return (node.text ?? "").includes(find) ? replacementNodes(node, find, replace) : [node];
178
+ }
179
+ return [
180
+ node.content === undefined
181
+ ? node
182
+ : { ...node, content: replaceInContent(node.content, find, replace) },
183
+ ];
184
+ });
185
+ }
186
+ function nodeOfInput(input) {
187
+ switch (input.type) {
188
+ case "paragraph":
189
+ return {
190
+ type: "paragraph",
191
+ ...(input.align === undefined ? {} : { attrs: { textAlign: input.align } }),
192
+ content: lineToContent(singleLine(input.text)),
193
+ };
194
+ case "heading":
195
+ return {
196
+ type: "heading",
197
+ attrs: { level: input.level },
198
+ content: lineToContent(singleLine(input.text)),
199
+ };
200
+ case "image":
201
+ return { type: "resource", attrs: { resource: input.resource, alt: input.alt ?? null } };
202
+ case "button":
203
+ return {
204
+ type: "button",
205
+ attrs: { href: input.href },
206
+ content: lineToContent(singleLine(input.text)),
207
+ };
208
+ case "divider":
209
+ case "pageBreak":
210
+ return { type: input.type };
211
+ }
212
+ }
213
+ const TOOLBAR_BLOCK_OF_INPUT = {
214
+ image: "image",
215
+ button: "button",
216
+ divider: "divider",
217
+ pageBreak: "pageBreak",
218
+ };
219
+ function isBlockAllowed(input, kind) {
220
+ if (input.type === "paragraph" || input.type === "heading")
221
+ return true;
222
+ return documentBlocksForKind(kind).includes(TOOLBAR_BLOCK_OF_INPUT[input.type]);
223
+ }
224
+ function inputIssue(input, path) {
225
+ if (input.type === "image" && !isDocumentResourceKey(input.resource)) {
226
+ return patchIssue(`${path}.block.resource`, "invalid_resource_key", {
227
+ resource: input.resource,
228
+ });
229
+ }
230
+ if (input.type === "button" && !isDocumentPlaceholderHref(input.href)) {
231
+ return patchIssue(`${path}.block.href`, "invalid_button_href", { href: input.href });
232
+ }
233
+ return null;
234
+ }
235
+ let cachedSchema = null;
236
+ function documentSchema() {
237
+ cachedSchema ??= getSchema(documentExtensions);
238
+ return cachedSchema;
239
+ }
240
+ function isValidDocument(doc) {
241
+ try {
242
+ documentSchema().nodeFromJSON(doc).check();
243
+ return true;
244
+ }
245
+ catch {
246
+ return false;
247
+ }
248
+ }
249
+ function lookupEntry(state, blockId, fieldPath) {
250
+ const entry = state.entries.get(blockId);
251
+ if (entry === undefined)
252
+ return { issue: patchIssue(fieldPath, "unknown_block", { blockId }) };
253
+ if (entry.removed)
254
+ return { issue: patchIssue(fieldPath, "block_removed", { blockId }) };
255
+ return { entry };
256
+ }
257
+ function blockInputIssue(op, path, kind) {
258
+ if (!isBlockAllowed(op.block, kind)) {
259
+ return patchIssue(`${path}.block.type`, "block_not_allowed", { type: op.block.type, kind });
260
+ }
261
+ if (op.block.type === "paragraph" &&
262
+ op.block.align !== undefined &&
263
+ !documentBlocksForKind(kind).includes("align")) {
264
+ return patchIssue(`${path}.block.align`, "block_not_allowed", { type: "align", kind });
265
+ }
266
+ return inputIssue(op.block, path);
267
+ }
268
+ function insertBlockOp(state, op, path) {
269
+ const anchor = op.after === null ? null : lookupEntry(state, op.after, `${path}.after`);
270
+ if (anchor !== null && "issue" in anchor)
271
+ return anchor.issue;
272
+ const invalid = blockInputIssue(op, path, state.kind);
273
+ if (invalid !== null)
274
+ return invalid;
275
+ const target = anchor === null ? state.insertedAtStart : anchor.entry.insertedAfter;
276
+ target.push(nodeOfInput(op.block));
277
+ return null;
278
+ }
279
+ function removeBlockOp(state, op, path) {
280
+ const found = lookupEntry(state, op.blockId, `${path}.blockId`);
281
+ if ("issue" in found)
282
+ return found.issue;
283
+ found.entry.removed = true;
284
+ return null;
285
+ }
286
+ function replaceTextOp(state, op, path) {
287
+ const found = lookupEntry(state, op.blockId, `${path}.blockId`);
288
+ if ("issue" in found)
289
+ return found.issue;
290
+ const { entry } = found;
291
+ const count = textNodesIn([entry.node]).reduce((sum, node) => sum + occurrencesOf(node.text ?? "", op.find), 0);
292
+ if (count === 0)
293
+ return patchIssue(`${path}.find`, "text_not_found", { find: op.find });
294
+ if (count > 1)
295
+ return patchIssue(`${path}.find`, "text_ambiguous", { find: op.find, count });
296
+ entry.node = replaceInContent([entry.node], op.find, op.replace)[0] ?? entry.node;
297
+ return null;
298
+ }
299
+ function setResourceOp(state, op, path) {
300
+ const found = lookupEntry(state, op.blockId, `${path}.blockId`);
301
+ if ("issue" in found)
302
+ return found.issue;
303
+ const { entry } = found;
304
+ if (entry.node.type !== "resource") {
305
+ return patchIssue(`${path}.blockId`, "block_type_mismatch", {
306
+ expected: "image",
307
+ actual: entry.node.type ?? "unknown",
308
+ });
309
+ }
310
+ if (!isDocumentResourceKey(op.resource)) {
311
+ return patchIssue(`${path}.resource`, "invalid_resource_key", { resource: op.resource });
312
+ }
313
+ const alt = op.alt === undefined ? (entry.node.attrs?.["alt"] ?? null) : op.alt;
314
+ entry.node = { ...entry.node, attrs: { resource: op.resource, alt } };
315
+ return null;
316
+ }
317
+ function setMetaOp(state, op, path) {
318
+ const forbidden = Object.keys(op.meta).find((key) => FORBIDDEN_META_KEYS.has(key));
319
+ if (forbidden !== undefined) {
320
+ return patchIssue(`${path}.meta`, "invalid_meta_key", { key: forbidden });
321
+ }
322
+ for (const [key, value] of Object.entries(op.meta)) {
323
+ if (value === null)
324
+ delete state.meta[key];
325
+ else
326
+ state.meta[key] = value;
327
+ }
328
+ return null;
329
+ }
330
+ function setVariableOp(state, op, path) {
331
+ if (!isDocumentReferenceName(op.name)) {
332
+ return patchIssue(`${path}.name`, "invalid_variable_name", { name: op.name });
333
+ }
334
+ if (op.definition === null) {
335
+ delete state.variableSchema[op.name];
336
+ return null;
337
+ }
338
+ const definition = toVariableDef(op.definition);
339
+ if (definition === null) {
340
+ return patchIssue(`${path}.definition`, "invalid_variable_definition", { name: op.name });
341
+ }
342
+ state.variableSchema[op.name] = definition;
343
+ return null;
344
+ }
345
+ function applyOp(state, op, path) {
346
+ switch (op.op) {
347
+ case "insertBlock":
348
+ return insertBlockOp(state, op, path);
349
+ case "removeBlock":
350
+ return removeBlockOp(state, op, path);
351
+ case "replaceText":
352
+ return replaceTextOp(state, op, path);
353
+ case "setResource":
354
+ return setResourceOp(state, op, path);
355
+ case "setMeta":
356
+ return setMetaOp(state, op, path);
357
+ case "setVariable":
358
+ return setVariableOp(state, op, path);
359
+ }
360
+ }
361
+ // Ops address blocks by the ids of the document as it was read, so inserted blocks are
362
+ // collected per anchor and the list is assembled once at the end.
363
+ export function applyDocumentOps(input) {
364
+ const entries = new Map();
365
+ (input.doc.content ?? []).forEach((node, index) => {
366
+ entries.set(documentBlockId(index), {
367
+ node: structuredClone(node),
368
+ removed: false,
369
+ insertedAfter: [],
370
+ });
371
+ });
372
+ const state = {
373
+ kind: input.kind,
374
+ entries,
375
+ insertedAtStart: [],
376
+ meta: { ...input.meta },
377
+ variableSchema: { ...input.variableSchema },
378
+ };
379
+ const issues = input.ops.flatMap((op, index) => applyOp(state, op, `ops.${index}`) ?? []);
380
+ if (issues.length > 0)
381
+ return { ok: false, issues };
382
+ const content = [
383
+ ...state.insertedAtStart,
384
+ ...[...entries.values()].flatMap((entry) => entry.removed ? entry.insertedAfter : [entry.node, ...entry.insertedAfter]),
385
+ ];
386
+ const doc = { ...input.doc, content };
387
+ if (!isValidDocument(doc))
388
+ return { ok: false, issues: [patchIssue("ops", "invalid_document")] };
389
+ return { ok: true, doc, meta: state.meta, variableSchema: state.variableSchema };
390
+ }
@@ -1,4 +1,5 @@
1
1
  import type { FieldIssue } from "@cosmicdrift/kumiko-framework/errors";
2
+ import type { JSONContent } from "@tiptap/core";
2
3
  import { type Element as ScanElement } from "happy-dom";
3
4
  import type { ContentFormat } from "./constants.ts";
4
5
  export type DocumentViolationCode = "script_element" | "event_handler_attribute" | "disallowed_link_href" | "external_image_source" | "image_without_resource" | "invalid_reference_name" | "embedded_content" | "unsafe_attribute_value" | "button_href_not_placeholder" | "document_requires_html_format" | "layout_requires_mjml_format" | "layout_invalid_structure" | "layout_missing_content_slot" | "layout_source_system_only";
@@ -24,6 +25,8 @@ export type SanitizeBodyResult = {
24
25
  export declare function violation(code: DocumentViolationCode, detail: string, message: string): DocumentViolation;
25
26
  export declare function withParsedDocument<T>(html: string, read: (body: ScanElement) => T): T;
26
27
  export declare function sanitizeDocumentHtml(html: string): SanitizeHtmlResult;
28
+ export declare function parseDocumentJson(html: string): JSONContent;
29
+ export declare function serializeDocumentJson(doc: JSONContent): string;
27
30
  export declare function flattenParagraphsToLineBreaks(sanitizedHtml: string): string;
28
31
  export declare function sanitizeTemplateBody(input: {
29
32
  readonly kind: string;
@@ -156,6 +156,12 @@ export function sanitizeDocumentHtml(html) {
156
156
  html: generateHTML(generateJSON(html, documentExtensions), documentExtensions),
157
157
  };
158
158
  }
159
+ export function parseDocumentJson(html) {
160
+ return generateJSON(html, documentExtensions);
161
+ }
162
+ export function serializeDocumentJson(doc) {
163
+ return generateHTML(doc, documentExtensions);
164
+ }
159
165
  // For a sanitized value placed inside a line of text: paragraphs become line
160
166
  // breaks. Other blocks (lists, headings) cannot be made inline and stay as they are.
161
167
  export function flattenParagraphsToLineBreaks(sanitizedHtml) {
@@ -31,6 +31,7 @@ export declare function createTemplateResolverFeature(opts?: TemplateResolverOpt
31
31
  resourceRemove: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
32
32
  resourceLinkPdf: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
33
33
  sendTest: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
34
+ patchDraft: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
34
35
  };
35
36
  queries: {
36
37
  findById: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
@@ -44,6 +45,7 @@ export declare function createTemplateResolverFeature(opts?: TemplateResolverOpt
44
45
  diffToSystem: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
45
46
  validate: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
46
47
  preview: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
48
+ documentBlocks: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
47
49
  exportBundle: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
48
50
  };
49
51
  treeHandle: import("@cosmicdrift/kumiko-types/tree-node").TreeActionsHandle<"template-resolver", {
@@ -8,6 +8,7 @@ import { makeCollectionSetWrite } from "./handlers/collection-set.write.js";
8
8
  import { copyLocaleWrite } from "./handlers/copy-locale.write.js";
9
9
  import { diffToSystemQuery } from "./handlers/diff-to-system.query.js";
10
10
  import { discardDraftSystemWrite, discardDraftWrite } from "./handlers/discard-draft.write.js";
11
+ import { documentBlocksQuery } from "./handlers/document-blocks.query.js";
11
12
  import { exportBundleQuery } from "./handlers/export-bundle.query.js";
12
13
  import { findByIdQuery } from "./handlers/find-by-id.query.js";
13
14
  import { importBundleWrite } from "./handlers/import-bundle.write.js";
@@ -15,6 +16,7 @@ import { importBundleSystemWrite } from "./handlers/import-bundle-system.write.j
15
16
  import { layoutSourceQuery } from "./handlers/layout-source.query.js";
16
17
  import { listQuery } from "./handlers/list.query.js";
17
18
  import { overviewQuery } from "./handlers/overview.query.js";
19
+ import { patchDraftWrite } from "./handlers/patch-draft.write.js";
18
20
  import { previewQuery } from "./handlers/preview.query.js";
19
21
  import { resetToSystemWrite } from "./handlers/reset-to-system.write.js";
20
22
  import { resourceLinkPdfWrite } from "./handlers/resource-link-pdf.write.js";
@@ -88,6 +90,7 @@ export function createTemplateResolverFeature(opts = {}) {
88
90
  resourceRemove: r.writeHandler(resourceRemoveWrite),
89
91
  resourceLinkPdf: r.writeHandler(resourceLinkPdfWrite),
90
92
  sendTest: r.writeHandler(sendTestWrite),
93
+ patchDraft: r.writeHandler(patchDraftWrite),
91
94
  };
92
95
  const queries = {
93
96
  findById: r.queryHandler(findByIdQuery),
@@ -101,6 +104,7 @@ export function createTemplateResolverFeature(opts = {}) {
101
104
  diffToSystem: r.queryHandler(diffToSystemQuery),
102
105
  validate: r.queryHandler(validateQuery),
103
106
  preview: r.queryHandler(previewQuery),
107
+ documentBlocks: r.queryHandler(documentBlocksQuery),
104
108
  exportBundle: r.queryHandler(exportBundleQuery),
105
109
  };
106
110
  for (const collection of collections) {
@@ -147,6 +147,7 @@ export async function importTemplateBundle(args) {
147
147
  contentFormat: args.bundle.contentFormat,
148
148
  variableSchema,
149
149
  meta: entry.meta,
150
+ ...(entry.title !== undefined && { title: entry.title }),
150
151
  };
151
152
  const written = await writeTemplateRow({
152
153
  ctx: args.ctx,
@@ -0,0 +1,23 @@
1
+ import { type HandlerContext, type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { DocumentHtmlKind } from "../constants.ts";
3
+ export type DocumentBase = {
4
+ readonly scope: "tenant" | "system";
5
+ /** The stored row's own version, which counts every draft save. */
6
+ readonly version: number;
7
+ };
8
+ export type LoadedDocument = {
9
+ readonly rowId: string;
10
+ readonly base: DocumentBase;
11
+ readonly status: string;
12
+ readonly content: string;
13
+ readonly meta: Readonly<Record<string, unknown>>;
14
+ readonly variableSchema: Readonly<Record<string, unknown>>;
15
+ readonly title: string | null;
16
+ readonly folder: string | null;
17
+ readonly parentTemplateId: string | null;
18
+ };
19
+ export declare function loadDocumentBase(ctx: HandlerContext, user: SessionUser, where: {
20
+ readonly slug: string;
21
+ readonly kind: DocumentHtmlKind;
22
+ readonly locale: string;
23
+ }): Promise<LoadedDocument | null>;
@@ -0,0 +1,55 @@
1
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
2
+ import { SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
4
+ import { templateResourcesTable } from "../table.js";
5
+ import { isHiddenSystemDraft, toWorkingRow } from "../template-draft.js";
6
+ import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.js";
7
+ import { parseStoredJson } from "./template-gates.js";
8
+ function recordOrEmpty(raw) {
9
+ const parsed = parseStoredJson(raw);
10
+ return isPlainObject(parsed) ? parsed : {};
11
+ }
12
+ // The version an editor works on: the caller's own row (its open draft if there is one),
13
+ // else the served system default. A system default as base is what a patch copies into a tenant draft.
14
+ export async function loadDocumentBase(ctx, user, where) {
15
+ const own = await fetchOne(ctx.db, templateResourcesTable, {
16
+ tenantId: user.tenantId,
17
+ ...where,
18
+ });
19
+ if (own !== undefined && !isSystemOnlyTemplateHiddenFrom(user, ctx.registry, own)) {
20
+ const working = toWorkingRow(own, user.tenantId);
21
+ return {
22
+ rowId: String(own.id),
23
+ base: { scope: own.scope === "system" ? "system" : "tenant", version: own.version },
24
+ status: working.status,
25
+ content: working.content ?? "",
26
+ meta: recordOrEmpty(working.meta),
27
+ variableSchema: recordOrEmpty(working.variableSchema),
28
+ title: working.title,
29
+ folder: working.folder,
30
+ parentTemplateId: working.parentTemplateId,
31
+ };
32
+ }
33
+ const system = await fetchOne(ctx.db, templateResourcesTable, {
34
+ tenantId: SYSTEM_TENANT_ID,
35
+ scope: "system",
36
+ status: "active",
37
+ ...where,
38
+ });
39
+ if (system === undefined ||
40
+ isHiddenSystemDraft(user, system) ||
41
+ isSystemOnlyTemplateHiddenFrom(user, ctx.registry, system)) {
42
+ return null;
43
+ }
44
+ return {
45
+ rowId: String(system.id),
46
+ base: { scope: "system", version: system.version },
47
+ status: system.status,
48
+ content: system.content ?? "",
49
+ meta: recordOrEmpty(system.meta),
50
+ variableSchema: recordOrEmpty(system.variableSchema),
51
+ title: system.title,
52
+ folder: system.folder,
53
+ parentTemplateId: String(system.id),
54
+ };
55
+ }
@@ -0,0 +1,22 @@
1
+ import * as z from "zod";
2
+ export declare const documentBlocksQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"document-blocks", z.ZodObject<{
3
+ slug: z.ZodString;
4
+ kind: z.ZodEnum<{
5
+ "document-pdf": "document-pdf";
6
+ mail: "mail";
7
+ }>;
8
+ locale: z.ZodString;
9
+ }, z.core.$strip>, {
10
+ slug: string;
11
+ kind: "document-pdf" | "mail";
12
+ locale: string;
13
+ base: import("./document-base.ts").DocumentBase;
14
+ status: string;
15
+ blocks: import("../document-ops.ts").DocumentBlock[];
16
+ meta: Readonly<Record<string, unknown>>;
17
+ variableSchema: Readonly<Record<string, unknown>>;
18
+ variables: Readonly<Record<string, import("@cosmicdrift/kumiko-types/config").TemplateVariableDef>>;
19
+ variablesDeclaredByFeature: boolean;
20
+ allowedBlocks: readonly import("../document-blocks.ts").DocumentToolbarBlock[];
21
+ resourceKeys: string[];
22
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -0,0 +1,46 @@
1
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { NotFoundError } from "@cosmicdrift/kumiko-framework/errors";
3
+ import * as z from "zod";
4
+ import { DOCUMENT_HTML_KINDS } from "../constants.js";
5
+ import { documentBlocksForKind } from "../document-blocks.js";
6
+ import { documentBlocksOf } from "../document-ops.js";
7
+ import { parseDocumentJson } from "../document-sanitizer.js";
8
+ import { resolveTemplateVariables } from "../template-validation.js";
9
+ import { loadDocumentBase } from "./document-base.js";
10
+ import { localeSchema, templateSlugSchema } from "./shared.js";
11
+ import { knownResourceKeysFor } from "./validate.query.js";
12
+ export const documentBlocksQuery = defineQueryHandler({
13
+ name: "document-blocks",
14
+ schema: z.object({
15
+ slug: templateSlugSchema,
16
+ kind: z.enum(DOCUMENT_HTML_KINDS),
17
+ locale: localeSchema,
18
+ }),
19
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
20
+ description: "Reads a mail or PDF document template as a list of blocks with stable ids (b0, b1, ...) for structured editing: each block has its type (paragraph, heading, list, table, repeat, image, button, divider, pageBreak, steps), its text with {{name}} placeholders and its settings, plus the meta, the stored and effective variables (variablesDeclaredByFeature is true when a notification declares them, so they cannot be changed), the block types allowed for the kind and the uploaded image keys. It reads the caller's own version (the open draft if there is one), else the served system default; base carries the scope and version that patch-draft expects back.",
21
+ handler: async (query, ctx) => {
22
+ const { slug, kind, locale } = query.payload;
23
+ const loaded = await loadDocumentBase(ctx, query.user, { slug, kind, locale });
24
+ if (loaded === null)
25
+ throw new NotFoundError("template-resource", slug);
26
+ return {
27
+ slug,
28
+ kind,
29
+ locale,
30
+ base: loaded.base,
31
+ status: loaded.status,
32
+ blocks: documentBlocksOf(parseDocumentJson(loaded.content)),
33
+ meta: loaded.meta,
34
+ variableSchema: loaded.variableSchema,
35
+ variables: resolveTemplateVariables(ctx.registry, slug, loaded.variableSchema),
36
+ variablesDeclaredByFeature: ctx.registry.getAllNotifications().get(slug)?.variables !== undefined,
37
+ allowedBlocks: documentBlocksForKind(kind),
38
+ resourceKeys: await knownResourceKeysFor(ctx, query.user, {
39
+ slug,
40
+ kind,
41
+ scope: "tenant",
42
+ locale,
43
+ }),
44
+ };
45
+ },
46
+ });
@@ -21,6 +21,7 @@ export declare const exportBundleQuery: import("@cosmicdrift/kumiko-types/define
21
21
  locales: Record<string, {
22
22
  body: string;
23
23
  meta?: unknown;
24
+ title?: string;
24
25
  }>;
25
26
  resources: Record<string, {
26
27
  fileName: string;
@@ -55,6 +55,14 @@ function assertLocalesShareFormat(rows) {
55
55
  ],
56
56
  });
57
57
  }
58
+ function localeEntryOf(row) {
59
+ const meta = parseStoredJson(row.meta);
60
+ return {
61
+ body: row.content ?? "",
62
+ ...(meta === undefined ? {} : { meta }),
63
+ ...(row.title === null ? {} : { title: row.title }),
64
+ };
65
+ }
58
66
  export const exportBundleQuery = defineQueryHandler({
59
67
  name: "export-bundle",
60
68
  schema: z.object({
@@ -79,8 +87,7 @@ export const exportBundleQuery = defineQueryHandler({
79
87
  const locales = {};
80
88
  const entryByKey = new Map();
81
89
  for (const row of rows) {
82
- const meta = parseStoredJson(row.meta);
83
- locales[row.locale] = { body: row.content ?? "", ...(meta === undefined ? {} : { meta }) };
90
+ locales[row.locale] = localeEntryOf(row);
84
91
  for (const [key, entry] of Object.entries(parseLinkedResourceEntries(row.linkedResources))) {
85
92
  if (!entryByKey.has(key))
86
93
  entryByKey.set(key, entry);
@@ -20,6 +20,7 @@ export declare const importBundleSystemWrite: import("@cosmicdrift/kumiko-types/
20
20
  locales: z.ZodRecord<z.ZodString, z.ZodObject<{
21
21
  body: z.ZodString;
22
22
  meta: z.ZodOptional<z.ZodUnknown>;
23
+ title: z.ZodOptional<z.ZodString>;
23
24
  }, z.core.$strict>>;
24
25
  resources: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
25
26
  fileName: z.ZodString;