@cosmicdrift/kumiko-bundled-features 0.362.0 → 0.363.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin-shell/feature.d.ts +3 -0
- package/dist/admin-shell/feature.js +6 -1
- package/dist/agent-tools/agent-manifest.js +2 -1
- package/dist/agent-tools/filter-ops.d.ts +1 -1
- package/dist/agent-tools/filter-ops.js +1 -3
- package/dist/agent-tools/tool-dispatch.js +2 -1
- package/dist/auth-email-password/email-templates.d.ts +4 -26
- package/dist/auth-email-password/email-templates.js +9 -101
- package/dist/auth-email-password/feature.js +24 -6
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
- package/dist/auth-email-password/handlers/request-account-unlock.write.js +2 -4
- package/dist/auth-email-password/handlers/request-email-verification.write.js +2 -4
- package/dist/auth-email-password/handlers/request-password-reset.write.js +2 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +3 -4
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -2
- package/dist/auth-email-password/handlers/token-request-handler.js +0 -1
- package/dist/auth-email-password/index.d.ts +2 -2
- package/dist/auth-email-password/index.js +1 -1
- package/dist/auth-email-password/magic-link-mail.d.ts +3 -2
- package/dist/auth-email-password/magic-link-mail.js +19 -14
- package/dist/auth-email-password/mail-notifications.d.ts +32 -0
- package/dist/auth-email-password/mail-notifications.js +116 -0
- package/dist/auth-email-password/signup-activation.d.ts +1 -1
- package/dist/auth-email-password/signup-activation.js +2 -3
- package/dist/auth-foundation/types.js +2 -8
- package/dist/auth-mfa/feature.js +4 -4
- package/dist/auth-mfa/hmac-token-codec.js +4 -4
- package/dist/auth-mfa/mfa-challenge-token.js +3 -3
- package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -3
- package/dist/auth-mfa/mfa-setup-token.js +8 -8
- package/dist/billing-foundation/constants.d.ts +3 -3
- package/dist/billing-foundation/constants.js +5 -4
- package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +32 -14
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +27 -32
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +3 -3
- package/dist/billing-foundation/consumer-protection/mail-notifications.d.ts +8 -0
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +95 -0
- package/dist/billing-foundation/consumer-protection/register-consent.js +2 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +20 -10
- package/dist/billing-foundation/consumer-protection/termination-mail.js +10 -21
- package/dist/billing-foundation/consumer-protection/termination-notify.js +2 -2
- package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -3
- package/dist/billing-foundation/consumer-protection/termination-request.js +2 -2
- package/dist/billing-foundation/handlers/process-event.write.js +2 -2
- package/dist/billing-foundation/handlers/process-payment-event.write.js +1 -1
- package/dist/billing-foundation/subscription-tier-sync.js +3 -4
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -1
- package/dist/cap-overview/web/cap-usage-cell.js +5 -4
- package/dist/channel-email/email-channel.d.ts +2 -2
- package/dist/channel-email/email-channel.js +89 -26
- package/dist/channel-email/feature.js +3 -3
- package/dist/channel-email/index.d.ts +1 -1
- package/dist/channel-email/index.js +1 -1
- package/dist/channel-email/pii-guard.js +10 -0
- package/dist/channel-email/smtp-transport.js +13 -0
- package/dist/channel-email/types.d.ts +8 -0
- package/dist/config/handlers/set.write.js +1 -2
- package/dist/custom-fields/lib/value-schema.js +10 -11
- package/dist/custom-fields/wire-user-data-rights.js +7 -6
- package/dist/data-retention/index.d.ts +1 -0
- package/dist/data-retention/index.js +1 -0
- package/dist/data-retention/run-retention-cleanup.js +6 -6
- package/dist/delivery/attachment-ref.d.ts +39 -0
- package/dist/delivery/attachment-ref.js +64 -0
- package/dist/delivery/channel-context.d.ts +8 -4
- package/dist/delivery/channel-context.js +76 -2
- package/dist/delivery/constants.d.ts +1 -0
- package/dist/delivery/constants.js +1 -0
- package/dist/delivery/delivery-service.d.ts +4 -1
- package/dist/delivery/delivery-service.js +38 -32
- package/dist/delivery/feature.js +1 -1
- package/dist/delivery/index.d.ts +5 -2
- package/dist/delivery/index.js +4 -1
- package/dist/delivery/jobs.js +25 -5
- package/dist/delivery/notify-factory.js +4 -1
- package/dist/delivery/testing.d.ts +4 -1
- package/dist/delivery/types.d.ts +20 -14
- package/dist/delivery/unsubscribe.js +4 -1
- package/dist/derivatives-sharp/render.js +1 -18
- package/dist/document-ingest-foundation/pages.js +4 -4
- package/dist/feature-toggles/toggle-sync-signal.js +4 -4
- package/dist/file-foundation/index.d.ts +1 -0
- package/dist/file-foundation/index.js +1 -0
- package/dist/file-foundation/read-file-ref-content.d.ts +27 -0
- package/dist/file-foundation/read-file-ref-content.js +49 -0
- package/dist/folders/web/folder-manager.js +3 -2
- package/dist/form-draft/db/queries/cleanup.js +4 -4
- package/dist/form-draft/db/queries/draft-count.js +2 -2
- package/dist/form-draft/db/queries/owned-file-refs.js +2 -2
- package/dist/form-draft/release-file-refs.js +3 -2
- package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +6 -6
- package/dist/inbound-mail-foundation/handlers/scope-visibility.d.ts +1 -1
- package/dist/inbound-mail-foundation/oauth-state.js +7 -7
- package/dist/inbound-mail-foundation/types.js +4 -4
- package/dist/inbound-mail-foundation/watch-supervisor.js +22 -13
- package/dist/inbound-provider-imap/imap-client.js +4 -3
- package/dist/ledger/recurring.js +1 -3
- package/dist/managed-pages/branding.d.ts +2 -0
- package/dist/managed-pages/branding.js +24 -2
- package/dist/managed-pages/feature.js +1 -0
- package/dist/managed-pages/i18n.js +1 -0
- package/dist/managed-pages/screens/branding-screen.js +8 -1
- package/dist/page-render/branding.d.ts +1 -0
- package/dist/page-render/branding.js +1 -0
- package/dist/page-render/markdown.js +1 -1
- package/dist/personal-access-tokens/feature.js +1 -1
- package/dist/personal-access-tokens/handlers/list.query.js +2 -1
- package/dist/personal-access-tokens/revoke-for-user.js +2 -1
- package/dist/renderer-foundation/api.d.ts +13 -13
- package/dist/renderer-foundation/api.js +1 -1
- package/dist/renderer-foundation/constants.js +2 -4
- package/dist/renderer-foundation/feature.js +7 -7
- package/dist/renderer-foundation/index.d.ts +2 -1
- package/dist/renderer-foundation/index.js +1 -0
- package/dist/renderer-foundation/types.d.ts +92 -32
- package/dist/renderer-pdf/feature.js +7 -1
- package/dist/sessions/db/queries/cleanup.js +2 -2
- package/dist/sessions/feature.js +1 -1
- package/dist/sessions/handlers/list.query.js +2 -1
- package/dist/shared/attachment-file-name.d.ts +3 -0
- package/dist/shared/attachment-file-name.js +35 -0
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.js +3 -0
- package/dist/shared/is-tenant-db.js +1 -1
- package/dist/shared/mail-template-body.d.ts +4 -0
- package/dist/shared/mail-template-body.js +17 -0
- package/dist/shared/sharp-input-limits.d.ts +6 -0
- package/dist/shared/sharp-input-limits.js +22 -0
- package/dist/shared/signup-handover.js +2 -2
- package/dist/tags/handlers/assign-tag.write.js +3 -1
- package/dist/tags/web/tag-chip.js +2 -0
- package/dist/template-resolver/api.d.ts +30 -2
- package/dist/template-resolver/api.js +40 -5
- package/dist/template-resolver/compile-document-body.d.ts +8 -0
- package/dist/template-resolver/compile-document-body.js +163 -0
- package/dist/template-resolver/constants.d.ts +13 -4
- package/dist/template-resolver/constants.js +30 -10
- package/dist/template-resolver/document-sanitizer.d.ts +32 -0
- package/dist/template-resolver/document-sanitizer.js +201 -0
- package/dist/template-resolver/document-schema.d.ts +21 -0
- package/dist/template-resolver/document-schema.js +296 -0
- package/dist/template-resolver/feature.d.ts +18 -0
- package/dist/template-resolver/feature.js +50 -6
- package/dist/template-resolver/handlers/bundle-import.d.ts +20 -0
- package/dist/template-resolver/handlers/bundle-import.js +147 -0
- package/dist/template-resolver/handlers/collection-set.write.js +25 -2
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +19 -0
- package/dist/template-resolver/handlers/copy-locale.write.js +116 -0
- package/dist/template-resolver/handlers/diff-to-system.query.d.ts +22 -0
- package/dist/template-resolver/handlers/diff-to-system.query.js +64 -0
- package/dist/template-resolver/handlers/export-bundle.query.d.ts +35 -0
- package/dist/template-resolver/handlers/export-bundle.query.js +68 -0
- package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
- package/dist/template-resolver/handlers/find-by-id.query.js +3 -3
- package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +39 -0
- package/dist/template-resolver/handlers/import-bundle-system.write.js +36 -0
- package/dist/template-resolver/handlers/import-bundle.write.d.ts +40 -0
- package/dist/template-resolver/handlers/import-bundle.write.js +19 -0
- package/dist/template-resolver/handlers/layout-source.query.d.ts +26 -0
- package/dist/template-resolver/handlers/layout-source.query.js +64 -0
- package/dist/template-resolver/handlers/list.query.d.ts +4 -1
- package/dist/template-resolver/handlers/list.query.js +5 -7
- package/dist/template-resolver/handlers/overview.query.d.ts +59 -0
- package/dist/template-resolver/handlers/overview.query.js +165 -0
- package/dist/template-resolver/handlers/preview.query.d.ts +12 -0
- package/dist/template-resolver/handlers/preview.query.js +24 -0
- package/dist/template-resolver/handlers/reset-to-system.write.d.ts +8 -0
- package/dist/template-resolver/handlers/reset-to-system.write.js +50 -0
- package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +8 -0
- package/dist/template-resolver/handlers/resource-link-pdf.write.js +32 -0
- package/dist/template-resolver/handlers/resource-remove.write.d.ts +9 -0
- package/dist/template-resolver/handlers/resource-remove.write.js +44 -0
- package/dist/template-resolver/handlers/resource-shared.d.ts +9 -0
- package/dist/template-resolver/handlers/resource-shared.js +14 -0
- package/dist/template-resolver/handlers/resource-upload.write.d.ts +13 -0
- package/dist/template-resolver/handlers/resource-upload.write.js +98 -0
- package/dist/template-resolver/handlers/send-test.write.d.ts +11 -0
- package/dist/template-resolver/handlers/send-test.write.js +81 -0
- package/dist/template-resolver/handlers/set.write.d.ts +4 -1
- package/dist/template-resolver/handlers/set.write.js +25 -3
- package/dist/template-resolver/handlers/shared.d.ts +9 -5
- package/dist/template-resolver/handlers/shared.js +6 -2
- package/dist/template-resolver/handlers/template-gates.d.ts +36 -0
- package/dist/template-resolver/handlers/template-gates.js +131 -0
- package/dist/template-resolver/handlers/template-write.d.ts +45 -0
- package/dist/template-resolver/handlers/template-write.js +109 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +17 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.js +48 -0
- package/dist/template-resolver/handlers/toggle-status.write.d.ts +12 -0
- package/dist/template-resolver/handlers/toggle-status.write.js +27 -11
- package/dist/template-resolver/handlers/upsert-system.write.d.ts +5 -8
- package/dist/template-resolver/handlers/upsert-system.write.js +14 -59
- package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +5 -8
- package/dist/template-resolver/handlers/upsert-tenant.write.js +10 -61
- package/dist/template-resolver/handlers/usage.query.d.ts +17 -0
- package/dist/template-resolver/handlers/usage.query.js +15 -0
- package/dist/template-resolver/handlers/validate.query.d.ts +74 -0
- package/dist/template-resolver/handlers/validate.query.js +80 -0
- package/dist/template-resolver/index.d.ts +18 -1
- package/dist/template-resolver/index.js +16 -1
- package/dist/template-resolver/layout-regions.d.ts +23 -0
- package/dist/template-resolver/layout-regions.js +125 -0
- package/dist/template-resolver/linked-resources.d.ts +17 -0
- package/dist/template-resolver/linked-resources.js +75 -0
- package/dist/template-resolver/preview-display-html.d.ts +7 -0
- package/dist/template-resolver/preview-display-html.js +27 -0
- package/dist/template-resolver/print-document.d.ts +1 -0
- package/dist/template-resolver/print-document.js +55 -0
- package/dist/template-resolver/qualified-names.d.ts +30 -0
- package/dist/template-resolver/qualified-names.js +30 -0
- package/dist/template-resolver/record-mapping.d.ts +2 -0
- package/dist/template-resolver/record-mapping.js +21 -0
- package/dist/template-resolver/resource-image.d.ts +15 -0
- package/dist/template-resolver/resource-image.js +34 -0
- package/dist/template-resolver/seeding.d.ts +14 -1
- package/dist/template-resolver/seeding.js +105 -15
- package/dist/template-resolver/store-template-resource.d.ts +64 -0
- package/dist/template-resolver/store-template-resource.js +264 -0
- package/dist/template-resolver/table.d.ts +47 -8
- package/dist/template-resolver/table.js +11 -0
- package/dist/template-resolver/template-bundle.d.ts +36 -0
- package/dist/template-resolver/template-bundle.js +60 -0
- package/dist/template-resolver/template-diff.d.ts +7 -0
- package/dist/template-resolver/template-diff.js +68 -0
- package/dist/template-resolver/template-meta.d.ts +60 -0
- package/dist/template-resolver/template-meta.js +101 -0
- package/dist/template-resolver/template-preview.d.ts +86 -0
- package/dist/template-resolver/template-preview.js +374 -0
- package/dist/template-resolver/template-resources.d.ts +14 -0
- package/dist/template-resolver/template-resources.js +42 -0
- package/dist/template-resolver/template-usage.d.ts +12 -0
- package/dist/template-resolver/template-usage.js +30 -0
- package/dist/template-resolver/template-validation.d.ts +22 -0
- package/dist/template-resolver/template-validation.js +172 -0
- package/dist/template-resolver/testing.js +5 -5
- package/dist/template-resolver/user-content-table.d.ts +14 -8
- package/dist/template-resolver/web/client-plugin.d.ts +1 -0
- package/dist/template-resolver/web/client-plugin.js +9 -6
- package/dist/template-resolver/web/document-editor-extensions.d.ts +15 -0
- package/dist/template-resolver/web/document-editor-extensions.js +107 -0
- package/dist/template-resolver/web/document-editor.d.ts +3 -0
- package/dist/template-resolver/web/document-editor.js +11 -0
- package/dist/template-resolver/web/document-tiptap-editor.d.ts +3 -0
- package/dist/template-resolver/web/document-tiptap-editor.js +376 -0
- package/dist/template-resolver/web/document-toolbar-blocks.d.ts +33 -0
- package/dist/template-resolver/web/document-toolbar-blocks.js +15 -0
- package/dist/template-resolver/web/editor-handle.d.ts +3 -0
- package/dist/template-resolver/web/editor-handle.js +24 -0
- package/dist/template-resolver/web/i18n.js +76 -0
- package/dist/template-resolver/web/index.d.ts +4 -0
- package/dist/template-resolver/web/index.js +4 -0
- package/dist/template-resolver/web/placeholder-line-editor-lazy.d.ts +4 -0
- package/dist/template-resolver/web/placeholder-line-editor-lazy.js +16 -0
- package/dist/template-resolver/web/placeholder-line-editor.d.ts +15 -0
- package/dist/template-resolver/web/placeholder-line-editor.js +94 -0
- package/dist/template-resolver/web/placeholder-text.d.ts +4 -0
- package/dist/template-resolver/web/placeholder-text.js +34 -0
- package/dist/template-resolver/web/use-stable-by-key.d.ts +1 -0
- package/dist/template-resolver/web/use-stable-by-key.js +13 -0
- package/dist/tenant/handlers/team-list.query.js +3 -2
- package/dist/tenant/seeding.js +31 -6
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +2 -1
- package/dist/user/db/queries/stream-tenant-backfill.js +4 -4
- package/dist/user/handlers/detail.query.js +2 -1
- package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
- package/dist/user-data-rights/email-templates.d.ts +1 -29
- package/dist/user-data-rights/email-templates.js +5 -106
- package/dist/user-data-rights/feature.js +53 -35
- package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
- package/dist/user-data-rights/lib/default-mailers.d.ts +15 -7
- package/dist/user-data-rights/lib/default-mailers.js +100 -48
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/user-data-rights/mail-notifications.d.ts +13 -0
- package/dist/user-data-rights/mail-notifications.js +118 -0
- package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
- package/dist/user-profile/web/profile-screen.js +2 -1
- package/dist/waitlist/feature.js +10 -0
- package/dist/waitlist/handlers/submit.write.js +3 -4
- package/dist/waitlist/mail.d.ts +19 -6
- package/dist/waitlist/mail.js +50 -14
- package/package.json +28 -17
- package/src/admin-shell/changes.json +6 -0
- package/src/auth-email-password/changes.json +14 -0
- package/src/billing-foundation/changes.json +7 -0
- package/src/channel-email/changes.json +19 -0
- package/src/data-retention/changes.json +6 -0
- package/src/delivery/changes.json +48 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/renderer-foundation/changes.json +39 -0
- package/src/template-resolver/changes.json +118 -0
- package/src/user-data-rights/changes.json +7 -0
- package/src/waitlist/changes.json +7 -0
- package/dist/renderer-simple/feature.d.ts +0 -10
- package/dist/renderer-simple/feature.js +0 -42
- package/dist/renderer-simple/index.d.ts +0 -3
- package/dist/renderer-simple/index.js +0 -2
- package/dist/renderer-simple/mail-blocks.d.ts +0 -21
- package/dist/renderer-simple/mail-blocks.js +0 -107
- package/dist/renderer-simple/resolve-variables.d.ts +0 -7
- package/dist/renderer-simple/resolve-variables.js +0 -47
- package/dist/renderer-simple/simple-renderer.d.ts +0 -24
- package/dist/renderer-simple/simple-renderer.js +0 -200
- package/src/renderer-simple/changes.json +0 -27
|
@@ -43,6 +43,7 @@ import { createEventStoreExecutor, createTenantDb, nullBlindIndexesForSubject, W
|
|
|
43
43
|
import { deleteStoredFileAndDerivatives } from "@cosmicdrift/kumiko-framework/derivatives";
|
|
44
44
|
import { createSystemUser, entityHasAnonymizableSubjectField, } from "@cosmicdrift/kumiko-framework/engine";
|
|
45
45
|
import { fileRefEntity, fileRefsTable, } from "@cosmicdrift/kumiko-framework/files";
|
|
46
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
46
47
|
import { runInSubTransaction } from "../shared/index.js";
|
|
47
48
|
import { computeCutoff } from "./keep-for.js";
|
|
48
49
|
import { resolveRetentionPolicyForTenant } from "./resolve-for-tenant.js";
|
|
@@ -219,10 +220,9 @@ function collectSingleFileHolders(registry) {
|
|
|
219
220
|
const { singleFields } = planFileFields(entity.fields);
|
|
220
221
|
if (singleFields.length === 0)
|
|
221
222
|
continue;
|
|
222
|
-
const table = proj.table; // @cast-boundary column-presence probe
|
|
223
223
|
holders.push({
|
|
224
224
|
table: proj.table,
|
|
225
|
-
tableHasTenantId: table
|
|
225
|
+
tableHasTenantId: propertyOf(proj.table, "tenantId") !== undefined,
|
|
226
226
|
singleFields,
|
|
227
227
|
});
|
|
228
228
|
}
|
|
@@ -473,10 +473,9 @@ export async function runRetentionCleanup(args) {
|
|
|
473
473
|
const entity = registry.getEntity(entityName);
|
|
474
474
|
if (!entity)
|
|
475
475
|
continue;
|
|
476
|
-
const table = proj.table; // @cast-boundary column-presence probe
|
|
477
476
|
const declaredReference = policy.reference ?? DEFAULT_REFERENCE_FIELD;
|
|
478
477
|
const referenceField = FRAMEWORK_REFERENCE_ALIAS[declaredReference] ?? declaredReference;
|
|
479
|
-
if (table
|
|
478
|
+
if (propertyOf(proj.table, referenceField) === undefined) {
|
|
480
479
|
skipped.push({ entityName, reason: "missing_reference_column" });
|
|
481
480
|
continue;
|
|
482
481
|
}
|
|
@@ -485,7 +484,7 @@ export async function runRetentionCleanup(args) {
|
|
|
485
484
|
// Tenant-Scope nur wenn die Tabelle eine tenantId-Spalte hat — identisch zu
|
|
486
485
|
// soft-delete-cleanup. Ohne diesen Filter wuerde ein Tenant die Rows eines
|
|
487
486
|
// anderen treffen.
|
|
488
|
-
if (table
|
|
487
|
+
if (propertyOf(proj.table, "tenantId") !== undefined) {
|
|
489
488
|
where["tenantId"] = tenantId;
|
|
490
489
|
}
|
|
491
490
|
switch (policy.strategy) {
|
|
@@ -531,7 +530,8 @@ export async function runRetentionCleanup(args) {
|
|
|
531
530
|
break;
|
|
532
531
|
}
|
|
533
532
|
case "softDelete": {
|
|
534
|
-
if (table
|
|
533
|
+
if (propertyOf(proj.table, "isDeleted") === undefined ||
|
|
534
|
+
propertyOf(proj.table, "deletedAt") === undefined) {
|
|
535
535
|
skipped.push({ entityName, reason: "missing_softdelete_columns" });
|
|
536
536
|
break;
|
|
537
537
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import type { FileAttachmentRef, InlineAttachmentRef, RenderedPdfAttachmentRef } from "../renderer-foundation/index.js";
|
|
3
|
+
export type { FileAttachmentRef, InlineAttachmentRef, RenderedPdfAttachmentRef };
|
|
4
|
+
export type EmailAttachmentRef = InlineAttachmentRef | FileAttachmentRef | RenderedPdfAttachmentRef;
|
|
5
|
+
export declare const MAX_EMAIL_ATTACHMENTS = 20;
|
|
6
|
+
export declare const MAX_RENDERED_PDF_VARIABLES_BYTES = 64000;
|
|
7
|
+
export declare const inlineAttachmentRefSchema: z.ZodObject<{
|
|
8
|
+
kind: z.ZodLiteral<"inline">;
|
|
9
|
+
cid: z.ZodString;
|
|
10
|
+
fileRefId: z.ZodString;
|
|
11
|
+
}, z.core.$strict>;
|
|
12
|
+
export declare const fileAttachmentRefSchema: z.ZodObject<{
|
|
13
|
+
kind: z.ZodLiteral<"file">;
|
|
14
|
+
fileRefId: z.ZodString;
|
|
15
|
+
fileName: z.ZodString;
|
|
16
|
+
}, z.core.$strict>;
|
|
17
|
+
export declare const renderedPdfAttachmentRefSchema: z.ZodObject<{
|
|
18
|
+
kind: z.ZodLiteral<"rendered-pdf">;
|
|
19
|
+
templateSlug: z.ZodString;
|
|
20
|
+
variables: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
21
|
+
fileName: z.ZodString;
|
|
22
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
23
|
+
}, z.core.$strict>;
|
|
24
|
+
export declare const emailAttachmentRefSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
25
|
+
kind: z.ZodLiteral<"inline">;
|
|
26
|
+
cid: z.ZodString;
|
|
27
|
+
fileRefId: z.ZodString;
|
|
28
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
29
|
+
kind: z.ZodLiteral<"file">;
|
|
30
|
+
fileRefId: z.ZodString;
|
|
31
|
+
fileName: z.ZodString;
|
|
32
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
33
|
+
kind: z.ZodLiteral<"rendered-pdf">;
|
|
34
|
+
templateSlug: z.ZodString;
|
|
35
|
+
variables: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
36
|
+
fileName: z.ZodString;
|
|
37
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
38
|
+
}, z.core.$strict>], "kind">;
|
|
39
|
+
export declare function toAttachmentRef(ref: EmailAttachmentRef): EmailAttachmentRef;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { attachmentFileNameSchema } from "../shared/index.js";
|
|
3
|
+
import { isDocumentResourceKey, templateSlugSchema } from "../template-resolver/index.js";
|
|
4
|
+
export const MAX_EMAIL_ATTACHMENTS = 20;
|
|
5
|
+
export const MAX_RENDERED_PDF_VARIABLES_BYTES = 64_000;
|
|
6
|
+
const MAX_ID_LENGTH = 64;
|
|
7
|
+
const fileRefIdSchema = z.string().min(1).max(MAX_ID_LENGTH);
|
|
8
|
+
export const inlineAttachmentRefSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
kind: z.literal("inline"),
|
|
11
|
+
cid: z.string().min(1).max(MAX_ID_LENGTH).refine(isDocumentResourceKey),
|
|
12
|
+
fileRefId: fileRefIdSchema,
|
|
13
|
+
})
|
|
14
|
+
.strict();
|
|
15
|
+
export const fileAttachmentRefSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
kind: z.literal("file"),
|
|
18
|
+
fileRefId: fileRefIdSchema,
|
|
19
|
+
fileName: attachmentFileNameSchema,
|
|
20
|
+
})
|
|
21
|
+
.strict();
|
|
22
|
+
export const renderedPdfAttachmentRefSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
kind: z.literal("rendered-pdf"),
|
|
25
|
+
templateSlug: templateSlugSchema,
|
|
26
|
+
variables: z
|
|
27
|
+
.record(z.string(), z.unknown())
|
|
28
|
+
.refine((value) => Buffer.byteLength(JSON.stringify(value)) <= MAX_RENDERED_PDF_VARIABLES_BYTES, {
|
|
29
|
+
message: `variables must not exceed ${MAX_RENDERED_PDF_VARIABLES_BYTES} bytes as JSON`,
|
|
30
|
+
}),
|
|
31
|
+
fileName: attachmentFileNameSchema.refine((name) => name.toLowerCase().endsWith(".pdf"), {
|
|
32
|
+
message: "file name must end with .pdf",
|
|
33
|
+
}),
|
|
34
|
+
locale: z
|
|
35
|
+
.string()
|
|
36
|
+
.min(2)
|
|
37
|
+
.max(8)
|
|
38
|
+
.regex(/^[a-z]{2}(-[a-z]{2})?$/i, "locale must be ISO 639-1 (e.g. de, en, en-us)")
|
|
39
|
+
.optional(),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
42
|
+
export const emailAttachmentRefSchema = z.discriminatedUnion("kind", [
|
|
43
|
+
inlineAttachmentRefSchema,
|
|
44
|
+
fileAttachmentRefSchema,
|
|
45
|
+
renderedPdfAttachmentRefSchema,
|
|
46
|
+
]);
|
|
47
|
+
// Rebuilds only the contract fields so bytes a caller attached to a ref object
|
|
48
|
+
// can never ride into a job payload.
|
|
49
|
+
export function toAttachmentRef(ref) {
|
|
50
|
+
switch (ref.kind) {
|
|
51
|
+
case "inline":
|
|
52
|
+
return { kind: "inline", cid: ref.cid, fileRefId: ref.fileRefId };
|
|
53
|
+
case "file":
|
|
54
|
+
return { kind: "file", fileRefId: ref.fileRefId, fileName: ref.fileName };
|
|
55
|
+
case "rendered-pdf":
|
|
56
|
+
return {
|
|
57
|
+
kind: "rendered-pdf",
|
|
58
|
+
templateSlug: ref.templateSlug,
|
|
59
|
+
variables: ref.variables,
|
|
60
|
+
fileName: ref.fileName,
|
|
61
|
+
...(ref.locale !== undefined && { locale: ref.locale }),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
-
import { type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { type DbConnection, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { ConfigAccessor, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
|
|
4
5
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
5
|
-
import type
|
|
6
|
-
|
|
6
|
+
import { type MailPayload } from "../renderer-foundation/index.js";
|
|
7
|
+
import type { ChannelContext, RenderedMail } from "./types.js";
|
|
8
|
+
export declare function createTemplateMailRenderer(db: DbConnection | TenantDb, registry: Registry, tenantId: TenantId, config: ConfigAccessor | undefined): (payload: MailPayload) => Promise<RenderedMail>;
|
|
9
|
+
export declare function createEmailAttachmentReader(db: DbConnection | TenantDb, registry: Registry, tenantId: TenantId, config: ConfigAccessor | undefined, filesForTenant: (tenantId: string) => FileContext | undefined): NonNullable<ChannelContext["readAttachment"]>;
|
|
10
|
+
export declare function buildChannelContext(db: DbConnection, registry: Registry, sseBroker: SseBroker | undefined, tenantId: TenantId, secrets?: SecretsContext, tenantConfig?: ConfigAccessor, filesForTenant?: (tenantId: string) => FileContext | undefined): ChannelContext;
|
|
@@ -1,8 +1,82 @@
|
|
|
1
1
|
import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { readFileRefContent } from "../file-foundation/index.js";
|
|
4
|
+
import { collectRendererPlugins, createRendererFoundationApi, RendererError, } from "../renderer-foundation/index.js";
|
|
5
|
+
import { FALLBACK_LOCALE } from "../template-resolver/index.js";
|
|
6
|
+
export function createTemplateMailRenderer(db, registry, tenantId, config) {
|
|
7
|
+
return async (payload) => {
|
|
8
|
+
const plugin = createRendererFoundationApi(collectRendererPlugins(registry)).createRendererForTenant({ tenantId, kind: "mail" });
|
|
9
|
+
const response = await plugin.render({ kind: "mail", payload }, { db, registry, tenantId, ...(config !== undefined && { config }) });
|
|
10
|
+
if (response.kind !== "mail") {
|
|
11
|
+
throw new RendererError(`[delivery] renderer "${plugin.name}" answered a mail request with kind "${response.kind}"`, "other");
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
subject: response.subject,
|
|
15
|
+
html: response.html,
|
|
16
|
+
text: response.text,
|
|
17
|
+
...(response.previewText !== undefined && { previewText: response.previewText }),
|
|
18
|
+
...(response.attachments !== undefined && { attachments: response.attachments }),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const INLINE_IMAGE_CONTENT_TYPE_PREFIX = "image/";
|
|
23
|
+
const PDF_CONTENT_TYPE = "application/pdf";
|
|
24
|
+
// Renders at send time so the job payload carries variables, not bytes. A failure throws:
|
|
25
|
+
// the send job retries instead of mailing without the document the recipient expects.
|
|
26
|
+
function createPdfRenderer(db, registry, tenantId, config, filesForTenant) {
|
|
27
|
+
return async (ref) => {
|
|
28
|
+
const plugin = createRendererFoundationApi(collectRendererPlugins(registry)).createRendererForTenant({ tenantId, kind: "document-pdf" });
|
|
29
|
+
const response = await plugin.render({
|
|
30
|
+
kind: "document-pdf",
|
|
31
|
+
payload: {
|
|
32
|
+
slug: ref.templateSlug,
|
|
33
|
+
locale: ref.locale ?? FALLBACK_LOCALE,
|
|
34
|
+
variables: ref.variables,
|
|
35
|
+
},
|
|
36
|
+
}, { db, registry, tenantId, filesForTenant, ...(config !== undefined && { config }) });
|
|
37
|
+
if (response.kind !== "document-pdf") {
|
|
38
|
+
throw new RendererError(`[delivery] renderer "${plugin.name}" answered a document-pdf request with kind "${response.kind}"`, "other");
|
|
39
|
+
}
|
|
40
|
+
return { content: response.pdfBytes, contentType: PDF_CONTENT_TYPE };
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Inline images and file attachments the renderer took from a template may come from the system
|
|
44
|
+
// tenant (system template or tenant override of one). A file ref from notify data stays limited to
|
|
45
|
+
// the job's own tenant, since a caller picks it. Inline content that is not an image is dropped, so
|
|
46
|
+
// a swapped fileRef id cannot smuggle a document into the mail body.
|
|
47
|
+
export function createEmailAttachmentReader(db, registry, tenantId, config, filesForTenant) {
|
|
48
|
+
const renderPdf = createPdfRenderer(db, registry, tenantId, config, filesForTenant);
|
|
49
|
+
return async (ref, origin = "message") => {
|
|
50
|
+
if (ref.kind === "rendered-pdf")
|
|
51
|
+
return renderPdf(ref);
|
|
52
|
+
const found = await readFileRefContent({
|
|
53
|
+
db,
|
|
54
|
+
filesForTenant,
|
|
55
|
+
fileRefId: ref.fileRefId,
|
|
56
|
+
allowedTenantIds: ref.kind === "file" && origin === "message" ? [tenantId] : [tenantId, SYSTEM_TENANT_ID],
|
|
57
|
+
});
|
|
58
|
+
if (found === null)
|
|
59
|
+
return null;
|
|
60
|
+
if (ref.kind === "inline" && !found.contentType.startsWith(INLINE_IMAGE_CONTENT_TYPE_PREFIX)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return { content: found.content, contentType: found.contentType };
|
|
64
|
+
};
|
|
65
|
+
}
|
|
2
66
|
// Build the per-tenant context a channel's resolve/render/send receives.
|
|
3
67
|
// Shared by the synchronous delivery-service path and the async job handlers
|
|
4
68
|
// (which pass sseBroker=undefined — only the inline inApp channel uses SSE,
|
|
5
69
|
// and it never runs through a job).
|
|
6
|
-
export function buildChannelContext(db, registry, sseBroker, tenantId, secrets) {
|
|
7
|
-
return {
|
|
70
|
+
export function buildChannelContext(db, registry, sseBroker, tenantId, secrets, tenantConfig, filesForTenant) {
|
|
71
|
+
return {
|
|
72
|
+
db: createTenantDb(db, tenantId),
|
|
73
|
+
registry,
|
|
74
|
+
sseBroker,
|
|
75
|
+
tenantId,
|
|
76
|
+
secrets,
|
|
77
|
+
renderMail: createTemplateMailRenderer(db, registry, tenantId, tenantConfig),
|
|
78
|
+
...(filesForTenant !== undefined && {
|
|
79
|
+
readAttachment: createEmailAttachmentReader(db, registry, tenantId, tenantConfig, filesForTenant),
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
8
82
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { NotifyPriority } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
export declare const DELIVERY_CHANNEL_EXTENSION: "deliveryChannel";
|
|
3
|
+
export declare const EMAIL_CHANNEL_NAME: "email";
|
|
3
4
|
export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, type DeliveryStatusValue, } from "./public-names.js";
|
|
4
5
|
export declare function deliveryIdempotencyRedisKey(tenantId: string, key: string): string;
|
|
5
6
|
export declare const deliveryPriorityRank: Record<NotifyPriority, number>;
|
|
@@ -3,6 +3,7 @@ import { DELIVERY_FEATURE, DeliveryJobNames } from "./public-names.js";
|
|
|
3
3
|
// Extension-point name for delivery-channel plugins (channel-email,
|
|
4
4
|
// channel-in-app, channel-push, ...).
|
|
5
5
|
export const DELIVERY_CHANNEL_EXTENSION = "deliveryChannel";
|
|
6
|
+
export const EMAIL_CHANNEL_NAME = "email";
|
|
6
7
|
export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, } from "./public-names.js";
|
|
7
8
|
// Single source for claim (delivery-service) and terminal-failure release (jobs).
|
|
8
9
|
export function deliveryIdempotencyRedisKey(tenantId, key) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import type { EscapeHatchAuditSink, NotifyJobDispatcher, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import type { ConfigAccessor, EscapeHatchAuditSink, NotifyJobDispatcher, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
|
|
4
5
|
import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
5
6
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
6
7
|
import type { Redis } from "ioredis";
|
|
@@ -23,6 +24,8 @@ export type DeliveryServiceOptions = {
|
|
|
23
24
|
readonly idempotencyRedis?: Redis;
|
|
24
25
|
readonly jobRunner?: NotifyJobDispatcher;
|
|
25
26
|
readonly secrets?: SecretsContext;
|
|
27
|
+
readonly filesForTenant?: (tenantId: string) => FileContext | undefined;
|
|
28
|
+
readonly tenantConfigFor?: (tenantId: TenantId) => ConfigAccessor;
|
|
26
29
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
27
30
|
readonly log?: Logger;
|
|
28
31
|
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createSystemDbView, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { assertNotificationVariables, createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
4
|
-
import { createEscapeHatchReporter } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
4
|
+
import { assertNotifyTargetsOwnTenant, createEscapeHatchReporter, } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
5
5
|
import { bridgeStub } from "@cosmicdrift/kumiko-framework/testing/handler-context";
|
|
6
6
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
7
|
+
import { fileContextFromResolver } from "../file-foundation/index.js";
|
|
7
8
|
import { redactErrorText } from "../shared/index.js";
|
|
8
9
|
import { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
9
10
|
import { appendAttemptEvent, logAttempt } from "./attempt-log.js";
|
|
@@ -25,12 +26,8 @@ export function collectChannels(registry) {
|
|
|
25
26
|
return { name: usage.entityName, ...usage.options };
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
|
-
// `collectRenderers` entfernt 2026-05-19: notificationRenderer-Extension-Point
|
|
29
|
-
// wurde nie konsumiert (channel-email nimmt renderer als Konstruktor-Option,
|
|
30
|
-
// nicht aus Extension-Usages). Multi-Kind-Plugin-Pool lebt jetzt im
|
|
31
|
-
// `renderer-foundation`-Bundle via `collectRendererPlugins`.
|
|
32
29
|
export function createDeliveryService(options) {
|
|
33
|
-
const { db, registry, sseBroker, channels, tenantUserIdsQuery, rateLimit, isChannelKilled, idempotencyRedis, jobRunner, secrets, escapeHatchAuditSink, log, } = options;
|
|
30
|
+
const { db, registry, sseBroker, channels, tenantUserIdsQuery, rateLimit, isChannelKilled, idempotencyRedis, jobRunner, secrets, filesForTenant, tenantConfigFor, escapeHatchAuditSink, log, } = options;
|
|
34
31
|
const idemRedis = idempotencyRedis ?? rateLimit?.redis;
|
|
35
32
|
const logError = createFallbackLogger("delivery", log);
|
|
36
33
|
// Rate limit check: atomic INCR + TTL + over-limit rollback via server-side
|
|
@@ -96,9 +93,9 @@ export function createDeliveryService(options) {
|
|
|
96
93
|
const routedChannelCount = channels.filter((channel) => route[channel.name]).length;
|
|
97
94
|
return routedChannelCount === 1 ? idempotencyKey : undefined;
|
|
98
95
|
}
|
|
99
|
-
async function resolveUserIdsForTenant(tenantId) {
|
|
96
|
+
async function resolveUserIdsForTenant(run, tenantId) {
|
|
100
97
|
if (!tenantUserIdsQuery) {
|
|
101
|
-
throw new Error("Tenant broadcast requires tenantUserIdsQuery in DeliveryServiceOptions");
|
|
98
|
+
throw new Error("Tenant broadcast (to: { tenant }) requires the tenant feature (default wiring) or an explicit tenantUserIdsQuery in DeliveryServiceOptions");
|
|
102
99
|
}
|
|
103
100
|
const handler = registry.getQueryHandler(tenantUserIdsQuery);
|
|
104
101
|
if (!handler) {
|
|
@@ -117,7 +114,7 @@ export function createDeliveryService(options) {
|
|
|
117
114
|
// path a cross-tenant read the dispatcher never allows. Cross-tenant reach only
|
|
118
115
|
// exists through the grant-carrying systemDb view below.
|
|
119
116
|
const isSystem = registry.isHandlerSystemScoped(tenantUserIdsQuery);
|
|
120
|
-
const tenantDb = createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
|
|
117
|
+
const tenantDb = createTenantDb(run.db, tenantId, "tenant", undefined, undefined, undefined, {
|
|
121
118
|
report,
|
|
122
119
|
});
|
|
123
120
|
// The dispatcher grants a systemScope handler's ctx.systemDb.unsafeRaw() ungated —
|
|
@@ -125,7 +122,7 @@ export function createDeliveryService(options) {
|
|
|
125
122
|
// caller. Only the systemDb view's own source TenantDb carries the grant, so it
|
|
126
123
|
// can't leak onto ctx.db.
|
|
127
124
|
const systemDbSourceTenantDb = isSystem
|
|
128
|
-
? createTenantDb(db, tenantId, "system", undefined, undefined, undefined, {
|
|
125
|
+
? createTenantDb(run.db, tenantId, "system", undefined, undefined, undefined, {
|
|
129
126
|
report,
|
|
130
127
|
unsafeRaw: { reason: `r.systemScope() handler "${tenantUserIdsQuery}"` },
|
|
131
128
|
})
|
|
@@ -157,7 +154,7 @@ export function createDeliveryService(options) {
|
|
|
157
154
|
// attempts instead append a queued event up front and a terminal event from
|
|
158
155
|
// the send job — see deliverViaChannel + jobs.ts.
|
|
159
156
|
async function logDelivery(run, entry) {
|
|
160
|
-
const attemptId = await logAttempt(db, registry, entry);
|
|
157
|
+
const attemptId = await logAttempt(run.db, registry, entry);
|
|
161
158
|
recordDelivery(run, entry, attemptId);
|
|
162
159
|
}
|
|
163
160
|
// The stored/returned error is only the code; the redacted message goes to the log.
|
|
@@ -199,7 +196,7 @@ export function createDeliveryService(options) {
|
|
|
199
196
|
error: null,
|
|
200
197
|
priority,
|
|
201
198
|
};
|
|
202
|
-
await appendAttemptEvent(db, registry, deliveryAttemptId, queuedEntry);
|
|
199
|
+
await appendAttemptEvent(run.db, registry, deliveryAttemptId, queuedEntry);
|
|
203
200
|
await run.jobDispatcher.dispatch(channel.render ? DeliveryJobs.render : DeliveryJobs.send, {
|
|
204
201
|
channelName: channel.name,
|
|
205
202
|
address,
|
|
@@ -276,7 +273,7 @@ export function createDeliveryService(options) {
|
|
|
276
273
|
// route address with no user account) — same suppression rule either way:
|
|
277
274
|
// no blind-index key configured means no hash, so nothing to look up. One
|
|
278
275
|
// query per notify() call, not per recipient and channel.
|
|
279
|
-
async function loadSuppressedAddresses(tenantId, notificationType, priority, addresses) {
|
|
276
|
+
async function loadSuppressedAddresses(run, tenantId, notificationType, priority, addresses) {
|
|
280
277
|
// Critical priority skips address opt-outs, same rule as user preferences.
|
|
281
278
|
if (priority === "critical")
|
|
282
279
|
return new Set();
|
|
@@ -286,32 +283,32 @@ export function createDeliveryService(options) {
|
|
|
286
283
|
if (addressHash !== undefined)
|
|
287
284
|
addressHashes.add(addressHash);
|
|
288
285
|
}
|
|
289
|
-
return selectOptedOutAddresses(db, tenantId, notificationType, [...addressHashes]);
|
|
286
|
+
return selectOptedOutAddresses(run.db, tenantId, notificationType, [...addressHashes]);
|
|
290
287
|
}
|
|
291
288
|
function isAddressSuppressed(suppressed, address, channelName) {
|
|
292
289
|
const addressHash = hashUnsubscribeAddress(address);
|
|
293
290
|
return addressHash !== undefined && suppressed.has(addressOptOutKey(addressHash, channelName));
|
|
294
291
|
}
|
|
295
292
|
const recipientChannelKey = (userId, channelName) => `${userId}|${channelName}`;
|
|
296
|
-
async function isChannelDeliverable(userId, tenantId, notificationType, channelName) {
|
|
293
|
+
async function isChannelDeliverable(run, userId, tenantId, notificationType, channelName) {
|
|
297
294
|
if (isChannelKilled && (await isChannelKilled(tenantId, channelName)))
|
|
298
295
|
return false;
|
|
299
|
-
return isChannelEnabled(userId, tenantId, notificationType, channelName);
|
|
296
|
+
return isChannelEnabled(run, userId, tenantId, notificationType, channelName);
|
|
300
297
|
}
|
|
301
298
|
// Resolves every user's channel addresses up front so the opt-out lookup can
|
|
302
299
|
// cover them all at once. A resolve that throws stays out of the map: the
|
|
303
300
|
// delivery loop resolves it again and reports channel_error as before.
|
|
304
301
|
// Channels the kill switch or the user's preferences disable are never resolved
|
|
305
302
|
// here: resolve may have side effects and deliverToUser skips those channels.
|
|
306
|
-
async function prefetchUserAddresses(userIds, tenantId, notificationType) {
|
|
303
|
+
async function prefetchUserAddresses(run, userIds, tenantId, notificationType) {
|
|
307
304
|
const addresses = new Map();
|
|
308
|
-
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
|
|
305
|
+
const channelCtx = buildChannelContext(run.db, registry, sseBroker, tenantId, secrets, tenantConfigFor?.(tenantId), run.filesForTenant);
|
|
309
306
|
for (const userId of userIds) {
|
|
310
307
|
for (const channel of channels) {
|
|
311
308
|
if (!channel.resolve)
|
|
312
309
|
continue;
|
|
313
310
|
try {
|
|
314
|
-
if (!(await isChannelDeliverable(userId, tenantId, notificationType, channel.name))) {
|
|
311
|
+
if (!(await isChannelDeliverable(run, userId, tenantId, notificationType, channel.name))) {
|
|
315
312
|
continue;
|
|
316
313
|
}
|
|
317
314
|
addresses.set(recipientChannelKey(userId, channel.name), await channel.resolve(userId, channelCtx));
|
|
@@ -332,8 +329,8 @@ export function createDeliveryService(options) {
|
|
|
332
329
|
// { type: "*", channel: "inApp", enabled: false } disables inApp globally
|
|
333
330
|
// { type: "orderAssigned", channel: "*", enabled: true } enables orderAssigned everywhere
|
|
334
331
|
// → orderAssigned on inApp: disabled (conservative) unless an exact entry overrides.
|
|
335
|
-
async function isChannelEnabled(userId, tenantId, notificationType, channelName) {
|
|
336
|
-
const prefs = await selectNotificationPreferences(db, tenantId, userId, notificationType, channelName);
|
|
332
|
+
async function isChannelEnabled(run, userId, tenantId, notificationType, channelName) {
|
|
333
|
+
const prefs = await selectNotificationPreferences(run.db, tenantId, userId, notificationType, channelName);
|
|
337
334
|
if (prefs.length === 0)
|
|
338
335
|
return true;
|
|
339
336
|
// Exact match (both specific) wins over any wildcard
|
|
@@ -345,7 +342,7 @@ export function createDeliveryService(options) {
|
|
|
345
342
|
return !prefs.some((p) => p.enabled === false);
|
|
346
343
|
}
|
|
347
344
|
async function deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetchedAddresses, suppressed) {
|
|
348
|
-
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
|
|
345
|
+
const channelCtx = buildChannelContext(run.db, registry, sseBroker, tenantId, secrets, tenantConfigFor?.(tenantId), run.filesForTenant);
|
|
349
346
|
for (const channel of channels) {
|
|
350
347
|
// Route-only channel (no per-user address): not a user-notification target.
|
|
351
348
|
if (!channel.resolve)
|
|
@@ -370,7 +367,7 @@ export function createDeliveryService(options) {
|
|
|
370
367
|
}
|
|
371
368
|
// Check preferences (critical priority skips preference check)
|
|
372
369
|
if (priority !== "critical") {
|
|
373
|
-
const enabled = await isChannelEnabled(userId, tenantId, notificationType, channel.name);
|
|
370
|
+
const enabled = await isChannelEnabled(run, userId, tenantId, notificationType, channel.name);
|
|
374
371
|
if (!enabled) {
|
|
375
372
|
await logDelivery(run, {
|
|
376
373
|
tenantId,
|
|
@@ -464,8 +461,8 @@ export function createDeliveryService(options) {
|
|
|
464
461
|
}
|
|
465
462
|
}
|
|
466
463
|
async function deliverDirect(route, notificationType, data, tenantId, priority, recipientId, run) {
|
|
467
|
-
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
|
|
468
|
-
const suppressed = await loadSuppressedAddresses(tenantId, notificationType, priority, Object.values(route));
|
|
464
|
+
const channelCtx = buildChannelContext(run.db, registry, sseBroker, tenantId, secrets, tenantConfigFor?.(tenantId), run.filesForTenant);
|
|
465
|
+
const suppressed = await loadSuppressedAddresses(run, tenantId, notificationType, priority, Object.values(route));
|
|
469
466
|
// Direct routing skips preferences (no user account) but NOT rate limit
|
|
470
467
|
// — direct sends can still be abused (webhook replays, test harnesses).
|
|
471
468
|
for (const channel of channels) {
|
|
@@ -549,29 +546,38 @@ export function createDeliveryService(options) {
|
|
|
549
546
|
userIds = [to];
|
|
550
547
|
}
|
|
551
548
|
else if ("tenant" in to) {
|
|
552
|
-
userIds = await resolveUserIdsForTenant(to.tenant);
|
|
549
|
+
userIds = await resolveUserIdsForTenant(run, to.tenant);
|
|
553
550
|
}
|
|
554
551
|
else {
|
|
555
552
|
userIds = to;
|
|
556
553
|
}
|
|
557
554
|
const prefetchedAddresses = priority === "critical"
|
|
558
555
|
? new Map()
|
|
559
|
-
: await prefetchUserAddresses(userIds, tenantId, notificationType);
|
|
560
|
-
const suppressed = await loadSuppressedAddresses(tenantId, notificationType, priority, [...prefetchedAddresses.values()].filter((address) => !!address));
|
|
556
|
+
: await prefetchUserAddresses(run, userIds, tenantId, notificationType);
|
|
557
|
+
const suppressed = await loadSuppressedAddresses(run, tenantId, notificationType, priority, [...prefetchedAddresses.values()].filter((address) => !!address));
|
|
561
558
|
for (const userId of userIds) {
|
|
562
559
|
await deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetchedAddresses, suppressed);
|
|
563
560
|
}
|
|
564
561
|
}
|
|
565
562
|
}
|
|
563
|
+
function dispatched(run) {
|
|
564
|
+
return { kind: "dispatched", deliveries: run.deliveries };
|
|
565
|
+
}
|
|
566
566
|
return {
|
|
567
|
-
async notify(notificationType, options, _user, tenantId, jobDispatcher) {
|
|
567
|
+
async notify(notificationType, options, _user, tenantId, jobDispatcher, deliveryDb, fileProviderResolver) {
|
|
568
|
+
assertNotifyTargetsOwnTenant(options, tenantId);
|
|
569
|
+
assertNotificationVariables(registry, notificationType, options.data);
|
|
568
570
|
const { route, idempotencyKey } = options;
|
|
569
571
|
const priority = options.priority ?? "normal";
|
|
570
572
|
const run = {
|
|
573
|
+
db: deliveryDb ?? db,
|
|
571
574
|
deliveries: [],
|
|
572
575
|
jobDispatcher: options.immediate ? undefined : (jobDispatcher ?? jobRunner),
|
|
573
576
|
locale: options.locale,
|
|
574
577
|
jobIdempotencyKey: jobIdempotencyKeyFor(idempotencyKey, route),
|
|
578
|
+
filesForTenant: fileProviderResolver === undefined
|
|
579
|
+
? filesForTenant
|
|
580
|
+
: (fileTenantId) => fileContextFromResolver(fileProviderResolver, fileTenantId),
|
|
575
581
|
};
|
|
576
582
|
if (idempotencyKey) {
|
|
577
583
|
const first = await claimIdempotency(tenantId, idempotencyKey);
|
|
@@ -586,7 +592,7 @@ export function createDeliveryService(options) {
|
|
|
586
592
|
error: "duplicate_idempotency_key",
|
|
587
593
|
priority,
|
|
588
594
|
});
|
|
589
|
-
return
|
|
595
|
+
return dispatched(run);
|
|
590
596
|
}
|
|
591
597
|
}
|
|
592
598
|
try {
|
|
@@ -602,7 +608,7 @@ export function createDeliveryService(options) {
|
|
|
602
608
|
throw err;
|
|
603
609
|
}
|
|
604
610
|
await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run);
|
|
605
|
-
return
|
|
611
|
+
return dispatched(run);
|
|
606
612
|
},
|
|
607
613
|
};
|
|
608
614
|
}
|
package/dist/delivery/feature.js
CHANGED
|
@@ -19,7 +19,7 @@ export function createDeliveryFeature(options) {
|
|
|
19
19
|
const resolvedAccess = options?.access ?? access.admin;
|
|
20
20
|
const attemptLogRetentionDays = resolveAttemptLogRetentionDays(options?.attemptLogRetentionDays);
|
|
21
21
|
return defineFeature("delivery", (r) => {
|
|
22
|
-
r.describe(
|
|
22
|
+
r.describe('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.');
|
|
23
23
|
r.uiHints({
|
|
24
24
|
displayLabel: "Notifications \u00b7 Dispatch Core",
|
|
25
25
|
category: "notifications",
|
package/dist/delivery/index.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
export { sanitizePdfAttachmentFileName } from "../shared/index.js";
|
|
1
2
|
export { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
3
|
+
export { type EmailAttachmentRef, emailAttachmentRefSchema, type FileAttachmentRef, fileAttachmentRefSchema, type InlineAttachmentRef, inlineAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, MAX_RENDERED_PDF_VARIABLES_BYTES, type RenderedPdfAttachmentRef, renderedPdfAttachmentRefSchema, toAttachmentRef, } from "./attachment-ref.js";
|
|
2
4
|
export { type AttemptLogRetentionOptions, DEFAULT_ATTEMPT_LOG_RETENTION_DAYS, runAttemptLogRetention, } from "./attempt-log-retention.js";
|
|
5
|
+
export { createEmailAttachmentReader, createTemplateMailRenderer } from "./channel-context.js";
|
|
3
6
|
export { type ChatWebhookChannelOptions, chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
|
|
4
7
|
export { type ChatSendFailureCode, type ChatSendResult, type ChatWebhookRequest, type ChatWebhookResponse, type ChatWebhookTarget, chatConnectionNameSchema, chatWebhookUrlSchema, checkChatWebhookTarget, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
|
|
5
8
|
export type { DeliveryStatusValue } from "./constants.js";
|
|
6
|
-
export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, } from "./constants.js";
|
|
9
|
+
export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, EMAIL_CHANNEL_NAME, } from "./constants.js";
|
|
7
10
|
export { collectChannels, createDeliveryService, type DeliveryServiceOptions, type KillSwitchResolver, type RateLimitConfig, } from "./delivery-service.js";
|
|
8
11
|
export { createDeliveryFeature, type DeliveryFeatureOptions } from "./feature.js";
|
|
9
12
|
export { createDeliveryNotifyFactory } from "./notify-factory.js";
|
|
10
13
|
export { deliveryAttemptsTable, notificationAddressOptOutEntity, notificationAddressOptOutsTable, notificationPreferenceEntity, notificationPreferencesTable, } from "./tables.js";
|
|
11
14
|
export { type CreateDeliveryTestContextOptions, createDeliveryTestContext } from "./testing.js";
|
|
12
|
-
export { type ChannelContext, type ChannelMessage, type ChannelResult, type DeliveryChannel, type DeliveryChannelMode, type DeliveryChannelPlugin, type DeliveryLogEntry, type DeliveryService, isDeliveryChannelPlugin, type
|
|
15
|
+
export { type AttachmentOrigin, type ChannelContext, type ChannelMessage, type ChannelResult, type DeliveryChannel, type DeliveryChannelMode, type DeliveryChannelPlugin, type DeliveryLogEntry, type DeliveryService, isDeliveryChannelPlugin, type RenderedMessage, } from "./types.js";
|
|
13
16
|
export { type AddressUnsubscribeTokenPayload, createUnsubscribeRoutes, DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE, signAddressUnsubscribeToken, signUnsubscribeToken, type UnsubscribeRouteOptions, type UnsubscribeTokenPayload, } from "./unsubscribe.js";
|
package/dist/delivery/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
export { sanitizePdfAttachmentFileName } from "../shared/index.js";
|
|
1
2
|
export { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
3
|
+
export { emailAttachmentRefSchema, fileAttachmentRefSchema, inlineAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, MAX_RENDERED_PDF_VARIABLES_BYTES, renderedPdfAttachmentRefSchema, toAttachmentRef, } from "./attachment-ref.js";
|
|
2
4
|
export { DEFAULT_ATTEMPT_LOG_RETENTION_DAYS, runAttemptLogRetention, } from "./attempt-log-retention.js";
|
|
5
|
+
export { createEmailAttachmentReader, createTemplateMailRenderer } from "./channel-context.js";
|
|
3
6
|
export { chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
|
|
4
7
|
export { chatConnectionNameSchema, chatWebhookUrlSchema, checkChatWebhookTarget, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
|
|
5
|
-
export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, } from "./constants.js";
|
|
8
|
+
export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, EMAIL_CHANNEL_NAME, } from "./constants.js";
|
|
6
9
|
export { collectChannels, createDeliveryService, } from "./delivery-service.js";
|
|
7
10
|
export { createDeliveryFeature } from "./feature.js";
|
|
8
11
|
export { createDeliveryNotifyFactory } from "./notify-factory.js";
|