@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,102 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import type { FileAttachmentRef } from "../renderer-foundation/index.js";
|
|
4
|
+
import { pdfAttachmentFileNamePatternSchema, templateSlugSchema } from "./template-meta.js";
|
|
5
|
+
|
|
6
|
+
export const templatePdfAttachmentLinkSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
kind: z.literal("rendered-pdf"),
|
|
9
|
+
templateSlug: templateSlugSchema,
|
|
10
|
+
fileName: pdfAttachmentFileNamePatternSchema,
|
|
11
|
+
})
|
|
12
|
+
.strict();
|
|
13
|
+
|
|
14
|
+
// A PDF the mail renderer asks for at send time: the document template is rendered with the
|
|
15
|
+
// mail's variables, so nothing is stored. `fileName` is a pattern that may hold {{placeholders}}.
|
|
16
|
+
export type TemplatePdfAttachmentLink = z.infer<typeof templatePdfAttachmentLinkSchema>;
|
|
17
|
+
|
|
18
|
+
// A string is an image the body embeds by key (the fileRef id); an object is a file attached to
|
|
19
|
+
// the mail as-is or a PDF rendered from a document template. Images are the only thing a document
|
|
20
|
+
// body can reference.
|
|
21
|
+
export type LinkedResourceEntry = string | FileAttachmentRef | TemplatePdfAttachmentLink;
|
|
22
|
+
|
|
23
|
+
export function isFileAttachmentEntry(entry: LinkedResourceEntry): entry is FileAttachmentRef {
|
|
24
|
+
return typeof entry !== "string" && entry.kind === "file";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isPdfAttachmentLink(
|
|
28
|
+
entry: LinkedResourceEntry,
|
|
29
|
+
): entry is TemplatePdfAttachmentLink {
|
|
30
|
+
return typeof entry !== "string" && entry.kind === "rendered-pdf";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toLinkedResourceEntry(raw: unknown): LinkedResourceEntry | null {
|
|
34
|
+
if (typeof raw === "string") return raw;
|
|
35
|
+
if (!isPlainObject(raw)) return null;
|
|
36
|
+
if (raw["kind"] === "rendered-pdf") {
|
|
37
|
+
const link = templatePdfAttachmentLinkSchema.safeParse(raw);
|
|
38
|
+
return link.success ? link.data : null;
|
|
39
|
+
}
|
|
40
|
+
const { fileRefId, fileName } = raw;
|
|
41
|
+
if (raw["kind"] !== "file" || typeof fileRefId !== "string" || typeof fileName !== "string") {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return { kind: "file", fileRefId, fileName };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseLinkedResourceEntries(
|
|
48
|
+
raw: string | null,
|
|
49
|
+
): Record<string, LinkedResourceEntry> {
|
|
50
|
+
if (!raw) return {};
|
|
51
|
+
let parsed: unknown;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(raw);
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
if (!isPlainObject(parsed)) return {};
|
|
58
|
+
const entries: Record<string, LinkedResourceEntry> = {};
|
|
59
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
60
|
+
const entry = toLinkedResourceEntry(value);
|
|
61
|
+
if (entry !== null) entries[key] = entry;
|
|
62
|
+
}
|
|
63
|
+
return entries;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function imageLinksOf(
|
|
67
|
+
entries: Readonly<Record<string, LinkedResourceEntry>>,
|
|
68
|
+
): Record<string, string> {
|
|
69
|
+
const images: Record<string, string> = {};
|
|
70
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
71
|
+
if (typeof entry === "string") images[key] = entry;
|
|
72
|
+
}
|
|
73
|
+
return images;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function fileAttachmentsOf(
|
|
77
|
+
entries: Readonly<Record<string, LinkedResourceEntry>>,
|
|
78
|
+
): FileAttachmentRef[] {
|
|
79
|
+
return Object.entries(entries)
|
|
80
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
81
|
+
.flatMap(([, entry]) => (isFileAttachmentEntry(entry) ? [entry] : []));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function pdfAttachmentLinksOf(
|
|
85
|
+
entries: Readonly<Record<string, LinkedResourceEntry>>,
|
|
86
|
+
): TemplatePdfAttachmentLink[] {
|
|
87
|
+
return Object.entries(entries)
|
|
88
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
89
|
+
.flatMap(([, entry]) => (isPdfAttachmentLink(entry) ? [entry] : []));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function attachmentEntriesOf(
|
|
93
|
+
entries: Readonly<Record<string, LinkedResourceEntry>>,
|
|
94
|
+
): [string, FileAttachmentRef | TemplatePdfAttachmentLink][] {
|
|
95
|
+
return Object.entries(entries).flatMap(([key, entry]) =>
|
|
96
|
+
typeof entry === "string" ? [] : [[key, entry]],
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function imageResourceKeysOf(raw: string | null): string[] {
|
|
101
|
+
return Object.keys(imageLinksOf(parseLinkedResourceEntries(raw)));
|
|
102
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// No imports: web/ re-exports these, and constants.ts pulls in the framework engine.
|
|
3
|
+
export const PDF_PAGE_FORMATS = ["A4", "Letter"] as const;
|
|
4
|
+
export type PdfPageFormat = (typeof PDF_PAGE_FORMATS)[number];
|
|
5
|
+
|
|
6
|
+
export const PDF_ORIENTATIONS = ["portrait", "landscape"] as const;
|
|
7
|
+
export type PdfOrientation = (typeof PDF_ORIENTATIONS)[number];
|
|
8
|
+
|
|
9
|
+
// Page margins in mm; the header/footer values apply when the layout contributes that region and the template sets no margin itself.
|
|
10
|
+
export const PDF_DEFAULT_MARGIN_MM = 15;
|
|
11
|
+
export const PDF_HEADER_MARGIN_MM = 30;
|
|
12
|
+
export const PDF_FOOTER_MARGIN_MM = 20;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { InlineAttachmentRef } from "../renderer-foundation/index.js";
|
|
2
|
+
|
|
3
|
+
export const PREVIEW_IMAGE_BUDGET_BYTES = 5 * 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
const DISPLAYABLE_IMAGE_TYPES: ReadonlySet<string> = new Set([
|
|
6
|
+
"image/png",
|
|
7
|
+
"image/jpeg",
|
|
8
|
+
"image/gif",
|
|
9
|
+
"image/webp",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export type PreviewAttachmentReader = (
|
|
13
|
+
ref: InlineAttachmentRef,
|
|
14
|
+
) => Promise<{ readonly content: Uint8Array; readonly contentType: string } | null>;
|
|
15
|
+
|
|
16
|
+
// The preview shows images the mail client would resolve by Content-ID; a browser cannot, so each
|
|
17
|
+
// resolved image becomes a data URI. `html` itself stays untouched because the send path needs the cids.
|
|
18
|
+
export async function buildDisplayHtml(
|
|
19
|
+
html: string,
|
|
20
|
+
inline: readonly InlineAttachmentRef[],
|
|
21
|
+
readAttachment: PreviewAttachmentReader,
|
|
22
|
+
): Promise<string> {
|
|
23
|
+
let displayHtml = html;
|
|
24
|
+
let usedBytes = 0;
|
|
25
|
+
for (const ref of inline) {
|
|
26
|
+
const needle = `src="cid:${ref.cid}"`;
|
|
27
|
+
if (!displayHtml.includes(needle)) continue;
|
|
28
|
+
const found = await readAttachment(ref);
|
|
29
|
+
if (found === null || !DISPLAYABLE_IMAGE_TYPES.has(found.contentType)) continue;
|
|
30
|
+
if (usedBytes + found.content.length > PREVIEW_IMAGE_BUDGET_BYTES) continue;
|
|
31
|
+
usedBytes += found.content.length;
|
|
32
|
+
const dataUri = `src="data:${found.contentType};base64,${Buffer.from(found.content).toString("base64")}"`;
|
|
33
|
+
displayHtml = displayHtml.replaceAll(needle, () => dataUri);
|
|
34
|
+
}
|
|
35
|
+
return displayHtml;
|
|
36
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Server-only (happy-dom). Prepares a rendered document body for the browser
|
|
2
|
+
// that prints it: repeating table headers become real <thead> rows, and
|
|
3
|
+
// everything that would make the browser fetch something external is removed.
|
|
4
|
+
|
|
5
|
+
import type { Element as ScanElement } from "happy-dom";
|
|
6
|
+
import { withParsedDocument } from "./document-sanitizer.js";
|
|
7
|
+
|
|
8
|
+
const REPEAT_HEADER_ATTRIBUTE = "data-repeat-header";
|
|
9
|
+
const FETCHING_ELEMENTS =
|
|
10
|
+
"script, link, iframe, object, embed, video, audio, source, meta, base, style";
|
|
11
|
+
const FETCHING_ATTRIBUTES = ["src", "srcset", "poster", "background", "data"] as const;
|
|
12
|
+
// Anchors only navigate; on svg image/use/feImage an href fetches.
|
|
13
|
+
const HREF_ATTRIBUTES = ["href", "xlink:href"] as const;
|
|
14
|
+
const EXTERNAL_STYLE_REFERENCE = /url\(|@import/i;
|
|
15
|
+
|
|
16
|
+
function isEmbeddedValue(value: string): boolean {
|
|
17
|
+
return value.trim().toLowerCase().startsWith("data:");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function moveRepeatHeadersIntoThead(table: ScanElement): void {
|
|
21
|
+
const rows = Array.from(table.querySelectorAll("tr")).filter(
|
|
22
|
+
(row) => row.closest("table") === table,
|
|
23
|
+
);
|
|
24
|
+
const leading: ScanElement[] = [];
|
|
25
|
+
for (const row of rows) {
|
|
26
|
+
if (!row.hasAttribute(REPEAT_HEADER_ATTRIBUTE)) break;
|
|
27
|
+
leading.push(row);
|
|
28
|
+
}
|
|
29
|
+
// skip: a table without leading repeat-header rows keeps its structure as written
|
|
30
|
+
if (leading.length === 0) return;
|
|
31
|
+
const thead = table.ownerDocument.createElement("thead");
|
|
32
|
+
for (const row of leading) {
|
|
33
|
+
row.removeAttribute(REPEAT_HEADER_ATTRIBUTE);
|
|
34
|
+
thead.appendChild(row);
|
|
35
|
+
}
|
|
36
|
+
table.insertBefore(thead, table.firstChild);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function prepareDocumentForPrint(html: string): string {
|
|
40
|
+
return withParsedDocument(html, (body) => {
|
|
41
|
+
for (const table of Array.from(body.querySelectorAll("table"))) {
|
|
42
|
+
moveRepeatHeadersIntoThead(table);
|
|
43
|
+
}
|
|
44
|
+
for (const element of Array.from(body.querySelectorAll(FETCHING_ELEMENTS))) element.remove();
|
|
45
|
+
for (const element of Array.from(body.querySelectorAll("*"))) {
|
|
46
|
+
for (const name of FETCHING_ATTRIBUTES) {
|
|
47
|
+
const value = element.getAttribute(name);
|
|
48
|
+
if (value !== null && !isEmbeddedValue(value)) element.removeAttribute(name);
|
|
49
|
+
}
|
|
50
|
+
if (element.tagName.toLowerCase() !== "a") {
|
|
51
|
+
for (const name of HREF_ATTRIBUTES) element.removeAttribute(name);
|
|
52
|
+
}
|
|
53
|
+
const style = element.getAttribute("style");
|
|
54
|
+
if (style !== null && EXTERNAL_STYLE_REFERENCE.test(style)) element.removeAttribute("style");
|
|
55
|
+
}
|
|
56
|
+
return body.innerHTML;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -7,12 +7,32 @@ export const TemplateResolverHandlers = {
|
|
|
7
7
|
upsertTenant: "template-resolver:write:upsert-tenant",
|
|
8
8
|
publish: "template-resolver:write:publish",
|
|
9
9
|
archive: "template-resolver:write:archive",
|
|
10
|
+
publishSystem: "template-resolver:write:publish-system",
|
|
11
|
+
archiveSystem: "template-resolver:write:archive-system",
|
|
12
|
+
discardDraft: "template-resolver:write:discard-draft",
|
|
13
|
+
discardDraftSystem: "template-resolver:write:discard-draft-system",
|
|
14
|
+
resetToSystem: "template-resolver:write:reset-to-system",
|
|
10
15
|
set: "template-resolver:write:set",
|
|
16
|
+
importBundle: "template-resolver:write:import-bundle",
|
|
17
|
+
importBundleSystem: "template-resolver:write:import-bundle-system",
|
|
18
|
+
copyLocale: "template-resolver:write:copy-locale",
|
|
19
|
+
resourceUpload: "template-resolver:write:resource-upload",
|
|
20
|
+
resourceRemove: "template-resolver:write:resource-remove",
|
|
21
|
+
resourceLinkPdf: "template-resolver:write:resource-link-pdf",
|
|
22
|
+
sendTest: "template-resolver:write:send-test",
|
|
11
23
|
} as const;
|
|
12
24
|
|
|
13
25
|
export const TemplateResolverQueries = {
|
|
14
26
|
findById: "template-resolver:query:find-by-id",
|
|
15
27
|
list: "template-resolver:query:list",
|
|
28
|
+
usage: "template-resolver:query:usage",
|
|
29
|
+
overview: "template-resolver:query:overview",
|
|
30
|
+
layoutSource: "template-resolver:query:layout-source",
|
|
31
|
+
tenantOverrides: "template-resolver:query:tenant-overrides",
|
|
32
|
+
diffToSystem: "template-resolver:query:diff-to-system",
|
|
33
|
+
validate: "template-resolver:query:validate",
|
|
34
|
+
preview: "template-resolver:query:preview",
|
|
35
|
+
exportBundle: "template-resolver:query:export-bundle",
|
|
16
36
|
// Public pair — anonymous-reachable, kind pinned to text-block.
|
|
17
37
|
bySlug: "template-resolver:query:by-slug",
|
|
18
38
|
byTenant: "template-resolver:query:by-tenant",
|
|
@@ -35,6 +55,20 @@ export const TemplateResolverErrors = {
|
|
|
35
55
|
invalidSlug: "invalid_slug",
|
|
36
56
|
invalidLocale: "invalid_locale",
|
|
37
57
|
systemAdminRequired: "system_admin_required",
|
|
58
|
+
noDraft: "no_draft",
|
|
38
59
|
alreadyActive: "template_already_active",
|
|
39
60
|
alreadyArchived: "template_already_archived",
|
|
61
|
+
targetExists: "target_exists",
|
|
62
|
+
systemResourcesNeedSystemTenant: "system_resources_need_system_tenant",
|
|
63
|
+
bundleLocalesDiffer: "bundle_locales_differ",
|
|
64
|
+
resourceTooLarge: "template_resource_too_large",
|
|
65
|
+
resourceUnsupportedImage: "template_resource_unsupported_image",
|
|
66
|
+
attachmentUnsupportedType: "template_attachment_unsupported_type",
|
|
67
|
+
attachmentLimit: "template_attachment_limit",
|
|
68
|
+
attachmentInvalid: "template_attachment_invalid",
|
|
69
|
+
attachmentPdfTemplateMissing: "template_attachment_pdf_template_missing",
|
|
70
|
+
previewRenderFailed: "template_preview_render_failed",
|
|
71
|
+
testMailNoChannel: "template_test_mail_no_channel",
|
|
72
|
+
testMailNoAddress: "template_test_mail_no_address",
|
|
73
|
+
testMailFailed: "template_test_mail_failed",
|
|
40
74
|
} as const;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NotificationRecordMapping } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
|
|
3
|
+
function readPath(row: Readonly<Record<string, unknown>>, path: string): unknown {
|
|
4
|
+
let current: unknown = row;
|
|
5
|
+
for (const segment of path.split(".")) {
|
|
6
|
+
if (typeof current !== "object" || current === null || !Object.hasOwn(current, segment)) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
current = Object.getOwnPropertyDescriptor(current, segment)?.value;
|
|
10
|
+
}
|
|
11
|
+
return current;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Variables whose path is missing on the row are left out, so the template
|
|
15
|
+
// renders them empty instead of printing "undefined".
|
|
16
|
+
export function mapRecordToVariables(
|
|
17
|
+
mapping: NotificationRecordMapping,
|
|
18
|
+
row: Readonly<Record<string, unknown>>,
|
|
19
|
+
): Record<string, unknown> {
|
|
20
|
+
const variables: Record<string, unknown> = {};
|
|
21
|
+
for (const [variable, path] of Object.entries(mapping.map)) {
|
|
22
|
+
const value = readPath(row, path);
|
|
23
|
+
if (value !== undefined) variables[variable] = value;
|
|
24
|
+
}
|
|
25
|
+
return variables;
|
|
26
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { assertNotSvg, SHARP_INPUT_OPTIONS } from "../shared/index.js";
|
|
2
|
+
|
|
3
|
+
const MAX_RESOURCE_WIDTH = 1200;
|
|
4
|
+
|
|
5
|
+
const CONTENT_TYPE_BY_FORMAT = {
|
|
6
|
+
png: "image/png",
|
|
7
|
+
jpeg: "image/jpeg",
|
|
8
|
+
gif: "image/gif",
|
|
9
|
+
webp: "image/webp",
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
type ResourceImageFormat = keyof typeof CONTENT_TYPE_BY_FORMAT;
|
|
13
|
+
|
|
14
|
+
export type NormalizedResourceImage = {
|
|
15
|
+
readonly data: Uint8Array;
|
|
16
|
+
readonly contentType: (typeof CONTENT_TYPE_BY_FORMAT)[ResourceImageFormat];
|
|
17
|
+
readonly width: number;
|
|
18
|
+
readonly height: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function isResourceImageFormat(format: string | undefined): format is ResourceImageFormat {
|
|
22
|
+
return format !== undefined && Object.hasOwn(CONTENT_TYPE_BY_FORMAT, format);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Re-encodes an uploaded image: the stored content type comes from the decoded format, never
|
|
26
|
+
// from the client, and re-encoding drops metadata and anything appended to the file.
|
|
27
|
+
// sharp is imported lazily because every app loads template-resolver, few of them need an image decoder.
|
|
28
|
+
export async function normalizeResourceImage(data: Uint8Array): Promise<NormalizedResourceImage> {
|
|
29
|
+
const { default: sharp } = await import("sharp");
|
|
30
|
+
const metadata = await sharp(data, SHARP_INPUT_OPTIONS).metadata();
|
|
31
|
+
assertNotSvg(metadata.format);
|
|
32
|
+
if (!isResourceImageFormat(metadata.format)) {
|
|
33
|
+
throw new Error(`unsupported image format "${metadata.format ?? "unknown"}"`);
|
|
34
|
+
}
|
|
35
|
+
const format = metadata.format;
|
|
36
|
+
const pipeline = sharp(data, SHARP_INPUT_OPTIONS)
|
|
37
|
+
.rotate()
|
|
38
|
+
.resize({ width: MAX_RESOURCE_WIDTH, withoutEnlargement: true })
|
|
39
|
+
.toFormat(format);
|
|
40
|
+
const { data: output, info } = await pipeline.toBuffer({ resolveWithObject: true });
|
|
41
|
+
return {
|
|
42
|
+
data: new Uint8Array(output),
|
|
43
|
+
contentType: CONTENT_TYPE_BY_FORMAT[format],
|
|
44
|
+
width: info.width,
|
|
45
|
+
height: info.height,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// Boot
|
|
2
|
-
// SYSTEM_TENANT_ID, scope=system, status=active)
|
|
3
|
-
//
|
|
1
|
+
// Boot/seed helper for app authors. Writes system templates (tenantId=
|
|
2
|
+
// SYSTEM_TENANT_ID, scope=system, status=active) idempotently — same projection
|
|
3
|
+
// path as the upsertSystem handler, without the access check.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// Use it from runProdApp seeds or TenantCreated hooks that install the welcome,
|
|
6
|
+
// incident or feature mail slugs, so the mail renderer can resolve them
|
|
7
|
+
// through the template-resolver.
|
|
8
8
|
//
|
|
9
|
-
// Default ifExists="skip". `createSystemUser(SYSTEM_TENANT_ID)
|
|
10
|
-
//
|
|
9
|
+
// Default ifExists="skip". The actor is `createSystemUser(SYSTEM_TENANT_ID)`,
|
|
10
|
+
// deliberately not TestUsers (prod seeds are not test utilities).
|
|
11
11
|
|
|
12
12
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
13
13
|
import {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
20
20
|
import {
|
|
21
21
|
createSystemUser,
|
|
22
|
+
type Registry,
|
|
22
23
|
type SessionUser,
|
|
23
24
|
SYSTEM_TENANT_ID,
|
|
24
25
|
type TenantId,
|
|
@@ -30,7 +31,13 @@ import {
|
|
|
30
31
|
import { runEventStoreSeed, type SeedIfExists } from "@cosmicdrift/kumiko-framework/seeding";
|
|
31
32
|
import { type ContentFormat, TEXT_BLOCK_KIND, type UpsertKind } from "./constants.js";
|
|
32
33
|
import { executor } from "./handlers/shared.js";
|
|
34
|
+
import { evaluateTemplateWrite } from "./handlers/template-gates.js";
|
|
35
|
+
import { TEMPLATE_RESOLVER_FEATURE } from "./qualified-names.js";
|
|
33
36
|
import { type TemplateResourceRow, templateResourcesTable } from "./table.js";
|
|
37
|
+
import type { TemplateBundle } from "./template-bundle.js";
|
|
38
|
+
import { activeStampChanges, servedStampChanges } from "./template-draft.js";
|
|
39
|
+
import { templateSlugSchema } from "./template-meta.js";
|
|
40
|
+
import { variablesFromSchema } from "./template-validation.js";
|
|
34
41
|
|
|
35
42
|
/** Drop projection orphans (no events anywhere) so boot seeds can recreate. */
|
|
36
43
|
async function resolveExistingForEventStoreSeed(
|
|
@@ -65,7 +72,10 @@ export type SeedSystemTemplateOptions = {
|
|
|
65
72
|
readonly content: string;
|
|
66
73
|
readonly contentFormat: ContentFormat;
|
|
67
74
|
readonly variableSchema?: Record<string, unknown>;
|
|
75
|
+
/** Omitted: links on an existing row stay untouched, so a re-seed on boot keeps images a
|
|
76
|
+
* system admin uploaded through resource-upload. */
|
|
68
77
|
readonly linkedResources?: Record<string, string>;
|
|
78
|
+
readonly meta?: unknown;
|
|
69
79
|
readonly parentTemplateId?: string | null;
|
|
70
80
|
readonly by?: SessionUser;
|
|
71
81
|
readonly ifExists?: SeedIfExists;
|
|
@@ -75,10 +85,29 @@ export async function seedSystemTemplate(
|
|
|
75
85
|
db: DbConnection,
|
|
76
86
|
opts: SeedSystemTemplateOptions,
|
|
77
87
|
): Promise<{ id: string }> {
|
|
78
|
-
const tenantId = SYSTEM_TENANT_ID
|
|
88
|
+
const tenantId = SYSTEM_TENANT_ID;
|
|
79
89
|
const by = opts.by ?? createSystemUser(tenantId);
|
|
80
90
|
const tdb = createTenantDb(db, tenantId, "system");
|
|
81
91
|
|
|
92
|
+
const label = `seedSystemTemplate ${opts.slug}/${opts.kind}/${opts.locale}`;
|
|
93
|
+
const slug = templateSlugSchema.safeParse(opts.slug);
|
|
94
|
+
if (!slug.success) throw new Error(`${label} rejected: ${slug.error.issues[0]?.message}`);
|
|
95
|
+
const evaluated = evaluateTemplateWrite({
|
|
96
|
+
kind: opts.kind,
|
|
97
|
+
scope: "system",
|
|
98
|
+
contentFormat: opts.contentFormat,
|
|
99
|
+
content: opts.content,
|
|
100
|
+
rawMeta: opts.meta,
|
|
101
|
+
variables: variablesFromSchema(opts.variableSchema ?? {}),
|
|
102
|
+
resourceKeys: Object.keys(opts.linkedResources ?? {}),
|
|
103
|
+
});
|
|
104
|
+
const issues = evaluated.ok ? evaluated.publishIssues : evaluated.issues;
|
|
105
|
+
if (!evaluated.ok || issues.length > 0) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`${label} rejected: ${issues.map((issue) => `${issue.code} (${issue.params?.["detail"] ?? issue.path})`).join("; ")}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
82
111
|
const existingRow = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
|
|
83
112
|
tenantId,
|
|
84
113
|
slug: opts.slug,
|
|
@@ -93,17 +122,20 @@ export async function seedSystemTemplate(
|
|
|
93
122
|
);
|
|
94
123
|
|
|
95
124
|
const variableSchema = JSON.stringify(opts.variableSchema ?? {});
|
|
96
|
-
const
|
|
125
|
+
const meta = evaluated.metaJson;
|
|
126
|
+
const linkedResources =
|
|
127
|
+
opts.linkedResources === undefined ? undefined : JSON.stringify(opts.linkedResources);
|
|
97
128
|
const parentTemplateId = opts.parentTemplateId ?? null;
|
|
98
129
|
|
|
99
130
|
const rowFields = {
|
|
100
131
|
slug: opts.slug,
|
|
101
132
|
kind: opts.kind,
|
|
102
133
|
locale: opts.locale,
|
|
103
|
-
content:
|
|
134
|
+
content: evaluated.content,
|
|
104
135
|
contentFormat: opts.contentFormat,
|
|
105
136
|
variableSchema,
|
|
106
|
-
linkedResources,
|
|
137
|
+
...(linkedResources !== undefined && { linkedResources }),
|
|
138
|
+
meta,
|
|
107
139
|
title: null,
|
|
108
140
|
folder: null,
|
|
109
141
|
scope: "system" as const,
|
|
@@ -111,9 +143,28 @@ export async function seedSystemTemplate(
|
|
|
111
143
|
status: "active" as const,
|
|
112
144
|
};
|
|
113
145
|
|
|
114
|
-
const applyUpdate = async (
|
|
146
|
+
const applyUpdate = async (
|
|
147
|
+
row: { id: string; version: number },
|
|
148
|
+
current: TemplateResourceRow | null | undefined,
|
|
149
|
+
): Promise<{ id: string }> => {
|
|
150
|
+
// Skip no-op updates so re-seeding on every pod boot does not bump
|
|
151
|
+
// version/modifiedAt or grow the event store.
|
|
152
|
+
if (
|
|
153
|
+
current?.content === rowFields.content &&
|
|
154
|
+
current?.contentFormat === rowFields.contentFormat &&
|
|
155
|
+
current?.title === rowFields.title &&
|
|
156
|
+
current?.variableSchema === rowFields.variableSchema &&
|
|
157
|
+
current?.meta === rowFields.meta &&
|
|
158
|
+
(linkedResources === undefined || current?.linkedResources === linkedResources)
|
|
159
|
+
) {
|
|
160
|
+
return { id: String(row.id) };
|
|
161
|
+
}
|
|
115
162
|
const result = await executor.update(
|
|
116
|
-
{
|
|
163
|
+
{
|
|
164
|
+
id: row.id,
|
|
165
|
+
version: row.version,
|
|
166
|
+
changes: { ...rowFields, ...activeStampChanges(row) },
|
|
167
|
+
},
|
|
117
168
|
by,
|
|
118
169
|
tdb,
|
|
119
170
|
);
|
|
@@ -127,7 +178,17 @@ export async function seedSystemTemplate(
|
|
|
127
178
|
existing,
|
|
128
179
|
ifExists: opts.ifExists,
|
|
129
180
|
create: async () => {
|
|
130
|
-
const result = await executor.create(
|
|
181
|
+
const result = await executor.create(
|
|
182
|
+
{
|
|
183
|
+
...rowFields,
|
|
184
|
+
...activeStampChanges(null),
|
|
185
|
+
draft: null,
|
|
186
|
+
linkedResources: linkedResources ?? "{}",
|
|
187
|
+
tenantId,
|
|
188
|
+
},
|
|
189
|
+
by,
|
|
190
|
+
tdb,
|
|
191
|
+
);
|
|
131
192
|
if (!result.isSuccess) {
|
|
132
193
|
// Parallel boot pods can both delete the orphan and race on create —
|
|
133
194
|
// unique on (tenantId, slug, kind, locale) → resolve winner and update.
|
|
@@ -152,7 +213,7 @@ export async function seedSystemTemplate(
|
|
|
152
213
|
existing: resolved,
|
|
153
214
|
ifExists: opts.ifExists,
|
|
154
215
|
create: () => Promise.reject(new Error("seedSystemTemplate: unreachable create")),
|
|
155
|
-
update: applyUpdate,
|
|
216
|
+
update: (row) => applyUpdate(row, again),
|
|
156
217
|
});
|
|
157
218
|
}
|
|
158
219
|
throw new Error(`seedSystemTemplate create failed: ${JSON.stringify(result)}`);
|
|
@@ -163,10 +224,90 @@ export async function seedSystemTemplate(
|
|
|
163
224
|
}
|
|
164
225
|
return { id: String(data.id) };
|
|
165
226
|
},
|
|
166
|
-
update: applyUpdate,
|
|
227
|
+
update: (row) => applyUpdate(row, existingRow),
|
|
167
228
|
});
|
|
168
229
|
}
|
|
169
230
|
|
|
231
|
+
export type SeedSystemTemplateBundleOptions = {
|
|
232
|
+
readonly by?: SessionUser;
|
|
233
|
+
readonly ifExists?: SeedIfExists;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Seeds one system row per bundle locale. Bundled resources need a file
|
|
237
|
+
// provider and the system tenant's file space, which a boot seed does not
|
|
238
|
+
// have, so those go through the import-bundle-system write instead.
|
|
239
|
+
export async function seedSystemTemplateBundle(
|
|
240
|
+
db: DbConnection,
|
|
241
|
+
bundle: TemplateBundle,
|
|
242
|
+
opts: SeedSystemTemplateBundleOptions = {},
|
|
243
|
+
): Promise<{ ids: Record<string, string> }> {
|
|
244
|
+
if (Object.keys(bundle.resources).length > 0) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`seedSystemTemplateBundle ${bundle.slug}/${bundle.kind}: bundles with resources cannot be seeded; use the import-bundle-system write`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const ids: Record<string, string> = {};
|
|
250
|
+
for (const [locale, entry] of Object.entries(bundle.locales)) {
|
|
251
|
+
const seeded = await seedSystemTemplate(db, {
|
|
252
|
+
slug: bundle.slug,
|
|
253
|
+
kind: bundle.kind,
|
|
254
|
+
locale,
|
|
255
|
+
content: entry.body,
|
|
256
|
+
contentFormat: bundle.contentFormat,
|
|
257
|
+
variableSchema: bundle.variableSchema ?? {},
|
|
258
|
+
meta: entry.meta,
|
|
259
|
+
...(opts.by !== undefined ? { by: opts.by } : {}),
|
|
260
|
+
...(opts.ifExists !== undefined ? { ifExists: opts.ifExists } : {}),
|
|
261
|
+
});
|
|
262
|
+
ids[locale] = seeded.id;
|
|
263
|
+
}
|
|
264
|
+
return { ids };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function hasNotificationTemplates(registry: Registry): boolean {
|
|
268
|
+
return Array.from(registry.getAllNotifications().values()).some(
|
|
269
|
+
(notification) => notification.template !== undefined,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Boot seed for notifications that ship their mail text with the code
|
|
274
|
+
// (`r.notification({ template, meta, variables })`). The code is authoritative,
|
|
275
|
+
// so existing system rows are updated; tenant overrides are untouched.
|
|
276
|
+
export async function seedNotificationTemplates(
|
|
277
|
+
db: DbConnection,
|
|
278
|
+
registry: Registry,
|
|
279
|
+
): Promise<void> {
|
|
280
|
+
const hasTemplates = hasNotificationTemplates(registry);
|
|
281
|
+
if (hasTemplates && registry.getFeature(TEMPLATE_RESOLVER_FEATURE) === undefined) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Notifications declare a mail template but the "${TEMPLATE_RESOLVER_FEATURE}" feature is not mounted`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (hasTemplates) {
|
|
287
|
+
// Dynamic: renderer-foundation imports this feature's barrel, a static import would cycle.
|
|
288
|
+
const { collectRendererPlugins } = await import("../renderer-foundation/index.js");
|
|
289
|
+
if (!collectRendererPlugins(registry).some((plugin) => plugin.kinds.includes("mail"))) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
'Notifications declare a mail template but no renderer plugin for kind "mail" is registered (mount renderer-foundation and @cosmicdrift/kumiko-renderer-mail-html)',
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const [slug, notification] of registry.getAllNotifications()) {
|
|
296
|
+
for (const [locale, { body }] of Object.entries(notification.template ?? {})) {
|
|
297
|
+
await seedSystemTemplate(db, {
|
|
298
|
+
slug,
|
|
299
|
+
kind: "mail",
|
|
300
|
+
locale,
|
|
301
|
+
content: body,
|
|
302
|
+
contentFormat: "html",
|
|
303
|
+
meta: notification.meta?.[locale],
|
|
304
|
+
variableSchema: notification.variables,
|
|
305
|
+
ifExists: "update",
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
170
311
|
export type SeedTextBlockOptions = {
|
|
171
312
|
readonly tenantId: TenantId;
|
|
172
313
|
readonly slug: string;
|
|
@@ -236,7 +377,13 @@ export async function seedTextBlock(
|
|
|
236
377
|
{
|
|
237
378
|
id: row.id,
|
|
238
379
|
version: row.version,
|
|
239
|
-
changes: {
|
|
380
|
+
changes: {
|
|
381
|
+
title: fields.title,
|
|
382
|
+
content: fields.content,
|
|
383
|
+
folder: fields.folder,
|
|
384
|
+
// A re-seed must not revive an archived block; only a served one gets a new served version.
|
|
385
|
+
...(current?.status === "active" && servedStampChanges(row)),
|
|
386
|
+
},
|
|
240
387
|
},
|
|
241
388
|
by,
|
|
242
389
|
tdb,
|
|
@@ -251,7 +398,11 @@ export async function seedTextBlock(
|
|
|
251
398
|
existing,
|
|
252
399
|
ifExists: opts.ifExists,
|
|
253
400
|
create: async () => {
|
|
254
|
-
const result = await executor.create(
|
|
401
|
+
const result = await executor.create(
|
|
402
|
+
{ ...fields, ...activeStampChanges(null), draft: null, tenantId: opts.tenantId },
|
|
403
|
+
by,
|
|
404
|
+
tdb,
|
|
405
|
+
);
|
|
255
406
|
if (!result.isSuccess) {
|
|
256
407
|
if (result.error?.code === "unique_violation") {
|
|
257
408
|
const again = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
|
|
@@ -306,7 +457,7 @@ export async function seedLegalContentFromJson(
|
|
|
306
457
|
blocks: readonly LegalContentBlock[],
|
|
307
458
|
opts: { readonly tenantId?: TenantId; readonly by?: SessionUser } = {},
|
|
308
459
|
): Promise<void> {
|
|
309
|
-
const tenantId = opts.tenantId ??
|
|
460
|
+
const tenantId = opts.tenantId ?? SYSTEM_TENANT_ID;
|
|
310
461
|
const by = opts.by ?? createSystemUser(tenantId);
|
|
311
462
|
for (const block of blocks) {
|
|
312
463
|
await seedTextBlock(db, {
|