@cosmicdrift/kumiko-samples 0.362.0 → 0.364.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/package.json +1 -1
- package/packages/bundled-features/package.json +23 -6
- package/packages/bundled-features/src/admin-shell/changes.json +6 -0
- package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
- package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
- package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
- package/packages/bundled-features/src/audit/changes.json +6 -0
- package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
- package/packages/bundled-features/src/auth-email-password/changes.json +51 -0
- package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
- package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
- package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
- package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
- package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +5 -13
- package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
- package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
- package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
- package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
- package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
- package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
- package/packages/bundled-features/src/billing-foundation/changes.json +43 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +90 -44
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +118 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +39 -33
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +36 -20
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
- package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
- package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +20 -11
- package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
- package/packages/bundled-features/src/cap-counter/changes.json +6 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
- package/packages/bundled-features/src/cap-counter/index.ts +1 -0
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
- package/packages/bundled-features/src/channel-email/changes.json +19 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +135 -29
- package/packages/bundled-features/src/channel-email/feature.ts +6 -3
- package/packages/bundled-features/src/channel-email/index.ts +7 -1
- package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
- package/packages/bundled-features/src/channel-email/types.ts +23 -0
- package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
- package/packages/bundled-features/src/config/changes.json +6 -0
- package/packages/bundled-features/src/config/handlers/set.write.ts +3 -7
- package/packages/bundled-features/src/config/resolver.ts +15 -12
- package/packages/bundled-features/src/config/write-helpers.ts +23 -0
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
- package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
- package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
- package/packages/bundled-features/src/data-retention/changes.json +6 -0
- package/packages/bundled-features/src/data-retention/index.ts +1 -0
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
- package/packages/bundled-features/src/delivery/attachment-ref.ts +105 -0
- package/packages/bundled-features/src/delivery/changes.json +66 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +152 -4
- package/packages/bundled-features/src/delivery/constants.ts +2 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
- package/packages/bundled-features/src/delivery/feature.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +19 -2
- package/packages/bundled-features/src/delivery/jobs.ts +97 -17
- package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
- package/packages/bundled-features/src/delivery/testing.ts +9 -1
- package/packages/bundled-features/src/delivery/types.ts +34 -18
- package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
- package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
- package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
- package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
- package/packages/bundled-features/src/file-foundation/changes.json +6 -0
- package/packages/bundled-features/src/file-foundation/index.ts +7 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +89 -0
- package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
- package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
- package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
- package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +42 -25
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
- package/packages/bundled-features/src/jobs/changes.json +19 -0
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
- package/packages/bundled-features/src/jobs/feature.ts +7 -4
- package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
- package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
- package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
- package/packages/bundled-features/src/ledger/changes.json +7 -0
- package/packages/bundled-features/src/ledger/entity.ts +3 -1
- package/packages/bundled-features/src/ledger/recurring.ts +1 -3
- package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
- package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
- package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
- package/packages/bundled-features/src/page-render/branding.ts +3 -0
- package/packages/bundled-features/src/page-render/markdown.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
- package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
- package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
- package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
- package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
- package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
- package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
- package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
- package/packages/bundled-features/src/renderer-foundation/types.ts +134 -35
- package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
- package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -3
- package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
- package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/sessions/feature.ts +2 -4
- package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
- package/packages/bundled-features/src/shared/changes.json +6 -0
- package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
- package/packages/bundled-features/src/shared/index.ts +6 -0
- package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +42 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
- package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
- package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
- package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
- package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
- package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
- package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
- package/packages/bundled-features/src/template-resolver/README.md +66 -8
- package/packages/bundled-features/src/template-resolver/api.ts +85 -9
- package/packages/bundled-features/src/template-resolver/changes.json +186 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +278 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +49 -11
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +345 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +59 -6
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +160 -0
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +33 -0
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +138 -0
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -6
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +151 -0
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +19 -18
- package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +323 -0
- package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
- package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +135 -0
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
- package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +196 -0
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +233 -0
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +137 -0
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +91 -17
- package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +16 -77
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +31 -0
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
- package/packages/bundled-features/src/template-resolver/index.ts +111 -0
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
- package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
- package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
- package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
- package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
- package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +34 -0
- package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +171 -20
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
- package/packages/bundled-features/src/template-resolver/table.ts +27 -1
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
- package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +192 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +676 -0
- package/packages/bundled-features/src/template-resolver/template-resources.ts +114 -0
- package/packages/bundled-features/src/template-resolver/template-usage.ts +68 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +230 -0
- package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +20 -13
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +215 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +37 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1065 -0
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +72 -0
- package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +121 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +176 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
- package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
- package/packages/bundled-features/src/tenant/seeding.ts +41 -7
- package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
- package/packages/bundled-features/src/user/changes.json +6 -0
- package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
- package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
- package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
- package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
- package/packages/bundled-features/src/user/seeding.ts +19 -5
- package/packages/bundled-features/src/user-data-rights/changes.json +13 -0
- package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
- package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
- package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
- package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
- package/packages/bundled-features/src/user-profile/changes.json +1 -1
- package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
- package/packages/bundled-features/src/waitlist/changes.json +7 -0
- package/packages/bundled-features/src/waitlist/feature.ts +17 -0
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
- package/packages/bundled-features/src/waitlist/mail.ts +69 -18
- package/packages/bundled-features/src/waitlist/options.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +3 -3
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
- package/samples/apps/ui-walkthrough/package.json +11 -0
- package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
- package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
- package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +239 -0
- package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +570 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +72 -0
- package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
- package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +41 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +118 -21
- package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +2 -0
- package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
- package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
- package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
- package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
- package/samples/recipes/delivery-notifications/README.md +26 -25
- package/samples/recipes/delivery-notifications/package.json +2 -1
- package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
- package/samples/recipes/seed-migration/README.md +1 -1
- package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
- package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
- package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
- package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
- package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { linkTemplatePdfAttachment } from "../store-template-resource.js";
|
|
4
|
+
import { resourceKeySchema, resourceTargetTenantId } from "./resource-shared.js";
|
|
5
|
+
|
|
6
|
+
export const resourceLinkPdfWrite = defineWriteHandler({
|
|
7
|
+
name: "resource-link-pdf",
|
|
8
|
+
schema: z.object({
|
|
9
|
+
templateId: z.string().min(1),
|
|
10
|
+
key: resourceKeySchema,
|
|
11
|
+
/** Slug of a document-pdf template of the tenant or the system. */
|
|
12
|
+
templateSlug: z.string().min(1),
|
|
13
|
+
/** Name pattern of the attached PDF; may hold {{placeholders}}, must end with .pdf. */
|
|
14
|
+
fileName: z.string().min(1),
|
|
15
|
+
/** Cross-tenant target — SystemAdmin only. */
|
|
16
|
+
tenantIdOverride: z.string().min(1).optional(),
|
|
17
|
+
}),
|
|
18
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
19
|
+
description:
|
|
20
|
+
"Links a PDF rendered from a document-pdf template into a mail template under the key. Every mail sent from the template then carries that PDF, rendered at send time with the mail's own variables and locale; nothing is uploaded or stored. fileName is a pattern that may use the mail's {{placeholders}} and must end with .pdf. The document template must exist for the tenant or the system. Counts toward the 10 attachments per mail template together with uploaded files.",
|
|
21
|
+
handler: async (event, ctx) => {
|
|
22
|
+
const { templateId, key, templateSlug, fileName } = event.payload;
|
|
23
|
+
const tenantId = resourceTargetTenantId(event.payload.tenantIdOverride, event.user.tenantId);
|
|
24
|
+
return linkTemplatePdfAttachment({
|
|
25
|
+
ctx,
|
|
26
|
+
user: event.user,
|
|
27
|
+
tenantId,
|
|
28
|
+
templateId,
|
|
29
|
+
key,
|
|
30
|
+
templateSlug,
|
|
31
|
+
fileName,
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import * as z from "zod";
|
|
5
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
6
|
+
import { parseLinkedResourceEntries } from "../linked-resources.js";
|
|
7
|
+
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
8
|
+
import { resourceKeySchema, resourceTargetTenantId } from "./resource-shared.js";
|
|
9
|
+
import { executor } from "./shared.js";
|
|
10
|
+
|
|
11
|
+
// The FileRef itself stays: copy-locale and tenant overrides can share ids and there is
|
|
12
|
+
// no reference count to tell when the last user is gone.
|
|
13
|
+
export const resourceRemoveWrite = defineWriteHandler({
|
|
14
|
+
name: "resource-remove",
|
|
15
|
+
schema: z.object({
|
|
16
|
+
templateId: z.string().min(1),
|
|
17
|
+
key: resourceKeySchema,
|
|
18
|
+
/** Cross-tenant target — SystemAdmin only. */
|
|
19
|
+
tenantIdOverride: z.string().min(1).optional(),
|
|
20
|
+
}),
|
|
21
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
22
|
+
description:
|
|
23
|
+
"Unlinks a resource key from a template, whether it holds an image, an attached file or a linked PDF template; removing a key the template does not have succeeds without a change.",
|
|
24
|
+
handler: async (event, ctx) => {
|
|
25
|
+
const { templateId, key } = event.payload;
|
|
26
|
+
const tenantId = resourceTargetTenantId(event.payload.tenantIdOverride, event.user.tenantId);
|
|
27
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
28
|
+
const row = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
|
|
29
|
+
tenantId,
|
|
30
|
+
id: templateId,
|
|
31
|
+
});
|
|
32
|
+
if (row === undefined) return writeFailure(new NotFoundError("template-resource", templateId));
|
|
33
|
+
|
|
34
|
+
const { [key]: removed, ...remaining } = parseLinkedResourceEntries(row.linkedResources);
|
|
35
|
+
if (removed === undefined) return { isSuccess: true as const, data: { key, removed: false } };
|
|
36
|
+
|
|
37
|
+
const updated = await executor.update(
|
|
38
|
+
{
|
|
39
|
+
id: row.id,
|
|
40
|
+
version: row.version,
|
|
41
|
+
changes: { linkedResources: JSON.stringify(remaining) },
|
|
42
|
+
},
|
|
43
|
+
event.user,
|
|
44
|
+
db,
|
|
45
|
+
{ streamTenantId },
|
|
46
|
+
);
|
|
47
|
+
if (!updated.isSuccess) return updated;
|
|
48
|
+
return { isSuccess: true as const, data: { key, removed: true } };
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { isDocumentResourceKey } from "../document-schema.js";
|
|
4
|
+
|
|
5
|
+
export const RESOURCE_MAX_BYTES_CONFIG_KEY = "template-resolver:config:resource-max-bytes";
|
|
6
|
+
export const DEFAULT_RESOURCE_MAX_BYTES = 2 * 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
export const resourceKeySchema = z.string().min(1).max(64).refine(isDocumentResourceKey, {
|
|
9
|
+
message: "resource key must be letters, digits, dot, dash or underscore",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const RESOURCE_PURPOSES = ["image", "attachment"] as const;
|
|
13
|
+
export type ResourcePurpose = (typeof RESOURCE_PURPOSES)[number];
|
|
14
|
+
|
|
15
|
+
export const MAX_FILE_ATTACHMENTS_PER_TEMPLATE = 10;
|
|
16
|
+
|
|
17
|
+
export function resourceTargetTenantId(
|
|
18
|
+
tenantIdOverride: string | undefined,
|
|
19
|
+
sessionTenantId: TenantId,
|
|
20
|
+
): TenantId {
|
|
21
|
+
// @cast-boundary engine-payload — override is a zod-validated string; forTenant
|
|
22
|
+
// rejects it for non-SystemAdmins.
|
|
23
|
+
return (tenantIdOverride ?? sessionTenantId) as TenantId;
|
|
24
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { defineWriteHandler, type WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import {
|
|
3
|
+
NotFoundError,
|
|
4
|
+
UnprocessableError,
|
|
5
|
+
writeFailure,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/errors";
|
|
7
|
+
import * as z from "zod";
|
|
8
|
+
import {
|
|
9
|
+
FileRefTooLargeError,
|
|
10
|
+
fileContextForTenant,
|
|
11
|
+
readFileRefContent,
|
|
12
|
+
} from "../../file-foundation/index.js";
|
|
13
|
+
import { attachmentFileNameSchema } from "../../shared/index.js";
|
|
14
|
+
import { TemplateResolverErrors } from "../qualified-names.js";
|
|
15
|
+
import {
|
|
16
|
+
readTemplateResourceMaxBytes,
|
|
17
|
+
type StoredTemplateAttachment,
|
|
18
|
+
type StoredTemplateResource,
|
|
19
|
+
storeTemplateAttachment,
|
|
20
|
+
storeTemplateResource,
|
|
21
|
+
} from "../store-template-resource.js";
|
|
22
|
+
import { RESOURCE_PURPOSES, resourceKeySchema, resourceTargetTenantId } from "./resource-shared.js";
|
|
23
|
+
|
|
24
|
+
type AttachmentTarget = {
|
|
25
|
+
readonly templateId: string;
|
|
26
|
+
readonly key: string;
|
|
27
|
+
readonly fileName: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function attachmentTargetOf(input: {
|
|
31
|
+
readonly templateId: string | undefined;
|
|
32
|
+
readonly key: string | undefined;
|
|
33
|
+
readonly fileName: string | undefined;
|
|
34
|
+
}): AttachmentTarget | null {
|
|
35
|
+
const { templateId, key, fileName } = input;
|
|
36
|
+
if (templateId === undefined || key === undefined || fileName === undefined) return null;
|
|
37
|
+
const parsedName = attachmentFileNameSchema.safeParse(fileName);
|
|
38
|
+
return parsedName.success ? { templateId, key, fileName: parsedName.data } : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Purpose "image": stores a re-encoded image as a FileRef and, with templateId + key, links it into
|
|
42
|
+
// the template; without templateId it only creates the FileRef (the branding logo upload path).
|
|
43
|
+
// Purpose "attachment": stores the file as uploaded and links it as a mail attachment.
|
|
44
|
+
export const resourceUploadWrite = defineWriteHandler({
|
|
45
|
+
name: "resource-upload",
|
|
46
|
+
schema: z
|
|
47
|
+
.object({
|
|
48
|
+
templateId: z.string().min(1).optional(),
|
|
49
|
+
key: resourceKeySchema.optional(),
|
|
50
|
+
/** An unclaimed upload of the caller (POST /api/files without entityType). */
|
|
51
|
+
sourceFileRefId: z.uuid(),
|
|
52
|
+
/** Cross-tenant target — SystemAdmin only. */
|
|
53
|
+
tenantIdOverride: z.string().min(1).optional(),
|
|
54
|
+
/** "attachment" links a PDF/PNG/JPEG as a file attached to every mail of the template. */
|
|
55
|
+
purpose: z.enum(RESOURCE_PURPOSES).default("image"),
|
|
56
|
+
/** Name the recipient sees; required for purpose "attachment". */
|
|
57
|
+
fileName: z.string().optional(),
|
|
58
|
+
})
|
|
59
|
+
.refine(
|
|
60
|
+
(p) => p.purpose === "attachment" || (p.templateId === undefined) === (p.key === undefined),
|
|
61
|
+
{
|
|
62
|
+
message: "templateId and key must be given together",
|
|
63
|
+
path: ["key"],
|
|
64
|
+
},
|
|
65
|
+
),
|
|
66
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
67
|
+
description:
|
|
68
|
+
"Turns a file uploaded via POST /api/files (multipart, no entityType) into a template resource. With purpose image (default) the upload is checked against the resource-max-bytes cap, re-encoded to a supported image format and stored as a new file; with templateId and key it is linked into that template under the key, without them only the file is created. With purpose attachment, templateId, key and fileName are required: a PDF, PNG or JPEG is stored unchanged and linked into a mail template as a file attached to every mail sent from it, at most 10 per template. A cap above the files route's maxUploadSize has no effect.",
|
|
69
|
+
handler: async (
|
|
70
|
+
event,
|
|
71
|
+
ctx,
|
|
72
|
+
): Promise<WriteResult<StoredTemplateResource | StoredTemplateAttachment>> => {
|
|
73
|
+
const { templateId, key, sourceFileRefId, purpose, fileName } = event.payload;
|
|
74
|
+
const attachmentTarget =
|
|
75
|
+
purpose === "attachment" ? attachmentTargetOf({ templateId, key, fileName }) : null;
|
|
76
|
+
if (purpose === "attachment" && attachmentTarget === null) {
|
|
77
|
+
return writeFailure(
|
|
78
|
+
new UnprocessableError(TemplateResolverErrors.attachmentInvalid, {
|
|
79
|
+
i18nKey: "templateResolver.errors.attachment.invalid",
|
|
80
|
+
details: { code: "target_required" },
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const maxBytes = await readTemplateResourceMaxBytes(ctx.config, ctx.registry);
|
|
85
|
+
|
|
86
|
+
// The source is read from the session tenant only: with tenantIdOverride the upload
|
|
87
|
+
// sits in the admin's own tenant while the normalized file lands in the target tenant.
|
|
88
|
+
// Only the caller's own unclaimed upload qualifies, so a record-attached image of
|
|
89
|
+
// someone else cannot be copied into a template.
|
|
90
|
+
let source: Awaited<ReturnType<typeof readFileRefContent>>;
|
|
91
|
+
try {
|
|
92
|
+
source = await readFileRefContent({
|
|
93
|
+
db: ctx.db,
|
|
94
|
+
filesForTenant: (tid) => fileContextForTenant(ctx, ctx.user.tenantId, tid),
|
|
95
|
+
fileRefId: sourceFileRefId,
|
|
96
|
+
allowedTenantIds: [event.user.tenantId],
|
|
97
|
+
unclaimedUploadOf: event.user.id,
|
|
98
|
+
maxBytes,
|
|
99
|
+
});
|
|
100
|
+
} catch (cause) {
|
|
101
|
+
if (cause instanceof FileRefTooLargeError) {
|
|
102
|
+
return writeFailure(
|
|
103
|
+
new UnprocessableError(TemplateResolverErrors.resourceTooLarge, {
|
|
104
|
+
i18nKey: "templateResolver.errors.resource.tooLarge",
|
|
105
|
+
details: { maxBytes, size: cause.size },
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
throw cause;
|
|
110
|
+
}
|
|
111
|
+
if (source === null) return writeFailure(new NotFoundError("file-ref", sourceFileRefId));
|
|
112
|
+
|
|
113
|
+
// The source upload stays as an unclaimed file: bundled-features cannot reach the fileRef executor to delete it.
|
|
114
|
+
const tenantId = resourceTargetTenantId(event.payload.tenantIdOverride, event.user.tenantId);
|
|
115
|
+
if (attachmentTarget !== null) {
|
|
116
|
+
return storeTemplateAttachment({
|
|
117
|
+
ctx,
|
|
118
|
+
user: event.user,
|
|
119
|
+
tenantId,
|
|
120
|
+
bytes: source.content,
|
|
121
|
+
maxBytes,
|
|
122
|
+
...attachmentTarget,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return storeTemplateResource({
|
|
126
|
+
ctx,
|
|
127
|
+
user: event.user,
|
|
128
|
+
tenantId,
|
|
129
|
+
bytes: source.content,
|
|
130
|
+
maxBytes,
|
|
131
|
+
...(templateId !== undefined && { templateId }),
|
|
132
|
+
...(key !== undefined && { key }),
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { defineWriteHandler, type Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
import {
|
|
5
|
+
collectChannels,
|
|
6
|
+
createEmailAttachmentReader,
|
|
7
|
+
EMAIL_CHANNEL_NAME,
|
|
8
|
+
} from "../../delivery/index.js";
|
|
9
|
+
import { fileContextForTenant } from "../../file-foundation/index.js";
|
|
10
|
+
import { TEST_MAIL_NOTIFICATION_TYPE, TEST_MAIL_SUBJECT_PREFIX } from "../constants.js";
|
|
11
|
+
import { TemplateResolverErrors } from "../qualified-names.js";
|
|
12
|
+
import { renderTemplatePreview } from "../template-preview.js";
|
|
13
|
+
import { localeSchema, templateSlugSchema } from "./shared.js";
|
|
14
|
+
|
|
15
|
+
const TEST_MAIL_RATE_LIMIT = { limit: 10, windowSeconds: 3600 } as const;
|
|
16
|
+
|
|
17
|
+
function emailChannelOf(registry: Registry) {
|
|
18
|
+
return collectChannels(registry).find((channel) => channel.name === EMAIL_CHANNEL_NAME);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Transport errors can carry provider responses or the address, so the client only gets a fixed code.
|
|
22
|
+
function testMailFailed(code: "channel_error" | "not_sent") {
|
|
23
|
+
return writeFailure(
|
|
24
|
+
new UnprocessableError(TemplateResolverErrors.testMailFailed, {
|
|
25
|
+
i18nKey: "templateResolver.errors.testMail.failed",
|
|
26
|
+
details: { code },
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function errorCodeOf(error: unknown): string | undefined {
|
|
32
|
+
const code = error instanceof Error ? Reflect.get(error, "code") : undefined;
|
|
33
|
+
return typeof code === "string" ? code : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The only recipient is the caller's own address, resolved server-side, so the handler can never mail a third party.
|
|
37
|
+
export const sendTestWrite = defineWriteHandler({
|
|
38
|
+
name: "send-test",
|
|
39
|
+
schema: z.object({
|
|
40
|
+
slug: templateSlugSchema,
|
|
41
|
+
locale: localeSchema,
|
|
42
|
+
content: z.string().max(200_000).optional(),
|
|
43
|
+
meta: z.unknown().optional(),
|
|
44
|
+
variableSchema: z.record(z.string(), z.unknown()).default({}),
|
|
45
|
+
includeAttachments: z.boolean().default(true),
|
|
46
|
+
prefixSubject: z.boolean().default(true),
|
|
47
|
+
}),
|
|
48
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
49
|
+
rateLimit: { per: "user+handler", ...TEST_MAIL_RATE_LIMIT },
|
|
50
|
+
description:
|
|
51
|
+
"Sends the rendered preview of a mail template draft, or of the stored template when no content is given, to the calling user's own address with the subject prefixed by [TEST] unless prefixSubject is false, carrying the inline images and, unless includeAttachments is false, the files attached to the template; there is no recipient input, the sample values come from the declared variable examples, and a draft that fails the write rules is rejected without sending.",
|
|
52
|
+
handler: async (event, ctx) => {
|
|
53
|
+
const { includeAttachments, prefixSubject, ...previewPayload } = event.payload;
|
|
54
|
+
const outcome = await renderTemplatePreview(ctx, event.user, {
|
|
55
|
+
...previewPayload,
|
|
56
|
+
kind: "mail",
|
|
57
|
+
});
|
|
58
|
+
if (!outcome.ok) return writeFailure(outcome.error);
|
|
59
|
+
const { preview, sender } = outcome;
|
|
60
|
+
const mailAttachments = includeAttachments
|
|
61
|
+
? outcome.mailAttachments
|
|
62
|
+
: outcome.mailAttachments.filter((ref) => ref.kind === "inline");
|
|
63
|
+
|
|
64
|
+
const channel = emailChannelOf(ctx.registry);
|
|
65
|
+
if (channel === undefined) {
|
|
66
|
+
return writeFailure(
|
|
67
|
+
new UnprocessableError(TemplateResolverErrors.testMailNoChannel, {
|
|
68
|
+
i18nKey: "templateResolver.errors.testMail.noChannel",
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const channelContext = {
|
|
73
|
+
db: ctx.db,
|
|
74
|
+
registry: ctx.registry,
|
|
75
|
+
sseBroker: undefined,
|
|
76
|
+
tenantId: event.user.tenantId,
|
|
77
|
+
secrets: ctx.secrets,
|
|
78
|
+
readAttachment: createEmailAttachmentReader(
|
|
79
|
+
ctx.db,
|
|
80
|
+
ctx.registry,
|
|
81
|
+
event.user.tenantId,
|
|
82
|
+
ctx.config,
|
|
83
|
+
(tenantId) => fileContextForTenant(ctx, ctx.user.tenantId, tenantId),
|
|
84
|
+
),
|
|
85
|
+
};
|
|
86
|
+
const address = (await channel.resolve?.(event.user.id, channelContext)) ?? null;
|
|
87
|
+
if (address === null) {
|
|
88
|
+
return writeFailure(
|
|
89
|
+
new UnprocessableError(TemplateResolverErrors.testMailNoAddress, {
|
|
90
|
+
i18nKey: "templateResolver.errors.testMail.noAddress",
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const subject = `${prefixSubject ? TEST_MAIL_SUBJECT_PREFIX : ""}${preview.subject ?? ""}`;
|
|
96
|
+
let sentStatus: string;
|
|
97
|
+
try {
|
|
98
|
+
const result = await channel.send(
|
|
99
|
+
address,
|
|
100
|
+
{
|
|
101
|
+
notificationType: TEST_MAIL_NOTIFICATION_TYPE,
|
|
102
|
+
title: subject,
|
|
103
|
+
body: undefined,
|
|
104
|
+
data: undefined,
|
|
105
|
+
locale: event.payload.locale,
|
|
106
|
+
},
|
|
107
|
+
channelContext,
|
|
108
|
+
{
|
|
109
|
+
subject,
|
|
110
|
+
html: preview.html,
|
|
111
|
+
text: preview.text ?? "",
|
|
112
|
+
...sender,
|
|
113
|
+
attachments: mailAttachments,
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
sentStatus = result.status;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// Class and code only: the message can carry the address or a provider response body.
|
|
119
|
+
ctx.log?.warn("template test mail: channel threw", {
|
|
120
|
+
tenantId: event.user.tenantId,
|
|
121
|
+
errorClass: error instanceof Error ? error.name : typeof error,
|
|
122
|
+
errorCode: errorCodeOf(error),
|
|
123
|
+
});
|
|
124
|
+
return testMailFailed("channel_error");
|
|
125
|
+
}
|
|
126
|
+
if (sentStatus !== "sent") {
|
|
127
|
+
ctx.log?.warn("template test mail: channel did not send", {
|
|
128
|
+
tenantId: event.user.tenantId,
|
|
129
|
+
status: sentStatus,
|
|
130
|
+
});
|
|
131
|
+
return testMailFailed("not_sent");
|
|
132
|
+
}
|
|
133
|
+
return { isSuccess: true as const, data: { subject, checks: preview.checks } };
|
|
134
|
+
},
|
|
135
|
+
});
|
|
@@ -4,11 +4,15 @@ import {
|
|
|
4
4
|
SYSTEM_TENANT_ID,
|
|
5
5
|
type TenantId,
|
|
6
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
7
8
|
import * as z from "zod";
|
|
8
9
|
import { requireForTenant } from "../../shared/index.js";
|
|
9
|
-
import { TEMPLATE_KINDS, TEXT_BLOCK_KIND } from "../constants.js";
|
|
10
|
+
import { TEMPLATE_KINDS, TEXT_BLOCK_KIND, UPSERT_KINDS } from "../constants.js";
|
|
10
11
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
12
|
+
import { resolveTemplateVariables } from "../template-validation.js";
|
|
11
13
|
import { contentFormatSchema, executor, folderSchema, localeSchema, slugSchema } from "./shared.js";
|
|
14
|
+
import { parseStoredJson, storedRecord } from "./template-gates.js";
|
|
15
|
+
import { gateTemplateWrite } from "./template-write.js";
|
|
12
16
|
|
|
13
17
|
// Upsert of a single resource, one operation per (tenantId, slug, kind,
|
|
14
18
|
// locale). This is the content-tree authoring path: unlike upsertTenant it
|
|
@@ -26,6 +30,9 @@ import { contentFormatSchema, executor, folderSchema, localeSchema, slugSchema }
|
|
|
26
30
|
// upsertTenant → publish draft stage. That is the deliberate split between
|
|
27
31
|
// the two authoring routes — use upsertSystem/upsertTenant when a template
|
|
28
32
|
// needs a draft or a variable schema.
|
|
33
|
+
//
|
|
34
|
+
// Kinds that upsertSystem/upsertTenant also write run the same validation
|
|
35
|
+
// pipeline here, and because saving publishes, publish issues reject too.
|
|
29
36
|
export const setWrite = defineWriteHandler({
|
|
30
37
|
name: "set",
|
|
31
38
|
schema: z.object({
|
|
@@ -63,17 +70,39 @@ export const setWrite = defineWriteHandler({
|
|
|
63
70
|
locale: event.payload.locale,
|
|
64
71
|
});
|
|
65
72
|
|
|
73
|
+
const scope = tenantId === SYSTEM_TENANT_ID ? ("system" as const) : ("tenant" as const);
|
|
74
|
+
let content = event.payload.content;
|
|
75
|
+
if (isOneOf(UPSERT_KINDS, kind)) {
|
|
76
|
+
const gated = gateTemplateWrite(
|
|
77
|
+
{
|
|
78
|
+
kind,
|
|
79
|
+
scope,
|
|
80
|
+
contentFormat: event.payload.contentFormat,
|
|
81
|
+
content,
|
|
82
|
+
rawMeta: parseStoredJson(existing?.meta ?? null),
|
|
83
|
+
variables: resolveTemplateVariables(
|
|
84
|
+
ctx.registry,
|
|
85
|
+
event.payload.slug,
|
|
86
|
+
storedRecord(existing?.variableSchema ?? null),
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
"active",
|
|
90
|
+
);
|
|
91
|
+
if (!gated.ok) return gated.failure;
|
|
92
|
+
content = gated.evaluated.content;
|
|
93
|
+
}
|
|
94
|
+
|
|
66
95
|
const fields = {
|
|
67
96
|
slug: event.payload.slug,
|
|
68
97
|
kind,
|
|
69
98
|
locale: event.payload.locale,
|
|
70
99
|
title: event.payload.title,
|
|
71
|
-
content
|
|
100
|
+
content,
|
|
72
101
|
contentFormat: event.payload.contentFormat,
|
|
73
102
|
folder: event.payload.folder ?? null,
|
|
74
103
|
variableSchema: "{}",
|
|
75
104
|
linkedResources: "{}",
|
|
76
|
-
scope
|
|
105
|
+
scope,
|
|
77
106
|
parentTemplateId: null,
|
|
78
107
|
// Text blocks have no draft stage — saving publishes, so resolveTemplate
|
|
79
108
|
// (which only returns active rows) finds them.
|
|
@@ -2,6 +2,7 @@ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { CONTENT_FORMATS, TEMPLATE_STATUSES, UPSERT_KINDS } from "../constants.js";
|
|
4
4
|
import { templateResourceEntity, templateResourcesTable } from "../table.js";
|
|
5
|
+
import { templateSlugSchema } from "../template-meta.js";
|
|
5
6
|
|
|
6
7
|
// One executor per bundle, shared by all four handlers for create/update via
|
|
7
8
|
// event store + optimistic lock.
|
|
@@ -16,6 +17,8 @@ export const slugSchema = z
|
|
|
16
17
|
.max(80)
|
|
17
18
|
.regex(/^[a-z0-9][a-z0-9-]*$/, "slug must be kebab-case (lowercase, digits, dashes)");
|
|
18
19
|
|
|
20
|
+
export { templateSlugSchema };
|
|
21
|
+
|
|
19
22
|
export const localeSchema = z
|
|
20
23
|
.string()
|
|
21
24
|
.min(2)
|
|
@@ -38,13 +41,15 @@ export const statusSchema = z.enum(TEMPLATE_STATUSES);
|
|
|
38
41
|
// Common Upsert-Payload — geteilt zwischen upsertSystem + upsertTenant.
|
|
39
42
|
// Unterschied: ACL + tenantId-Bestimmung, sonst identisch.
|
|
40
43
|
export const upsertPayloadSchema = z.object({
|
|
41
|
-
slug:
|
|
44
|
+
slug: templateSlugSchema,
|
|
42
45
|
kind: kindSchema,
|
|
43
46
|
locale: localeSchema,
|
|
44
47
|
content: z.string().max(200_000),
|
|
45
48
|
contentFormat: contentFormatSchema,
|
|
46
49
|
variableSchema: z.record(z.string(), z.unknown()).default({}),
|
|
47
|
-
|
|
50
|
+
// Shape depends on kind; validated against template-meta.ts in the handler so
|
|
51
|
+
// problems surface as field issues on `meta.*`.
|
|
52
|
+
meta: z.unknown().optional(),
|
|
48
53
|
parentTemplateId: z.string().min(1).optional(),
|
|
49
54
|
title: titleSchema.nullable().optional(),
|
|
50
55
|
folder: folderSchema.nullable().optional(),
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { Registry, TemplateVariables } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import type { FieldIssue } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
4
|
+
import {
|
|
5
|
+
CONTENT_FORMATS,
|
|
6
|
+
type ContentFormat,
|
|
7
|
+
TEMPLATE_KINDS,
|
|
8
|
+
type TemplateKind,
|
|
9
|
+
} from "../constants.js";
|
|
10
|
+
import { documentViolationsToFieldIssues } from "../document-sanitizer.js";
|
|
11
|
+
import { sanitizeTemplateWriteBody, type TemplateWriteScope } from "../layout-regions.js";
|
|
12
|
+
import { imageResourceKeysOf } from "../linked-resources.js";
|
|
13
|
+
import type { TemplateResourceRow } from "../table.js";
|
|
14
|
+
import { parseTemplateMeta, type TemplateMetaIssue } from "../template-meta.js";
|
|
15
|
+
import {
|
|
16
|
+
resolveTemplateVariables,
|
|
17
|
+
type TemplateValidation,
|
|
18
|
+
validateTemplatePlaceholders,
|
|
19
|
+
} from "../template-validation.js";
|
|
20
|
+
|
|
21
|
+
const PLACEHOLDER_VALIDATED_KINDS: ReadonlySet<TemplateKind> = new Set([
|
|
22
|
+
"mail",
|
|
23
|
+
"document-pdf",
|
|
24
|
+
"pdf-layout",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
// The parser's wording is English and meant for logs; the UI gets a key per setting instead.
|
|
28
|
+
const META_FIELDS_WITH_MESSAGES: ReadonlySet<string> = new Set([
|
|
29
|
+
"subject",
|
|
30
|
+
"previewText",
|
|
31
|
+
"senderName",
|
|
32
|
+
"replyTo",
|
|
33
|
+
"layoutSlug",
|
|
34
|
+
"fileName",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function metaIssueToFieldIssue(issue: TemplateMetaIssue): FieldIssue {
|
|
38
|
+
const hasMessage = META_FIELDS_WITH_MESSAGES.has(issue.field);
|
|
39
|
+
return {
|
|
40
|
+
path: issue.path,
|
|
41
|
+
code: "invalid_meta",
|
|
42
|
+
i18nKey: hasMessage
|
|
43
|
+
? `templateResolver.errors.template.meta.${issue.field}.${issue.missing ? "required" : "invalid"}`
|
|
44
|
+
: "templateResolver.errors.template.invalidMeta",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function placeholderIssues(
|
|
49
|
+
validation: TemplateValidation,
|
|
50
|
+
opts: { readonly requireAllRequired: boolean },
|
|
51
|
+
): FieldIssue[] {
|
|
52
|
+
const issues: FieldIssue[] = [];
|
|
53
|
+
if (validation.unknown.length > 0) {
|
|
54
|
+
issues.push({
|
|
55
|
+
path: "content",
|
|
56
|
+
code: "unknown_placeholder",
|
|
57
|
+
i18nKey: "templateResolver.errors.template.unknownPlaceholders",
|
|
58
|
+
params: { detail: validation.unknown.join(", ") },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (opts.requireAllRequired && validation.missingRequired.length > 0) {
|
|
62
|
+
issues.push({
|
|
63
|
+
path: "content",
|
|
64
|
+
code: "missing_required_placeholder",
|
|
65
|
+
i18nKey: "templateResolver.errors.template.missingRequiredPlaceholders",
|
|
66
|
+
params: { detail: validation.missingRequired.join(", ") },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return issues;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type TemplateWriteInput = {
|
|
73
|
+
readonly kind: TemplateKind;
|
|
74
|
+
readonly scope: TemplateWriteScope;
|
|
75
|
+
readonly contentFormat: ContentFormat;
|
|
76
|
+
readonly content: string | null;
|
|
77
|
+
readonly rawMeta: unknown;
|
|
78
|
+
readonly variables: TemplateVariables;
|
|
79
|
+
/** Keys with an uploaded file besides the built-in `logo`; only feeds the unknownResources warning. */
|
|
80
|
+
readonly resourceKeys?: readonly string[];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type TemplateWriteResult =
|
|
84
|
+
| {
|
|
85
|
+
readonly ok: true;
|
|
86
|
+
readonly content: string | null;
|
|
87
|
+
readonly metaJson: string | null;
|
|
88
|
+
readonly validation: TemplateValidation | null;
|
|
89
|
+
/** Problems that still allow a draft but block publishing. */
|
|
90
|
+
readonly publishIssues: readonly FieldIssue[];
|
|
91
|
+
}
|
|
92
|
+
| {
|
|
93
|
+
readonly ok: false;
|
|
94
|
+
readonly issues: readonly FieldIssue[];
|
|
95
|
+
/** Set when the placeholder scan ran, so callers can still list what it found. */
|
|
96
|
+
readonly validation: TemplateValidation | null;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const NO_PUBLISH_ISSUES: readonly FieldIssue[] = [];
|
|
100
|
+
|
|
101
|
+
function isPlaceholderValidated(kind: TemplateKind, contentFormat: ContentFormat): boolean {
|
|
102
|
+
if (kind === "mail-layout") return contentFormat === "html";
|
|
103
|
+
return PLACEHOLDER_VALIDATED_KINDS.has(kind);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The single validation pipeline of every template write: sanitize and layout
|
|
107
|
+
// rules, then meta, then placeholders.
|
|
108
|
+
export function evaluateTemplateWrite(input: TemplateWriteInput): TemplateWriteResult {
|
|
109
|
+
const body = sanitizeTemplateWriteBody({
|
|
110
|
+
kind: input.kind,
|
|
111
|
+
scope: input.scope,
|
|
112
|
+
contentFormat: input.contentFormat,
|
|
113
|
+
content: input.content,
|
|
114
|
+
});
|
|
115
|
+
if (!body.ok)
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
issues: documentViolationsToFieldIssues(body.violations),
|
|
119
|
+
validation: null,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const parsedMeta = parseTemplateMeta(input.kind, input.rawMeta);
|
|
123
|
+
if (!parsedMeta.ok) {
|
|
124
|
+
return { ok: false, issues: parsedMeta.issues.map(metaIssueToFieldIssue), validation: null };
|
|
125
|
+
}
|
|
126
|
+
const metaJson = parsedMeta.meta === null ? null : JSON.stringify(parsedMeta.meta);
|
|
127
|
+
if (!isPlaceholderValidated(input.kind, input.contentFormat)) {
|
|
128
|
+
return {
|
|
129
|
+
ok: true,
|
|
130
|
+
content: body.content,
|
|
131
|
+
metaJson,
|
|
132
|
+
validation: null,
|
|
133
|
+
publishIssues: NO_PUBLISH_ISSUES,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const validation = validateTemplatePlaceholders({
|
|
138
|
+
kind: input.kind,
|
|
139
|
+
body: body.content ?? "",
|
|
140
|
+
meta: parsedMeta.meta,
|
|
141
|
+
variables: input.variables,
|
|
142
|
+
resourceKeys: input.resourceKeys,
|
|
143
|
+
});
|
|
144
|
+
const issues = placeholderIssues(validation, { requireAllRequired: false });
|
|
145
|
+
if (issues.length > 0) return { ok: false, issues, validation };
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
content: body.content,
|
|
149
|
+
metaJson,
|
|
150
|
+
validation,
|
|
151
|
+
publishIssues: placeholderIssues(validation, { requireAllRequired: true }),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isTemplateKind(kind: string): kind is TemplateKind {
|
|
156
|
+
return isOneOf(TEMPLATE_KINDS, kind);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function parseStoredJson(raw: string | null): unknown {
|
|
160
|
+
if (!raw) return undefined;
|
|
161
|
+
try {
|
|
162
|
+
return JSON.parse(raw);
|
|
163
|
+
} catch {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isGatedKind(kind: string): kind is TemplateKind {
|
|
169
|
+
return isTemplateKind(kind) && (kind === "mail-layout" || PLACEHOLDER_VALIDATED_KINDS.has(kind));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function storedRecord(raw: string | null): Readonly<Record<string, unknown>> {
|
|
173
|
+
const parsed = parseStoredJson(raw);
|
|
174
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
175
|
+
? Object.fromEntries(Object.entries(parsed))
|
|
176
|
+
: {};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Publishing makes a row servable, so a stored row must pass the same pipeline
|
|
180
|
+
// as an active write would have.
|
|
181
|
+
export function publishGateIssues(
|
|
182
|
+
registry: Registry,
|
|
183
|
+
row: TemplateResourceRow,
|
|
184
|
+
): readonly FieldIssue[] {
|
|
185
|
+
if (!isGatedKind(row.kind) || !isOneOf(CONTENT_FORMATS, row.contentFormat)) return [];
|
|
186
|
+
const result = evaluateTemplateWrite({
|
|
187
|
+
kind: row.kind,
|
|
188
|
+
scope: row.scope === "system" ? "system" : "tenant",
|
|
189
|
+
contentFormat: row.contentFormat,
|
|
190
|
+
content: row.content,
|
|
191
|
+
rawMeta: parseStoredJson(row.meta),
|
|
192
|
+
variables: resolveTemplateVariables(registry, row.slug, storedRecord(row.variableSchema)),
|
|
193
|
+
resourceKeys: imageResourceKeysOf(row.linkedResources),
|
|
194
|
+
});
|
|
195
|
+
return result.ok ? result.publishIssues : result.issues;
|
|
196
|
+
}
|