@cosmicdrift/kumiko-samples 0.362.0 → 0.364.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 +23 -6
- package/packages/bundled-features/src/admin-shell/changes.json +6 -0
- package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
- package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
- package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
- package/packages/bundled-features/src/audit/changes.json +6 -0
- package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
- package/packages/bundled-features/src/auth-email-password/changes.json +51 -0
- package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
- package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
- package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
- package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
- package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +5 -13
- package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
- package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
- package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
- package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
- package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
- package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
- package/packages/bundled-features/src/billing-foundation/changes.json +43 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +90 -44
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +118 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +39 -33
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +36 -20
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
- package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
- package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +20 -11
- package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
- package/packages/bundled-features/src/cap-counter/changes.json +6 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
- package/packages/bundled-features/src/cap-counter/index.ts +1 -0
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
- package/packages/bundled-features/src/channel-email/changes.json +19 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +135 -29
- package/packages/bundled-features/src/channel-email/feature.ts +6 -3
- package/packages/bundled-features/src/channel-email/index.ts +7 -1
- package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
- package/packages/bundled-features/src/channel-email/types.ts +23 -0
- package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
- package/packages/bundled-features/src/config/changes.json +6 -0
- package/packages/bundled-features/src/config/handlers/set.write.ts +3 -7
- package/packages/bundled-features/src/config/resolver.ts +15 -12
- package/packages/bundled-features/src/config/write-helpers.ts +23 -0
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
- package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
- package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
- package/packages/bundled-features/src/data-retention/changes.json +6 -0
- package/packages/bundled-features/src/data-retention/index.ts +1 -0
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
- package/packages/bundled-features/src/delivery/attachment-ref.ts +105 -0
- package/packages/bundled-features/src/delivery/changes.json +66 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +152 -4
- package/packages/bundled-features/src/delivery/constants.ts +2 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
- package/packages/bundled-features/src/delivery/feature.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +19 -2
- package/packages/bundled-features/src/delivery/jobs.ts +97 -17
- package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
- package/packages/bundled-features/src/delivery/testing.ts +9 -1
- package/packages/bundled-features/src/delivery/types.ts +34 -18
- package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
- package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
- package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
- package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
- package/packages/bundled-features/src/file-foundation/changes.json +6 -0
- package/packages/bundled-features/src/file-foundation/index.ts +7 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +89 -0
- package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
- package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
- package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
- package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +42 -25
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
- package/packages/bundled-features/src/jobs/changes.json +19 -0
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
- package/packages/bundled-features/src/jobs/feature.ts +7 -4
- package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
- package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
- package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
- package/packages/bundled-features/src/ledger/changes.json +7 -0
- package/packages/bundled-features/src/ledger/entity.ts +3 -1
- package/packages/bundled-features/src/ledger/recurring.ts +1 -3
- package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
- package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
- package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
- package/packages/bundled-features/src/page-render/branding.ts +3 -0
- package/packages/bundled-features/src/page-render/markdown.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
- package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
- package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
- package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
- package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
- package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
- package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
- package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
- package/packages/bundled-features/src/renderer-foundation/types.ts +134 -35
- package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
- package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -3
- package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
- package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/sessions/feature.ts +2 -4
- package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
- package/packages/bundled-features/src/shared/changes.json +6 -0
- package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
- package/packages/bundled-features/src/shared/index.ts +6 -0
- package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +42 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
- package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
- package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
- package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
- package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
- package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
- package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
- package/packages/bundled-features/src/template-resolver/README.md +66 -8
- package/packages/bundled-features/src/template-resolver/api.ts +85 -9
- package/packages/bundled-features/src/template-resolver/changes.json +186 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +278 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +49 -11
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +345 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +59 -6
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +160 -0
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +33 -0
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +138 -0
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -6
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +151 -0
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +19 -18
- package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +323 -0
- package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
- package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +135 -0
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
- package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +196 -0
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +233 -0
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +137 -0
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +91 -17
- package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +16 -77
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +31 -0
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
- package/packages/bundled-features/src/template-resolver/index.ts +111 -0
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
- package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
- package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
- package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
- package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
- package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +34 -0
- package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +171 -20
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
- package/packages/bundled-features/src/template-resolver/table.ts +27 -1
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
- package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +192 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +676 -0
- package/packages/bundled-features/src/template-resolver/template-resources.ts +114 -0
- package/packages/bundled-features/src/template-resolver/template-usage.ts +68 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +230 -0
- package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +20 -13
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +215 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +37 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1065 -0
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +72 -0
- package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +121 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +176 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
- package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
- package/packages/bundled-features/src/tenant/seeding.ts +41 -7
- package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
- package/packages/bundled-features/src/user/changes.json +6 -0
- package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
- package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
- package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
- package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
- package/packages/bundled-features/src/user/seeding.ts +19 -5
- package/packages/bundled-features/src/user-data-rights/changes.json +13 -0
- package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
- package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
- package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
- package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
- package/packages/bundled-features/src/user-profile/changes.json +1 -1
- package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
- package/packages/bundled-features/src/waitlist/changes.json +7 -0
- package/packages/bundled-features/src/waitlist/feature.ts +17 -0
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
- package/packages/bundled-features/src/waitlist/mail.ts +69 -18
- package/packages/bundled-features/src/waitlist/options.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +3 -3
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
- package/samples/apps/ui-walkthrough/package.json +11 -0
- package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
- package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
- package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +239 -0
- package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +570 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +72 -0
- package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
- package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +41 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +118 -21
- package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +2 -0
- package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
- package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
- package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
- package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
- package/samples/recipes/delivery-notifications/README.md +26 -25
- package/samples/recipes/delivery-notifications/package.json +2 -1
- package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
- package/samples/recipes/seed-migration/README.md +1 -1
- package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
- package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
- package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
- package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
- package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
|
@@ -29,21 +29,21 @@ export async function selectStaleDraftsBatch(
|
|
|
29
29
|
olderThanDays: number,
|
|
30
30
|
batchSize: number,
|
|
31
31
|
): Promise<readonly StaleDraftRow[]> {
|
|
32
|
-
const rows =
|
|
33
|
-
db,
|
|
34
|
-
`SELECT "id", "tenant_id", "owner_id", "draft_key", "draft", "inserted_at" FROM "read_form_drafts"
|
|
35
|
-
WHERE COALESCE("modified_at", "inserted_at") < now() - ($1::int * interval '1 day')
|
|
36
|
-
ORDER BY COALESCE("modified_at", "inserted_at") ASC
|
|
37
|
-
LIMIT $2`,
|
|
38
|
-
[olderThanDays, batchSize],
|
|
39
|
-
)) as readonly {
|
|
32
|
+
const rows = await unsafeReadRetrying<{
|
|
40
33
|
id: string;
|
|
41
34
|
tenant_id: string;
|
|
42
35
|
owner_id: string;
|
|
43
36
|
draft_key: string;
|
|
44
37
|
draft: FormDraftBlob;
|
|
45
38
|
inserted_at: Date;
|
|
46
|
-
}
|
|
39
|
+
}>(
|
|
40
|
+
db,
|
|
41
|
+
`SELECT "id", "tenant_id", "owner_id", "draft_key", "draft", "inserted_at" FROM "read_form_drafts"
|
|
42
|
+
WHERE COALESCE("modified_at", "inserted_at") < now() - ($1::int * interval '1 day')
|
|
43
|
+
ORDER BY COALESCE("modified_at", "inserted_at") ASC
|
|
44
|
+
LIMIT $2`,
|
|
45
|
+
[olderThanDays, batchSize],
|
|
46
|
+
);
|
|
47
47
|
return rows.map((row) => ({
|
|
48
48
|
id: row.id,
|
|
49
49
|
tenantId: row.tenant_id as TenantId, // @cast-boundary db-row
|
|
@@ -61,7 +61,7 @@ export async function isDraftStillStale(
|
|
|
61
61
|
id: string,
|
|
62
62
|
olderThanDays: number,
|
|
63
63
|
): Promise<boolean> {
|
|
64
|
-
const rows =
|
|
64
|
+
const rows = await unsafeReadRetrying<unknown>(
|
|
65
65
|
db,
|
|
66
66
|
`SELECT 1 FROM "read_form_drafts"
|
|
67
67
|
WHERE "id" = $1
|
|
@@ -69,6 +69,6 @@ export async function isDraftStillStale(
|
|
|
69
69
|
AND COALESCE("modified_at", "inserted_at") < now() - ($3::int * interval '1 day')
|
|
70
70
|
LIMIT 1`,
|
|
71
71
|
[id, tenantId, olderThanDays],
|
|
72
|
-
)
|
|
72
|
+
);
|
|
73
73
|
return rows.length > 0;
|
|
74
74
|
}
|
|
@@ -11,11 +11,11 @@ export async function countDraftsByOwner(
|
|
|
11
11
|
tenantId: TenantId,
|
|
12
12
|
ownerId: string,
|
|
13
13
|
): Promise<number> {
|
|
14
|
-
const rows =
|
|
14
|
+
const rows = await unsafeReadRetrying<{ count: number }>(
|
|
15
15
|
db,
|
|
16
16
|
`SELECT count(*)::int AS "count" FROM "read_form_drafts"
|
|
17
17
|
WHERE "tenant_id" = $1 AND "owner_id" = $2`,
|
|
18
18
|
[tenantId, ownerId],
|
|
19
|
-
)
|
|
19
|
+
);
|
|
20
20
|
return rows[0]?.count ?? 0;
|
|
21
21
|
}
|
|
@@ -62,13 +62,13 @@ export async function filterOwnedFileRefs(
|
|
|
62
62
|
isCreateMode: boolean,
|
|
63
63
|
): Promise<readonly OwnedFileRef[]> {
|
|
64
64
|
if (candidateKeys.length === 0) return [];
|
|
65
|
-
const rows =
|
|
65
|
+
const rows = await unsafeReadRetrying<{ id: string; storage_key: string }>(
|
|
66
66
|
db,
|
|
67
67
|
`SELECT "id", "storage_key" FROM "file_refs"
|
|
68
68
|
WHERE "tenant_id" = $1 AND "inserted_by_id" = $2
|
|
69
69
|
AND "storage_key" = ANY($3::text[]) AND "is_deleted" = false
|
|
70
70
|
AND ($5::boolean OR "inserted_at" > $4::timestamptz)`,
|
|
71
71
|
[tenantId, ownerId, candidateKeys, draftInsertedAt.toString(), isCreateMode],
|
|
72
|
-
)
|
|
72
|
+
);
|
|
73
73
|
return rows.map((row) => ({ id: row.id, storageKey: row.storage_key }));
|
|
74
74
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
2
|
import type { FormDraftBlob } from "./schemas.js";
|
|
2
3
|
|
|
3
4
|
// The draft blob's `values` is free-form by design (issue #1889: form-draft
|
|
@@ -8,8 +9,8 @@ import type { FormDraftBlob } from "./schemas.js";
|
|
|
8
9
|
// step writes into the draft, whether under a single "file" field or nested
|
|
9
10
|
// in a "files"/"images" array.
|
|
10
11
|
function isFileRefPointer(value: unknown): value is { readonly storageKey: string } {
|
|
11
|
-
if (
|
|
12
|
-
const storageKey =
|
|
12
|
+
if (!isPlainObject(value)) return false;
|
|
13
|
+
const storageKey = value["storageKey"];
|
|
13
14
|
return typeof storageKey === "string" && storageKey.length > 0;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.364.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Watch supervisor skips and logs accounts whose address does not decrypt",
|
|
6
|
+
"detail": "Previously a single such account made the whole account snapshot throw."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.340.0",
|
|
4
10
|
"type": "improvement",
|
package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts
CHANGED
|
@@ -95,7 +95,7 @@ export const connectAccountHandler: WriteHandlerDef = {
|
|
|
95
95
|
authMethod: payload.authMethod,
|
|
96
96
|
ownerUserId,
|
|
97
97
|
displayName: payload.displayName,
|
|
98
|
-
address: encryptedFields
|
|
98
|
+
address: encryptedFields.address,
|
|
99
99
|
// active ab Connect — der Supervisor greift den Account beim
|
|
100
100
|
// nächsten Tick auf und degradiert ihn bei Credential-Fehlern.
|
|
101
101
|
status: InboundMailAccountStatuses.active,
|
package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts
CHANGED
|
@@ -199,11 +199,11 @@ export const ingestMessageHandler: WriteHandlerDef = {
|
|
|
199
199
|
ownerUserId: payload.ownerUserId,
|
|
200
200
|
messageIdHeader: effectiveMessageIdHeader,
|
|
201
201
|
threadKey,
|
|
202
|
-
from: encryptedMessageFields
|
|
203
|
-
to: encryptedMessageFields
|
|
204
|
-
cc: encryptedMessageFields
|
|
205
|
-
subject: encryptedMessageFields
|
|
206
|
-
snippet: encryptedMessageFields
|
|
202
|
+
from: encryptedMessageFields.from,
|
|
203
|
+
to: encryptedMessageFields.to,
|
|
204
|
+
cc: encryptedMessageFields.cc,
|
|
205
|
+
subject: encryptedMessageFields.subject,
|
|
206
|
+
snippet: encryptedMessageFields.snippet,
|
|
207
207
|
receivedAtIso: payload.receivedAtIso,
|
|
208
208
|
bodyRef: payload.bodyRef,
|
|
209
209
|
scope: payload.scope,
|
|
@@ -296,7 +296,7 @@ export const ingestMessageHandler: WriteHandlerDef = {
|
|
|
296
296
|
|
|
297
297
|
const threadEventPayload: MailThreadEventPayload = {
|
|
298
298
|
threadKey,
|
|
299
|
-
subject: encryptedThreadFields
|
|
299
|
+
subject: encryptedThreadFields.subject,
|
|
300
300
|
lastMessageAtIso: newLastAt,
|
|
301
301
|
messageCount,
|
|
302
302
|
};
|
package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts
CHANGED
|
@@ -34,7 +34,7 @@ export const listAccountsQuery: QueryHandlerDef = {
|
|
|
34
34
|
if (!piiKms) return { rows };
|
|
35
35
|
const decryptedRows = await Promise.all(
|
|
36
36
|
rows.map((row) =>
|
|
37
|
-
decryptPiiFieldValues(row
|
|
37
|
+
decryptPiiFieldValues(row, MAIL_ACCOUNT_PII_FIELDS, piiKms, {
|
|
38
38
|
requestId: `inbound-mail-foundation:list-accounts:${ctx.user.tenantId}`,
|
|
39
39
|
}),
|
|
40
40
|
),
|
package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts
CHANGED
|
@@ -49,7 +49,7 @@ export const listMessagesQuery: QueryHandlerDef = {
|
|
|
49
49
|
if (!piiKms) return { rows };
|
|
50
50
|
const decryptedRows = await Promise.all(
|
|
51
51
|
rows.map((row) =>
|
|
52
|
-
decryptPiiFieldValues(row
|
|
52
|
+
decryptPiiFieldValues(row, INBOUND_MESSAGE_PII_FIELDS, piiKms, {
|
|
53
53
|
requestId: `inbound-mail-foundation:list-messages:${ctx.user.tenantId}`,
|
|
54
54
|
}),
|
|
55
55
|
),
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// für read_mail_accounts und read_inbound_messages (Scope-Vererbung).
|
|
5
5
|
|
|
6
6
|
export function isVisibleToCaller(
|
|
7
|
-
row: unknown
|
|
7
|
+
row: Readonly<Record<string, unknown>>,
|
|
8
8
|
user: { readonly id: string; readonly roles: readonly string[] },
|
|
9
9
|
): boolean {
|
|
10
|
-
const owner =
|
|
10
|
+
const owner = row["ownerUserId"];
|
|
11
11
|
if (owner == null || owner === "") return true;
|
|
12
12
|
if (owner === user.id) return true;
|
|
13
13
|
return user.roles.includes("TenantAdmin") || user.roles.includes("SystemAdmin");
|
|
@@ -140,14 +140,17 @@ export function createOAuthAccessTokenManager(
|
|
|
140
140
|
const secretKey = inboundCredentialSecretKey(account.id);
|
|
141
141
|
return transaction(deps.db, async (tx) => {
|
|
142
142
|
await acquireNamespacedAdvisoryLock(tx, OAUTH_REFRESH_LOCK_NAMESPACE, lockKey(account));
|
|
143
|
-
|
|
143
|
+
// Secret read/write join the lock transaction: a second pool connection per refresh
|
|
144
|
+
// would exhaust a small pool when many accounts refresh at once.
|
|
145
|
+
const secrets = deps.secrets.withRunner(tx);
|
|
146
|
+
const stored = await secrets.get(account.tenantId, secretKey);
|
|
144
147
|
if (!stored) {
|
|
145
148
|
throw new InboundAuthError("no refresh token stored for account");
|
|
146
149
|
}
|
|
147
150
|
const currentRefreshToken = stored.reveal();
|
|
148
151
|
const tokens = await refreshWithTimeout(plugin, account, currentRefreshToken);
|
|
149
152
|
if (tokens.refreshToken && tokens.refreshToken !== currentRefreshToken) {
|
|
150
|
-
await
|
|
153
|
+
await secrets.set(
|
|
151
154
|
account.tenantId,
|
|
152
155
|
secretKey,
|
|
153
156
|
tokens.refreshToken,
|
|
@@ -180,12 +183,14 @@ export function createOAuthAccessTokenManager(
|
|
|
180
183
|
async storeRefreshToken(account, refreshToken) {
|
|
181
184
|
await transaction(deps.db, async (tx) => {
|
|
182
185
|
await acquireNamespacedAdvisoryLock(tx, OAUTH_REFRESH_LOCK_NAMESPACE, lockKey(account));
|
|
183
|
-
await deps.secrets
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
await deps.secrets
|
|
187
|
+
.withRunner(tx)
|
|
188
|
+
.set(
|
|
189
|
+
account.tenantId,
|
|
190
|
+
inboundCredentialSecretKey(account.id),
|
|
191
|
+
refreshToken,
|
|
192
|
+
inboundRefreshTokenSecretOptions(account.id),
|
|
193
|
+
);
|
|
189
194
|
});
|
|
190
195
|
cache.delete(account.id);
|
|
191
196
|
},
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
14
14
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
15
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
15
16
|
|
|
16
17
|
const STATE_PURPOSE = "inbound-mail-oauth-connect";
|
|
17
18
|
|
|
@@ -91,13 +92,12 @@ export function verifyOAuthState(
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
function isOAuthStatePayload(v: unknown): v is OAuthStatePayload {
|
|
94
|
-
if (
|
|
95
|
-
const o = v as Record<string, unknown>;
|
|
95
|
+
if (!isPlainObject(v)) return false;
|
|
96
96
|
return (
|
|
97
|
-
typeof
|
|
98
|
-
(
|
|
99
|
-
typeof
|
|
100
|
-
typeof
|
|
101
|
-
typeof
|
|
97
|
+
typeof v["tenantId"] === "string" &&
|
|
98
|
+
(v["ownerUserId"] === null || typeof v["ownerUserId"] === "string") &&
|
|
99
|
+
typeof v["providerKey"] === "string" &&
|
|
100
|
+
typeof v["mailbox"] === "string" &&
|
|
101
|
+
typeof v["nonce"] === "string"
|
|
102
102
|
);
|
|
103
103
|
}
|
|
@@ -157,12 +157,8 @@ async function purgeExpiredSeenAnchors(
|
|
|
157
157
|
// direkt tenant-scoped, kein Account-Join. Trifft auch Anker, deren Account
|
|
158
158
|
// schon disconnected/geloescht ist.
|
|
159
159
|
const where = { tenantId, seenAt: { lt: cutoff } };
|
|
160
|
-
const expired = await selectMany<{ accountId: string }>(
|
|
161
|
-
db,
|
|
162
|
-
seenMessageTable as EntityTableMeta,
|
|
163
|
-
where,
|
|
164
|
-
);
|
|
160
|
+
const expired = await selectMany<{ accountId: string }>(db, seenMessageTable, where);
|
|
165
161
|
if (expired.length === 0) return 0;
|
|
166
|
-
await deleteMany(db, seenMessageTable
|
|
162
|
+
await deleteMany(db, seenMessageTable, where);
|
|
167
163
|
return expired.length;
|
|
168
164
|
}
|
|
@@ -62,27 +62,19 @@ async function archiveAndDeleteRows(
|
|
|
62
62
|
export async function mailAccountTenantDestroyHook(ctx: TenantDataHookCtx): Promise<void> {
|
|
63
63
|
const accountIds = await archiveAndDeleteRows(
|
|
64
64
|
ctx,
|
|
65
|
-
mailAccountsProjectionTable
|
|
65
|
+
mailAccountsProjectionTable,
|
|
66
66
|
MAIL_ACCOUNT_AGGREGATE_TYPE,
|
|
67
67
|
);
|
|
68
68
|
for (const accountId of accountIds) {
|
|
69
|
-
await ctx.db.deleteMany(syncCursorTable
|
|
70
|
-
await ctx.db.deleteMany(seenMessageTable
|
|
69
|
+
await ctx.db.deleteMany(syncCursorTable, { accountId });
|
|
70
|
+
await ctx.db.deleteMany(seenMessageTable, { accountId });
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export async function inboundMessageTenantDestroyHook(ctx: TenantDataHookCtx): Promise<void> {
|
|
75
|
-
await archiveAndDeleteRows(
|
|
76
|
-
ctx,
|
|
77
|
-
inboundMessagesProjectionTable as EntityTableMeta,
|
|
78
|
-
INBOUND_MESSAGE_AGGREGATE_TYPE,
|
|
79
|
-
);
|
|
75
|
+
await archiveAndDeleteRows(ctx, inboundMessagesProjectionTable, INBOUND_MESSAGE_AGGREGATE_TYPE);
|
|
80
76
|
}
|
|
81
77
|
|
|
82
78
|
export async function mailThreadTenantDestroyHook(ctx: TenantDataHookCtx): Promise<void> {
|
|
83
|
-
await archiveAndDeleteRows(
|
|
84
|
-
ctx,
|
|
85
|
-
mailThreadsProjectionTable as EntityTableMeta,
|
|
86
|
-
MAIL_THREAD_AGGREGATE_TYPE,
|
|
87
|
-
);
|
|
79
|
+
await archiveAndDeleteRows(ctx, mailThreadsProjectionTable, MAIL_THREAD_AGGREGATE_TYPE);
|
|
88
80
|
}
|
|
@@ -257,17 +257,15 @@ export class InboundTransientError extends Error {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
type KindCarrier = { readonly kind?: unknown };
|
|
261
|
-
|
|
262
260
|
export function isInboundAuthError(e: unknown): e is InboundAuthError {
|
|
263
|
-
return e instanceof Error &&
|
|
261
|
+
return e instanceof Error && "kind" in e && e.kind === "inbound-auth-error";
|
|
264
262
|
}
|
|
265
263
|
export function isInboundRateLimitError(e: unknown): e is InboundRateLimitError {
|
|
266
|
-
return e instanceof Error &&
|
|
264
|
+
return e instanceof Error && "kind" in e && e.kind === "inbound-rate-limit-error";
|
|
267
265
|
}
|
|
268
266
|
export function isInboundCursorInvalidError(e: unknown): e is InboundCursorInvalidError {
|
|
269
|
-
return e instanceof Error &&
|
|
267
|
+
return e instanceof Error && "kind" in e && e.kind === "inbound-cursor-invalid-error";
|
|
270
268
|
}
|
|
271
269
|
export function isInboundTransientError(e: unknown): e is InboundTransientError {
|
|
272
|
-
return e instanceof Error &&
|
|
270
|
+
return e instanceof Error && "kind" in e && e.kind === "inbound-transient-error";
|
|
273
271
|
}
|
|
@@ -49,9 +49,10 @@ import {
|
|
|
49
49
|
configuredPiiSubjectKms,
|
|
50
50
|
decryptPiiFieldValues,
|
|
51
51
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
52
|
-
import type { DbConnection
|
|
52
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
53
53
|
import type { DistributedLock } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
54
54
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
55
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
55
56
|
import { InboundMailAccountStatuses, InboundMailFoundationHandlers } from "./constants.js";
|
|
56
57
|
import { MAIL_ACCOUNT_PII_FIELDS, syncCursorTable } from "./entities.js";
|
|
57
58
|
import {
|
|
@@ -158,6 +159,17 @@ export type InboundMailSupervisor = {
|
|
|
158
159
|
readonly stop: () => Promise<void>;
|
|
159
160
|
};
|
|
160
161
|
|
|
162
|
+
type ActiveMailAccountRow = {
|
|
163
|
+
readonly id: string;
|
|
164
|
+
readonly tenantId: string;
|
|
165
|
+
readonly provider: string;
|
|
166
|
+
readonly authMethod: string;
|
|
167
|
+
readonly ownerUserId?: string | null;
|
|
168
|
+
readonly address: string;
|
|
169
|
+
readonly displayName?: string;
|
|
170
|
+
readonly watchState?: string;
|
|
171
|
+
};
|
|
172
|
+
|
|
161
173
|
export function createInboundMailSupervisor(
|
|
162
174
|
deps: InboundMailSupervisorDeps,
|
|
163
175
|
): InboundMailSupervisor {
|
|
@@ -206,30 +218,34 @@ export function createInboundMailSupervisor(
|
|
|
206
218
|
// (Provider arbeiten nie mit Ciphertext).
|
|
207
219
|
// ---------------------------------------------------------------
|
|
208
220
|
async function listActiveAccounts(): Promise<readonly MailAccountRecord[]> {
|
|
209
|
-
const rows = await selectMany(deps.db, mailAccountsProjectionTable, {
|
|
221
|
+
const rows = await selectMany<ActiveMailAccountRow>(deps.db, mailAccountsProjectionTable, {
|
|
210
222
|
status: InboundMailAccountStatuses.active,
|
|
211
223
|
});
|
|
212
224
|
const piiKms = configuredPiiSubjectKms();
|
|
213
225
|
const records: MailAccountRecord[] = [];
|
|
214
|
-
for (const
|
|
215
|
-
const
|
|
216
|
-
? await decryptPiiFieldValues(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
226
|
+
for (const row of rows) {
|
|
227
|
+
const decrypted = piiKms
|
|
228
|
+
? await decryptPiiFieldValues(row, MAIL_ACCOUNT_PII_FIELDS, piiKms, {
|
|
229
|
+
requestId: "inbound-mail-foundation:supervisor:list-accounts",
|
|
230
|
+
})
|
|
231
|
+
: row;
|
|
232
|
+
const decryptedAddress = decrypted["address"];
|
|
233
|
+
// Falling back to row.address would hand the ciphertext to a provider.
|
|
234
|
+
// One undecryptable account must not stop polling of every other tenant's accounts.
|
|
235
|
+
if (typeof decryptedAddress !== "string") {
|
|
236
|
+
log(`inbound-mail: account ${row.id} skipped — address did not decrypt to text`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
223
239
|
records.push({
|
|
224
|
-
id: row
|
|
225
|
-
tenantId: row
|
|
226
|
-
provider: row
|
|
227
|
-
authMethod: row
|
|
228
|
-
ownerUserId:
|
|
229
|
-
address:
|
|
230
|
-
displayName:
|
|
240
|
+
id: row.id,
|
|
241
|
+
tenantId: row.tenantId,
|
|
242
|
+
provider: row.provider,
|
|
243
|
+
authMethod: row.authMethod,
|
|
244
|
+
ownerUserId: row.ownerUserId ?? null,
|
|
245
|
+
address: decryptedAddress,
|
|
246
|
+
displayName: row.displayName ?? "",
|
|
231
247
|
status: InboundMailAccountStatuses.active,
|
|
232
|
-
watchState:
|
|
248
|
+
watchState: row.watchState ?? "idle",
|
|
233
249
|
});
|
|
234
250
|
}
|
|
235
251
|
return records;
|
|
@@ -239,13 +255,14 @@ export function createInboundMailSupervisor(
|
|
|
239
255
|
// Cursor-Persistenz (unmanaged direct-write store).
|
|
240
256
|
// ---------------------------------------------------------------
|
|
241
257
|
async function loadCursor(accountId: string): Promise<SyncCursorPayload | null> {
|
|
242
|
-
const row = await fetchOne<{ cursor: string }>(deps.db, syncCursorTable
|
|
258
|
+
const row = await fetchOne<{ cursor: string }>(deps.db, syncCursorTable, {
|
|
243
259
|
accountId,
|
|
244
260
|
scope: CURSOR_SCOPE,
|
|
245
261
|
});
|
|
246
262
|
if (!row) return null;
|
|
247
263
|
try {
|
|
248
|
-
|
|
264
|
+
const parsed: unknown = JSON.parse(row.cursor);
|
|
265
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
249
266
|
} catch {
|
|
250
267
|
return null; // korrupter Cursor = wie kein Cursor → Backfill, Dedup fängt Dubletten
|
|
251
268
|
}
|
|
@@ -258,21 +275,21 @@ export function createInboundMailSupervisor(
|
|
|
258
275
|
const accountId = account.id;
|
|
259
276
|
const now = Temporal.Now.instant().toString();
|
|
260
277
|
const serialized = JSON.stringify(cursor);
|
|
261
|
-
const existing = await fetchOne<{ id: string }>(deps.db, syncCursorTable
|
|
278
|
+
const existing = await fetchOne<{ id: string }>(deps.db, syncCursorTable, {
|
|
262
279
|
accountId,
|
|
263
280
|
scope: CURSOR_SCOPE,
|
|
264
281
|
});
|
|
265
282
|
if (existing) {
|
|
266
283
|
await updateMany(
|
|
267
284
|
deps.db,
|
|
268
|
-
syncCursorTable
|
|
285
|
+
syncCursorTable,
|
|
269
286
|
{ cursor: serialized, updatedAt: now },
|
|
270
287
|
{ accountId, scope: CURSOR_SCOPE },
|
|
271
288
|
);
|
|
272
289
|
// skip: update-Pfad fertig — nicht in den insert-Zweig durchfallen.
|
|
273
290
|
return;
|
|
274
291
|
}
|
|
275
|
-
await insertOne(deps.db, syncCursorTable
|
|
292
|
+
await insertOne(deps.db, syncCursorTable, {
|
|
276
293
|
id: crypto.randomUUID(),
|
|
277
294
|
// Direct-write store: no executor stamps the NOT NULL tenant_id for us.
|
|
278
295
|
tenantId: account.tenantId,
|
|
@@ -288,7 +305,7 @@ export function createInboundMailSupervisor(
|
|
|
288
305
|
// → nächster fetch läuft als Backfill.
|
|
289
306
|
await updateMany(
|
|
290
307
|
deps.db,
|
|
291
|
-
syncCursorTable
|
|
308
|
+
syncCursorTable,
|
|
292
309
|
{ cursor: "", updatedAt: Temporal.Now.instant().toString() },
|
|
293
310
|
{ accountId, scope: CURSOR_SCOPE },
|
|
294
311
|
);
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
19
19
|
import { legacyDateToInstant } from "@cosmicdrift/kumiko-framework/time";
|
|
20
20
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
21
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
21
22
|
import { ImapFlow } from "imapflow";
|
|
22
23
|
import { type AddressObject, type ParsedMail, simpleParser } from "mailparser";
|
|
23
24
|
import type { ImapCredentialDocument } from "./credential-document.js";
|
|
@@ -97,11 +98,11 @@ export function mapImapError(err: unknown, host: string): Error {
|
|
|
97
98
|
// imapflow-Fehler tragen die Server-Antwort in responseText, die
|
|
98
99
|
// message ist nur "Command failed" — beides in den Match einbeziehen;
|
|
99
100
|
// authenticationFailed ist das verlässliche Flag.
|
|
100
|
-
const carrier = err
|
|
101
|
-
const responseText = typeof carrier?.responseText === "string" ? carrier
|
|
101
|
+
const carrier = isPlainObject(err) ? err : undefined;
|
|
102
|
+
const responseText = typeof carrier?.["responseText"] === "string" ? carrier["responseText"] : "";
|
|
102
103
|
const msg = `${err instanceof Error ? err.message : String(err)} ${responseText}`.trim();
|
|
103
104
|
if (
|
|
104
|
-
carrier?.authenticationFailed === true ||
|
|
105
|
+
carrier?.["authenticationFailed"] === true ||
|
|
105
106
|
/auth(entication)? failed|invalid credentials|invalid login|LOGIN failed|AUTHENTICATIONFAILED/i.test(
|
|
106
107
|
msg,
|
|
107
108
|
)
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.364.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "jobs list query caps the page size at 200",
|
|
6
|
+
"detail": "limit above 200 is rejected with a validation error."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.364.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Stale-run sweep uses the job-run retention window for queued runs",
|
|
12
|
+
"detail": "The sweep now only fails queued runs that are older than the configured retention days instead of every queued run past the stale cutoff, so freshly queued runs of a long backlog are no longer failed."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.364.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "Tenant-visible job runs no longer collide across tenants, jobs and lanes",
|
|
18
|
+
"detail": "store_tenant_job_runs was unique on bull_job_id alone, but BullMQ ids count per queue, so a job on the api lane and one on the worker lane (or two tenants after a Redis reset) shared an id and overwrote each other's row. The unique index now covers (tenant_id, job_name, bull_job_id), every write filters by all three, and an enqueue replaces a finished row left over under the same key. onJobDropped now receives { tenantId } as a third argument.",
|
|
19
|
+
"migration": "Run `kumiko-schema generate` in your app to add the migration that creates store_tenant_job_runs_tenant_job_bull_job_id_unique and drops store_tenant_job_runs_bull_job_id_unique (generated as a commented DROP INDEX; uncomment it). A custom onJobDropped callback passed to the job runner now gets a third argument { tenantId }."
|
|
20
|
+
},
|
|
2
21
|
{
|
|
3
22
|
"version": "0.360.0",
|
|
4
23
|
"type": "fix",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { deleteManyBatched, selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
16
16
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
17
17
|
import { mapWithConcurrency } from "../../../shared/index.js";
|
|
18
|
+
import { DEFAULT_JOB_RUN_RETENTION_DAYS } from "../../handlers/retention-cleanup.job.js";
|
|
18
19
|
import { encryptFailureError } from "../../job-run-logger.js";
|
|
19
20
|
import { jobRunsTable } from "../../job-run-table.js";
|
|
20
21
|
import { tenantJobRunsTable } from "../../tenant-job-run-table.js";
|
|
@@ -31,18 +32,24 @@ export type StaleRunSweepResult = {
|
|
|
31
32
|
readonly tenantQueuedDropped: number;
|
|
32
33
|
};
|
|
33
34
|
|
|
35
|
+
// A queued tenant row can legitimately wait much longer than a run takes (backlog, delay/backoff,
|
|
36
|
+
// sequential requeue), so it is only treated as lost once it is older than the retention window,
|
|
37
|
+
// not after the running-timeout.
|
|
34
38
|
export async function markStaleJobRunsFailed(
|
|
35
39
|
db: DbConnection,
|
|
36
40
|
timeoutHours: number,
|
|
41
|
+
queuedRetentionDays: number = DEFAULT_JOB_RUN_RETENTION_DAYS,
|
|
37
42
|
): Promise<StaleRunSweepResult> {
|
|
38
43
|
const cutoff = Temporal.Now.instant().subtract({ hours: timeoutHours });
|
|
44
|
+
const queuedCutoff = Temporal.Now.instant().subtract({ hours: queuedRetentionDays * 24 });
|
|
39
45
|
const now = Temporal.Now.instant();
|
|
40
46
|
|
|
41
47
|
// Fetch the matched rows first: `error` carries the same per-triggering-
|
|
42
48
|
// user subject annotation as job-run-logger.ts's onJobFailed path
|
|
43
49
|
// (personal: { of: "triggeredById" } on job-run-table.ts), so it is encrypted
|
|
44
|
-
// under
|
|
45
|
-
//
|
|
50
|
+
// under the triggering user's DEK, or the system tenant key when
|
|
51
|
+
// triggeredById is null (whenAbsent: "tenant", fw#3654). Runs are grouped by
|
|
52
|
+
// that subject: one encrypt and one updateMany per subject instead of per run.
|
|
46
53
|
const stale = await selectMany<{ id: string; triggeredById: string | null }>(db, jobRunsTable, {
|
|
47
54
|
status: "running",
|
|
48
55
|
startedAt: { lt: cutoff },
|
|
@@ -86,7 +93,8 @@ export async function markStaleJobRunsFailed(
|
|
|
86
93
|
|
|
87
94
|
// Tenant-visible run state follows the same clock: a running row whose
|
|
88
95
|
// worker died is a failure, a queued row that never started (job lost from
|
|
89
|
-
// Redis, enqueue hook without a run) would otherwise show "waiting" forever
|
|
96
|
+
// Redis, enqueue hook without a run) would otherwise show "waiting" forever;
|
|
97
|
+
// it is dropped once it has outlived the retention window.
|
|
90
98
|
const tenantRunsFailed = await updateMany(
|
|
91
99
|
db,
|
|
92
100
|
tenantJobRunsTable,
|
|
@@ -96,7 +104,7 @@ export async function markStaleJobRunsFailed(
|
|
|
96
104
|
const tenantQueuedStale = await deleteManyBatched(
|
|
97
105
|
db,
|
|
98
106
|
tenantJobRunsTable,
|
|
99
|
-
{ status: "queued", updatedAt: { lt:
|
|
107
|
+
{ status: "queued", updatedAt: { lt: queuedCutoff } },
|
|
100
108
|
{ limit: STALE_DELETE_BATCH_SIZE },
|
|
101
109
|
);
|
|
102
110
|
|
|
@@ -36,7 +36,8 @@ import { tenantJobRunsTableMeta } from "./tenant-job-run-table.js";
|
|
|
36
36
|
|
|
37
37
|
export type JobsFeatureOptions = {
|
|
38
38
|
// How long a job run (and its logs) stays in store_job_runs/
|
|
39
|
-
// store_job_run_logs before the daily retention-cleanup job deletes it.
|
|
39
|
+
// store_job_run_logs before the daily retention-cleanup job deletes it. Also the age after
|
|
40
|
+
// which a tenant-visible "queued" row counts as lost and is dropped by the stale-run sweep.
|
|
40
41
|
readonly retentionDays?: number;
|
|
41
42
|
// How long a run can sit at status "running" before the hourly
|
|
42
43
|
// stale-run-sweep job marks it "failed" (#2246 — a process kill mid-run
|
|
@@ -51,7 +52,7 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
51
52
|
const staleRunTimeoutHours = options.staleRunTimeoutHours ?? DEFAULT_JOB_RUN_STALE_TIMEOUT_HOURS;
|
|
52
53
|
return defineFeature("jobs", (r) => {
|
|
53
54
|
r.describe(
|
|
54
|
-
"Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079). A job that declares `tenantVisibleRun` also keeps its run state per tenant and subject in `store_tenant_job_runs` (queued, running, plus the latest completed or failed run, no payload, error text or logs), which the tenant reads through `jobs:query:tenant-runs` (fw#3616); the stale-run sweep fails its stuck `running` rows and drops
|
|
55
|
+
"Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079). A job that declares `tenantVisibleRun` also keeps its run state per tenant and subject in `store_tenant_job_runs` (queued, running, plus the latest completed or failed run, no payload, error text or logs), which the tenant reads through `jobs:query:tenant-runs` (fw#3616); the stale-run sweep fails its stuck `running` rows and drops `queued` rows older than `retentionDays`.",
|
|
55
56
|
);
|
|
56
57
|
r.uiHints({
|
|
57
58
|
displayLabel: "Jobs · Audit & Operator UI",
|
|
@@ -61,7 +62,9 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
61
62
|
r.systemScope();
|
|
62
63
|
r.storeTable(jobRunsTableMeta, {
|
|
63
64
|
reason: "direct_write.job_runs",
|
|
64
|
-
// payload/error carry personal:{of:"triggeredById"} (job-run-table.ts)
|
|
65
|
+
// payload/error carry personal:{of:"triggeredById"} (job-run-table.ts): encrypted under the
|
|
66
|
+
// triggering user's DEK, or the system tenant key when triggeredById is null
|
|
67
|
+
// (whenAbsent: "tenant", fw#3654).
|
|
65
68
|
// job-run-logger.ts manually encrypts both before every insert/update
|
|
66
69
|
// (encryptStartedPayload/encryptFailureError) since this store bypasses
|
|
67
70
|
// the executor's automatic PII pipeline (#2243).
|
|
@@ -115,7 +118,7 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
115
118
|
r.job(
|
|
116
119
|
"stale-run-sweep",
|
|
117
120
|
{ trigger: { cron: "0 * * * *" }, concurrency: "skip" },
|
|
118
|
-
createStaleRunSweepJob(staleRunTimeoutHours),
|
|
121
|
+
createStaleRunSweepJob(staleRunTimeoutHours, retentionDays),
|
|
119
122
|
);
|
|
120
123
|
|
|
121
124
|
const handlers = {
|
|
@@ -12,7 +12,7 @@ export type ManualJobCatalogEntry = {
|
|
|
12
12
|
function payloadSchemaJson(job: JobDefinition): Record<string, unknown> | null {
|
|
13
13
|
if (job.schema === undefined) return null;
|
|
14
14
|
try {
|
|
15
|
-
return z.toJSONSchema(job.schema)
|
|
15
|
+
return z.toJSONSchema(job.schema);
|
|
16
16
|
} catch {
|
|
17
17
|
// Non-JSON-Schema-able Zod types (transforms/effects) — UI still lists the job.
|
|
18
18
|
return null;
|
|
@@ -8,6 +8,8 @@ import { jobRunsTable } from "../job-run-table.js";
|
|
|
8
8
|
|
|
9
9
|
const KMS_POOL_CONCURRENCY = 4;
|
|
10
10
|
const DEFAULT_PAGE_SIZE = 50;
|
|
11
|
+
// The offset cursor re-reads everything up to the page end, so the page size bounds that read.
|
|
12
|
+
const MAX_LIST_LIMIT = 200;
|
|
11
13
|
|
|
12
14
|
const jobRunStatusSchema = z.enum(["queued", "running", "completed", "failed"]);
|
|
13
15
|
|
|
@@ -68,7 +70,7 @@ export const listQuery = defineQueryHandler({
|
|
|
68
70
|
.optional(),
|
|
69
71
|
sort: z.enum(["jobName", "status", "startedAt", "duration"]).optional(),
|
|
70
72
|
sortDirection: z.enum(["asc", "desc"]).optional(),
|
|
71
|
-
limit: z.number().int().min(1).optional(),
|
|
73
|
+
limit: z.number().int().min(1).max(MAX_LIST_LIMIT).optional(),
|
|
72
74
|
cursor: z.string().optional(),
|
|
73
75
|
totalCount: z.boolean().optional(),
|
|
74
76
|
}),
|