@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
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { configuredPiiSubjectKms, encryptPiiFieldValues, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
15
15
|
import * as z from "zod";
|
|
16
16
|
import { subscriptionAggregateId } from "../aggregate-id.js";
|
|
17
|
-
import { isSubscriptionBlockingCheckout, SubscriptionEventTypes, SubscriptionStatuses, } from "../constants.js";
|
|
17
|
+
import { isSubscriptionBlockingCheckout, isSwitchableSubscriptionStatus, SubscriptionEventTypes, SubscriptionStatuses, } from "../constants.js";
|
|
18
18
|
import { SUBSCRIPTION_PII_FIELDS, subscriptionEntity } from "../entities.js";
|
|
19
19
|
import { INVOICE_PAID_EVENT_QN, INVOICE_PAYMENT_FAILED_EVENT_QN, SUBSCRIPTION_AGGREGATE_TYPE, SUBSCRIPTION_CANCELED_EVENT_QN, SUBSCRIPTION_CREATED_EVENT_QN, SUBSCRIPTION_UPDATED_EVENT_QN, } from "../events.js";
|
|
20
20
|
import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
|
|
@@ -154,16 +154,20 @@ export async function appendSubscriptionEvent(ctx, tenantId, payload) {
|
|
|
154
154
|
// Tenant-Admin direkt.
|
|
155
155
|
// One subscription aggregate exists per tenant, but a tenant can legitimately hold two provider
|
|
156
156
|
// subscriptions over time (a stale `incomplete` one superseded by a fresh checkout). A late or
|
|
157
|
-
// retried event of the old subscription must not overwrite the row of the live one, so
|
|
158
|
-
//
|
|
157
|
+
// retried event of the old subscription must not overwrite the row of the live one, so an event of a
|
|
158
|
+
// different provider subscription only replaces a row that no longer blocks a checkout. The exception
|
|
159
|
+
// is a `created` of a subscription that is itself live (a provider switch); a late `created` of a
|
|
160
|
+
// never-completed `incomplete` one stays stale, since delivery order is not guaranteed.
|
|
159
161
|
function isStaleSubscriptionEvent(current, payload, now) {
|
|
160
162
|
if (current === null)
|
|
161
163
|
return false;
|
|
162
|
-
if (payload.type === SubscriptionEventTypes.created)
|
|
163
|
-
return false;
|
|
164
164
|
const sameSubscription = current.providerName === payload.providerName &&
|
|
165
165
|
current.providerSubscriptionId === payload.providerSubscriptionId;
|
|
166
|
-
|
|
166
|
+
if (sameSubscription || !isSubscriptionBlockingCheckout(current, now))
|
|
167
|
+
return false;
|
|
168
|
+
const supersedesCurrent = payload.type === SubscriptionEventTypes.created &&
|
|
169
|
+
isSwitchableSubscriptionStatus(payload.status);
|
|
170
|
+
return !supersedesCurrent;
|
|
167
171
|
}
|
|
168
172
|
export function createProcessEventHandler(now) {
|
|
169
173
|
return {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export const BILLING_FOUNDATION_I18N = {
|
|
2
|
+
"billing-foundation:feature.label": { en: "Billing" },
|
|
3
|
+
"billing-foundation:notify.contract-confirmation.label": { en: "Contract confirmation" },
|
|
4
|
+
"billing-foundation:notify.contract-termination-receipt.label": { en: "Termination receipt" },
|
|
5
|
+
"billing-foundation:notify.contract-termination-operator-notice.label": {
|
|
6
|
+
en: "Termination notice to operator",
|
|
7
|
+
},
|
|
2
8
|
"screen:billing-plans.title": { en: "Billing" },
|
|
3
9
|
"billing-foundation.plans.billingDisabled": {
|
|
4
10
|
en: "Billing is not active yet. Your current plan stays in place.",
|
|
@@ -90,6 +96,9 @@ export const BILLING_FOUNDATION_I18N = {
|
|
|
90
96
|
"billing-foundation.errors.redirectOriginNotAllowed": {
|
|
91
97
|
en: "This redirect URL is not allowed for this app.",
|
|
92
98
|
},
|
|
99
|
+
"billing-foundation.errors.providerCustomerUnknown": {
|
|
100
|
+
en: "The billing account for this subscription no longer exists. Subscribe again to continue.",
|
|
101
|
+
},
|
|
93
102
|
"billing-foundation.errors.providerHasNoPriceCatalog": {
|
|
94
103
|
en: "This provider has no known prices configured.",
|
|
95
104
|
},
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import type { ExtraRouteDefinition } from "@cosmicdrift/kumiko-framework/api";
|
|
1
|
+
import type { ExtraRouteDefinition, SignatureExtraRouteDeps } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
export declare const SUBSCRIPTION_WEBHOOK_PATH = "/webhooks/subscription/:providerName";
|
|
3
4
|
export type SubscriptionTierSyncDeps<TTier extends string> = {
|
|
4
5
|
readonly isTierName: (value: string) => value is TTier;
|
|
5
6
|
readonly defaultTier: TTier;
|
|
6
7
|
readonly onSyncError?: "log" | "fail-webhook";
|
|
7
8
|
};
|
|
9
|
+
export declare const TIER_SYNC_MANUAL_GRANT_SKIPPED = "tier_sync_manual_grant_skipped";
|
|
8
10
|
export declare function effectiveTierFromSubscription<TTier extends string>(status: string | undefined, tier: string | undefined, isTierName: (value: string) => value is TTier, defaultTier: TTier): TTier;
|
|
9
11
|
export declare function createSubscriptionTierSync<TTier extends string>(deps: SubscriptionTierSyncDeps<TTier>): {
|
|
10
12
|
createWebhookRoute: () => ExtraRouteDefinition;
|
|
13
|
+
syncTierFromSubscription: (tenantId: TenantId, routeDeps: Pick<SignatureExtraRouteDeps, "dispatchSystemQuery" | "dispatchSystemWrite">) => Promise<{
|
|
14
|
+
code: string;
|
|
15
|
+
message: string;
|
|
16
|
+
} | null>;
|
|
11
17
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// near-identical webhook-route.ts app copies in show-pony and publicstatus
|
|
3
3
|
// (infra#446) — the only per-app variables were the TierName union/default,
|
|
4
4
|
// so it is now a factory parameter.
|
|
5
|
-
import { TierAssignmentSources, TierEngineHandlers, TierEngineQueries, tierAssignmentAggregateId, } from "@cosmicdrift/kumiko-bundled-features/tier-engine";
|
|
5
|
+
import { isTierAssignmentSource, TierAssignmentSources, TierEngineHandlers, TierEngineQueries, tierAssignmentAggregateId, } from "@cosmicdrift/kumiko-bundled-features/tier-engine";
|
|
6
6
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
7
7
|
import { subscriptionAggregateId } from "./aggregate-id.js";
|
|
8
8
|
import { SubscriptionFoundationQueries, SubscriptionStatuses } from "./constants.js";
|
|
@@ -10,6 +10,9 @@ import { createSubscriptionWebhookRoute } from "./webhook-handler.js";
|
|
|
10
10
|
// Outside /api — signature routes carry their own auth (verify()) and are
|
|
11
11
|
// not JWT-guarded, so provider dashboards (Stripe, PayPal, ...) point here.
|
|
12
12
|
export const SUBSCRIPTION_WEBHOOK_PATH = "/webhooks/subscription/:providerName";
|
|
13
|
+
// A manual grant that differs from what billing would assign is reported with this
|
|
14
|
+
// code but never fails the webhook: a provider retry cannot resolve it.
|
|
15
|
+
export const TIER_SYNC_MANUAL_GRANT_SKIPPED = "tier_sync_manual_grant_skipped";
|
|
13
16
|
export function effectiveTierFromSubscription(status, tier, isTierName, defaultTier) {
|
|
14
17
|
const usable = status === SubscriptionStatuses.active || status === SubscriptionStatuses.trialing;
|
|
15
18
|
return usable && typeof tier === "string" && isTierName(tier) ? tier : defaultTier;
|
|
@@ -29,7 +32,13 @@ function findTierAssignment(rows, tenantId) {
|
|
|
29
32
|
if (!row || typeof row["id"] !== "string" || typeof row["version"] !== "number") {
|
|
30
33
|
return undefined;
|
|
31
34
|
}
|
|
32
|
-
|
|
35
|
+
const source = row["source"];
|
|
36
|
+
return {
|
|
37
|
+
id: row["id"],
|
|
38
|
+
version: row["version"],
|
|
39
|
+
tier: row["tier"],
|
|
40
|
+
source: isTierAssignmentSource(source) ? source : null,
|
|
41
|
+
};
|
|
33
42
|
}
|
|
34
43
|
export function createSubscriptionTierSync(deps) {
|
|
35
44
|
async function syncTierFromSubscription(tenantId, routeDeps) {
|
|
@@ -68,10 +77,14 @@ export function createSubscriptionTierSync(deps) {
|
|
|
68
77
|
}
|
|
69
78
|
return null;
|
|
70
79
|
}
|
|
71
|
-
if (assignment.source === TierAssignmentSources.manual)
|
|
72
|
-
return null;
|
|
73
80
|
if (assignment.tier === effective)
|
|
74
81
|
return null;
|
|
82
|
+
if (assignment.source === TierAssignmentSources.manual) {
|
|
83
|
+
return {
|
|
84
|
+
code: TIER_SYNC_MANUAL_GRANT_SKIPPED,
|
|
85
|
+
message: `manual tier grant "${String(assignment.tier)}" kept although the subscription yields "${effective}"`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
75
88
|
const result = await routeDeps.dispatchSystemWrite({
|
|
76
89
|
handlerQn: TierEngineHandlers.update,
|
|
77
90
|
payload: {
|
|
@@ -108,8 +121,9 @@ export function createSubscriptionTierSync(deps) {
|
|
|
108
121
|
const syncError = await syncTierFromSubscription(tenantId, routeDeps);
|
|
109
122
|
if (syncError) {
|
|
110
123
|
// biome-ignore lint/suspicious/noConsole: operator visibility for a post-commit sync failure
|
|
111
|
-
console.warn(`[subscription-tier-sync] tier sync
|
|
112
|
-
if (deps.onSyncError === "fail-webhook"
|
|
124
|
+
console.warn(`[subscription-tier-sync] tier sync for tenant ${tenantId} after successful webhook write: ${syncError.code} ${syncError.message}`);
|
|
125
|
+
if (deps.onSyncError === "fail-webhook" &&
|
|
126
|
+
syncError.code !== TIER_SYNC_MANUAL_GRANT_SKIPPED) {
|
|
113
127
|
return {
|
|
114
128
|
isSuccess: false,
|
|
115
129
|
error: {
|
|
@@ -125,5 +139,5 @@ export function createSubscriptionTierSync(deps) {
|
|
|
125
139
|
},
|
|
126
140
|
});
|
|
127
141
|
}
|
|
128
|
-
return { createWebhookRoute };
|
|
142
|
+
return { createWebhookRoute, syncTierFromSubscription };
|
|
129
143
|
}
|
|
@@ -289,7 +289,10 @@ export type LegalLinkSet = {
|
|
|
289
289
|
export declare function isLegalLinkSet(links: LegalLinkSet | Readonly<Record<ConsentLocale, LegalLinkSet>>): links is LegalLinkSet;
|
|
290
290
|
/** Consumer-protection consent gate (§ 312j / § 356 BGB style). When set,
|
|
291
291
|
* `start-plan-checkout` and `create-checkout-session` require a `consent`
|
|
292
|
-
* payload and record it as an event before the buyer reaches the provider.
|
|
292
|
+
* payload and record it as an event before the buyer reaches the provider.
|
|
293
|
+
* Every provider used with `create-checkout-session` must implement
|
|
294
|
+
* `retrievePrices`: the live price is part of the consent record, and a
|
|
295
|
+
* provider without it fails the checkout with an InternalError. */
|
|
293
296
|
export type ConsumerProtectionOptions = {
|
|
294
297
|
/** Slug of the template-resolver `text-block` holding the terms text; its
|
|
295
298
|
* content hash is stored with every recorded consent. */
|
|
@@ -304,7 +307,9 @@ export type ConsumerProtectionOptions = {
|
|
|
304
307
|
readonly terminationScope?: TerminationScope;
|
|
305
308
|
/** Names the item of a one-off payment (mode "payment") for the consent record
|
|
306
309
|
* and the confirmation mail; the label key is resolved through the registry
|
|
307
|
-
* i18n, so it must be qualified with the registering feature.
|
|
310
|
+
* i18n, so it must be qualified with the registering feature. The label key
|
|
311
|
+
* is 1-200 characters, string params at most 100; a violation fails the
|
|
312
|
+
* checkout with an InternalError before the provider session is created. */
|
|
308
313
|
readonly oneOffItemLabel?: (ctx: HandlerContext, priceId: string) => Promise<OrderItem | undefined>;
|
|
309
314
|
/** Root-relative paths or absolute https URLs. One set for every locale, or
|
|
310
315
|
* one set per consent locale (`de` and `en` required). */
|
|
@@ -173,6 +173,11 @@ export async function markCapSoftWarned(ctx, options) {
|
|
|
173
173
|
}
|
|
174
174
|
return retryCounterWriteOnVersionConflict(() => attemptMark(writeDb));
|
|
175
175
|
}
|
|
176
|
+
// A release is written up to a reservation TTL after its increment; once the increment left the
|
|
177
|
+
// window the release must not subtract from other increments still inside it.
|
|
178
|
+
function isBefore(isoInstant, cutoff) {
|
|
179
|
+
return Temporal.Instant.compare(Temporal.Instant.from(isoInstant), cutoff) < 0;
|
|
180
|
+
}
|
|
176
181
|
async function sumRollingEventAmounts(db, tenantId, options, type) {
|
|
177
182
|
const aggregateId = rollingCapAggregateId(tenantId, options.capName);
|
|
178
183
|
const cutoff = Temporal.Now.instant().subtract({ hours: options.windowDays * 24 });
|
|
@@ -189,6 +194,9 @@ async function sumRollingEventAmounts(db, tenantId, options, type) {
|
|
|
189
194
|
// unknown by drizzle's $type<Record<string,unknown>>); narrowing the
|
|
190
195
|
// shape here mirrors enforceRollingCap's own read-side contract.
|
|
191
196
|
const payload = row["payload"];
|
|
197
|
+
if (typeof payload.incrementedAt === "string" && isBefore(payload.incrementedAt, cutoff)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
192
200
|
if (typeof payload.amount === "number") {
|
|
193
201
|
total += payload.amount;
|
|
194
202
|
}
|
|
@@ -12,6 +12,8 @@ export async function reserveEnforcedCalendarCap(ctx, cap) {
|
|
|
12
12
|
profile: cap.profile,
|
|
13
13
|
notify: cap.notify,
|
|
14
14
|
amount: cap.amount,
|
|
15
|
+
...(cap.periodEndIso !== undefined && { periodEndIso: cap.periodEndIso }),
|
|
16
|
+
...(cap.i18nKey !== undefined && { i18nKey: cap.i18nKey }),
|
|
15
17
|
markSoftWarnedOutsideTransaction: outsideTransaction,
|
|
16
18
|
});
|
|
17
19
|
}
|
|
@@ -61,13 +61,13 @@ export async function reserveCalendarCap(ctx, options, outsideTransaction = true
|
|
|
61
61
|
counterValue: valueBeforeBooking + options.amount,
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
function rollingEvent(user, capName, amount, expectedVersion, type) {
|
|
64
|
+
function rollingEvent(user, capName, amount, expectedVersion, type, extraPayload = {}) {
|
|
65
65
|
return {
|
|
66
66
|
aggregateId: rollingCapAggregateId(user.tenantId, capName),
|
|
67
67
|
aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
|
|
68
68
|
expectedVersion,
|
|
69
69
|
type,
|
|
70
|
-
payload: { capName, amount },
|
|
70
|
+
payload: { capName, amount, ...extraPayload },
|
|
71
71
|
metadata: { userId: user.id },
|
|
72
72
|
};
|
|
73
73
|
}
|
|
@@ -104,7 +104,11 @@ async function giveBackCalendar(txDb, user, row) {
|
|
|
104
104
|
}
|
|
105
105
|
async function giveBackRolling(txDb, user, row) {
|
|
106
106
|
const version = await getStreamVersionInTenantDb(txDb, rollingCapAggregateId(user.tenantId, row.capName));
|
|
107
|
-
await appendEventInTenantDb(txDb,
|
|
107
|
+
await appendEventInTenantDb(txDb,
|
|
108
|
+
// The window filter must use the increment's time: a sweep releases up to a TTL later.
|
|
109
|
+
rollingEvent(user, row.capName, row.amount, version, ROLLING_RELEASED_EVENT_QN, {
|
|
110
|
+
incrementedAt: row.createdAt.toString(),
|
|
111
|
+
}));
|
|
108
112
|
return NOTHING_TO_RELEASE;
|
|
109
113
|
}
|
|
110
114
|
async function giveBackReservation(txDb, user, id) {
|
|
@@ -57,6 +57,9 @@ export declare function assertBelowHardCap(valueSeenByLastUnit: number, cap: {
|
|
|
57
57
|
readonly capName: string;
|
|
58
58
|
readonly limit: number;
|
|
59
59
|
readonly profile: CapToleranceProfileName;
|
|
60
|
+
readonly periodStartIso?: string;
|
|
61
|
+
readonly periodEndIso?: string;
|
|
62
|
+
readonly i18nKey?: string;
|
|
60
63
|
}): void;
|
|
61
64
|
/**
|
|
62
65
|
* Synchronous read-and-check of the calling tenant's counter for
|
|
@@ -85,6 +88,8 @@ export declare function enforceCap(ctx: CapCounterContext, options: {
|
|
|
85
88
|
readonly limit: number;
|
|
86
89
|
readonly profile: CapToleranceProfileName;
|
|
87
90
|
readonly amount?: number;
|
|
91
|
+
readonly periodEndIso?: string;
|
|
92
|
+
readonly i18nKey?: string;
|
|
88
93
|
}): Promise<EnforceCapResult>;
|
|
89
94
|
/**
|
|
90
95
|
* Synchronous read-and-check of the calling tenant's Rolling-Window-
|
|
@@ -120,6 +125,9 @@ export declare function enforceRollingCap(ctx: CapCounterContext, options: {
|
|
|
120
125
|
* Thrown by enforceCap/enforceRollingCap when value ≥ hard-threshold.
|
|
121
126
|
* Extends `KumikoError` — the dispatcher auto-maps this to HTTP 429
|
|
122
127
|
* with `code: "cap_exceeded"`, no caller-side catch needed.
|
|
128
|
+
*
|
|
129
|
+
* Calendar caps carry `resetAt` (end of the period) and `retryAfterSeconds`, which the API
|
|
130
|
+
* answers as Retry-After. Rolling caps have no fixed reset instant, so both stay undefined.
|
|
123
131
|
*/
|
|
124
132
|
export declare class CapExceededError extends KumikoError {
|
|
125
133
|
readonly capName: string;
|
|
@@ -128,7 +136,13 @@ export declare class CapExceededError extends KumikoError {
|
|
|
128
136
|
readonly tolerance: CapToleranceProfile;
|
|
129
137
|
readonly code: "cap_exceeded";
|
|
130
138
|
readonly httpStatus = 429;
|
|
131
|
-
|
|
139
|
+
readonly resetAt: string | undefined;
|
|
140
|
+
constructor(capName: string, limit: number, currentValue: number, tolerance: CapToleranceProfile, options?: {
|
|
141
|
+
readonly resetAt?: string;
|
|
142
|
+
readonly i18nKey?: string;
|
|
143
|
+
readonly cause?: Error;
|
|
144
|
+
readonly now?: Temporal.Instant;
|
|
145
|
+
});
|
|
132
146
|
}
|
|
133
147
|
/**
|
|
134
148
|
* Calendar-month period start in UTC. Use this for monthly caps
|
|
@@ -137,6 +151,7 @@ export declare class CapExceededError extends KumikoError {
|
|
|
137
151
|
* Returns ISO string for the 1st of the current month at 00:00 UTC.
|
|
138
152
|
*/
|
|
139
153
|
export declare function currentCalendarMonthStartIso(now?: Temporal.Instant): string;
|
|
154
|
+
export declare function calendarMonthPeriodEndIso(periodStartIso: string): string;
|
|
140
155
|
/**
|
|
141
156
|
* Soft-hit-notifier callback. Caller liefert die Funktion die ein
|
|
142
157
|
* delivery-event emittet (z.B. `delivery.send({to, template, payload})`).
|
|
@@ -169,6 +184,8 @@ export declare function enforceCapAndMaybeNotify(ctx: CapCounterContext, options
|
|
|
169
184
|
readonly profile: CapToleranceProfileName;
|
|
170
185
|
readonly notify: SoftHitNotifier;
|
|
171
186
|
readonly amount?: number;
|
|
187
|
+
readonly periodEndIso?: string;
|
|
188
|
+
readonly i18nKey?: string;
|
|
172
189
|
readonly markSoftWarnedOutsideTransaction?: boolean;
|
|
173
190
|
}): Promise<EnforceCapResult>;
|
|
174
191
|
/**
|
|
@@ -17,8 +17,24 @@ export const CAP_TOLERANCES = {
|
|
|
17
17
|
export function assertBelowHardCap(valueSeenByLastUnit, cap) {
|
|
18
18
|
const tolerance = CAP_TOLERANCES[cap.profile];
|
|
19
19
|
if (valueSeenByLastUnit >= cap.limit * tolerance.hard) {
|
|
20
|
-
|
|
20
|
+
const resetAt = resolvePeriodEndIso(cap.periodStartIso, cap.periodEndIso);
|
|
21
|
+
throw new CapExceededError(cap.capName, cap.limit, valueSeenByLastUnit, tolerance, {
|
|
22
|
+
...(resetAt !== undefined && { resetAt }),
|
|
23
|
+
...(cap.i18nKey !== undefined && { i18nKey: cap.i18nKey }),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function resolvePeriodEndIso(periodStartIso, periodEndIso) {
|
|
28
|
+
if (periodStartIso === undefined)
|
|
29
|
+
return periodEndIso;
|
|
30
|
+
if (periodEndIso === undefined)
|
|
31
|
+
return calendarMonthPeriodEndIso(periodStartIso);
|
|
32
|
+
const start = Temporal.Instant.from(periodStartIso);
|
|
33
|
+
const end = Temporal.Instant.from(periodEndIso);
|
|
34
|
+
if (Temporal.Instant.compare(end, start) <= 0) {
|
|
35
|
+
throw new Error(`cap-counter: periodEndIso (${periodEndIso}) must be after periodStartIso (${periodStartIso})`);
|
|
21
36
|
}
|
|
37
|
+
return end.toString();
|
|
22
38
|
}
|
|
23
39
|
// =============================================================================
|
|
24
40
|
// Enforce-Cap helper
|
|
@@ -115,6 +131,9 @@ export async function enforceRollingCap(ctx, options) {
|
|
|
115
131
|
* Thrown by enforceCap/enforceRollingCap when value ≥ hard-threshold.
|
|
116
132
|
* Extends `KumikoError` — the dispatcher auto-maps this to HTTP 429
|
|
117
133
|
* with `code: "cap_exceeded"`, no caller-side catch needed.
|
|
134
|
+
*
|
|
135
|
+
* Calendar caps carry `resetAt` (end of the period) and `retryAfterSeconds`, which the API
|
|
136
|
+
* answers as Retry-After. Rolling caps have no fixed reset instant, so both stay undefined.
|
|
118
137
|
*/
|
|
119
138
|
export class CapExceededError extends KumikoError {
|
|
120
139
|
capName;
|
|
@@ -123,17 +142,32 @@ export class CapExceededError extends KumikoError {
|
|
|
123
142
|
tolerance;
|
|
124
143
|
code = "cap_exceeded";
|
|
125
144
|
httpStatus = 429;
|
|
126
|
-
|
|
145
|
+
resetAt;
|
|
146
|
+
constructor(capName, limit, currentValue, tolerance, options) {
|
|
147
|
+
const resetAt = options?.resetAt;
|
|
148
|
+
const retryAfterSeconds = resetAt === undefined
|
|
149
|
+
? undefined
|
|
150
|
+
: Math.max(0, Math.ceil(Temporal.Instant.from(resetAt).epochMilliseconds / 1000 -
|
|
151
|
+
(options?.now ?? Temporal.Now.instant()).epochMilliseconds / 1000));
|
|
127
152
|
super({
|
|
128
|
-
message: `Cap "${capName}" exceeded: current=${currentValue}, limit=${limit}, hard-threshold=${limit * tolerance.hard}. Upgrade tier or wait for next period reset
|
|
129
|
-
i18nKey: "errors.cap.exceeded",
|
|
130
|
-
i18nParams: { capName, limit, currentValue },
|
|
131
|
-
details: {
|
|
153
|
+
message: `Cap "${capName}" exceeded: current=${currentValue}, limit=${limit}, hard-threshold=${limit * tolerance.hard}. Upgrade tier or wait for next period reset.${resetAt === undefined ? "" : ` Resets at ${resetAt}.`}`,
|
|
154
|
+
i18nKey: options?.i18nKey ?? "errors.cap.exceeded",
|
|
155
|
+
i18nParams: { capName, limit, currentValue, ...(resetAt !== undefined && { resetAt }) },
|
|
156
|
+
details: {
|
|
157
|
+
capName,
|
|
158
|
+
limit,
|
|
159
|
+
currentValue,
|
|
160
|
+
tolerance,
|
|
161
|
+
...(resetAt !== undefined && { resetAt, retryAfterSeconds }),
|
|
162
|
+
},
|
|
163
|
+
...(retryAfterSeconds !== undefined && { retryAfterSeconds }),
|
|
164
|
+
...(options?.cause && { cause: options.cause }),
|
|
132
165
|
});
|
|
133
166
|
this.capName = capName;
|
|
134
167
|
this.limit = limit;
|
|
135
168
|
this.currentValue = currentValue;
|
|
136
169
|
this.tolerance = tolerance;
|
|
170
|
+
this.resetAt = resetAt;
|
|
137
171
|
}
|
|
138
172
|
}
|
|
139
173
|
// =============================================================================
|
|
@@ -158,6 +192,13 @@ export function currentCalendarMonthStartIso(now = Temporal.Now.instant()) {
|
|
|
158
192
|
});
|
|
159
193
|
return start.toInstant().toString();
|
|
160
194
|
}
|
|
195
|
+
export function calendarMonthPeriodEndIso(periodStartIso) {
|
|
196
|
+
return Temporal.Instant.from(periodStartIso)
|
|
197
|
+
.toZonedDateTimeISO("UTC")
|
|
198
|
+
.add({ months: 1 })
|
|
199
|
+
.toInstant()
|
|
200
|
+
.toString();
|
|
201
|
+
}
|
|
161
202
|
/**
|
|
162
203
|
* Calendar-Period-enforcement + automatische soft-hit-Notification.
|
|
163
204
|
* Ruft `enforceCap`, bei `crossed: true` den Notifier UND
|
|
@@ -175,6 +216,8 @@ export async function enforceCapAndMaybeNotify(ctx, options) {
|
|
|
175
216
|
limit: options.limit,
|
|
176
217
|
profile: options.profile,
|
|
177
218
|
...(options.amount !== undefined && { amount: options.amount }),
|
|
219
|
+
...(options.periodEndIso !== undefined && { periodEndIso: options.periodEndIso }),
|
|
220
|
+
...(options.i18nKey !== undefined && { i18nKey: options.i18nKey }),
|
|
178
221
|
});
|
|
179
222
|
if (result.state === "soft-hit" && result.crossed) {
|
|
180
223
|
await options.notify({
|
|
@@ -10,5 +10,6 @@ export declare const rollingIncrementedSchema: z.ZodObject<{
|
|
|
10
10
|
export declare const rollingReleasedSchema: z.ZodObject<{
|
|
11
11
|
capName: z.ZodString;
|
|
12
12
|
amount: z.ZodNumber;
|
|
13
|
+
incrementedAt: z.ZodOptional<z.ZodString>;
|
|
13
14
|
}, z.core.$strip>;
|
|
14
15
|
export declare const incrementRollingCapHandler: WriteHandlerDef;
|
|
@@ -34,6 +34,8 @@ export const rollingIncrementedSchema = incrementRollingSchema;
|
|
|
34
34
|
/** Payload of the event that gives a reserved rolling amount back. */
|
|
35
35
|
export const rollingReleasedSchema = incrementRollingSchema.extend({
|
|
36
36
|
amount: z.number().int().positive(),
|
|
37
|
+
/** When the released increment was booked; absent on events written before it was recorded. */
|
|
38
|
+
incrementedAt: z.string().optional(),
|
|
37
39
|
});
|
|
38
40
|
// Rolling-Increment-Handler — append-only. Race-frei: zwei parallele
|
|
39
41
|
// Increments für (tenant, cap) hängen sich am selben aggregate-stream
|
|
@@ -2,7 +2,7 @@ export { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js"
|
|
|
2
2
|
export { type BookCapUsageOptions, bookCapUsage, type MarkCapSoftWarnedOptions, markCapSoftWarned, type ReadRollingCapUsageOptions, readRollingCapUsage, } from "./book-cap-usage.js";
|
|
3
3
|
export { type CalendarCapUsageReservation, type CalendarCapUsageReservationSpec, reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
|
|
4
4
|
export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_QN, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
|
|
5
|
-
export { CAP_TOLERANCES, CapExceededError, type CapToleranceProfile, type CapToleranceProfileName, currentCalendarMonthStartIso, type EnforceCapResult, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, type SoftHitNotifier, type StockCapResult, } from "./enforce-cap.js";
|
|
5
|
+
export { CAP_TOLERANCES, CapExceededError, type CapToleranceProfile, type CapToleranceProfileName, calendarMonthPeriodEndIso, currentCalendarMonthStartIso, type EnforceCapResult, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, type SoftHitNotifier, type StockCapResult, } from "./enforce-cap.js";
|
|
6
6
|
export { capCounterEntity } from "./entity.js";
|
|
7
7
|
export { capCounterFeature } from "./feature.js";
|
|
8
8
|
export { readPayloadField } from "./payload-field.js";
|
|
@@ -3,7 +3,7 @@ export { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js"
|
|
|
3
3
|
export { bookCapUsage, markCapSoftWarned, readRollingCapUsage, } from "./book-cap-usage.js";
|
|
4
4
|
export { reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
|
|
5
5
|
export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_QN, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
|
|
6
|
-
export { CAP_TOLERANCES, CapExceededError, currentCalendarMonthStartIso, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, } from "./enforce-cap.js";
|
|
6
|
+
export { CAP_TOLERANCES, CapExceededError, calendarMonthPeriodEndIso, currentCalendarMonthStartIso, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, } from "./enforce-cap.js";
|
|
7
7
|
export { capCounterEntity } from "./entity.js";
|
|
8
8
|
export { capCounterFeature } from "./feature.js";
|
|
9
9
|
export { readPayloadField } from "./payload-field.js";
|
|
@@ -10,6 +10,8 @@ import { type CapToleranceProfileName, type SoftHitNotifier } from "./enforce-ca
|
|
|
10
10
|
export type CalendarCapDef = {
|
|
11
11
|
readonly capName: string;
|
|
12
12
|
readonly periodStartIso: string;
|
|
13
|
+
readonly periodEndIso?: string;
|
|
14
|
+
readonly i18nKey?: string;
|
|
13
15
|
readonly limit: number;
|
|
14
16
|
readonly profile: CapToleranceProfileName;
|
|
15
17
|
/** Increment-amount post-success. Default 1. */
|
|
@@ -116,7 +116,9 @@ export function createTenantCapsListQuery(caps, listCaps) {
|
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
118
|
const db = ctx.systemDb.acknowledgeCrossTenant("cap-overview:tenant-caps:list — SystemAdmin platform-wide tenant overview");
|
|
119
|
-
const tenants = await decryptTenantNames(
|
|
119
|
+
const tenants = await decryptTenantNames(
|
|
120
|
+
// Destroyed tenants decrypt to the erased sentinel, which a picker or list would show verbatim.
|
|
121
|
+
await selectMany(db, tenantTable, { destroyedAt: null }), "cap-overview:tenant-caps:list");
|
|
120
122
|
const assignments = await selectMany(db, tierAssignmentTable, {});
|
|
121
123
|
const subscriptions = await selectMany(db, subscriptionsProjectionTable, {});
|
|
122
124
|
const assignmentByTenant = new Map(assignments.map((row) => [row.tenantId, row]));
|
|
@@ -15,7 +15,9 @@ export const tenantOptionsQuery = defineQueryHandler({
|
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
const db = ctx.systemDb.acknowledgeCrossTenant("cap-overview:tenant-options — SystemAdmin dashboard tenant-filter options");
|
|
18
|
-
const tenants = await decryptTenantNames(
|
|
18
|
+
const tenants = await decryptTenantNames(
|
|
19
|
+
// Destroyed tenants decrypt to the erased sentinel, which the picker would show verbatim.
|
|
20
|
+
await selectMany(db, tenantTable, { destroyedAt: null }), "cap-overview:tenant-options");
|
|
19
21
|
const sorted = [...tenants].sort((a, b) => a.name.localeCompare(b.name));
|
|
20
22
|
return { rows: sorted.map((tenant) => ({ value: tenant.id, label: tenant.name })) };
|
|
21
23
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { chatMessageText, createChatWebhookChannel, truncateChars, } from "../delivery/index.js";
|
|
2
|
+
const DISCORD_WEBHOOK_PREFIX = "channel-discord:webhooks.";
|
|
2
3
|
export const DISCORD_SECRET_KEYS = {
|
|
3
|
-
webhookPrefix:
|
|
4
|
-
webhookKeyFor: (connection) =>
|
|
4
|
+
webhookPrefix: DISCORD_WEBHOOK_PREFIX,
|
|
5
|
+
webhookKeyFor: (connection) => `${DISCORD_WEBHOOK_PREFIX}${connection}`,
|
|
5
6
|
};
|
|
6
7
|
export const DISCORD_DEFAULT_ALLOWED_HOSTS = ["discord.com", "discordapp.com"];
|
|
7
8
|
export const DISCORD_WEBHOOK_PATH_PREFIX = "/api/webhooks/";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
|
|
2
|
+
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
2
3
|
import * as z from "zod";
|
|
3
4
|
import { DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE, DELIVERY_UNSUBSCRIBE_PATH, EMAIL_CHANNEL_NAME, fileAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, renderedPdfAttachmentRefSchema, } from "../delivery/index.js";
|
|
4
|
-
import { decryptStoredPii } from "../shared/index.js";
|
|
5
|
+
import { decryptStoredPii, extensionForResourceContentType } from "../shared/index.js";
|
|
5
6
|
import { guardEmailMessage } from "./pii-guard.js";
|
|
6
7
|
// Envelope (From / Reply-To / threading headers) rides on the channel data —
|
|
7
8
|
// the notification's email template echoes it out of the notify() call, since
|
|
@@ -22,7 +23,8 @@ const UNSUBSCRIBE_HEADER_NAMES = new Set([
|
|
|
22
23
|
"list-unsubscribe",
|
|
23
24
|
"list-unsubscribe-post",
|
|
24
25
|
]);
|
|
25
|
-
// Only the framework's own unsubscribe route can honor a one-click POST
|
|
26
|
+
// Only the framework's own unsubscribe route can honor a one-click POST; an app's
|
|
27
|
+
// own unsubscribe page still gets the plain List-Unsubscribe link (RFC 2369).
|
|
26
28
|
function listUnsubscribeHeaders(data) {
|
|
27
29
|
const rawUrl = data?.["unsubscribeUrl"];
|
|
28
30
|
if (typeof rawUrl !== "string")
|
|
@@ -36,8 +38,9 @@ function listUnsubscribeHeaders(data) {
|
|
|
36
38
|
}
|
|
37
39
|
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
38
40
|
return undefined;
|
|
39
|
-
if (url.pathname !== DELIVERY_UNSUBSCRIBE_PATH)
|
|
40
|
-
return
|
|
41
|
+
if (url.pathname !== DELIVERY_UNSUBSCRIBE_PATH) {
|
|
42
|
+
return { "List-Unsubscribe": `<${url.toString()}>` };
|
|
43
|
+
}
|
|
41
44
|
return {
|
|
42
45
|
"List-Unsubscribe": `<${url.toString()}>`,
|
|
43
46
|
"List-Unsubscribe-Post": DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE,
|
|
@@ -81,15 +84,13 @@ export function fileAttachmentsFrom(data, messageLocale) {
|
|
|
81
84
|
? { ...ref, locale: messageLocale }
|
|
82
85
|
: ref);
|
|
83
86
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"image/webp": "webp",
|
|
89
|
-
};
|
|
87
|
+
function inlineFileName(cid, contentType) {
|
|
88
|
+
const extension = extensionForResourceContentType(contentType);
|
|
89
|
+
return extension === undefined ? cid : `${cid}.${extension}`;
|
|
90
|
+
}
|
|
90
91
|
async function resolveAttachments(sources, ctx) {
|
|
91
92
|
if (sources.length === 0)
|
|
92
|
-
return [];
|
|
93
|
+
return { kind: "resolved", attachments: [] };
|
|
93
94
|
if (ctx.readAttachment === undefined) {
|
|
94
95
|
throw new Error("email channel: the message has attachments but the channel context cannot read files");
|
|
95
96
|
}
|
|
@@ -113,16 +114,12 @@ async function resolveAttachments(sources, ctx) {
|
|
|
113
114
|
// A missing inline image only degrades the look; a missing file is content the recipient expects.
|
|
114
115
|
if (ref.kind === "inline")
|
|
115
116
|
continue;
|
|
116
|
-
|
|
117
|
+
return { kind: "missing-file", fileRefId: ref.fileRefId };
|
|
117
118
|
}
|
|
118
|
-
const fileName = ref.kind === "file"
|
|
119
|
-
? ref.fileName
|
|
120
|
-
: INLINE_EXTENSION_BY_CONTENT_TYPE[found.contentType]
|
|
121
|
-
? `${ref.cid}.${INLINE_EXTENSION_BY_CONTENT_TYPE[found.contentType]}`
|
|
122
|
-
: ref.cid;
|
|
119
|
+
const fileName = ref.kind === "file" ? ref.fileName : inlineFileName(ref.cid, found.contentType);
|
|
123
120
|
resolved.push({ ...ref, fileName, content: found.content, contentType: found.contentType });
|
|
124
121
|
}
|
|
125
|
-
return resolved;
|
|
122
|
+
return { kind: "resolved", attachments: resolved };
|
|
126
123
|
}
|
|
127
124
|
export function createEmailChannel(options) {
|
|
128
125
|
const { transport, resolveEmail } = options;
|
|
@@ -138,13 +135,20 @@ export function createEmailChannel(options) {
|
|
|
138
135
|
throw new Error(`email channel: notification "${notificationType}" has a mail template but the channel context carries no renderMail`);
|
|
139
136
|
}
|
|
140
137
|
const unsubscribeUrl = message.data?.["unsubscribeUrl"];
|
|
141
|
-
const { subject, html, text, attachments } = await ctx.renderMail({
|
|
138
|
+
const { subject, html, text, attachments, fromName, replyTo } = await ctx.renderMail({
|
|
142
139
|
slug: notificationType,
|
|
143
140
|
locale: message.locale ?? "en",
|
|
144
141
|
variables: message.data ?? {},
|
|
145
142
|
...(typeof unsubscribeUrl === "string" && { unsubscribeUrl }),
|
|
146
143
|
});
|
|
147
|
-
return {
|
|
144
|
+
return {
|
|
145
|
+
html,
|
|
146
|
+
subject,
|
|
147
|
+
text,
|
|
148
|
+
...(fromName !== undefined && { fromName }),
|
|
149
|
+
...(replyTo !== undefined && { replyTo }),
|
|
150
|
+
...(attachments !== undefined && { attachments }),
|
|
151
|
+
};
|
|
148
152
|
}
|
|
149
153
|
return {
|
|
150
154
|
name: EMAIL_CHANNEL_NAME,
|
|
@@ -161,12 +165,25 @@ export function createEmailChannel(options) {
|
|
|
161
165
|
return renderMessage(message, ctx);
|
|
162
166
|
},
|
|
163
167
|
async send(address, message, ctx, rendered) {
|
|
164
|
-
const { html, subject, text, attachments } = rendered ?? (await renderMessage(message, ctx));
|
|
165
|
-
|
|
166
|
-
const
|
|
168
|
+
const { html, subject, text, attachments, fromName, replyTo } = rendered ?? (await renderMessage(message, ctx));
|
|
169
|
+
// Template-level sender defaults; notify data wins.
|
|
170
|
+
const envelope = {
|
|
171
|
+
...(fromName && { fromName }),
|
|
172
|
+
...(replyTo && { replyTo }),
|
|
173
|
+
...emailEnvelopeFrom(message.data),
|
|
174
|
+
};
|
|
175
|
+
const resolution = await resolveAttachments([
|
|
167
176
|
...(attachments ?? []).map((ref) => ({ ref, origin: "template" })),
|
|
168
177
|
...fileAttachmentsFrom(message.data, message.locale).map((ref) => ({ ref, origin: "message" })),
|
|
169
178
|
], ctx);
|
|
179
|
+
if (resolution.kind === "missing-file") {
|
|
180
|
+
// A missing file is permanent, a retry cannot bring it back.
|
|
181
|
+
createFallbackLogger("delivery").warn("mail attachment file not found, send failed", {
|
|
182
|
+
fileRefId: resolution.fileRefId,
|
|
183
|
+
});
|
|
184
|
+
return { status: "failed", error: "send_failed", address };
|
|
185
|
+
}
|
|
186
|
+
const resolvedAttachments = resolution.attachments;
|
|
170
187
|
await transport.send(guardEmailMessage({
|
|
171
188
|
to: address,
|
|
172
189
|
subject,
|
|
@@ -21,11 +21,16 @@ function isControlCharacter(char) {
|
|
|
21
21
|
const code = char.charCodeAt(0);
|
|
22
22
|
return code <= 0x1f || code === 0x7f;
|
|
23
23
|
}
|
|
24
|
+
// Mail clients often show only the display name: an embedded address ("<", ">", "@") would
|
|
25
|
+
// pass for a foreign sender on the platform's own SPF/DKIM-signed domain.
|
|
26
|
+
function isUnsafeDisplayNameCharacter(char) {
|
|
27
|
+
return isControlCharacter(char) || char === "<" || char === ">" || char === "@";
|
|
28
|
+
}
|
|
24
29
|
// Control characters in a display name would let a caller inject headers.
|
|
25
30
|
function sanitizedDisplayName(name) {
|
|
26
31
|
const cleaned = name
|
|
27
32
|
?.split("")
|
|
28
|
-
.map((char) => (
|
|
33
|
+
.map((char) => (isUnsafeDisplayNameCharacter(char) ? " " : char))
|
|
29
34
|
.join("")
|
|
30
35
|
.trim();
|
|
31
36
|
return cleaned === undefined || cleaned === "" ? undefined : cleaned;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
export declare const markAllReadWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"markAllRead", z.ZodObject<{}, z.core.$strip>, {
|
|
3
3
|
marked: number;
|
|
4
|
-
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
4
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
@@ -3,4 +3,4 @@ export declare const markReadWrite: import("@cosmicdrift/kumiko-types/define-han
|
|
|
3
3
|
id: z.ZodNumber;
|
|
4
4
|
}, z.core.$strip>, {
|
|
5
5
|
id: number | undefined;
|
|
6
|
-
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
6
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|