@cosmicdrift/kumiko-bundled-features 0.362.0 → 0.363.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.
- package/dist/admin-shell/feature.d.ts +3 -0
- package/dist/admin-shell/feature.js +6 -1
- package/dist/agent-tools/agent-manifest.js +2 -1
- package/dist/agent-tools/filter-ops.d.ts +1 -1
- package/dist/agent-tools/filter-ops.js +1 -3
- package/dist/agent-tools/tool-dispatch.js +2 -1
- package/dist/auth-email-password/email-templates.d.ts +4 -26
- package/dist/auth-email-password/email-templates.js +9 -101
- package/dist/auth-email-password/feature.js +24 -6
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
- package/dist/auth-email-password/handlers/request-account-unlock.write.js +2 -4
- package/dist/auth-email-password/handlers/request-email-verification.write.js +2 -4
- package/dist/auth-email-password/handlers/request-password-reset.write.js +2 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +3 -4
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -2
- package/dist/auth-email-password/handlers/token-request-handler.js +0 -1
- package/dist/auth-email-password/index.d.ts +2 -2
- package/dist/auth-email-password/index.js +1 -1
- package/dist/auth-email-password/magic-link-mail.d.ts +3 -2
- package/dist/auth-email-password/magic-link-mail.js +19 -14
- package/dist/auth-email-password/mail-notifications.d.ts +32 -0
- package/dist/auth-email-password/mail-notifications.js +116 -0
- package/dist/auth-email-password/signup-activation.d.ts +1 -1
- package/dist/auth-email-password/signup-activation.js +2 -3
- package/dist/auth-foundation/types.js +2 -8
- package/dist/auth-mfa/feature.js +4 -4
- package/dist/auth-mfa/hmac-token-codec.js +4 -4
- package/dist/auth-mfa/mfa-challenge-token.js +3 -3
- package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -3
- package/dist/auth-mfa/mfa-setup-token.js +8 -8
- package/dist/billing-foundation/constants.d.ts +3 -3
- package/dist/billing-foundation/constants.js +5 -4
- package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +32 -14
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +27 -32
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +3 -3
- package/dist/billing-foundation/consumer-protection/mail-notifications.d.ts +8 -0
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +95 -0
- package/dist/billing-foundation/consumer-protection/register-consent.js +2 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +20 -10
- package/dist/billing-foundation/consumer-protection/termination-mail.js +10 -21
- package/dist/billing-foundation/consumer-protection/termination-notify.js +2 -2
- package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -3
- package/dist/billing-foundation/consumer-protection/termination-request.js +2 -2
- package/dist/billing-foundation/handlers/process-event.write.js +2 -2
- package/dist/billing-foundation/handlers/process-payment-event.write.js +1 -1
- package/dist/billing-foundation/subscription-tier-sync.js +3 -4
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -1
- package/dist/cap-overview/web/cap-usage-cell.js +5 -4
- package/dist/channel-email/email-channel.d.ts +2 -2
- package/dist/channel-email/email-channel.js +89 -26
- package/dist/channel-email/feature.js +3 -3
- package/dist/channel-email/index.d.ts +1 -1
- package/dist/channel-email/index.js +1 -1
- package/dist/channel-email/pii-guard.js +10 -0
- package/dist/channel-email/smtp-transport.js +13 -0
- package/dist/channel-email/types.d.ts +8 -0
- package/dist/config/handlers/set.write.js +1 -2
- package/dist/custom-fields/lib/value-schema.js +10 -11
- package/dist/custom-fields/wire-user-data-rights.js +7 -6
- package/dist/data-retention/index.d.ts +1 -0
- package/dist/data-retention/index.js +1 -0
- package/dist/data-retention/run-retention-cleanup.js +6 -6
- package/dist/delivery/attachment-ref.d.ts +39 -0
- package/dist/delivery/attachment-ref.js +64 -0
- package/dist/delivery/channel-context.d.ts +8 -4
- package/dist/delivery/channel-context.js +76 -2
- package/dist/delivery/constants.d.ts +1 -0
- package/dist/delivery/constants.js +1 -0
- package/dist/delivery/delivery-service.d.ts +4 -1
- package/dist/delivery/delivery-service.js +38 -32
- package/dist/delivery/feature.js +1 -1
- package/dist/delivery/index.d.ts +5 -2
- package/dist/delivery/index.js +4 -1
- package/dist/delivery/jobs.js +25 -5
- package/dist/delivery/notify-factory.js +4 -1
- package/dist/delivery/testing.d.ts +4 -1
- package/dist/delivery/types.d.ts +20 -14
- package/dist/delivery/unsubscribe.js +4 -1
- package/dist/derivatives-sharp/render.js +1 -18
- package/dist/document-ingest-foundation/pages.js +4 -4
- package/dist/feature-toggles/toggle-sync-signal.js +4 -4
- package/dist/file-foundation/index.d.ts +1 -0
- package/dist/file-foundation/index.js +1 -0
- package/dist/file-foundation/read-file-ref-content.d.ts +27 -0
- package/dist/file-foundation/read-file-ref-content.js +49 -0
- package/dist/folders/web/folder-manager.js +3 -2
- package/dist/form-draft/db/queries/cleanup.js +4 -4
- package/dist/form-draft/db/queries/draft-count.js +2 -2
- package/dist/form-draft/db/queries/owned-file-refs.js +2 -2
- package/dist/form-draft/release-file-refs.js +3 -2
- package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +6 -6
- package/dist/inbound-mail-foundation/handlers/scope-visibility.d.ts +1 -1
- package/dist/inbound-mail-foundation/oauth-state.js +7 -7
- package/dist/inbound-mail-foundation/types.js +4 -4
- package/dist/inbound-mail-foundation/watch-supervisor.js +22 -13
- package/dist/inbound-provider-imap/imap-client.js +4 -3
- package/dist/ledger/recurring.js +1 -3
- package/dist/managed-pages/branding.d.ts +2 -0
- package/dist/managed-pages/branding.js +24 -2
- package/dist/managed-pages/feature.js +1 -0
- package/dist/managed-pages/i18n.js +1 -0
- package/dist/managed-pages/screens/branding-screen.js +8 -1
- package/dist/page-render/branding.d.ts +1 -0
- package/dist/page-render/branding.js +1 -0
- package/dist/page-render/markdown.js +1 -1
- package/dist/personal-access-tokens/feature.js +1 -1
- package/dist/personal-access-tokens/handlers/list.query.js +2 -1
- package/dist/personal-access-tokens/revoke-for-user.js +2 -1
- package/dist/renderer-foundation/api.d.ts +13 -13
- package/dist/renderer-foundation/api.js +1 -1
- package/dist/renderer-foundation/constants.js +2 -4
- package/dist/renderer-foundation/feature.js +7 -7
- package/dist/renderer-foundation/index.d.ts +2 -1
- package/dist/renderer-foundation/index.js +1 -0
- package/dist/renderer-foundation/types.d.ts +92 -32
- package/dist/renderer-pdf/feature.js +7 -1
- package/dist/sessions/db/queries/cleanup.js +2 -2
- package/dist/sessions/feature.js +1 -1
- package/dist/sessions/handlers/list.query.js +2 -1
- package/dist/shared/attachment-file-name.d.ts +3 -0
- package/dist/shared/attachment-file-name.js +35 -0
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.js +3 -0
- package/dist/shared/is-tenant-db.js +1 -1
- package/dist/shared/mail-template-body.d.ts +4 -0
- package/dist/shared/mail-template-body.js +17 -0
- package/dist/shared/sharp-input-limits.d.ts +6 -0
- package/dist/shared/sharp-input-limits.js +22 -0
- package/dist/shared/signup-handover.js +2 -2
- package/dist/tags/handlers/assign-tag.write.js +3 -1
- package/dist/tags/web/tag-chip.js +2 -0
- package/dist/template-resolver/api.d.ts +30 -2
- package/dist/template-resolver/api.js +40 -5
- package/dist/template-resolver/compile-document-body.d.ts +8 -0
- package/dist/template-resolver/compile-document-body.js +163 -0
- package/dist/template-resolver/constants.d.ts +13 -4
- package/dist/template-resolver/constants.js +30 -10
- package/dist/template-resolver/document-sanitizer.d.ts +32 -0
- package/dist/template-resolver/document-sanitizer.js +201 -0
- package/dist/template-resolver/document-schema.d.ts +21 -0
- package/dist/template-resolver/document-schema.js +296 -0
- package/dist/template-resolver/feature.d.ts +18 -0
- package/dist/template-resolver/feature.js +50 -6
- package/dist/template-resolver/handlers/bundle-import.d.ts +20 -0
- package/dist/template-resolver/handlers/bundle-import.js +147 -0
- package/dist/template-resolver/handlers/collection-set.write.js +25 -2
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +19 -0
- package/dist/template-resolver/handlers/copy-locale.write.js +116 -0
- package/dist/template-resolver/handlers/diff-to-system.query.d.ts +22 -0
- package/dist/template-resolver/handlers/diff-to-system.query.js +64 -0
- package/dist/template-resolver/handlers/export-bundle.query.d.ts +35 -0
- package/dist/template-resolver/handlers/export-bundle.query.js +68 -0
- package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
- package/dist/template-resolver/handlers/find-by-id.query.js +3 -3
- package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +39 -0
- package/dist/template-resolver/handlers/import-bundle-system.write.js +36 -0
- package/dist/template-resolver/handlers/import-bundle.write.d.ts +40 -0
- package/dist/template-resolver/handlers/import-bundle.write.js +19 -0
- package/dist/template-resolver/handlers/layout-source.query.d.ts +26 -0
- package/dist/template-resolver/handlers/layout-source.query.js +64 -0
- package/dist/template-resolver/handlers/list.query.d.ts +4 -1
- package/dist/template-resolver/handlers/list.query.js +5 -7
- package/dist/template-resolver/handlers/overview.query.d.ts +59 -0
- package/dist/template-resolver/handlers/overview.query.js +165 -0
- package/dist/template-resolver/handlers/preview.query.d.ts +12 -0
- package/dist/template-resolver/handlers/preview.query.js +24 -0
- package/dist/template-resolver/handlers/reset-to-system.write.d.ts +8 -0
- package/dist/template-resolver/handlers/reset-to-system.write.js +50 -0
- package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +8 -0
- package/dist/template-resolver/handlers/resource-link-pdf.write.js +32 -0
- package/dist/template-resolver/handlers/resource-remove.write.d.ts +9 -0
- package/dist/template-resolver/handlers/resource-remove.write.js +44 -0
- package/dist/template-resolver/handlers/resource-shared.d.ts +9 -0
- package/dist/template-resolver/handlers/resource-shared.js +14 -0
- package/dist/template-resolver/handlers/resource-upload.write.d.ts +13 -0
- package/dist/template-resolver/handlers/resource-upload.write.js +98 -0
- package/dist/template-resolver/handlers/send-test.write.d.ts +11 -0
- package/dist/template-resolver/handlers/send-test.write.js +81 -0
- package/dist/template-resolver/handlers/set.write.d.ts +4 -1
- package/dist/template-resolver/handlers/set.write.js +25 -3
- package/dist/template-resolver/handlers/shared.d.ts +9 -5
- package/dist/template-resolver/handlers/shared.js +6 -2
- package/dist/template-resolver/handlers/template-gates.d.ts +36 -0
- package/dist/template-resolver/handlers/template-gates.js +131 -0
- package/dist/template-resolver/handlers/template-write.d.ts +45 -0
- package/dist/template-resolver/handlers/template-write.js +109 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +17 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.js +48 -0
- package/dist/template-resolver/handlers/toggle-status.write.d.ts +12 -0
- package/dist/template-resolver/handlers/toggle-status.write.js +27 -11
- package/dist/template-resolver/handlers/upsert-system.write.d.ts +5 -8
- package/dist/template-resolver/handlers/upsert-system.write.js +14 -59
- package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +5 -8
- package/dist/template-resolver/handlers/upsert-tenant.write.js +10 -61
- package/dist/template-resolver/handlers/usage.query.d.ts +17 -0
- package/dist/template-resolver/handlers/usage.query.js +15 -0
- package/dist/template-resolver/handlers/validate.query.d.ts +74 -0
- package/dist/template-resolver/handlers/validate.query.js +80 -0
- package/dist/template-resolver/index.d.ts +18 -1
- package/dist/template-resolver/index.js +16 -1
- package/dist/template-resolver/layout-regions.d.ts +23 -0
- package/dist/template-resolver/layout-regions.js +125 -0
- package/dist/template-resolver/linked-resources.d.ts +17 -0
- package/dist/template-resolver/linked-resources.js +75 -0
- package/dist/template-resolver/preview-display-html.d.ts +7 -0
- package/dist/template-resolver/preview-display-html.js +27 -0
- package/dist/template-resolver/print-document.d.ts +1 -0
- package/dist/template-resolver/print-document.js +55 -0
- package/dist/template-resolver/qualified-names.d.ts +30 -0
- package/dist/template-resolver/qualified-names.js +30 -0
- package/dist/template-resolver/record-mapping.d.ts +2 -0
- package/dist/template-resolver/record-mapping.js +21 -0
- package/dist/template-resolver/resource-image.d.ts +15 -0
- package/dist/template-resolver/resource-image.js +34 -0
- package/dist/template-resolver/seeding.d.ts +14 -1
- package/dist/template-resolver/seeding.js +105 -15
- package/dist/template-resolver/store-template-resource.d.ts +64 -0
- package/dist/template-resolver/store-template-resource.js +264 -0
- package/dist/template-resolver/table.d.ts +47 -8
- package/dist/template-resolver/table.js +11 -0
- package/dist/template-resolver/template-bundle.d.ts +36 -0
- package/dist/template-resolver/template-bundle.js +60 -0
- package/dist/template-resolver/template-diff.d.ts +7 -0
- package/dist/template-resolver/template-diff.js +68 -0
- package/dist/template-resolver/template-meta.d.ts +60 -0
- package/dist/template-resolver/template-meta.js +101 -0
- package/dist/template-resolver/template-preview.d.ts +86 -0
- package/dist/template-resolver/template-preview.js +374 -0
- package/dist/template-resolver/template-resources.d.ts +14 -0
- package/dist/template-resolver/template-resources.js +42 -0
- package/dist/template-resolver/template-usage.d.ts +12 -0
- package/dist/template-resolver/template-usage.js +30 -0
- package/dist/template-resolver/template-validation.d.ts +22 -0
- package/dist/template-resolver/template-validation.js +172 -0
- package/dist/template-resolver/testing.js +5 -5
- package/dist/template-resolver/user-content-table.d.ts +14 -8
- package/dist/template-resolver/web/client-plugin.d.ts +1 -0
- package/dist/template-resolver/web/client-plugin.js +9 -6
- package/dist/template-resolver/web/document-editor-extensions.d.ts +15 -0
- package/dist/template-resolver/web/document-editor-extensions.js +107 -0
- package/dist/template-resolver/web/document-editor.d.ts +3 -0
- package/dist/template-resolver/web/document-editor.js +11 -0
- package/dist/template-resolver/web/document-tiptap-editor.d.ts +3 -0
- package/dist/template-resolver/web/document-tiptap-editor.js +376 -0
- package/dist/template-resolver/web/document-toolbar-blocks.d.ts +33 -0
- package/dist/template-resolver/web/document-toolbar-blocks.js +15 -0
- package/dist/template-resolver/web/editor-handle.d.ts +3 -0
- package/dist/template-resolver/web/editor-handle.js +24 -0
- package/dist/template-resolver/web/i18n.js +76 -0
- package/dist/template-resolver/web/index.d.ts +4 -0
- package/dist/template-resolver/web/index.js +4 -0
- package/dist/template-resolver/web/placeholder-line-editor-lazy.d.ts +4 -0
- package/dist/template-resolver/web/placeholder-line-editor-lazy.js +16 -0
- package/dist/template-resolver/web/placeholder-line-editor.d.ts +15 -0
- package/dist/template-resolver/web/placeholder-line-editor.js +94 -0
- package/dist/template-resolver/web/placeholder-text.d.ts +4 -0
- package/dist/template-resolver/web/placeholder-text.js +34 -0
- package/dist/template-resolver/web/use-stable-by-key.d.ts +1 -0
- package/dist/template-resolver/web/use-stable-by-key.js +13 -0
- package/dist/tenant/handlers/team-list.query.js +3 -2
- package/dist/tenant/seeding.js +31 -6
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +2 -1
- package/dist/user/db/queries/stream-tenant-backfill.js +4 -4
- package/dist/user/handlers/detail.query.js +2 -1
- package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
- package/dist/user-data-rights/email-templates.d.ts +1 -29
- package/dist/user-data-rights/email-templates.js +5 -106
- package/dist/user-data-rights/feature.js +53 -35
- package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
- package/dist/user-data-rights/lib/default-mailers.d.ts +15 -7
- package/dist/user-data-rights/lib/default-mailers.js +100 -48
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/user-data-rights/mail-notifications.d.ts +13 -0
- package/dist/user-data-rights/mail-notifications.js +118 -0
- package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
- package/dist/user-profile/web/profile-screen.js +2 -1
- package/dist/waitlist/feature.js +10 -0
- package/dist/waitlist/handlers/submit.write.js +3 -4
- package/dist/waitlist/mail.d.ts +19 -6
- package/dist/waitlist/mail.js +50 -14
- package/package.json +28 -17
- package/src/admin-shell/changes.json +6 -0
- package/src/auth-email-password/changes.json +14 -0
- package/src/billing-foundation/changes.json +7 -0
- package/src/channel-email/changes.json +19 -0
- package/src/data-retention/changes.json +6 -0
- package/src/delivery/changes.json +48 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/renderer-foundation/changes.json +39 -0
- package/src/template-resolver/changes.json +118 -0
- package/src/user-data-rights/changes.json +7 -0
- package/src/waitlist/changes.json +7 -0
- package/dist/renderer-simple/feature.d.ts +0 -10
- package/dist/renderer-simple/feature.js +0 -42
- package/dist/renderer-simple/index.d.ts +0 -3
- package/dist/renderer-simple/index.js +0 -2
- package/dist/renderer-simple/mail-blocks.d.ts +0 -21
- package/dist/renderer-simple/mail-blocks.js +0 -107
- package/dist/renderer-simple/resolve-variables.d.ts +0 -7
- package/dist/renderer-simple/resolve-variables.js +0 -47
- package/dist/renderer-simple/simple-renderer.d.ts +0 -24
- package/dist/renderer-simple/simple-renderer.js +0 -200
- package/src/renderer-simple/changes.json +0 -27
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import { generateHTML, generateJSON } from "@tiptap/html/server";
|
|
3
|
+
import { Window } from "happy-dom";
|
|
4
|
+
import { DOCUMENT_HTML_KINDS } from "./constants.js";
|
|
5
|
+
import { documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
6
|
+
export function violation(code, detail, message) {
|
|
7
|
+
return { code, detail, message };
|
|
8
|
+
}
|
|
9
|
+
// Same switches tiptap's generateJSON uses, plus navigation off: the scan
|
|
10
|
+
// window must never fetch or navigate anything.
|
|
11
|
+
function createScanWindow() {
|
|
12
|
+
return new Window({
|
|
13
|
+
settings: {
|
|
14
|
+
disableJavaScriptEvaluation: true,
|
|
15
|
+
disableJavaScriptFileLoading: true,
|
|
16
|
+
disableCSSFileLoading: true,
|
|
17
|
+
disableIframePageLoading: true,
|
|
18
|
+
disableComputedStyleRendering: true,
|
|
19
|
+
navigation: {
|
|
20
|
+
disableMainFrameNavigation: true,
|
|
21
|
+
disableChildFrameNavigation: true,
|
|
22
|
+
disableChildPageNavigation: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function scanScriptAndHandlers(tag, element, out) {
|
|
28
|
+
if (tag === "script") {
|
|
29
|
+
out.push(violation("script_element", "script", "<script> elements are not allowed in template bodies"));
|
|
30
|
+
}
|
|
31
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
32
|
+
if (/^on/i.test(attribute.name)) {
|
|
33
|
+
out.push(violation("event_handler_attribute", attribute.name, `Inline event handler "${attribute.name}" on <${tag}> is not allowed`));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function scanAnchor(element, out) {
|
|
38
|
+
const href = element.getAttribute("href");
|
|
39
|
+
if (href !== null && !isAllowedDocumentLinkHref(href)) {
|
|
40
|
+
out.push(violation("disallowed_link_href", href, `Link target "${href}" is not allowed; use https, http, mailto, tel or a {{placeholder}}`));
|
|
41
|
+
}
|
|
42
|
+
if (element.hasAttribute("data-button") && !isDocumentPlaceholderHref(href ?? "")) {
|
|
43
|
+
out.push(violation("button_href_not_placeholder", href ?? "", `Button target "${href ?? ""}" must be a {{placeholder}}`));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function scanImage(element, out) {
|
|
47
|
+
const source = element.getAttribute("src") ?? element.getAttribute("srcset");
|
|
48
|
+
if (source !== null) {
|
|
49
|
+
out.push(violation("external_image_source", source, `Images must reference an uploaded resource via data-resource; external or inline image source "${source}" is not allowed`));
|
|
50
|
+
}
|
|
51
|
+
if (!element.hasAttribute("data-resource")) {
|
|
52
|
+
out.push(violation("image_without_resource", "img", "Images must reference an uploaded resource via the data-resource attribute"));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const REFERENCE_ATTRIBUTES = [
|
|
56
|
+
["data-variable", isDocumentReferenceName],
|
|
57
|
+
["data-each", isDocumentReferenceName],
|
|
58
|
+
["data-resource", isDocumentResourceKey],
|
|
59
|
+
];
|
|
60
|
+
function scanReferences(tag, element, out) {
|
|
61
|
+
for (const [attribute, isValid] of REFERENCE_ATTRIBUTES) {
|
|
62
|
+
const value = element.getAttribute(attribute);
|
|
63
|
+
if (value !== null && !isValid(value)) {
|
|
64
|
+
out.push(violation("invalid_reference_name", value, `Value "${value}" of ${attribute} on <${tag}> is not a valid name`));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const EMBEDDING_TAGS = new Set([
|
|
69
|
+
"iframe",
|
|
70
|
+
"frame",
|
|
71
|
+
"frameset",
|
|
72
|
+
"object",
|
|
73
|
+
"embed",
|
|
74
|
+
"applet",
|
|
75
|
+
"portal",
|
|
76
|
+
]);
|
|
77
|
+
const SVG_IMAGE_TAGS = new Set(["image", "feimage", "use"]);
|
|
78
|
+
const URL_ATTRIBUTES = ["href", "xlink:href", "action", "formaction"];
|
|
79
|
+
function scanEmbedding(tag, out) {
|
|
80
|
+
if (EMBEDDING_TAGS.has(tag)) {
|
|
81
|
+
out.push(violation("embedded_content", tag, `<${tag}> elements are not allowed in template bodies`));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// html <a href> has its own rules in scanAnchor; everywhere else (svg, base,
|
|
85
|
+
// form controls) a URL attribute has no business in a template body.
|
|
86
|
+
function scanUrlAttributes(tag, element, out) {
|
|
87
|
+
for (const name of URL_ATTRIBUTES) {
|
|
88
|
+
const value = element.getAttribute(name);
|
|
89
|
+
if (value === null || (tag === "a" && name === "href"))
|
|
90
|
+
continue;
|
|
91
|
+
if (SVG_IMAGE_TAGS.has(tag) && name.endsWith("href")) {
|
|
92
|
+
out.push(violation("external_image_source", value, `Images must reference an uploaded resource via data-resource; image source "${value}" is not allowed`));
|
|
93
|
+
}
|
|
94
|
+
else if (tag === "base" || !isAllowedDocumentLinkHref(value)) {
|
|
95
|
+
out.push(violation("disallowed_link_href", value, `Link target "${value}" is not allowed; use https, http, mailto, tel or a {{placeholder}}`));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// happy-dom serializes < and > inside attribute values unescaped, so a later
|
|
100
|
+
// parser (MJML) could read them as markup.
|
|
101
|
+
function scanAltText(element, out) {
|
|
102
|
+
const alt = element.getAttribute("alt");
|
|
103
|
+
if (alt !== null && /[<>]/.test(alt)) {
|
|
104
|
+
out.push(violation("unsafe_attribute_value", alt, `Attribute alt must not contain "<" or ">": "${alt}"`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function scanElement(element, out) {
|
|
108
|
+
const tag = element.tagName.toLowerCase();
|
|
109
|
+
scanEmbedding(tag, out);
|
|
110
|
+
scanUrlAttributes(tag, element, out);
|
|
111
|
+
scanAltText(element, out);
|
|
112
|
+
scanScriptAndHandlers(tag, element, out);
|
|
113
|
+
if (tag === "a")
|
|
114
|
+
scanAnchor(element, out);
|
|
115
|
+
if (tag === "img")
|
|
116
|
+
scanImage(element, out);
|
|
117
|
+
scanReferences(tag, element, out);
|
|
118
|
+
}
|
|
119
|
+
// Parses `html` in the hardened scan window and always closes it again, also
|
|
120
|
+
// when `read` throws. Shared by the sanitizer and the placeholder scan so there
|
|
121
|
+
// is exactly one DOM setup.
|
|
122
|
+
export function withParsedDocument(html, read) {
|
|
123
|
+
const window = createScanWindow();
|
|
124
|
+
try {
|
|
125
|
+
const parsed = new window.DOMParser().parseFromString(["<!DOCTYPE html><html><body>", html, "</body></html>"].join(""), "text/html");
|
|
126
|
+
return read(parsed.body);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
window.happyDOM.abort();
|
|
130
|
+
window.happyDOM.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function scanDocumentHtml(html) {
|
|
134
|
+
const violations = [];
|
|
135
|
+
withParsedDocument(html, (body) => {
|
|
136
|
+
// A second `<body ...>` in the input merges its attributes into this body.
|
|
137
|
+
for (const element of [body, ...Array.from(body.querySelectorAll("*"))]) {
|
|
138
|
+
scanElement(element, violations);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return violations;
|
|
142
|
+
}
|
|
143
|
+
// Rejects hostile constructs, then rebuilds the body from the document model.
|
|
144
|
+
// The rebuilt HTML is always what gets returned (never the input), so anything
|
|
145
|
+
// the model cannot express is dropped silently.
|
|
146
|
+
export function sanitizeDocumentHtml(html) {
|
|
147
|
+
const violations = scanDocumentHtml(html);
|
|
148
|
+
if (violations.length > 0)
|
|
149
|
+
return { ok: false, violations };
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
html: generateHTML(generateJSON(html, documentExtensions), documentExtensions),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// Snippets accept any format, but their html is as untrusted as a mail body.
|
|
156
|
+
function isSanitizedHtmlKind(kind) {
|
|
157
|
+
return kind === "snippet" || isOneOf(DOCUMENT_HTML_KINDS, kind);
|
|
158
|
+
}
|
|
159
|
+
const HTML_ONLY_KINDS = new Set(DOCUMENT_HTML_KINDS);
|
|
160
|
+
function requiredFormatViolation(kind, contentFormat) {
|
|
161
|
+
if (HTML_ONLY_KINDS.has(kind) && contentFormat !== "html") {
|
|
162
|
+
return violation("document_requires_html_format", contentFormat, `Templates of kind ${kind} require contentFormat html, got "${contentFormat}"`);
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
export function sanitizeTemplateBody(input) {
|
|
167
|
+
const formatViolation = requiredFormatViolation(input.kind, input.contentFormat);
|
|
168
|
+
if (formatViolation !== null)
|
|
169
|
+
return { ok: false, violations: [formatViolation] };
|
|
170
|
+
if (!isSanitizedHtmlKind(input.kind) ||
|
|
171
|
+
input.contentFormat !== "html" ||
|
|
172
|
+
input.content === null) {
|
|
173
|
+
return { ok: true, content: input.content };
|
|
174
|
+
}
|
|
175
|
+
const result = sanitizeDocumentHtml(input.content);
|
|
176
|
+
return result.ok ? { ok: true, content: result.html } : result;
|
|
177
|
+
}
|
|
178
|
+
const VIOLATION_I18N_KEYS = {
|
|
179
|
+
script_element: "templateResolver.errors.document.scriptElement",
|
|
180
|
+
event_handler_attribute: "templateResolver.errors.document.eventHandlerAttribute",
|
|
181
|
+
disallowed_link_href: "templateResolver.errors.document.disallowedLinkHref",
|
|
182
|
+
external_image_source: "templateResolver.errors.document.externalImageSource",
|
|
183
|
+
image_without_resource: "templateResolver.errors.document.imageWithoutResource",
|
|
184
|
+
invalid_reference_name: "templateResolver.errors.document.invalidReferenceName",
|
|
185
|
+
embedded_content: "templateResolver.errors.document.embeddedContent",
|
|
186
|
+
unsafe_attribute_value: "templateResolver.errors.document.unsafeAttributeValue",
|
|
187
|
+
button_href_not_placeholder: "templateResolver.errors.document.buttonHrefNotPlaceholder",
|
|
188
|
+
document_requires_html_format: "templateResolver.errors.document.documentRequiresHtmlFormat",
|
|
189
|
+
layout_requires_mjml_format: "templateResolver.errors.document.layoutRequiresMjmlFormat",
|
|
190
|
+
layout_invalid_structure: "templateResolver.errors.document.layoutInvalidStructure",
|
|
191
|
+
layout_missing_content_slot: "templateResolver.errors.document.layoutMissingContentSlot",
|
|
192
|
+
layout_source_system_only: "templateResolver.errors.document.layoutSourceSystemOnly",
|
|
193
|
+
};
|
|
194
|
+
export function documentViolationsToFieldIssues(violations) {
|
|
195
|
+
return violations.map((item) => ({
|
|
196
|
+
path: "content",
|
|
197
|
+
code: item.code,
|
|
198
|
+
i18nKey: VIOLATION_I18N_KEYS[item.code],
|
|
199
|
+
params: { detail: item.detail },
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Extension, type Extensions, Node } from "@tiptap/core";
|
|
2
|
+
export declare function isDocumentPlaceholderHref(href: string): boolean;
|
|
3
|
+
export declare function isAllowedDocumentLinkHref(href: string): boolean;
|
|
4
|
+
export declare function isDocumentReferenceName(value: string): boolean;
|
|
5
|
+
export declare function isDocumentResourceKey(value: string): boolean;
|
|
6
|
+
export declare const DOCUMENT_NODE_NAMES: readonly ["variable", "resource", "button", "divider", "pageBreak"];
|
|
7
|
+
export declare const DocumentVariable: Node<any, any>;
|
|
8
|
+
export declare const DocumentResource: Node<any, any>;
|
|
9
|
+
export declare const DocumentButton: Node<any, any>;
|
|
10
|
+
export declare const DocumentDivider: Node<any, any>;
|
|
11
|
+
export declare const DocumentPageBreak: Node<any, any>;
|
|
12
|
+
export declare const DocumentTableRow: Node<import("@tiptap/extension-table").TableRowOptions, any>;
|
|
13
|
+
export declare const DocumentRepeatRow: Node<import("@tiptap/extension-table").TableRowOptions, any>;
|
|
14
|
+
export declare const DocumentTable: Node<import("@tiptap/extension-table").TableOptions, any>;
|
|
15
|
+
export declare const DocumentTableCell: Node<import("@tiptap/extension-table").TableCellOptions, any>;
|
|
16
|
+
export declare const DocumentTableHeader: Node<import("@tiptap/extension-table").TableHeaderOptions, any>;
|
|
17
|
+
export declare const DocumentLink: import("@tiptap/core").Mark<import("@tiptap/extension-link").LinkOptions, any>;
|
|
18
|
+
export declare const DOCUMENT_TEXT_ALIGNMENTS: readonly ["left", "center", "right", "justify"];
|
|
19
|
+
export type DocumentTextAlignment = (typeof DOCUMENT_TEXT_ALIGNMENTS)[number];
|
|
20
|
+
export declare const DocumentTextAlign: Extension<any, any>;
|
|
21
|
+
export declare const documentExtensions: Extensions;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Canonical document model for template bodies (kinds mail, document-pdf).
|
|
3
|
+
// Universal on purpose: the web editor and the server sanitizer load the same
|
|
4
|
+
// extension list, so what the editor can produce is exactly what the server
|
|
5
|
+
// keeps. No DOM access at module level, no happy-dom here.
|
|
6
|
+
import { isOneOf, isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
7
|
+
import { Extension, Node } from "@tiptap/core";
|
|
8
|
+
import { Link } from "@tiptap/extension-link";
|
|
9
|
+
import { Table, TableCell, TableHeader, TableRow } from "@tiptap/extension-table";
|
|
10
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
11
|
+
const PLACEHOLDER_HREF = /^\{\{[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*\}\}$/;
|
|
12
|
+
const REFERENCE_NAME = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
13
|
+
const RESOURCE_KEY = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
14
|
+
const ALLOWED_LINK_SCHEMES = ["https://", "http://", "mailto:", "tel:"];
|
|
15
|
+
export function isDocumentPlaceholderHref(href) {
|
|
16
|
+
return PLACEHOLDER_HREF.test(href);
|
|
17
|
+
}
|
|
18
|
+
const UNSAFE_URL_CHARACTERS = new Set(["<", ">", '"']);
|
|
19
|
+
const FORBIDDEN_REFERENCE_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
20
|
+
function hasUnsafeUrlCharacter(value) {
|
|
21
|
+
return Array.from(value).some((character) => {
|
|
22
|
+
const code = character.charCodeAt(0);
|
|
23
|
+
return UNSAFE_URL_CHARACTERS.has(character) || code <= 0x1f || code === 0x7f;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function isParsableWebUrl(href, protocol) {
|
|
27
|
+
try {
|
|
28
|
+
return new URL(href).protocol === protocol;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function hasAllowedLinkScheme(href) {
|
|
35
|
+
if (href.startsWith("https://"))
|
|
36
|
+
return isParsableWebUrl(href, "https:");
|
|
37
|
+
if (href.startsWith("http://"))
|
|
38
|
+
return isParsableWebUrl(href, "http:");
|
|
39
|
+
return ALLOWED_LINK_SCHEMES.some((scheme) => href.startsWith(scheme));
|
|
40
|
+
}
|
|
41
|
+
// Exact lowercase prefix on the unchanged value: no trim, no case folding, so
|
|
42
|
+
// "JaVaScRiPt:", "HTTPS://" or a leading tab can never slip through as a
|
|
43
|
+
// scheme variant. Markup and control characters are refused outright because
|
|
44
|
+
// downstream parsers (MJML, PDF renderer) re-read the serialized attribute.
|
|
45
|
+
export function isAllowedDocumentLinkHref(href) {
|
|
46
|
+
if (hasUnsafeUrlCharacter(href))
|
|
47
|
+
return false;
|
|
48
|
+
return hasAllowedLinkScheme(href) || isDocumentPlaceholderHref(href);
|
|
49
|
+
}
|
|
50
|
+
// Segments that hit the prototype chain are refused: the renderer later walks
|
|
51
|
+
// the dotted path into a data object.
|
|
52
|
+
export function isDocumentReferenceName(value) {
|
|
53
|
+
return (REFERENCE_NAME.test(value) &&
|
|
54
|
+
!value.split(".").some((segment) => FORBIDDEN_REFERENCE_SEGMENTS.has(segment)));
|
|
55
|
+
}
|
|
56
|
+
export function isDocumentResourceKey(value) {
|
|
57
|
+
return RESOURCE_KEY.test(value);
|
|
58
|
+
}
|
|
59
|
+
export const DOCUMENT_NODE_NAMES = [
|
|
60
|
+
"variable",
|
|
61
|
+
"resource",
|
|
62
|
+
"button",
|
|
63
|
+
"divider",
|
|
64
|
+
"pageBreak",
|
|
65
|
+
];
|
|
66
|
+
export const DocumentVariable = Node.create({
|
|
67
|
+
name: "variable",
|
|
68
|
+
group: "inline",
|
|
69
|
+
inline: true,
|
|
70
|
+
atom: true,
|
|
71
|
+
addAttributes() {
|
|
72
|
+
return { name: { default: null } };
|
|
73
|
+
},
|
|
74
|
+
parseHTML() {
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
tag: "span[data-variable]",
|
|
78
|
+
getAttrs: (element) => {
|
|
79
|
+
const name = element.getAttribute("data-variable");
|
|
80
|
+
return name !== null && isDocumentReferenceName(name) ? { name } : false;
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
},
|
|
85
|
+
renderHTML({ node }) {
|
|
86
|
+
return ["span", { "data-variable": node.attrs["name"] }];
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
export const DocumentResource = Node.create({
|
|
90
|
+
name: "resource",
|
|
91
|
+
group: "block",
|
|
92
|
+
atom: true,
|
|
93
|
+
addAttributes() {
|
|
94
|
+
return { resource: { default: null }, alt: { default: null } };
|
|
95
|
+
},
|
|
96
|
+
parseHTML() {
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
tag: "img[data-resource]",
|
|
100
|
+
getAttrs: (element) => {
|
|
101
|
+
const resource = element.getAttribute("data-resource");
|
|
102
|
+
if (resource === null || !isDocumentResourceKey(resource))
|
|
103
|
+
return false;
|
|
104
|
+
return { resource, alt: element.getAttribute("alt") };
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
},
|
|
109
|
+
// Never emits src: the renderer resolves the resource key at render time.
|
|
110
|
+
renderHTML({ node }) {
|
|
111
|
+
const alt = node.attrs["alt"];
|
|
112
|
+
return [
|
|
113
|
+
"img",
|
|
114
|
+
alt === null || alt === undefined
|
|
115
|
+
? { "data-resource": node.attrs["resource"] }
|
|
116
|
+
: { "data-resource": node.attrs["resource"], alt },
|
|
117
|
+
];
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
export const DocumentButton = Node.create({
|
|
121
|
+
name: "button",
|
|
122
|
+
group: "block",
|
|
123
|
+
content: "text*",
|
|
124
|
+
marks: "",
|
|
125
|
+
defining: true,
|
|
126
|
+
addAttributes() {
|
|
127
|
+
return { href: { default: null } };
|
|
128
|
+
},
|
|
129
|
+
parseHTML() {
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
tag: "a[data-button]",
|
|
133
|
+
// Must win against the Link mark's `a[href]` rule.
|
|
134
|
+
priority: 1000,
|
|
135
|
+
getAttrs: (element) => {
|
|
136
|
+
const href = element.getAttribute("href");
|
|
137
|
+
return href !== null && isDocumentPlaceholderHref(href) ? { href } : false;
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
},
|
|
142
|
+
renderHTML({ node }) {
|
|
143
|
+
return ["a", { "data-button": "", href: node.attrs["href"] }, 0];
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
export const DocumentDivider = Node.create({
|
|
147
|
+
name: "divider",
|
|
148
|
+
group: "block",
|
|
149
|
+
atom: true,
|
|
150
|
+
parseHTML() {
|
|
151
|
+
return [{ tag: "hr" }];
|
|
152
|
+
},
|
|
153
|
+
renderHTML() {
|
|
154
|
+
return ["hr"];
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
export const DocumentPageBreak = Node.create({
|
|
158
|
+
name: "pageBreak",
|
|
159
|
+
group: "block",
|
|
160
|
+
atom: true,
|
|
161
|
+
parseHTML() {
|
|
162
|
+
return [{ tag: "div[data-page-break]" }];
|
|
163
|
+
},
|
|
164
|
+
renderHTML() {
|
|
165
|
+
return ["div", { "data-page-break": "" }];
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
// A repeat row is a table row carrying `each`: prosemirror-tables requires
|
|
169
|
+
// the tableRow node type, so a separate node type is not an option.
|
|
170
|
+
export const DocumentTableRow = TableRow.extend({
|
|
171
|
+
addAttributes() {
|
|
172
|
+
return {
|
|
173
|
+
each: {
|
|
174
|
+
default: null,
|
|
175
|
+
parseHTML: (element) => {
|
|
176
|
+
const each = element.getAttribute("data-each");
|
|
177
|
+
return each !== null && isDocumentReferenceName(each) ? each : null;
|
|
178
|
+
},
|
|
179
|
+
renderHTML: (attributes) => {
|
|
180
|
+
const each = attributes["each"];
|
|
181
|
+
return typeof each === "string" && each ? { "data-each": each } : {};
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
repeatHeader: {
|
|
185
|
+
default: false,
|
|
186
|
+
parseHTML: (element) => element.hasAttribute("data-repeat-header"),
|
|
187
|
+
renderHTML: (attributes) => attributes["repeatHeader"] ? { "data-repeat-header": "" } : {},
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
export const DocumentRepeatRow = DocumentTableRow;
|
|
193
|
+
export const DocumentTable = Table.extend({
|
|
194
|
+
renderHTML() {
|
|
195
|
+
return ["table", ["tbody", 0]];
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
// Span attributes are rendered only when they differ from the default, and
|
|
199
|
+
// colwidth (an editor-resize artefact) never, so `<td><p>x</p></td>` is the
|
|
200
|
+
// canonical cell.
|
|
201
|
+
function plainObjectOrEmpty(value) {
|
|
202
|
+
return isPlainObject(value) ? value : {};
|
|
203
|
+
}
|
|
204
|
+
function canonicalCellAttributes(attributes) {
|
|
205
|
+
return {
|
|
206
|
+
...attributes,
|
|
207
|
+
colspan: {
|
|
208
|
+
...plainObjectOrEmpty(attributes["colspan"]),
|
|
209
|
+
renderHTML: (attrs) => attrs["colspan"] && attrs["colspan"] !== 1 ? { colspan: attrs["colspan"] } : {},
|
|
210
|
+
},
|
|
211
|
+
rowspan: {
|
|
212
|
+
...plainObjectOrEmpty(attributes["rowspan"]),
|
|
213
|
+
renderHTML: (attrs) => attrs["rowspan"] && attrs["rowspan"] !== 1 ? { rowspan: attrs["rowspan"] } : {},
|
|
214
|
+
},
|
|
215
|
+
colwidth: {
|
|
216
|
+
...plainObjectOrEmpty(attributes["colwidth"]),
|
|
217
|
+
renderHTML: () => ({}),
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export const DocumentTableCell = TableCell.extend({
|
|
222
|
+
addAttributes() {
|
|
223
|
+
return canonicalCellAttributes(this.parent?.() ?? {});
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
export const DocumentTableHeader = TableHeader.extend({
|
|
227
|
+
addAttributes() {
|
|
228
|
+
return canonicalCellAttributes(this.parent?.() ?? {});
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
// Only href survives a parse: target, rel, class and title from the input would
|
|
232
|
+
// otherwise be kept verbatim, and the renderer decides those itself.
|
|
233
|
+
export const DocumentLink = Link.extend({
|
|
234
|
+
addAttributes() {
|
|
235
|
+
return { href: { default: null, parseHTML: (element) => element.getAttribute("href") } };
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
export const DOCUMENT_TEXT_ALIGNMENTS = ["left", "center", "right", "justify"];
|
|
239
|
+
// Serialized as data-align, never as a style attribute: the sanitizer keeps
|
|
240
|
+
// styles out of the model, and each renderer maps the value to its own CSS.
|
|
241
|
+
export const DocumentTextAlign = Extension.create({
|
|
242
|
+
name: "documentTextAlign",
|
|
243
|
+
addGlobalAttributes() {
|
|
244
|
+
return [
|
|
245
|
+
{
|
|
246
|
+
types: ["paragraph", "heading"],
|
|
247
|
+
attributes: {
|
|
248
|
+
textAlign: {
|
|
249
|
+
default: null,
|
|
250
|
+
parseHTML: (element) => {
|
|
251
|
+
const align = element.getAttribute("data-align");
|
|
252
|
+
return isOneOf(DOCUMENT_TEXT_ALIGNMENTS, align) ? align : null;
|
|
253
|
+
},
|
|
254
|
+
renderHTML: (attributes) => {
|
|
255
|
+
const align = attributes["textAlign"];
|
|
256
|
+
return isOneOf(DOCUMENT_TEXT_ALIGNMENTS, align) ? { "data-align": align } : {};
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
export const documentExtensions = [
|
|
265
|
+
StarterKit.configure({
|
|
266
|
+
blockquote: false,
|
|
267
|
+
code: false,
|
|
268
|
+
codeBlock: false,
|
|
269
|
+
strike: false,
|
|
270
|
+
horizontalRule: false,
|
|
271
|
+
dropcursor: false,
|
|
272
|
+
gapcursor: false,
|
|
273
|
+
undoRedo: false,
|
|
274
|
+
trailingNode: false,
|
|
275
|
+
listKeymap: false,
|
|
276
|
+
heading: { levels: [1, 2, 3] },
|
|
277
|
+
link: false,
|
|
278
|
+
}),
|
|
279
|
+
DocumentLink.configure({
|
|
280
|
+
openOnClick: false,
|
|
281
|
+
autolink: false,
|
|
282
|
+
linkOnPaste: false,
|
|
283
|
+
HTMLAttributes: { target: null, rel: null, class: null },
|
|
284
|
+
isAllowedUri: (url) => isAllowedDocumentLinkHref(url),
|
|
285
|
+
}),
|
|
286
|
+
DocumentTable,
|
|
287
|
+
DocumentTableRow,
|
|
288
|
+
DocumentTableHeader,
|
|
289
|
+
DocumentTableCell,
|
|
290
|
+
DocumentVariable,
|
|
291
|
+
DocumentResource,
|
|
292
|
+
DocumentButton,
|
|
293
|
+
DocumentDivider,
|
|
294
|
+
DocumentPageBreak,
|
|
295
|
+
DocumentTextAlign,
|
|
296
|
+
];
|
|
@@ -18,13 +18,31 @@ export declare function createTemplateResolverFeature(opts?: TemplateResolverOpt
|
|
|
18
18
|
upsertTenant: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
19
19
|
publish: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
20
20
|
archive: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
21
|
+
publishSystem: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
22
|
+
archiveSystem: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
23
|
+
resetToSystem: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
21
24
|
set: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
25
|
+
importBundle: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
26
|
+
importBundleSystem: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
27
|
+
copyLocale: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
28
|
+
resourceUpload: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
29
|
+
resourceRemove: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
30
|
+
resourceLinkPdf: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
31
|
+
sendTest: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
22
32
|
};
|
|
23
33
|
queries: {
|
|
24
34
|
findById: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
25
35
|
list: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
26
36
|
bySlug: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
27
37
|
byTenant: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
38
|
+
usage: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
39
|
+
overview: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
40
|
+
layoutSource: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
41
|
+
tenantOverrides: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
42
|
+
diffToSystem: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
43
|
+
validate: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
44
|
+
preview: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
45
|
+
exportBundle: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
|
|
28
46
|
};
|
|
29
47
|
treeHandle: import("@cosmicdrift/kumiko-types/tree-node").TreeActionsHandle<"template-resolver", {
|
|
30
48
|
readonly edit: {
|
|
@@ -1,15 +1,31 @@
|
|
|
1
|
-
import { defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import { createSystemConfig, defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { bySlugQuery } from "./handlers/by-slug.query.js";
|
|
3
3
|
import { byTenantQuery } from "./handlers/by-tenant.query.js";
|
|
4
4
|
import { makeCollectionItemQuery } from "./handlers/collection-item.query.js";
|
|
5
5
|
import { makeCollectionListQuery } from "./handlers/collection-list.query.js";
|
|
6
6
|
import { makeCollectionSetWrite } from "./handlers/collection-set.write.js";
|
|
7
|
+
import { copyLocaleWrite } from "./handlers/copy-locale.write.js";
|
|
8
|
+
import { diffToSystemQuery } from "./handlers/diff-to-system.query.js";
|
|
9
|
+
import { exportBundleQuery } from "./handlers/export-bundle.query.js";
|
|
7
10
|
import { findByIdQuery } from "./handlers/find-by-id.query.js";
|
|
11
|
+
import { importBundleWrite } from "./handlers/import-bundle.write.js";
|
|
12
|
+
import { importBundleSystemWrite } from "./handlers/import-bundle-system.write.js";
|
|
13
|
+
import { layoutSourceQuery } from "./handlers/layout-source.query.js";
|
|
8
14
|
import { listQuery } from "./handlers/list.query.js";
|
|
15
|
+
import { overviewQuery } from "./handlers/overview.query.js";
|
|
16
|
+
import { previewQuery } from "./handlers/preview.query.js";
|
|
17
|
+
import { resetToSystemWrite } from "./handlers/reset-to-system.write.js";
|
|
18
|
+
import { resourceLinkPdfWrite } from "./handlers/resource-link-pdf.write.js";
|
|
19
|
+
import { resourceRemoveWrite } from "./handlers/resource-remove.write.js";
|
|
20
|
+
import { resourceUploadWrite } from "./handlers/resource-upload.write.js";
|
|
21
|
+
import { sendTestWrite } from "./handlers/send-test.write.js";
|
|
9
22
|
import { setWrite } from "./handlers/set.write.js";
|
|
10
|
-
import {
|
|
23
|
+
import { tenantOverridesQuery } from "./handlers/tenant-overrides.query.js";
|
|
24
|
+
import { archiveSystemWrite, archiveWrite, publishSystemWrite, publishWrite, } from "./handlers/toggle-status.write.js";
|
|
11
25
|
import { upsertSystemWrite } from "./handlers/upsert-system.write.js";
|
|
12
26
|
import { upsertTenantWrite } from "./handlers/upsert-tenant.write.js";
|
|
27
|
+
import { usageQuery } from "./handlers/usage.query.js";
|
|
28
|
+
import { validateQuery } from "./handlers/validate.query.js";
|
|
13
29
|
import { templateResourceEntity } from "./table.js";
|
|
14
30
|
import { userContentEntryEntity } from "./user-content-table.js";
|
|
15
31
|
export function createTemplateResolverFeature(opts = {}) {
|
|
@@ -17,11 +33,14 @@ export function createTemplateResolverFeature(opts = {}) {
|
|
|
17
33
|
const hasUserOwned = collections.some((c) => c.ownership === "user");
|
|
18
34
|
return defineFeature("template-resolver", (r) => {
|
|
19
35
|
r.describe([
|
|
20
|
-
"Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail-
|
|
21
|
-
"Reading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers
|
|
36
|
+
"Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`).",
|
|
37
|
+
"Reading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`.",
|
|
22
38
|
"Text an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation.",
|
|
23
39
|
"How a collection is edited follows from `contentFormat`: `plain` gives a text area, `rich` a small WYSIWYG (bold, italic, headings, lists, links), `markdown` a text area that stores markdown text (same insertable chips as `plain`). All three offer the collection's `variableSchema` as insertable chips and a preview rendered with sample data — an editor sees what `{{firstName}}` becomes without sending a mail. An app can register its own editor for a format; the last `clientFeature` that registers a format wins (a conflict logs a warning).",
|
|
24
40
|
'A collection is tenant-wide by default. With `ownership: "user"` every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate `user-content-entry` entity and count as user data, so mounting one also requires the `template-resolver-user-data` feature and a migration on the app side.',
|
|
41
|
+
"`validate` dry-runs the write pipeline for a draft (sanitizer, layout rules, placeholders) and reports what the template uses and misses. `export-bundle` / `import-bundle` (and `import-bundle-system`) move one template with all locales and resource files between tenants or environments; `copy-locale` starts a translation from an existing locale. Layouts are structured: a mail or PDF layout is `data-header` / `data-content` / `data-signature` / `data-footer` regions, and mjml layout source is system-only.",
|
|
42
|
+
"`preview` renders a draft (or the stored template) with sample values taken from the `example` of each declared variable, through the same gates and the same mail renderer as a real send, and returns subject, html, `displayHtml` (the html with cid images as data URIs for an editor), text, attachments (inline images and files) and `checks` (images resolved, plain text present, size under 10 MiB, required placeholders used); nothing is saved or sent. `send-test` mails that rendering to the calling user's own address only, subject prefixed with `[TEST]`, at most 10 per user and hour; it needs `delivery`, `channel-email` and `renderer-mail-html` to be mounted.",
|
|
43
|
+
'Images used by a template (`<img data-resource="logo">`) are uploaded with `resourceUpload`, which re-encodes them, stores a file and links it under the key; `resourceRemove` unlinks it. `linkedResources` is written only through these two handlers. With `purpose: "attachment"` and a `fileName`, `resourceUpload` stores a PDF, PNG or JPEG as a file attachment of a mail template (original bytes, at most 10) that the mail renderer attaches after the inline images. `resourceLinkPdf` instead links a `document-pdf` template under a key: every mail sent from the template then carries that PDF, rendered at send time with the mail\'s own variables and locale, under a file name pattern such as `invoice-{{number}}.pdf`; files and linked PDFs share the limit of 10.',
|
|
25
44
|
"Replaces the former `text-content` feature; its blocks now live here as kind `text-block`.",
|
|
26
45
|
].join("\n\n"));
|
|
27
46
|
r.uiHints({
|
|
@@ -29,6 +48,11 @@ export function createTemplateResolverFeature(opts = {}) {
|
|
|
29
48
|
category: "notifications",
|
|
30
49
|
recommended: false,
|
|
31
50
|
});
|
|
51
|
+
r.config({
|
|
52
|
+
keys: {
|
|
53
|
+
resourceMaxBytes: createSystemConfig("number", { default: 2_097_152, bounds: { min: 1 } }),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
32
56
|
r.entity("template-resource", templateResourceEntity);
|
|
33
57
|
// Only when the app actually mounts a user-owned collection: the entity's
|
|
34
58
|
// `content` is `userOwned`, which makes it subject data — the boot guard
|
|
@@ -41,13 +65,31 @@ export function createTemplateResolverFeature(opts = {}) {
|
|
|
41
65
|
upsertTenant: r.writeHandler(upsertTenantWrite),
|
|
42
66
|
publish: r.writeHandler(publishWrite),
|
|
43
67
|
archive: r.writeHandler(archiveWrite),
|
|
68
|
+
publishSystem: r.writeHandler(publishSystemWrite),
|
|
69
|
+
archiveSystem: r.writeHandler(archiveSystemWrite),
|
|
70
|
+
resetToSystem: r.writeHandler(resetToSystemWrite),
|
|
44
71
|
set: r.writeHandler(setWrite),
|
|
72
|
+
importBundle: r.writeHandler(importBundleWrite),
|
|
73
|
+
importBundleSystem: r.writeHandler(importBundleSystemWrite),
|
|
74
|
+
copyLocale: r.writeHandler(copyLocaleWrite),
|
|
75
|
+
resourceUpload: r.writeHandler(resourceUploadWrite),
|
|
76
|
+
resourceRemove: r.writeHandler(resourceRemoveWrite),
|
|
77
|
+
resourceLinkPdf: r.writeHandler(resourceLinkPdfWrite),
|
|
78
|
+
sendTest: r.writeHandler(sendTestWrite),
|
|
45
79
|
};
|
|
46
80
|
const queries = {
|
|
47
81
|
findById: r.queryHandler(findByIdQuery),
|
|
48
82
|
list: r.queryHandler(listQuery),
|
|
49
83
|
bySlug: r.queryHandler(bySlugQuery),
|
|
50
84
|
byTenant: r.queryHandler(byTenantQuery),
|
|
85
|
+
usage: r.queryHandler(usageQuery),
|
|
86
|
+
overview: r.queryHandler(overviewQuery),
|
|
87
|
+
layoutSource: r.queryHandler(layoutSourceQuery),
|
|
88
|
+
tenantOverrides: r.queryHandler(tenantOverridesQuery),
|
|
89
|
+
diffToSystem: r.queryHandler(diffToSystemQuery),
|
|
90
|
+
validate: r.queryHandler(validateQuery),
|
|
91
|
+
preview: r.queryHandler(previewQuery),
|
|
92
|
+
exportBundle: r.queryHandler(exportBundleQuery),
|
|
51
93
|
};
|
|
52
94
|
for (const collection of collections) {
|
|
53
95
|
r.contentCollection(collection);
|
|
@@ -58,10 +100,12 @@ export function createTemplateResolverFeature(opts = {}) {
|
|
|
58
100
|
// Visual-tree actions for the text-block content tree. The handle is
|
|
59
101
|
// propagated through the setup export so other features can build
|
|
60
102
|
// compile-time-typed cross-feature edit targets.
|
|
103
|
+
// r.treeActions keeps literal types (const generic); the args only declare string-typed slots.
|
|
104
|
+
const emptyString = "";
|
|
61
105
|
const treeHandle = r.treeActions({
|
|
62
|
-
edit: { args: { slug:
|
|
106
|
+
edit: { args: { slug: emptyString, locale: emptyString } },
|
|
63
107
|
list: {},
|
|
64
|
-
create: { args: { folder:
|
|
108
|
+
create: { args: { folder: emptyString } },
|
|
65
109
|
});
|
|
66
110
|
return { handlers, queries, treeHandle };
|
|
67
111
|
});
|