@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,80 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { defineQueryHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
import { imageResourceKeysOf } from "../linked-resources.js";
|
|
5
|
+
import { templateResourcesTable } from "../table.js";
|
|
6
|
+
import { resolveTemplateVariables, systemPlaceholdersFor } from "../template-validation.js";
|
|
7
|
+
import { contentFormatSchema, kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
|
|
8
|
+
import { evaluateTemplateWrite } from "./template-gates.js";
|
|
9
|
+
async function knownResourceKeys(db, where) {
|
|
10
|
+
const rows = await selectMany(db, templateResourcesTable, {
|
|
11
|
+
tenantId: where.tenantId,
|
|
12
|
+
slug: where.slug,
|
|
13
|
+
kind: where.kind,
|
|
14
|
+
scope: where.scope,
|
|
15
|
+
...(where.locale === undefined ? {} : { locale: where.locale }),
|
|
16
|
+
...(where.scope === "system" ? { status: "active" } : {}),
|
|
17
|
+
});
|
|
18
|
+
return rows.flatMap((row) => imageResourceKeysOf(row.linkedResources));
|
|
19
|
+
}
|
|
20
|
+
// The dry run of the write pipeline; preview feeds on the evaluation, validate returns the response.
|
|
21
|
+
export async function validateTemplateDraft(ctx, user, payload) {
|
|
22
|
+
const variables = resolveTemplateVariables(ctx.registry, payload.slug, payload.variableSchema);
|
|
23
|
+
const { slug, kind, scope, locale } = payload;
|
|
24
|
+
const known = [
|
|
25
|
+
...(await knownResourceKeys(ctx.db, {
|
|
26
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
27
|
+
slug,
|
|
28
|
+
kind,
|
|
29
|
+
scope: "system",
|
|
30
|
+
locale,
|
|
31
|
+
})),
|
|
32
|
+
...(scope === "tenant"
|
|
33
|
+
? await knownResourceKeys(ctx.db, { tenantId: user.tenantId, slug, kind, scope, locale })
|
|
34
|
+
: []),
|
|
35
|
+
];
|
|
36
|
+
const evaluation = evaluateTemplateWrite({
|
|
37
|
+
kind,
|
|
38
|
+
scope,
|
|
39
|
+
contentFormat: payload.contentFormat,
|
|
40
|
+
content: payload.content,
|
|
41
|
+
rawMeta: payload.meta,
|
|
42
|
+
variables,
|
|
43
|
+
resourceKeys: known,
|
|
44
|
+
});
|
|
45
|
+
const validation = evaluation.validation;
|
|
46
|
+
const response = {
|
|
47
|
+
canSave: evaluation.ok,
|
|
48
|
+
canPublish: evaluation.ok && evaluation.publishIssues.length === 0,
|
|
49
|
+
issues: evaluation.ok ? [] : [...evaluation.issues],
|
|
50
|
+
publishIssues: evaluation.ok ? [...evaluation.publishIssues] : [],
|
|
51
|
+
used: [...(validation?.used ?? [])],
|
|
52
|
+
unknown: [...(validation?.unknown ?? [])],
|
|
53
|
+
missingRequired: [...(validation?.missingRequired ?? [])],
|
|
54
|
+
warnings: (validation?.unknownResources ?? []).map((key) => ({
|
|
55
|
+
code: "resource_not_uploaded",
|
|
56
|
+
detail: key,
|
|
57
|
+
i18nKey: "templateResolver.warnings.resourceNotUploaded",
|
|
58
|
+
})),
|
|
59
|
+
variables,
|
|
60
|
+
systemPlaceholders: [...systemPlaceholdersFor(kind)].sort(),
|
|
61
|
+
};
|
|
62
|
+
return { response, evaluation };
|
|
63
|
+
}
|
|
64
|
+
export const validateQuery = defineQueryHandler({
|
|
65
|
+
name: "validate",
|
|
66
|
+
schema: z.object({
|
|
67
|
+
slug: templateSlugSchema,
|
|
68
|
+
kind: kindSchema,
|
|
69
|
+
contentFormat: contentFormatSchema,
|
|
70
|
+
content: z.string().max(200_000),
|
|
71
|
+
meta: z.unknown().optional(),
|
|
72
|
+
variableSchema: z.record(z.string(), z.unknown()).default({}),
|
|
73
|
+
/** Narrows the known resource keys to one locale's rows. */
|
|
74
|
+
locale: localeSchema.optional(),
|
|
75
|
+
scope: z.enum(["tenant", "system"]).default("tenant"),
|
|
76
|
+
}),
|
|
77
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
78
|
+
description: "Dry-runs the write pipeline for a template draft without saving: reports whether it can be saved or published, the sanitizer and placeholder problems, which placeholders it uses, which are unknown, which required variables are missing, and the variables and system placeholders available for the kind; use it to give an editor live feedback.",
|
|
79
|
+
handler: async (query, ctx) => (await validateTemplateDraft(ctx, query.user, query.payload)).response,
|
|
80
|
+
});
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
export { createTemplateResolverApi, type ResolveRequest, requireTemplateResolver, TemplateNotFoundError, type TemplateResolverApi, type TemplateResource, } from "./api.js";
|
|
2
|
-
export {
|
|
2
|
+
export { type CompileDocumentBodyOptions, collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.js";
|
|
3
|
+
export { CONTENT_FORMATS, type ContentFormat, DEFAULT_MAIL_LAYOUT_SLUG, DOCUMENT_HTML_KINDS, type DocumentHtmlKind, FALLBACK_LOCALE, NON_RENDER_KINDS, type NonRenderKind, PDF_PAGE_FORMATS, type PdfPageFormat, RENDER_KINDS, type RenderKind, SYSTEM_TENANT_ID, TEMPLATE_KINDS, TEMPLATE_SCOPES, TEMPLATE_STATUSES, TEXT_BLOCK_KIND, type TemplateKind, type TemplateScope, type TemplateStatus, UPSERT_KINDS, type UpsertKind, } from "./constants.js";
|
|
4
|
+
export { type DocumentViolation, type DocumentViolationCode, documentViolationsToFieldIssues, type SanitizeBodyResult, type SanitizeHtmlResult, sanitizeDocumentHtml, sanitizeTemplateBody, } from "./document-sanitizer.js";
|
|
5
|
+
export { DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentRepeatRow, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, type DocumentTextAlignment, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
3
6
|
export { createTemplateResolverFeature } from "./feature.js";
|
|
7
|
+
export type { LayoutSourceResponse, LayoutSourceRow } from "./handlers/layout-source.query.js";
|
|
8
|
+
export type { OverviewKindGroup, OverviewOrigin, OverviewResponse, OverviewRow, OverviewUsage, } from "./handlers/overview.query.js";
|
|
9
|
+
export { localeSchema } from "./handlers/shared.js";
|
|
10
|
+
export { type LayoutRegions, readLayoutRegions, type SanitizeLayoutResult, sanitizeLayoutHtml, } from "./layout-regions.js";
|
|
11
|
+
export { fileAttachmentsOf, imageLinksOf, type LinkedResourceEntry, parseLinkedResourceEntries, pdfAttachmentLinksOf, type TemplatePdfAttachmentLink, templatePdfAttachmentLinkSchema, } from "./linked-resources.js";
|
|
12
|
+
export { prepareDocumentForPrint } from "./print-document.js";
|
|
4
13
|
export { TEMPLATE_RESOLVER_FEATURE, TemplateResolverErrors, TemplateResolverHandlers, TemplateResolverQueries, } from "./qualified-names.js";
|
|
14
|
+
export { mapRecordToVariables } from "./record-mapping.js";
|
|
15
|
+
export { readTemplateResourceMaxBytes, type StoredTemplateResource, storeTemplateResource, } from "./store-template-resource.js";
|
|
5
16
|
export { type TemplateResourceRow, templateResourceEntity, templateResourcesTable, } from "./table.js";
|
|
17
|
+
export { type TemplateBundle, templateBundleSchema } from "./template-bundle.js";
|
|
18
|
+
export { isMailTemplateMeta, type LayoutTemplateMeta, MAIL_CATEGORIES, type MailCategory, type MailTemplateMeta, type PdfTemplateMeta, parseTemplateMeta, pdfAttachmentFileNamePatternSchema, readPdfTemplateMeta, type TemplateMeta, type TemplateMetaIssue, type TemplateMetaResult, templateSlugSchema, } from "./template-meta.js";
|
|
19
|
+
export { type DocumentDraftOutcome, evaluateDocumentDraft } from "./template-preview.js";
|
|
20
|
+
export { readTemplateResource, type TemplateResourceFile } from "./template-resources.js";
|
|
21
|
+
export { collectTemplateUsage, type TemplateUsageEntry } from "./template-usage.js";
|
|
22
|
+
export { hasPlaceholderProblems, resolveTemplateVariables, SYSTEM_PLACEHOLDERS, systemPlaceholdersFor, type TemplateValidation, validateTemplatePlaceholders, } from "./template-validation.js";
|
|
6
23
|
export { type UserContentEntryRow, userContentEntriesTable, userContentEntryEntity, } from "./user-content-table.js";
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
export { createTemplateResolverApi, requireTemplateResolver, TemplateNotFoundError, } from "./api.js";
|
|
2
|
-
export {
|
|
2
|
+
export { collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.js";
|
|
3
|
+
export { CONTENT_FORMATS, DEFAULT_MAIL_LAYOUT_SLUG, DOCUMENT_HTML_KINDS, FALLBACK_LOCALE, NON_RENDER_KINDS, PDF_PAGE_FORMATS, RENDER_KINDS, SYSTEM_TENANT_ID, TEMPLATE_KINDS, TEMPLATE_SCOPES, TEMPLATE_STATUSES, TEXT_BLOCK_KIND, UPSERT_KINDS, } from "./constants.js";
|
|
4
|
+
export { documentViolationsToFieldIssues, sanitizeDocumentHtml, sanitizeTemplateBody, } from "./document-sanitizer.js";
|
|
5
|
+
export { DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentRepeatRow, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
3
6
|
export { createTemplateResolverFeature } from "./feature.js";
|
|
7
|
+
export { localeSchema } from "./handlers/shared.js";
|
|
8
|
+
export { readLayoutRegions, sanitizeLayoutHtml, } from "./layout-regions.js";
|
|
9
|
+
export { fileAttachmentsOf, imageLinksOf, parseLinkedResourceEntries, pdfAttachmentLinksOf, templatePdfAttachmentLinkSchema, } from "./linked-resources.js";
|
|
10
|
+
export { prepareDocumentForPrint } from "./print-document.js";
|
|
4
11
|
export { TEMPLATE_RESOLVER_FEATURE, TemplateResolverErrors, TemplateResolverHandlers, TemplateResolverQueries, } from "./qualified-names.js";
|
|
12
|
+
export { mapRecordToVariables } from "./record-mapping.js";
|
|
13
|
+
export { readTemplateResourceMaxBytes, storeTemplateResource, } from "./store-template-resource.js";
|
|
5
14
|
export { templateResourceEntity, templateResourcesTable, } from "./table.js";
|
|
15
|
+
export { templateBundleSchema } from "./template-bundle.js";
|
|
16
|
+
export { isMailTemplateMeta, MAIL_CATEGORIES, parseTemplateMeta, pdfAttachmentFileNamePatternSchema, readPdfTemplateMeta, templateSlugSchema, } from "./template-meta.js";
|
|
17
|
+
export { evaluateDocumentDraft } from "./template-preview.js";
|
|
18
|
+
export { readTemplateResource } from "./template-resources.js";
|
|
19
|
+
export { collectTemplateUsage } from "./template-usage.js";
|
|
20
|
+
export { hasPlaceholderProblems, resolveTemplateVariables, SYSTEM_PLACEHOLDERS, systemPlaceholdersFor, validateTemplatePlaceholders, } from "./template-validation.js";
|
|
6
21
|
export { userContentEntriesTable, userContentEntryEntity, } from "./user-content-table.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ContentFormat } from "./constants.js";
|
|
2
|
+
import { type DocumentViolation, type SanitizeBodyResult } from "./document-sanitizer.js";
|
|
3
|
+
export type LayoutRegions = {
|
|
4
|
+
readonly header: string;
|
|
5
|
+
readonly signature: string;
|
|
6
|
+
readonly footer: string;
|
|
7
|
+
};
|
|
8
|
+
export type SanitizeLayoutResult = {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly html: string;
|
|
11
|
+
} | {
|
|
12
|
+
readonly ok: false;
|
|
13
|
+
readonly violations: readonly DocumentViolation[];
|
|
14
|
+
};
|
|
15
|
+
export declare function sanitizeLayoutHtml(html: string): SanitizeLayoutResult;
|
|
16
|
+
export declare function readLayoutRegions(html: string): LayoutRegions;
|
|
17
|
+
export type TemplateWriteScope = "system" | "tenant";
|
|
18
|
+
export declare function sanitizeTemplateWriteBody(input: {
|
|
19
|
+
readonly kind: string;
|
|
20
|
+
readonly scope: TemplateWriteScope;
|
|
21
|
+
readonly contentFormat: ContentFormat;
|
|
22
|
+
readonly content: string | null;
|
|
23
|
+
}): SanitizeBodyResult;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Server-only (happy-dom). A structured layout is the editable frame around a
|
|
2
|
+
// mail or PDF body: header, signature and footer regions plus one empty
|
|
3
|
+
// content slot. Everything else is rejected so a layout can never carry markup
|
|
4
|
+
// the renderer did not plan for.
|
|
5
|
+
import { sanitizeDocumentHtml, sanitizeTemplateBody, violation, withParsedDocument, } from "./document-sanitizer.js";
|
|
6
|
+
const REGION_ATTRIBUTES = ["data-header", "data-content", "data-signature", "data-footer"];
|
|
7
|
+
const MAIL_LAYOUT_CONTENT_SLOT = /\{\{\{\s*content\s*\}\}\}/g;
|
|
8
|
+
const ELEMENT_NODE = 1;
|
|
9
|
+
const TEXT_NODE = 3;
|
|
10
|
+
function regionAttributeOf(element) {
|
|
11
|
+
if (element.tagName.toLowerCase() !== "div")
|
|
12
|
+
return null;
|
|
13
|
+
const present = REGION_ATTRIBUTES.filter((name) => element.hasAttribute(name));
|
|
14
|
+
return present.length === 1 ? (present[0] ?? null) : null;
|
|
15
|
+
}
|
|
16
|
+
function parseRegions(html) {
|
|
17
|
+
return withParsedDocument(html, (body) => {
|
|
18
|
+
const innerHtml = {};
|
|
19
|
+
const violations = [];
|
|
20
|
+
let contentIsEmpty = true;
|
|
21
|
+
const invalid = (detail, message) => violations.push(violation("layout_invalid_structure", detail, message));
|
|
22
|
+
for (const node of Array.from(body.childNodes)) {
|
|
23
|
+
if (node.nodeType === ELEMENT_NODE)
|
|
24
|
+
continue;
|
|
25
|
+
if (node.nodeType !== TEXT_NODE || (node.textContent ?? "").trim() !== "") {
|
|
26
|
+
invalid("text", "Text and comments outside the layout regions are not allowed");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const element of Array.from(body.children)) {
|
|
30
|
+
const region = regionAttributeOf(element);
|
|
31
|
+
if (region === null) {
|
|
32
|
+
invalid(element.tagName.toLowerCase(), "Only <div data-header>, <div data-content>, <div data-signature> and <div data-footer> are allowed at the top level");
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (innerHtml[region] !== undefined) {
|
|
36
|
+
invalid(region, `Layout region ${region} may appear only once`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
innerHtml[region] = element.innerHTML;
|
|
40
|
+
if (region === "data-content") {
|
|
41
|
+
contentIsEmpty = element.children.length === 0 && (element.textContent ?? "").trim() === "";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { innerHtml, contentIsEmpty, violations };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function sanitizeLayoutHtml(html) {
|
|
48
|
+
const parsed = parseRegions(html);
|
|
49
|
+
const violations = [...parsed.violations];
|
|
50
|
+
if (parsed.innerHtml["data-content"] === undefined) {
|
|
51
|
+
violations.push(violation("layout_missing_content_slot", "data-content", "A layout needs exactly one empty <div data-content></div> where the body is placed"));
|
|
52
|
+
}
|
|
53
|
+
else if (!parsed.contentIsEmpty) {
|
|
54
|
+
violations.push(violation("layout_invalid_structure", "data-content", "The data-content region must be empty"));
|
|
55
|
+
}
|
|
56
|
+
const cleanedHtml = {};
|
|
57
|
+
for (const name of ["data-header", "data-signature", "data-footer"]) {
|
|
58
|
+
const inner = parsed.innerHtml[name];
|
|
59
|
+
if (inner === undefined || inner.trim() === "")
|
|
60
|
+
continue;
|
|
61
|
+
const result = sanitizeDocumentHtml(inner);
|
|
62
|
+
if (result.ok)
|
|
63
|
+
cleanedHtml[name] = result.html;
|
|
64
|
+
else
|
|
65
|
+
violations.push(...result.violations);
|
|
66
|
+
}
|
|
67
|
+
if (violations.length > 0)
|
|
68
|
+
return { ok: false, violations };
|
|
69
|
+
const part = (name) => {
|
|
70
|
+
const regionHtml = cleanedHtml[name];
|
|
71
|
+
return regionHtml === undefined ? "" : `<div ${name}>${regionHtml}</div>`;
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
html: [
|
|
76
|
+
part("data-header"),
|
|
77
|
+
"<div data-content></div>",
|
|
78
|
+
part("data-signature"),
|
|
79
|
+
part("data-footer"),
|
|
80
|
+
].join(""),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function readLayoutRegions(html) {
|
|
84
|
+
const { innerHtml } = parseRegions(html);
|
|
85
|
+
return {
|
|
86
|
+
header: innerHtml["data-header"] ?? "",
|
|
87
|
+
signature: innerHtml["data-signature"] ?? "",
|
|
88
|
+
footer: innerHtml["data-footer"] ?? "",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function rejected(violationItem) {
|
|
92
|
+
return { ok: false, violations: [violationItem] };
|
|
93
|
+
}
|
|
94
|
+
function sanitizeMailLayout(scope, contentFormat, content) {
|
|
95
|
+
if (contentFormat === "html")
|
|
96
|
+
return fromLayoutResult(sanitizeLayoutHtml(content));
|
|
97
|
+
if (contentFormat !== "mjml") {
|
|
98
|
+
return rejected(violation("layout_requires_mjml_format", contentFormat, `Templates of kind mail-layout require contentFormat mjml or html, got "${contentFormat}"`));
|
|
99
|
+
}
|
|
100
|
+
if (scope !== "system") {
|
|
101
|
+
return rejected(violation("layout_source_system_only", "mjml", "Only system layouts may use mjml source; tenant layouts use the structured html format"));
|
|
102
|
+
}
|
|
103
|
+
if ((content.match(MAIL_LAYOUT_CONTENT_SLOT) ?? []).length !== 1) {
|
|
104
|
+
return rejected(violation("layout_missing_content_slot", "{{{content}}}", "A mjml layout must contain {{{content}}} exactly once"));
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, content };
|
|
107
|
+
}
|
|
108
|
+
function fromLayoutResult(result) {
|
|
109
|
+
return result.ok ? { ok: true, content: result.html } : result;
|
|
110
|
+
}
|
|
111
|
+
// One entry for every write path: layout kinds follow the scope-dependent
|
|
112
|
+
// layout rules, everything else the plain document sanitizer.
|
|
113
|
+
export function sanitizeTemplateWriteBody(input) {
|
|
114
|
+
const { kind, scope, contentFormat, content } = input;
|
|
115
|
+
if (kind === "mail-layout" && content !== null) {
|
|
116
|
+
return sanitizeMailLayout(scope, contentFormat, content);
|
|
117
|
+
}
|
|
118
|
+
if (kind === "pdf-layout" && content !== null) {
|
|
119
|
+
if (contentFormat !== "html") {
|
|
120
|
+
return rejected(violation("document_requires_html_format", contentFormat, `Templates of kind pdf-layout require contentFormat html, got "${contentFormat}"`));
|
|
121
|
+
}
|
|
122
|
+
return fromLayoutResult(sanitizeLayoutHtml(content));
|
|
123
|
+
}
|
|
124
|
+
return sanitizeTemplateBody({ kind, contentFormat, content });
|
|
125
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import type { FileAttachmentRef } from "../renderer-foundation/index.js";
|
|
3
|
+
export declare const templatePdfAttachmentLinkSchema: z.ZodObject<{
|
|
4
|
+
kind: z.ZodLiteral<"rendered-pdf">;
|
|
5
|
+
templateSlug: z.ZodString;
|
|
6
|
+
fileName: z.ZodString;
|
|
7
|
+
}, z.core.$strict>;
|
|
8
|
+
export type TemplatePdfAttachmentLink = z.infer<typeof templatePdfAttachmentLinkSchema>;
|
|
9
|
+
export type LinkedResourceEntry = string | FileAttachmentRef | TemplatePdfAttachmentLink;
|
|
10
|
+
export declare function isFileAttachmentEntry(entry: LinkedResourceEntry): entry is FileAttachmentRef;
|
|
11
|
+
export declare function isPdfAttachmentLink(entry: LinkedResourceEntry): entry is TemplatePdfAttachmentLink;
|
|
12
|
+
export declare function parseLinkedResourceEntries(raw: string | null): Record<string, LinkedResourceEntry>;
|
|
13
|
+
export declare function imageLinksOf(entries: Readonly<Record<string, LinkedResourceEntry>>): Record<string, string>;
|
|
14
|
+
export declare function fileAttachmentsOf(entries: Readonly<Record<string, LinkedResourceEntry>>): FileAttachmentRef[];
|
|
15
|
+
export declare function pdfAttachmentLinksOf(entries: Readonly<Record<string, LinkedResourceEntry>>): TemplatePdfAttachmentLink[];
|
|
16
|
+
export declare function attachmentEntriesOf(entries: Readonly<Record<string, LinkedResourceEntry>>): [string, FileAttachmentRef | TemplatePdfAttachmentLink][];
|
|
17
|
+
export declare function imageResourceKeysOf(raw: string | null): string[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { pdfAttachmentFileNamePatternSchema, templateSlugSchema } from "./template-meta.js";
|
|
4
|
+
export const templatePdfAttachmentLinkSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
kind: z.literal("rendered-pdf"),
|
|
7
|
+
templateSlug: templateSlugSchema,
|
|
8
|
+
fileName: pdfAttachmentFileNamePatternSchema,
|
|
9
|
+
})
|
|
10
|
+
.strict();
|
|
11
|
+
export function isFileAttachmentEntry(entry) {
|
|
12
|
+
return typeof entry !== "string" && entry.kind === "file";
|
|
13
|
+
}
|
|
14
|
+
export function isPdfAttachmentLink(entry) {
|
|
15
|
+
return typeof entry !== "string" && entry.kind === "rendered-pdf";
|
|
16
|
+
}
|
|
17
|
+
function toLinkedResourceEntry(raw) {
|
|
18
|
+
if (typeof raw === "string")
|
|
19
|
+
return raw;
|
|
20
|
+
if (!isPlainObject(raw))
|
|
21
|
+
return null;
|
|
22
|
+
if (raw["kind"] === "rendered-pdf") {
|
|
23
|
+
const link = templatePdfAttachmentLinkSchema.safeParse(raw);
|
|
24
|
+
return link.success ? link.data : null;
|
|
25
|
+
}
|
|
26
|
+
const { fileRefId, fileName } = raw;
|
|
27
|
+
if (raw["kind"] !== "file" || typeof fileRefId !== "string" || typeof fileName !== "string") {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return { kind: "file", fileRefId, fileName };
|
|
31
|
+
}
|
|
32
|
+
export function parseLinkedResourceEntries(raw) {
|
|
33
|
+
if (!raw)
|
|
34
|
+
return {};
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
if (!isPlainObject(parsed))
|
|
43
|
+
return {};
|
|
44
|
+
const entries = {};
|
|
45
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
46
|
+
const entry = toLinkedResourceEntry(value);
|
|
47
|
+
if (entry !== null)
|
|
48
|
+
entries[key] = entry;
|
|
49
|
+
}
|
|
50
|
+
return entries;
|
|
51
|
+
}
|
|
52
|
+
export function imageLinksOf(entries) {
|
|
53
|
+
const images = {};
|
|
54
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
55
|
+
if (typeof entry === "string")
|
|
56
|
+
images[key] = entry;
|
|
57
|
+
}
|
|
58
|
+
return images;
|
|
59
|
+
}
|
|
60
|
+
export function fileAttachmentsOf(entries) {
|
|
61
|
+
return Object.entries(entries)
|
|
62
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
63
|
+
.flatMap(([, entry]) => (isFileAttachmentEntry(entry) ? [entry] : []));
|
|
64
|
+
}
|
|
65
|
+
export function pdfAttachmentLinksOf(entries) {
|
|
66
|
+
return Object.entries(entries)
|
|
67
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
68
|
+
.flatMap(([, entry]) => (isPdfAttachmentLink(entry) ? [entry] : []));
|
|
69
|
+
}
|
|
70
|
+
export function attachmentEntriesOf(entries) {
|
|
71
|
+
return Object.entries(entries).flatMap(([key, entry]) => typeof entry === "string" ? [] : [[key, entry]]);
|
|
72
|
+
}
|
|
73
|
+
export function imageResourceKeysOf(raw) {
|
|
74
|
+
return Object.keys(imageLinksOf(parseLinkedResourceEntries(raw)));
|
|
75
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { InlineAttachmentRef } from "../renderer-foundation/index.js";
|
|
2
|
+
export declare const PREVIEW_IMAGE_BUDGET_BYTES: number;
|
|
3
|
+
export type PreviewAttachmentReader = (ref: InlineAttachmentRef) => Promise<{
|
|
4
|
+
readonly content: Uint8Array;
|
|
5
|
+
readonly contentType: string;
|
|
6
|
+
} | null>;
|
|
7
|
+
export declare function buildDisplayHtml(html: string, inline: readonly InlineAttachmentRef[], readAttachment: PreviewAttachmentReader): Promise<string>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const PREVIEW_IMAGE_BUDGET_BYTES = 5 * 1024 * 1024;
|
|
2
|
+
const DISPLAYABLE_IMAGE_TYPES = new Set([
|
|
3
|
+
"image/png",
|
|
4
|
+
"image/jpeg",
|
|
5
|
+
"image/gif",
|
|
6
|
+
"image/webp",
|
|
7
|
+
]);
|
|
8
|
+
// The preview shows images the mail client would resolve by Content-ID; a browser cannot, so each
|
|
9
|
+
// resolved image becomes a data URI. `html` itself stays untouched because the send path needs the cids.
|
|
10
|
+
export async function buildDisplayHtml(html, inline, readAttachment) {
|
|
11
|
+
let displayHtml = html;
|
|
12
|
+
let usedBytes = 0;
|
|
13
|
+
for (const ref of inline) {
|
|
14
|
+
const needle = `src="cid:${ref.cid}"`;
|
|
15
|
+
if (!displayHtml.includes(needle))
|
|
16
|
+
continue;
|
|
17
|
+
const found = await readAttachment(ref);
|
|
18
|
+
if (found === null || !DISPLAYABLE_IMAGE_TYPES.has(found.contentType))
|
|
19
|
+
continue;
|
|
20
|
+
if (usedBytes + found.content.length > PREVIEW_IMAGE_BUDGET_BYTES)
|
|
21
|
+
continue;
|
|
22
|
+
usedBytes += found.content.length;
|
|
23
|
+
const dataUri = `src="data:${found.contentType};base64,${Buffer.from(found.content).toString("base64")}"`;
|
|
24
|
+
displayHtml = displayHtml.replaceAll(needle, () => dataUri);
|
|
25
|
+
}
|
|
26
|
+
return displayHtml;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function prepareDocumentForPrint(html: string): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Server-only (happy-dom). Prepares a rendered document body for the browser
|
|
2
|
+
// that prints it: repeating table headers become real <thead> rows, and
|
|
3
|
+
// everything that would make the browser fetch something external is removed.
|
|
4
|
+
import { withParsedDocument } from "./document-sanitizer.js";
|
|
5
|
+
const REPEAT_HEADER_ATTRIBUTE = "data-repeat-header";
|
|
6
|
+
const FETCHING_ELEMENTS = "script, link, iframe, object, embed, video, audio, source, meta, base, style";
|
|
7
|
+
const FETCHING_ATTRIBUTES = ["src", "srcset", "poster", "background", "data"];
|
|
8
|
+
// Anchors only navigate; on svg image/use/feImage an href fetches.
|
|
9
|
+
const HREF_ATTRIBUTES = ["href", "xlink:href"];
|
|
10
|
+
const EXTERNAL_STYLE_REFERENCE = /url\(|@import/i;
|
|
11
|
+
function isEmbeddedValue(value) {
|
|
12
|
+
return value.trim().toLowerCase().startsWith("data:");
|
|
13
|
+
}
|
|
14
|
+
function moveRepeatHeadersIntoThead(table) {
|
|
15
|
+
const rows = Array.from(table.querySelectorAll("tr")).filter((row) => row.closest("table") === table);
|
|
16
|
+
const leading = [];
|
|
17
|
+
for (const row of rows) {
|
|
18
|
+
if (!row.hasAttribute(REPEAT_HEADER_ATTRIBUTE))
|
|
19
|
+
break;
|
|
20
|
+
leading.push(row);
|
|
21
|
+
}
|
|
22
|
+
// skip: a table without leading repeat-header rows keeps its structure as written
|
|
23
|
+
if (leading.length === 0)
|
|
24
|
+
return;
|
|
25
|
+
const thead = table.ownerDocument.createElement("thead");
|
|
26
|
+
for (const row of leading) {
|
|
27
|
+
row.removeAttribute(REPEAT_HEADER_ATTRIBUTE);
|
|
28
|
+
thead.appendChild(row);
|
|
29
|
+
}
|
|
30
|
+
table.insertBefore(thead, table.firstChild);
|
|
31
|
+
}
|
|
32
|
+
export function prepareDocumentForPrint(html) {
|
|
33
|
+
return withParsedDocument(html, (body) => {
|
|
34
|
+
for (const table of Array.from(body.querySelectorAll("table"))) {
|
|
35
|
+
moveRepeatHeadersIntoThead(table);
|
|
36
|
+
}
|
|
37
|
+
for (const element of Array.from(body.querySelectorAll(FETCHING_ELEMENTS)))
|
|
38
|
+
element.remove();
|
|
39
|
+
for (const element of Array.from(body.querySelectorAll("*"))) {
|
|
40
|
+
for (const name of FETCHING_ATTRIBUTES) {
|
|
41
|
+
const value = element.getAttribute(name);
|
|
42
|
+
if (value !== null && !isEmbeddedValue(value))
|
|
43
|
+
element.removeAttribute(name);
|
|
44
|
+
}
|
|
45
|
+
if (element.tagName.toLowerCase() !== "a") {
|
|
46
|
+
for (const name of HREF_ATTRIBUTES)
|
|
47
|
+
element.removeAttribute(name);
|
|
48
|
+
}
|
|
49
|
+
const style = element.getAttribute("style");
|
|
50
|
+
if (style !== null && EXTERNAL_STYLE_REFERENCE.test(style))
|
|
51
|
+
element.removeAttribute("style");
|
|
52
|
+
}
|
|
53
|
+
return body.innerHTML;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -4,11 +4,29 @@ export declare const TemplateResolverHandlers: {
|
|
|
4
4
|
readonly upsertTenant: "template-resolver:write:upsert-tenant";
|
|
5
5
|
readonly publish: "template-resolver:write:publish";
|
|
6
6
|
readonly archive: "template-resolver:write:archive";
|
|
7
|
+
readonly publishSystem: "template-resolver:write:publish-system";
|
|
8
|
+
readonly archiveSystem: "template-resolver:write:archive-system";
|
|
9
|
+
readonly resetToSystem: "template-resolver:write:reset-to-system";
|
|
7
10
|
readonly set: "template-resolver:write:set";
|
|
11
|
+
readonly importBundle: "template-resolver:write:import-bundle";
|
|
12
|
+
readonly importBundleSystem: "template-resolver:write:import-bundle-system";
|
|
13
|
+
readonly copyLocale: "template-resolver:write:copy-locale";
|
|
14
|
+
readonly resourceUpload: "template-resolver:write:resource-upload";
|
|
15
|
+
readonly resourceRemove: "template-resolver:write:resource-remove";
|
|
16
|
+
readonly resourceLinkPdf: "template-resolver:write:resource-link-pdf";
|
|
17
|
+
readonly sendTest: "template-resolver:write:send-test";
|
|
8
18
|
};
|
|
9
19
|
export declare const TemplateResolverQueries: {
|
|
10
20
|
readonly findById: "template-resolver:query:find-by-id";
|
|
11
21
|
readonly list: "template-resolver:query:list";
|
|
22
|
+
readonly usage: "template-resolver:query:usage";
|
|
23
|
+
readonly overview: "template-resolver:query:overview";
|
|
24
|
+
readonly layoutSource: "template-resolver:query:layout-source";
|
|
25
|
+
readonly tenantOverrides: "template-resolver:query:tenant-overrides";
|
|
26
|
+
readonly diffToSystem: "template-resolver:query:diff-to-system";
|
|
27
|
+
readonly validate: "template-resolver:query:validate";
|
|
28
|
+
readonly preview: "template-resolver:query:preview";
|
|
29
|
+
readonly exportBundle: "template-resolver:query:export-bundle";
|
|
12
30
|
readonly bySlug: "template-resolver:query:by-slug";
|
|
13
31
|
readonly byTenant: "template-resolver:query:by-tenant";
|
|
14
32
|
};
|
|
@@ -21,4 +39,16 @@ export declare const TemplateResolverErrors: {
|
|
|
21
39
|
readonly systemAdminRequired: "system_admin_required";
|
|
22
40
|
readonly alreadyActive: "template_already_active";
|
|
23
41
|
readonly alreadyArchived: "template_already_archived";
|
|
42
|
+
readonly targetExists: "target_exists";
|
|
43
|
+
readonly systemResourcesNeedSystemTenant: "system_resources_need_system_tenant";
|
|
44
|
+
readonly resourceTooLarge: "template_resource_too_large";
|
|
45
|
+
readonly resourceUnsupportedImage: "template_resource_unsupported_image";
|
|
46
|
+
readonly attachmentUnsupportedType: "template_attachment_unsupported_type";
|
|
47
|
+
readonly attachmentLimit: "template_attachment_limit";
|
|
48
|
+
readonly attachmentInvalid: "template_attachment_invalid";
|
|
49
|
+
readonly attachmentPdfTemplateMissing: "template_attachment_pdf_template_missing";
|
|
50
|
+
readonly previewRenderFailed: "template_preview_render_failed";
|
|
51
|
+
readonly testMailNoChannel: "template_test_mail_no_channel";
|
|
52
|
+
readonly testMailNoAddress: "template_test_mail_no_address";
|
|
53
|
+
readonly testMailFailed: "template_test_mail_failed";
|
|
24
54
|
};
|
|
@@ -6,11 +6,29 @@ export const TemplateResolverHandlers = {
|
|
|
6
6
|
upsertTenant: "template-resolver:write:upsert-tenant",
|
|
7
7
|
publish: "template-resolver:write:publish",
|
|
8
8
|
archive: "template-resolver:write:archive",
|
|
9
|
+
publishSystem: "template-resolver:write:publish-system",
|
|
10
|
+
archiveSystem: "template-resolver:write:archive-system",
|
|
11
|
+
resetToSystem: "template-resolver:write:reset-to-system",
|
|
9
12
|
set: "template-resolver:write:set",
|
|
13
|
+
importBundle: "template-resolver:write:import-bundle",
|
|
14
|
+
importBundleSystem: "template-resolver:write:import-bundle-system",
|
|
15
|
+
copyLocale: "template-resolver:write:copy-locale",
|
|
16
|
+
resourceUpload: "template-resolver:write:resource-upload",
|
|
17
|
+
resourceRemove: "template-resolver:write:resource-remove",
|
|
18
|
+
resourceLinkPdf: "template-resolver:write:resource-link-pdf",
|
|
19
|
+
sendTest: "template-resolver:write:send-test",
|
|
10
20
|
};
|
|
11
21
|
export const TemplateResolverQueries = {
|
|
12
22
|
findById: "template-resolver:query:find-by-id",
|
|
13
23
|
list: "template-resolver:query:list",
|
|
24
|
+
usage: "template-resolver:query:usage",
|
|
25
|
+
overview: "template-resolver:query:overview",
|
|
26
|
+
layoutSource: "template-resolver:query:layout-source",
|
|
27
|
+
tenantOverrides: "template-resolver:query:tenant-overrides",
|
|
28
|
+
diffToSystem: "template-resolver:query:diff-to-system",
|
|
29
|
+
validate: "template-resolver:query:validate",
|
|
30
|
+
preview: "template-resolver:query:preview",
|
|
31
|
+
exportBundle: "template-resolver:query:export-bundle",
|
|
14
32
|
// Public pair — anonymous-reachable, kind pinned to text-block.
|
|
15
33
|
bySlug: "template-resolver:query:by-slug",
|
|
16
34
|
byTenant: "template-resolver:query:by-tenant",
|
|
@@ -32,4 +50,16 @@ export const TemplateResolverErrors = {
|
|
|
32
50
|
systemAdminRequired: "system_admin_required",
|
|
33
51
|
alreadyActive: "template_already_active",
|
|
34
52
|
alreadyArchived: "template_already_archived",
|
|
53
|
+
targetExists: "target_exists",
|
|
54
|
+
systemResourcesNeedSystemTenant: "system_resources_need_system_tenant",
|
|
55
|
+
resourceTooLarge: "template_resource_too_large",
|
|
56
|
+
resourceUnsupportedImage: "template_resource_unsupported_image",
|
|
57
|
+
attachmentUnsupportedType: "template_attachment_unsupported_type",
|
|
58
|
+
attachmentLimit: "template_attachment_limit",
|
|
59
|
+
attachmentInvalid: "template_attachment_invalid",
|
|
60
|
+
attachmentPdfTemplateMissing: "template_attachment_pdf_template_missing",
|
|
61
|
+
previewRenderFailed: "template_preview_render_failed",
|
|
62
|
+
testMailNoChannel: "template_test_mail_no_channel",
|
|
63
|
+
testMailNoAddress: "template_test_mail_no_address",
|
|
64
|
+
testMailFailed: "template_test_mail_failed",
|
|
35
65
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function readPath(row, path) {
|
|
2
|
+
let current = row;
|
|
3
|
+
for (const segment of path.split(".")) {
|
|
4
|
+
if (typeof current !== "object" || current === null || !Object.hasOwn(current, segment)) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
current = Object.getOwnPropertyDescriptor(current, segment)?.value;
|
|
8
|
+
}
|
|
9
|
+
return current;
|
|
10
|
+
}
|
|
11
|
+
// Variables whose path is missing on the row are left out, so the template
|
|
12
|
+
// renders them empty instead of printing "undefined".
|
|
13
|
+
export function mapRecordToVariables(mapping, row) {
|
|
14
|
+
const variables = {};
|
|
15
|
+
for (const [variable, path] of Object.entries(mapping.map)) {
|
|
16
|
+
const value = readPath(row, path);
|
|
17
|
+
if (value !== undefined)
|
|
18
|
+
variables[variable] = value;
|
|
19
|
+
}
|
|
20
|
+
return variables;
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare const CONTENT_TYPE_BY_FORMAT: {
|
|
2
|
+
readonly png: "image/png";
|
|
3
|
+
readonly jpeg: "image/jpeg";
|
|
4
|
+
readonly gif: "image/gif";
|
|
5
|
+
readonly webp: "image/webp";
|
|
6
|
+
};
|
|
7
|
+
type ResourceImageFormat = keyof typeof CONTENT_TYPE_BY_FORMAT;
|
|
8
|
+
export type NormalizedResourceImage = {
|
|
9
|
+
readonly data: Uint8Array;
|
|
10
|
+
readonly contentType: (typeof CONTENT_TYPE_BY_FORMAT)[ResourceImageFormat];
|
|
11
|
+
readonly width: number;
|
|
12
|
+
readonly height: number;
|
|
13
|
+
};
|
|
14
|
+
export declare function normalizeResourceImage(data: Uint8Array): Promise<NormalizedResourceImage>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { assertNotSvg, SHARP_INPUT_OPTIONS } from "../shared/index.js";
|
|
2
|
+
const MAX_RESOURCE_WIDTH = 1200;
|
|
3
|
+
const CONTENT_TYPE_BY_FORMAT = {
|
|
4
|
+
png: "image/png",
|
|
5
|
+
jpeg: "image/jpeg",
|
|
6
|
+
gif: "image/gif",
|
|
7
|
+
webp: "image/webp",
|
|
8
|
+
};
|
|
9
|
+
function isResourceImageFormat(format) {
|
|
10
|
+
return format !== undefined && Object.hasOwn(CONTENT_TYPE_BY_FORMAT, format);
|
|
11
|
+
}
|
|
12
|
+
// Re-encodes an uploaded image: the stored content type comes from the decoded format, never
|
|
13
|
+
// from the client, and re-encoding drops metadata and anything appended to the file.
|
|
14
|
+
// sharp is imported lazily because every app loads template-resolver, few of them need an image decoder.
|
|
15
|
+
export async function normalizeResourceImage(data) {
|
|
16
|
+
const { default: sharp } = await import("sharp");
|
|
17
|
+
const metadata = await sharp(data, SHARP_INPUT_OPTIONS).metadata();
|
|
18
|
+
assertNotSvg(metadata.format);
|
|
19
|
+
if (!isResourceImageFormat(metadata.format)) {
|
|
20
|
+
throw new Error(`unsupported image format "${metadata.format ?? "unknown"}"`);
|
|
21
|
+
}
|
|
22
|
+
const format = metadata.format;
|
|
23
|
+
const pipeline = sharp(data, SHARP_INPUT_OPTIONS)
|
|
24
|
+
.rotate()
|
|
25
|
+
.resize({ width: MAX_RESOURCE_WIDTH, withoutEnlargement: true })
|
|
26
|
+
.toFormat(format);
|
|
27
|
+
const { data: output, info } = await pipeline.toBuffer({ resolveWithObject: true });
|
|
28
|
+
return {
|
|
29
|
+
data: new Uint8Array(output),
|
|
30
|
+
contentType: CONTENT_TYPE_BY_FORMAT[format],
|
|
31
|
+
width: info.width,
|
|
32
|
+
height: info.height,
|
|
33
|
+
};
|
|
34
|
+
}
|