@cosmicdrift/kumiko-bundled-features 0.388.0 → 0.390.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/agent-tools/index.d.ts +1 -1
- package/dist/agent-tools/index.js +1 -1
- package/dist/agent-tools/tool-dispatch.d.ts +8 -3
- package/dist/agent-tools/tool-dispatch.js +36 -25
- package/dist/audit/feature.js +2 -1
- package/dist/auth-email-password/auth-user-row.js +1 -1
- package/dist/auth-email-password/bootstrap.js +6 -1
- package/dist/auth-email-password/feature.js +1 -1
- package/dist/auth-email-password/handlers/change-password.write.js +7 -7
- package/dist/auth-email-password/handlers/confirm-account-unlock.write.js +3 -3
- package/dist/auth-email-password/handlers/confirm-token-flow.js +4 -4
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +8 -7
- package/dist/auth-email-password/handlers/invite-accept.write.js +10 -7
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
- package/dist/auth-email-password/handlers/invite-info.query.js +2 -2
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +8 -7
- package/dist/auth-email-password/handlers/login.write.js +17 -15
- package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/request-account-unlock.write.js +1 -0
- package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/request-email-verification.write.js +1 -0
- package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/request-password-reset.write.js +1 -0
- package/dist/auth-email-password/handlers/reset-password.write.js +2 -2
- package/dist/auth-email-password/handlers/signup-confirm.write.js +12 -12
- package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/signup-request.write.js +16 -8
- package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +4 -2
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +2 -0
- package/dist/auth-email-password/handlers/token-request-handler.js +7 -2
- package/dist/auth-email-password/handlers/verify-email.write.js +2 -2
- package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
- package/dist/auth-email-password/magic-link-mail.js +7 -3
- package/dist/auth-email-password/seeding.d.ts +1 -1
- package/dist/auth-email-password/seeding.js +9 -8
- package/dist/auth-email-password/signup-activation.d.ts +1 -0
- package/dist/auth-email-password/signup-activation.js +1 -0
- package/dist/auth-email-password/web/auth-client.d.ts +2 -2
- package/dist/auth-email-password/web/auth-client.js +8 -4
- package/dist/auth-email-password/web/auth-redirect.d.ts +5 -1
- package/dist/auth-email-password/web/auth-redirect.js +20 -25
- package/dist/auth-email-password/web/default-auth-pages-gate.js +6 -3
- package/dist/auth-email-password/web/forgot-password-screen.d.ts +4 -1
- package/dist/auth-email-password/web/forgot-password-screen.js +7 -2
- package/dist/auth-email-password/web/index.d.ts +1 -0
- package/dist/auth-email-password/web/index.js +1 -0
- package/dist/auth-email-password/web/login-screen.js +11 -6
- package/dist/auth-email-password/web/reset-password-screen.js +7 -1
- package/dist/auth-email-password/web/return-path.d.ts +4 -0
- package/dist/auth-email-password/web/return-path.js +34 -0
- package/dist/auth-email-password/web/session.js +6 -1
- package/dist/auth-email-password/web/signup-complete-screen.js +11 -3
- package/dist/auth-email-password/web/signup-screen.d.ts +4 -1
- package/dist/auth-email-password/web/signup-screen.js +8 -3
- package/dist/auth-foundation/feature.js +12 -4
- package/dist/auth-mfa/config.d.ts +1 -1
- package/dist/auth-mfa/config.js +3 -7
- package/dist/auth-mfa/handlers/disable.write.js +5 -5
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +19 -14
- package/dist/auth-mfa/handlers/enable-confirm.write.js +3 -3
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +6 -1
- package/dist/auth-mfa/handlers/enable-start.write.js +1 -1
- package/dist/auth-mfa/handlers/reencrypt.job.js +14 -9
- package/dist/auth-mfa/handlers/regenerate-recovery.write.js +4 -4
- package/dist/auth-mfa/handlers/verify.write.js +19 -14
- package/dist/auth-mfa/mfa-code-verifier.js +9 -4
- package/dist/auth-mfa/mfa-status-checker.js +13 -6
- package/dist/auth-mfa/verify-factor.js +1 -1
- package/dist/billing-foundation/billing-info-query.js +2 -2
- package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -1
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +5 -5
- package/dist/billing-foundation/consumer-protection/register-consent.js +1 -1
- package/dist/billing-foundation/consumer-protection/register-termination.js +1 -1
- package/dist/billing-foundation/consumer-protection/termination-open-cancels.d.ts +4 -0
- package/dist/billing-foundation/consumer-protection/termination-open-cancels.js +14 -0
- package/dist/billing-foundation/consumer-protection/termination-provider-lock.d.ts +2 -0
- package/dist/billing-foundation/consumer-protection/termination-provider-lock.js +3 -0
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +4 -4
- package/dist/billing-foundation/consumer-protection/termination-record.js +11 -48
- package/dist/billing-foundation/consumer-protection/termination-request.js +10 -8
- package/dist/billing-foundation/consumer-protection/termination-revoke.js +10 -12
- package/dist/billing-foundation/events.js +1 -1
- package/dist/billing-foundation/feature.js +3 -3
- package/dist/billing-foundation/get-subscription-for-tenant.d.ts +1 -1
- package/dist/billing-foundation/handlers/process-event.write.js +5 -5
- package/dist/billing-foundation/handlers/process-payment-event.write.js +3 -3
- package/dist/billing-foundation/plan-catalog.js +2 -2
- package/dist/billing-foundation/projection.js +1 -1
- package/dist/billing-foundation/types.js +1 -1
- package/dist/cap-counter/aggregate-id.d.ts +1 -1
- package/dist/cap-counter/aggregate-id.js +1 -1
- package/dist/cap-counter/book-cap-usage.d.ts +2 -2
- package/dist/cap-counter/book-cap-usage.js +3 -3
- package/dist/cap-counter/cap-reservation.js +16 -7
- package/dist/cap-counter/constants.js +1 -1
- package/dist/cap-counter/feature.js +3 -2
- package/dist/cap-counter/handlers/increment-rolling.write.js +1 -1
- package/dist/cap-counter/reservation-chain.js +1 -1
- package/dist/cap-counter/with-cap-enforcement.d.ts +3 -3
- package/dist/cap-counter/with-cap-enforcement.js +1 -1
- package/dist/cap-overview/feature.js +1 -0
- package/dist/cap-overview/handlers/caps-usage.query.js +1 -1
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +1 -1
- package/dist/compliance-profiles/seeding.d.ts +1 -1
- package/dist/compliance-profiles/seeding.js +9 -3
- package/dist/config/feature.js +6 -6
- package/dist/config/handlers/cascade.query.js +1 -1
- package/dist/config/handlers/readiness.query.js +1 -1
- package/dist/config/handlers/reencrypt.job.js +23 -13
- package/dist/config/handlers/reset.write.js +3 -3
- package/dist/config/handlers/selected-extensions.query.js +1 -1
- package/dist/config/handlers/set.write.js +15 -5
- package/dist/config/handlers/values.query.js +1 -1
- package/dist/crypto-shredding/handlers/forget-subject.write.js +9 -9
- package/dist/crypto-shredding/resolve-file-bytes-deleter.js +3 -3
- package/dist/custom-fields/handlers/clear-custom-field.write.js +1 -1
- package/dist/custom-fields/handlers/delete-system-field.write.js +1 -1
- package/dist/custom-fields/handlers/delete-tenant-field.write.js +1 -1
- package/dist/custom-fields/handlers/set-custom-field.write.js +1 -1
- package/dist/custom-fields/index.d.ts +1 -0
- package/dist/custom-fields/index.js +1 -0
- package/dist/custom-fields/lib/parse-serialized-field.js +1 -1
- package/dist/data-retention/feature.js +1 -1
- package/dist/data-retention/run-retention-cleanup.js +12 -2
- package/dist/delivery/attempt-log.js +1 -1
- package/dist/delivery/delivery-service.js +5 -5
- package/dist/delivery/feature.js +3 -2
- package/dist/delivery/index.d.ts +1 -0
- package/dist/delivery/index.js +1 -0
- package/dist/delivery/jobs.js +11 -11
- package/dist/delivery/system-channel-context.d.ts +15 -0
- package/dist/delivery/system-channel-context.js +5 -0
- package/dist/document-ingest-foundation/append-missing-extract-event.d.ts +21 -0
- package/dist/document-ingest-foundation/append-missing-extract-event.js +42 -0
- package/dist/document-ingest-foundation/events.d.ts +11 -0
- package/dist/document-ingest-foundation/events.js +12 -1
- package/dist/document-ingest-foundation/index.d.ts +2 -1
- package/dist/document-ingest-foundation/index.js +2 -1
- package/dist/feature-toggles/feature.js +1 -0
- package/dist/feature-toggles/handlers/registered.query.js +1 -1
- package/dist/feature-toggles/handlers/set.write.js +1 -1
- package/dist/feature-toggles/toggle-sync-signal.js +1 -1
- package/dist/file-derivatives/feature.d.ts +1 -1
- package/dist/file-derivatives/feature.js +1 -1
- package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
- package/dist/file-derivatives/handlers/public-variant.query.js +5 -5
- package/dist/file-foundation/feature.js +1 -1
- package/dist/file-foundation/read-file-ref-content.d.ts +4 -2
- package/dist/file-foundation/read-file-ref-content.js +3 -3
- package/dist/file-provider-s3/feature.js +1 -1
- package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +8 -8
- package/dist/form-draft/feature.js +1 -1
- package/dist/form-draft/handlers/cleanup.job.js +22 -12
- package/dist/form-draft/handlers/discard.write.js +3 -3
- package/dist/form-draft/handlers/get.query.js +1 -1
- package/dist/foundation-shared/config-helpers.d.ts +1 -1
- package/dist/foundation-shared/config-helpers.js +1 -1
- package/dist/host-routing/validate-options.js +3 -0
- package/dist/inbound-mail-foundation/events.js +1 -1
- package/dist/inbound-mail-foundation/feature.js +2 -1
- package/dist/inbound-mail-foundation/handlers/account-state.d.ts +3 -1
- package/dist/inbound-mail-foundation/handlers/account-state.js +1 -1
- package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
- package/dist/inbound-mail-foundation/handlers/disconnect-account.write.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +10 -10
- package/dist/inbound-mail-foundation/handlers/update-account.write.js +1 -1
- package/dist/inbound-mail-foundation/projection.js +1 -1
- package/dist/inbound-mail-foundation/watch-supervisor.js +1 -1
- package/dist/inbound-provider-imap/feature.js +1 -1
- package/dist/jobs/events.js +1 -1
- package/dist/jobs/feature.js +1 -0
- package/dist/jobs/handlers/consumer.write.js +1 -1
- package/dist/jobs/handlers/projection-rebuild.job.js +1 -1
- package/dist/jobs/handlers/reindex-entity.job.js +5 -5
- package/dist/jobs/handlers/retention-cleanup.job.js +1 -1
- package/dist/jobs/handlers/retry.write.js +1 -1
- package/dist/jobs/handlers/stale-run-sweep.job.js +1 -1
- package/dist/jobs/handlers/trigger.write.js +2 -2
- package/dist/jobs/job-run-logger.d.ts +1 -0
- package/dist/jobs/job-run-logger.js +38 -18
- package/dist/ledger/web/index.js +1 -1
- package/dist/legal-pages/lib/boot-check.d.ts +10 -4
- package/dist/legal-pages/lib/boot-check.js +2 -2
- package/dist/legal-pages/seed.d.ts +3 -1
- package/dist/legal-pages/seed.js +2 -2
- package/dist/mail-foundation/feature.d.ts +2 -2
- package/dist/mail-transport-smtp/config-keys.js +1 -1
- package/dist/mail-transport-smtp/feature.js +7 -6
- package/dist/managed-pages/constants.d.ts +1 -0
- package/dist/managed-pages/constants.js +1 -0
- package/dist/managed-pages/feature.js +3 -1
- package/dist/managed-pages/index.d.ts +1 -0
- package/dist/managed-pages/index.js +1 -0
- package/dist/managed-pages/seeding.d.ts +1 -1
- package/dist/managed-pages/seeding.js +10 -4
- package/dist/notes-history/handlers/add-note.write.js +2 -2
- package/dist/personal-access-tokens/feature.js +2 -1
- package/dist/personal-access-tokens/handlers/create.write.js +2 -2
- package/dist/personal-access-tokens/handlers/revoke.write.js +1 -1
- package/dist/rate-limiting/handlers/status.query.js +2 -2
- package/dist/readiness/handlers/status.query.js +1 -1
- package/dist/renderer-foundation/api.d.ts +4 -2
- package/dist/renderer-foundation/api.js +2 -2
- package/dist/secrets/announce-secret-change.d.ts +6 -2
- package/dist/secrets/announce-secret-change.js +3 -3
- package/dist/secrets/feature.d.ts +1 -1
- package/dist/secrets/feature.js +8 -8
- package/dist/secrets/handlers/delete.write.js +1 -1
- package/dist/secrets/handlers/rotate.job.js +13 -8
- package/dist/secrets/handlers/set.write.js +1 -1
- package/dist/secrets/secrets-context.js +16 -6
- package/dist/secrets/write-gate.js +8 -2
- package/dist/seo/feature.js +1 -1
- package/dist/sessions/feature.js +2 -1
- package/dist/sessions/handlers/cleanup.job.js +2 -2
- package/dist/sessions/handlers/detail.query.d.ts +1 -0
- package/dist/sessions/handlers/detail.query.js +5 -2
- package/dist/sessions/handlers/list.query.d.ts +1 -0
- package/dist/sessions/handlers/list.query.js +5 -2
- package/dist/sessions/handlers/mine.query.d.ts +1 -0
- package/dist/sessions/handlers/mine.query.js +5 -2
- package/dist/sessions/handlers/revoke-all-for-user.write.js +3 -3
- package/dist/sessions/handlers/revoke-all-others.write.js +1 -1
- package/dist/sessions/handlers/revoke.write.js +1 -1
- package/dist/sessions/index.d.ts +1 -1
- package/dist/sessions/index.js +1 -1
- package/dist/sessions/screens.js +8 -4
- package/dist/sessions/session-callbacks.d.ts +0 -2
- package/dist/sessions/session-callbacks.js +1 -3
- package/dist/sessions/session-revoked-event.js +1 -1
- package/dist/sessions/web/session-device-cell.js +10 -12
- package/dist/sessions/web/user-agent.d.ts +1 -0
- package/dist/sessions/web/user-agent.js +11 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +2 -0
- package/dist/shared/purge-files-reaching-subject.d.ts +1 -0
- package/dist/shared/purge-files-reaching-subject.js +31 -4
- package/dist/shared/require-for-tenant.d.ts +1 -1
- package/dist/shared/require-for-tenant.js +6 -4
- package/dist/shared/run-in-sub-transaction.js +5 -5
- package/dist/shared/stream-tenant-backfill.d.ts +28 -0
- package/dist/shared/stream-tenant-backfill.js +119 -0
- package/dist/shared/stream-tenant-derived-state.d.ts +25 -0
- package/dist/shared/stream-tenant-derived-state.js +68 -0
- package/dist/step-dispatcher/deliver-dispatch-request.js +16 -3
- package/dist/step-dispatcher/feature.js +1 -1
- package/dist/step-dispatcher/mail-runner.d.ts +1 -0
- package/dist/subscription-dodo/plugin-methods.js +1 -1
- package/dist/subscription-dodo/runtime.js +4 -4
- package/dist/subscription-stripe/plugin-methods.js +3 -3
- package/dist/subscription-stripe/runtime.js +4 -4
- package/dist/tags/feature.js +1 -0
- package/dist/template-resolver/api.d.ts +4 -2
- package/dist/template-resolver/api.js +2 -2
- package/dist/template-resolver/document-ops.d.ts +5 -0
- package/dist/template-resolver/document-ops.js +18 -2
- package/dist/template-resolver/feature.js +2 -2
- package/dist/template-resolver/handlers/bundle-import.d.ts +4 -3
- package/dist/template-resolver/handlers/export-bundle.query.js +1 -1
- package/dist/template-resolver/handlers/layout-source.query.d.ts +4 -0
- package/dist/template-resolver/handlers/layout-source.query.js +26 -6
- package/dist/template-resolver/handlers/overview.query.js +17 -9
- package/dist/template-resolver/handlers/patch-draft.write.js +1 -1
- package/dist/template-resolver/handlers/send-test-shared.js +1 -1
- package/dist/template-resolver/handlers/send-test-status.query.d.ts +2 -1
- package/dist/template-resolver/handlers/send-test-status.query.js +18 -15
- package/dist/template-resolver/handlers/send-test.write.js +2 -2
- package/dist/template-resolver/handlers/tenant-override-selection.d.ts +3 -1
- package/dist/template-resolver/handlers/tenant-override-selection.js +1 -1
- package/dist/template-resolver/handlers/tenant-overrides.query.js +1 -1
- package/dist/template-resolver/handlers/usage.query.js +1 -1
- package/dist/template-resolver/seeding.d.ts +1 -0
- package/dist/template-resolver/seeding.js +17 -4
- package/dist/template-resolver/store-template-resource.d.ts +7 -6
- package/dist/template-resolver/store-template-resource.js +48 -8
- package/dist/template-resolver/template-resources.d.ts +2 -2
- package/dist/template-resolver/template-resources.js +2 -2
- package/dist/template-resolver/testing.js +2 -2
- package/dist/template-resolver/web/document-insert-fields.d.ts +4 -0
- package/dist/template-resolver/web/document-insert-fields.js +69 -0
- package/dist/template-resolver/web/document-insert-model.d.ts +43 -0
- package/dist/template-resolver/web/document-insert-model.js +106 -0
- package/dist/template-resolver/web/document-insert-panel.d.ts +13 -0
- package/dist/template-resolver/web/document-insert-panel.js +49 -0
- package/dist/template-resolver/web/document-tiptap-bubble.d.ts +4 -0
- package/dist/template-resolver/web/document-tiptap-bubble.js +85 -0
- package/dist/template-resolver/web/document-tiptap-content.d.ts +3 -0
- package/dist/template-resolver/web/document-tiptap-content.js +12 -0
- package/dist/template-resolver/web/document-tiptap-context.d.ts +41 -0
- package/dist/template-resolver/web/document-tiptap-context.js +43 -0
- package/dist/template-resolver/web/document-tiptap-editor.js +50 -495
- package/dist/template-resolver/web/document-tiptap-helpers.d.ts +44 -0
- package/dist/template-resolver/web/document-tiptap-helpers.js +129 -0
- package/dist/template-resolver/web/document-tiptap-layout.d.ts +4 -0
- package/dist/template-resolver/web/document-tiptap-layout.js +24 -0
- package/dist/template-resolver/web/document-tiptap-toolbar-groups.d.ts +6 -0
- package/dist/template-resolver/web/document-tiptap-toolbar-groups.js +33 -0
- package/dist/template-resolver/web/document-tiptap-toolbar.d.ts +3 -0
- package/dist/template-resolver/web/document-tiptap-toolbar.js +91 -0
- package/dist/template-resolver/web/editor-handle.d.ts +1 -0
- package/dist/template-resolver/web/editor-handle.js +15 -0
- package/dist/template-resolver/web/i18n.js +3 -0
- package/dist/template-resolver/web/placeholder-line-editor.js +16 -5
- package/dist/template-resolver/web/qualified-names.d.ts +1 -0
- package/dist/template-resolver/web/qualified-names.js +1 -0
- package/dist/template-resolver/web/use-document-tiptap-model.d.ts +54 -0
- package/dist/template-resolver/web/use-document-tiptap-model.js +96 -0
- package/dist/tenant/feature.js +1 -0
- package/dist/tenant/handlers/cancel-invitation.write.js +1 -1
- package/dist/tenant/handlers/memberships.query.js +1 -1
- package/dist/tenant/handlers/remove-member.write.js +2 -2
- package/dist/tenant/handlers/team-list.query.js +3 -3
- package/dist/tenant/handlers/update-member-roles.write.js +1 -1
- package/dist/tenant/index.d.ts +1 -0
- package/dist/tenant/index.js +1 -0
- package/dist/tenant/invited-membership.js +6 -1
- package/dist/tenant/schema/tenant.d.ts +2 -2
- package/dist/tenant/schema/tenant.js +4 -1
- package/dist/tenant/seeding.d.ts +2 -2
- package/dist/tenant/seeding.js +52 -52
- package/dist/tenant/stream-tenant-backfill.d.ts +21 -0
- package/dist/tenant/stream-tenant-backfill.js +63 -0
- package/dist/tenant/testing.d.ts +12 -0
- package/dist/tenant/testing.js +33 -0
- package/dist/tenant-handover/events.js +1 -1
- package/dist/tenant-handover/handlers/claim.write.js +1 -1
- package/dist/tenant-lifecycle/feature.js +5 -5
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +14 -5
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +12 -4
- package/dist/tenant-lifecycle/run-tenant-destroy.js +23 -13
- package/dist/tenant-lifecycle/stages.js +28 -2
- package/dist/tenant-lifecycle/testing.d.ts +1 -1
- package/dist/tenant-lifecycle/testing.js +6 -5
- package/dist/tier-engine/entity.js +1 -1
- package/dist/tier-engine/feature.js +8 -3
- package/dist/tier-engine/handlers/active-tier.query.js +3 -3
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +6 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -4
- package/dist/user/db/queries/stream-tenant-backfill.d.ts +20 -1
- package/dist/user/db/queries/stream-tenant-backfill.js +40 -77
- package/dist/user/feature.js +1 -0
- package/dist/user/handlers/find-for-auth.query.js +2 -2
- package/dist/user/handlers/update-roles.js +2 -2
- package/dist/user/index.d.ts +1 -1
- package/dist/user/index.js +1 -1
- package/dist/user/invited-global-roles.js +6 -1
- package/dist/user/principal-status.js +1 -1
- package/dist/user/seeding.d.ts +1 -1
- package/dist/user/seeding.js +20 -5
- package/dist/user/testing.d.ts +3 -0
- package/dist/user/testing.js +10 -0
- package/dist/user-data-rights/audit-download.js +12 -2
- package/dist/user-data-rights/feature.js +30 -26
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
- package/dist/user-data-rights/handlers/deletion-grace-period.js +1 -1
- package/dist/user-data-rights/handlers/download-by-job.query.js +2 -2
- package/dist/user-data-rights/handlers/download-by-token.query.js +2 -2
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/restrict-account.write.js +4 -4
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +8 -6
- package/dist/user-data-rights/index.d.ts +1 -1
- package/dist/user-data-rights/index.js +1 -1
- package/dist/user-data-rights/lib/mail-transport-resolver.js +1 -1
- package/dist/user-data-rights/lib/storage-provider-resolver.js +1 -1
- package/dist/user-data-rights/lib/update-user-lifecycle.js +6 -1
- package/dist/user-data-rights/run-export-jobs.js +7 -2
- package/dist/user-data-rights/run-forget-cleanup.d.ts +1 -0
- package/dist/user-data-rights/run-forget-cleanup.js +9 -1
- package/dist/user-data-rights/run-user-export.js +7 -1
- package/dist/user-data-rights/web/constants.d.ts +14 -0
- package/dist/user-data-rights/web/constants.js +14 -0
- package/dist/user-data-rights/web/index.d.ts +1 -0
- package/dist/user-data-rights/web/index.js +1 -0
- package/dist/user-profile/handlers/change-email.write.js +8 -6
- package/dist/waitlist/feature.js +1 -0
- package/dist/waitlist/handlers/export-purge.write.js +1 -1
- package/dist/waitlist/handlers/invite.write.js +2 -2
- package/dist/waitlist/handlers/reject.write.js +1 -1
- package/dist/waitlist/handlers/submit.write.js +20 -11
- package/dist/waitlist/import.d.ts +2 -1
- package/dist/waitlist/import.js +22 -2
- package/dist/waitlist/legacy-migrate.js +8 -3
- package/dist/waitlist/link-on-signup.js +6 -1
- package/dist/waitlist/options.d.ts +2 -1
- package/dist/workflow-runner/db/queries/due-runs.js +1 -1
- package/dist/workflow-runner/event-subscriber.js +4 -3
- package/dist/workflow-runner/event-trigger.js +2 -1
- package/dist/workflow-runner/feature.js +2 -2
- package/dist/workflow-runner/handlers/resume-run.write.js +6 -6
- package/dist/workflow-runner/runner.d.ts +3 -1
- package/dist/workflow-runner/runner.js +3 -3
- package/package.json +41 -23
- package/src/agent-tools/changes.json +6 -0
- package/src/auth-email-password/changes.json +18 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +6 -0
- package/src/cap-counter/changes.json +12 -0
- package/src/config/changes.json +6 -0
- package/src/crypto-shredding/changes.json +12 -0
- package/src/custom-fields/changes.json +6 -0
- package/src/delivery/changes.json +12 -0
- package/src/document-ingest-foundation/changes.json +6 -0
- package/src/host-routing/changes.json +6 -0
- package/src/jobs/changes.json +6 -0
- package/src/mail-transport-smtp/changes.json +12 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/secrets/changes.json +6 -0
- package/src/sessions/changes.json +19 -0
- package/src/step-dispatcher/changes.json +6 -0
- package/src/template-resolver/changes.json +54 -0
- package/src/tenant/changes.json +19 -0
- package/src/tenant-lifecycle/changes.json +6 -0
- package/src/user-data-rights/changes.json +36 -0
- package/src/waitlist/changes.json +18 -0
- package/src/workflow-runner/changes.json +6 -0
|
@@ -53,7 +53,7 @@ export function createEnableStartHandler(opts) {
|
|
|
53
53
|
if (!accountLabel) {
|
|
54
54
|
// The declarative secretMint screen has no client component left to
|
|
55
55
|
// read the session email off — derive the otpauth label server-side.
|
|
56
|
-
accountLabel = readableAccountLabel(await ctx.queryAs(event.user, UserQueries.me, {}));
|
|
56
|
+
accountLabel = readableAccountLabel(await ctx.dispatch.queryAs(event.user, UserQueries.me, {}));
|
|
57
57
|
}
|
|
58
58
|
const secret = generateTotpSecret();
|
|
59
59
|
const recoveryCodes = generateRecoveryCodes();
|
|
@@ -47,12 +47,12 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
|
|
|
47
47
|
// hoisted function declarations below capture this — pin the narrowed
|
|
48
48
|
// type explicitly so TS keeps it inside the closures.
|
|
49
49
|
const cipher = maybeCipher;
|
|
50
|
-
if (!ctx.masterKeyProvider) {
|
|
50
|
+
if (!ctx.services.masterKeyProvider) {
|
|
51
51
|
throw new InternalError({
|
|
52
|
-
message: "[auth-mfa:reencrypt] ctx.masterKeyProvider missing — wire it via extraContext.masterKeyProvider at boot.",
|
|
52
|
+
message: "[auth-mfa:reencrypt] ctx.services.masterKeyProvider missing — wire it via extraContext.masterKeyProvider at boot.",
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
-
const provider = ctx.masterKeyProvider;
|
|
55
|
+
const provider = ctx.services.masterKeyProvider;
|
|
56
56
|
// Undefined = per-subject crypto-shredding isn't configured for this app —
|
|
57
57
|
// both fields are then plain envelope strings, no PII layer to peel.
|
|
58
58
|
const piiKms = configuredPiiSubjectKms();
|
|
@@ -65,7 +65,12 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
|
|
|
65
65
|
function tdbFor(tenantId) {
|
|
66
66
|
let existing = tdbCache.get(tenantId);
|
|
67
67
|
if (!existing) {
|
|
68
|
-
existing = createTenantDb({
|
|
68
|
+
existing = createTenantDb({
|
|
69
|
+
db,
|
|
70
|
+
tenantId,
|
|
71
|
+
mode: "system",
|
|
72
|
+
reason: "MFA key re-encryption job rewrites enrollments of every tenant",
|
|
73
|
+
});
|
|
69
74
|
tdbCache.set(tenantId, existing);
|
|
70
75
|
}
|
|
71
76
|
return existing;
|
|
@@ -101,7 +106,7 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
|
|
|
101
106
|
const totpClass = classifyStoredEnvelope(envelopeValues.totpSecret, targetVersion);
|
|
102
107
|
const recoveryClass = classifyStoredEnvelope(envelopeValues.recoveryCodes, targetVersion);
|
|
103
108
|
if (totpClass === "unrecognized" || recoveryClass === "unrecognized") {
|
|
104
|
-
ctx.log?.warn?.(`[auth-mfa:reencrypt] row ${row.id} has a field that is not a current-cipher envelope, not re-encryptable`);
|
|
109
|
+
ctx.obs.log?.warn?.(`[auth-mfa:reencrypt] row ${row.id} has a field that is not a current-cipher envelope, not re-encryptable`);
|
|
105
110
|
return "failed";
|
|
106
111
|
}
|
|
107
112
|
if (totpClass === "current" && recoveryClass === "current") {
|
|
@@ -128,7 +133,7 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
|
|
|
128
133
|
if (!result.isSuccess) {
|
|
129
134
|
if (result.error.code === "version_conflict")
|
|
130
135
|
return "skipped";
|
|
131
|
-
ctx.log?.warn?.(`[auth-mfa:reencrypt] executor rejected row ${row.id}`, {
|
|
136
|
+
ctx.obs.log?.warn?.(`[auth-mfa:reencrypt] executor rejected row ${row.id}`, {
|
|
132
137
|
code: result.error.code,
|
|
133
138
|
});
|
|
134
139
|
return "failed";
|
|
@@ -140,9 +145,9 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
|
|
|
140
145
|
migrateRow,
|
|
141
146
|
maxFailures,
|
|
142
147
|
deadlineAt: deadline,
|
|
143
|
-
signal: ctx.signal,
|
|
148
|
+
signal: ctx.lifecycle.signal,
|
|
144
149
|
onRowError: (row, err) => {
|
|
145
|
-
ctx.log?.warn?.(`[auth-mfa:reencrypt] failed to re-encrypt row ${row.id}`, { err });
|
|
150
|
+
ctx.obs.log?.warn?.(`[auth-mfa:reencrypt] failed to re-encrypt row ${row.id}`, { err });
|
|
146
151
|
},
|
|
147
152
|
});
|
|
148
153
|
const result = {
|
|
@@ -152,5 +157,5 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
|
|
|
152
157
|
batchesProcessed: outcome.batchesProcessed,
|
|
153
158
|
stoppedReason: outcome.stoppedReason,
|
|
154
159
|
};
|
|
155
|
-
ctx.log?.info?.(`[auth-mfa:reencrypt] complete: ${JSON.stringify(result)}`);
|
|
160
|
+
ctx.obs.log?.info?.(`[auth-mfa:reencrypt] complete: ${JSON.stringify(result)}`);
|
|
156
161
|
}
|
|
@@ -37,16 +37,16 @@ export function createRegenerateRecoveryHandler(opts) {
|
|
|
37
37
|
// afterward.
|
|
38
38
|
//
|
|
39
39
|
// Fail closed, matching verify.write.ts/enable-confirm-preauth.write.ts:
|
|
40
|
-
// without ctx.redis, verifyMfaFactor's TOTP-replay burn is skipped,
|
|
40
|
+
// without ctx.services.redis, verifyMfaFactor's TOTP-replay burn is skipped,
|
|
41
41
|
// letting an observed code be replayed within the ±1-step window to
|
|
42
42
|
// regenerate (and thus invalidate) recovery codes — a session-authed
|
|
43
43
|
// route, so pre-unwiring JWTs stay a valid attack path.
|
|
44
|
-
if (!ctx.redis) {
|
|
44
|
+
if (!ctx.services.redis) {
|
|
45
45
|
return writeFailure(new InternalError({
|
|
46
|
-
message: "regenerate-recovery (mfa) requires ctx.redis for TOTP replay protection",
|
|
46
|
+
message: "regenerate-recovery (mfa) requires ctx.services.redis for TOTP replay protection",
|
|
47
47
|
}));
|
|
48
48
|
}
|
|
49
|
-
const replay = { redis: ctx.redis, userId: event.user.id };
|
|
49
|
+
const replay = { redis: ctx.services.redis, userId: event.user.id };
|
|
50
50
|
const verify = await verifyMfaFactor(row, event.payload.code, replay);
|
|
51
51
|
if (!verify.ok)
|
|
52
52
|
return invalidTotpCode();
|
|
@@ -29,8 +29,8 @@ export function createMfaVerifyHandler(opts) {
|
|
|
29
29
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
30
30
|
escapeHatch: {
|
|
31
31
|
grants: ["systemIdentity", "unsafeRaw"],
|
|
32
|
-
reason: "Pre-auth MFA step has no session yet — re-derives it via ctx.queryAs(SYSTEM, " +
|
|
33
|
-
"user:findForAuth) and ctx.resolveActiveMembership for the user the challenge token names. " +
|
|
32
|
+
reason: "Pre-auth MFA step has no session yet — re-derives it via ctx.dispatch.queryAs(SYSTEM, " +
|
|
33
|
+
"user:findForAuth) and ctx.auth.resolveActiveMembership for the user the challenge token names. " +
|
|
34
34
|
"Also reads the MFA enrollment of the tenant named in the signed login/setup token, not the " +
|
|
35
35
|
"guest dispatch tenant.",
|
|
36
36
|
},
|
|
@@ -46,19 +46,19 @@ export function createMfaVerifyHandler(opts) {
|
|
|
46
46
|
// this route sits behind a challenge token from a successful login,
|
|
47
47
|
// but its own secret is still just a short TOTP/recovery code —
|
|
48
48
|
// silently skipping the brute-force cap and single-use burn when
|
|
49
|
-
// ctx.redis is absent would let a stolen challenge token be guessed
|
|
49
|
+
// ctx.services.redis is absent would let a stolen challenge token be guessed
|
|
50
50
|
// against unboundedly, and let a recovery code be replayed via a
|
|
51
51
|
// reissued challenge (mfa-verify-attempts.ts's counter is deliberately
|
|
52
52
|
// NOT reset by reissuance, precisely to survive this).
|
|
53
|
-
if (!ctx.redis) {
|
|
53
|
+
if (!ctx.services.redis) {
|
|
54
54
|
return writeFailure(new InternalError({
|
|
55
|
-
message: "verify (mfa) requires ctx.redis for the brute-force cap and single-use burn",
|
|
55
|
+
message: "verify (mfa) requires ctx.services.redis for the brute-force cap and single-use burn",
|
|
56
56
|
}));
|
|
57
57
|
}
|
|
58
58
|
// Per-account brute-force cap — survives challenge-token reissuance
|
|
59
59
|
// on purpose (see mfa-verify-attempts.ts header). Checked BEFORE the
|
|
60
60
|
// TOTP/recovery verify so a locked account can't be guessed against.
|
|
61
|
-
const state = await getMfaVerifyLockoutState(ctx.redis, userId);
|
|
61
|
+
const state = await getMfaVerifyLockoutState(ctx.services.redis, userId);
|
|
62
62
|
if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
|
|
63
63
|
const now = Date.now();
|
|
64
64
|
if (state.lockedUntil > now) {
|
|
@@ -69,7 +69,12 @@ export function createMfaVerifyHandler(opts) {
|
|
|
69
69
|
// "system" mode: this handler runs with a guest identity whose own
|
|
70
70
|
// tenantId is meaningless here — the challenge token is the source
|
|
71
71
|
// of truth for which tenant's row to read.
|
|
72
|
-
const scopedDb = createTenantDb({
|
|
72
|
+
const scopedDb = createTenantDb({
|
|
73
|
+
db: ctx.db.unsafeRaw(),
|
|
74
|
+
tenantId,
|
|
75
|
+
mode: "system",
|
|
76
|
+
reason: "MFA challenge has no session, the tenant comes from the verified challenge token",
|
|
77
|
+
});
|
|
73
78
|
const scopedUser = { id: userId, tenantId, roles: ["User"] };
|
|
74
79
|
const row = await findUserMfaRow(scopedDb, scopedUser);
|
|
75
80
|
// MFA got disabled between login and verify (race, or a stale
|
|
@@ -77,10 +82,10 @@ export function createMfaVerifyHandler(opts) {
|
|
|
77
82
|
// bad code, no detail leak about account state.
|
|
78
83
|
if (!row)
|
|
79
84
|
return invalidChallengeToken();
|
|
80
|
-
const replay = { redis: ctx.redis, userId };
|
|
85
|
+
const replay = { redis: ctx.services.redis, userId };
|
|
81
86
|
const verify = await verifyMfaFactor(row, event.payload.code, replay);
|
|
82
87
|
if (!verify.ok) {
|
|
83
|
-
await recordFailedMfaVerifyAttempt(ctx.redis, userId, maxAttempts, lockoutMinutes);
|
|
88
|
+
await recordFailedMfaVerifyAttempt(ctx.services.redis, userId, maxAttempts, lockoutMinutes);
|
|
84
89
|
return invalidTotpCode();
|
|
85
90
|
}
|
|
86
91
|
// Burn the challenge token only after a successful code check — a
|
|
@@ -91,7 +96,7 @@ export function createMfaVerifyHandler(opts) {
|
|
|
91
96
|
// is reached, so this burn only needs to guard a DIFFERENT code
|
|
92
97
|
// (recovery, or a still-valid-but-not-yet-burned TOTP step) reused
|
|
93
98
|
// against the same challenge token.
|
|
94
|
-
const burnResult = await burnToken(ctx.redis, "mfa-challenge", userId, verified.expiresAtMs);
|
|
99
|
+
const burnResult = await burnToken(ctx.services.redis, "mfa-challenge", userId, verified.expiresAtMs);
|
|
95
100
|
if (burnResult === "already-used")
|
|
96
101
|
return invalidChallengeToken();
|
|
97
102
|
// Recovery-code use consumes the code — persist the reduced hash-list
|
|
@@ -106,12 +111,12 @@ export function createMfaVerifyHandler(opts) {
|
|
|
106
111
|
if (!updateResult.isSuccess)
|
|
107
112
|
return updateResult;
|
|
108
113
|
}
|
|
109
|
-
await clearMfaVerifyAttempts(ctx.redis, userId);
|
|
114
|
+
await clearMfaVerifyAttempts(ctx.services.redis, userId);
|
|
110
115
|
// Re-derive the full session the way login.write.ts would have, for
|
|
111
116
|
// the SAME tenant the challenge token already committed to (no
|
|
112
117
|
// "pick a tenant" step here — that already happened at login time).
|
|
113
118
|
const systemUser = createSystemUser(tenantId, ["SystemAdmin"]);
|
|
114
|
-
const userRow = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
119
|
+
const userRow = (await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
|
|
115
120
|
id: userId,
|
|
116
121
|
})); // @cast-boundary engine-payload
|
|
117
122
|
// Re-check status + membership the way login.write.ts does after its
|
|
@@ -130,7 +135,7 @@ export function createMfaVerifyHandler(opts) {
|
|
|
130
135
|
const globalRoles = parseRoles(userRow?.roles ?? null);
|
|
131
136
|
// Membership revoked or principal blocked between login and verify —
|
|
132
137
|
// mirror login.write.ts's refusal instead of falling back to globalRoles only.
|
|
133
|
-
const active = await ctx.resolveActiveMembership(userId, tenantId);
|
|
138
|
+
const active = await ctx.auth.resolveActiveMembership(userId, tenantId);
|
|
134
139
|
if (active.kind === "rejected")
|
|
135
140
|
return invalidChallengeToken();
|
|
136
141
|
// buildSessionRoles calls stripForbiddenMembershipRoles to strip reserved
|
|
@@ -144,7 +149,7 @@ export function createMfaVerifyHandler(opts) {
|
|
|
144
149
|
...sessionTimezoneField(userRow?.timezone),
|
|
145
150
|
...sessionLocaleField(userRow?.locale),
|
|
146
151
|
};
|
|
147
|
-
const claims = await ctx.
|
|
152
|
+
const claims = await ctx.auth.resolveClaims(baseSession);
|
|
148
153
|
const session = Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
|
|
149
154
|
return { isSuccess: true, data: { kind: "mfa-verify-success", session } };
|
|
150
155
|
},
|
|
@@ -14,15 +14,20 @@ export function createMfaCodeVerifier() {
|
|
|
14
14
|
declareEscapeHatch({
|
|
15
15
|
reason: "reads the MFA enrollment of the user being re-authenticated on the caller's handler context; the calling handler declares its own escapeHatch",
|
|
16
16
|
});
|
|
17
|
-
const scopedDb = createTenantDb({
|
|
17
|
+
const scopedDb = createTenantDb({
|
|
18
|
+
db: ctx.db.unsafeRaw(),
|
|
19
|
+
tenantId,
|
|
20
|
+
mode: "system",
|
|
21
|
+
reason: "re-authentication reads the MFA enrollment of the user's own tenant, not the handler's dispatch tenant",
|
|
22
|
+
});
|
|
18
23
|
const row = await findUserMfaRow(scopedDb, { id: userId, tenantId, roles: [] });
|
|
19
24
|
if (!row)
|
|
20
25
|
return { enrolled: false, ok: false };
|
|
21
|
-
// Fail closed without ctx.redis, matching disable.write.ts/verify.write.ts —
|
|
26
|
+
// Fail closed without ctx.services.redis, matching disable.write.ts/verify.write.ts —
|
|
22
27
|
// skipping the TOTP-replay burn would let an observed code be replayed.
|
|
23
|
-
if (!code || !ctx.redis)
|
|
28
|
+
if (!code || !ctx.services.redis)
|
|
24
29
|
return { enrolled: true, ok: false };
|
|
25
|
-
const verify = await verifyMfaFactor(row, code, { redis: ctx.redis, userId });
|
|
30
|
+
const verify = await verifyMfaFactor(row, code, { redis: ctx.services.redis, userId });
|
|
26
31
|
return { enrolled: true, ok: verify.ok && verify.method === "totp" };
|
|
27
32
|
};
|
|
28
33
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
1
2
|
import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
import { access, declareEscapeHatch, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
4
|
import { mfaRequiredConfigHandle, mfaRequiredConfigKey } from "./config.js";
|
|
@@ -10,7 +11,12 @@ export function createMfaStatusChecker(opts) {
|
|
|
10
11
|
declareEscapeHatch({
|
|
11
12
|
reason: "reads the MFA enrollment of the tenant named in the signed login/setup token, not the guest dispatch tenant, on the caller's handler context; the calling handler declares its own escapeHatch",
|
|
12
13
|
});
|
|
13
|
-
const scopedDb = createTenantDb({
|
|
14
|
+
const scopedDb = createTenantDb({
|
|
15
|
+
db: ctx.db.unsafeRaw(),
|
|
16
|
+
tenantId,
|
|
17
|
+
mode: "system",
|
|
18
|
+
reason: "login/setup token names the tenant to check, the guest dispatch tenant is meaningless",
|
|
19
|
+
});
|
|
14
20
|
const scopedUser = { id: userId, tenantId, roles: ["User"] };
|
|
15
21
|
const row = await findUserMfaRow(scopedDb, scopedUser);
|
|
16
22
|
if (row) {
|
|
@@ -21,20 +27,21 @@ export function createMfaStatusChecker(opts) {
|
|
|
21
27
|
return { required: true, challengeToken: token };
|
|
22
28
|
}
|
|
23
29
|
// ctx.config is bound to the CALLING user's tenant (the anonymous
|
|
24
|
-
// identity's tenant for login) — wrong tenant here. ctx.configResolver takes tenantId
|
|
30
|
+
// identity's tenant for login) — wrong tenant here. ctx.services.configResolver takes tenantId
|
|
25
31
|
// explicitly, which is what a pre-session check against the LOGGING-IN
|
|
26
32
|
// user's tenant needs.
|
|
27
33
|
// @cast-boundary engine-payload — "select" config values are validated
|
|
28
34
|
// against the declared `options` at write-time by the config feature.
|
|
29
|
-
const policy = ((await ctx.configResolver?.get(mfaRequiredConfigHandle.name, mfaRequiredConfigKey(opts.requiredPolicy), tenantId, userId, scopedDb)) ?? "optional");
|
|
35
|
+
const policy = ((await ctx.services.configResolver?.get(mfaRequiredConfigHandle.name, mfaRequiredConfigKey(opts.requiredPolicy), tenantId, userId, scopedDb)) ?? "optional");
|
|
30
36
|
if (policy === "optional")
|
|
31
37
|
return { required: false };
|
|
32
38
|
const buildSetupToken = () => signMfaPreauthSetupToken({ userId, tenantId }, MFA_PREAUTH_SETUP_TOKEN_TTL_MINUTES, opts.challengeTokenSecret).token;
|
|
33
39
|
if (policy === "all")
|
|
34
40
|
return { setupRequired: true, preauthSetupToken: buildSetupToken() };
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
const isEnforced = policy === "systemAdmins"
|
|
42
|
+
? roles.includes(ROLES.SystemAdmin)
|
|
43
|
+
: roles.some((role) => access.admin.includes(role));
|
|
44
|
+
return isEnforced
|
|
38
45
|
? { setupRequired: true, preauthSetupToken: buildSetupToken() }
|
|
39
46
|
: { required: false };
|
|
40
47
|
};
|
|
@@ -13,7 +13,7 @@ import { STEP_SECONDS, verifyTotp } from "./totp.js";
|
|
|
13
13
|
// must not verify again within its ±1-step window — otherwise a code an
|
|
14
14
|
// attacker observed once, via phishing proxy or shoulder-surfing, stays
|
|
15
15
|
// usable for ~90s of parallel logins). Omitted where a caller has no
|
|
16
|
-
// ctx.redis available; the code-verify itself still works, just without
|
|
16
|
+
// ctx.services.redis available; the code-verify itself still works, just without
|
|
17
17
|
// replay protection for that call.
|
|
18
18
|
export async function verifyMfaFactor(row, code, replay) {
|
|
19
19
|
const secret = base32Decode(row.totpSecret);
|
|
@@ -39,8 +39,8 @@ export function createBillingInfoQueryConfig(deps) {
|
|
|
39
39
|
const billingLive = ctx.config
|
|
40
40
|
? (await ctx.config(STRIPE_BILLING_LIVE_CONFIG_QN)) === true
|
|
41
41
|
: false;
|
|
42
|
-
const apiKeySet = ctx.secrets
|
|
43
|
-
? await ctx.secrets.has(SYSTEM_TENANT_ID, STRIPE_API_KEY_CONFIG_QN)
|
|
42
|
+
const apiKeySet = ctx.services.secrets
|
|
43
|
+
? await ctx.services.secrets.has(SYSTEM_TENANT_ID, STRIPE_API_KEY_CONFIG_QN)
|
|
44
44
|
: false;
|
|
45
45
|
const sub = await ctx.db.fetchOne(subscriptionsProjectionTable, { id: subscriptionAggregateId(ctx.user.tenantId) });
|
|
46
46
|
return {
|
|
@@ -112,7 +112,7 @@ export async function recordConsent(ctx, prepared, details) {
|
|
|
112
112
|
};
|
|
113
113
|
const tenantId = ctx.user.tenantId;
|
|
114
114
|
const isSubscription = prepared.mode === "subscription";
|
|
115
|
-
await ctx.
|
|
115
|
+
await ctx.events.unsafeAppend({
|
|
116
116
|
aggregateId: isSubscription ? subscriptionAggregateId(tenantId) : paymentAggregateId(tenantId),
|
|
117
117
|
aggregateType: isSubscription ? SUBSCRIPTION_AGGREGATE_TYPE : PAYMENT_AGGREGATE_TYPE,
|
|
118
118
|
type: CHECKOUT_CONSENT_RECORDED_EVENT_QN,
|
|
@@ -64,7 +64,7 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
|
|
|
64
64
|
message: `billing-foundation: ${payload.sourceAggregateId} is not the subscription or payment stream of tenant ${tenantId}`,
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
|
-
const stream = await ctx.fetchForWriting({
|
|
67
|
+
const stream = await ctx.events.fetchForWriting({
|
|
68
68
|
aggregateId: payload.sourceAggregateId,
|
|
69
69
|
aggregateType: isSubscription ? SUBSCRIPTION_AGGREGATE_TYPE : PAYMENT_AGGREGATE_TYPE,
|
|
70
70
|
});
|
|
@@ -81,7 +81,7 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
if (!consentEvent || !consent) {
|
|
84
|
-
ctx.log?.warn(`[billing-foundation:issue-contract-confirmation] no recorded consent ${payload.consentId} on the stream; no mail sent`);
|
|
84
|
+
ctx.obs.log?.warn(`[billing-foundation:issue-contract-confirmation] no recorded consent ${payload.consentId} on the stream; no mail sent`);
|
|
85
85
|
return {
|
|
86
86
|
isSuccess: true,
|
|
87
87
|
data: {
|
|
@@ -104,14 +104,14 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
|
|
|
104
104
|
}
|
|
105
105
|
// Everything that can fail runs before the append, so a failed run
|
|
106
106
|
// leaves no marker behind and the job retry starts clean.
|
|
107
|
-
const recipient = recipientRowSchema.safeParse(await ctx.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
|
|
107
|
+
const recipient = recipientRowSchema.safeParse(await ctx.dispatch.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
|
|
108
108
|
id: consent.actorUserId,
|
|
109
109
|
}));
|
|
110
110
|
const recipientEmail = recipient.success && recipient.data.isDeleted !== true
|
|
111
111
|
? await decryptStoredUserEmail(recipient.data.email, "billing-foundation:issue-contract-confirmation")
|
|
112
112
|
: null;
|
|
113
113
|
if (!recipient.success || recipientEmail === null) {
|
|
114
|
-
ctx.log?.warn(`[billing-foundation:issue-contract-confirmation] recipient of consent ${payload.consentId} not found; no mail sent`);
|
|
114
|
+
ctx.obs.log?.warn(`[billing-foundation:issue-contract-confirmation] recipient of consent ${payload.consentId} not found; no mail sent`);
|
|
115
115
|
return {
|
|
116
116
|
isSuccess: true,
|
|
117
117
|
data: {
|
|
@@ -128,7 +128,7 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
|
|
|
128
128
|
locale,
|
|
129
129
|
});
|
|
130
130
|
if (terms.version !== consent.termsTemplateVersion) {
|
|
131
|
-
ctx.log?.warn(`[billing-foundation:issue-contract-confirmation] terms text block changed since consent ${payload.consentId} (version ${consent.termsTemplateVersion} -> ${terms.version}); the mail carries the current text`);
|
|
131
|
+
ctx.obs.log?.warn(`[billing-foundation:issue-contract-confirmation] terms text block changed since consent ${payload.consentId} (version ${consent.termsTemplateVersion} -> ${terms.version}); the mail carries the current text`);
|
|
132
132
|
}
|
|
133
133
|
const i18n = createI18n(ctx.registry, { defaultLocale: "en" });
|
|
134
134
|
const tierLabel = consent.tier !== null && options.catalog
|
|
@@ -49,7 +49,7 @@ export function registerConsumerProtection(r, options, consumerProtection) {
|
|
|
49
49
|
// user), never from payload data.
|
|
50
50
|
const tenantId = ctx.systemUser.tenantId;
|
|
51
51
|
const currentPeriodEndIso = payload["currentPeriodEndIso"];
|
|
52
|
-
const result = await ctx.write(SubscriptionFoundationHandlers.issueContractConfirmation, {
|
|
52
|
+
const result = await ctx.dispatch.write(SubscriptionFoundationHandlers.issueContractConfirmation, {
|
|
53
53
|
consentId,
|
|
54
54
|
sourceAggregateId: isSubscriptionEvent
|
|
55
55
|
? subscriptionAggregateId(tenantId)
|
|
@@ -37,7 +37,7 @@ export function registerContractTermination(r, options, consumerProtection) {
|
|
|
37
37
|
// skip: trigger event carries no usable declaration
|
|
38
38
|
if (!declared.success)
|
|
39
39
|
return;
|
|
40
|
-
const result = await ctx.write(SubscriptionFoundationHandlers.recordContractTermination, {
|
|
40
|
+
const result = await ctx.dispatch.write(SubscriptionFoundationHandlers.recordContractTermination, {
|
|
41
41
|
requestId: declared.data.requestId,
|
|
42
42
|
declarationType: declared.data.declarationType,
|
|
43
43
|
terminationKind: declared.data.terminationKind,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CONTRACT_TERMINATION_REQUESTED_EVENT_QN, contractTerminationRequestedPayloadSchema, } from "../events.js";
|
|
2
|
+
import { hasRevocation } from "./termination-record.js";
|
|
3
|
+
// A revoked declaration must not resume a subscription another declaration still cancels at the provider.
|
|
4
|
+
export function hasOtherOpenProviderCancel(events, requestId) {
|
|
5
|
+
return events.some((e) => {
|
|
6
|
+
if (e.type !== CONTRACT_TERMINATION_REQUESTED_EVENT_QN)
|
|
7
|
+
return false;
|
|
8
|
+
const recorded = contractTerminationRequestedPayloadSchema.safeParse(e.payload).data;
|
|
9
|
+
return (recorded !== undefined &&
|
|
10
|
+
recorded.requestId !== requestId &&
|
|
11
|
+
recorded.providerCancel !== "none" &&
|
|
12
|
+
!hasRevocation(events, recorded.requestId));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -14,22 +14,22 @@ function receiptBucketKey(ctx, email) {
|
|
|
14
14
|
* recorded, a missing receipt is the lesser harm. */
|
|
15
15
|
export async function mayMailReceipt(ctx, email) {
|
|
16
16
|
// skip: no rate limiter wired, nothing to count against
|
|
17
|
-
if (!ctx.rateLimit)
|
|
17
|
+
if (!ctx.services.rateLimit)
|
|
18
18
|
return "allowed";
|
|
19
19
|
try {
|
|
20
20
|
const bucketKey = receiptBucketKey(ctx, email);
|
|
21
21
|
if (bucketKey === undefined) {
|
|
22
|
-
ctx.log?.warn("[billing-foundation:termination] no rate-limit digest available, receipt mail withheld");
|
|
22
|
+
ctx.obs.log?.warn("[billing-foundation:termination] no rate-limit digest available, receipt mail withheld");
|
|
23
23
|
return "unavailable";
|
|
24
24
|
}
|
|
25
|
-
const decision = await ctx.rateLimit.check(bucketKey, {
|
|
25
|
+
const decision = await ctx.services.rateLimit.check(bucketKey, {
|
|
26
26
|
limit: RECEIPT_MAILS_PER_RECIPIENT,
|
|
27
27
|
windowSeconds: RECEIPT_WINDOW_SECONDS,
|
|
28
28
|
});
|
|
29
29
|
return decision.allowed ? "allowed" : "limited";
|
|
30
30
|
}
|
|
31
31
|
catch (error) {
|
|
32
|
-
ctx.log?.warn(`[billing-foundation:termination] receipt rate limiter failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
+
ctx.obs.log?.warn(`[billing-foundation:termination] receipt rate limiter failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
33
|
return "unavailable";
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -12,6 +12,7 @@ import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
|
|
|
12
12
|
import { isTerminalSubscriptionStatus, SubscriptionCancelTimings } from "../web/constants.js";
|
|
13
13
|
import { CONSENT_LOCALES, FALLBACK_CONSENT_LOCALE, } from "../web/consumer-protection/consent-locale.js";
|
|
14
14
|
import { notifyOperator } from "./termination-notify.js";
|
|
15
|
+
import { TERMINATION_PROVIDER_LOCK_KEY, TERMINATION_PROVIDER_LOCK_NAMESPACE, } from "./termination-provider-lock.js";
|
|
15
16
|
import { isWithinWithdrawalPeriod } from "./withdrawal-period.js";
|
|
16
17
|
const REQUESTED_CANCEL_TIMINGS = [
|
|
17
18
|
SubscriptionCancelTimings.periodEnd,
|
|
@@ -85,7 +86,7 @@ async function cancelAtProvider(ctx, tenantId, when, receivedAtIso) {
|
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
88
|
catch (error) {
|
|
88
|
-
ctx.log?.warn(`[billing-foundation:record-contract-termination] provider cancel failed for tenant ${tenantId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
ctx.obs.log?.warn(`[billing-foundation:record-contract-termination] provider cancel failed for tenant ${tenantId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
90
|
return { providerCancel: "none", effectiveAtIso: null, noticeReason: "provider_error" };
|
|
90
91
|
}
|
|
91
92
|
return {
|
|
@@ -105,7 +106,7 @@ export function createDeclareContractTerminationHandler() {
|
|
|
105
106
|
handler: async (event, ctx) => {
|
|
106
107
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
107
108
|
const payload = event.payload;
|
|
108
|
-
const stream = await ctx.fetchForWriting({
|
|
109
|
+
const stream = await ctx.events.fetchForWriting({
|
|
109
110
|
aggregateId: subscriptionAggregateId(event.user.tenantId),
|
|
110
111
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
111
112
|
});
|
|
@@ -127,30 +128,6 @@ function revokedRecordResult(receivedAtIso) {
|
|
|
127
128
|
},
|
|
128
129
|
};
|
|
129
130
|
}
|
|
130
|
-
// A revocation that landed during the provider cancel found nothing to take back yet.
|
|
131
|
-
async function takeBackLateRevokedCancel(ctx, consumerProtection, args) {
|
|
132
|
-
const { payload, tenantId, receivedAtIso, providerCancel } = args;
|
|
133
|
-
// skip: nothing was cancelled at the provider, nothing to resume
|
|
134
|
-
if (providerCancel !== "period-end")
|
|
135
|
-
return;
|
|
136
|
-
const reasons = await resumeAtProvider(ctx, await getSubscriptionForTenant(ctx, tenantId));
|
|
137
|
-
// skip: resumed cleanly, the revocation notice already tells the operator
|
|
138
|
-
if (reasons.length === 0)
|
|
139
|
-
return;
|
|
140
|
-
await notifyOperator(ctx, consumerProtection, {
|
|
141
|
-
locale: payload.locale ?? FALLBACK_CONSENT_LOCALE,
|
|
142
|
-
requestId: payload.requestId,
|
|
143
|
-
receivedAtIso,
|
|
144
|
-
channel: "public",
|
|
145
|
-
declaration: {
|
|
146
|
-
declarationType: payload.declarationType,
|
|
147
|
-
terminationKind: payload.terminationKind,
|
|
148
|
-
},
|
|
149
|
-
reasons,
|
|
150
|
-
tenantId,
|
|
151
|
-
providerCancel,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
131
|
export function createRecordContractTerminationHandler(options, consumerProtection) {
|
|
155
132
|
return {
|
|
156
133
|
name: "record-contract-termination",
|
|
@@ -161,13 +138,15 @@ export function createRecordContractTerminationHandler(options, consumerProtecti
|
|
|
161
138
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
162
139
|
const payload = event.payload;
|
|
163
140
|
const tenantId = event.user.tenantId;
|
|
141
|
+
// The provider cancel/resume and the stream append must not interleave across declarations of the same subscription.
|
|
142
|
+
await ctx.db.advisoryXactLock(TERMINATION_PROVIDER_LOCK_NAMESPACE, TERMINATION_PROVIDER_LOCK_KEY);
|
|
164
143
|
if (payload.channel === "public" && payload.when === SubscriptionCancelTimings.immediately) {
|
|
165
144
|
throw new UnprocessableError("immediate_cancel_not_allowed", {
|
|
166
145
|
message: "billing-foundation: the public channel never cancels at the provider immediately",
|
|
167
146
|
});
|
|
168
147
|
}
|
|
169
148
|
const receivedAtIso = payload.receivedAtIso ?? options.now().toString();
|
|
170
|
-
const stream = await ctx.fetchForWriting({
|
|
149
|
+
const stream = await ctx.events.fetchForWriting({
|
|
171
150
|
aggregateId: subscriptionAggregateId(tenantId),
|
|
172
151
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
173
152
|
});
|
|
@@ -193,28 +172,12 @@ export function createRecordContractTerminationHandler(options, consumerProtecti
|
|
|
193
172
|
effectiveAtIso,
|
|
194
173
|
providerCancel,
|
|
195
174
|
};
|
|
196
|
-
// Re-fetch after the external call:
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
const streamAfterProviderCall = await ctx.fetchForWriting({
|
|
175
|
+
// Re-fetch after the external call: webhooks append to this stream without the lock, so a
|
|
176
|
+
// stale expectedVersion would fail the append although the provider already cancelled.
|
|
177
|
+
const streamAfterProviderCall = await ctx.events.fetchForWriting({
|
|
200
178
|
aggregateId: subscriptionAggregateId(tenantId),
|
|
201
179
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
202
180
|
});
|
|
203
|
-
// A concurrent run with the same requestId may have recorded while we were
|
|
204
|
-
// at the provider; a second append would duplicate the event and the notice.
|
|
205
|
-
const recordedMeanwhile = findRecordedTermination(streamAfterProviderCall.events, payload.requestId);
|
|
206
|
-
if (recordedMeanwhile)
|
|
207
|
-
return recordedTerminationResult(recordedMeanwhile);
|
|
208
|
-
// A revocation that landed during the provider call found no cancel to take back.
|
|
209
|
-
if (hasRevocation(streamAfterProviderCall.events, payload.requestId)) {
|
|
210
|
-
await takeBackLateRevokedCancel(ctx, consumerProtection, {
|
|
211
|
-
payload,
|
|
212
|
-
tenantId,
|
|
213
|
-
receivedAtIso,
|
|
214
|
-
providerCancel,
|
|
215
|
-
});
|
|
216
|
-
return revokedRecordResult(receivedAtIso);
|
|
217
|
-
}
|
|
218
181
|
await streamAfterProviderCall.appendOne({
|
|
219
182
|
type: CONTRACT_TERMINATION_REQUESTED_EVENT_QN,
|
|
220
183
|
payload: recorded,
|
|
@@ -285,7 +248,7 @@ export async function resumeAtProvider(ctx, subscription) {
|
|
|
285
248
|
});
|
|
286
249
|
}
|
|
287
250
|
catch (error) {
|
|
288
|
-
ctx.log?.warn(`[billing-foundation:revoke-contract-termination] provider resume failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
251
|
+
ctx.obs.log?.warn(`[billing-foundation:revoke-contract-termination] provider resume failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
289
252
|
return ["revocation_resume_failed"];
|
|
290
253
|
}
|
|
291
254
|
return [];
|
|
@@ -307,7 +270,7 @@ export function createRecordUnmatchedContractTerminationHandler() {
|
|
|
307
270
|
handler: async (event, ctx) => {
|
|
308
271
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
309
272
|
const payload = event.payload;
|
|
310
|
-
const stream = await ctx.fetchForWriting({
|
|
273
|
+
const stream = await ctx.events.fetchForWriting({
|
|
311
274
|
aggregateId: terminationUnmatchedAggregateId(payload.requestId),
|
|
312
275
|
aggregateType: CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE,
|
|
313
276
|
});
|
|
@@ -95,10 +95,12 @@ async function findTerminableTenantIds(ctx, options, requestTenantId, email) {
|
|
|
95
95
|
declareEscapeHatch({
|
|
96
96
|
reason: "anonymous declarant has no session — finds the contract by email via user:find-for-auth and tenant:memberships as a system user; the calling handler declares its own escapeHatch",
|
|
97
97
|
});
|
|
98
|
-
const user = authUserRowSchema.safeParse(await ctx.queryAs(createSystemUser(requestTenantId), UserQueries.findForAuth, {
|
|
98
|
+
const user = authUserRowSchema.safeParse(await ctx.dispatch.queryAs(createSystemUser(requestTenantId), UserQueries.findForAuth, {
|
|
99
|
+
email,
|
|
100
|
+
}));
|
|
99
101
|
if (!user.success || user.data.isDeleted === true)
|
|
100
102
|
return [];
|
|
101
|
-
const memberships = membershipRowsSchema.safeParse(await ctx.queryAs(createSystemUser(requestTenantId), TenantQueries.memberships, {
|
|
103
|
+
const memberships = membershipRowsSchema.safeParse(await ctx.dispatch.queryAs(createSystemUser(requestTenantId), TenantQueries.memberships, {
|
|
102
104
|
userId: user.data.id,
|
|
103
105
|
}));
|
|
104
106
|
if (!memberships.success)
|
|
@@ -111,7 +113,7 @@ async function findTerminableTenantIds(ctx, options, requestTenantId, email) {
|
|
|
111
113
|
];
|
|
112
114
|
const terminable = [];
|
|
113
115
|
for (const tenantId of adminTenantIds) {
|
|
114
|
-
const probe = terminableResultSchema.safeParse(await ctx.queryAs(createSystemUser(tenantId), SubscriptionFoundationQueries.terminableSubscription, {}));
|
|
116
|
+
const probe = terminableResultSchema.safeParse(await ctx.dispatch.queryAs(createSystemUser(tenantId), SubscriptionFoundationQueries.terminableSubscription, {}));
|
|
115
117
|
if (probe.success && probe.data.terminable)
|
|
116
118
|
terminable.push(tenantId);
|
|
117
119
|
}
|
|
@@ -126,7 +128,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
|
|
|
126
128
|
rateLimit: { per: "ip+handler", limit: 5, windowSeconds: 600 },
|
|
127
129
|
...(consumerProtection.terminationScope === "platform" && { tenantlessAnonymous: true }),
|
|
128
130
|
escapeHatch: {
|
|
129
|
-
reason: "Anonymous declarant has no session — finds the contract by email via ctx.queryAs(system, user:find-for-auth) and tenant:memberships; the tenant written to comes only from those lookups.",
|
|
131
|
+
reason: "Anonymous declarant has no session — finds the contract by email via ctx.dispatch.queryAs(system, user:find-for-auth) and tenant:memberships; the tenant written to comes only from those lookups.",
|
|
130
132
|
},
|
|
131
133
|
handler: async (event, ctx) => {
|
|
132
134
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
@@ -143,7 +145,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
|
|
|
143
145
|
if (candidates.length === 1 && onlyCandidate !== undefined) {
|
|
144
146
|
matchedTenantId = onlyCandidate;
|
|
145
147
|
const revocation = createRevocationToken();
|
|
146
|
-
const declared = await ctx.writeAs(createSystemUser(onlyCandidate), SubscriptionFoundationHandlers.declareContractTermination, {
|
|
148
|
+
const declared = await ctx.dispatch.writeAs(createSystemUser(onlyCandidate), SubscriptionFoundationHandlers.declareContractTermination, {
|
|
147
149
|
requestId,
|
|
148
150
|
declarationType,
|
|
149
151
|
terminationKind,
|
|
@@ -166,7 +168,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
|
|
|
166
168
|
}
|
|
167
169
|
else {
|
|
168
170
|
reasons.push(candidates.length === 0 ? "unmatched" : "ambiguous");
|
|
169
|
-
const recorded = await ctx.writeAs(createSystemUser(SYSTEM_TENANT_ID), SubscriptionFoundationHandlers.recordUnmatchedContractTermination, {
|
|
171
|
+
const recorded = await ctx.dispatch.writeAs(createSystemUser(SYSTEM_TENANT_ID), SubscriptionFoundationHandlers.recordUnmatchedContractTermination, {
|
|
170
172
|
requestId,
|
|
171
173
|
declarationType,
|
|
172
174
|
terminationKind,
|
|
@@ -218,7 +220,7 @@ export function createTerminateContractHandler(options, consumerProtection) {
|
|
|
218
220
|
const tenantId = event.user.tenantId;
|
|
219
221
|
const requestId = generateId();
|
|
220
222
|
const { declarationType, terminationKind } = payload;
|
|
221
|
-
const caller = authUserRowSchema.safeParse(await ctx.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
|
|
223
|
+
const caller = authUserRowSchema.safeParse(await ctx.dispatch.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
|
|
222
224
|
id: event.user.id,
|
|
223
225
|
}));
|
|
224
226
|
const callerEmail = caller.success && caller.data.email && caller.data.isDeleted !== true
|
|
@@ -229,7 +231,7 @@ export function createTerminateContractHandler(options, consumerProtection) {
|
|
|
229
231
|
message: "billing-foundation:terminate-contract: calling user has no email on record",
|
|
230
232
|
});
|
|
231
233
|
}
|
|
232
|
-
const recorded = await ctx.writeAs(createSystemUser(tenantId), SubscriptionFoundationHandlers.recordContractTermination, {
|
|
234
|
+
const recorded = await ctx.dispatch.writeAs(createSystemUser(tenantId), SubscriptionFoundationHandlers.recordContractTermination, {
|
|
233
235
|
requestId,
|
|
234
236
|
declarationType,
|
|
235
237
|
terminationKind,
|