@cosmicdrift/kumiko-bundled-features 0.363.0 → 0.365.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/audit/feature.js +1 -1
- package/dist/audit/handlers/list.query.js +9 -5
- package/dist/audit/run-escape-hatch-retention.d.ts +1 -0
- package/dist/audit/run-escape-hatch-retention.js +39 -12
- package/dist/auth-email-password/bootstrap.d.ts +9 -8
- package/dist/auth-email-password/bootstrap.js +12 -4
- package/dist/auth-email-password/email-rate-limits.d.ts +4 -0
- package/dist/auth-email-password/email-rate-limits.js +13 -0
- package/dist/auth-email-password/feature.js +2 -0
- package/dist/auth-email-password/handlers/change-password.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/change-password.write.js +6 -2
- package/dist/auth-email-password/handlers/confirm-account-unlock.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -3
- package/dist/auth-email-password/handlers/invite-accept.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/invite-create.write.d.ts +4 -5
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -1
- package/dist/auth-email-password/handlers/invite-signup-complete.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/login.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/logout.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/logout.write.js +6 -2
- package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/reset-password.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/signup-confirm.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +2 -1
- package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.d.ts +2 -1
- package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +1 -1
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -1
- package/dist/auth-email-password/handlers/token-request-handler.js +2 -1
- package/dist/auth-email-password/handlers/verify-email.write.d.ts +1 -1
- package/dist/auth-email-password/magic-link-mail.js +6 -3
- package/dist/auth-email-password/mail-i18n.d.ts +3 -0
- package/dist/auth-email-password/mail-i18n.js +10 -0
- package/dist/auth-email-password/web/auth-gate.js +16 -4
- package/dist/auth-mfa/handlers/disable.write.d.ts +1 -1
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.d.ts +1 -1
- package/dist/auth-mfa/handlers/enable-confirm.write.d.ts +1 -1
- package/dist/auth-mfa/handlers/enable-start-preauth.write.d.ts +1 -1
- package/dist/auth-mfa/handlers/enable-start.write.d.ts +1 -1
- package/dist/auth-mfa/handlers/regenerate-recovery.write.d.ts +1 -1
- package/dist/auth-mfa/handlers/verify.write.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +52 -5
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +21 -2
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +1 -1
- package/dist/billing-foundation/consumer-protection/register-termination.js +11 -1
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +5 -3
- package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -2
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +6 -0
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +34 -0
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +2 -0
- package/dist/billing-foundation/consumer-protection/termination-record.js +15 -2
- package/dist/billing-foundation/consumer-protection/termination-request.js +30 -20
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/termination-texts.js +6 -0
- package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +10 -0
- package/dist/billing-foundation/consumer-protection/withdrawal-period.js +26 -0
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +11 -5
- package/dist/billing-foundation/handlers/process-event.write.js +10 -6
- package/dist/billing-foundation/i18n.js +9 -0
- package/dist/billing-foundation/subscription-tier-sync.d.ts +7 -1
- package/dist/billing-foundation/subscription-tier-sync.js +21 -7
- package/dist/billing-foundation/types.d.ts +7 -2
- package/dist/cap-counter/book-cap-usage.js +8 -0
- package/dist/cap-counter/calendar-cap-usage-reservation.js +2 -0
- package/dist/cap-counter/cap-reservation.js +7 -3
- package/dist/cap-counter/enforce-cap.d.ts +18 -1
- package/dist/cap-counter/enforce-cap.js +49 -6
- package/dist/cap-counter/handlers/increment-rolling.write.d.ts +1 -0
- package/dist/cap-counter/handlers/increment-rolling.write.js +2 -0
- package/dist/cap-counter/index.d.ts +1 -1
- package/dist/cap-counter/index.js +1 -1
- package/dist/cap-counter/with-cap-enforcement.d.ts +2 -0
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +3 -1
- package/dist/cap-overview/handlers/tenant-options.query.js +3 -1
- package/dist/channel-discord/discord-channel.js +3 -2
- package/dist/channel-email/email-channel.js +40 -23
- package/dist/channel-email/smtp-transport.js +6 -1
- package/dist/channel-in-app/handlers/mark-all-read.write.d.ts +1 -1
- package/dist/channel-in-app/handlers/mark-read.write.d.ts +1 -1
- package/dist/channel-slack/slack-channel.js +3 -2
- package/dist/channel-teams/teams-channel.js +3 -2
- package/dist/compliance-profiles/handlers/set-profile.write.d.ts +1 -1
- package/dist/config/handlers/reset.write.d.ts +1 -1
- package/dist/config/handlers/set.write.d.ts +1 -1
- package/dist/config/handlers/set.write.js +2 -2
- package/dist/config/resolver.d.ts +1 -0
- package/dist/config/resolver.js +11 -14
- package/dist/config/write-helpers.d.ts +2 -0
- package/dist/config/write-helpers.js +19 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
- package/dist/crypto-shredding/handlers/forget-subject.write.js +30 -20
- package/dist/custom-fields/db/queries/retention.d.ts +1 -1
- package/dist/custom-fields/db/queries/retention.js +2 -1
- package/dist/custom-fields/handlers/update-tenant-field.write.js +0 -1
- package/dist/data-retention/run-retention-cleanup.js +4 -5
- package/dist/delivery/attachment-ref.d.ts +2 -0
- package/dist/delivery/attachment-ref.js +30 -1
- package/dist/delivery/channel-context.js +19 -1
- package/dist/delivery/delivery-service.js +71 -82
- package/dist/delivery/handlers/resubscribe-address.write.d.ts +1 -1
- package/dist/delivery/handlers/resubscribe-user.write.d.ts +1 -1
- package/dist/delivery/handlers/set-preference.write.d.ts +1 -1
- package/dist/delivery/handlers/unsubscribe-address.write.d.ts +1 -1
- package/dist/delivery/handlers/unsubscribe-user.write.d.ts +1 -1
- package/dist/delivery/index.d.ts +1 -1
- package/dist/delivery/index.js +1 -1
- package/dist/delivery/jobs.js +43 -11
- package/dist/delivery/types.d.ts +7 -1
- package/dist/feature-toggles/handlers/set.write.d.ts +1 -1
- package/dist/feature-toggles/toggle-runtime.d.ts +5 -3
- package/dist/feature-toggles/toggle-runtime.js +25 -12
- package/dist/file-derivatives/handlers/public-variant.query.js +1 -1
- package/dist/file-foundation/read-file-ref-content.d.ts +1 -1
- package/dist/file-foundation/read-file-ref-content.js +6 -4
- package/dist/files-tenant-data/hooks.js +4 -0
- package/dist/files-tenant-data/index.d.ts +1 -0
- package/dist/files-tenant-data/index.js +2 -1
- package/dist/form-draft/db/queries/owned-file-refs.d.ts +1 -1
- package/dist/form-draft/db/queries/owned-file-refs.js +9 -2
- package/dist/form-draft/handlers/discard.write.d.ts +1 -1
- package/dist/form-draft/handlers/discard.write.js +1 -1
- package/dist/form-draft/handlers/save.write.d.ts +1 -1
- package/dist/inbound-mail-foundation/oauth-access-token.js +8 -3
- package/dist/inbound-mail-foundation/watch-supervisor.js +3 -1
- package/dist/jobs/db/queries/stale-run-sweep.d.ts +1 -1
- package/dist/jobs/db/queries/stale-run-sweep.js +12 -5
- package/dist/jobs/feature.js +5 -3
- package/dist/jobs/handlers/list.query.js +3 -1
- package/dist/jobs/handlers/retry.write.d.ts +1 -1
- package/dist/jobs/handlers/stale-run-sweep.job.d.ts +1 -1
- package/dist/jobs/handlers/stale-run-sweep.job.js +2 -2
- package/dist/jobs/handlers/trigger.write.d.ts +1 -1
- package/dist/jobs/job-run-logger.js +35 -10
- package/dist/jobs/tenant-job-run-table.js +3 -2
- package/dist/ledger/entity.js +3 -1
- package/dist/managed-pages/handlers/set.write.d.ts +1 -1
- package/dist/metrics/filters.d.ts +16 -1
- package/dist/metrics/filters.js +27 -11
- package/dist/personal-access-tokens/handlers/create.write.d.ts +1 -1
- package/dist/personal-access-tokens/handlers/revoke.write.d.ts +1 -1
- package/dist/personal-access-tokens/revoke-for-user.js +3 -7
- package/dist/renderer-foundation/types.d.ts +2 -0
- package/dist/renderer-pdf/render-table-pdf.d.ts +27 -33
- package/dist/renderer-pdf/render-table-pdf.js +95 -34
- package/dist/secrets/feature.js +4 -3
- package/dist/secrets/handlers/delete.write.d.ts +1 -1
- package/dist/secrets/handlers/delete.write.js +3 -1
- package/dist/secrets/handlers/set.write.d.ts +1 -1
- package/dist/secrets/secrets-context.js +15 -3
- package/dist/secrets/write-gate.d.ts +3 -1
- package/dist/secrets/write-gate.js +6 -3
- package/dist/sessions/handlers/revoke-all-for-user.write.d.ts +1 -1
- package/dist/sessions/handlers/revoke-all-others.write.d.ts +1 -1
- package/dist/sessions/handlers/revoke.write.d.ts +1 -1
- package/dist/shared/chunked-entity-migration.d.ts +1 -0
- package/dist/shared/chunked-entity-migration.js +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/mail-template-body.d.ts +3 -0
- package/dist/shared/mail-template-body.js +10 -0
- package/dist/shared/resource-image-formats.d.ts +23 -0
- package/dist/shared/resource-image-formats.js +21 -0
- package/dist/shared/sharp-input-limits.js +7 -8
- package/dist/step-dispatcher/webhook-runner.js +2 -2
- package/dist/subscription-stripe/plugin-methods.js +18 -8
- package/dist/subscription-stripe/verify-webhook.d.ts +1 -1
- package/dist/template-resolver/api.js +3 -2
- package/dist/template-resolver/compile-document-body.d.ts +7 -0
- package/dist/template-resolver/compile-document-body.js +114 -21
- package/dist/template-resolver/compile-steps-block.d.ts +10 -0
- package/dist/template-resolver/compile-steps-block.js +45 -0
- package/dist/template-resolver/constants.d.ts +6 -3
- package/dist/template-resolver/constants.js +13 -2
- package/dist/template-resolver/document-sanitizer.js +3 -0
- package/dist/template-resolver/document-schema.d.ts +3 -2
- package/dist/template-resolver/document-schema.js +29 -4
- package/dist/template-resolver/document-steps.d.ts +17 -0
- package/dist/template-resolver/document-steps.js +93 -0
- package/dist/template-resolver/feature.d.ts +2 -0
- package/dist/template-resolver/feature.js +13 -1
- package/dist/template-resolver/handlers/bundle-import.d.ts +7 -1
- package/dist/template-resolver/handlers/bundle-import.js +63 -38
- package/dist/template-resolver/handlers/collection-set.write.d.ts +1 -1
- package/dist/template-resolver/handlers/collection-set.write.js +4 -3
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +1 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +95 -49
- package/dist/template-resolver/handlers/diff-to-system.query.js +5 -38
- package/dist/template-resolver/handlers/discard-draft.write.d.ts +13 -0
- package/dist/template-resolver/handlers/discard-draft.write.js +37 -0
- package/dist/template-resolver/handlers/export-bundle.query.js +51 -1
- package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
- package/dist/template-resolver/handlers/find-by-id.query.js +6 -3
- package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +1 -1
- package/dist/template-resolver/handlers/import-bundle-system.write.js +16 -5
- package/dist/template-resolver/handlers/import-bundle.write.d.ts +1 -1
- package/dist/template-resolver/handlers/import-bundle.write.js +11 -1
- package/dist/template-resolver/handlers/layout-source.query.d.ts +5 -0
- package/dist/template-resolver/handlers/layout-source.query.js +46 -23
- package/dist/template-resolver/handlers/list.query.js +4 -1
- package/dist/template-resolver/handlers/override-diff.d.ts +20 -0
- package/dist/template-resolver/handlers/override-diff.js +41 -0
- package/dist/template-resolver/handlers/overview.query.d.ts +9 -0
- package/dist/template-resolver/handlers/overview.query.js +60 -12
- package/dist/template-resolver/handlers/reset-to-system.write.d.ts +1 -1
- package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +1 -1
- package/dist/template-resolver/handlers/resource-remove.write.d.ts +1 -1
- package/dist/template-resolver/handlers/resource-upload.write.d.ts +2 -2
- package/dist/template-resolver/handlers/resource-upload.write.js +36 -6
- package/dist/template-resolver/handlers/send-test.write.d.ts +3 -1
- package/dist/template-resolver/handlers/send-test.write.js +37 -10
- package/dist/template-resolver/handlers/set.write.d.ts +1 -1
- package/dist/template-resolver/handlers/template-gates.js +13 -2
- package/dist/template-resolver/handlers/template-write.js +36 -4
- package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +32 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.js +80 -16
- package/dist/template-resolver/handlers/toggle-status.write.d.ts +17 -4
- package/dist/template-resolver/handlers/toggle-status.write.js +35 -19
- package/dist/template-resolver/handlers/upsert-system.write.d.ts +1 -1
- package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +1 -1
- package/dist/template-resolver/handlers/upsert-tenant.write.js +1 -1
- package/dist/template-resolver/handlers/usage.query.js +11 -2
- package/dist/template-resolver/handlers/validate.query.js +4 -1
- package/dist/template-resolver/index.d.ts +5 -4
- package/dist/template-resolver/index.js +5 -4
- package/dist/template-resolver/layout-appearance.d.ts +10 -0
- package/dist/template-resolver/layout-appearance.js +36 -0
- package/dist/template-resolver/mail-constants.d.ts +2 -0
- package/dist/template-resolver/mail-constants.js +3 -0
- package/dist/template-resolver/pdf-constants.d.ts +7 -0
- package/dist/template-resolver/pdf-constants.js +8 -0
- package/dist/template-resolver/qualified-names.d.ts +4 -0
- package/dist/template-resolver/qualified-names.js +4 -0
- package/dist/template-resolver/resource-image.d.ts +2 -9
- package/dist/template-resolver/resource-image.js +2 -11
- package/dist/template-resolver/seeding.js +23 -4
- package/dist/template-resolver/store-template-resource.d.ts +53 -9
- package/dist/template-resolver/store-template-resource.js +250 -97
- package/dist/template-resolver/table.d.ts +103 -0
- package/dist/template-resolver/table.js +9 -2
- package/dist/template-resolver/template-bundle.js +4 -0
- package/dist/template-resolver/template-draft.d.ts +54 -0
- package/dist/template-resolver/template-draft.js +72 -0
- package/dist/template-resolver/template-meta.d.ts +35 -6
- package/dist/template-resolver/template-meta.js +67 -16
- package/dist/template-resolver/template-preview.d.ts +19 -2
- package/dist/template-resolver/template-preview.js +98 -55
- package/dist/template-resolver/template-resources.js +3 -4
- package/dist/template-resolver/template-usage.d.ts +5 -1
- package/dist/template-resolver/template-usage.js +14 -9
- package/dist/template-resolver/template-validation.js +13 -9
- package/dist/template-resolver/web/client-plugin.js +4 -2
- package/dist/template-resolver/web/document-editor-extensions.d.ts +5 -1
- package/dist/template-resolver/web/document-editor-extensions.js +76 -13
- package/dist/template-resolver/web/document-editor.d.ts +2 -1
- package/dist/template-resolver/web/document-editor.js +12 -2
- package/dist/template-resolver/web/document-tiptap-editor.d.ts +1 -1
- package/dist/template-resolver/web/document-tiptap-editor.js +173 -34
- package/dist/template-resolver/web/document-toolbar-blocks.d.ts +13 -5
- package/dist/template-resolver/web/document-toolbar-blocks.js +6 -0
- package/dist/template-resolver/web/i18n.js +24 -4
- package/dist/template-resolver/web/index.d.ts +2 -0
- package/dist/template-resolver/web/index.js +2 -0
- package/dist/template-resolver/web/placeholder-line-editor.d.ts +6 -2
- package/dist/template-resolver/web/placeholder-line-editor.js +9 -7
- package/dist/tenant/handlers/add-member.write.d.ts +1 -1
- package/dist/tenant/handlers/cancel-invitation.write.d.ts +1 -1
- package/dist/tenant/handlers/create.write.d.ts +1 -1
- package/dist/tenant/handlers/remove-member.write.d.ts +1 -1
- package/dist/tenant/handlers/toggle-enabled.write.d.ts +2 -2
- package/dist/tenant/handlers/update-member-roles.write.d.ts +1 -1
- package/dist/tenant/handlers/update-member-roles.write.js +3 -6
- package/dist/tenant/handlers/update.write.d.ts +1 -1
- package/dist/tenant/seeding.js +23 -24
- package/dist/tenant-handover/handlers/claim.write.d.ts +1 -1
- package/dist/tenant-handover/move-entity-graph.js +14 -4
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
- package/dist/tenant-lifecycle/handlers/request-destruction.write.d.ts +1 -1
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +1 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +8 -1
- package/dist/tenant-lifecycle/stages.js +3 -2
- package/dist/tier-engine/constants.d.ts +2 -0
- package/dist/tier-engine/constants.js +4 -0
- package/dist/tier-engine/entity.d.ts +15 -14
- package/dist/tier-engine/entity.js +4 -3
- package/dist/tier-engine/feature.js +9 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +6 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +7 -4
- package/dist/tier-engine/index.d.ts +1 -1
- package/dist/tier-engine/index.js +1 -1
- package/dist/tier-engine/tier-resolver.d.ts +13 -13
- package/dist/user/constants.d.ts +1 -0
- package/dist/user/handlers/create.write.d.ts +1 -1
- package/dist/user/handlers/update.write.d.ts +1 -1
- package/dist/user/invited-global-roles.d.ts +2 -4
- package/dist/user/resolve-display-names.js +5 -9
- package/dist/user/seeding.js +20 -1
- package/dist/user-data-rights/email-templates.js +0 -1
- package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/lift-restriction.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/request-deletion.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/request-export.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/restrict-account.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.d.ts +1 -1
- package/dist/user-data-rights/i18n.js +5 -0
- package/dist/user-data-rights/lib/update-user-lifecycle.d.ts +2 -0
- package/dist/user-data-rights/lib/update-user-lifecycle.js +6 -2
- package/dist/user-data-rights/run-forget-cleanup.js +2 -2
- package/dist/user-profile/handlers/change-email.write.d.ts +1 -1
- package/dist/waitlist/feature.js +1 -1
- package/dist/waitlist/handlers/invite.write.d.ts +1 -1
- package/dist/waitlist/handlers/invite.write.js +21 -3
- package/dist/waitlist/handlers/reject.write.d.ts +1 -1
- package/dist/waitlist/handlers/reject.write.js +3 -16
- package/dist/waitlist/handlers/submit.write.d.ts +1 -1
- package/dist/waitlist/handlers/submit.write.js +29 -7
- package/dist/waitlist/i18n.js +9 -0
- package/dist/waitlist/lib.d.ts +2 -1
- package/dist/waitlist/lib.js +15 -0
- package/dist/waitlist/options.js +1 -1
- package/dist/workflow-runner/event-trigger.js +2 -2
- package/dist/workflow-runner/feature.js +1 -1
- package/dist/workflow-runner/handlers/resume-run.write.js +7 -2
- package/package.json +19 -10
- package/src/audit/changes.json +18 -0
- package/src/auth-email-password/changes.json +55 -0
- package/src/billing-foundation/changes.json +48 -0
- package/src/cap-counter/changes.json +19 -0
- package/src/cap-overview/changes.json +6 -0
- package/src/channel-email/changes.json +18 -0
- package/src/config/changes.json +6 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/custom-fields/changes.json +6 -0
- package/src/delivery/changes.json +30 -0
- package/src/document-ingest-foundation/changes.json +6 -0
- package/src/feature-toggles/changes.json +7 -0
- package/src/file-foundation/changes.json +6 -0
- package/src/files-tenant-data/changes.json +6 -0
- package/src/inbound-mail-foundation/changes.json +6 -0
- package/src/jobs/changes.json +31 -0
- package/src/ledger/changes.json +7 -0
- package/src/metrics/changes.json +6 -0
- package/src/personal-access-tokens/changes.json +6 -0
- package/src/renderer-pdf/changes.json +12 -0
- package/src/secrets/changes.json +12 -0
- package/src/shared/changes.json +6 -0
- package/src/subscription-stripe/changes.json +6 -0
- package/src/template-resolver/changes.json +149 -0
- package/src/tenant/changes.json +6 -0
- package/src/tenant-lifecycle/changes.json +6 -0
- package/src/tier-engine/changes.json +18 -0
- package/src/user/changes.json +6 -0
- package/src/user-data-rights/changes.json +12 -0
- package/src/user-profile/changes.json +1 -1
- package/src/waitlist/changes.json +18 -0
- package/src/workflow-runner/changes.json +6 -0
|
@@ -79,11 +79,20 @@ export function createDeliveryService(options) {
|
|
|
79
79
|
}
|
|
80
80
|
// A key that guarded nothing must not turn the caller's retry into a skipped duplicate.
|
|
81
81
|
// Partial success keeps it: a retry would re-send to those who got it.
|
|
82
|
-
|
|
82
|
+
// A failing release is logged and swallowed: it must neither replace the dispatch error the
|
|
83
|
+
// caller retries on nor fail a notify() that already dispatched.
|
|
84
|
+
async function releaseIdempotencyIfNothingDelivered(tenantId, key, run, notificationType) {
|
|
83
85
|
// skip: no key claimed, or a recipient already got the notification
|
|
84
86
|
if (!key || run.deliveries.some(isDeliveredOrQueued))
|
|
85
87
|
return;
|
|
86
|
-
|
|
88
|
+
try {
|
|
89
|
+
await idemRedis?.del(deliveryIdempotencyRedisKey(tenantId, key));
|
|
90
|
+
}
|
|
91
|
+
catch (releaseErr) {
|
|
92
|
+
logError.warn(`idempotency key release failed: ${redactedMessageOf(releaseErr)}`, {
|
|
93
|
+
notificationType,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
87
96
|
}
|
|
88
97
|
// Only a single-channel route lets the queued job free the key on terminal failure
|
|
89
98
|
// without risking a duplicate send on another channel.
|
|
@@ -290,35 +299,50 @@ export function createDeliveryService(options) {
|
|
|
290
299
|
return addressHash !== undefined && suppressed.has(addressOptOutKey(addressHash, channelName));
|
|
291
300
|
}
|
|
292
301
|
const recipientChannelKey = (userId, channelName) => `${userId}|${channelName}`;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
302
|
+
// Kill switch, then the user's preference, then the rate limit — the order deliverToUser reports
|
|
303
|
+
// skips in. The rate limit consumes a token, so each recipient/channel pair is evaluated once.
|
|
304
|
+
async function evaluateChannelGate(run, userId, tenantId, notificationType, channelName, priority) {
|
|
305
|
+
if (isChannelKilled && (await isChannelKilled(tenantId, channelName))) {
|
|
306
|
+
return "channel_disabled";
|
|
307
|
+
}
|
|
308
|
+
// Critical priority skips the preference check
|
|
309
|
+
if (priority !== "critical" &&
|
|
310
|
+
!(await isChannelEnabled(run, userId, tenantId, notificationType, channelName))) {
|
|
311
|
+
return "preference_disabled";
|
|
312
|
+
}
|
|
313
|
+
if (rateLimit && !(await checkRateLimit(rateLimit, tenantId, channelName))) {
|
|
314
|
+
return "rate_limited";
|
|
315
|
+
}
|
|
316
|
+
return "open";
|
|
297
317
|
}
|
|
298
318
|
// Resolves every user's channel addresses up front so the opt-out lookup can
|
|
299
|
-
// cover them all at once. A resolve that throws stays out of the map:
|
|
300
|
-
// delivery loop resolves it again and reports channel_error as before.
|
|
301
|
-
// Channels
|
|
302
|
-
// here: resolve may have side effects and deliverToUser skips those channels.
|
|
303
|
-
|
|
319
|
+
// cover them all at once. A resolve that throws stays out of the address map:
|
|
320
|
+
// the delivery loop resolves it again and reports channel_error as before.
|
|
321
|
+
// Channels that are gated (kill switch, preferences, rate limit) are never resolved
|
|
322
|
+
// here: resolve may have side effects and deliverToUser skips those channels. The
|
|
323
|
+
// gate verdict is kept so deliverToUser does not query it a second time.
|
|
324
|
+
async function prefetchRecipients(run, userIds, tenantId, notificationType, priority) {
|
|
304
325
|
const addresses = new Map();
|
|
326
|
+
const gates = new Map();
|
|
305
327
|
const channelCtx = buildChannelContext(run.db, registry, sseBroker, tenantId, secrets, tenantConfigFor?.(tenantId), run.filesForTenant);
|
|
306
328
|
for (const userId of userIds) {
|
|
307
329
|
for (const channel of channels) {
|
|
308
330
|
if (!channel.resolve)
|
|
309
331
|
continue;
|
|
332
|
+
const key = recipientChannelKey(userId, channel.name);
|
|
310
333
|
try {
|
|
311
|
-
|
|
334
|
+
const gate = await evaluateChannelGate(run, userId, tenantId, notificationType, channel.name, priority);
|
|
335
|
+
gates.set(key, gate);
|
|
336
|
+
if (gate !== "open")
|
|
312
337
|
continue;
|
|
313
|
-
|
|
314
|
-
addresses.set(recipientChannelKey(userId, channel.name), await channel.resolve(userId, channelCtx));
|
|
338
|
+
addresses.set(key, await channel.resolve(userId, channelCtx));
|
|
315
339
|
}
|
|
316
340
|
catch {
|
|
317
341
|
// skip: re-resolved (and logged) by deliverToUser
|
|
318
342
|
}
|
|
319
343
|
}
|
|
320
344
|
}
|
|
321
|
-
return addresses;
|
|
345
|
+
return { addresses, gates };
|
|
322
346
|
}
|
|
323
347
|
// Check if user has disabled this notification+channel combo.
|
|
324
348
|
// Specificity order: exact > any wildcard. When only wildcards match and they
|
|
@@ -341,68 +365,33 @@ export function createDeliveryService(options) {
|
|
|
341
365
|
// and conservative — DB ordering no longer decides the outcome).
|
|
342
366
|
return !prefs.some((p) => p.enabled === false);
|
|
343
367
|
}
|
|
344
|
-
async function deliverToUser(userId, notificationType, data, tenantId, priority, run,
|
|
368
|
+
async function deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetched, suppressed) {
|
|
345
369
|
const channelCtx = buildChannelContext(run.db, registry, sseBroker, tenantId, secrets, tenantConfigFor?.(tenantId), run.filesForTenant);
|
|
346
370
|
for (const channel of channels) {
|
|
347
371
|
// Route-only channel (no per-user address): not a user-notification target.
|
|
348
372
|
if (!channel.resolve)
|
|
349
373
|
continue;
|
|
350
374
|
const message = buildMessage(notificationType, data, channel.name, run.locale);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
// Check preferences (critical priority skips preference check)
|
|
369
|
-
if (priority !== "critical") {
|
|
370
|
-
const enabled = await isChannelEnabled(run, userId, tenantId, notificationType, channel.name);
|
|
371
|
-
if (!enabled) {
|
|
372
|
-
await logDelivery(run, {
|
|
373
|
-
tenantId,
|
|
374
|
-
notificationType,
|
|
375
|
-
channel: channel.name,
|
|
376
|
-
recipientId: userId,
|
|
377
|
-
recipientAddress: null,
|
|
378
|
-
status: "skipped",
|
|
379
|
-
error: "preference_disabled",
|
|
380
|
-
priority,
|
|
381
|
-
});
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
// Rate limiting
|
|
386
|
-
if (rateLimit) {
|
|
387
|
-
const allowed = await checkRateLimit(rateLimit, tenantId, channel.name);
|
|
388
|
-
if (!allowed) {
|
|
389
|
-
await logDelivery(run, {
|
|
390
|
-
tenantId,
|
|
391
|
-
notificationType,
|
|
392
|
-
channel: channel.name,
|
|
393
|
-
recipientId: userId,
|
|
394
|
-
recipientAddress: null,
|
|
395
|
-
status: "skipped",
|
|
396
|
-
error: "rate_limited",
|
|
397
|
-
priority,
|
|
398
|
-
});
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
375
|
+
const prefetchKey = recipientChannelKey(userId, channel.name);
|
|
376
|
+
const gate = prefetched.gates.get(prefetchKey) ??
|
|
377
|
+
(await evaluateChannelGate(run, userId, tenantId, notificationType, channel.name, priority));
|
|
378
|
+
if (gate !== "open") {
|
|
379
|
+
await logDelivery(run, {
|
|
380
|
+
tenantId,
|
|
381
|
+
notificationType,
|
|
382
|
+
channel: channel.name,
|
|
383
|
+
recipientId: userId,
|
|
384
|
+
recipientAddress: null,
|
|
385
|
+
status: "skipped",
|
|
386
|
+
error: gate,
|
|
387
|
+
priority,
|
|
388
|
+
});
|
|
389
|
+
continue;
|
|
401
390
|
}
|
|
402
391
|
try {
|
|
403
|
-
const
|
|
404
|
-
const address =
|
|
405
|
-
?
|
|
392
|
+
const wasPrefetched = prefetched.addresses.has(prefetchKey);
|
|
393
|
+
const address = wasPrefetched
|
|
394
|
+
? prefetched.addresses.get(prefetchKey)
|
|
406
395
|
: await channel.resolve(userId, channelCtx);
|
|
407
396
|
if (!address) {
|
|
408
397
|
await logDelivery(run, {
|
|
@@ -417,7 +406,12 @@ export function createDeliveryService(options) {
|
|
|
417
406
|
});
|
|
418
407
|
continue;
|
|
419
408
|
}
|
|
420
|
-
|
|
409
|
+
// A fallback resolve yields an address the batched opt-out lookup never covered.
|
|
410
|
+
const suppressedForAddress = wasPrefetched
|
|
411
|
+
? suppressed
|
|
412
|
+
: await loadSuppressedAddresses(run, tenantId, notificationType, priority, [address]);
|
|
413
|
+
if (priority !== "critical" &&
|
|
414
|
+
isAddressSuppressed(suppressedForAddress, address, channel.name)) {
|
|
421
415
|
await logDelivery(run, {
|
|
422
416
|
tenantId,
|
|
423
417
|
notificationType,
|
|
@@ -551,12 +545,12 @@ export function createDeliveryService(options) {
|
|
|
551
545
|
else {
|
|
552
546
|
userIds = to;
|
|
553
547
|
}
|
|
554
|
-
const
|
|
555
|
-
? new Map()
|
|
556
|
-
: await
|
|
557
|
-
const suppressed = await loadSuppressedAddresses(run, tenantId, notificationType, priority, [...
|
|
548
|
+
const prefetched = priority === "critical"
|
|
549
|
+
? { addresses: new Map(), gates: new Map() }
|
|
550
|
+
: await prefetchRecipients(run, userIds, tenantId, notificationType, priority);
|
|
551
|
+
const suppressed = await loadSuppressedAddresses(run, tenantId, notificationType, priority, [...prefetched.addresses.values()].filter((address) => !!address));
|
|
558
552
|
for (const userId of userIds) {
|
|
559
|
-
await deliverToUser(userId, notificationType, data, tenantId, priority, run,
|
|
553
|
+
await deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetched, suppressed);
|
|
560
554
|
}
|
|
561
555
|
}
|
|
562
556
|
}
|
|
@@ -599,15 +593,10 @@ export function createDeliveryService(options) {
|
|
|
599
593
|
await dispatchNotification(notificationType, options, tenantId, priority, run);
|
|
600
594
|
}
|
|
601
595
|
catch (err) {
|
|
602
|
-
|
|
603
|
-
await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run).catch((releaseErr) => {
|
|
604
|
-
logError.warn(`idempotency key release failed: ${redactedMessageOf(releaseErr)}`, {
|
|
605
|
-
notificationType,
|
|
606
|
-
});
|
|
607
|
-
});
|
|
596
|
+
await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run, notificationType);
|
|
608
597
|
throw err;
|
|
609
598
|
}
|
|
610
|
-
await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run);
|
|
599
|
+
await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run, notificationType);
|
|
611
600
|
return dispatched(run);
|
|
612
601
|
},
|
|
613
602
|
};
|
|
@@ -6,4 +6,4 @@ export declare const resubscribeAddressWrite: import("@cosmicdrift/kumiko-types/
|
|
|
6
6
|
}, z.core.$strip>, {
|
|
7
7
|
notificationType: string;
|
|
8
8
|
channel: string;
|
|
9
|
-
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
9
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
@@ -6,4 +6,4 @@ export declare const resubscribeUserWrite: import("@cosmicdrift/kumiko-types/def
|
|
|
6
6
|
}, z.core.$strip>, {
|
|
7
7
|
notificationType: string;
|
|
8
8
|
channel: string;
|
|
9
|
-
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
9
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
@@ -7,4 +7,4 @@ export declare const setPreferenceWrite: import("@cosmicdrift/kumiko-types/defin
|
|
|
7
7
|
notificationType: string;
|
|
8
8
|
channel: string;
|
|
9
9
|
enabled: boolean;
|
|
10
|
-
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
10
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
@@ -6,4 +6,4 @@ export declare const unsubscribeAddressWrite: import("@cosmicdrift/kumiko-types/
|
|
|
6
6
|
}, z.core.$strip>, {
|
|
7
7
|
notificationType: string;
|
|
8
8
|
channel: string;
|
|
9
|
-
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
9
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
@@ -6,4 +6,4 @@ export declare const unsubscribeUserWrite: import("@cosmicdrift/kumiko-types/def
|
|
|
6
6
|
}, z.core.$strip>, {
|
|
7
7
|
notificationType: string;
|
|
8
8
|
channel: string;
|
|
9
|
-
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
9
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
package/dist/delivery/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { sanitizePdfAttachmentFileName } from "../shared/index.js";
|
|
2
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";
|
|
3
|
+
export { DELIVERY_ENVELOPE_DATA_KEYS, type EmailAttachmentRef, emailAttachmentRefSchema, type FileAttachmentRef, fileAttachmentRefSchema, type InlineAttachmentRef, inlineAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, MAX_RENDERED_PDF_VARIABLES_BYTES, type RenderedPdfAttachmentRef, renderedPdfAttachmentRefSchema, toAttachmentRef, withoutDeliveryEnvelopeKeys, } from "./attachment-ref.js";
|
|
4
4
|
export { type AttemptLogRetentionOptions, DEFAULT_ATTEMPT_LOG_RETENTION_DAYS, runAttemptLogRetention, } from "./attempt-log-retention.js";
|
|
5
5
|
export { createEmailAttachmentReader, createTemplateMailRenderer } from "./channel-context.js";
|
|
6
6
|
export { type ChatWebhookChannelOptions, chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
|
package/dist/delivery/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { sanitizePdfAttachmentFileName } from "../shared/index.js";
|
|
2
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";
|
|
3
|
+
export { DELIVERY_ENVELOPE_DATA_KEYS, emailAttachmentRefSchema, fileAttachmentRefSchema, inlineAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, MAX_RENDERED_PDF_VARIABLES_BYTES, renderedPdfAttachmentRefSchema, toAttachmentRef, withoutDeliveryEnvelopeKeys, } from "./attachment-ref.js";
|
|
4
4
|
export { DEFAULT_ATTEMPT_LOG_RETENTION_DAYS, runAttemptLogRetention, } from "./attempt-log-retention.js";
|
|
5
5
|
export { createEmailAttachmentReader, createTemplateMailRenderer } from "./channel-context.js";
|
|
6
6
|
export { chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
|
package/dist/delivery/jobs.js
CHANGED
|
@@ -36,6 +36,8 @@ const sendJobPayloadSchema = renderJobPayloadSchema.extend({
|
|
|
36
36
|
html: z.string(),
|
|
37
37
|
subject: z.string(),
|
|
38
38
|
text: z.string().optional(),
|
|
39
|
+
fromName: z.string().optional(),
|
|
40
|
+
replyTo: z.string().optional(),
|
|
39
41
|
attachments: z.array(emailAttachmentRefSchema).max(MAX_EMAIL_ATTACHMENTS).optional(),
|
|
40
42
|
})
|
|
41
43
|
.optional(),
|
|
@@ -63,6 +65,8 @@ function toSendRendered(rendered) {
|
|
|
63
65
|
html: rendered.html,
|
|
64
66
|
subject: rendered.subject,
|
|
65
67
|
...(rendered.text !== undefined && { text: rendered.text }),
|
|
68
|
+
...(rendered.fromName !== undefined && { fromName: rendered.fromName }),
|
|
69
|
+
...(rendered.replyTo !== undefined && { replyTo: rendered.replyTo }),
|
|
66
70
|
...(rendered.attachments !== undefined && {
|
|
67
71
|
attachments: rendered.attachments.map(toAttachmentRef),
|
|
68
72
|
}),
|
|
@@ -96,12 +100,11 @@ function entryFor(p, status, error, address, confirmed) {
|
|
|
96
100
|
...(confirmed === false && { confirmed }),
|
|
97
101
|
};
|
|
98
102
|
}
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (!ctx.finalAttempt || !p.idempotencyKey)
|
|
103
|
+
// Assumes ctx.redis is the handle the service claimed on; runProdApp wires the same
|
|
104
|
+
// instance into both.
|
|
105
|
+
async function releaseIdempotencyKey(ctx, p) {
|
|
106
|
+
// skip: no idempotency key was claimed for this notification
|
|
107
|
+
if (!p.idempotencyKey)
|
|
105
108
|
return;
|
|
106
109
|
if (!ctx.redis) {
|
|
107
110
|
ctx.log.warn("delivery: idempotency key not released, job context has no redis", {
|
|
@@ -118,6 +121,26 @@ async function releaseIdempotencyOnTerminalFailure(ctx, p) {
|
|
|
118
121
|
});
|
|
119
122
|
}
|
|
120
123
|
}
|
|
124
|
+
// Only the final retry frees the key: earlier failures are retried by the runner and
|
|
125
|
+
// must still dedup.
|
|
126
|
+
async function releaseIdempotencyOnTerminalFailure(ctx, p) {
|
|
127
|
+
// skip: not the last attempt, the runner retries and the key must keep deduplicating
|
|
128
|
+
if (!ctx.finalAttempt)
|
|
129
|
+
return;
|
|
130
|
+
await releaseIdempotencyKey(ctx, p);
|
|
131
|
+
}
|
|
132
|
+
// Covers every throw after the payload parsed, not just the attempt-logged ones: a
|
|
133
|
+
// missing registry or unknown channel on the last attempt would otherwise hold the key
|
|
134
|
+
// until its TTL.
|
|
135
|
+
async function releasingKeyOnTerminalFailure(ctx, p, run) {
|
|
136
|
+
try {
|
|
137
|
+
return await run();
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
await releaseIdempotencyOnTerminalFailure(ctx, p);
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
121
144
|
// The job runner stores err.message in the run row, and BullMQ keeps failedReason and
|
|
122
145
|
// the stack trace in Redis in plain text, so the rethrown error carries only the code.
|
|
123
146
|
// The redacted message goes to the log.
|
|
@@ -128,7 +151,6 @@ async function failAttempt(ctx, db, registry, p, code, err) {
|
|
|
128
151
|
channel: p.channelName,
|
|
129
152
|
});
|
|
130
153
|
await appendAttemptEvent(db, registry, p.deliveryAttemptId, entryFor(p, "failed", code, p.address));
|
|
131
|
-
await releaseIdempotencyOnTerminalFailure(ctx, p);
|
|
132
154
|
throw new Error(code);
|
|
133
155
|
}
|
|
134
156
|
// Render the message and hand off to delivery.send. On failure: record the
|
|
@@ -136,10 +158,13 @@ async function failAttempt(ctx, db, registry, p, code, err) {
|
|
|
136
158
|
// the send step is never reached, so a stuck render can't half-send.
|
|
137
159
|
export const deliveryRenderJob = async (payload, ctx) => {
|
|
138
160
|
const p = renderJobPayloadSchema.parse(payload);
|
|
161
|
+
return releasingKeyOnTerminalFailure(ctx, p, () => renderStep(p, ctx));
|
|
162
|
+
};
|
|
163
|
+
async function renderStep(p, ctx) {
|
|
139
164
|
const tenantId = p.tenantId; // @cast-boundary engine-payload — stream tenant
|
|
140
165
|
const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
|
|
141
166
|
const channel = resolveChannel(registry, p.channelName);
|
|
142
|
-
const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets, ctx.config, (fileTenantId) => fileContextForTenant(ctx, fileTenantId));
|
|
167
|
+
const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets, ctx.config, (fileTenantId) => fileContextForTenant(ctx, ctx.systemUser.tenantId, fileTenantId));
|
|
143
168
|
let rendered;
|
|
144
169
|
try {
|
|
145
170
|
if (!channel.render) {
|
|
@@ -157,21 +182,28 @@ export const deliveryRenderJob = async (payload, ctx) => {
|
|
|
157
182
|
catch (err) {
|
|
158
183
|
return failAttempt(ctx, db, registry, p, "channel_error", err);
|
|
159
184
|
}
|
|
160
|
-
}
|
|
185
|
+
}
|
|
161
186
|
// Deliver via the channel using the rendered payload (if any). On failure:
|
|
162
187
|
// record the terminal failed event and rethrow so BullMQ retries the send with
|
|
163
188
|
// the same already-rendered HTML — no re-render needed.
|
|
164
189
|
export const deliverySendJob = async (payload, ctx) => {
|
|
165
190
|
const p = sendJobPayloadSchema.parse(payload);
|
|
191
|
+
return releasingKeyOnTerminalFailure(ctx, p, () => sendStep(p, ctx));
|
|
192
|
+
};
|
|
193
|
+
async function sendStep(p, ctx) {
|
|
166
194
|
const tenantId = p.tenantId; // @cast-boundary engine-payload — stream tenant
|
|
167
195
|
const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
|
|
168
196
|
const channel = resolveChannel(registry, p.channelName);
|
|
169
|
-
const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets, ctx.config, (fileTenantId) => fileContextForTenant(ctx, fileTenantId));
|
|
197
|
+
const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets, ctx.config, (fileTenantId) => fileContextForTenant(ctx, ctx.systemUser.tenantId, fileTenantId));
|
|
170
198
|
try {
|
|
171
199
|
const result = await channel.send(p.address, toMessage(p), channelCtx, p.rendered);
|
|
172
200
|
await appendAttemptEvent(db, registry, p.deliveryAttemptId, entryFor(p, result.status, result.error ?? null, result.address ?? p.address, result.confirmed));
|
|
201
|
+
// A soft failure ends the job successfully, so no retry follows and the caller's
|
|
202
|
+
// retry must not be skipped as a duplicate.
|
|
203
|
+
if (result.status === "failed")
|
|
204
|
+
await releaseIdempotencyKey(ctx, p);
|
|
173
205
|
}
|
|
174
206
|
catch (err) {
|
|
175
207
|
return failAttempt(ctx, db, registry, p, "send_failed", err);
|
|
176
208
|
}
|
|
177
|
-
}
|
|
209
|
+
}
|
package/dist/delivery/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
|
3
3
|
import type { DeliveryErrorCode, NotifyDeliveryStatus, NotifyDispatched, NotifyJobDispatcher, NotifyOptions, NotifyPriority, Registry, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
|
|
5
5
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
6
|
-
import type { MailPayload } from "../renderer-foundation/index.js";
|
|
6
|
+
import type { MailPayload, RenderedTemplateRef } from "../renderer-foundation/index.js";
|
|
7
7
|
import type { EmailAttachmentRef } from "./attachment-ref.js";
|
|
8
8
|
import { DELIVERY_CHANNEL_EXTENSION } from "./constants.js";
|
|
9
9
|
export type RenderedMail = {
|
|
@@ -11,7 +11,11 @@ export type RenderedMail = {
|
|
|
11
11
|
readonly html: string;
|
|
12
12
|
readonly text: string;
|
|
13
13
|
readonly previewText?: string;
|
|
14
|
+
readonly fromName?: string;
|
|
15
|
+
readonly replyTo?: string;
|
|
14
16
|
readonly attachments?: readonly EmailAttachmentRef[];
|
|
17
|
+
/** The layout version the html was rendered through; the editor preview names it. */
|
|
18
|
+
readonly layout?: RenderedTemplateRef;
|
|
15
19
|
};
|
|
16
20
|
export type ChannelContext = {
|
|
17
21
|
readonly db: TenantDb;
|
|
@@ -43,6 +47,8 @@ export type RenderedMessage = {
|
|
|
43
47
|
readonly html: string;
|
|
44
48
|
readonly subject: string;
|
|
45
49
|
readonly text?: string;
|
|
50
|
+
readonly fromName?: string;
|
|
51
|
+
readonly replyTo?: string;
|
|
46
52
|
readonly attachments?: readonly EmailAttachmentRef[];
|
|
47
53
|
};
|
|
48
54
|
export declare const DELIVERY_CHANNEL_MODES: readonly ["inline", "queued"];
|
|
@@ -7,4 +7,4 @@ export declare function createSetWriteHandler(getRuntime: (() => GlobalFeatureTo
|
|
|
7
7
|
featureName: string;
|
|
8
8
|
enabled: boolean;
|
|
9
9
|
previousEnabled: boolean | null;
|
|
10
|
-
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
10
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { type Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { type ClosableCacheSyncBus } from "@cosmicdrift/kumiko-framework/redis";
|
|
3
|
+
import { type CacheSyncBus, type ClosableCacheSyncBus } from "@cosmicdrift/kumiko-framework/redis";
|
|
4
4
|
export type ToggleSyncSignal = {
|
|
5
5
|
publish(featureName: string, enabled: boolean): void;
|
|
6
6
|
onMessage(listener: (featureName: string, enabled: boolean) => void): void;
|
|
@@ -12,14 +12,16 @@ export declare class GlobalFeatureToggleRuntime {
|
|
|
12
12
|
private readonly syncSignal?;
|
|
13
13
|
private readonly ownedBus?;
|
|
14
14
|
private snapshot;
|
|
15
|
-
private
|
|
15
|
+
private readonly pendingFlipBuffers;
|
|
16
|
+
private loadInFlight;
|
|
16
17
|
constructor(db: DbConnection, registry: Registry, syncSignal?: ToggleSyncSignal | undefined, ownedBus?: ClosableCacheSyncBus | undefined);
|
|
17
18
|
close(): Promise<void>;
|
|
18
19
|
initialize(): Promise<void>;
|
|
20
|
+
private loadSnapshot;
|
|
19
21
|
refresh(): Promise<void>;
|
|
20
22
|
apply(featureName: string, enabled: boolean): void;
|
|
21
23
|
broadcastToggle(featureName: string, enabled: boolean): void;
|
|
22
24
|
readOverride(featureName: string): boolean | undefined;
|
|
23
25
|
effectiveFeatures: () => ReadonlySet<string>;
|
|
24
26
|
}
|
|
25
|
-
export declare function createFeatureToggleRuntime(db: DbConnection, registry: Registry, syncSignal?: ToggleSyncSignal): Promise<GlobalFeatureToggleRuntime>;
|
|
27
|
+
export declare function createFeatureToggleRuntime(db: DbConnection, registry: Registry, syncSignal?: ToggleSyncSignal, cacheSyncBus?: CacheSyncBus): Promise<GlobalFeatureToggleRuntime>;
|
|
@@ -22,9 +22,11 @@ export class GlobalFeatureToggleRuntime {
|
|
|
22
22
|
syncSignal;
|
|
23
23
|
ownedBus;
|
|
24
24
|
snapshot = new Map();
|
|
25
|
-
//
|
|
26
|
-
// started before
|
|
27
|
-
|
|
25
|
+
// One buffer per requested load, open from the call until its SELECT finished: the SELECT
|
|
26
|
+
// may have started before a flip committed, so its rows can be older than the flips collected.
|
|
27
|
+
pendingFlipBuffers = new Set();
|
|
28
|
+
// Loads run one at a time so the older rows of one cannot overwrite the result of another.
|
|
29
|
+
loadInFlight = Promise.resolve();
|
|
28
30
|
constructor(db, registry, syncSignal, ownedBus) {
|
|
29
31
|
this.db = db;
|
|
30
32
|
this.registry = registry;
|
|
@@ -47,9 +49,14 @@ export class GlobalFeatureToggleRuntime {
|
|
|
47
49
|
async close() {
|
|
48
50
|
await this.ownedBus?.close();
|
|
49
51
|
}
|
|
50
|
-
|
|
52
|
+
initialize() {
|
|
51
53
|
const pendingFlips = new Map();
|
|
52
|
-
this.
|
|
54
|
+
this.pendingFlipBuffers.add(pendingFlips);
|
|
55
|
+
const load = this.loadInFlight.then(() => this.loadSnapshot(pendingFlips));
|
|
56
|
+
this.loadInFlight = load.catch(() => undefined);
|
|
57
|
+
return load;
|
|
58
|
+
}
|
|
59
|
+
async loadSnapshot(pendingFlips) {
|
|
53
60
|
try {
|
|
54
61
|
const rows = await selectMany(this.db, globalFeatureStateTable);
|
|
55
62
|
this.snapshot = new Map([
|
|
@@ -58,7 +65,7 @@ export class GlobalFeatureToggleRuntime {
|
|
|
58
65
|
]);
|
|
59
66
|
}
|
|
60
67
|
finally {
|
|
61
|
-
this.
|
|
68
|
+
this.pendingFlipBuffers.delete(pendingFlips);
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
// Re-read the full snapshot. Called from the set-handler after a
|
|
@@ -74,7 +81,8 @@ export class GlobalFeatureToggleRuntime {
|
|
|
74
81
|
// lock. Kept alongside refresh() so both options are explicit.
|
|
75
82
|
apply(featureName, enabled) {
|
|
76
83
|
this.snapshot.set(featureName, enabled);
|
|
77
|
-
this.
|
|
84
|
+
for (const pendingFlips of this.pendingFlipBuffers)
|
|
85
|
+
pendingFlips.set(featureName, enabled);
|
|
78
86
|
}
|
|
79
87
|
// Called by the toggle-cache-sync MSP handler (feature.ts) — the one
|
|
80
88
|
// process whose shared cursor won a given toggle-set event. With a
|
|
@@ -116,11 +124,16 @@ export class GlobalFeatureToggleRuntime {
|
|
|
116
124
|
}
|
|
117
125
|
// Factory for app-boot wiring: instantiate, initialize, return the runtime (for the
|
|
118
126
|
// set-handler to refresh; its effectiveFeatures feeds createDispatcher). Without an explicit
|
|
119
|
-
// syncSignal the
|
|
120
|
-
// REDIS_URL is set, process-local otherwise. close() releases
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
// syncSignal the signal runs on the given cacheSyncBus or, failing that, on a bus the runtime
|
|
128
|
+
// creates itself (Redis-backed when REDIS_URL is set, process-local otherwise). close() releases
|
|
129
|
+
// only a bus the runtime created.
|
|
130
|
+
export async function createFeatureToggleRuntime(db, registry, syncSignal,
|
|
131
|
+
// The server's own bus (the one handed to the dispatcher): reusing it avoids a second set of
|
|
132
|
+
// Redis connections that nothing closes at shutdown.
|
|
133
|
+
cacheSyncBus) {
|
|
134
|
+
const ownedBus = syncSignal || cacheSyncBus ? undefined : createDefaultCacheSyncBus();
|
|
135
|
+
const sharedBus = cacheSyncBus ?? ownedBus;
|
|
136
|
+
const signal = syncSignal ?? (sharedBus ? createCacheSyncToggleSignal(sharedBus) : undefined);
|
|
124
137
|
const runtime = new GlobalFeatureToggleRuntime(db, registry, signal, ownedBus);
|
|
125
138
|
try {
|
|
126
139
|
await runtime.initialize();
|
|
@@ -99,7 +99,7 @@ export const publicVariantQuery = defineQueryHandler({
|
|
|
99
99
|
if (!spec)
|
|
100
100
|
return null;
|
|
101
101
|
const result = await derivatives.variant(query.payload.fileRefId, spec, query.payload.variant);
|
|
102
|
-
const bytes = await files.
|
|
102
|
+
const bytes = await files.refFromFileRef(result).read();
|
|
103
103
|
// ponytail: fully buffered (no streaming), +33% over the wire from
|
|
104
104
|
// base64 — HttpRouteHandlerDeps only exposes {app, systemQuery} (no
|
|
105
105
|
// direct ctx.files access from the Hono handler itself), and the local
|
|
@@ -9,7 +9,7 @@ export type FileRefContent = {
|
|
|
9
9
|
export declare function fileContextForTenant(ctx: {
|
|
10
10
|
readonly _fileProviderResolver?: FileProviderResolver | undefined;
|
|
11
11
|
readonly files?: FileContext | undefined;
|
|
12
|
-
}, tenantId: string): FileContext | undefined;
|
|
12
|
+
}, contextTenantId: string, tenantId: string): FileContext | undefined;
|
|
13
13
|
export declare function fileContextFromResolver(resolver: FileProviderResolver, tenantId: string): FileContext;
|
|
14
14
|
export declare class FileRefTooLargeError extends Error {
|
|
15
15
|
readonly size: number;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { isUuid } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { createFileContext, fileRefsTable, } from "@cosmicdrift/kumiko-framework/files";
|
|
4
|
-
// Tenant-bound on purpose:
|
|
5
|
-
|
|
4
|
+
// Tenant-bound on purpose: without a resolver `ctx.files` only reaches the storage of the
|
|
5
|
+
// context's own tenant (`contextTenantId`), so a request for another tenant's files is
|
|
6
|
+
// undefined instead of silently reading through the wrong tenant's storage.
|
|
7
|
+
export function fileContextForTenant(ctx, contextTenantId, tenantId) {
|
|
6
8
|
const resolver = ctx._fileProviderResolver;
|
|
7
9
|
if (resolver === undefined)
|
|
8
|
-
return ctx.files;
|
|
10
|
+
return tenantId === contextTenantId ? ctx.files : undefined;
|
|
9
11
|
return fileContextFromResolver(resolver, tenantId);
|
|
10
12
|
}
|
|
11
13
|
export function fileContextFromResolver(resolver, tenantId) {
|
|
@@ -44,6 +46,6 @@ export async function readFileRefContent(args) {
|
|
|
44
46
|
if (files === undefined) {
|
|
45
47
|
throw new Error("readFileRefContent: no file storage is available for the file's tenant");
|
|
46
48
|
}
|
|
47
|
-
const content = await files.
|
|
49
|
+
const content = await files.refFromFileRef(row).read();
|
|
48
50
|
return { content, contentType: row.mimeType, tenantId: row.tenantId, size };
|
|
49
51
|
}
|
|
@@ -97,6 +97,7 @@ export const fileRefTenantDestroyHook = async (ctx) => {
|
|
|
97
97
|
},
|
|
98
98
|
maxFailures: 1,
|
|
99
99
|
deadlineAt: ctx.deadlineAt ?? Number.POSITIVE_INFINITY,
|
|
100
|
+
minBatches: 1,
|
|
100
101
|
onRowError: (_row, err) => {
|
|
101
102
|
firstFailure = err instanceof Error ? err.message : String(err);
|
|
102
103
|
},
|
|
@@ -176,6 +177,9 @@ export const fileRefStorageDestroyHook = async (tenantId, ctx) => {
|
|
|
176
177
|
},
|
|
177
178
|
maxFailures: 1,
|
|
178
179
|
deadlineAt: ctx.deadlineAt ?? Number.POSITIVE_INFINITY,
|
|
180
|
+
// list() and the survivor lookup above can use up the whole budget; one chunk per tick
|
|
181
|
+
// still deletes, so the stage never reports "no progress".
|
|
182
|
+
minBatches: 1,
|
|
179
183
|
onRowError: (_key, err) => {
|
|
180
184
|
firstFailure = err instanceof Error ? err.message : String(err);
|
|
181
185
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
export declare const FILES_TENANT_DATA_FEATURE: "files-tenant-data";
|
|
2
3
|
export declare function createFilesTenantDataFeature(): FeatureDefinition;
|
|
3
4
|
export { sweepOrphanedDerivativesJob, sweepOrphanedDerivativesPayloadSchema, } from "./handlers/sweep-orphaned-derivatives.job.js";
|
|
4
5
|
export { fileRefStorageDestroyHook, fileRefTenantDestroyHook } from "./hooks.js";
|
|
@@ -14,8 +14,9 @@ import { defineFeature, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, } from "@cosmicdr
|
|
|
14
14
|
import { sweepOrphanedDerivativesJob, sweepOrphanedDerivativesPayloadSchema, } from "./handlers/sweep-orphaned-derivatives.job.js";
|
|
15
15
|
import { fileRefStorageDestroyHook, fileRefTenantDestroyHook } from "./hooks.js";
|
|
16
16
|
const SWEEP_ORPHANED_DERIVATIVES_ESCAPE_HATCH_REASON = "iterates every tenant and checks fileRef owners per tenant";
|
|
17
|
+
export const FILES_TENANT_DATA_FEATURE = "files-tenant-data";
|
|
17
18
|
export function createFilesTenantDataFeature() {
|
|
18
|
-
return defineFeature(
|
|
19
|
+
return defineFeature(FILES_TENANT_DATA_FEATURE, (r) => {
|
|
19
20
|
r.describe("GDPR coverage for the `files` feature's `fileRef` entity: tenant-destroy row purge (EXT_TENANT_DATA) and full storage-prefix binary wipe (EXT_STORAGE_PROVIDER), plus a manual backfill/GC job that sweeps derivatives orphaned by a forget/tenant-destroy that ran before #2461 wired binary cleanup into those flows. Requires `tenant-lifecycle`, optionalRequires `files`.");
|
|
20
21
|
r.uiHints({
|
|
21
22
|
displayLabel: "Files · Tenant-Destroy & Derivative GC",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
4
4
|
export declare function filterOwnedStorageKeys(db: DbRunner, tenantId: TenantId, ownerId: string, candidateKeys: readonly string[], draftInsertedAt: Temporal.Instant, isCreateMode: boolean): Promise<readonly string[]>;
|