@cosmicdrift/kumiko-bundled-features 0.361.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/aggregate-id.d.ts +1 -0
- package/dist/document-ingest-foundation/aggregate-id.js +8 -0
- package/dist/document-ingest-foundation/events.js +3 -1
- package/dist/document-ingest-foundation/feature.js +4 -2
- package/dist/document-ingest-foundation/index.d.ts +1 -0
- package/dist/document-ingest-foundation/index.js +1 -0
- 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/download-by-token.query.js +5 -1
- package/dist/user-data-rights/handlers/list-download-attempts.query.d.ts +1 -1
- package/dist/user-data-rights/handlers/list-download-attempts.query.js +10 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
- package/dist/user-data-rights/index.d.ts +1 -0
- package/dist/user-data-rights/index.js +1 -0
- 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/schema/download-attempt.js +6 -12
- package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
- package/dist/user-data-rights-defaults/feature.js +6 -1
- package/dist/user-data-rights-defaults/hooks/download-attempt.userdata-hook.d.ts +3 -0
- package/dist/user-data-rights-defaults/hooks/download-attempt.userdata-hook.js +48 -0
- 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/document-ingest-foundation/changes.json +6 -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 +14 -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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { access, definePagedQueryHandler, MAX_LIST_LIMIT, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
4
|
import * as z from "zod";
|
|
4
5
|
import { decryptStoredPii } from "../../shared/index.js";
|
|
5
6
|
import { userSessionTable } from "../schema/user-session.js";
|
|
@@ -10,7 +11,7 @@ import { userSessionTable } from "../schema/user-session.js";
|
|
|
10
11
|
// non-exposed column.
|
|
11
12
|
const SORTABLE_COLUMNS = ["id", "userId", "createdAt", "expiresAt", "revokedAt"];
|
|
12
13
|
function isSortableColumn(value) {
|
|
13
|
-
return SORTABLE_COLUMNS
|
|
14
|
+
return isOneOf(SORTABLE_COLUMNS, value);
|
|
14
15
|
}
|
|
15
16
|
// Admin view of every session in the active tenant. ctx.db (TenantDb)
|
|
16
17
|
// applies tenant-scoping automatically on selects from tables with a
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
const MAX_FILE_NAME_LENGTH = 255;
|
|
3
|
+
function hasControlCharacter(value) {
|
|
4
|
+
return value.split("").some((char) => {
|
|
5
|
+
const code = char.charCodeAt(0);
|
|
6
|
+
return code <= 0x1f || code === 0x7f;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export const attachmentFileNameSchema = z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.max(MAX_FILE_NAME_LENGTH)
|
|
13
|
+
.refine((name) => !name.includes("/") && !name.includes("\\") && !hasControlCharacter(name), {
|
|
14
|
+
message: "file name must not contain path separators or control characters",
|
|
15
|
+
});
|
|
16
|
+
const PDF_FILE_NAME_MAX_LENGTH = 200;
|
|
17
|
+
const PDF_EXTENSION = ".pdf";
|
|
18
|
+
// Shared by everything that names a rendered PDF, so a mail attachment and a download of the same
|
|
19
|
+
// template get the same name. The cut leaves room for the extension: the attachment schema caps
|
|
20
|
+
// rendered-pdf names, and a name that only fits without ".pdf" would fail it.
|
|
21
|
+
export function sanitizePdfAttachmentFileName(raw, templateSlug) {
|
|
22
|
+
const cleaned = raw
|
|
23
|
+
.split("")
|
|
24
|
+
.filter((char) => !hasControlCharacter(char))
|
|
25
|
+
.join("")
|
|
26
|
+
.replace(/[/\\";]/g, "-")
|
|
27
|
+
.replaceAll("..", "-")
|
|
28
|
+
.trim()
|
|
29
|
+
.slice(0, PDF_FILE_NAME_MAX_LENGTH - PDF_EXTENSION.length)
|
|
30
|
+
.trim();
|
|
31
|
+
const lastSegment = templateSlug.split(":").pop() ?? templateSlug;
|
|
32
|
+
if (cleaned === "")
|
|
33
|
+
return `${lastSegment}${PDF_EXTENSION}`;
|
|
34
|
+
return cleaned.toLowerCase().endsWith(PDF_EXTENSION) ? cleaned : `${cleaned}${PDF_EXTENSION}`;
|
|
35
|
+
}
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { collectErasureFailure, throwIfErasureFailed } from "./assert-erased.js";
|
|
2
2
|
export { assertNoWhereRuleInOwnershipWrite } from "./assert-no-where-rule-in-ownership-write.js";
|
|
3
|
+
export { attachmentFileNameSchema, sanitizePdfAttachmentFileName, } from "./attachment-file-name.js";
|
|
3
4
|
export { type ChunkedMigrationOptions, type ChunkedMigrationResult, type ChunkedMigrationStopReason, type MigrationRowOutcome, runChunkedMigration, } from "./chunked-entity-migration.js";
|
|
4
5
|
export { classifyStoredEnvelope, type StoredEnvelopeClassification, } from "./classify-stored-envelope.js";
|
|
5
6
|
export { decryptStoredPii } from "./decrypt-stored-pii.js";
|
|
@@ -9,6 +10,7 @@ export { isWithinGracePeriod } from "./grace-period.js";
|
|
|
9
10
|
export { isIdentityV3Hash, verifyIdentityV3Hash } from "./identity-v3-hash.js";
|
|
10
11
|
export { isTenantDb } from "./is-tenant-db.js";
|
|
11
12
|
export { createLockoutCounter, type LockoutCounterState } from "./lockout-counter.js";
|
|
13
|
+
export { mailHeading, mailParagraph, mailRepeatedRows } from "./mail-template-body.js";
|
|
12
14
|
export { mapWithConcurrency } from "./map-with-concurrency.js";
|
|
13
15
|
export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
|
|
14
16
|
export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
|
|
@@ -20,6 +22,7 @@ export { type RowBoundGrantResult, redeemRowBoundGrant, signRowBoundGrant, } fro
|
|
|
20
22
|
export { runInSubTransaction } from "./run-in-sub-transaction.js";
|
|
21
23
|
export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
|
|
22
24
|
export { sessionTimezoneField } from "./session-timezone-field.js";
|
|
25
|
+
export { assertNotSvg, MAX_INPUT_PIXELS, SHARP_INPUT_OPTIONS } from "./sharp-input-limits.js";
|
|
23
26
|
export { peekTokenSubject, signToken, TokenPurpose, type VerifyResult, verifyToken, } from "./signed-token.js";
|
|
24
27
|
export { EXT_SIGNUP_HANDOVER, findSignupHandoverProvider, isSignupHandoverProvider, SIGNUP_HANDOVER_BENIGN_CLAIM_REJECTION_CODE, type SignupHandoverBinding, type SignupHandoverProvider, } from "./signup-handover.js";
|
|
25
28
|
export { createSingleUseTokenStore, hashSingleUseToken } from "./single-use-token-store.js";
|
package/dist/shared/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { collectErasureFailure, throwIfErasureFailed } from "./assert-erased.js";
|
|
2
2
|
export { assertNoWhereRuleInOwnershipWrite } from "./assert-no-where-rule-in-ownership-write.js";
|
|
3
|
+
export { attachmentFileNameSchema, sanitizePdfAttachmentFileName, } from "./attachment-file-name.js";
|
|
3
4
|
export { runChunkedMigration, } from "./chunked-entity-migration.js";
|
|
4
5
|
export { classifyStoredEnvelope, } from "./classify-stored-envelope.js";
|
|
5
6
|
export { decryptStoredPii } from "./decrypt-stored-pii.js";
|
|
@@ -9,6 +10,7 @@ export { isWithinGracePeriod } from "./grace-period.js";
|
|
|
9
10
|
export { isIdentityV3Hash, verifyIdentityV3Hash } from "./identity-v3-hash.js";
|
|
10
11
|
export { isTenantDb } from "./is-tenant-db.js";
|
|
11
12
|
export { createLockoutCounter } from "./lockout-counter.js";
|
|
13
|
+
export { mailHeading, mailParagraph, mailRepeatedRows } from "./mail-template-body.js";
|
|
12
14
|
export { mapWithConcurrency } from "./map-with-concurrency.js";
|
|
13
15
|
export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
|
|
14
16
|
export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
|
|
@@ -20,6 +22,7 @@ export { redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
|
|
|
20
22
|
export { runInSubTransaction } from "./run-in-sub-transaction.js";
|
|
21
23
|
export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
|
|
22
24
|
export { sessionTimezoneField } from "./session-timezone-field.js";
|
|
25
|
+
export { assertNotSvg, MAX_INPUT_PIXELS, SHARP_INPUT_OPTIONS } from "./sharp-input-limits.js";
|
|
23
26
|
export { peekTokenSubject, signToken, TokenPurpose, verifyToken, } from "./signed-token.js";
|
|
24
27
|
export { EXT_SIGNUP_HANDOVER, findSignupHandoverProvider, isSignupHandoverProvider, SIGNUP_HANDOVER_BENIGN_CLAIM_REJECTION_CODE, } from "./signup-handover.js";
|
|
25
28
|
export { createSingleUseTokenStore, hashSingleUseToken } from "./single-use-token-store.js";
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
// Shared so retention/compliance-profile resolvers that accept either (bulk cron
|
|
4
4
|
// vs. per-hook TenantDb) branch through one predicate instead of duplicating it.
|
|
5
5
|
export function isTenantDb(db) {
|
|
6
|
-
return typeof db.unsafeRaw === "function";
|
|
6
|
+
return "unsafeRaw" in db && typeof db.unsafeRaw === "function";
|
|
7
7
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function mailTextWithSlotsHtml(text: string): string;
|
|
2
|
+
export declare function mailParagraph(text: string): string;
|
|
3
|
+
export declare function mailHeading(text: string): string;
|
|
4
|
+
export declare function mailRepeatedRows(listName: string, field: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
const SLOT_PATTERN = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
3
|
+
// Author text uses `{name}` slots. The text is escaped first and the slots are
|
|
4
|
+
// swapped for variable nodes afterwards, so a slot can never smuggle markup.
|
|
5
|
+
export function mailTextWithSlotsHtml(text) {
|
|
6
|
+
return escapeHtml(text).replace(SLOT_PATTERN, (_, name) => `<span data-variable="${escapeHtmlAttr(name)}"></span>`);
|
|
7
|
+
}
|
|
8
|
+
export function mailParagraph(text) {
|
|
9
|
+
return `<p>${mailTextWithSlotsHtml(text)}</p>`;
|
|
10
|
+
}
|
|
11
|
+
export function mailHeading(text) {
|
|
12
|
+
return `<h2>${mailTextWithSlotsHtml(text)}</h2>`;
|
|
13
|
+
}
|
|
14
|
+
// One table row per list entry; the cell shows the entry's `field`.
|
|
15
|
+
export function mailRepeatedRows(listName, field) {
|
|
16
|
+
return `<table><tbody><tr data-each="${escapeHtmlAttr(listName)}"><td><p><span data-variable="${escapeHtmlAttr(field)}"></span></p></td></tr></tbody></table>`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Sharp-free so callers can import the limits without loading the native binding.
|
|
2
|
+
// Above this, a decoded pixel buffer runs into hundreds of MB before any
|
|
3
|
+
// resize even happens — sharp's own default (~268MP) is still large enough
|
|
4
|
+
// to be an OOM primitive for a single request.
|
|
5
|
+
export const MAX_INPUT_PIXELS = 100_000_000;
|
|
6
|
+
// sequentialRead trades random pixel access for lower peak memory on
|
|
7
|
+
// streamed formats — applied at every decode entry point below since none
|
|
8
|
+
// of them need non-sequential access to the *original* bytes (extract runs
|
|
9
|
+
// against a single already-buffered region per call).
|
|
10
|
+
export const SHARP_INPUT_OPTIONS = {
|
|
11
|
+
limitInputPixels: MAX_INPUT_PIXELS,
|
|
12
|
+
sequentialRead: true,
|
|
13
|
+
};
|
|
14
|
+
// Shared trust boundary for every image byte source this renderer decodes —
|
|
15
|
+
// the original upload (renderImage) and an overlay layer's bytes (applyOverlays)
|
|
16
|
+
// alike: a base64 string in a field declaration isn't more trustworthy than
|
|
17
|
+
// an upload just because it came from app code.
|
|
18
|
+
export function assertNotSvg(format) {
|
|
19
|
+
if (format === "svg") {
|
|
20
|
+
throw new Error("derivatives-sharp: SVG is not supported — rendering untrusted SVG through libvips is a trust boundary this renderer doesn't need to cross.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// The grant itself never crosses this boundary: verifyGrant returns only
|
|
11
11
|
// the { entityType, rowId, sourceTenantId } binding, and mintClaim returns a
|
|
12
12
|
// freshly signed short-lived token rather than handing back the original.
|
|
13
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
13
14
|
export const EXT_SIGNUP_HANDOVER = "signupHandover";
|
|
14
15
|
// Part of the contract: a provider's minted claim write may reject two
|
|
15
16
|
// different ways. Benign — this code, nothing was written (forged,
|
|
@@ -20,8 +21,7 @@ export const EXT_SIGNUP_HANDOVER = "signupHandover";
|
|
|
20
21
|
// together with it, never treat that as a skippable no-op.
|
|
21
22
|
export const SIGNUP_HANDOVER_BENIGN_CLAIM_REJECTION_CODE = "invalid_or_expired_grant";
|
|
22
23
|
export function isSignupHandoverProvider(options) {
|
|
23
|
-
return (
|
|
24
|
-
options !== null &&
|
|
24
|
+
return (isPlainObject(options) &&
|
|
25
25
|
typeof options["verifyGrant"] === "function" &&
|
|
26
26
|
typeof options["mintClaim"] === "function");
|
|
27
27
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
3
|
import { denyUnlessJoinRowParentVisible } from "../../shared/index.js";
|
|
3
4
|
import { tagAssignmentAggregateId } from "../aggregate-id.js";
|
|
4
5
|
import { DEFAULT_TAG_ACCESS } from "../constants.js";
|
|
@@ -49,7 +50,8 @@ export function createAssignTagHandler(access = DEFAULT_TAG_ACCESS, assignmentEx
|
|
|
49
50
|
// above and this restore() call, so restore() correctly refuses with
|
|
50
51
|
// "not_deleted" — but the desired end state is already true. Converge
|
|
51
52
|
// like the create() race below instead of surfacing a spurious error.
|
|
52
|
-
if (restored.error.details
|
|
53
|
+
if (isPlainObject(restored.error.details) &&
|
|
54
|
+
restored.error.details["reason"] === "not_deleted") {
|
|
53
55
|
return { isSuccess: true, data: { id } };
|
|
54
56
|
}
|
|
55
57
|
if (restored.error.code !== "not_found")
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
4
|
+
import type { FileAttachmentRef } from "../renderer-foundation/index.js";
|
|
4
5
|
import type { ContentFormat, TemplateKind } from "./constants.js";
|
|
6
|
+
import { type TemplatePdfAttachmentLink } from "./linked-resources.js";
|
|
7
|
+
import { type TemplateMeta } from "./template-meta.js";
|
|
5
8
|
export type TemplateResource = {
|
|
6
9
|
readonly id: string;
|
|
7
10
|
readonly version: number;
|
|
@@ -14,7 +17,15 @@ export type TemplateResource = {
|
|
|
14
17
|
readonly content: string;
|
|
15
18
|
readonly contentFormat: ContentFormat;
|
|
16
19
|
readonly variableSchema: Record<string, unknown>;
|
|
20
|
+
/** Embeddable images: resource key -> fileRef id. */
|
|
17
21
|
readonly linkedResources: Record<string, string>;
|
|
22
|
+
/** Files attached to every mail rendered from this template, ordered by key. */
|
|
23
|
+
readonly fileAttachments?: readonly FileAttachmentRef[];
|
|
24
|
+
/** PDFs rendered from document templates and attached at send time, ordered by key. */
|
|
25
|
+
readonly pdfAttachmentLinks?: readonly TemplatePdfAttachmentLink[];
|
|
26
|
+
/** Parsed per kind; null when absent or when the stored JSON no longer
|
|
27
|
+
* matches the kind's schema (the renderer then fails with a clear error). */
|
|
28
|
+
readonly meta: TemplateMeta | null;
|
|
18
29
|
readonly scope: "system" | "tenant";
|
|
19
30
|
readonly parentTemplateId: string | null;
|
|
20
31
|
readonly status: "draft" | "active" | "archived";
|
|
@@ -26,6 +37,13 @@ export type ResolveRequest = {
|
|
|
26
37
|
readonly kind: TemplateKind;
|
|
27
38
|
readonly locale: string;
|
|
28
39
|
};
|
|
40
|
+
export type LayoutKind = "mail-layout" | "pdf-layout";
|
|
41
|
+
export type ResolveLayoutRequest = {
|
|
42
|
+
readonly tenantId: TenantId;
|
|
43
|
+
readonly slug: string;
|
|
44
|
+
readonly kind: LayoutKind;
|
|
45
|
+
readonly locale: string;
|
|
46
|
+
};
|
|
29
47
|
export type TemplateResolverApi = {
|
|
30
48
|
/**
|
|
31
49
|
* Findet ein konkretes Template by (tenantId, slug, kind, locale).
|
|
@@ -60,8 +78,18 @@ export type TemplateResolverApi = {
|
|
|
60
78
|
* User cross-tenant Templates abfragen (Tenant-Isolation gebrochen).
|
|
61
79
|
*/
|
|
62
80
|
readonly resolveTemplate: (args: ResolveRequest) => Promise<TemplateResource>;
|
|
81
|
+
/**
|
|
82
|
+
* Layouts are locale-neutral in the product (the layout editor saves under the locale of the
|
|
83
|
+
* row it loaded), so the locale chain is wider than resolveTemplate: tenant (requested,
|
|
84
|
+
* FALLBACK_LOCALE, any active locale), then system (same). Only active rows count; among
|
|
85
|
+
* "any" the highest version wins, then the alphabetically first locale, so the pick is stable.
|
|
86
|
+
* Throws TemplateNotFoundError when neither scope has an active row.
|
|
87
|
+
*
|
|
88
|
+
* **Caller invariant:** `tenantId` must come from the server, as for `resolveTemplate`.
|
|
89
|
+
*/
|
|
90
|
+
readonly resolveLayout: (args: ResolveLayoutRequest) => Promise<TemplateResource>;
|
|
63
91
|
};
|
|
64
|
-
export declare function createTemplateResolverApi(db: DbRunner): TemplateResolverApi;
|
|
92
|
+
export declare function createTemplateResolverApi(db: DbRunner | TenantDb): TemplateResolverApi;
|
|
65
93
|
export declare class TemplateNotFoundError extends Error {
|
|
66
94
|
readonly query: {
|
|
67
95
|
slug: string;
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
5
5
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
6
6
|
import { FALLBACK_LOCALE, SYSTEM_TENANT_ID } from "./constants.js";
|
|
7
|
+
import { fileAttachmentsOf, imageLinksOf, parseLinkedResourceEntries, pdfAttachmentLinksOf, } from "./linked-resources.js";
|
|
7
8
|
import { templateResourcesTable } from "./table.js";
|
|
9
|
+
import { parseTemplateMeta } from "./template-meta.js";
|
|
8
10
|
export function createTemplateResolverApi(db) {
|
|
9
11
|
return {
|
|
10
12
|
findExact: async ({ tenantId, slug, kind, locale, scope }) => {
|
|
@@ -31,8 +33,30 @@ export function createTemplateResolverApi(db) {
|
|
|
31
33
|
}
|
|
32
34
|
throw new TemplateNotFoundError({ slug, kind, locale });
|
|
33
35
|
},
|
|
36
|
+
resolveLayout: async ({ tenantId, slug, kind, locale }) => {
|
|
37
|
+
for (const tid of [tenantId, SYSTEM_TENANT_ID]) {
|
|
38
|
+
const rows = await selectMany(db, templateResourcesTable, {
|
|
39
|
+
tenantId: tid,
|
|
40
|
+
slug,
|
|
41
|
+
kind,
|
|
42
|
+
});
|
|
43
|
+
const row = pickActiveLayoutRow(rows, locale);
|
|
44
|
+
if (row !== null)
|
|
45
|
+
return toPublic(row);
|
|
46
|
+
}
|
|
47
|
+
throw new TemplateNotFoundError({ slug, kind, locale });
|
|
48
|
+
},
|
|
34
49
|
};
|
|
35
50
|
}
|
|
51
|
+
function pickActiveLayoutRow(rows, locale) {
|
|
52
|
+
const active = rows
|
|
53
|
+
.filter((row) => row.status === "active")
|
|
54
|
+
.sort((a, b) => b.version - a.version || a.locale.localeCompare(b.locale));
|
|
55
|
+
return (active.find((row) => row.locale === locale) ??
|
|
56
|
+
active.find((row) => row.locale === FALLBACK_LOCALE) ??
|
|
57
|
+
active[0] ??
|
|
58
|
+
null);
|
|
59
|
+
}
|
|
36
60
|
async function fetchTemplate(db, tenantId, slug, kind, locale) {
|
|
37
61
|
const rows = await selectMany(db, templateResourcesTable, { tenantId, slug, kind, locale }, { limit: 1 });
|
|
38
62
|
// @cast-boundary db-row — db.select returnt unbenanntes unknown[],
|
|
@@ -43,7 +67,6 @@ function toPublic(row) {
|
|
|
43
67
|
// @cast-boundary db-row — Drizzle-Schema typisiert kind/contentFormat/
|
|
44
68
|
// scope/status als generic text. CHECK-Constraints in der DB schränken
|
|
45
69
|
// sie auf die Union-Types ein; Cast assertet das Schema-Wissen.
|
|
46
|
-
// linkedResources ist ein text-column mit JSON-payload (string→string map).
|
|
47
70
|
const kind = row.kind;
|
|
48
71
|
// @cast-boundary db-row — siehe kind.
|
|
49
72
|
const contentFormat = row.contentFormat;
|
|
@@ -51,9 +74,7 @@ function toPublic(row) {
|
|
|
51
74
|
const scope = row.scope;
|
|
52
75
|
// @cast-boundary db-row — siehe kind.
|
|
53
76
|
const status = row.status;
|
|
54
|
-
|
|
55
|
-
// linkedResources-Spalte enthält per Schema {key: signedUrl}-Map.
|
|
56
|
-
const linkedResources = parseJson(row.linkedResources);
|
|
77
|
+
const linkedEntries = parseLinkedResourceEntries(row.linkedResources);
|
|
57
78
|
return {
|
|
58
79
|
id: String(row.id),
|
|
59
80
|
version: row.version,
|
|
@@ -66,13 +87,27 @@ function toPublic(row) {
|
|
|
66
87
|
content: row.content ?? "",
|
|
67
88
|
contentFormat,
|
|
68
89
|
variableSchema: parseJson(row.variableSchema),
|
|
69
|
-
linkedResources,
|
|
90
|
+
linkedResources: imageLinksOf(linkedEntries),
|
|
91
|
+
fileAttachments: fileAttachmentsOf(linkedEntries),
|
|
92
|
+
pdfAttachmentLinks: pdfAttachmentLinksOf(linkedEntries),
|
|
93
|
+
meta: parseStoredMeta(kind, row.meta),
|
|
70
94
|
scope,
|
|
71
95
|
parentTemplateId: row.parentTemplateId,
|
|
72
96
|
status,
|
|
73
97
|
modifiedAt: row.modifiedAt,
|
|
74
98
|
};
|
|
75
99
|
}
|
|
100
|
+
function parseStoredMeta(kind, raw) {
|
|
101
|
+
if (!raw)
|
|
102
|
+
return null;
|
|
103
|
+
try {
|
|
104
|
+
const result = parseTemplateMeta(kind, JSON.parse(raw));
|
|
105
|
+
return result.ok ? result.meta : null;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
76
111
|
function parseJson(raw) {
|
|
77
112
|
if (!raw)
|
|
78
113
|
return {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TemplateVariables } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
export type CompileDocumentBodyOptions = {
|
|
3
|
+
readonly resourceSrc?: (key: string) => string | undefined;
|
|
4
|
+
readonly alignmentAsInlineStyle?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function collectDocumentResourceKeys(html: string): readonly string[];
|
|
7
|
+
export declare function toResourceDataUri(contentType: string, bytes: Uint8Array): string;
|
|
8
|
+
export declare function compileDocumentBodyToHandlebars(html: string, variables: TemplateVariables, options?: CompileDocumentBodyOptions): string;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import { withParsedDocument } from "./document-sanitizer.js";
|
|
3
|
+
import { DOCUMENT_TEXT_ALIGNMENTS, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
4
|
+
// Private-use characters mark where a Handlebars expression goes. Author text
|
|
5
|
+
// is scrubbed of them first and every `{`/`}` is entity-encoded before the
|
|
6
|
+
// markers are swapped for real expressions, so the only Handlebars syntax in
|
|
7
|
+
// the output is the syntax this module wrote.
|
|
8
|
+
const EXPRESSION_OPEN = "\uE000";
|
|
9
|
+
const EXPRESSION_CLOSE = "\uE001";
|
|
10
|
+
const MARKER_PATTERN = /\uE000(\d+)\uE001/g;
|
|
11
|
+
function marker(state, expression) {
|
|
12
|
+
state.expressions.push(expression);
|
|
13
|
+
return `${EXPRESSION_OPEN}${state.expressions.length - 1}${EXPRESSION_CLOSE}`;
|
|
14
|
+
}
|
|
15
|
+
function ownVariable(variables, name) {
|
|
16
|
+
return Object.hasOwn(variables, name) ? variables[name] : undefined;
|
|
17
|
+
}
|
|
18
|
+
function itemFieldsOf(variables, listName) {
|
|
19
|
+
const def = ownVariable(variables, listName);
|
|
20
|
+
return def?.type === "list" ? def.item : null;
|
|
21
|
+
}
|
|
22
|
+
function rowScopeOf(element, state) {
|
|
23
|
+
const listName = element.closest("tr[data-each]")?.getAttribute("data-each") ?? null;
|
|
24
|
+
if (listName === null)
|
|
25
|
+
return null;
|
|
26
|
+
const itemFields = itemFieldsOf(state.variables, listName);
|
|
27
|
+
return itemFields === null ? null : { listName, itemFields };
|
|
28
|
+
}
|
|
29
|
+
function resolveReference(name, scope, variables) {
|
|
30
|
+
if (scope !== null && ownVariable(scope.itemFields, name) !== undefined) {
|
|
31
|
+
return { path: name, def: ownVariable(scope.itemFields, name) };
|
|
32
|
+
}
|
|
33
|
+
return { path: scope === null ? name : `@root.${name}`, def: ownVariable(variables, name) };
|
|
34
|
+
}
|
|
35
|
+
function valueExpression(path, def) {
|
|
36
|
+
if (def?.type === "money")
|
|
37
|
+
return `{{money ${path}}}`;
|
|
38
|
+
if (def?.type === "date")
|
|
39
|
+
return `{{date ${path}}}`;
|
|
40
|
+
return `{{${path}}}`;
|
|
41
|
+
}
|
|
42
|
+
function rewriteVariable(element, state) {
|
|
43
|
+
const name = element.getAttribute("data-variable") ?? "";
|
|
44
|
+
const replacement = element.ownerDocument.createTextNode(isDocumentReferenceName(name)
|
|
45
|
+
? marker(state, valueExpression(...pathAndDef(name, element, state)))
|
|
46
|
+
: "");
|
|
47
|
+
element.replaceWith(replacement);
|
|
48
|
+
}
|
|
49
|
+
function pathAndDef(name, element, state) {
|
|
50
|
+
const { path, def } = resolveReference(name, rowScopeOf(element, state), state.variables);
|
|
51
|
+
return [path, def];
|
|
52
|
+
}
|
|
53
|
+
function rewriteLink(element, state) {
|
|
54
|
+
const href = element.getAttribute("href") ?? "";
|
|
55
|
+
if (isDocumentPlaceholderHref(href)) {
|
|
56
|
+
const name = href.slice(2, -2);
|
|
57
|
+
if (isDocumentReferenceName(name)) {
|
|
58
|
+
const [path] = pathAndDef(name, element, state);
|
|
59
|
+
element.setAttribute("href", marker(state, `{{link ${path}}}`));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
element.setAttribute("href", "#");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (!isAllowedDocumentLinkHref(href)) {
|
|
66
|
+
element.setAttribute("href", "#");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function rewriteRepeatRow(row, state) {
|
|
70
|
+
const listName = row.getAttribute("data-each") ?? "";
|
|
71
|
+
const document = row.ownerDocument;
|
|
72
|
+
if (itemFieldsOf(state.variables, listName) === null || !isDocumentReferenceName(listName)) {
|
|
73
|
+
row.remove();
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
row.before(document.createTextNode(marker(state, `{{#each ${listName}}}`)));
|
|
77
|
+
row.after(document.createTextNode(marker(state, "{{/each}}")));
|
|
78
|
+
row.removeAttribute("data-each");
|
|
79
|
+
row.removeAttribute("data-repeat-header");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function stripActiveContent(element) {
|
|
83
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
84
|
+
if (/^on/i.test(attribute.name))
|
|
85
|
+
element.removeAttribute(attribute.name);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function neutralizeBraces(html) {
|
|
89
|
+
return html.replace(/[{}]/g, (brace) => (brace === "{" ? "{" : "}"));
|
|
90
|
+
}
|
|
91
|
+
const RESOURCE_IMAGE_CONTENT_TYPES = new Set([
|
|
92
|
+
"image/png",
|
|
93
|
+
"image/jpeg",
|
|
94
|
+
"image/gif",
|
|
95
|
+
"image/webp",
|
|
96
|
+
]);
|
|
97
|
+
// Unique valid resource keys in document order.
|
|
98
|
+
export function collectDocumentResourceKeys(html) {
|
|
99
|
+
return withParsedDocument(html, (root) => {
|
|
100
|
+
const keys = new Set();
|
|
101
|
+
for (const element of Array.from(root.querySelectorAll("img[data-resource]"))) {
|
|
102
|
+
const key = element.getAttribute("data-resource") ?? "";
|
|
103
|
+
if (isDocumentResourceKey(key))
|
|
104
|
+
keys.add(key);
|
|
105
|
+
}
|
|
106
|
+
return [...keys];
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export function toResourceDataUri(contentType, bytes) {
|
|
110
|
+
if (!RESOURCE_IMAGE_CONTENT_TYPES.has(contentType)) {
|
|
111
|
+
throw new Error(`resource content type "${contentType}" cannot be embedded as an image`);
|
|
112
|
+
}
|
|
113
|
+
return `data:${contentType};base64,${Buffer.from(bytes).toString("base64")}`;
|
|
114
|
+
}
|
|
115
|
+
function rewriteAlignment(element) {
|
|
116
|
+
const align = element.getAttribute("data-align");
|
|
117
|
+
element.removeAttribute("data-align");
|
|
118
|
+
if (isOneOf(DOCUMENT_TEXT_ALIGNMENTS, align))
|
|
119
|
+
element.setAttribute("style", `text-align:${align}`);
|
|
120
|
+
}
|
|
121
|
+
function rewriteResourceImage(element, resourceSrc) {
|
|
122
|
+
const key = element.getAttribute("data-resource") ?? "";
|
|
123
|
+
const src = isDocumentResourceKey(key) ? resourceSrc?.(key) : undefined;
|
|
124
|
+
if (src === undefined) {
|
|
125
|
+
element.remove();
|
|
126
|
+
// skip: an unresolved resource key drops the image
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
element.setAttribute("src", src);
|
|
130
|
+
element.removeAttribute("data-resource");
|
|
131
|
+
}
|
|
132
|
+
// Turns a sanitized document body into Handlebars source. Rows and links are
|
|
133
|
+
// rewritten in document order; list rows last so their placeholders still see
|
|
134
|
+
// the `tr[data-each]` ancestor.
|
|
135
|
+
export function compileDocumentBodyToHandlebars(html, variables, options = {}) {
|
|
136
|
+
const state = { variables, expressions: [] };
|
|
137
|
+
const scrubbed = html.replaceAll(EXPRESSION_OPEN, "").replaceAll(EXPRESSION_CLOSE, "");
|
|
138
|
+
const compiled = withParsedDocument(scrubbed, (root) => {
|
|
139
|
+
for (const element of Array.from(root.querySelectorAll("script")))
|
|
140
|
+
element.remove();
|
|
141
|
+
for (const element of Array.from(root.querySelectorAll("img[data-resource]"))) {
|
|
142
|
+
rewriteResourceImage(element, options.resourceSrc);
|
|
143
|
+
}
|
|
144
|
+
for (const element of Array.from(root.querySelectorAll("*")))
|
|
145
|
+
stripActiveContent(element);
|
|
146
|
+
if (options.alignmentAsInlineStyle === true) {
|
|
147
|
+
for (const element of Array.from(root.querySelectorAll("[data-align]"))) {
|
|
148
|
+
rewriteAlignment(element);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const element of Array.from(root.querySelectorAll("span[data-variable]"))) {
|
|
152
|
+
rewriteVariable(element, state);
|
|
153
|
+
}
|
|
154
|
+
for (const element of Array.from(root.querySelectorAll("a[href]"))) {
|
|
155
|
+
rewriteLink(element, state);
|
|
156
|
+
}
|
|
157
|
+
for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
|
|
158
|
+
rewriteRepeatRow(element, state);
|
|
159
|
+
}
|
|
160
|
+
return root.innerHTML;
|
|
161
|
+
});
|
|
162
|
+
return neutralizeBraces(compiled).replace(MARKER_PATTERN, (_match, index) => state.expressions[Number(index)] ?? "");
|
|
163
|
+
}
|
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
export declare const RENDER_KINDS: readonly ["
|
|
1
|
+
export declare const RENDER_KINDS: readonly ["mail", "document-pdf", "image-snapshot"];
|
|
2
2
|
export type RenderKind = (typeof RENDER_KINDS)[number];
|
|
3
|
-
export declare const
|
|
3
|
+
export declare const DOCUMENT_HTML_KINDS: readonly ["mail", "document-pdf"];
|
|
4
|
+
export type DocumentHtmlKind = (typeof DOCUMENT_HTML_KINDS)[number];
|
|
5
|
+
export declare const NON_RENDER_KINDS: readonly ["notification", "text-block", "snippet", "ai-prompt", "mail-layout", "pdf-layout"];
|
|
4
6
|
export type NonRenderKind = (typeof NON_RENDER_KINDS)[number];
|
|
5
|
-
export declare const TEMPLATE_KINDS: readonly ["
|
|
7
|
+
export declare const TEMPLATE_KINDS: readonly ["mail", "document-pdf", "image-snapshot", "notification", "text-block", "snippet", "ai-prompt", "mail-layout", "pdf-layout"];
|
|
6
8
|
export type TemplateKind = (typeof TEMPLATE_KINDS)[number];
|
|
7
|
-
export declare const UPSERT_KINDS: readonly ["
|
|
9
|
+
export declare const UPSERT_KINDS: readonly ["mail", "document-pdf", "image-snapshot", "ai-prompt", "mail-layout", "pdf-layout"];
|
|
8
10
|
export type UpsertKind = (typeof UPSERT_KINDS)[number];
|
|
9
11
|
export declare const TEXT_BLOCK_KIND: "text-block";
|
|
12
|
+
export declare const COLLECTION_REFUSED_KINDS: readonly string[];
|
|
10
13
|
export declare const CONTENT_FORMATS: readonly ["markdown", "mjml", "html", "plain"];
|
|
11
14
|
export type ContentFormat = (typeof CONTENT_FORMATS)[number];
|
|
12
15
|
export declare const TEMPLATE_SCOPES: readonly ["system", "tenant"];
|
|
13
16
|
export type TemplateScope = (typeof TEMPLATE_SCOPES)[number];
|
|
14
17
|
export declare const TEMPLATE_STATUSES: readonly ["draft", "active", "archived"];
|
|
15
18
|
export type TemplateStatus = (typeof TEMPLATE_STATUSES)[number];
|
|
19
|
+
export declare const PDF_PAGE_FORMATS: readonly ["A4", "Letter"];
|
|
20
|
+
export type PdfPageFormat = (typeof PDF_PAGE_FORMATS)[number];
|
|
21
|
+
export declare const DEFAULT_MAIL_LAYOUT_SLUG = "default";
|
|
22
|
+
export declare const MAIL_PREVIEW_MAX_BYTES: number;
|
|
23
|
+
export declare const TEST_MAIL_NOTIFICATION_TYPE = "template-resolver:test-mail";
|
|
24
|
+
export declare const TEST_MAIL_SUBJECT_PREFIX = "[TEST] ";
|
|
16
25
|
export { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
17
26
|
export declare const FALLBACK_LOCALE = "de";
|
|
@@ -6,30 +6,50 @@
|
|
|
6
6
|
// Plugin-Renderer in `renderer-foundation` matchen auf kind; der
|
|
7
7
|
// Resolver hier ist kind-agnostisch — er lädt nur, das Content-Format
|
|
8
8
|
// (markdown/mjml/html) entscheidet wer's rendert.
|
|
9
|
-
export const RENDER_KINDS = [
|
|
9
|
+
export const RENDER_KINDS = ["mail", "document-pdf", "image-snapshot"];
|
|
10
|
+
// Kinds whose html body is a rich document: the server rebuilds it through the
|
|
11
|
+
// document schema on every write (see document-sanitizer.ts).
|
|
12
|
+
export const DOCUMENT_HTML_KINDS = ["mail", "document-pdf"];
|
|
13
|
+
// Kinds without a renderer plugin: `notification` is the retired sections-JSON
|
|
14
|
+
// notification kind, kept so rows stored before mails moved to `mail` stay
|
|
15
|
+
// valid on replay; nothing writes or renders it any more. `text-block` is the Markdown text store
|
|
16
|
+
// (ex text-content feature), `snippet` is the content of app collections such
|
|
17
|
+
// as reply snippets and signatures (own kind so they stay out of the text-block
|
|
18
|
+
// content tree), `ai-prompt` holds LLM prompts, `mail-layout` and
|
|
19
|
+
// `pdf-layout` hold the frame a mail/pdf body is rendered into. All are read,
|
|
20
|
+
// not rendered on their own — keeping them out of RENDER_KINDS is what stops
|
|
21
|
+
// DEFAULT_PLUGIN_BY_KIND from demanding a plugin for them.
|
|
22
|
+
export const NON_RENDER_KINDS = [
|
|
10
23
|
"notification",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
24
|
+
"text-block",
|
|
25
|
+
"snippet",
|
|
26
|
+
"ai-prompt",
|
|
27
|
+
"mail-layout",
|
|
28
|
+
"pdf-layout",
|
|
14
29
|
];
|
|
15
|
-
// Kinds without a renderer plugin: `text-block` is the Markdown text store
|
|
16
|
-
// (ex text-content feature), `ai-prompt` holds LLM prompts. Both are read,
|
|
17
|
-
// not rendered — keeping them out of RENDER_KINDS is what stops
|
|
18
|
-
// DEFAULT_PLUGIN_BY_KIND from demanding a plugin for them.
|
|
19
|
-
export const NON_RENDER_KINDS = ["text-block", "ai-prompt"];
|
|
20
30
|
// Full kind domain of the entity; RENDER_KINDS is the renderer-foundation subset.
|
|
21
31
|
export const TEMPLATE_KINDS = [...RENDER_KINDS, ...NON_RENDER_KINDS];
|
|
22
32
|
// Kinds written through upsertSystem/upsertTenant. `text-block` is absent on
|
|
23
33
|
// purpose: it goes through set.write (title + folder tree); two write paths
|
|
24
34
|
// onto the same row would drift apart.
|
|
25
|
-
export const UPSERT_KINDS = [...RENDER_KINDS, "ai-prompt"];
|
|
35
|
+
export const UPSERT_KINDS = [...RENDER_KINDS, "ai-prompt", "mail-layout", "pdf-layout"];
|
|
26
36
|
// The only kind served by the anonymous-capable by-slug/by-tenant queries and
|
|
27
37
|
// by set.write — mail templates and AI prompts are operational internals and
|
|
28
38
|
// must not leak through the anonymous path.
|
|
29
39
|
export const TEXT_BLOCK_KIND = "text-block";
|
|
40
|
+
// Kinds a content collection must not write: a tenant row of these kinds would
|
|
41
|
+
// shadow the system notification template or layout without the meta and
|
|
42
|
+
// placeholder gates that upsertTenant applies.
|
|
43
|
+
export const COLLECTION_REFUSED_KINDS = ["mail", "mail-layout", "pdf-layout"];
|
|
30
44
|
export const CONTENT_FORMATS = ["markdown", "mjml", "html", "plain"];
|
|
31
45
|
export const TEMPLATE_SCOPES = ["system", "tenant"];
|
|
32
46
|
export const TEMPLATE_STATUSES = ["draft", "active", "archived"];
|
|
47
|
+
export const PDF_PAGE_FORMATS = ["A4", "Letter"];
|
|
48
|
+
export const DEFAULT_MAIL_LAYOUT_SLUG = "default";
|
|
49
|
+
// The mail transport enforces no size limit of its own; 10 MiB is a common provider ceiling.
|
|
50
|
+
export const MAIL_PREVIEW_MAX_BYTES = 10 * 1024 * 1024;
|
|
51
|
+
export const TEST_MAIL_NOTIFICATION_TYPE = "template-resolver:test-mail";
|
|
52
|
+
export const TEST_MAIL_SUBJECT_PREFIX = "[TEST] ";
|
|
33
53
|
// System-Templates leben unter der canonical SYSTEM_TENANT_ID-Sentinel-UUID.
|
|
34
54
|
// Re-Export aus framework — single source of truth, vermeidet Drift.
|
|
35
55
|
export { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|