@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
|
@@ -201,8 +201,8 @@ export const downloadByTokenQuery = defineQueryHandler({
|
|
|
201
201
|
// has none under resolverTrust: "authoritative").
|
|
202
202
|
const provider = await makeTenantStorageProviderResolver({
|
|
203
203
|
registry: ctx.registry,
|
|
204
|
-
configResolver: ctx.configResolver,
|
|
205
|
-
secrets: ctx.secrets,
|
|
204
|
+
configResolver: ctx.services.configResolver,
|
|
205
|
+
secrets: ctx.services.secrets,
|
|
206
206
|
// @cast-boundary db-runner — helpers use only the query API that
|
|
207
207
|
// DbConnection and DbTx share.
|
|
208
208
|
db: runner as import("@cosmicdrift/kumiko-types/db-connection").DbConnection,
|
|
@@ -50,7 +50,7 @@ export function createRequestDeletionHandler(opts: RequestDeletionOptions = {})
|
|
|
50
50
|
escapeHatch: {
|
|
51
51
|
grants: ["systemIdentity", "unsafeRaw"],
|
|
52
52
|
reason:
|
|
53
|
-
"resolveGracePeriod reads the tenant compliance profile via ctx.queryAs(SYSTEM, " +
|
|
53
|
+
"resolveGracePeriod reads the tenant compliance profile via ctx.dispatch.queryAs(SYSTEM, " +
|
|
54
54
|
"...) to compute the grace period end — the calling user's own identity has no read " +
|
|
55
55
|
"access to that tenant-config projection. startDeletionGracePeriod also appends the " +
|
|
56
56
|
"user lifecycle event on the SYSTEM_TENANT_ID user stream.",
|
|
@@ -12,7 +12,7 @@ import { updateUserLifecycle } from "../lib/update-user-lifecycle.ts";
|
|
|
12
12
|
|
|
13
13
|
// POST /api/user/restrict — GDPR Art. 18 account freeze.
|
|
14
14
|
// Flips status=Active → Restricted and revokes all live sessions
|
|
15
|
-
// for the user via cross-feature ctx.writeAs(sessions.revokeAllForUser).
|
|
15
|
+
// for the user via cross-feature ctx.dispatch.writeAs(sessions.revokeAllForUser).
|
|
16
16
|
//
|
|
17
17
|
// Write API behaviour:
|
|
18
18
|
// - Login is blocked: login.write.ts checks status=Restricted (Atom 3).
|
|
@@ -53,7 +53,7 @@ export const restrictAccountWrite = defineWriteHandler({
|
|
|
53
53
|
},
|
|
54
54
|
escapeHatch: {
|
|
55
55
|
reason:
|
|
56
|
-
"Revokes all live sessions of the (possibly self-)restricted user via ctx.writeAs(SYSTEM, " +
|
|
56
|
+
"Revokes all live sessions of the (possibly self-)restricted user via ctx.dispatch.writeAs(SYSTEM, " +
|
|
57
57
|
"sessions:write:user-session:revoke-all-for-user) — that handler's own access gate needs " +
|
|
58
58
|
"privileged roles the acting caller doesn't necessarily have. It also checks the target " +
|
|
59
59
|
"user's membership in the admin's tenant and appends the user lifecycle status change on " +
|
|
@@ -112,10 +112,10 @@ export const restrictAccountWrite = defineWriteHandler({
|
|
|
112
112
|
|
|
113
113
|
// Cross-feature: all live sessions are revoked; otherwise the user could
|
|
114
114
|
// continue writing with an existing JWT until token expiry.
|
|
115
|
-
// ctx.writeAs(systemUser, ...) gives the privileged handler the
|
|
115
|
+
// ctx.dispatch.writeAs(systemUser, ...) gives the privileged handler the
|
|
116
116
|
// System user roles required by its access gate.
|
|
117
117
|
const systemUser = createSystemUser(event.user.tenantId);
|
|
118
|
-
await ctx.writeAs(systemUser, "sessions:write:user-session:revoke-all-for-user", {
|
|
118
|
+
await ctx.dispatch.writeAs(systemUser, "sessions:write:user-session:revoke-all-for-user", {
|
|
119
119
|
userId: targetUserId,
|
|
120
120
|
});
|
|
121
121
|
|
|
@@ -17,7 +17,11 @@ import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
|
17
17
|
import * as z from "zod";
|
|
18
18
|
import { resolveAppTenantModel } from "../lib/resolve-tenant-model.ts";
|
|
19
19
|
import { makeTenantStorageProviderResolver } from "../lib/storage-provider-resolver.ts";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
FORGET_CLEANUP_HANDLER_NAME,
|
|
22
|
+
runForgetCleanup,
|
|
23
|
+
type SendDeletionExecutedEmailFn,
|
|
24
|
+
} from "../run-forget-cleanup.ts";
|
|
21
25
|
|
|
22
26
|
export type RunForgetCleanupOptions = {
|
|
23
27
|
readonly sendDeletionExecutedEmail?: SendDeletionExecutedEmailFn;
|
|
@@ -56,7 +60,7 @@ export function createRunForgetCleanupHandler(opts: RunForgetCleanupOptions = {}
|
|
|
56
60
|
const forgetDb = runner as DbConnection; // @cast-boundary db-runner — DbRunner narrows to DbConnection, config reads tolerate the outer tx
|
|
57
61
|
const tenantModel = await resolveAppTenantModel({
|
|
58
62
|
registry: ctx.registry,
|
|
59
|
-
configResolver: ctx.configResolver,
|
|
63
|
+
configResolver: ctx.services.configResolver,
|
|
60
64
|
db: forgetDb,
|
|
61
65
|
userId: ctx.user.id,
|
|
62
66
|
});
|
|
@@ -65,14 +69,16 @@ export function createRunForgetCleanupHandler(opts: RunForgetCleanupOptions = {}
|
|
|
65
69
|
registry: ctx.registry,
|
|
66
70
|
now: T.Now.instant(),
|
|
67
71
|
tenantModel,
|
|
68
|
-
...(ctx.searchAdapter !== undefined && {
|
|
72
|
+
...(ctx.services.searchAdapter !== undefined && {
|
|
73
|
+
searchAdapter: ctx.services.searchAdapter,
|
|
74
|
+
}),
|
|
69
75
|
buildStorageProvider: makeTenantStorageProviderResolver({
|
|
70
76
|
registry: ctx.registry,
|
|
71
|
-
configResolver: ctx.configResolver,
|
|
72
|
-
secrets: ctx.secrets,
|
|
77
|
+
configResolver: ctx.services.configResolver,
|
|
78
|
+
secrets: ctx.services.secrets,
|
|
73
79
|
db: forgetDb,
|
|
74
80
|
userId: ctx.user.id,
|
|
75
|
-
handlerName:
|
|
81
|
+
handlerName: FORGET_CLEANUP_HANDLER_NAME,
|
|
76
82
|
}),
|
|
77
83
|
...(opts.sendDeletionExecutedEmail && {
|
|
78
84
|
sendDeletionExecutedEmail: opts.sendDeletionExecutedEmail,
|
|
@@ -31,4 +31,11 @@ export {
|
|
|
31
31
|
exportJobEntity,
|
|
32
32
|
exportJobsTable,
|
|
33
33
|
} from "./schema/export-job.ts";
|
|
34
|
-
export {
|
|
34
|
+
export {
|
|
35
|
+
TENANT_MODEL_CONFIG_KEY,
|
|
36
|
+
USER_DATA_RIGHTS_FEATURE,
|
|
37
|
+
UserDataRightsHandlers,
|
|
38
|
+
UserDataRightsJobs,
|
|
39
|
+
UserDataRightsQueries,
|
|
40
|
+
UserDataRightsScreens,
|
|
41
|
+
} from "./web/constants.ts";
|
|
@@ -31,7 +31,7 @@ export function makeTenantMailTransportResolver(
|
|
|
31
31
|
return async (tenantId) => {
|
|
32
32
|
if (!ctx.configResolver) {
|
|
33
33
|
throw new Error(
|
|
34
|
-
`${ctx.handlerName}: ctx.configResolver missing — cannot resolve the mail transport for tenant ${tenantId}`,
|
|
34
|
+
`${ctx.handlerName}: ctx.services.configResolver missing — cannot resolve the mail transport for tenant ${tenantId}`,
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
const config = createConfigAccessor(
|
|
@@ -30,7 +30,7 @@ export function makeTenantStorageProviderResolver(
|
|
|
30
30
|
return async (tenantId) => {
|
|
31
31
|
if (!ctx.configResolver) {
|
|
32
32
|
throw new InternalError({
|
|
33
|
-
message: `[${ctx.handlerName}] ctx.configResolver missing — cannot resolve the file provider for tenant ${tenantId}`,
|
|
33
|
+
message: `[${ctx.handlerName}] ctx.services.configResolver missing — cannot resolve the file provider for tenant ${tenantId}`,
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
const config = createConfigAccessor(
|
|
@@ -62,7 +62,12 @@ export async function updateUserLifecycle(
|
|
|
62
62
|
// (restrict/lift-restriction/grace-period/cancel/forget) that holds no row
|
|
63
63
|
// version to pass as `payload.version`, which update() requires otherwise.
|
|
64
64
|
// Callers needing a guard use `options.expect` instead.
|
|
65
|
-
const tenantDb = createTenantDb({
|
|
65
|
+
const tenantDb = createTenantDb({
|
|
66
|
+
db: conn,
|
|
67
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
68
|
+
mode: "system",
|
|
69
|
+
reason: "User is tenant-agnostic, lifecycle transitions run in the system tenant",
|
|
70
|
+
});
|
|
66
71
|
const result = await userExecutor.update(
|
|
67
72
|
{ id: userId, changes },
|
|
68
73
|
createSystemUser(SYSTEM_TENANT_ID),
|
|
@@ -54,7 +54,7 @@ import { generateDownloadToken } from "./token-helpers.ts";
|
|
|
54
54
|
type Instant = InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
|
|
55
55
|
|
|
56
56
|
// Gauge short name (feature.ts registers it via r.metric + resolves ctx.meter
|
|
57
|
-
// into a MetricsHandle — jobs get no bound ctx.metrics, see feature.ts).
|
|
57
|
+
// into a MetricsHandle — jobs get no bound ctx.obs.metrics, see feature.ts).
|
|
58
58
|
// No "_seconds"/"_total" suffix: validateMetricName rejects both on a gauge.
|
|
59
59
|
// Unit lives in the r.metric() `unit: "seconds"` declaration instead.
|
|
60
60
|
export const EXPORT_CLEANUP_BACKLOG_AGE_METRIC = "export_cleanup_backlog_age";
|
|
@@ -727,7 +727,13 @@ function reportCleanupLastRun(metrics: MetricsHandle | undefined, now: Instant):
|
|
|
727
727
|
// `requestedFromTenantId` preserves stream identity and keeps the stream counter consistent.
|
|
728
728
|
// @wrapper-known semantic-alias
|
|
729
729
|
function systemTenantDb(db: DbConnection, tenantId: TenantId) {
|
|
730
|
-
return createTenantDb({
|
|
730
|
+
return createTenantDb({
|
|
731
|
+
db,
|
|
732
|
+
tenantId,
|
|
733
|
+
mode: "system",
|
|
734
|
+
reason:
|
|
735
|
+
"export jobs run per user across tenants, requestedFromTenantId keeps the stream identity",
|
|
736
|
+
});
|
|
731
737
|
}
|
|
732
738
|
|
|
733
739
|
function buildExportStorageKey(job: JobRow): string {
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
type UserDataDeleteStrategy,
|
|
57
57
|
type UserDataStorageProvider,
|
|
58
58
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
59
|
+
import { runInSystemRowLevelSecurityScope } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
|
|
59
60
|
import {
|
|
60
61
|
createEscapeHatchReporter,
|
|
61
62
|
UNATTRIBUTED_ACTOR,
|
|
@@ -243,13 +244,23 @@ const HOOK_ORDER_DEFAULT = EXT_USER_DATA_ORDER.DEFAULT;
|
|
|
243
244
|
// console.warn so a crafted reason can't forge extra log lines, and cap the
|
|
244
245
|
// length against a log-flood. The unsanitized reason still lands in the
|
|
245
246
|
// returned `incomplete` entry untouched.
|
|
247
|
+
// Secret-read audit discriminator, not a QN; the value must stay stable for the audit trail.
|
|
248
|
+
export const FORGET_CLEANUP_HANDLER_NAME = "user-data-rights:run-forget-cleanup" as const;
|
|
249
|
+
|
|
246
250
|
export function sanitizeReasonForLog(reason: string): string {
|
|
247
251
|
return reason.replace(/[\r\n]+/g, " ").slice(0, 500);
|
|
248
252
|
}
|
|
249
253
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
): Promise<RunForgetCleanupResult> {
|
|
254
|
+
// Cross-tenant by design: the due-user and membership reads on the given runner and raw hook
|
|
255
|
+
// statements must see every tenant, whatever RLS scope the caller (job, operator handler) runs in.
|
|
256
|
+
export function runForgetCleanup(args: RunForgetCleanupArgs): Promise<RunForgetCleanupResult> {
|
|
257
|
+
return runInSystemRowLevelSecurityScope(
|
|
258
|
+
"forget cleanup anonymizes users across all tenants",
|
|
259
|
+
() => forgetDueUsers(args),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function forgetDueUsers(args: RunForgetCleanupArgs): Promise<RunForgetCleanupResult> {
|
|
253
264
|
const { db, registry, now, sendDeletionExecutedEmail, buildStorageProvider } = args;
|
|
254
265
|
const kms = args.kms ?? configuredPiiSubjectKms();
|
|
255
266
|
const appTenantModel: TenantUserModel = args.tenantModel ?? "multi-user";
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
type UserDataExportHook,
|
|
44
44
|
type UserDataExportSnippet,
|
|
45
45
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
46
|
+
import { runInSystemRowLevelSecurityScope } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
|
|
46
47
|
import {
|
|
47
48
|
createEscapeHatchReporter,
|
|
48
49
|
UNATTRIBUTED_ACTOR,
|
|
@@ -163,7 +164,16 @@ function buildHookDb(
|
|
|
163
164
|
* **Operator signal:** if bundle.json exceeds 100 MB in the ZIP, trigger
|
|
164
165
|
* telemetry and evaluate a schema refactor.
|
|
165
166
|
*/
|
|
166
|
-
|
|
167
|
+
// Cross-tenant by design: the membership read on the given runner and raw hook statements
|
|
168
|
+
// must see every tenant of the user, whatever RLS scope the caller runs in.
|
|
169
|
+
export function runUserExport(args: RunUserExportArgs): Promise<UserExportBundle> {
|
|
170
|
+
return runInSystemRowLevelSecurityScope(
|
|
171
|
+
"user data export collects the subject's rows from all tenants",
|
|
172
|
+
() => exportUserData(args),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function exportUserData(args: RunUserExportArgs): Promise<UserExportBundle> {
|
|
167
177
|
const { db, registry, userId, now } = args;
|
|
168
178
|
|
|
169
179
|
const memberships = await selectMany<{ tenantId: TenantId }>(db, tenantMembershipsTable, {
|
|
@@ -27,6 +27,8 @@ export const UserDataRightsQueries = {
|
|
|
27
27
|
exportStatus: "user-data-rights:query:export-status",
|
|
28
28
|
myAuditLog: "user-data-rights:query:my-audit-log",
|
|
29
29
|
downloadByJob: "user-data-rights:query:download-by-job",
|
|
30
|
+
downloadByToken: "user-data-rights:query:download-by-token",
|
|
31
|
+
listDownloadAttempts: "user-data-rights:query:list-download-attempts",
|
|
30
32
|
} as const;
|
|
31
33
|
|
|
32
34
|
// App-level config: tenant-occupancy model. "single-user" tells the forget
|
|
@@ -36,11 +38,25 @@ export const UserDataRightsQueries = {
|
|
|
36
38
|
// `overrides.set(TENANT_MODEL_CONFIG_KEY, "single-user")`.
|
|
37
39
|
export const TENANT_MODEL_CONFIG_KEY = "user-data-rights:config:tenant-model" as const;
|
|
38
40
|
|
|
41
|
+
export const UserDataRightsJobs = {
|
|
42
|
+
runExportJobs: "user-data-rights:job:run-export-jobs",
|
|
43
|
+
runForgetCleanup: "user-data-rights:job:run-forget-cleanup",
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
export const UserDataRightsScreens = {
|
|
47
|
+
exportJobList: "user-data-rights:screen:export-job-list",
|
|
48
|
+
downloadAttemptList: "user-data-rights:screen:download-attempt-list",
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
39
51
|
export const UserDataRightsHandlers = {
|
|
40
52
|
requestExport: "user-data-rights:write:request-export",
|
|
41
53
|
requestDeletion: "user-data-rights:write:request-deletion",
|
|
54
|
+
requestDeletionByEmail: "user-data-rights:write:request-deletion-by-email",
|
|
55
|
+
confirmDeletionByToken: "user-data-rights:write:confirm-deletion-by-token",
|
|
42
56
|
cancelDeletion: "user-data-rights:write:cancel-deletion",
|
|
43
57
|
restrictAccount: "user-data-rights:write:restrict-account",
|
|
58
|
+
liftRestriction: "user-data-rights:write:lift-restriction",
|
|
59
|
+
runForgetCleanup: "user-data-rights:write:run-forget-cleanup",
|
|
44
60
|
} as const;
|
|
45
61
|
|
|
46
62
|
// Foreign QN: the lifecycle status (active / deletionRequested / restricted)
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
export { type UserDataRightsClientOptions, userDataRightsClient } from "./client-plugin.tsx";
|
|
8
8
|
export type { ConfirmAccountDeletionScreenProps } from "./confirm-deletion-screen.tsx";
|
|
9
9
|
export { ConfirmAccountDeletionScreen } from "./confirm-deletion-screen.tsx";
|
|
10
|
+
export {
|
|
11
|
+
UserDataRightsHandlers,
|
|
12
|
+
UserDataRightsJobs,
|
|
13
|
+
UserDataRightsQueries,
|
|
14
|
+
UserDataRightsScreens,
|
|
15
|
+
} from "./constants.ts";
|
|
10
16
|
export { defaultTranslations } from "./default-translations.ts";
|
|
11
17
|
export { formatDate } from "./privacy-center-screen.tsx";
|
|
12
18
|
export { makePublicDeletionGate, type PublicDeletionRoutes } from "./public-deletion-gate.tsx";
|
|
@@ -24,7 +24,7 @@ function invalidCredentials() {
|
|
|
24
24
|
|
|
25
25
|
// Change-email — authenticated, self-only. The user confirms with their
|
|
26
26
|
// current password (re-authentication as with change-password); the new
|
|
27
|
-
// address is written through the user-update handler via ctx.writeAs(system)
|
|
27
|
+
// address is written through the user-update handler via ctx.dispatch.writeAs(system)
|
|
28
28
|
// because `email` is field-level privileged and would otherwise be silently
|
|
29
29
|
// stripped during self-updates. emailVerified is set to false — the app then
|
|
30
30
|
// triggers the verification flow (request-email-verification is public; the
|
|
@@ -45,8 +45,8 @@ export const changeEmailWrite = defineWriteHandler({
|
|
|
45
45
|
escapeHatch: {
|
|
46
46
|
grants: ["systemIdentity"],
|
|
47
47
|
reason:
|
|
48
|
-
"Reads the caller's own passwordHash via ctx.queryAs(SYSTEM, user:findForAuth), checks " +
|
|
49
|
-
"email uniqueness the same way, and writes the new email via ctx.writeAs(SYSTEM, " +
|
|
48
|
+
"Reads the caller's own passwordHash via ctx.dispatch.queryAs(SYSTEM, user:findForAuth), checks " +
|
|
49
|
+
"email uniqueness the same way, and writes the new email via ctx.dispatch.writeAs(SYSTEM, " +
|
|
50
50
|
"user:update) — field-access on passwordHash/email is privileged-only.",
|
|
51
51
|
},
|
|
52
52
|
// Payload carries the current password.
|
|
@@ -56,7 +56,7 @@ export const changeEmailWrite = defineWriteHandler({
|
|
|
56
56
|
handler: async (event, ctx) => {
|
|
57
57
|
const systemUser = createSystemUser(event.user.tenantId);
|
|
58
58
|
|
|
59
|
-
const me = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
59
|
+
const me = (await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
|
|
60
60
|
id: event.user.id,
|
|
61
61
|
})) as { id: string; email: string; passwordHash: string | null; version: number } | null; // @cast-boundary db-runner
|
|
62
62
|
|
|
@@ -79,7 +79,9 @@ export const changeEmailWrite = defineWriteHandler({
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const existing = await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
82
|
+
const existing = await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
|
|
83
|
+
email: newEmail,
|
|
84
|
+
});
|
|
83
85
|
if (existing !== null) {
|
|
84
86
|
return writeFailure(
|
|
85
87
|
new UnprocessableError(UserErrors.emailAlreadyExists, {
|
|
@@ -93,7 +95,7 @@ export const changeEmailWrite = defineWriteHandler({
|
|
|
93
95
|
// backfillUserStreamTenants.
|
|
94
96
|
const writer = createSystemUser(SYSTEM_TENANT_ID);
|
|
95
97
|
|
|
96
|
-
const writeRes = await ctx.writeAs(writer, UserHandlers.update, {
|
|
98
|
+
const writeRes = await ctx.dispatch.writeAs(writer, UserHandlers.update, {
|
|
97
99
|
id: me.id,
|
|
98
100
|
version: me.version,
|
|
99
101
|
changes: { email: newEmail, emailVerified: false },
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.389.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "A failing notifyRecipient SQL statement no longer aborts the submit",
|
|
6
|
+
"detail": "The resolver receives a ctx whose systemDb runs in a savepoint, so a failed statement is rolled back and the submit answers 200 with the entry stored. Only ctx.systemDb is savepoint-scoped; other ctx handles stay in the handler transaction."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.389.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "waitlist purge retry after a failed shred is covered",
|
|
12
|
+
"detail": "An integration test fails the second key shred of a purge, checks that the entries stay and no audit event is written, and that the retried purge erases every entry and records its audit event. No runtime change."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.389.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "Waitlist import enforces the live field limits",
|
|
18
|
+
"detail": "migrateLegacyWaitlist counts a source row with a value above the live field limit as unreadable instead of importing it; importWaitlistEntry throws for it."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.388.0",
|
|
4
22
|
"type": "fix",
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
|
|
33
33
|
export function createWaitlistFeature(opts: WaitlistOptions = {}): FeatureDefinition {
|
|
34
34
|
return defineFeature(WAITLIST_FEATURE, (r) => {
|
|
35
|
+
r.adoptsBundledLists();
|
|
35
36
|
r.describe(
|
|
36
37
|
"Public waitlist: an anonymous, rate-limited `submit` handler collects name, email and optional company, portfolio and message (a honeypot field and per-email dedupe keep bots and repeat submits out, and the response body is identical whether or not an address was already listed), and mails the submitter a confirmation. SystemAdmins review entries in a list screen and invite or reject them; an invite creates the entrant's own tenant (or joins one shared tenant) and mails an accept link with the configured membership role, never a global role. Entries are platform-wide, their personal data is crypto-shredding-ready, and the EXT_USER_DATA hooks export and erase entries matching a user's email.",
|
|
37
38
|
);
|
|
@@ -21,7 +21,7 @@ const PurgeSchema = z.object({ confirmCount: z.number().int().min(0) }).strict()
|
|
|
21
21
|
const ExportSchema = z.object({}).strict();
|
|
22
22
|
|
|
23
23
|
async function appendAudit(ctx: HandlerContext, type: string, count: number): Promise<void> {
|
|
24
|
-
await ctx.
|
|
24
|
+
await ctx.events.unsafeAppend({
|
|
25
25
|
aggregateId: generateId(),
|
|
26
26
|
aggregateType: WAITLIST_AUDIT_AGGREGATE_TYPE,
|
|
27
27
|
type,
|
|
@@ -111,7 +111,7 @@ async function ensureOwnTenant(
|
|
|
111
111
|
|
|
112
112
|
const newTenantId = parseTenantId(crypto.randomUUID());
|
|
113
113
|
if (!newTenantId) throw new InternalError({ message: "waitlist:invite: tenant id mint failed" });
|
|
114
|
-
const created = await ctx.writeAs(createSystemUser(newTenantId), TenantHandlers.create, {
|
|
114
|
+
const created = await ctx.dispatch.writeAs(createSystemUser(newTenantId), TenantHandlers.create, {
|
|
115
115
|
id: newTenantId,
|
|
116
116
|
key,
|
|
117
117
|
name: entry.company ?? entry.name,
|
|
@@ -167,7 +167,7 @@ export function createInviteHandler(invite: WaitlistInviteOptions = DEFAULT_INVI
|
|
|
167
167
|
// inviteCreate is admin-only (no system role); the system variant with an
|
|
168
168
|
// empty globalRoles list is the only in-process path, and a waitlist
|
|
169
169
|
// invite must never grant a global role.
|
|
170
|
-
const invited = await ctx.writeAs(
|
|
170
|
+
const invited = await ctx.dispatch.writeAs(
|
|
171
171
|
createSystemUser(target.tenantId),
|
|
172
172
|
AuthHandlers.systemInviteCreate,
|
|
173
173
|
{
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
acquireNamespacedAdvisoryLock,
|
|
3
|
+
createSystemDbView,
|
|
4
|
+
createTenantDb,
|
|
5
|
+
runInSavepointIfSupported,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
2
7
|
import {
|
|
3
8
|
access,
|
|
4
9
|
defineWriteHandler,
|
|
5
10
|
type HandlerContext,
|
|
11
|
+
type TenantId,
|
|
6
12
|
type WriteResult,
|
|
7
13
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
14
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
@@ -25,7 +31,7 @@ import { WAITLIST_NOTIFICATION_TYPES, WAITLIST_STATUS } from "../web/constants.t
|
|
|
25
31
|
// framework's other fixed advisory-lock keys.
|
|
26
32
|
const SUBMIT_LOCK_NAMESPACE = 0x776c7374;
|
|
27
33
|
const SUBMIT_LOCK_REASON =
|
|
28
|
-
"takes the per-email advisory lock so parallel submits cannot both pass the duplicate check; TenantDb has
|
|
34
|
+
"takes the per-email advisory lock so parallel submits cannot both pass the duplicate check and wraps the admin-recipient resolver in a savepoint so a failing statement cannot abort the submit transaction; TenantDb has neither API";
|
|
29
35
|
|
|
30
36
|
// Logged-in callers carry no anonymous role, so every signed-in role must be listed.
|
|
31
37
|
const SUBMIT_ROLES = access.roles(
|
|
@@ -73,18 +79,31 @@ async function hasOpenEntryForEmail(email: string, ctx: HandlerContext): Promise
|
|
|
73
79
|
return found.rows.length > 0;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
// The resolver usually reads config. It runs before the per-email advisory lock so a
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
82
|
+
// The resolver usually reads config. It runs before the per-email advisory lock so a slow
|
|
83
|
+
// resolver does not extend the lock. Postgres aborts the whole transaction after a failed
|
|
84
|
+
// statement, even one whose JS error is caught here, so the resolver gets a ctx whose
|
|
85
|
+
// systemDb is bound to its own savepoint: a failing statement rolls back only that.
|
|
86
|
+
// Statements through any other ctx handle (ctx.dispatch.query, ctx.dispatch.write) stay in the handler transaction.
|
|
80
87
|
async function resolveAdminRecipient(
|
|
81
88
|
opts: WaitlistOptions,
|
|
82
89
|
ctx: HandlerContext,
|
|
90
|
+
systemDb: NonNullable<HandlerContext["systemDb"]>,
|
|
91
|
+
tenantId: TenantId,
|
|
83
92
|
): Promise<string | null> {
|
|
84
93
|
try {
|
|
85
|
-
return
|
|
94
|
+
return await runInSavepointIfSupported(systemDb.unsafeRaw(SUBMIT_LOCK_REASON), async (sp) => {
|
|
95
|
+
const savepointSystemDb = createSystemDbView(
|
|
96
|
+
createTenantDb({
|
|
97
|
+
db: sp,
|
|
98
|
+
tenantId,
|
|
99
|
+
mode: "system",
|
|
100
|
+
reason: "waitlist entries are system-tenant rows submitted by an anonymous guest",
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
return (await opts.notifyRecipient?.({ ...ctx, systemDb: savepointSystemDb })) || null;
|
|
104
|
+
});
|
|
86
105
|
} catch (err) {
|
|
87
|
-
ctx.log?.warn("[waitlist:submit] admin notice recipient resolution failed", {
|
|
106
|
+
ctx.obs.log?.warn("[waitlist:submit] admin notice recipient resolution failed", {
|
|
88
107
|
error: err instanceof Error ? err.message : String(err),
|
|
89
108
|
});
|
|
90
109
|
return null;
|
|
@@ -102,7 +121,7 @@ async function sendBestEffort(
|
|
|
102
121
|
}
|
|
103
122
|
await send(ctx.notify);
|
|
104
123
|
} catch (err) {
|
|
105
|
-
ctx.log?.warn(`[waitlist:submit] ${label} mail failed`, {
|
|
124
|
+
ctx.obs.log?.warn(`[waitlist:submit] ${label} mail failed`, {
|
|
106
125
|
error: err instanceof Error ? err.message : String(err),
|
|
107
126
|
});
|
|
108
127
|
}
|
|
@@ -130,7 +149,12 @@ export function createSubmitHandler(opts: WaitlistOptions) {
|
|
|
130
149
|
message: "waitlist:submit requires ctx.systemDb — is r.systemScope() set?",
|
|
131
150
|
});
|
|
132
151
|
}
|
|
133
|
-
const adminRecipient = await resolveAdminRecipient(
|
|
152
|
+
const adminRecipient = await resolveAdminRecipient(
|
|
153
|
+
opts,
|
|
154
|
+
ctx,
|
|
155
|
+
ctx.systemDb,
|
|
156
|
+
event.user.tenantId,
|
|
157
|
+
);
|
|
134
158
|
await acquireNamespacedAdvisoryLock(
|
|
135
159
|
ctx.systemDb.unsafeRaw(SUBMIT_LOCK_REASON),
|
|
136
160
|
SUBMIT_LOCK_NAMESPACE,
|
|
@@ -3,7 +3,7 @@ import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
|
3
3
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
5
5
|
import { normalizeEmail, platformActor, waitlistExecutor } from "./lib.ts";
|
|
6
|
-
import type
|
|
6
|
+
import { WAITLIST_FIELD_LIMITS, type WaitlistStatus } from "./web/constants.ts";
|
|
7
7
|
|
|
8
8
|
export type WaitlistImportEntry = {
|
|
9
9
|
/** Stable identity of the source row (e.g. `offlot:<uuid>`); the new entry id derives from it. */
|
|
@@ -43,18 +43,38 @@ export function isWaitlistImportId(id: string): boolean {
|
|
|
43
43
|
return id[14] === "5";
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// The live submit schema caps these fields; an imported value above the cap would be
|
|
47
|
+
// stored but could never pass a later update through the same payload schema.
|
|
48
|
+
export function waitlistImportEntryWithinLimits(entry: WaitlistImportEntry): boolean {
|
|
49
|
+
const limited: ReadonlyArray<readonly [string | undefined, number]> = [
|
|
50
|
+
[entry.name, WAITLIST_FIELD_LIMITS.name],
|
|
51
|
+
[entry.email, WAITLIST_FIELD_LIMITS.email],
|
|
52
|
+
[entry.locale, WAITLIST_FIELD_LIMITS.locale],
|
|
53
|
+
[entry.company, WAITLIST_FIELD_LIMITS.company],
|
|
54
|
+
[entry.portfolio, WAITLIST_FIELD_LIMITS.portfolio],
|
|
55
|
+
[entry.message, WAITLIST_FIELD_LIMITS.message],
|
|
56
|
+
];
|
|
57
|
+
return limited.every(([value, max]) => value === undefined || value.length <= max);
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
function presentOnly(fields: Record<string, unknown>): Record<string, unknown> {
|
|
47
61
|
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
48
62
|
}
|
|
49
63
|
|
|
50
|
-
// Writes through the entity executor, so events
|
|
51
|
-
//
|
|
64
|
+
// Writes through the entity executor, so events and projection match a live submit; subject
|
|
65
|
+
// encryption applies only when the caller configured a PII KMS (the CLI wires it from env
|
|
66
|
+
// and aborts without one). No mail, no rate limit, no duplicate-by-email
|
|
52
67
|
// check: the source row is the truth and the id makes the call idempotent.
|
|
53
68
|
export async function importWaitlistEntry(
|
|
54
69
|
db: TenantDb,
|
|
55
70
|
entry: WaitlistImportEntry,
|
|
56
71
|
options: { readonly dryRun?: boolean } = {},
|
|
57
72
|
): Promise<WaitlistImportOutcome> {
|
|
73
|
+
if (!waitlistImportEntryWithinLimits(entry)) {
|
|
74
|
+
throw new InternalError({
|
|
75
|
+
message: `waitlist import entry ${entry.sourceKey} exceeds a field length limit`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
58
78
|
const id = waitlistImportId(entry.sourceKey);
|
|
59
79
|
const existing = await waitlistExecutor.detail({ id }, platformActor(), db);
|
|
60
80
|
if (existing) return "skipped";
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
importWaitlistEntry,
|
|
29
29
|
isWaitlistImportId,
|
|
30
30
|
type WaitlistImportEntry,
|
|
31
|
+
waitlistImportEntryWithinLimits,
|
|
31
32
|
waitlistImportId,
|
|
32
33
|
} from "./import.ts";
|
|
33
34
|
import { WAITLIST_STATUS, type WaitlistStatus } from "./web/constants.ts";
|
|
@@ -265,12 +266,17 @@ export async function migrateLegacyWaitlist(
|
|
|
265
266
|
options.from === "offlot"
|
|
266
267
|
? await mapOfflotRow(row)
|
|
267
268
|
: mapPublicstatusRow(row, options.defaultLocale);
|
|
268
|
-
if (!entry) {
|
|
269
|
+
if (!entry || !waitlistImportEntryWithinLimits(entry)) {
|
|
269
270
|
unreadable += 1;
|
|
270
271
|
continue;
|
|
271
272
|
}
|
|
272
273
|
await transaction(db, async (tx) => {
|
|
273
|
-
const tenantDb = createTenantDb({
|
|
274
|
+
const tenantDb = createTenantDb({
|
|
275
|
+
db: tx,
|
|
276
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
277
|
+
mode: "system",
|
|
278
|
+
reason: "legacy waitlist import writes system-tenant rows",
|
|
279
|
+
});
|
|
274
280
|
const outcome = await importWaitlistEntry(tenantDb, entry, { dryRun: options.dryRun });
|
|
275
281
|
if (outcome === "created") created += 1;
|
|
276
282
|
else skipped += 1;
|
|
@@ -69,7 +69,13 @@ export const linkWaitlistOnSignup: MultiStreamApplyFn = async (event, tx) => {
|
|
|
69
69
|
// skip: another membership came first.
|
|
70
70
|
if (first && first.id !== event.aggregateId) return;
|
|
71
71
|
|
|
72
|
-
const db = createTenantDb({
|
|
72
|
+
const db = createTenantDb({
|
|
73
|
+
db: tx,
|
|
74
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
75
|
+
mode: "system",
|
|
76
|
+
reason:
|
|
77
|
+
"linking a signup to its waitlist entry updates system-tenant rows from a membership event",
|
|
78
|
+
});
|
|
73
79
|
const user = await fetchOne<{ email?: unknown }>(tx, userTable, { id: membership.userId });
|
|
74
80
|
// skip: user row unreadable (erased or not projected yet).
|
|
75
81
|
if (typeof user?.email !== "string") return;
|
|
@@ -22,7 +22,8 @@ export type WaitlistInviteOptions =
|
|
|
22
22
|
export type WaitlistOptions = {
|
|
23
23
|
/** Address for an admin notification mail on each new entry. Null, an empty
|
|
24
24
|
* string or an absent option sends none. Best effort, never fails the submit,
|
|
25
|
-
* also when the resolver throws or rejects.
|
|
25
|
+
* also when the resolver throws or rejects. `ctx.systemDb` is bound to a savepoint, so a
|
|
26
|
+
* failing statement through it is rolled back without aborting the submit transaction. */
|
|
26
27
|
readonly notifyRecipient?: (ctx: HandlerContext) => string | null | Promise<string | null>;
|
|
27
28
|
/** Language of the admin notice mail (en, de or es; anything else falls back to en).
|
|
28
29
|
* Not taken from the request: a submit comes from the entrant, not the operator. */
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.390.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Woken workflow runs are due on the next resume tick",
|
|
6
|
+
"detail": "The event subscriber stamped wakeAt with the app clock, while the resume job selects due rows with the database clock. With an app clock ahead of the database, a woken row was not due yet and waited for a later tick. wakeAt is now set with the database clock, like the due-runs query."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.370.0",
|
|
4
10
|
"type": "breaking",
|
|
@@ -8,7 +8,7 @@ export type DueRow = { readonly runId: string; readonly stepIndex: number };
|
|
|
8
8
|
export const DUE_RUNS_BATCH_SIZE = 100;
|
|
9
9
|
|
|
10
10
|
// Dedup happens solely via resume-run's VersionConflictError-checked claim
|
|
11
|
-
// (ctx.
|
|
11
|
+
// (ctx.events.tryAppend on WORKFLOW_RESUMED) — a second dispatch for the same
|
|
12
12
|
// row loses that race and no-ops. No row locks: in an autocommit statement
|
|
13
13
|
// they would be released right after the SELECT and guard nothing.
|
|
14
14
|
// `now()` stays the DB clock so job-host clock skew cannot mark rows due early.
|