@cosmicdrift/kumiko-samples 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/package.json +1 -1
- package/packages/bundled-features/package.json +25 -13
- package/packages/bundled-features/src/agent-tools/changes.json +6 -0
- package/packages/bundled-features/src/agent-tools/index.ts +1 -1
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +42 -26
- package/packages/bundled-features/src/audit/feature.ts +2 -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 +18 -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/request-account-unlock.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
- 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 +16 -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 +10 -2
- package/packages/bundled-features/src/auth-email-password/handlers/verify-email.write.ts +3 -2
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
- package/packages/bundled-features/src/auth-email-password/seeding.ts +14 -9
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
- package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
- package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
- package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
- package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
- package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
- package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
- package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
- package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
- package/packages/bundled-features/src/auth-foundation/feature.ts +13 -4
- package/packages/bundled-features/src/auth-mfa/changes.json +6 -0
- package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
- 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 +17 -8
- 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 +21 -8
- package/packages/bundled-features/src/cap-counter/changes.json +12 -0
- package/packages/bundled-features/src/cap-counter/constants.ts +1 -1
- package/packages/bundled-features/src/cap-counter/feature.ts +3 -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/feature.ts +1 -0
- 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/changes.json +6 -0
- 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 +22 -5
- package/packages/bundled-features/src/config/handlers/values.query.ts +1 -1
- package/packages/bundled-features/src/crypto-shredding/changes.json +12 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +9 -9
- 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/custom-fields/lib/parse-serialized-field.ts +1 -1
- 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/changes.json +12 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +6 -5
- package/packages/bundled-features/src/delivery/feature.ts +3 -2
- package/packages/bundled-features/src/delivery/index.ts +5 -0
- package/packages/bundled-features/src/delivery/jobs.ts +11 -11
- package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
- package/packages/bundled-features/src/delivery/testing.ts +1 -1
- package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/document-ingest-foundation/events.ts +16 -1
- package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
- package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
- 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/host-routing/changes.json +6 -0
- package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/events.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +2 -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/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/feature.ts +1 -0
- 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/ledger/web/index.ts +1 -1
- 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/legal-pages/seed.ts +5 -2
- package/packages/bundled-features/src/mail-foundation/feature.ts +2 -2
- package/packages/bundled-features/src/mail-transport-smtp/changes.json +12 -0
- package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +13 -12
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
- package/packages/bundled-features/src/managed-pages/index.ts +1 -0
- package/packages/bundled-features/src/managed-pages/seeding.ts +13 -8
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +2 -2
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -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/changes.json +6 -0
- 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 +10 -3
- package/packages/bundled-features/src/seo/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/changes.json +19 -0
- package/packages/bundled-features/src/sessions/feature.ts +2 -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/index.ts +1 -1
- package/packages/bundled-features/src/sessions/screens.ts +8 -4
- package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
- 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 +36 -4
- package/packages/bundled-features/src/shared/require-for-tenant.ts +7 -5
- package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -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/changes.json +6 -0
- package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -1
- package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -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/tags/feature.ts +1 -0
- package/packages/bundled-features/src/template-resolver/api.ts +3 -3
- package/packages/bundled-features/src/template-resolver/changes.json +54 -0
- package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
- package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
- 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/layout-source.query.ts +36 -11
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +32 -7
- package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/send-test-shared.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
- 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 +22 -5
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +68 -19
- 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/i18n.ts +6 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +19 -6
- package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
- 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/feature.ts +1 -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/team-list.query.ts +3 -3
- 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/changes.json +6 -0
- 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 +42 -4
- 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/feature.ts +1 -0
- 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 +36 -0
- package/packages/bundled-features/src/user-data-rights/feature.ts +30 -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 +8 -1
- 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 +16 -0
- package/packages/bundled-features/src/user-data-rights/web/index.ts +6 -0
- package/packages/bundled-features/src/user-profile/handlers/change-email.write.ts +8 -6
- package/packages/bundled-features/src/waitlist/changes.json +18 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -0
- 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 +34 -10
- package/packages/bundled-features/src/waitlist/import.ts +23 -3
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +8 -2
- package/packages/bundled-features/src/waitlist/link-on-signup.ts +7 -1
- package/packages/bundled-features/src/waitlist/options.ts +2 -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/src/feature.ts +19 -5
- package/samples/apps/config-demo/src/app/server.ts +1 -1
- package/samples/apps/styleguide/src/app/client.tsx +2 -0
- package/samples/apps/styleguide/src/app/seed.ts +6 -4
- package/samples/apps/styleguide/src/app/server.ts +6 -3
- package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
- package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
- package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
- package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
- package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
- package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
- package/samples/apps/use-all-bundled/bin/main.ts +2 -0
- package/samples/apps/use-all-bundled/feature-manifest.json +17 -1
- package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
- package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
- package/samples/apps/use-all-bundled/package.json +2 -1
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
- package/samples/apps/use-all-bundled/src/app/seed.ts +12 -2
- package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
- package/samples/apps/user-data-rights-demo/README.md +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/golden-path/src/feature.ts +2 -1
- 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 +4 -0
- package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
- package/samples/recipes/list-table-options/README.md +50 -0
- package/samples/recipes/list-table-options/package.json +11 -0
- package/samples/recipes/list-table-options/src/feature.ts +221 -0
- package/samples/recipes/managed-config/README.md +3 -3
- package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
- 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 +4 -3
- package/samples/recipes/state-machine/src/reducer.ts +1 -1
- package/samples/recipes/tenant-isolation/README.md +1 -0
- package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
- package/samples/recipes/workflow-engine/src/resume-loop.ts +4 -4
- package/samples/recipes/workflow-engine/src/workflow-runner.ts +2 -2
|
@@ -12,7 +12,7 @@ import * as z from "zod";
|
|
|
12
12
|
// _SHORT/_QN pair mirrors cap-counter's rolling-incremented event:
|
|
13
13
|
// _SHORT — r.defineEvent(short, schema) in feature.ts, framework prefixes
|
|
14
14
|
// it to the QN below.
|
|
15
|
-
// _QN — ctx.
|
|
15
|
+
// _QN — ctx.events.unsafeAppend({ type }) / raw append()'s `type` field.
|
|
16
16
|
export const SESSION_REVOKED_EVENT_SHORT = "session-revoked" as const;
|
|
17
17
|
export const SESSION_REVOKED_EVENT_QN = "sessions:event:session-revoked" as const;
|
|
18
18
|
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
// Column renderer for
|
|
1
|
+
// Column renderer for session devices, registered via `sessionsClient()`. Takes the server-made
|
|
2
|
+
// `deviceLabel` string, or the parsed `device` object for screens that bind that column.
|
|
2
3
|
|
|
3
4
|
import { type ColumnRendererProps, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
4
5
|
import type { ReactNode } from "react";
|
|
5
|
-
import { parsedDeviceSchema } from "./user-agent.ts";
|
|
6
|
+
import { formatDeviceLabel, parsedDeviceSchema } from "./user-agent.ts";
|
|
7
|
+
|
|
8
|
+
function deviceLabelOf(value: unknown): string | null {
|
|
9
|
+
if (typeof value === "string") return value === "" ? null : value;
|
|
10
|
+
const parsed = parsedDeviceSchema.safeParse(value);
|
|
11
|
+
return parsed.success ? formatDeviceLabel(parsed.data) : null;
|
|
12
|
+
}
|
|
6
13
|
|
|
7
14
|
export function SessionDeviceCell({ value }: ColumnRendererProps): ReactNode {
|
|
8
15
|
const t = useTranslation();
|
|
9
|
-
|
|
10
|
-
const device = parsed.success ? parsed.data : null;
|
|
11
|
-
if (device === null) return t("sessions.device.unknown");
|
|
12
|
-
const browser =
|
|
13
|
-
device.browser === null
|
|
14
|
-
? null
|
|
15
|
-
: device.browserMajor === null
|
|
16
|
-
? device.browser
|
|
17
|
-
: `${device.browser} ${device.browserMajor}`;
|
|
18
|
-
return [browser, device.os].filter((part) => part !== null).join(" · ");
|
|
16
|
+
return deviceLabelOf(value) ?? t("sessions.device.unknown");
|
|
19
17
|
}
|
|
@@ -56,6 +56,18 @@ const OPERATING_SYSTEM_PATTERNS: readonly {
|
|
|
56
56
|
{ os: "Linux", pattern: /\bLinux\b/ },
|
|
57
57
|
];
|
|
58
58
|
|
|
59
|
+
export function formatDeviceLabel(device: ParsedDevice): string | null {
|
|
60
|
+
if (device === null) return null;
|
|
61
|
+
const browser =
|
|
62
|
+
device.browser === null
|
|
63
|
+
? null
|
|
64
|
+
: device.browserMajor === null
|
|
65
|
+
? device.browser
|
|
66
|
+
: `${device.browser} ${device.browserMajor}`;
|
|
67
|
+
const parts = [browser, device.os].filter((part) => part !== null);
|
|
68
|
+
return parts.length === 0 ? null : parts.join(" · ");
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
export function parseUserAgent(userAgent: string | null): ParsedDevice {
|
|
60
72
|
if (userAgent === null || userAgent === "") return null;
|
|
61
73
|
let browser: UserAgentBrowser | null = null;
|
|
@@ -92,5 +92,20 @@ export {
|
|
|
92
92
|
type SignupHandoverProvider,
|
|
93
93
|
} from "./signup-handover.ts";
|
|
94
94
|
export { createSingleUseTokenStore, hashSingleUseToken } from "./single-use-token-store.ts";
|
|
95
|
+
export {
|
|
96
|
+
backfillAggregateStreamTenants,
|
|
97
|
+
type StreamBackfillCandidate,
|
|
98
|
+
type StreamBackfillFailure,
|
|
99
|
+
type StreamBackfillOptions,
|
|
100
|
+
type StreamBackfillResult,
|
|
101
|
+
type StreamTenantTarget,
|
|
102
|
+
} from "./stream-tenant-backfill.ts";
|
|
103
|
+
export {
|
|
104
|
+
createSearchBucketCleanup,
|
|
105
|
+
type DerivedStateMode,
|
|
106
|
+
rebuildAndReindex,
|
|
107
|
+
type StreamBackfillStepFailure,
|
|
108
|
+
stepFailures,
|
|
109
|
+
} from "./stream-tenant-derived-state.ts";
|
|
95
110
|
export type { SystemQueryFn } from "./system-query.ts";
|
|
96
111
|
export { type BurnResult, burnToken, unburnToken } from "./token-burn-store.ts";
|
|
@@ -112,6 +112,29 @@ function fieldsResolvingToSubject(
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// A user owning many records must not load them all at once; the id cursor keeps each page
|
|
116
|
+
// bounded while rows purged in place stay out of the way.
|
|
117
|
+
export const PURGE_CANDIDATE_PAGE_SIZE = 200;
|
|
118
|
+
|
|
119
|
+
async function* candidateRowsPaged(
|
|
120
|
+
db: DbRunner,
|
|
121
|
+
table: FileEntity["table"],
|
|
122
|
+
where: WhereObject,
|
|
123
|
+
): AsyncGenerator<Record<string, unknown>> {
|
|
124
|
+
let cursor: unknown;
|
|
125
|
+
for (;;) {
|
|
126
|
+
const pageWhere: WhereObject = cursor === undefined ? where : { ...where, id: { gt: cursor } };
|
|
127
|
+
const page = await selectMany<Record<string, unknown>>(db, table, pageWhere, {
|
|
128
|
+
orderBy: { col: "id", direction: "asc" },
|
|
129
|
+
limit: PURGE_CANDIDATE_PAGE_SIZE,
|
|
130
|
+
});
|
|
131
|
+
yield* page;
|
|
132
|
+
// skip: a short page is the last one
|
|
133
|
+
if (page.length < PURGE_CANDIDATE_PAGE_SIZE) return;
|
|
134
|
+
cursor = page[page.length - 1]?.["id"];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
const fileRefExecutor = createEventStoreExecutor(fileRefsTable, fileRefEntity, {
|
|
116
139
|
entityName: "fileRef",
|
|
117
140
|
});
|
|
@@ -165,8 +188,7 @@ export async function purgeFilesReachingSubject(args: {
|
|
|
165
188
|
for (const fileEntity of fileEntities) {
|
|
166
189
|
const seenRowIds = new Set<string>();
|
|
167
190
|
for (const where of candidateWheres(subject, recordTenantId, fileEntity)) {
|
|
168
|
-
|
|
169
|
-
for (const row of rows) {
|
|
191
|
+
for await (const row of candidateRowsPaged(db, fileEntity.table, where)) {
|
|
170
192
|
const rowId = String(row["id"]);
|
|
171
193
|
const tenantId = parseTenantId(row["tenantId"]);
|
|
172
194
|
// skip: a row without a tenant cannot own tenant-scoped file refs
|
|
@@ -184,7 +206,12 @@ export async function purgeFilesReachingSubject(args: {
|
|
|
184
206
|
}
|
|
185
207
|
const deleteBytes = await storagePromise;
|
|
186
208
|
const systemUser = createSystemUser(tenantId);
|
|
187
|
-
const tdb = createTenantDb({
|
|
209
|
+
const tdb = createTenantDb({
|
|
210
|
+
db,
|
|
211
|
+
tenantId,
|
|
212
|
+
mode: "system",
|
|
213
|
+
reason: "subject erasure purges file refs in every tenant that holds them",
|
|
214
|
+
});
|
|
188
215
|
|
|
189
216
|
const outcome = await purgeRowFileRefs({
|
|
190
217
|
db,
|
|
@@ -331,7 +358,12 @@ async function purgePendingFileRef(
|
|
|
331
358
|
const forgotten = await fileRefExecutor.forget(
|
|
332
359
|
{ id: fileRefId },
|
|
333
360
|
createSystemUser(tenantId),
|
|
334
|
-
createTenantDb({
|
|
361
|
+
createTenantDb({
|
|
362
|
+
db,
|
|
363
|
+
tenantId,
|
|
364
|
+
mode: "system",
|
|
365
|
+
reason: "subject erasure purges file refs in every tenant that holds them",
|
|
366
|
+
}),
|
|
335
367
|
);
|
|
336
368
|
return forgotten.isSuccess
|
|
337
369
|
? undefined
|
|
@@ -5,14 +5,16 @@ import type {
|
|
|
5
5
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
6
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
7
7
|
|
|
8
|
-
// ctx.
|
|
8
|
+
// ctx.tenants.writeTarget is absent for r.systemScope() handlers and without a db; a write handler that
|
|
9
9
|
// needs a cross-tenant target must fail loudly instead of falling back to ctx.db.
|
|
10
10
|
export function requireForTenant(
|
|
11
|
-
ctx: Pick<HandlerContext, "
|
|
11
|
+
ctx: Pick<HandlerContext, "tenants">,
|
|
12
12
|
targetTenantId: TenantId,
|
|
13
13
|
): TenantWriteTarget {
|
|
14
|
-
if (!ctx.
|
|
15
|
-
throw new InternalError({
|
|
14
|
+
if (!ctx.tenants.writeTarget) {
|
|
15
|
+
throw new InternalError({
|
|
16
|
+
message: "ctx.tenants.writeTarget is not available in this handler context",
|
|
17
|
+
});
|
|
16
18
|
}
|
|
17
|
-
return ctx.
|
|
19
|
+
return ctx.tenants.writeTarget(targetTenantId);
|
|
18
20
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
|
|
3
3
|
// Per-unit sub-transaction (one user, one purged row), nesting-aware across both db shapes:
|
|
4
|
-
// - top-level connection (
|
|
5
|
-
// -
|
|
4
|
+
// - top-level connection (Bun.SQL pool) → `.begin` (BEGIN)
|
|
5
|
+
// - TransactionSQL (inside the dispatcher's outer tx, where every
|
|
6
6
|
// writeHandler already runs) → `.savepoint` (SAVEPOINT)
|
|
7
|
-
// A
|
|
8
|
-
// threw
|
|
7
|
+
// A transaction handle cannot begin another transaction, so the previous unconditional `.begin`
|
|
8
|
+
// threw on every user when invoked through the dispatcher
|
|
9
9
|
// (the cron path) → zero deletions in production, while direct-connection tests
|
|
10
10
|
// stayed green. Selecting the available method makes the sub-tx work in both
|
|
11
11
|
// contexts; on throw the savepoint rolls back just this user (others survive).
|
|
@@ -21,7 +21,7 @@ export async function runInSubTransaction<T>(
|
|
|
21
21
|
savepoint?: (f: (tx: DbRunner) => Promise<T>) => Promise<T>;
|
|
22
22
|
};
|
|
23
23
|
// savepoint-first: empirically (Bun 1.3.14, not re-checked on 1.4.0) the
|
|
24
|
-
// surfaces are NOT mutually exclusive — a
|
|
24
|
+
// surfaces are NOT mutually exclusive — a TransactionSQL exposes begin AND
|
|
25
25
|
// savepoint, only the top-level connection has begin alone. begin-first
|
|
26
26
|
// picked the nested BEGIN inside a tx (the incident class above);
|
|
27
27
|
// savepoint-first hits the savepoint there and falls back to begin top-level.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// One-time migration tooling for event streams that live on the wrong tenant.
|
|
2
|
+
//
|
|
3
|
+
// An aggregate type that became tenant-independent (systemStream) or whose
|
|
4
|
+
// stream must follow the tenant in its payload has older streams on whichever
|
|
5
|
+
// tenant happened to write them. Newer writes then version-conflict against
|
|
6
|
+
// the empty target stream, and a raw tenant_id UPDATE trips
|
|
7
|
+
// events_aggregate_version_uq as soon as an aggregate has BOTH a legacy
|
|
8
|
+
// stream and newer events on the target (split stream).
|
|
9
|
+
//
|
|
10
|
+
// This tool merges per aggregate: all events of the aggregate, ordered by
|
|
11
|
+
// global event id, are renumbered 1..n and retenanted to the target - two-phase
|
|
12
|
+
// version writes (negative interim values) dodge the unique index while the
|
|
13
|
+
// per-aggregate transaction is in flight. Snapshots of migrated aggregates
|
|
14
|
+
// are dropped (version numbering changed; the next snapshotting load
|
|
15
|
+
// recreates them); archived-stream markers move with the stream.
|
|
16
|
+
//
|
|
17
|
+
// Idempotent: candidates are only aggregates that still have events outside
|
|
18
|
+
// their target tenant. One failing aggregate does not abort the run: the rest
|
|
19
|
+
// of the estate migrates, failures are reported.
|
|
20
|
+
//
|
|
21
|
+
// After a run, rebuild the affected projections so read-table tenant_id
|
|
22
|
+
// reflects the stream move.
|
|
23
|
+
|
|
24
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
25
|
+
import {
|
|
26
|
+
asRawClient,
|
|
27
|
+
toPostgresArrayLiteral,
|
|
28
|
+
transaction,
|
|
29
|
+
unsafeReadRetrying,
|
|
30
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
31
|
+
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
32
|
+
|
|
33
|
+
export type StreamTenantTarget =
|
|
34
|
+
| { readonly kind: "system" }
|
|
35
|
+
// The tenantId carried by the aggregate's earliest event payload.
|
|
36
|
+
| { readonly kind: "payload-tenant" };
|
|
37
|
+
|
|
38
|
+
export type StreamBackfillCandidate = {
|
|
39
|
+
readonly aggregateId: string;
|
|
40
|
+
readonly targetTenantId: string;
|
|
41
|
+
readonly sourceTenantIds: readonly string[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type StreamBackfillFailure = { readonly aggregateId: string; readonly error: string };
|
|
45
|
+
|
|
46
|
+
export type StreamBackfillResult = {
|
|
47
|
+
readonly aggregatesMigrated: number;
|
|
48
|
+
readonly eventsMigrated: number;
|
|
49
|
+
readonly failed: readonly StreamBackfillFailure[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type StreamBackfillOptions = {
|
|
53
|
+
readonly aggregateType: string;
|
|
54
|
+
readonly target: StreamTenantTarget;
|
|
55
|
+
// Runs per candidate before its events move; a throw skips the aggregate
|
|
56
|
+
// and lands in `failed` (search cleanup must not be lost by a move).
|
|
57
|
+
readonly beforeMove?: (candidate: StreamBackfillCandidate) => Promise<void>;
|
|
58
|
+
// Restricts the candidate scan to events on these tenants, which lets Postgres use
|
|
59
|
+
// events_tenant_type_idx instead of grouping every event of the type. Streams on a tenant
|
|
60
|
+
// outside the list are not found; omit it for the full scan.
|
|
61
|
+
readonly scanTenantIds?: readonly string[];
|
|
62
|
+
// Called once when the scan found candidates, before the first move. "skip" ends the run
|
|
63
|
+
// with nothing moved (this process cannot do the work); a throw propagates to the caller.
|
|
64
|
+
readonly onFirstCandidate?: () => Promise<"proceed" | "skip">;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type CandidateRow = {
|
|
68
|
+
aggregate_id: string;
|
|
69
|
+
target_tenant_id: string | null;
|
|
70
|
+
source_tenant_ids: string[];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export async function backfillAggregateStreamTenants(
|
|
74
|
+
db: DbConnection,
|
|
75
|
+
options: StreamBackfillOptions,
|
|
76
|
+
): Promise<StreamBackfillResult> {
|
|
77
|
+
const rows = await readCandidateRows(db, options);
|
|
78
|
+
if (rows.length > 0 && options.onFirstCandidate) {
|
|
79
|
+
if ((await options.onFirstCandidate()) === "skip") {
|
|
80
|
+
return { aggregatesMigrated: 0, eventsMigrated: 0, failed: [] };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let aggregatesMigrated = 0;
|
|
85
|
+
let eventsMigrated = 0;
|
|
86
|
+
const failed: StreamBackfillFailure[] = [];
|
|
87
|
+
|
|
88
|
+
for (const row of rows) {
|
|
89
|
+
try {
|
|
90
|
+
if (!row.target_tenant_id) {
|
|
91
|
+
throw new Error(`earliest event of the aggregate carries no payload.tenantId`);
|
|
92
|
+
}
|
|
93
|
+
const candidate: StreamBackfillCandidate = {
|
|
94
|
+
aggregateId: row.aggregate_id,
|
|
95
|
+
targetTenantId: row.target_tenant_id,
|
|
96
|
+
sourceTenantIds: row.source_tenant_ids,
|
|
97
|
+
};
|
|
98
|
+
await options.beforeMove?.(candidate);
|
|
99
|
+
eventsMigrated += await migrateAggregate(db, options.aggregateType, candidate);
|
|
100
|
+
aggregatesMigrated++;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
failed.push({
|
|
103
|
+
aggregateId: row.aggregate_id,
|
|
104
|
+
error: e instanceof Error ? e.message : String(e),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { aggregatesMigrated, eventsMigrated, failed };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function readCandidateRows(
|
|
112
|
+
db: DbConnection,
|
|
113
|
+
options: StreamBackfillOptions,
|
|
114
|
+
): Promise<readonly CandidateRow[]> {
|
|
115
|
+
const targetSql =
|
|
116
|
+
options.target.kind === "system"
|
|
117
|
+
? `$2::text`
|
|
118
|
+
: `(SELECT e2."payload"->>'tenantId' FROM "kumiko_events" e2
|
|
119
|
+
WHERE e2."aggregate_type" = $1 AND e2."aggregate_id" = e."aggregate_id"
|
|
120
|
+
ORDER BY e2."id" ASC LIMIT 1)`;
|
|
121
|
+
const params: unknown[] = [options.aggregateType];
|
|
122
|
+
if (options.target.kind === "system") params.push(SYSTEM_TENANT_ID);
|
|
123
|
+
let scanSql = "";
|
|
124
|
+
if (options.scanTenantIds) {
|
|
125
|
+
// Events already on the SYSTEM target are not legacy; leaving them out keeps the scan to
|
|
126
|
+
// the legacy events only. A payload target differs per aggregate, so nothing is left out.
|
|
127
|
+
const scanned =
|
|
128
|
+
options.target.kind === "system"
|
|
129
|
+
? options.scanTenantIds.filter((id) => id !== SYSTEM_TENANT_ID)
|
|
130
|
+
: options.scanTenantIds;
|
|
131
|
+
params.push(toPostgresArrayLiteral(scanned));
|
|
132
|
+
scanSql = ` AND e."tenant_id" = ANY($${params.length}::uuid[])`;
|
|
133
|
+
}
|
|
134
|
+
// An aggregate whose earliest event has no payload tenant yields a NULL
|
|
135
|
+
// target and is reported as failed instead of being silently skipped.
|
|
136
|
+
return unsafeReadRetrying<CandidateRow>(
|
|
137
|
+
db,
|
|
138
|
+
`SELECT e."aggregate_id" AS aggregate_id,
|
|
139
|
+
${targetSql} AS target_tenant_id,
|
|
140
|
+
array_agg(DISTINCT e."tenant_id"::text) AS source_tenant_ids
|
|
141
|
+
FROM "kumiko_events" e
|
|
142
|
+
WHERE e."aggregate_type" = $1${scanSql}
|
|
143
|
+
GROUP BY e."aggregate_id"
|
|
144
|
+
HAVING bool_or(e."tenant_id"::text IS DISTINCT FROM ${targetSql})`,
|
|
145
|
+
params,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function migrateAggregate(
|
|
150
|
+
db: DbConnection,
|
|
151
|
+
aggregateType: string,
|
|
152
|
+
candidate: StreamBackfillCandidate,
|
|
153
|
+
): Promise<number> {
|
|
154
|
+
const { aggregateId, targetTenantId } = candidate;
|
|
155
|
+
return transaction(db, async (tx) => {
|
|
156
|
+
const raw = asRawClient(tx);
|
|
157
|
+
// Global id order = commit-adjacent replay order; merges a split stream
|
|
158
|
+
// into one consistent version sequence.
|
|
159
|
+
const events = await raw.unsafe<{ id: bigint | string }>(
|
|
160
|
+
`SELECT "id" FROM "kumiko_events"
|
|
161
|
+
WHERE "aggregate_type" = $1 AND "aggregate_id" = $2::uuid
|
|
162
|
+
ORDER BY "id" ASC
|
|
163
|
+
FOR UPDATE`,
|
|
164
|
+
[aggregateType, aggregateId],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Negative interim versions: real versions are always >= 1, so nothing
|
|
168
|
+
// can collide with events_aggregate_version_uq mid-migration.
|
|
169
|
+
for (const [i, ev] of events.entries()) {
|
|
170
|
+
await raw.unsafe(
|
|
171
|
+
`UPDATE "kumiko_events" SET "tenant_id" = $1::uuid, "version" = $2 WHERE "id" = $3`,
|
|
172
|
+
[targetTenantId, -(i + 1), ev.id],
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
for (const [i, ev] of events.entries()) {
|
|
176
|
+
await raw.unsafe(`UPDATE "kumiko_events" SET "version" = $1 WHERE "id" = $2`, [i + 1, ev.id]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Version numbering changed, so any snapshot of this aggregate is stale.
|
|
180
|
+
await raw.unsafe(`DELETE FROM "kumiko_snapshots" WHERE "aggregate_id" = $1::uuid`, [
|
|
181
|
+
aggregateId,
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
// Archived markers key on (tenant_id, aggregate_id): move them with the
|
|
185
|
+
// stream, keeping an existing target marker if both exist.
|
|
186
|
+
await raw.unsafe(
|
|
187
|
+
`INSERT INTO "kumiko_archived_streams" ("tenant_id", "aggregate_id", "aggregate_type", "archived_at", "archived_by", "reason")
|
|
188
|
+
SELECT $1::uuid, "aggregate_id", "aggregate_type", "archived_at", "archived_by", "reason"
|
|
189
|
+
FROM "kumiko_archived_streams"
|
|
190
|
+
WHERE "aggregate_id" = $2::uuid AND "tenant_id" <> $1::uuid
|
|
191
|
+
ON CONFLICT DO NOTHING`,
|
|
192
|
+
[targetTenantId, aggregateId],
|
|
193
|
+
);
|
|
194
|
+
await raw.unsafe(
|
|
195
|
+
`DELETE FROM "kumiko_archived_streams" WHERE "aggregate_id" = $1::uuid AND "tenant_id" <> $2::uuid`,
|
|
196
|
+
[aggregateId, targetTenantId],
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return events.length;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Derived state of a stream-tenant backfill: search docs in the buckets the streams
|
|
2
|
+
// left, projection rows and the index on SYSTEM_TENANT_ID. The stream move itself lives
|
|
3
|
+
// in stream-tenant-backfill.ts; this part is repeatable and safe to run without moves.
|
|
4
|
+
|
|
5
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
6
|
+
import { type Registry, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import { rebuildProjection } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
8
|
+
import { reindexEntity, type SearchAdapter } from "@cosmicdrift/kumiko-framework/search";
|
|
9
|
+
import type { StreamBackfillCandidate, StreamBackfillFailure } from "./stream-tenant-backfill.ts";
|
|
10
|
+
|
|
11
|
+
export type StreamBackfillStepFailure = StreamBackfillFailure & { readonly step: string };
|
|
12
|
+
|
|
13
|
+
// "if-moved" skips rebuild and reindex when no stream moved; "always" repairs
|
|
14
|
+
// derived state after a run that moved streams and then crashed.
|
|
15
|
+
export type DerivedStateMode = "if-moved" | "always";
|
|
16
|
+
|
|
17
|
+
export function createSearchBucketCleanup(
|
|
18
|
+
searchAdapter: SearchAdapter | undefined,
|
|
19
|
+
entityType: string,
|
|
20
|
+
): {
|
|
21
|
+
readonly beforeMove: (candidate: StreamBackfillCandidate) => Promise<void>;
|
|
22
|
+
readonly cleanedAggregates: () => number;
|
|
23
|
+
} {
|
|
24
|
+
let cleaned = 0;
|
|
25
|
+
return {
|
|
26
|
+
cleanedAggregates: () => cleaned,
|
|
27
|
+
beforeMove: async (candidate) => {
|
|
28
|
+
// skip: no adapter means no search docs to clean
|
|
29
|
+
if (!searchAdapter) return;
|
|
30
|
+
const oldBuckets = candidate.sourceTenantIds.filter((id) => id !== candidate.targetTenantId);
|
|
31
|
+
for (const bucket of oldBuckets) {
|
|
32
|
+
if (searchAdapter.removeBatch) {
|
|
33
|
+
await searchAdapter.removeBatch(bucket, [
|
|
34
|
+
{ entityType, entityId: candidate.aggregateId },
|
|
35
|
+
]);
|
|
36
|
+
} else {
|
|
37
|
+
await searchAdapter.remove(bucket, entityType, candidate.aggregateId);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (oldBuckets.length > 0) cleaned++;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type DerivedStateResult = {
|
|
46
|
+
readonly projectionsRebuilt: readonly string[];
|
|
47
|
+
readonly reindexedRows: number;
|
|
48
|
+
readonly failures: readonly StreamBackfillStepFailure[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export async function rebuildAndReindex(deps: {
|
|
52
|
+
readonly db: DbConnection;
|
|
53
|
+
readonly registry: Registry;
|
|
54
|
+
readonly searchAdapter?: SearchAdapter;
|
|
55
|
+
readonly projections: readonly string[];
|
|
56
|
+
readonly searchEntity: string;
|
|
57
|
+
}): Promise<DerivedStateResult> {
|
|
58
|
+
const { db, registry, searchAdapter, projections, searchEntity } = deps;
|
|
59
|
+
const failures: StreamBackfillStepFailure[] = [];
|
|
60
|
+
const projectionsRebuilt: string[] = [];
|
|
61
|
+
for (const projection of projections) {
|
|
62
|
+
try {
|
|
63
|
+
await rebuildProjection(projection, { db, registry });
|
|
64
|
+
projectionsRebuilt.push(projection);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
failures.push({ aggregateId: projection, step: "rebuild", error: messageOf(e) });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let reindexedRows = 0;
|
|
71
|
+
if (searchAdapter && isEntitySearchable(registry, searchEntity)) {
|
|
72
|
+
try {
|
|
73
|
+
const reindex = await reindexEntity(
|
|
74
|
+
db,
|
|
75
|
+
registry,
|
|
76
|
+
searchAdapter,
|
|
77
|
+
searchEntity,
|
|
78
|
+
SYSTEM_TENANT_ID,
|
|
79
|
+
);
|
|
80
|
+
reindexedRows = reindex.indexedRows;
|
|
81
|
+
for (const f of reindex.failures) {
|
|
82
|
+
failures.push({ aggregateId: f.entityId, step: "reindex", error: f.reason });
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
failures.push({ aggregateId: searchEntity, step: "reindex", error: messageOf(e) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { projectionsRebuilt, reindexedRows, failures };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isEntitySearchable(registry: Registry, entityName: string): boolean {
|
|
92
|
+
return (
|
|
93
|
+
registry.getSearchableFields(entityName).length > 0 ||
|
|
94
|
+
registry.getSearchPayloadExtensions(entityName).length > 0
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function stepFailures(
|
|
99
|
+
step: string,
|
|
100
|
+
failed: readonly StreamBackfillFailure[],
|
|
101
|
+
): StreamBackfillStepFailure[] {
|
|
102
|
+
return failed.map((f) => ({ ...f, step }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function messageOf(e: unknown): string {
|
|
106
|
+
return e instanceof Error ? e.message : String(e);
|
|
107
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.389.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Mail steps do not retry permanent rejections or throws",
|
|
6
|
+
"detail": "MailDispatchResult failures take an optional smtpCode; 5xx is permanent and recorded after the first attempt. A throwing mail runner is not retried, since the message may already be sent. A mail runner that wants a retry after a transport error should return a failure without smtpCode instead of throwing."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.388.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -54,9 +54,17 @@ type DeliveryResult =
|
|
|
54
54
|
| { readonly ok: true; readonly status: number }
|
|
55
55
|
| { readonly ok: false; readonly error: string; readonly retryable: boolean };
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
const SMTP_PERMANENT_REJECTION_MIN = 500;
|
|
58
|
+
const SMTP_PERMANENT_REJECTION_MAX = 599;
|
|
59
|
+
|
|
60
|
+
// Without an SMTP code the adapter gave nothing to classify, so the failure counts as transient.
|
|
58
61
|
function mailResultAsDelivery(result: MailDispatchResult): DeliveryResult {
|
|
59
|
-
|
|
62
|
+
if (result.ok) return result;
|
|
63
|
+
const isPermanent =
|
|
64
|
+
result.smtpCode !== undefined &&
|
|
65
|
+
result.smtpCode >= SMTP_PERMANENT_REJECTION_MIN &&
|
|
66
|
+
result.smtpCode <= SMTP_PERMANENT_REJECTION_MAX;
|
|
67
|
+
return { ok: false, error: result.error, retryable: !isPermanent };
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
export type DeliverDispatchRequestArgs = {
|
|
@@ -205,7 +213,13 @@ async function sendDispatch(
|
|
|
205
213
|
aggregateId: target.aggregateId,
|
|
206
214
|
errorName: err instanceof Error ? err.name : "unknown",
|
|
207
215
|
});
|
|
208
|
-
|
|
216
|
+
// A mail adapter has no idempotency key: a throw after the provider accepted the message
|
|
217
|
+
// would send it again, so only the webhook (which carries one) is retried.
|
|
218
|
+
return {
|
|
219
|
+
ok: false,
|
|
220
|
+
error: DISPATCH_THREW_ERROR,
|
|
221
|
+
retryable: dispatchSpec.stepKind === "webhook.send",
|
|
222
|
+
};
|
|
209
223
|
}
|
|
210
224
|
}
|
|
211
225
|
|
|
@@ -94,7 +94,7 @@ export function createStepDispatcherFeature(): FeatureDefinition {
|
|
|
94
94
|
tenantId,
|
|
95
95
|
userId,
|
|
96
96
|
callerFeature: STEP_DISPATCHER_FEATURE,
|
|
97
|
-
...(ctx.secrets && { secrets: ctx.secrets }),
|
|
97
|
+
...(ctx.services.secrets && { secrets: ctx.services.secrets }),
|
|
98
98
|
}),
|
|
99
99
|
});
|
|
100
100
|
},
|
|
@@ -15,7 +15,8 @@ export type MailSpec = z.infer<typeof mailSpecSchema>;
|
|
|
15
15
|
|
|
16
16
|
export type MailDispatchResult =
|
|
17
17
|
| { readonly ok: true; readonly status: number }
|
|
18
|
-
|
|
18
|
+
// smtpCode is the adapter's SMTP reply code; a 5xx is a permanent rejection and is not retried.
|
|
19
|
+
| { readonly ok: false; readonly error: string; readonly smtpCode?: number };
|
|
19
20
|
|
|
20
21
|
let mailRunner: (spec: MailSpec) => Promise<MailDispatchResult> = async () => ({
|
|
21
22
|
ok: false,
|
|
@@ -225,7 +225,7 @@ async function fillPriceCache(
|
|
|
225
225
|
} else {
|
|
226
226
|
const message =
|
|
227
227
|
outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
228
|
-
ctx.log?.warn("subscription-dodo: product lookup failed", { message });
|
|
228
|
+
ctx.obs.log?.warn("subscription-dodo: product lookup failed", { message });
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
}
|
|
@@ -64,11 +64,11 @@ export function createDodoRuntimes(deps: DodoRuntimeDeps): DodoRuntimes {
|
|
|
64
64
|
|
|
65
65
|
async function ctxApiKey(ctx: HandlerContext): Promise<string> {
|
|
66
66
|
let key: string | undefined;
|
|
67
|
-
if (ctx.secrets) {
|
|
68
|
-
const got = await requireSecretsContext(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
);
|
|
67
|
+
if (ctx.services.secrets) {
|
|
68
|
+
const got = await requireSecretsContext(
|
|
69
|
+
{ secrets: ctx.services.secrets, user: ctx.user },
|
|
70
|
+
SUBSCRIPTION_DODO_FEATURE,
|
|
71
|
+
).get(SYSTEM_TENANT_ID, deps.apiKeyHandle.name);
|
|
72
72
|
key = parseStoredSecret(got?.reveal());
|
|
73
73
|
}
|
|
74
74
|
if (!key && deps.fallback.apiKey && deps.fallback.apiKey.length > 0) {
|
|
@@ -102,8 +102,8 @@ export function createDodoRuntimes(deps: DodoRuntimeDeps): DodoRuntimes {
|
|
|
102
102
|
if (live !== true) return false;
|
|
103
103
|
if (deps.fallback.apiKey !== undefined && deps.fallback.apiKey.length > 0) return true;
|
|
104
104
|
// Existence-only probe on every billing-plans render: must stay un-audited.
|
|
105
|
-
return ctx.secrets
|
|
106
|
-
? await ctx.secrets.has(SYSTEM_TENANT_ID, deps.apiKeyHandle.name)
|
|
105
|
+
return ctx.services.secrets
|
|
106
|
+
? await ctx.services.secrets.has(SYSTEM_TENANT_ID, deps.apiKeyHandle.name)
|
|
107
107
|
: false;
|
|
108
108
|
},
|
|
109
109
|
},
|
|
@@ -169,7 +169,9 @@ export function createStripeCheckoutSession(
|
|
|
169
169
|
if (!options.providerCustomerId || !isUnknownCustomerStripeError(error)) throw error;
|
|
170
170
|
// Stripe account switch (test -> live) or a customer deleted in the dashboard leaves a
|
|
171
171
|
// stored customer id Stripe no longer knows; checkout creates a fresh customer instead.
|
|
172
|
-
ctx.log?.warn(
|
|
172
|
+
ctx.obs.log?.warn(
|
|
173
|
+
"subscription-stripe: stored customer unknown to Stripe, retrying without it",
|
|
174
|
+
);
|
|
173
175
|
session = await stripe.checkout.sessions.create(sessionParams(undefined));
|
|
174
176
|
}
|
|
175
177
|
|
|
@@ -383,7 +385,7 @@ async function resolvePricesViaCache(
|
|
|
383
385
|
} else {
|
|
384
386
|
const message =
|
|
385
387
|
outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
386
|
-
ctx.log?.warn("subscription-stripe: prices.retrieve failed", { message });
|
|
388
|
+
ctx.obs.log?.warn("subscription-stripe: prices.retrieve failed", { message });
|
|
387
389
|
}
|
|
388
390
|
}
|
|
389
391
|
}
|
|
@@ -402,7 +404,7 @@ export function createStripeRetrievePrices(runtime: StripeCtxRuntime, cache: Str
|
|
|
402
404
|
const rawInterval = price.recurring?.interval;
|
|
403
405
|
if (rawInterval && mapped.interval === null && !warnedUnknownIntervalPriceIds.has(priceId)) {
|
|
404
406
|
warnedUnknownIntervalPriceIds.add(priceId);
|
|
405
|
-
ctx.log?.warn("subscription-stripe: price has an unknown recurring interval", {
|
|
407
|
+
ctx.obs.log?.warn("subscription-stripe: price has an unknown recurring interval", {
|
|
406
408
|
priceId,
|
|
407
409
|
interval: rawInterval,
|
|
408
410
|
});
|