@cosmicdrift/kumiko-samples 0.389.0 → 0.390.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +27 -15
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/auth-user-row.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +13 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
- package/packages/bundled-features/src/auth-email-password/feature.ts +2 -2
- package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +7 -7
- package/packages/bundled-features/src/auth-email-password/handlers/confirm-account-unlock.write.ts +3 -3
- package/packages/bundled-features/src/auth-email-password/handlers/confirm-token-flow.ts +5 -5
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +9 -7
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +11 -7
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -4
- package/packages/bundled-features/src/auth-email-password/handlers/invite-info.query.ts +2 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +9 -7
- package/packages/bundled-features/src/auth-email-password/handlers/login.write.ts +23 -16
- package/packages/bundled-features/src/auth-email-password/handlers/reset-password.write.ts +3 -2
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +12 -12
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +12 -8
- package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +4 -2
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +2 -2
- package/packages/bundled-features/src/auth-email-password/handlers/verify-email.write.ts +3 -2
- package/packages/bundled-features/src/auth-email-password/seeding.ts +14 -9
- package/packages/bundled-features/src/auth-foundation/feature.ts +13 -4
- package/packages/bundled-features/src/auth-mfa/handlers/disable.write.ts +5 -5
- package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +24 -14
- package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm.write.ts +9 -3
- package/packages/bundled-features/src/auth-mfa/handlers/enable-start-preauth.write.ts +6 -1
- package/packages/bundled-features/src/auth-mfa/handlers/enable-start.write.ts +3 -1
- package/packages/bundled-features/src/auth-mfa/handlers/reencrypt.job.ts +14 -9
- package/packages/bundled-features/src/auth-mfa/handlers/regenerate-recovery.write.ts +5 -4
- package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +25 -14
- package/packages/bundled-features/src/auth-mfa/mfa-code-verifier.ts +10 -4
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +9 -3
- package/packages/bundled-features/src/auth-mfa/verify-factor.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/billing-info-query.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/changes.json +6 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +5 -5
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +10 -7
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +15 -12
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-open-cancels.ts +22 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-provider-lock.ts +4 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +4 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +17 -58
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +10 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +18 -15
- package/packages/bundled-features/src/billing-foundation/events.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/feature.ts +8 -3
- package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +5 -5
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +3 -3
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/projection.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/types.ts +1 -1
- package/packages/bundled-features/src/cap-counter/aggregate-id.ts +1 -1
- package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +5 -4
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +1 -1
- package/packages/bundled-features/src/cap-counter/constants.ts +1 -1
- package/packages/bundled-features/src/cap-counter/feature.ts +2 -2
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +1 -1
- package/packages/bundled-features/src/cap-counter/reservation-chain.ts +1 -1
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +4 -4
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +1 -1
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +1 -1
- package/packages/bundled-features/src/compliance-profiles/seeding.ts +12 -7
- package/packages/bundled-features/src/config/feature.ts +6 -6
- package/packages/bundled-features/src/config/handlers/cascade.query.ts +1 -1
- package/packages/bundled-features/src/config/handlers/readiness.query.ts +1 -1
- package/packages/bundled-features/src/config/handlers/reencrypt.job.ts +23 -13
- package/packages/bundled-features/src/config/handlers/reset.write.ts +3 -3
- package/packages/bundled-features/src/config/handlers/selected-extensions.query.ts +1 -1
- package/packages/bundled-features/src/config/handlers/set.write.ts +4 -4
- package/packages/bundled-features/src/config/handlers/values.query.ts +1 -1
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +8 -8
- package/packages/bundled-features/src/crypto-shredding/resolve-file-bytes-deleter.ts +3 -3
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +1 -1
- package/packages/bundled-features/src/custom-fields/handlers/delete-system-field.write.ts +1 -1
- package/packages/bundled-features/src/custom-fields/handlers/delete-tenant-field.write.ts +1 -1
- package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +1 -1
- package/packages/bundled-features/src/custom-fields/index.ts +4 -0
- package/packages/bundled-features/src/data-retention/feature.ts +1 -1
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +12 -2
- package/packages/bundled-features/src/delivery/attempt-log.ts +1 -1
- package/packages/bundled-features/src/delivery/delivery-service.ts +6 -5
- package/packages/bundled-features/src/delivery/feature.ts +2 -2
- package/packages/bundled-features/src/delivery/jobs.ts +11 -11
- package/packages/bundled-features/src/delivery/testing.ts +1 -1
- package/packages/bundled-features/src/document-ingest-foundation/events.ts +1 -1
- package/packages/bundled-features/src/feature-toggles/handlers/registered.query.ts +1 -1
- package/packages/bundled-features/src/feature-toggles/handlers/set.write.ts +1 -1
- package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +1 -1
- package/packages/bundled-features/src/file-derivatives/feature.ts +2 -2
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +5 -1
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +7 -5
- package/packages/bundled-features/src/file-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -5
- package/packages/bundled-features/src/file-provider-s3/feature.ts +4 -1
- package/packages/bundled-features/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +8 -8
- package/packages/bundled-features/src/form-draft/feature.ts +1 -1
- package/packages/bundled-features/src/form-draft/handlers/cleanup.job.ts +22 -12
- package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +3 -3
- package/packages/bundled-features/src/form-draft/handlers/get.query.ts +1 -1
- package/packages/bundled-features/src/foundation-shared/config-helpers.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/events.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-state.ts +2 -2
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +10 -10
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +1 -1
- package/packages/bundled-features/src/inbound-provider-imap/changes.json +6 -0
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +4 -1
- package/packages/bundled-features/src/jobs/changes.json +6 -0
- package/packages/bundled-features/src/jobs/events.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/consumer.write.ts +7 -4
- package/packages/bundled-features/src/jobs/handlers/projection-rebuild.job.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/reindex-entity.job.ts +11 -5
- package/packages/bundled-features/src/jobs/handlers/retention-cleanup.job.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/retry.write.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/trigger.write.ts +2 -2
- package/packages/bundled-features/src/jobs/job-run-logger.ts +139 -90
- package/packages/bundled-features/src/legal-pages/README.md +1 -1
- package/packages/bundled-features/src/legal-pages/lib/boot-check.ts +12 -8
- package/packages/bundled-features/src/mail-foundation/feature.ts +2 -2
- package/packages/bundled-features/src/mail-transport-smtp/changes.json +6 -0
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +4 -1
- package/packages/bundled-features/src/managed-pages/seeding.ts +13 -8
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +2 -2
- package/packages/bundled-features/src/personal-access-tokens/handlers/revoke.write.ts +1 -1
- package/packages/bundled-features/src/rate-limiting/handlers/status.query.ts +2 -2
- package/packages/bundled-features/src/readiness/handlers/status.query.ts +4 -1
- package/packages/bundled-features/src/renderer-foundation/api.ts +3 -3
- package/packages/bundled-features/src/secrets/announce-secret-change.ts +4 -4
- package/packages/bundled-features/src/secrets/feature.ts +9 -9
- package/packages/bundled-features/src/secrets/handlers/delete.write.ts +4 -3
- package/packages/bundled-features/src/secrets/handlers/rotate.job.ts +13 -8
- package/packages/bundled-features/src/secrets/handlers/set.write.ts +4 -3
- package/packages/bundled-features/src/secrets/secrets-context.ts +16 -6
- package/packages/bundled-features/src/secrets/write-gate.ts +1 -1
- package/packages/bundled-features/src/seo/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/changes.json +12 -0
- package/packages/bundled-features/src/sessions/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/cleanup.job.ts +2 -2
- package/packages/bundled-features/src/sessions/handlers/detail.query.ts +5 -2
- package/packages/bundled-features/src/sessions/handlers/list.query.ts +5 -2
- package/packages/bundled-features/src/sessions/handlers/mine.query.ts +5 -2
- package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +3 -3
- package/packages/bundled-features/src/sessions/handlers/revoke-all-others.write.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke.write.ts +1 -1
- package/packages/bundled-features/src/sessions/screens.ts +8 -4
- package/packages/bundled-features/src/sessions/session-revoked-event.ts +1 -1
- package/packages/bundled-features/src/sessions/web/session-device-cell.tsx +10 -12
- package/packages/bundled-features/src/sessions/web/user-agent.ts +12 -0
- package/packages/bundled-features/src/shared/index.ts +15 -0
- package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +12 -2
- package/packages/bundled-features/src/shared/require-for-tenant.ts +7 -5
- package/packages/bundled-features/src/shared/stream-tenant-backfill.ts +201 -0
- package/packages/bundled-features/src/shared/stream-tenant-derived-state.ts +107 -0
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -1
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +1 -1
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +7 -7
- package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +5 -3
- package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -7
- package/packages/bundled-features/src/template-resolver/api.ts +3 -3
- package/packages/bundled-features/src/template-resolver/changes.json +18 -0
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +5 -4
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/send-test-shared.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +6 -2
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +4 -2
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/seeding.ts +12 -2
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +18 -13
- package/packages/bundled-features/src/template-resolver/template-resources.ts +4 -4
- package/packages/bundled-features/src/template-resolver/testing.ts +2 -2
- package/packages/bundled-features/src/template-resolver/web/document-insert-fields.tsx +222 -0
- package/packages/bundled-features/src/template-resolver/web/document-insert-model.ts +164 -0
- package/packages/bundled-features/src/template-resolver/web/document-insert-panel.tsx +115 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-bubble.tsx +177 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-content.tsx +38 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-context.ts +119 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +51 -1098
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-helpers.ts +161 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-layout.tsx +83 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-toolbar-groups.tsx +153 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-toolbar.tsx +182 -0
- package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +18 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +19 -6
- package/packages/bundled-features/src/template-resolver/web/use-document-tiptap-model.ts +138 -0
- package/packages/bundled-features/src/tenant/changes.json +19 -0
- package/packages/bundled-features/src/tenant/handlers/cancel-invitation.write.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/memberships.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/remove-member.write.ts +2 -2
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +1 -1
- package/packages/bundled-features/src/tenant/index.ts +5 -0
- package/packages/bundled-features/src/tenant/invited-membership.ts +6 -1
- package/packages/bundled-features/src/tenant/schema/tenant.ts +4 -1
- package/packages/bundled-features/src/tenant/seeding.ts +65 -62
- package/packages/bundled-features/src/tenant/stream-tenant-backfill.ts +115 -0
- package/packages/bundled-features/src/tenant/testing.ts +77 -0
- package/packages/bundled-features/src/tenant-handover/events.ts +1 -1
- package/packages/bundled-features/src/tenant-handover/handlers/claim.write.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/feature.ts +5 -5
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +22 -10
- package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +22 -11
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +23 -12
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +7 -3
- package/packages/bundled-features/src/tenant-lifecycle/testing.ts +9 -8
- package/packages/bundled-features/src/tier-engine/entity.ts +1 -1
- package/packages/bundled-features/src/tier-engine/feature.ts +8 -3
- package/packages/bundled-features/src/tier-engine/handlers/active-tier.query.ts +3 -3
- package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +7 -1
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +3 -3
- package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +71 -97
- package/packages/bundled-features/src/user/handlers/find-for-auth.query.ts +2 -2
- package/packages/bundled-features/src/user/handlers/update-roles.ts +2 -2
- package/packages/bundled-features/src/user/index.ts +2 -0
- package/packages/bundled-features/src/user/invited-global-roles.ts +6 -1
- package/packages/bundled-features/src/user/principal-status.ts +1 -1
- package/packages/bundled-features/src/user/seeding.ts +24 -6
- package/packages/bundled-features/src/user/testing.ts +22 -0
- package/packages/bundled-features/src/user-data-rights/COMPLIANCE.md +1 -1
- package/packages/bundled-features/src/user-data-rights/README.md +1 -1
- package/packages/bundled-features/src/user-data-rights/audit-download.ts +12 -2
- package/packages/bundled-features/src/user-data-rights/changes.json +24 -0
- package/packages/bundled-features/src/user-data-rights/feature.ts +29 -24
- package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/deletion-grace-period.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/restrict-account.write.ts +4 -4
- package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +12 -6
- package/packages/bundled-features/src/user-data-rights/index.ts +2 -0
- package/packages/bundled-features/src/user-data-rights/lib/mail-transport-resolver.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/lib/storage-provider-resolver.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +6 -1
- package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +8 -2
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +14 -3
- package/packages/bundled-features/src/user-data-rights/run-user-export.ts +11 -1
- package/packages/bundled-features/src/user-data-rights/web/constants.ts +10 -0
- package/packages/bundled-features/src/user-data-rights/web/index.ts +6 -1
- package/packages/bundled-features/src/user-profile/handlers/change-email.write.ts +8 -6
- package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +1 -1
- package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +2 -2
- package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +1 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +9 -4
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +6 -1
- package/packages/bundled-features/src/waitlist/link-on-signup.ts +7 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
- package/packages/bundled-features/src/workflow-runner/db/queries/due-runs.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +4 -3
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
- package/packages/bundled-features/src/workflow-runner/feature.ts +2 -2
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +8 -8
- package/packages/bundled-features/src/workflow-runner/runner.ts +6 -4
- package/samples/apps/cap-billing-demo/package.json +1 -1
- package/samples/apps/cap-billing-demo/src/feature.ts +19 -5
- package/samples/apps/config-demo/src/app/server.ts +1 -1
- package/samples/apps/styleguide/src/app/server.ts +2 -2
- package/samples/apps/ui-walkthrough/package.json +1 -1
- package/samples/apps/use-all-bundled/feature-manifest.json +1 -1
- package/samples/apps/use-all-bundled/package.json +1 -1
- package/samples/apps/use-all-bundled/src/app/seed.ts +12 -2
- package/samples/apps/user-data-rights-demo/README.md +1 -1
- package/samples/apps/user-data-rights-demo/package.json +1 -1
- package/samples/apps/user-data-rights-demo/src/feature.ts +3 -2
- package/samples/recipes/api-worker-split/README.md +2 -1
- package/samples/recipes/api-worker-split/src/feature.ts +2 -1
- package/samples/recipes/cross-feature-events/README.md +5 -5
- package/samples/recipes/cross-feature-events/package.json +1 -1
- package/samples/recipes/cross-feature-events/src/feature.ts +4 -4
- package/samples/recipes/event-sourcing/README.md +5 -5
- package/samples/recipes/event-sourcing/package.json +1 -1
- package/samples/recipes/event-sourcing/src/feature.ts +15 -15
- package/samples/recipes/files-post-processing/README.md +1 -1
- package/samples/recipes/files-post-processing/package.json +1 -1
- package/samples/recipes/files-post-processing/src/feature.ts +6 -4
- package/samples/recipes/lane-routing/package.json +1 -1
- package/samples/recipes/legal-pages/README.md +2 -2
- package/samples/recipes/lifecycle-hooks/README.md +2 -2
- package/samples/recipes/lifecycle-hooks/src/feature.ts +1 -1
- package/samples/recipes/managed-config/README.md +3 -3
- package/samples/recipes/search/README.md +1 -1
- package/samples/recipes/search/src/feature.ts +3 -3
- package/samples/recipes/secrets-demo/README.md +1 -1
- package/samples/recipes/secrets-demo/src/feature.ts +5 -2
- package/samples/recipes/state-machine/src/events.ts +1 -1
- package/samples/recipes/state-machine/src/reducer.ts +1 -1
- package/samples/recipes/workflow-engine/src/resume-loop.ts +4 -4
- package/samples/recipes/workflow-engine/src/workflow-runner.ts +2 -2
|
@@ -50,8 +50,8 @@ export function createMfaVerifyHandler(opts: MfaVerifyOptions) {
|
|
|
50
50
|
escapeHatch: {
|
|
51
51
|
grants: ["systemIdentity", "unsafeRaw"],
|
|
52
52
|
reason:
|
|
53
|
-
"Pre-auth MFA step has no session yet — re-derives it via ctx.queryAs(SYSTEM, " +
|
|
54
|
-
"user:findForAuth) and ctx.resolveActiveMembership for the user the challenge token names. " +
|
|
53
|
+
"Pre-auth MFA step has no session yet — re-derives it via ctx.dispatch.queryAs(SYSTEM, " +
|
|
54
|
+
"user:findForAuth) and ctx.auth.resolveActiveMembership for the user the challenge token names. " +
|
|
55
55
|
"Also reads the MFA enrollment of the tenant named in the signed login/setup token, not the " +
|
|
56
56
|
"guest dispatch tenant.",
|
|
57
57
|
},
|
|
@@ -71,14 +71,15 @@ export function createMfaVerifyHandler(opts: MfaVerifyOptions) {
|
|
|
71
71
|
// this route sits behind a challenge token from a successful login,
|
|
72
72
|
// but its own secret is still just a short TOTP/recovery code —
|
|
73
73
|
// silently skipping the brute-force cap and single-use burn when
|
|
74
|
-
// ctx.redis is absent would let a stolen challenge token be guessed
|
|
74
|
+
// ctx.services.redis is absent would let a stolen challenge token be guessed
|
|
75
75
|
// against unboundedly, and let a recovery code be replayed via a
|
|
76
76
|
// reissued challenge (mfa-verify-attempts.ts's counter is deliberately
|
|
77
77
|
// NOT reset by reissuance, precisely to survive this).
|
|
78
|
-
if (!ctx.redis) {
|
|
78
|
+
if (!ctx.services.redis) {
|
|
79
79
|
return writeFailure(
|
|
80
80
|
new InternalError({
|
|
81
|
-
message:
|
|
81
|
+
message:
|
|
82
|
+
"verify (mfa) requires ctx.services.redis for the brute-force cap and single-use burn",
|
|
82
83
|
}),
|
|
83
84
|
);
|
|
84
85
|
}
|
|
@@ -86,7 +87,7 @@ export function createMfaVerifyHandler(opts: MfaVerifyOptions) {
|
|
|
86
87
|
// Per-account brute-force cap — survives challenge-token reissuance
|
|
87
88
|
// on purpose (see mfa-verify-attempts.ts header). Checked BEFORE the
|
|
88
89
|
// TOTP/recovery verify so a locked account can't be guessed against.
|
|
89
|
-
const state = await getMfaVerifyLockoutState(ctx.redis, userId);
|
|
90
|
+
const state = await getMfaVerifyLockoutState(ctx.services.redis, userId);
|
|
90
91
|
if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
|
|
91
92
|
const now = Date.now();
|
|
92
93
|
if (state.lockedUntil > now) {
|
|
@@ -98,7 +99,12 @@ export function createMfaVerifyHandler(opts: MfaVerifyOptions) {
|
|
|
98
99
|
// "system" mode: this handler runs with a guest identity whose own
|
|
99
100
|
// tenantId is meaningless here — the challenge token is the source
|
|
100
101
|
// of truth for which tenant's row to read.
|
|
101
|
-
const scopedDb = createTenantDb({
|
|
102
|
+
const scopedDb = createTenantDb({
|
|
103
|
+
db: ctx.db.unsafeRaw(),
|
|
104
|
+
tenantId,
|
|
105
|
+
mode: "system",
|
|
106
|
+
reason: "MFA challenge has no session, the tenant comes from the verified challenge token",
|
|
107
|
+
});
|
|
102
108
|
const scopedUser: SessionUser = { id: userId, tenantId, roles: ["User"] };
|
|
103
109
|
const row = await findUserMfaRow(scopedDb, scopedUser);
|
|
104
110
|
// MFA got disabled between login and verify (race, or a stale
|
|
@@ -106,10 +112,10 @@ export function createMfaVerifyHandler(opts: MfaVerifyOptions) {
|
|
|
106
112
|
// bad code, no detail leak about account state.
|
|
107
113
|
if (!row) return invalidChallengeToken();
|
|
108
114
|
|
|
109
|
-
const replay = { redis: ctx.redis, userId };
|
|
115
|
+
const replay = { redis: ctx.services.redis, userId };
|
|
110
116
|
const verify = await verifyMfaFactor(row, event.payload.code, replay);
|
|
111
117
|
if (!verify.ok) {
|
|
112
|
-
await recordFailedMfaVerifyAttempt(ctx.redis, userId, maxAttempts, lockoutMinutes);
|
|
118
|
+
await recordFailedMfaVerifyAttempt(ctx.services.redis, userId, maxAttempts, lockoutMinutes);
|
|
113
119
|
return invalidTotpCode();
|
|
114
120
|
}
|
|
115
121
|
|
|
@@ -121,7 +127,12 @@ export function createMfaVerifyHandler(opts: MfaVerifyOptions) {
|
|
|
121
127
|
// is reached, so this burn only needs to guard a DIFFERENT code
|
|
122
128
|
// (recovery, or a still-valid-but-not-yet-burned TOTP step) reused
|
|
123
129
|
// against the same challenge token.
|
|
124
|
-
const burnResult = await burnToken(
|
|
130
|
+
const burnResult = await burnToken(
|
|
131
|
+
ctx.services.redis,
|
|
132
|
+
"mfa-challenge",
|
|
133
|
+
userId,
|
|
134
|
+
verified.expiresAtMs,
|
|
135
|
+
);
|
|
125
136
|
if (burnResult === "already-used") return invalidChallengeToken();
|
|
126
137
|
|
|
127
138
|
// Recovery-code use consumes the code — persist the reduced hash-list
|
|
@@ -140,13 +151,13 @@ export function createMfaVerifyHandler(opts: MfaVerifyOptions) {
|
|
|
140
151
|
if (!updateResult.isSuccess) return updateResult;
|
|
141
152
|
}
|
|
142
153
|
|
|
143
|
-
await clearMfaVerifyAttempts(ctx.redis, userId);
|
|
154
|
+
await clearMfaVerifyAttempts(ctx.services.redis, userId);
|
|
144
155
|
|
|
145
156
|
// Re-derive the full session the way login.write.ts would have, for
|
|
146
157
|
// the SAME tenant the challenge token already committed to (no
|
|
147
158
|
// "pick a tenant" step here — that already happened at login time).
|
|
148
159
|
const systemUser = createSystemUser(tenantId, ["SystemAdmin"]);
|
|
149
|
-
const userRow = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
160
|
+
const userRow = (await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
|
|
150
161
|
id: userId,
|
|
151
162
|
})) as {
|
|
152
163
|
roles?: string | null;
|
|
@@ -174,7 +185,7 @@ export function createMfaVerifyHandler(opts: MfaVerifyOptions) {
|
|
|
174
185
|
|
|
175
186
|
// Membership revoked or principal blocked between login and verify —
|
|
176
187
|
// mirror login.write.ts's refusal instead of falling back to globalRoles only.
|
|
177
|
-
const active = await ctx.resolveActiveMembership(userId, tenantId);
|
|
188
|
+
const active = await ctx.auth.resolveActiveMembership(userId, tenantId);
|
|
178
189
|
if (active.kind === "rejected") return invalidChallengeToken();
|
|
179
190
|
// buildSessionRoles calls stripForbiddenMembershipRoles to strip reserved
|
|
180
191
|
// roles from the membership portion (globalRoles keeps SystemAdmin) —
|
|
@@ -188,7 +199,7 @@ export function createMfaVerifyHandler(opts: MfaVerifyOptions) {
|
|
|
188
199
|
...sessionTimezoneField(userRow?.timezone),
|
|
189
200
|
...sessionLocaleField(userRow?.locale),
|
|
190
201
|
};
|
|
191
|
-
const claims = await ctx.
|
|
202
|
+
const claims = await ctx.auth.resolveClaims(baseSession);
|
|
192
203
|
const session: SessionUser =
|
|
193
204
|
Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
|
|
194
205
|
|
|
@@ -29,13 +29,19 @@ export function createMfaCodeVerifier(): MfaCodeVerifier {
|
|
|
29
29
|
reason:
|
|
30
30
|
"reads the MFA enrollment of the user being re-authenticated on the caller's handler context; the calling handler declares its own escapeHatch",
|
|
31
31
|
});
|
|
32
|
-
const scopedDb = createTenantDb({
|
|
32
|
+
const scopedDb = createTenantDb({
|
|
33
|
+
db: ctx.db.unsafeRaw(),
|
|
34
|
+
tenantId,
|
|
35
|
+
mode: "system",
|
|
36
|
+
reason:
|
|
37
|
+
"re-authentication reads the MFA enrollment of the user's own tenant, not the handler's dispatch tenant",
|
|
38
|
+
});
|
|
33
39
|
const row = await findUserMfaRow(scopedDb, { id: userId, tenantId, roles: [] });
|
|
34
40
|
if (!row) return { enrolled: false, ok: false };
|
|
35
|
-
// Fail closed without ctx.redis, matching disable.write.ts/verify.write.ts —
|
|
41
|
+
// Fail closed without ctx.services.redis, matching disable.write.ts/verify.write.ts —
|
|
36
42
|
// skipping the TOTP-replay burn would let an observed code be replayed.
|
|
37
|
-
if (!code || !ctx.redis) return { enrolled: true, ok: false };
|
|
38
|
-
const verify = await verifyMfaFactor(row, code, { redis: ctx.redis, userId });
|
|
43
|
+
if (!code || !ctx.services.redis) return { enrolled: true, ok: false };
|
|
44
|
+
const verify = await verifyMfaFactor(row, code, { redis: ctx.services.redis, userId });
|
|
39
45
|
return { enrolled: true, ok: verify.ok && verify.method === "totp" };
|
|
40
46
|
};
|
|
41
47
|
}
|
|
@@ -48,7 +48,13 @@ export function createMfaStatusChecker(opts: {
|
|
|
48
48
|
reason:
|
|
49
49
|
"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",
|
|
50
50
|
});
|
|
51
|
-
const scopedDb = createTenantDb({
|
|
51
|
+
const scopedDb = createTenantDb({
|
|
52
|
+
db: ctx.db.unsafeRaw(),
|
|
53
|
+
tenantId,
|
|
54
|
+
mode: "system",
|
|
55
|
+
reason:
|
|
56
|
+
"login/setup token names the tenant to check, the guest dispatch tenant is meaningless",
|
|
57
|
+
});
|
|
52
58
|
const scopedUser: SessionUser = { id: userId, tenantId, roles: ["User"] };
|
|
53
59
|
const row = await findUserMfaRow(scopedDb, scopedUser);
|
|
54
60
|
|
|
@@ -65,12 +71,12 @@ export function createMfaStatusChecker(opts: {
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
// ctx.config is bound to the CALLING user's tenant (the anonymous
|
|
68
|
-
// identity's tenant for login) — wrong tenant here. ctx.configResolver takes tenantId
|
|
74
|
+
// identity's tenant for login) — wrong tenant here. ctx.services.configResolver takes tenantId
|
|
69
75
|
// explicitly, which is what a pre-session check against the LOGGING-IN
|
|
70
76
|
// user's tenant needs.
|
|
71
77
|
// @cast-boundary engine-payload — "select" config values are validated
|
|
72
78
|
// against the declared `options` at write-time by the config feature.
|
|
73
|
-
const policy = ((await ctx.configResolver?.get(
|
|
79
|
+
const policy = ((await ctx.services.configResolver?.get(
|
|
74
80
|
mfaRequiredConfigHandle.name,
|
|
75
81
|
mfaRequiredConfigKey(opts.requiredPolicy),
|
|
76
82
|
tenantId,
|
|
@@ -21,7 +21,7 @@ export type MfaFactorVerifyResult =
|
|
|
21
21
|
// must not verify again within its ±1-step window — otherwise a code an
|
|
22
22
|
// attacker observed once, via phishing proxy or shoulder-surfing, stays
|
|
23
23
|
// usable for ~90s of parallel logins). Omitted where a caller has no
|
|
24
|
-
// ctx.redis available; the code-verify itself still works, just without
|
|
24
|
+
// ctx.services.redis available; the code-verify itself still works, just without
|
|
25
25
|
// replay protection for that call.
|
|
26
26
|
export async function verifyMfaFactor(
|
|
27
27
|
row: UserMfaRow,
|
|
@@ -68,8 +68,8 @@ export function createBillingInfoQueryConfig<TTier extends string>(
|
|
|
68
68
|
const billingLive = ctx.config
|
|
69
69
|
? (await ctx.config(STRIPE_BILLING_LIVE_CONFIG_QN)) === true
|
|
70
70
|
: false;
|
|
71
|
-
const apiKeySet = ctx.secrets
|
|
72
|
-
? await ctx.secrets.has(SYSTEM_TENANT_ID, STRIPE_API_KEY_CONFIG_QN)
|
|
71
|
+
const apiKeySet = ctx.services.secrets
|
|
72
|
+
? await ctx.services.secrets.has(SYSTEM_TENANT_ID, STRIPE_API_KEY_CONFIG_QN)
|
|
73
73
|
: false;
|
|
74
74
|
|
|
75
75
|
const sub = await ctx.db.fetchOne<{
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.390.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Revoking one termination no longer resumes a subscription another termination still cancels",
|
|
6
|
+
"detail": "A revocation only resumes the subscription at the provider when no other recorded termination still cancels it. Recording and revoking a termination now run one at a time per tenant, so provider calls can no longer interleave with the event stream."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.388.0",
|
|
4
10
|
"type": "fix",
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts
CHANGED
|
@@ -172,7 +172,7 @@ export async function recordConsent(
|
|
|
172
172
|
};
|
|
173
173
|
const tenantId = ctx.user.tenantId;
|
|
174
174
|
const isSubscription = prepared.mode === "subscription";
|
|
175
|
-
await ctx.
|
|
175
|
+
await ctx.events.unsafeAppend({
|
|
176
176
|
aggregateId: isSubscription ? subscriptionAggregateId(tenantId) : paymentAggregateId(tenantId),
|
|
177
177
|
aggregateType: isSubscription ? SUBSCRIPTION_AGGREGATE_TYPE : PAYMENT_AGGREGATE_TYPE,
|
|
178
178
|
type: CHECKOUT_CONSENT_RECORDED_EVENT_QN,
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts
CHANGED
|
@@ -102,7 +102,7 @@ export function createIssueContractConfirmationHandler(
|
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
const stream = await ctx.fetchForWriting({
|
|
105
|
+
const stream = await ctx.events.fetchForWriting({
|
|
106
106
|
aggregateId: payload.sourceAggregateId,
|
|
107
107
|
aggregateType: isSubscription ? SUBSCRIPTION_AGGREGATE_TYPE : PAYMENT_AGGREGATE_TYPE,
|
|
108
108
|
});
|
|
@@ -119,7 +119,7 @@ export function createIssueContractConfirmationHandler(
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
if (!consentEvent || !consent) {
|
|
122
|
-
ctx.log?.warn(
|
|
122
|
+
ctx.obs.log?.warn(
|
|
123
123
|
`[billing-foundation:issue-contract-confirmation] no recorded consent ${payload.consentId} on the stream; no mail sent`,
|
|
124
124
|
);
|
|
125
125
|
return {
|
|
@@ -150,7 +150,7 @@ export function createIssueContractConfirmationHandler(
|
|
|
150
150
|
// Everything that can fail runs before the append, so a failed run
|
|
151
151
|
// leaves no marker behind and the job retry starts clean.
|
|
152
152
|
const recipient = recipientRowSchema.safeParse(
|
|
153
|
-
await ctx.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
|
|
153
|
+
await ctx.dispatch.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
|
|
154
154
|
id: consent.actorUserId,
|
|
155
155
|
}),
|
|
156
156
|
);
|
|
@@ -162,7 +162,7 @@ export function createIssueContractConfirmationHandler(
|
|
|
162
162
|
)
|
|
163
163
|
: null;
|
|
164
164
|
if (!recipient.success || recipientEmail === null) {
|
|
165
|
-
ctx.log?.warn(
|
|
165
|
+
ctx.obs.log?.warn(
|
|
166
166
|
`[billing-foundation:issue-contract-confirmation] recipient of consent ${payload.consentId} not found; no mail sent`,
|
|
167
167
|
);
|
|
168
168
|
return {
|
|
@@ -182,7 +182,7 @@ export function createIssueContractConfirmationHandler(
|
|
|
182
182
|
locale,
|
|
183
183
|
});
|
|
184
184
|
if (terms.version !== consent.termsTemplateVersion) {
|
|
185
|
-
ctx.log?.warn(
|
|
185
|
+
ctx.obs.log?.warn(
|
|
186
186
|
`[billing-foundation:issue-contract-confirmation] terms text block changed since consent ${payload.consentId} (version ${consent.termsTemplateVersion} -> ${terms.version}); the mail carries the current text`,
|
|
187
187
|
);
|
|
188
188
|
}
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts
CHANGED
|
@@ -76,13 +76,16 @@ export function registerConsumerProtection(
|
|
|
76
76
|
// user), never from payload data.
|
|
77
77
|
const tenantId = ctx.systemUser.tenantId;
|
|
78
78
|
const currentPeriodEndIso = payload["currentPeriodEndIso"];
|
|
79
|
-
const result = await ctx.write(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
:
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
const result = await ctx.dispatch.write(
|
|
80
|
+
SubscriptionFoundationHandlers.issueContractConfirmation,
|
|
81
|
+
{
|
|
82
|
+
consentId,
|
|
83
|
+
sourceAggregateId: isSubscriptionEvent
|
|
84
|
+
? subscriptionAggregateId(tenantId)
|
|
85
|
+
: paymentAggregateId(tenantId),
|
|
86
|
+
...(typeof currentPeriodEndIso === "string" && { currentPeriodEndIso }),
|
|
87
|
+
},
|
|
88
|
+
);
|
|
86
89
|
if (!result.isSuccess) {
|
|
87
90
|
throw new Error(
|
|
88
91
|
`billing-foundation:${trigger.jobName}: issue-contract-confirmation failed: ${JSON.stringify(result.error)}`,
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts
CHANGED
|
@@ -80,18 +80,21 @@ export function registerContractTermination(
|
|
|
80
80
|
const declared = contractTerminationDeclaredPayloadSchema.safeParse(payload);
|
|
81
81
|
// skip: trigger event carries no usable declaration
|
|
82
82
|
if (!declared.success) return;
|
|
83
|
-
const result = await ctx.write(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
83
|
+
const result = await ctx.dispatch.write(
|
|
84
|
+
SubscriptionFoundationHandlers.recordContractTermination,
|
|
85
|
+
{
|
|
86
|
+
requestId: declared.data.requestId,
|
|
87
|
+
declarationType: declared.data.declarationType,
|
|
88
|
+
terminationKind: declared.data.terminationKind,
|
|
89
|
+
channel: "public",
|
|
90
|
+
when:
|
|
91
|
+
declared.data.declarationType === "withdrawal"
|
|
92
|
+
? "none"
|
|
93
|
+
: SubscriptionCancelTimings.periodEnd,
|
|
94
|
+
receivedAtIso: declared.data.receivedAtIso,
|
|
95
|
+
locale: declared.data.locale,
|
|
96
|
+
},
|
|
97
|
+
);
|
|
95
98
|
if (!result.isSuccess) {
|
|
96
99
|
throw new Error(
|
|
97
100
|
`billing-foundation:cancel-on-public-termination-declared: record-contract-termination failed: ${JSON.stringify(result.error)}`,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONTRACT_TERMINATION_REQUESTED_EVENT_QN,
|
|
3
|
+
contractTerminationRequestedPayloadSchema,
|
|
4
|
+
} from "../events.ts";
|
|
5
|
+
import { hasRevocation } from "./termination-record.ts";
|
|
6
|
+
|
|
7
|
+
// A revoked declaration must not resume a subscription another declaration still cancels at the provider.
|
|
8
|
+
export function hasOtherOpenProviderCancel(
|
|
9
|
+
events: readonly { type: string; payload: unknown }[],
|
|
10
|
+
requestId: string,
|
|
11
|
+
): boolean {
|
|
12
|
+
return events.some((e) => {
|
|
13
|
+
if (e.type !== CONTRACT_TERMINATION_REQUESTED_EVENT_QN) return false;
|
|
14
|
+
const recorded = contractTerminationRequestedPayloadSchema.safeParse(e.payload).data;
|
|
15
|
+
return (
|
|
16
|
+
recorded !== undefined &&
|
|
17
|
+
recorded.requestId !== requestId &&
|
|
18
|
+
recorded.providerCancel !== "none" &&
|
|
19
|
+
!hasRevocation(events, recorded.requestId)
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -24,22 +24,22 @@ export async function mayMailReceipt(
|
|
|
24
24
|
email: string,
|
|
25
25
|
): Promise<ReceiptMailVerdict> {
|
|
26
26
|
// skip: no rate limiter wired, nothing to count against
|
|
27
|
-
if (!ctx.rateLimit) return "allowed";
|
|
27
|
+
if (!ctx.services.rateLimit) return "allowed";
|
|
28
28
|
try {
|
|
29
29
|
const bucketKey = receiptBucketKey(ctx, email);
|
|
30
30
|
if (bucketKey === undefined) {
|
|
31
|
-
ctx.log?.warn(
|
|
31
|
+
ctx.obs.log?.warn(
|
|
32
32
|
"[billing-foundation:termination] no rate-limit digest available, receipt mail withheld",
|
|
33
33
|
);
|
|
34
34
|
return "unavailable";
|
|
35
35
|
}
|
|
36
|
-
const decision = await ctx.rateLimit.check(bucketKey, {
|
|
36
|
+
const decision = await ctx.services.rateLimit.check(bucketKey, {
|
|
37
37
|
limit: RECEIPT_MAILS_PER_RECIPIENT,
|
|
38
38
|
windowSeconds: RECEIPT_WINDOW_SECONDS,
|
|
39
39
|
});
|
|
40
40
|
return decision.allowed ? "allowed" : "limited";
|
|
41
41
|
} catch (error) {
|
|
42
|
-
ctx.log?.warn(
|
|
42
|
+
ctx.obs.log?.warn(
|
|
43
43
|
`[billing-foundation:termination] receipt rate limiter failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
44
44
|
);
|
|
45
45
|
return "unavailable";
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts
CHANGED
|
@@ -39,6 +39,10 @@ import {
|
|
|
39
39
|
FALLBACK_CONSENT_LOCALE,
|
|
40
40
|
} from "../web/consumer-protection/consent-locale.ts";
|
|
41
41
|
import { notifyOperator } from "./termination-notify.ts";
|
|
42
|
+
import {
|
|
43
|
+
TERMINATION_PROVIDER_LOCK_KEY,
|
|
44
|
+
TERMINATION_PROVIDER_LOCK_NAMESPACE,
|
|
45
|
+
} from "./termination-provider-lock.ts";
|
|
42
46
|
import type { OperatorNoticeReason } from "./termination-texts.ts";
|
|
43
47
|
import { isWithinWithdrawalPeriod } from "./withdrawal-period.ts";
|
|
44
48
|
|
|
@@ -141,7 +145,7 @@ async function cancelAtProvider(
|
|
|
141
145
|
when,
|
|
142
146
|
});
|
|
143
147
|
} catch (error) {
|
|
144
|
-
ctx.log?.warn(
|
|
148
|
+
ctx.obs.log?.warn(
|
|
145
149
|
`[billing-foundation:record-contract-termination] provider cancel failed for tenant ${tenantId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
146
150
|
);
|
|
147
151
|
return { providerCancel: "none", effectiveAtIso: null, noticeReason: "provider_error" };
|
|
@@ -165,7 +169,7 @@ export function createDeclareContractTerminationHandler(): WriteHandlerDef {
|
|
|
165
169
|
handler: async (event, ctx) => {
|
|
166
170
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
167
171
|
const payload = event.payload as z.infer<typeof declareContractTerminationSchema>;
|
|
168
|
-
const stream = await ctx.fetchForWriting({
|
|
172
|
+
const stream = await ctx.events.fetchForWriting({
|
|
169
173
|
aggregateId: subscriptionAggregateId(event.user.tenantId),
|
|
170
174
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
171
175
|
});
|
|
@@ -189,38 +193,6 @@ function revokedRecordResult(receivedAtIso: string) {
|
|
|
189
193
|
};
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
// A revocation that landed during the provider cancel found nothing to take back yet.
|
|
193
|
-
async function takeBackLateRevokedCancel(
|
|
194
|
-
ctx: HandlerContext,
|
|
195
|
-
consumerProtection: ConsumerProtectionOptions,
|
|
196
|
-
args: {
|
|
197
|
-
readonly payload: z.infer<typeof recordContractTerminationSchema>;
|
|
198
|
-
readonly tenantId: string;
|
|
199
|
-
readonly receivedAtIso: string;
|
|
200
|
-
readonly providerCancel: ProviderCancelOutcome;
|
|
201
|
-
},
|
|
202
|
-
): Promise<void> {
|
|
203
|
-
const { payload, tenantId, receivedAtIso, providerCancel } = args;
|
|
204
|
-
// skip: nothing was cancelled at the provider, nothing to resume
|
|
205
|
-
if (providerCancel !== "period-end") return;
|
|
206
|
-
const reasons = await resumeAtProvider(ctx, await getSubscriptionForTenant(ctx, tenantId));
|
|
207
|
-
// skip: resumed cleanly, the revocation notice already tells the operator
|
|
208
|
-
if (reasons.length === 0) return;
|
|
209
|
-
await notifyOperator(ctx, consumerProtection, {
|
|
210
|
-
locale: payload.locale ?? FALLBACK_CONSENT_LOCALE,
|
|
211
|
-
requestId: payload.requestId,
|
|
212
|
-
receivedAtIso,
|
|
213
|
-
channel: "public",
|
|
214
|
-
declaration: {
|
|
215
|
-
declarationType: payload.declarationType,
|
|
216
|
-
terminationKind: payload.terminationKind,
|
|
217
|
-
},
|
|
218
|
-
reasons,
|
|
219
|
-
tenantId,
|
|
220
|
-
providerCancel,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
196
|
export function createRecordContractTerminationHandler(
|
|
225
197
|
options: ResolvedBillingFoundationOptions,
|
|
226
198
|
consumerProtection: ConsumerProtectionOptions,
|
|
@@ -234,6 +206,11 @@ export function createRecordContractTerminationHandler(
|
|
|
234
206
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
235
207
|
const payload = event.payload as z.infer<typeof recordContractTerminationSchema>;
|
|
236
208
|
const tenantId = event.user.tenantId;
|
|
209
|
+
// The provider cancel/resume and the stream append must not interleave across declarations of the same subscription.
|
|
210
|
+
await ctx.db.advisoryXactLock(
|
|
211
|
+
TERMINATION_PROVIDER_LOCK_NAMESPACE,
|
|
212
|
+
TERMINATION_PROVIDER_LOCK_KEY,
|
|
213
|
+
);
|
|
237
214
|
if (payload.channel === "public" && payload.when === SubscriptionCancelTimings.immediately) {
|
|
238
215
|
throw new UnprocessableError("immediate_cancel_not_allowed", {
|
|
239
216
|
message:
|
|
@@ -242,7 +219,7 @@ export function createRecordContractTerminationHandler(
|
|
|
242
219
|
}
|
|
243
220
|
|
|
244
221
|
const receivedAtIso = payload.receivedAtIso ?? options.now().toString();
|
|
245
|
-
const stream = await ctx.fetchForWriting({
|
|
222
|
+
const stream = await ctx.events.fetchForWriting({
|
|
246
223
|
aggregateId: subscriptionAggregateId(tenantId),
|
|
247
224
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
248
225
|
});
|
|
@@ -277,30 +254,12 @@ export function createRecordContractTerminationHandler(
|
|
|
277
254
|
effectiveAtIso,
|
|
278
255
|
providerCancel,
|
|
279
256
|
};
|
|
280
|
-
// Re-fetch after the external call:
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
const streamAfterProviderCall = await ctx.fetchForWriting({
|
|
257
|
+
// Re-fetch after the external call: webhooks append to this stream without the lock, so a
|
|
258
|
+
// stale expectedVersion would fail the append although the provider already cancelled.
|
|
259
|
+
const streamAfterProviderCall = await ctx.events.fetchForWriting({
|
|
284
260
|
aggregateId: subscriptionAggregateId(tenantId),
|
|
285
261
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
286
262
|
});
|
|
287
|
-
// A concurrent run with the same requestId may have recorded while we were
|
|
288
|
-
// at the provider; a second append would duplicate the event and the notice.
|
|
289
|
-
const recordedMeanwhile = findRecordedTermination(
|
|
290
|
-
streamAfterProviderCall.events,
|
|
291
|
-
payload.requestId,
|
|
292
|
-
);
|
|
293
|
-
if (recordedMeanwhile) return recordedTerminationResult(recordedMeanwhile);
|
|
294
|
-
// A revocation that landed during the provider call found no cancel to take back.
|
|
295
|
-
if (hasRevocation(streamAfterProviderCall.events, payload.requestId)) {
|
|
296
|
-
await takeBackLateRevokedCancel(ctx, consumerProtection, {
|
|
297
|
-
payload,
|
|
298
|
-
tenantId,
|
|
299
|
-
receivedAtIso,
|
|
300
|
-
providerCancel,
|
|
301
|
-
});
|
|
302
|
-
return revokedRecordResult(receivedAtIso);
|
|
303
|
-
}
|
|
304
263
|
await streamAfterProviderCall.appendOne({
|
|
305
264
|
type: CONTRACT_TERMINATION_REQUESTED_EVENT_QN,
|
|
306
265
|
payload: recorded,
|
|
@@ -389,7 +348,7 @@ export async function resumeAtProvider(
|
|
|
389
348
|
providerSubscriptionId: subscription.providerSubscriptionId,
|
|
390
349
|
});
|
|
391
350
|
} catch (error) {
|
|
392
|
-
ctx.log?.warn(
|
|
351
|
+
ctx.obs.log?.warn(
|
|
393
352
|
`[billing-foundation:revoke-contract-termination] provider resume failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
394
353
|
);
|
|
395
354
|
return ["revocation_resume_failed"];
|
|
@@ -414,7 +373,7 @@ export function createRecordUnmatchedContractTerminationHandler(): WriteHandlerD
|
|
|
414
373
|
handler: async (event, ctx) => {
|
|
415
374
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
416
375
|
const payload = event.payload as z.infer<typeof recordUnmatchedContractTerminationSchema>;
|
|
417
|
-
const stream = await ctx.fetchForWriting({
|
|
376
|
+
const stream = await ctx.events.fetchForWriting({
|
|
418
377
|
aggregateId: terminationUnmatchedAggregateId(payload.requestId),
|
|
419
378
|
aggregateType: CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE,
|
|
420
379
|
});
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts
CHANGED
|
@@ -160,11 +160,13 @@ async function findTerminableTenantIds(
|
|
|
160
160
|
"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",
|
|
161
161
|
});
|
|
162
162
|
const user = authUserRowSchema.safeParse(
|
|
163
|
-
await ctx.queryAs(createSystemUser(requestTenantId), UserQueries.findForAuth, {
|
|
163
|
+
await ctx.dispatch.queryAs(createSystemUser(requestTenantId), UserQueries.findForAuth, {
|
|
164
|
+
email,
|
|
165
|
+
}),
|
|
164
166
|
);
|
|
165
167
|
if (!user.success || user.data.isDeleted === true) return [];
|
|
166
168
|
const memberships = membershipRowsSchema.safeParse(
|
|
167
|
-
await ctx.queryAs(createSystemUser(requestTenantId), TenantQueries.memberships, {
|
|
169
|
+
await ctx.dispatch.queryAs(createSystemUser(requestTenantId), TenantQueries.memberships, {
|
|
168
170
|
userId: user.data.id,
|
|
169
171
|
}),
|
|
170
172
|
);
|
|
@@ -180,7 +182,7 @@ async function findTerminableTenantIds(
|
|
|
180
182
|
const terminable: string[] = [];
|
|
181
183
|
for (const tenantId of adminTenantIds) {
|
|
182
184
|
const probe = terminableResultSchema.safeParse(
|
|
183
|
-
await ctx.queryAs(
|
|
185
|
+
await ctx.dispatch.queryAs(
|
|
184
186
|
createSystemUser(tenantId),
|
|
185
187
|
SubscriptionFoundationQueries.terminableSubscription,
|
|
186
188
|
{},
|
|
@@ -205,7 +207,7 @@ export function createRequestContractTerminationHandler(
|
|
|
205
207
|
...(consumerProtection.terminationScope === "platform" && { tenantlessAnonymous: true }),
|
|
206
208
|
escapeHatch: {
|
|
207
209
|
reason:
|
|
208
|
-
"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.",
|
|
210
|
+
"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.",
|
|
209
211
|
},
|
|
210
212
|
handler: async (event, ctx) => {
|
|
211
213
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
@@ -228,7 +230,7 @@ export function createRequestContractTerminationHandler(
|
|
|
228
230
|
if (candidates.length === 1 && onlyCandidate !== undefined) {
|
|
229
231
|
matchedTenantId = onlyCandidate;
|
|
230
232
|
const revocation = createRevocationToken();
|
|
231
|
-
const declared = await ctx.writeAs(
|
|
233
|
+
const declared = await ctx.dispatch.writeAs(
|
|
232
234
|
createSystemUser(onlyCandidate),
|
|
233
235
|
SubscriptionFoundationHandlers.declareContractTermination,
|
|
234
236
|
{
|
|
@@ -253,7 +255,7 @@ export function createRequestContractTerminationHandler(
|
|
|
253
255
|
}
|
|
254
256
|
} else {
|
|
255
257
|
reasons.push(candidates.length === 0 ? "unmatched" : "ambiguous");
|
|
256
|
-
const recorded = await ctx.writeAs(
|
|
258
|
+
const recorded = await ctx.dispatch.writeAs(
|
|
257
259
|
createSystemUser(SYSTEM_TENANT_ID),
|
|
258
260
|
SubscriptionFoundationHandlers.recordUnmatchedContractTermination,
|
|
259
261
|
{
|
|
@@ -318,7 +320,7 @@ export function createTerminateContractHandler(
|
|
|
318
320
|
const { declarationType, terminationKind } = payload;
|
|
319
321
|
|
|
320
322
|
const caller = authUserRowSchema.safeParse(
|
|
321
|
-
await ctx.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
|
|
323
|
+
await ctx.dispatch.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
|
|
322
324
|
id: event.user.id,
|
|
323
325
|
}),
|
|
324
326
|
);
|
|
@@ -332,7 +334,7 @@ export function createTerminateContractHandler(
|
|
|
332
334
|
});
|
|
333
335
|
}
|
|
334
336
|
|
|
335
|
-
const recorded = await ctx.writeAs(
|
|
337
|
+
const recorded = await ctx.dispatch.writeAs(
|
|
336
338
|
createSystemUser(tenantId),
|
|
337
339
|
SubscriptionFoundationHandlers.recordContractTermination,
|
|
338
340
|
{
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts
CHANGED
|
@@ -24,6 +24,11 @@ import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from
|
|
|
24
24
|
import { SubscriptionFoundationHandlers } from "../web/constants.ts";
|
|
25
25
|
import type { ConsentLocale } from "./consent-text.ts";
|
|
26
26
|
import { notifyOperator } from "./termination-notify.ts";
|
|
27
|
+
import { hasOtherOpenProviderCancel } from "./termination-open-cancels.ts";
|
|
28
|
+
import {
|
|
29
|
+
TERMINATION_PROVIDER_LOCK_KEY,
|
|
30
|
+
TERMINATION_PROVIDER_LOCK_NAMESPACE,
|
|
31
|
+
} from "./termination-provider-lock.ts";
|
|
27
32
|
import { findRecordedTermination, hasRevocation, resumeAtProvider } from "./termination-record.ts";
|
|
28
33
|
import type { OperatorNoticeReason } from "./termination-texts.ts";
|
|
29
34
|
|
|
@@ -103,7 +108,7 @@ export function createRevokeContractTerminationHandler(): WriteHandlerDef {
|
|
|
103
108
|
handler: async (event, ctx) => {
|
|
104
109
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
105
110
|
const payload = event.payload as z.infer<typeof revokeContractTerminationSchema>;
|
|
106
|
-
const result = await ctx.writeAs(
|
|
111
|
+
const result = await ctx.dispatch.writeAs(
|
|
107
112
|
createSystemUser(payload.tenantId),
|
|
108
113
|
SubscriptionFoundationHandlers.recordContractTerminationRevocation,
|
|
109
114
|
{ requestId: payload.requestId, token: payload.token },
|
|
@@ -129,11 +134,16 @@ export function createRecordContractTerminationRevocationHandler(
|
|
|
129
134
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
130
135
|
const payload = event.payload as z.infer<typeof recordContractTerminationRevocationSchema>;
|
|
131
136
|
const tenantId = event.user.tenantId;
|
|
137
|
+
// The provider cancel/resume and the stream append must not interleave across declarations of the same subscription.
|
|
138
|
+
await ctx.db.advisoryXactLock(
|
|
139
|
+
TERMINATION_PROVIDER_LOCK_NAMESPACE,
|
|
140
|
+
TERMINATION_PROVIDER_LOCK_KEY,
|
|
141
|
+
);
|
|
132
142
|
const outcome = (value: RevocationOutcome) => ({
|
|
133
143
|
isSuccess: true as const,
|
|
134
144
|
data: { outcome: value },
|
|
135
145
|
});
|
|
136
|
-
const stream = await ctx.fetchForWriting({
|
|
146
|
+
const stream = await ctx.events.fetchForWriting({
|
|
137
147
|
aggregateId: subscriptionAggregateId(tenantId),
|
|
138
148
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
139
149
|
});
|
|
@@ -162,25 +172,18 @@ export function createRecordContractTerminationRevocationHandler(
|
|
|
162
172
|
}
|
|
163
173
|
|
|
164
174
|
const reasons: OperatorNoticeReason[] = ["revoked"];
|
|
165
|
-
if (
|
|
175
|
+
if (
|
|
176
|
+
requested?.providerCancel === "period-end" &&
|
|
177
|
+
!hasOtherOpenProviderCancel(stream.events, payload.requestId)
|
|
178
|
+
) {
|
|
166
179
|
reasons.push(...(await resumeAtProvider(ctx, subscription)));
|
|
167
180
|
}
|
|
168
181
|
// Re-fetch after the external call, like record-contract-termination.
|
|
169
|
-
const streamAfterProviderCall = await ctx.fetchForWriting({
|
|
182
|
+
const streamAfterProviderCall = await ctx.events.fetchForWriting({
|
|
170
183
|
aggregateId: subscriptionAggregateId(tenantId),
|
|
171
184
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
172
185
|
});
|
|
173
|
-
|
|
174
|
-
const requestedMeanwhile =
|
|
175
|
-
requested === undefined
|
|
176
|
-
? findRecordedTermination(streamAfterProviderCall.events, payload.requestId)
|
|
177
|
-
: undefined;
|
|
178
|
-
if (requestedMeanwhile?.providerCancel === "period-end") {
|
|
179
|
-
reasons.push(
|
|
180
|
-
...(await resumeAtProvider(ctx, await getSubscriptionForTenant(ctx, tenantId))),
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
const providerCancel = (requested ?? requestedMeanwhile)?.providerCancel;
|
|
186
|
+
const providerCancel = requested?.providerCancel;
|
|
184
187
|
const revoked: ContractTerminationRevokedPayload = {
|
|
185
188
|
requestId: payload.requestId,
|
|
186
189
|
revokedAtIso: now.toString(),
|