@cosmicdrift/kumiko-samples 0.362.0 → 0.363.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +17 -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/auth-email-password/changes.json +14 -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/invite-create.write.ts +4 -5
- 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/token-request-handler.ts +3 -12
- 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-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 +7 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +38 -44
- 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 +115 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +30 -30
- 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-request.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
- 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 +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +3 -6
- 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 +122 -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/handlers/set.write.ts +1 -5
- package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
- 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 +84 -0
- package/packages/bundled-features/src/delivery/changes.json +48 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +129 -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 +17 -2
- package/packages/bundled-features/src/delivery/jobs.ts +48 -5
- 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 +28 -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-foundation/index.ts +7 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -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/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-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 +40 -25
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
- package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -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 +132 -35
- package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
- 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/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 +25 -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/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 +63 -7
- package/packages/bundled-features/src/template-resolver/api.ts +83 -8
- package/packages/bundled-features/src/template-resolver/changes.json +118 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +56 -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 +142 -0
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
- 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 +108 -0
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -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 +106 -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 +184 -0
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
- 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 +15 -76
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
- package/packages/bundled-features/src/template-resolver/index.ts +100 -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/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 +30 -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 +147 -17
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
- package/packages/bundled-features/src/template-resolver/table.ts +14 -0
- 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-meta.ts +129 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
- package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
- package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
- package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
- package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +19 -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 +169 -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/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/roles-input-schema.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/changes.json +7 -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/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/workflow-runner/event-trigger.ts +1 -2
- package/samples/apps/ui-walkthrough/package.json +2 -0
- 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 +154 -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 +354 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -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 +37 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +115 -20
- 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
|
@@ -1,4 +1,52 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.363.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "NotificationRenderer and RendererInput removed, immediate notify renders with the tenant config",
|
|
6
|
+
"detail": "The inline (immediate, job-less) delivery path now gets the tenant config accessor like the delivery.render job, so the mail layout shows the tenant name in both paths. DeliveryServiceOptions and CreateDeliveryTestContextOptions accept tenantConfigFor; runProdApp wires it from the boot config resolver. The types NotificationRenderer and RendererInput are removed from the delivery exports because the legacy renderer path is gone.",
|
|
7
|
+
"migration": "Drop NotificationRenderer and RendererInput imports; render mails with template + meta on r.notification (see renderer-foundation)."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.363.0",
|
|
11
|
+
"type": "improvement",
|
|
12
|
+
"title": "ChannelContext.renderMail renders template notifications for the email channel",
|
|
13
|
+
"detail": "A notification that declares a template is rendered through the mail renderer plugin with the recipient locale and the notification variables; the email channel fails when renderMail is missing instead of falling back to the legacy renderer. The job path passes the tenant config accessor to the renderer, the inline path renders without one."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "0.363.0",
|
|
17
|
+
"type": "improvement",
|
|
18
|
+
"title": "Mails can attach a PDF that is rendered from a document template when the mail is sent",
|
|
19
|
+
"detail": "notify data.attachments accepts { kind: \"rendered-pdf\", templateSlug, variables, fileName, locale? }. The send job renders the PDF through the document-pdf renderer, so the job payload carries the variables (at most 64000 bytes as JSON), not the bytes. Variables are stored in the job payload, so keep personal data out of them or accept that retention."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"version": "0.363.0",
|
|
23
|
+
"type": "improvement",
|
|
24
|
+
"title": "ChannelContext.readAttachment takes an attachment origin",
|
|
25
|
+
"detail": "readAttachment(ref, origin) with origin \"template\" (from the renderer) or \"message\" (default, from notify data). Only template attachments may fall back to the system tenant; file refs from notify data stay limited to the sending tenant."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"version": "0.363.0",
|
|
29
|
+
"type": "improvement",
|
|
30
|
+
"title": "EMAIL_CHANNEL_NAME, createTemplateMailRenderer and createEmailAttachmentReader are exported",
|
|
31
|
+
"detail": "The email channel name is a shared constant instead of a string literal. The mail renderer and the inline-attachment reader that build the channel context are exported and accept a tenant-bound db, so a handler can send through the same path as a notification."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"version": "0.363.0",
|
|
35
|
+
"type": "improvement",
|
|
36
|
+
"title": "Notification messages carry attachments (file refs and inline cid images)",
|
|
37
|
+
"detail": "RenderedMessage and the send job payload accept attachments as references (kind file or inline), never bytes. ChannelContext.readAttachment resolves them tenant-scoped at send time; createDeliveryService takes a new filesForTenant option for the inline fallback path."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"version": "0.363.0",
|
|
41
|
+
"type": "improvement",
|
|
42
|
+
"title": "NotifyFactory takes an optional fileProviderResolver, so immediate and bootstrap mails can read attachments too"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"version": "0.363.0",
|
|
46
|
+
"type": "fix",
|
|
47
|
+
"title": "Queued mails render with the tenant's config again",
|
|
48
|
+
"detail": "The delivery render and send jobs read the tenant config through ctx.configFor, which job contexts do not carry, so queued mails rendered without tenant name, locale, currency and branding. Both jobs now use ctx.config, which the job runner builds for the payload's tenant."
|
|
49
|
+
},
|
|
2
50
|
{
|
|
3
51
|
"version": "0.357.0",
|
|
4
52
|
"type": "improvement",
|
|
@@ -1,8 +1,115 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
-
import { createTenantDb, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { createTenantDb, type DbConnection, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { ConfigAccessor, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
|
+
import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
|
|
4
6
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
5
|
-
import
|
|
7
|
+
import { readFileRefContent } from "../file-foundation/index.js";
|
|
8
|
+
import {
|
|
9
|
+
collectRendererPlugins,
|
|
10
|
+
createRendererFoundationApi,
|
|
11
|
+
type MailPayload,
|
|
12
|
+
RendererError,
|
|
13
|
+
} from "../renderer-foundation/index.js";
|
|
14
|
+
import { FALLBACK_LOCALE } from "../template-resolver/index.js";
|
|
15
|
+
import type { EmailAttachmentRef, RenderedPdfAttachmentRef } from "./attachment-ref.js";
|
|
16
|
+
import type { AttachmentOrigin, ChannelContext, RenderedMail } from "./types.js";
|
|
17
|
+
|
|
18
|
+
export function createTemplateMailRenderer(
|
|
19
|
+
db: DbConnection | TenantDb,
|
|
20
|
+
registry: Registry,
|
|
21
|
+
tenantId: TenantId,
|
|
22
|
+
config: ConfigAccessor | undefined,
|
|
23
|
+
): (payload: MailPayload) => Promise<RenderedMail> {
|
|
24
|
+
return async (payload) => {
|
|
25
|
+
const plugin = createRendererFoundationApi(
|
|
26
|
+
collectRendererPlugins(registry),
|
|
27
|
+
).createRendererForTenant({ tenantId, kind: "mail" });
|
|
28
|
+
const response = await plugin.render(
|
|
29
|
+
{ kind: "mail", payload },
|
|
30
|
+
{ db, registry, tenantId, ...(config !== undefined && { config }) },
|
|
31
|
+
);
|
|
32
|
+
if (response.kind !== "mail") {
|
|
33
|
+
throw new RendererError(
|
|
34
|
+
`[delivery] renderer "${plugin.name}" answered a mail request with kind "${response.kind}"`,
|
|
35
|
+
"other",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
subject: response.subject,
|
|
40
|
+
html: response.html,
|
|
41
|
+
text: response.text,
|
|
42
|
+
...(response.previewText !== undefined && { previewText: response.previewText }),
|
|
43
|
+
...(response.attachments !== undefined && { attachments: response.attachments }),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const INLINE_IMAGE_CONTENT_TYPE_PREFIX = "image/";
|
|
49
|
+
const PDF_CONTENT_TYPE = "application/pdf";
|
|
50
|
+
|
|
51
|
+
// Renders at send time so the job payload carries variables, not bytes. A failure throws:
|
|
52
|
+
// the send job retries instead of mailing without the document the recipient expects.
|
|
53
|
+
function createPdfRenderer(
|
|
54
|
+
db: DbConnection | TenantDb,
|
|
55
|
+
registry: Registry,
|
|
56
|
+
tenantId: TenantId,
|
|
57
|
+
config: ConfigAccessor | undefined,
|
|
58
|
+
filesForTenant: (tenantId: string) => FileContext | undefined,
|
|
59
|
+
) {
|
|
60
|
+
return async (ref: RenderedPdfAttachmentRef) => {
|
|
61
|
+
const plugin = createRendererFoundationApi(
|
|
62
|
+
collectRendererPlugins(registry),
|
|
63
|
+
).createRendererForTenant({ tenantId, kind: "document-pdf" });
|
|
64
|
+
const response = await plugin.render(
|
|
65
|
+
{
|
|
66
|
+
kind: "document-pdf",
|
|
67
|
+
payload: {
|
|
68
|
+
slug: ref.templateSlug,
|
|
69
|
+
locale: ref.locale ?? FALLBACK_LOCALE,
|
|
70
|
+
variables: ref.variables,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{ db, registry, tenantId, filesForTenant, ...(config !== undefined && { config }) },
|
|
74
|
+
);
|
|
75
|
+
if (response.kind !== "document-pdf") {
|
|
76
|
+
throw new RendererError(
|
|
77
|
+
`[delivery] renderer "${plugin.name}" answered a document-pdf request with kind "${response.kind}"`,
|
|
78
|
+
"other",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return { content: response.pdfBytes, contentType: PDF_CONTENT_TYPE };
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Inline images and file attachments the renderer took from a template may come from the system
|
|
86
|
+
// tenant (system template or tenant override of one). A file ref from notify data stays limited to
|
|
87
|
+
// the job's own tenant, since a caller picks it. Inline content that is not an image is dropped, so
|
|
88
|
+
// a swapped fileRef id cannot smuggle a document into the mail body.
|
|
89
|
+
export function createEmailAttachmentReader(
|
|
90
|
+
db: DbConnection | TenantDb,
|
|
91
|
+
registry: Registry,
|
|
92
|
+
tenantId: TenantId,
|
|
93
|
+
config: ConfigAccessor | undefined,
|
|
94
|
+
filesForTenant: (tenantId: string) => FileContext | undefined,
|
|
95
|
+
): NonNullable<ChannelContext["readAttachment"]> {
|
|
96
|
+
const renderPdf = createPdfRenderer(db, registry, tenantId, config, filesForTenant);
|
|
97
|
+
return async (ref: EmailAttachmentRef, origin: AttachmentOrigin = "message") => {
|
|
98
|
+
if (ref.kind === "rendered-pdf") return renderPdf(ref);
|
|
99
|
+
const found = await readFileRefContent({
|
|
100
|
+
db,
|
|
101
|
+
filesForTenant,
|
|
102
|
+
fileRefId: ref.fileRefId,
|
|
103
|
+
allowedTenantIds:
|
|
104
|
+
ref.kind === "file" && origin === "message" ? [tenantId] : [tenantId, SYSTEM_TENANT_ID],
|
|
105
|
+
});
|
|
106
|
+
if (found === null) return null;
|
|
107
|
+
if (ref.kind === "inline" && !found.contentType.startsWith(INLINE_IMAGE_CONTENT_TYPE_PREFIX)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return { content: found.content, contentType: found.contentType };
|
|
111
|
+
};
|
|
112
|
+
}
|
|
6
113
|
|
|
7
114
|
// Build the per-tenant context a channel's resolve/render/send receives.
|
|
8
115
|
// Shared by the synchronous delivery-service path and the async job handlers
|
|
@@ -14,6 +121,24 @@ export function buildChannelContext(
|
|
|
14
121
|
sseBroker: SseBroker | undefined,
|
|
15
122
|
tenantId: TenantId,
|
|
16
123
|
secrets?: SecretsContext,
|
|
124
|
+
tenantConfig?: ConfigAccessor,
|
|
125
|
+
filesForTenant?: (tenantId: string) => FileContext | undefined,
|
|
17
126
|
): ChannelContext {
|
|
18
|
-
return {
|
|
127
|
+
return {
|
|
128
|
+
db: createTenantDb(db, tenantId),
|
|
129
|
+
registry,
|
|
130
|
+
sseBroker,
|
|
131
|
+
tenantId,
|
|
132
|
+
secrets,
|
|
133
|
+
renderMail: createTemplateMailRenderer(db, registry, tenantId, tenantConfig),
|
|
134
|
+
...(filesForTenant !== undefined && {
|
|
135
|
+
readAttachment: createEmailAttachmentReader(
|
|
136
|
+
db,
|
|
137
|
+
registry,
|
|
138
|
+
tenantId,
|
|
139
|
+
tenantConfig,
|
|
140
|
+
filesForTenant,
|
|
141
|
+
),
|
|
142
|
+
}),
|
|
143
|
+
};
|
|
19
144
|
}
|
|
@@ -6,6 +6,8 @@ import { DELIVERY_FEATURE, DeliveryJobNames } from "./public-names.js";
|
|
|
6
6
|
// channel-in-app, channel-push, ...).
|
|
7
7
|
export const DELIVERY_CHANNEL_EXTENSION = "deliveryChannel" as const;
|
|
8
8
|
|
|
9
|
+
export const EMAIL_CHANNEL_NAME = "email" as const;
|
|
10
|
+
|
|
9
11
|
export {
|
|
10
12
|
DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT,
|
|
11
13
|
DELIVERY_ATTEMPT_EVENT,
|
|
@@ -2,22 +2,32 @@ import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
|
2
2
|
import type { DbConnection, DbRow } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import { createSystemDbView, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
4
4
|
import type {
|
|
5
|
+
ConfigAccessor,
|
|
5
6
|
DeliveryErrorCode,
|
|
6
7
|
EscapeHatchAuditSink,
|
|
7
8
|
NotifyDelivery,
|
|
9
|
+
NotifyDispatched,
|
|
8
10
|
NotifyJobDispatcher,
|
|
9
11
|
NotifyOptions,
|
|
10
12
|
NotifyPriority,
|
|
11
13
|
Registry,
|
|
12
14
|
TenantId,
|
|
13
15
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
14
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
assertNotificationVariables,
|
|
18
|
+
createSystemUser,
|
|
19
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
20
|
+
import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
|
|
15
21
|
import { createFallbackLogger, type Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
16
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
assertNotifyTargetsOwnTenant,
|
|
24
|
+
createEscapeHatchReporter,
|
|
25
|
+
} from "@cosmicdrift/kumiko-framework/pipeline";
|
|
17
26
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
18
27
|
import { bridgeStub } from "@cosmicdrift/kumiko-framework/testing/handler-context";
|
|
19
28
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
20
29
|
import type { Redis } from "ioredis";
|
|
30
|
+
import { fileContextFromResolver } from "../file-foundation/index.js";
|
|
21
31
|
import { redactErrorText } from "../shared/index.js";
|
|
22
32
|
import { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
23
33
|
import { appendAttemptEvent, logAttempt } from "./attempt-log.js";
|
|
@@ -77,6 +87,11 @@ export type DeliveryServiceOptions = {
|
|
|
77
87
|
// Credentials source for inline-delivered chat channels (webhook URL / bot
|
|
78
88
|
// token). Jobs get ctx.secrets from the job runner instead.
|
|
79
89
|
readonly secrets?: SecretsContext;
|
|
90
|
+
// File storage for inline-delivered mails with attachments; jobs use their own ctx instead.
|
|
91
|
+
readonly filesForTenant?: (tenantId: string) => FileContext | undefined;
|
|
92
|
+
// Tenant config for inline rendering (mail layout reads tenant name, locale);
|
|
93
|
+
// job-delivered mails get the same accessor from the job context's configFor.
|
|
94
|
+
readonly tenantConfigFor?: (tenantId: TenantId) => ConfigAccessor;
|
|
80
95
|
// Attributed escape-hatch audit for resolveUserIdsForTenant's ctx.systemDb —
|
|
81
96
|
// absent means an unattributed warn log (see fallbackEscapeHatchReporter).
|
|
82
97
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
@@ -96,11 +111,6 @@ export function collectChannels(registry: Registry): DeliveryChannel[] {
|
|
|
96
111
|
});
|
|
97
112
|
}
|
|
98
113
|
|
|
99
|
-
// `collectRenderers` entfernt 2026-05-19: notificationRenderer-Extension-Point
|
|
100
|
-
// wurde nie konsumiert (channel-email nimmt renderer als Konstruktor-Option,
|
|
101
|
-
// nicht aus Extension-Usages). Multi-Kind-Plugin-Pool lebt jetzt im
|
|
102
|
-
// `renderer-foundation`-Bundle via `collectRendererPlugins`.
|
|
103
|
-
|
|
104
114
|
export function createDeliveryService(options: DeliveryServiceOptions): DeliveryService {
|
|
105
115
|
const {
|
|
106
116
|
db,
|
|
@@ -113,6 +123,8 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
113
123
|
idempotencyRedis,
|
|
114
124
|
jobRunner,
|
|
115
125
|
secrets,
|
|
126
|
+
filesForTenant,
|
|
127
|
+
tenantConfigFor,
|
|
116
128
|
escapeHatchAuditSink,
|
|
117
129
|
log,
|
|
118
130
|
} = options;
|
|
@@ -214,9 +226,14 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
214
226
|
return routedChannelCount === 1 ? idempotencyKey : undefined;
|
|
215
227
|
}
|
|
216
228
|
|
|
217
|
-
async function resolveUserIdsForTenant(
|
|
229
|
+
async function resolveUserIdsForTenant(
|
|
230
|
+
run: NotifyRun,
|
|
231
|
+
tenantId: TenantId,
|
|
232
|
+
): Promise<readonly string[]> {
|
|
218
233
|
if (!tenantUserIdsQuery) {
|
|
219
|
-
throw new Error(
|
|
234
|
+
throw new Error(
|
|
235
|
+
"Tenant broadcast (to: { tenant }) requires the tenant feature (default wiring) or an explicit tenantUserIdsQuery in DeliveryServiceOptions",
|
|
236
|
+
);
|
|
220
237
|
}
|
|
221
238
|
const handler = registry.getQueryHandler(tenantUserIdsQuery);
|
|
222
239
|
if (!handler) {
|
|
@@ -235,7 +252,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
235
252
|
// path a cross-tenant read the dispatcher never allows. Cross-tenant reach only
|
|
236
253
|
// exists through the grant-carrying systemDb view below.
|
|
237
254
|
const isSystem = registry.isHandlerSystemScoped(tenantUserIdsQuery);
|
|
238
|
-
const tenantDb = createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
|
|
255
|
+
const tenantDb = createTenantDb(run.db, tenantId, "tenant", undefined, undefined, undefined, {
|
|
239
256
|
report,
|
|
240
257
|
});
|
|
241
258
|
// The dispatcher grants a systemScope handler's ctx.systemDb.unsafeRaw() ungated —
|
|
@@ -243,7 +260,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
243
260
|
// caller. Only the systemDb view's own source TenantDb carries the grant, so it
|
|
244
261
|
// can't leak onto ctx.db.
|
|
245
262
|
const systemDbSourceTenantDb = isSystem
|
|
246
|
-
? createTenantDb(db, tenantId, "system", undefined, undefined, undefined, {
|
|
263
|
+
? createTenantDb(run.db, tenantId, "system", undefined, undefined, undefined, {
|
|
247
264
|
report,
|
|
248
265
|
unsafeRaw: { reason: `r.systemScope() handler "${tenantUserIdsQuery}"` },
|
|
249
266
|
})
|
|
@@ -268,12 +285,16 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
268
285
|
// State of one notify() call: collects the per-channel outcome returned to
|
|
269
286
|
// the caller. Passed explicitly — the service itself is shared across calls.
|
|
270
287
|
type NotifyRun = {
|
|
288
|
+
// All DB work of the call runs here: inside a write transaction the caller passes the
|
|
289
|
+
// outside-tx pool, because the app pool's slots may all be held by waiting write txs.
|
|
290
|
+
readonly db: DbConnection;
|
|
271
291
|
readonly deliveries: NotifyDelivery[];
|
|
272
292
|
readonly jobDispatcher: NotifyJobDispatcher | undefined;
|
|
273
293
|
readonly locale: string | undefined;
|
|
274
294
|
// Set only when exactly one channel carries the call: the queued job may then free
|
|
275
295
|
// the key on terminal failure without risking a duplicate send on another channel.
|
|
276
296
|
readonly jobIdempotencyKey: string | undefined;
|
|
297
|
+
readonly filesForTenant: ((tenantId: string) => FileContext | undefined) | undefined;
|
|
277
298
|
};
|
|
278
299
|
|
|
279
300
|
function recordDelivery(
|
|
@@ -295,7 +316,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
295
316
|
// attempts instead append a queued event up front and a terminal event from
|
|
296
317
|
// the send job — see deliverViaChannel + jobs.ts.
|
|
297
318
|
async function logDelivery(run: NotifyRun, entry: DeliveryLogEntry): Promise<void> {
|
|
298
|
-
const attemptId = await logAttempt(db, registry, entry);
|
|
319
|
+
const attemptId = await logAttempt(run.db, registry, entry);
|
|
299
320
|
recordDelivery(run, entry, attemptId);
|
|
300
321
|
}
|
|
301
322
|
|
|
@@ -372,7 +393,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
372
393
|
error: null,
|
|
373
394
|
priority,
|
|
374
395
|
};
|
|
375
|
-
await appendAttemptEvent(db, registry, deliveryAttemptId, queuedEntry);
|
|
396
|
+
await appendAttemptEvent(run.db, registry, deliveryAttemptId, queuedEntry);
|
|
376
397
|
await run.jobDispatcher.dispatch(
|
|
377
398
|
channel.render ? DeliveryJobs.render : DeliveryJobs.send,
|
|
378
399
|
{
|
|
@@ -460,6 +481,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
460
481
|
// no blind-index key configured means no hash, so nothing to look up. One
|
|
461
482
|
// query per notify() call, not per recipient and channel.
|
|
462
483
|
async function loadSuppressedAddresses(
|
|
484
|
+
run: NotifyRun,
|
|
463
485
|
tenantId: TenantId,
|
|
464
486
|
notificationType: string,
|
|
465
487
|
priority: NotifyPriority,
|
|
@@ -472,7 +494,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
472
494
|
const addressHash = hashUnsubscribeAddress(address);
|
|
473
495
|
if (addressHash !== undefined) addressHashes.add(addressHash);
|
|
474
496
|
}
|
|
475
|
-
return selectOptedOutAddresses(db, tenantId, notificationType, [...addressHashes]);
|
|
497
|
+
return selectOptedOutAddresses(run.db, tenantId, notificationType, [...addressHashes]);
|
|
476
498
|
}
|
|
477
499
|
|
|
478
500
|
function isAddressSuppressed(
|
|
@@ -487,13 +509,14 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
487
509
|
const recipientChannelKey = (userId: string, channelName: string) => `${userId}|${channelName}`;
|
|
488
510
|
|
|
489
511
|
async function isChannelDeliverable(
|
|
512
|
+
run: NotifyRun,
|
|
490
513
|
userId: string,
|
|
491
514
|
tenantId: TenantId,
|
|
492
515
|
notificationType: string,
|
|
493
516
|
channelName: string,
|
|
494
517
|
): Promise<boolean> {
|
|
495
518
|
if (isChannelKilled && (await isChannelKilled(tenantId, channelName))) return false;
|
|
496
|
-
return isChannelEnabled(userId, tenantId, notificationType, channelName);
|
|
519
|
+
return isChannelEnabled(run, userId, tenantId, notificationType, channelName);
|
|
497
520
|
}
|
|
498
521
|
|
|
499
522
|
// Resolves every user's channel addresses up front so the opt-out lookup can
|
|
@@ -502,17 +525,28 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
502
525
|
// Channels the kill switch or the user's preferences disable are never resolved
|
|
503
526
|
// here: resolve may have side effects and deliverToUser skips those channels.
|
|
504
527
|
async function prefetchUserAddresses(
|
|
528
|
+
run: NotifyRun,
|
|
505
529
|
userIds: readonly string[],
|
|
506
530
|
tenantId: TenantId,
|
|
507
531
|
notificationType: string,
|
|
508
532
|
): Promise<Map<string, string | null>> {
|
|
509
533
|
const addresses = new Map<string, string | null>();
|
|
510
|
-
const channelCtx = buildChannelContext(
|
|
534
|
+
const channelCtx = buildChannelContext(
|
|
535
|
+
run.db,
|
|
536
|
+
registry,
|
|
537
|
+
sseBroker,
|
|
538
|
+
tenantId,
|
|
539
|
+
secrets,
|
|
540
|
+
tenantConfigFor?.(tenantId),
|
|
541
|
+
run.filesForTenant,
|
|
542
|
+
);
|
|
511
543
|
for (const userId of userIds) {
|
|
512
544
|
for (const channel of channels) {
|
|
513
545
|
if (!channel.resolve) continue;
|
|
514
546
|
try {
|
|
515
|
-
if (
|
|
547
|
+
if (
|
|
548
|
+
!(await isChannelDeliverable(run, userId, tenantId, notificationType, channel.name))
|
|
549
|
+
) {
|
|
516
550
|
continue;
|
|
517
551
|
}
|
|
518
552
|
addresses.set(
|
|
@@ -537,13 +571,14 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
537
571
|
// { type: "orderAssigned", channel: "*", enabled: true } enables orderAssigned everywhere
|
|
538
572
|
// → orderAssigned on inApp: disabled (conservative) unless an exact entry overrides.
|
|
539
573
|
async function isChannelEnabled(
|
|
574
|
+
run: NotifyRun,
|
|
540
575
|
userId: string,
|
|
541
576
|
tenantId: TenantId,
|
|
542
577
|
notificationType: string,
|
|
543
578
|
channelName: string,
|
|
544
579
|
): Promise<boolean> {
|
|
545
580
|
const prefs = await selectNotificationPreferences(
|
|
546
|
-
db,
|
|
581
|
+
run.db,
|
|
547
582
|
tenantId,
|
|
548
583
|
userId,
|
|
549
584
|
notificationType,
|
|
@@ -573,7 +608,15 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
573
608
|
prefetchedAddresses: ReadonlyMap<string, string | null>,
|
|
574
609
|
suppressed: ReadonlySet<AddressOptOutKey>,
|
|
575
610
|
): Promise<void> {
|
|
576
|
-
const channelCtx = buildChannelContext(
|
|
611
|
+
const channelCtx = buildChannelContext(
|
|
612
|
+
run.db,
|
|
613
|
+
registry,
|
|
614
|
+
sseBroker,
|
|
615
|
+
tenantId,
|
|
616
|
+
secrets,
|
|
617
|
+
tenantConfigFor?.(tenantId),
|
|
618
|
+
run.filesForTenant,
|
|
619
|
+
);
|
|
577
620
|
|
|
578
621
|
for (const channel of channels) {
|
|
579
622
|
// Route-only channel (no per-user address): not a user-notification target.
|
|
@@ -601,7 +644,13 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
601
644
|
|
|
602
645
|
// Check preferences (critical priority skips preference check)
|
|
603
646
|
if (priority !== "critical") {
|
|
604
|
-
const enabled = await isChannelEnabled(
|
|
647
|
+
const enabled = await isChannelEnabled(
|
|
648
|
+
run,
|
|
649
|
+
userId,
|
|
650
|
+
tenantId,
|
|
651
|
+
notificationType,
|
|
652
|
+
channel.name,
|
|
653
|
+
);
|
|
605
654
|
if (!enabled) {
|
|
606
655
|
await logDelivery(run, {
|
|
607
656
|
tenantId,
|
|
@@ -707,8 +756,17 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
707
756
|
recipientId: string | null,
|
|
708
757
|
run: NotifyRun,
|
|
709
758
|
): Promise<void> {
|
|
710
|
-
const channelCtx = buildChannelContext(
|
|
759
|
+
const channelCtx = buildChannelContext(
|
|
760
|
+
run.db,
|
|
761
|
+
registry,
|
|
762
|
+
sseBroker,
|
|
763
|
+
tenantId,
|
|
764
|
+
secrets,
|
|
765
|
+
tenantConfigFor?.(tenantId),
|
|
766
|
+
run.filesForTenant,
|
|
767
|
+
);
|
|
711
768
|
const suppressed = await loadSuppressedAddresses(
|
|
769
|
+
run,
|
|
712
770
|
tenantId,
|
|
713
771
|
notificationType,
|
|
714
772
|
priority,
|
|
@@ -815,7 +873,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
815
873
|
if (typeof to === "string") {
|
|
816
874
|
userIds = [to];
|
|
817
875
|
} else if ("tenant" in to) {
|
|
818
|
-
userIds = await resolveUserIdsForTenant(to.tenant);
|
|
876
|
+
userIds = await resolveUserIdsForTenant(run, to.tenant);
|
|
819
877
|
} else {
|
|
820
878
|
userIds = to;
|
|
821
879
|
}
|
|
@@ -823,8 +881,9 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
823
881
|
const prefetchedAddresses =
|
|
824
882
|
priority === "critical"
|
|
825
883
|
? new Map<string, string | null>()
|
|
826
|
-
: await prefetchUserAddresses(userIds, tenantId, notificationType);
|
|
884
|
+
: await prefetchUserAddresses(run, userIds, tenantId, notificationType);
|
|
827
885
|
const suppressed = await loadSuppressedAddresses(
|
|
886
|
+
run,
|
|
828
887
|
tenantId,
|
|
829
888
|
notificationType,
|
|
830
889
|
priority,
|
|
@@ -846,15 +905,34 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
846
905
|
}
|
|
847
906
|
}
|
|
848
907
|
|
|
908
|
+
function dispatched(run: NotifyRun): NotifyDispatched {
|
|
909
|
+
return { kind: "dispatched", deliveries: run.deliveries };
|
|
910
|
+
}
|
|
911
|
+
|
|
849
912
|
return {
|
|
850
|
-
async notify(
|
|
913
|
+
async notify(
|
|
914
|
+
notificationType,
|
|
915
|
+
options,
|
|
916
|
+
_user,
|
|
917
|
+
tenantId,
|
|
918
|
+
jobDispatcher,
|
|
919
|
+
deliveryDb,
|
|
920
|
+
fileProviderResolver,
|
|
921
|
+
) {
|
|
922
|
+
assertNotifyTargetsOwnTenant(options, tenantId);
|
|
923
|
+
assertNotificationVariables(registry, notificationType, options.data);
|
|
851
924
|
const { route, idempotencyKey } = options;
|
|
852
925
|
const priority: NotifyPriority = options.priority ?? "normal";
|
|
853
926
|
const run: NotifyRun = {
|
|
927
|
+
db: deliveryDb ?? db,
|
|
854
928
|
deliveries: [],
|
|
855
929
|
jobDispatcher: options.immediate ? undefined : (jobDispatcher ?? jobRunner),
|
|
856
930
|
locale: options.locale,
|
|
857
931
|
jobIdempotencyKey: jobIdempotencyKeyFor(idempotencyKey, route),
|
|
932
|
+
filesForTenant:
|
|
933
|
+
fileProviderResolver === undefined
|
|
934
|
+
? filesForTenant
|
|
935
|
+
: (fileTenantId) => fileContextFromResolver(fileProviderResolver, fileTenantId),
|
|
858
936
|
};
|
|
859
937
|
|
|
860
938
|
if (idempotencyKey) {
|
|
@@ -870,7 +948,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
870
948
|
error: "duplicate_idempotency_key",
|
|
871
949
|
priority,
|
|
872
950
|
});
|
|
873
|
-
return
|
|
951
|
+
return dispatched(run);
|
|
874
952
|
}
|
|
875
953
|
}
|
|
876
954
|
|
|
@@ -888,7 +966,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
888
966
|
throw err;
|
|
889
967
|
}
|
|
890
968
|
await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run);
|
|
891
|
-
return
|
|
969
|
+
return dispatched(run);
|
|
892
970
|
},
|
|
893
971
|
};
|
|
894
972
|
}
|
|
@@ -61,7 +61,7 @@ export function createDeliveryFeature(options?: DeliveryFeatureOptions): Feature
|
|
|
61
61
|
const attemptLogRetentionDays = resolveAttemptLogRetentionDays(options?.attemptLogRetentionDays);
|
|
62
62
|
return defineFeature("delivery", (r) => {
|
|
63
63
|
r.describe(
|
|
64
|
-
|
|
64
|
+
'The notification dispatch core: call `ctx.notify(notificationType, { to, route, data, priority, idempotencyKey })` from any handler to fan out a notification across all registered channels (email, in-app, push). Inside a write handler the delivery runs after the write has committed (the call returns `{ kind: "deferred" }`, a rolled-back write sends nothing); `immediate: true` delivers synchronously and returns `{ kind: "dispatched", deliveries }`. It stores per-user channel preferences in the `notification-preference` entity, opt-outs for no-account recipient addresses in `notification-address-opt-out` (keyed by a blind-index hash, never the plaintext address), logs every attempt to `store_delivery_attempts`, and enforces idempotency and rate-limiting \u2014 add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything. Unsubscribe/resubscribe links are served by `createUnsubscribeRoutes({ secret })` mounted via the app\'s `extraRoutes` at `/api/delivery/unsubscribe` and `/api/delivery/resubscribe`: `GET /unsubscribe?token=` renders a confirmation page (no write), `POST /unsubscribe` performs the opt-out (RFC 8058 one-click, token from the form body or query), `POST /resubscribe` undoes it (JSON `{token}` or form body only, never the query — reachable by a mail-client link prefetcher) — sign links with `signUnsubscribeToken` / `signAddressUnsubscribeToken` using the same secret. `channel-email` sets `List-Unsubscribe` / `List-Unsubscribe-Post` automatically when a message\'s `data.unsubscribeUrl` points at the unsubscribe route.',
|
|
65
65
|
);
|
|
66
66
|
r.uiHints({
|
|
67
67
|
displayLabel: "Notifications \u00b7 Dispatch Core",
|
|
@@ -1,9 +1,24 @@
|
|
|
1
|
+
export { sanitizePdfAttachmentFileName } from "../shared/index.js";
|
|
1
2
|
export { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
3
|
+
export {
|
|
4
|
+
type EmailAttachmentRef,
|
|
5
|
+
emailAttachmentRefSchema,
|
|
6
|
+
type FileAttachmentRef,
|
|
7
|
+
fileAttachmentRefSchema,
|
|
8
|
+
type InlineAttachmentRef,
|
|
9
|
+
inlineAttachmentRefSchema,
|
|
10
|
+
MAX_EMAIL_ATTACHMENTS,
|
|
11
|
+
MAX_RENDERED_PDF_VARIABLES_BYTES,
|
|
12
|
+
type RenderedPdfAttachmentRef,
|
|
13
|
+
renderedPdfAttachmentRefSchema,
|
|
14
|
+
toAttachmentRef,
|
|
15
|
+
} from "./attachment-ref.js";
|
|
2
16
|
export {
|
|
3
17
|
type AttemptLogRetentionOptions,
|
|
4
18
|
DEFAULT_ATTEMPT_LOG_RETENTION_DAYS,
|
|
5
19
|
runAttemptLogRetention,
|
|
6
20
|
} from "./attempt-log-retention.js";
|
|
21
|
+
export { createEmailAttachmentReader, createTemplateMailRenderer } from "./channel-context.js";
|
|
7
22
|
export {
|
|
8
23
|
type ChatWebhookChannelOptions,
|
|
9
24
|
chatMessageText,
|
|
@@ -36,6 +51,7 @@ export {
|
|
|
36
51
|
DeliveryJobs,
|
|
37
52
|
DeliveryQueries,
|
|
38
53
|
DeliveryStatus,
|
|
54
|
+
EMAIL_CHANNEL_NAME,
|
|
39
55
|
} from "./constants.js";
|
|
40
56
|
export {
|
|
41
57
|
collectChannels,
|
|
@@ -55,6 +71,7 @@ export {
|
|
|
55
71
|
} from "./tables.js";
|
|
56
72
|
export { type CreateDeliveryTestContextOptions, createDeliveryTestContext } from "./testing.js";
|
|
57
73
|
export {
|
|
74
|
+
type AttachmentOrigin,
|
|
58
75
|
type ChannelContext,
|
|
59
76
|
type ChannelMessage,
|
|
60
77
|
type ChannelResult,
|
|
@@ -64,9 +81,7 @@ export {
|
|
|
64
81
|
type DeliveryLogEntry,
|
|
65
82
|
type DeliveryService,
|
|
66
83
|
isDeliveryChannelPlugin,
|
|
67
|
-
type NotificationRenderer,
|
|
68
84
|
type RenderedMessage,
|
|
69
|
-
type RendererInput,
|
|
70
85
|
} from "./types.js";
|
|
71
86
|
export {
|
|
72
87
|
type AddressUnsubscribeTokenPayload,
|