@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
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.364.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Tier cache: stale single reload cannot overwrite a newer full resync",
|
|
6
|
+
"detail": "A single-tenant reload that started before a full resync and resolved after it could restore an outdated tier. The resync now advances the per-tenant ticket for deleted and adopted tenants too, and assignment changes are published remote-only."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.354.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -338,7 +338,8 @@ export function createTierEngineFeature<
|
|
|
338
338
|
};
|
|
339
339
|
const publishAssignmentChange = (tenantId: TenantId): void => {
|
|
340
340
|
const message: TierAssignmentSyncMessage = { tenantId };
|
|
341
|
-
|
|
341
|
+
// The writer already updated its own cache; only other pods need to re-read.
|
|
342
|
+
busHolder.bus?.publish(CACHE_SYNC_TOPICS.tierAssignment, message, { remoteOnly: true });
|
|
342
343
|
};
|
|
343
344
|
|
|
344
345
|
// Trial: zeit-abgeleitet aus tenant.inserted_at (≈ Signup), live am
|
|
@@ -533,10 +534,17 @@ export function createTierEngineFeature<
|
|
|
533
534
|
const isNewerThanSnapshot = (tenantId: TenantId): boolean =>
|
|
534
535
|
(latestTicket.get(tenantId) ?? 0) > startedAtTicket;
|
|
535
536
|
for (const tenantId of [...cache.keys()]) {
|
|
536
|
-
if (!loaded.has(tenantId) && !isNewerThanSnapshot(tenantId))
|
|
537
|
+
if (!loaded.has(tenantId) && !isNewerThanSnapshot(tenantId)) {
|
|
538
|
+
cache.delete(tenantId);
|
|
539
|
+
latestTicket.set(tenantId, startedAtTicket);
|
|
540
|
+
}
|
|
537
541
|
}
|
|
538
542
|
for (const [tenantId, features] of loaded) {
|
|
539
|
-
if (!isNewerThanSnapshot(tenantId))
|
|
543
|
+
if (!isNewerThanSnapshot(tenantId)) {
|
|
544
|
+
cache.set(tenantId, features);
|
|
545
|
+
// Makes a single reload that started before this snapshot discard its older result.
|
|
546
|
+
latestTicket.set(tenantId, startedAtTicket);
|
|
547
|
+
}
|
|
540
548
|
}
|
|
541
549
|
};
|
|
542
550
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.364.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "resolveUserDisplayNames no longer swallows decrypt errors",
|
|
6
|
+
"detail": "A misconfigured or unavailable KMS made the delivery log silently show addresses or ids instead of names. Decrypt errors now propagate like in the member directory; users that are missing or have no display name are still left out."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.361.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -37,12 +37,12 @@ export type UserStreamBackfillResult = {
|
|
|
37
37
|
export async function backfillUserStreamTenants(
|
|
38
38
|
db: DbConnection,
|
|
39
39
|
): Promise<UserStreamBackfillResult> {
|
|
40
|
-
const candidates =
|
|
40
|
+
const candidates = await unsafeReadRetrying<{ aggregate_id: string }>(
|
|
41
41
|
db,
|
|
42
42
|
`SELECT DISTINCT "aggregate_id" FROM "kumiko_events"
|
|
43
43
|
WHERE "aggregate_type" = 'user' AND "tenant_id" <> $1::uuid`,
|
|
44
44
|
[SYSTEM_TENANT_ID],
|
|
45
|
-
)
|
|
45
|
+
);
|
|
46
46
|
|
|
47
47
|
let aggregatesMigrated = 0;
|
|
48
48
|
let eventsMigrated = 0;
|
|
@@ -67,13 +67,13 @@ async function migrateAggregate(db: DbConnection, aggregateId: string): Promise<
|
|
|
67
67
|
const raw = asRawClient(tx);
|
|
68
68
|
// Global id order = commit-adjacent replay order; merges a split stream
|
|
69
69
|
// (legacy tenant + SYSTEM) into one consistent version sequence.
|
|
70
|
-
const events =
|
|
70
|
+
const events = await raw.unsafe<{ id: bigint | string }>(
|
|
71
71
|
`SELECT "id" FROM "kumiko_events"
|
|
72
72
|
WHERE "aggregate_type" = 'user' AND "aggregate_id" = $1::uuid
|
|
73
73
|
ORDER BY "id" ASC
|
|
74
74
|
FOR UPDATE`,
|
|
75
75
|
[aggregateId],
|
|
76
|
-
)
|
|
76
|
+
);
|
|
77
77
|
|
|
78
78
|
// Phase 1: negative interim versions — real versions are always >= 1, so
|
|
79
79
|
// nothing can collide with events_aggregate_version_uq mid-migration.
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
defineEntityDetailHandler,
|
|
4
4
|
type HandlerContext,
|
|
5
5
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
6
7
|
import { userEntity } from "../schema/user.js";
|
|
7
8
|
import { attachTenantLabels, dbForList } from "./list.query.js";
|
|
8
9
|
|
|
@@ -24,12 +25,9 @@ export const detailQuery = {
|
|
|
24
25
|
ctx: HandlerContext,
|
|
25
26
|
) => {
|
|
26
27
|
const result = await baseDetail.handler?.(query, ctx);
|
|
27
|
-
if (result
|
|
28
|
+
if (!isPlainObject(result)) return result;
|
|
28
29
|
if (!ctx.registry.features.has("tenant")) return result;
|
|
29
|
-
const [enriched] = await attachTenantLabels(
|
|
30
|
-
[result as Record<string, unknown>],
|
|
31
|
-
dbForList(ctx),
|
|
32
|
-
);
|
|
30
|
+
const [enriched] = await attachTenantLabels([result], dbForList(ctx));
|
|
33
31
|
return enriched ?? result;
|
|
34
32
|
},
|
|
35
33
|
};
|
|
@@ -6,9 +6,10 @@ import { userTable } from "./schema/user.js";
|
|
|
6
6
|
// Shares the KMS adapter's small dedicated pool, same as member-directory.
|
|
7
7
|
const KMS_POOL_CONCURRENCY = 4;
|
|
8
8
|
|
|
9
|
-
// One batched lookup for a page of user ids. Users that are missing
|
|
10
|
-
// display name,
|
|
11
|
-
//
|
|
9
|
+
// One batched lookup for a page of user ids. Users that are missing or have no
|
|
10
|
+
// display name are left out, so callers fall back to the raw id. Decrypt errors
|
|
11
|
+
// propagate (a misconfigured or unavailable KMS fails loudly, like member-directory);
|
|
12
|
+
// crypto-shredding is handled by the KMS layer.
|
|
12
13
|
export async function resolveUserDisplayNames(
|
|
13
14
|
db: TenantDb,
|
|
14
15
|
userIds: readonly string[],
|
|
@@ -18,18 +19,10 @@ export async function resolveUserDisplayNames(
|
|
|
18
19
|
const users = await selectMany(db, userTable, { id: [...userIds] });
|
|
19
20
|
const named = users.filter((user) => typeof user.displayName === "string");
|
|
20
21
|
await mapWithConcurrency(named, KMS_POOL_CONCURRENCY, async (user) => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
String(user.displayName),
|
|
26
|
-
"displayName",
|
|
27
|
-
"user:resolve-display-names",
|
|
28
|
-
),
|
|
29
|
-
);
|
|
30
|
-
} catch {
|
|
31
|
-
// Unreadable name: the caller's fallback applies.
|
|
32
|
-
}
|
|
22
|
+
names.set(
|
|
23
|
+
String(user.id),
|
|
24
|
+
await decryptStoredPii(String(user.displayName), "displayName", "user:resolve-display-names"),
|
|
25
|
+
);
|
|
33
26
|
});
|
|
34
27
|
return names;
|
|
35
28
|
}
|
|
@@ -8,8 +8,8 @@ export const rolesInputSchema = z.union([
|
|
|
8
8
|
z.string().transform((raw, ctx) => {
|
|
9
9
|
try {
|
|
10
10
|
const parsed: unknown = JSON.parse(raw);
|
|
11
|
-
if (Array.isArray(parsed) && parsed.every((v) => typeof v === "string")) {
|
|
12
|
-
return parsed
|
|
11
|
+
if (Array.isArray(parsed) && parsed.every((v): v is string => typeof v === "string")) {
|
|
12
|
+
return parsed;
|
|
13
13
|
}
|
|
14
14
|
} catch {
|
|
15
15
|
/* fall through */
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// das Event UND schreibt die Projection-Zeile in einer TX.
|
|
12
12
|
|
|
13
13
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
14
|
+
import { computeBlindIndex, configuredBlindIndexKey } from "@cosmicdrift/kumiko-framework/crypto";
|
|
14
15
|
import {
|
|
15
16
|
createEventStoreExecutor,
|
|
16
17
|
createTenantDb,
|
|
@@ -139,12 +140,25 @@ export async function rewriteSeededUserEmail(
|
|
|
139
140
|
if (typeof id !== "string") {
|
|
140
141
|
throw new Error("rewriteSeededUserEmail: existing user row has no string id");
|
|
141
142
|
}
|
|
143
|
+
const version = existingRow["version"];
|
|
144
|
+
if (typeof version !== "number") {
|
|
145
|
+
throw new Error("rewriteSeededUserEmail: existing user row has no numeric version");
|
|
146
|
+
}
|
|
147
|
+
// Every executor.update appends a user:updated event; skip when the stored
|
|
148
|
+
// blind index already matches so repeated boots do not grow the stream.
|
|
149
|
+
// Queried by the bidx column itself: fetchOne does not return it, and an
|
|
150
|
+
// `email` condition would also match a plaintext row with a blank index.
|
|
151
|
+
const blindIndexKey = configuredBlindIndexKey();
|
|
152
|
+
if (blindIndexKey !== undefined) {
|
|
153
|
+
const indexed = await fetchOne(db, userTable, {
|
|
154
|
+
id,
|
|
155
|
+
emailBidx: computeBlindIndex(blindIndexKey, email),
|
|
156
|
+
});
|
|
157
|
+
// skip: the stored blind index already matches, a rewrite would only append a no-op event.
|
|
158
|
+
if (indexed) return;
|
|
159
|
+
}
|
|
142
160
|
const tdb = createTenantDb(db, by.tenantId, "system");
|
|
143
|
-
const result = await userExecutor.update(
|
|
144
|
-
{ id, version: existingRow["version"] as number, changes: { email } },
|
|
145
|
-
by,
|
|
146
|
-
tdb,
|
|
147
|
-
);
|
|
161
|
+
const result = await userExecutor.update({ id, version, changes: { email } }, by, tdb);
|
|
148
162
|
// version_conflict: a concurrent write already changed the row — fine for a seed helper.
|
|
149
163
|
// unique_violation: a duplicate row already holds the blind index; the repair
|
|
150
164
|
// has to wait for a manual duplicate cleanup.
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.364.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Regression tests prove that forget also shreds the previous email in user.updated events",
|
|
6
|
+
"detail": "No runtime change. Integration tests with a configured subject KMS show that after an email change followed by forget, the old and new email in user.updated changes and previous decrypt only to the erased sentinel and the subject key is gone. Without a configured KMS, event payloads stay plaintext by design."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.363.0",
|
|
10
|
+
"type": "breaking",
|
|
11
|
+
"title": "GDPR default mails are r.notification declarations, render*Email is gone",
|
|
12
|
+
"detail": "export-ready, export-failed, deletion-requested and deletion-executed are declared as trigger-less notifications (user-data-rights:notify:<name>) with variables and localized templates built from the GDPR translations, so a tenant can override the text. The default export-ready, export-failed and deletion-requested mails send through ctx.notify to the user (export mails with immediate so the plain magic-link token never lands in a job payload). deletion-executed renders the seeded template through the mail renderer and sends over the tenant mail transport without a delivery log, because the user is already gone. App-provided send*Email callbacks still win. The defaults now need delivery with channel-email; a boot warning is logged once when mail-foundation and a mail transport are mounted without them. New exports are GdprMailNotificationTypes, gdprMailLocalizedTemplate, gdprMailTemplateLocale, registerGdprMails and formatGdprMailTimestamp.",
|
|
13
|
+
"migration": "renderExportReadyEmail, renderExportFailedEmail, renderDeletionRequestedEmail, renderDeletionExecutedEmail and RenderedEmail are removed from user-data-rights/email-templates; the translations stay. An app that mounts it without includeBundled needs template-resolver, renderer-foundation and @cosmicdrift/kumiko-renderer-mail-html at boot (composeRendererStack() from dev-server mounts all of them), plus a delivery with the channel-email feature; apps without the template tables need a schema migration (`kumiko schema generate`). Delivery preferences that referenced the mails use the new user-data-rights:notify:<name> types."
|
|
14
|
+
},
|
|
2
15
|
{
|
|
3
16
|
"version": "0.362.0",
|
|
4
17
|
"type": "fix",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import type {
|
|
2
|
+
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
4
4
|
import { userTable } from "../../../user/index.js";
|
|
5
5
|
|
|
6
6
|
export async function selectUsersDueForForgetCleanup(
|
|
7
|
-
db:
|
|
7
|
+
db: DbRunner,
|
|
8
8
|
status: string,
|
|
9
9
|
gracePeriodEndCutoff: Temporal.Instant | string,
|
|
10
10
|
): Promise<readonly { id: string }[]> {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Translations for the GDPR notification mails (the templates in
|
|
2
|
+
// mail-notifications.ts are built from them). English ships here; other
|
|
3
|
+
// languages register via @cosmicdrift/kumiko-locale-* (localeDe() /
|
|
3
4
|
// localeEs() call registerMailTranslations). Apps that want their own
|
|
4
5
|
// branding pass send*Email callbacks and these defaults do not run.
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
7
|
+
import { registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
|
|
8
8
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
9
9
|
|
|
10
10
|
export type GdprMailLocale = string;
|
|
@@ -41,151 +41,7 @@ export const GDPR_MAIL_EN: Readonly<Record<string, string>> = {
|
|
|
41
41
|
|
|
42
42
|
registerMailTranslations("en", GDPR_MAIL_EN);
|
|
43
43
|
|
|
44
|
-
export
|
|
45
|
-
readonly subject: string;
|
|
46
|
-
readonly html: string;
|
|
47
|
-
readonly text: string;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export type RenderExportReadyEmailArgs = {
|
|
51
|
-
readonly downloadUrl: string;
|
|
52
|
-
readonly expiresAt: string;
|
|
53
|
-
readonly locale?: GdprMailLocale;
|
|
54
|
-
readonly appName?: string;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export type RenderExportFailedEmailArgs = {
|
|
58
|
-
readonly locale?: GdprMailLocale;
|
|
59
|
-
readonly appName?: string;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export type RenderDeletionRequestedEmailArgs = {
|
|
63
|
-
readonly gracePeriodEnd: string;
|
|
64
|
-
readonly locale?: GdprMailLocale;
|
|
65
|
-
readonly appName?: string;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export type RenderDeletionExecutedEmailArgs = {
|
|
69
|
-
readonly executedAt: string;
|
|
70
|
-
readonly locale?: GdprMailLocale;
|
|
71
|
-
readonly appName?: string;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
function t(locale: string, key: string, params?: Readonly<Record<string, string>>): string {
|
|
75
|
-
return mailT(locale, key, params);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function appNameFor(args: { locale?: GdprMailLocale; appName?: string }): string {
|
|
79
|
-
const locale = args.locale ?? "en";
|
|
80
|
-
return args.appName ?? t(locale, "gdpr.mail.appNameDefault");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function renderExportReadyEmail(args: RenderExportReadyEmailArgs): RenderedEmail {
|
|
84
|
-
const locale = args.locale ?? "en";
|
|
85
|
-
const app = appNameFor(args);
|
|
86
|
-
const subject = t(locale, "gdpr.mail.exportReady.subject", { app });
|
|
87
|
-
const body = `
|
|
88
|
-
<p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
|
|
89
|
-
<p style="margin: 0 0 24px; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.exportReady.intro", { app }))}</p>
|
|
90
|
-
<p style="margin: 0 0 24px;">${renderButton({ url: args.downloadUrl, label: t(locale, "gdpr.mail.exportReady.button") })}</p>
|
|
91
|
-
<p style="margin: 0 0 8px; font-size: 13px; color: #555;">${escapeHtml(t(locale, "gdpr.mail.exportReady.expiry", { when: formatTimestamp(args.expiresAt) }))}</p>
|
|
92
|
-
${renderFallbackUrl({ url: args.downloadUrl, label: t(locale, "gdpr.mail.fallbackUrl") })}`;
|
|
93
|
-
const text = plainTextBody([
|
|
94
|
-
t(locale, "gdpr.mail.greeting"),
|
|
95
|
-
t(locale, "gdpr.mail.exportReady.intro", { app }),
|
|
96
|
-
`${t(locale, "gdpr.mail.exportReady.button")}: ${args.downloadUrl}`,
|
|
97
|
-
t(locale, "gdpr.mail.exportReady.expiry", { when: formatTimestamp(args.expiresAt) }),
|
|
98
|
-
]);
|
|
99
|
-
return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function renderExportFailedEmail(args: RenderExportFailedEmailArgs): RenderedEmail {
|
|
103
|
-
const locale = args.locale ?? "en";
|
|
104
|
-
const app = appNameFor(args);
|
|
105
|
-
const subject = t(locale, "gdpr.mail.exportFailed.subject", { app });
|
|
106
|
-
const body = `
|
|
107
|
-
<p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
|
|
108
|
-
<p style="margin: 0; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.exportFailed.intro", { app }))}</p>`;
|
|
109
|
-
const text = plainTextBody([
|
|
110
|
-
t(locale, "gdpr.mail.greeting"),
|
|
111
|
-
t(locale, "gdpr.mail.exportFailed.intro", { app }),
|
|
112
|
-
]);
|
|
113
|
-
return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function renderDeletionRequestedEmail(
|
|
117
|
-
args: RenderDeletionRequestedEmailArgs,
|
|
118
|
-
): RenderedEmail {
|
|
119
|
-
const locale = args.locale ?? "en";
|
|
120
|
-
const app = appNameFor(args);
|
|
121
|
-
const when = formatTimestamp(args.gracePeriodEnd);
|
|
122
|
-
const subject = t(locale, "gdpr.mail.deletionRequested.subject", { app });
|
|
123
|
-
const body = `
|
|
124
|
-
<p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
|
|
125
|
-
<p style="margin: 0 0 16px; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.deletionRequested.intro", { app, when }))}</p>
|
|
126
|
-
<p style="margin: 0; font-size: 13px; color: #555;">${escapeHtml(t(locale, "gdpr.mail.deletionRequested.cancel"))}</p>`;
|
|
127
|
-
const text = plainTextBody([
|
|
128
|
-
t(locale, "gdpr.mail.greeting"),
|
|
129
|
-
t(locale, "gdpr.mail.deletionRequested.intro", { app, when }),
|
|
130
|
-
t(locale, "gdpr.mail.deletionRequested.cancel"),
|
|
131
|
-
]);
|
|
132
|
-
return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function renderDeletionExecutedEmail(args: RenderDeletionExecutedEmailArgs): RenderedEmail {
|
|
136
|
-
const locale = args.locale ?? "en";
|
|
137
|
-
const app = appNameFor(args);
|
|
138
|
-
const when = formatTimestamp(args.executedAt);
|
|
139
|
-
const subject = t(locale, "gdpr.mail.deletionExecuted.subject", { app });
|
|
140
|
-
const body = `
|
|
141
|
-
<p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
|
|
142
|
-
<p style="margin: 0; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.deletionExecuted.intro", { app, when }))}</p>`;
|
|
143
|
-
const text = plainTextBody([
|
|
144
|
-
t(locale, "gdpr.mail.greeting"),
|
|
145
|
-
t(locale, "gdpr.mail.deletionExecuted.intro", { app, when }),
|
|
146
|
-
]);
|
|
147
|
-
return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function plainTextBody(paragraphs: readonly string[]): string {
|
|
151
|
-
return paragraphs.join("\n\n");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function wrapCell(bodyHtml: string): string {
|
|
155
|
-
return `<tr><td>${bodyHtml}</td></tr>`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function renderShell(args: { title: string; bodyHtml: string; locale: GdprMailLocale }): string {
|
|
159
|
-
return `<!DOCTYPE html>
|
|
160
|
-
<html lang="${escapeHtmlAttr(args.locale)}">
|
|
161
|
-
<head>
|
|
162
|
-
<meta charset="utf-8" />
|
|
163
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
164
|
-
<title>${escapeHtml(args.title)}</title>
|
|
165
|
-
</head>
|
|
166
|
-
<body style="margin: 0; padding: 0; background: #f7f7f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #1a1a1a;">
|
|
167
|
-
<table width="100%" cellpadding="0" cellspacing="0" style="padding: 24px 0;">
|
|
168
|
-
<tr>
|
|
169
|
-
<td align="center">
|
|
170
|
-
<table width="560" cellpadding="0" cellspacing="0" style="max-width: 560px; background: #ffffff; border-radius: 8px; padding: 32px;">
|
|
171
|
-
${args.bodyHtml}
|
|
172
|
-
</table>
|
|
173
|
-
</td>
|
|
174
|
-
</tr>
|
|
175
|
-
</table>
|
|
176
|
-
</body>
|
|
177
|
-
</html>`;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function renderButton(args: { url: string; label: string }): string {
|
|
181
|
-
return `<a href="${escapeHtmlAttr(args.url)}" style="display: inline-block; background: #1a1a1a; color: #ffffff; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 500;">${escapeHtml(args.label)}</a>`;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function renderFallbackUrl(args: { url: string; label: string }): string {
|
|
185
|
-
return `<p style="margin: 24px 0 0; font-size: 12px; color: #666;">${escapeHtml(args.label)}<br /><a href="${escapeHtmlAttr(args.url)}" style="color: #1a1a1a; word-break: break-all;">${escapeHtml(args.url)}</a></p>`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function formatTimestamp(iso: string): string {
|
|
44
|
+
export function formatGdprMailTimestamp(iso: string): string {
|
|
189
45
|
try {
|
|
190
46
|
const z = Temporal.Instant.from(iso).toZonedDateTimeISO("UTC");
|
|
191
47
|
return `${z.year}-${pad2(z.month)}-${pad2(z.day)} ${pad2(z.hour)}:${pad2(z.minute)} UTC`;
|
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
import { UNKNOWN_CLIENT_IP } from "@cosmicdrift/kumiko-framework/api";
|
|
3
3
|
import {
|
|
4
4
|
createSystemConfig,
|
|
5
|
+
createSystemUser,
|
|
5
6
|
defineFeature,
|
|
6
7
|
EXT_USER_DATA,
|
|
7
8
|
type FeatureDefinition,
|
|
8
9
|
i18nKey,
|
|
9
10
|
SYSTEM_USER_ID,
|
|
11
|
+
type TenantId,
|
|
10
12
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
13
|
+
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
11
14
|
import {
|
|
12
15
|
createMetricsHandle,
|
|
13
16
|
createNoopMetricsHandle,
|
|
14
17
|
} from "@cosmicdrift/kumiko-framework/observability";
|
|
18
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
19
|
+
import { createConfigAccessor } from "../config/index.js";
|
|
15
20
|
import { validateGdprHookCompleteness, validateGdprPiiHookCoverage } from "./boot-checks.js";
|
|
16
21
|
import {
|
|
17
22
|
EXPORT_SECTION_EXTENSION_NAME,
|
|
@@ -45,6 +50,8 @@ import { createRunForgetCleanupHandler } from "./handlers/run-forget-cleanup.wri
|
|
|
45
50
|
import { USER_DATA_RIGHTS_I18N } from "./i18n.js";
|
|
46
51
|
import {
|
|
47
52
|
type GdprMailDefaults,
|
|
53
|
+
isGdprMailDeliveryAvailable,
|
|
54
|
+
isMailRendererAvailable,
|
|
48
55
|
isMailTransportAvailable,
|
|
49
56
|
makeDefaultDeletionExecutedEmail,
|
|
50
57
|
makeDefaultExportFailedEmail,
|
|
@@ -53,6 +60,7 @@ import {
|
|
|
53
60
|
import { makeTenantMailTransportResolver } from "./lib/mail-transport-resolver.js";
|
|
54
61
|
import { resolveAppTenantModel } from "./lib/resolve-tenant-model.js";
|
|
55
62
|
import { makeTenantStorageProviderResolver } from "./lib/storage-provider-resolver.js";
|
|
63
|
+
import { registerGdprMails } from "./mail-notifications.js";
|
|
56
64
|
import {
|
|
57
65
|
EXPORT_CLEANUP_BACKLOG_AGE_METRIC,
|
|
58
66
|
EXPORT_CLEANUP_LAST_RUN_TIMESTAMP_METRIC,
|
|
@@ -163,11 +171,22 @@ export type UserDataRightsOptions = {
|
|
|
163
171
|
readonly privacyCenterShowDeletion?: boolean;
|
|
164
172
|
};
|
|
165
173
|
|
|
174
|
+
function isMailTransportMountedWithoutDelivery(features: readonly FeatureDefinition[]): boolean {
|
|
175
|
+
const names = new Set(features.map((feature) => feature.name));
|
|
176
|
+
const hasTransport = [...names].some((name) => name.startsWith("mail-transport-"));
|
|
177
|
+
return (
|
|
178
|
+
names.has("mail-foundation") &&
|
|
179
|
+
hasTransport &&
|
|
180
|
+
!(names.has("delivery") && names.has("channel-email"))
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
166
184
|
export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): FeatureDefinition {
|
|
167
185
|
// One-shot operator warning (the export cron fires daily — warn once per
|
|
168
186
|
// process, not every run). Lives in the factory scope so the cron closure
|
|
169
187
|
// shares it across runs.
|
|
170
188
|
let warnedMissingExportUrl = false;
|
|
189
|
+
let warnedMissingMailDelivery = false;
|
|
171
190
|
const showDeletion = opts.privacyCenterShowDeletion ?? true;
|
|
172
191
|
return defineFeature("user-data-rights", (r) => {
|
|
173
192
|
r.describe(
|
|
@@ -197,7 +216,15 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
197
216
|
// EXT_USER_DATA channel owner: any r.useExtension(EXT_USER_DATA, ...)
|
|
198
217
|
// usage already requires user-data-rights to be mounted.
|
|
199
218
|
r.bootCheck(({ features }) => validateGdprHookCompleteness(features));
|
|
200
|
-
r.bootCheck(({ features }) =>
|
|
219
|
+
r.bootCheck(({ features }) => {
|
|
220
|
+
validateGdprPiiHookCoverage(features);
|
|
221
|
+
if (!warnedMissingMailDelivery && isMailTransportMountedWithoutDelivery(features)) {
|
|
222
|
+
warnedMissingMailDelivery = true;
|
|
223
|
+
createFallbackLogger("user-data-rights").warn(
|
|
224
|
+
"mail-foundation and a mail transport are mounted but delivery/channel-email are not — the default export and deletion-request mails need them and will not be sent",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
201
228
|
|
|
202
229
|
// App-level tenant-occupancy model. Default "multi-user" → tenant-scoped
|
|
203
230
|
// contributors (e.g. credit) NEVER erase tenant data on a per-user forget
|
|
@@ -312,6 +339,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
312
339
|
});
|
|
313
340
|
|
|
314
341
|
r.translations({ keys: USER_DATA_RIGHTS_I18N });
|
|
342
|
+
registerGdprMails(r);
|
|
315
343
|
// Privacy-center dot-form labels (i18nKey()-marked below), duplicated in
|
|
316
344
|
// web/i18n.ts's client bundle since privacy-center-screen.tsx renders
|
|
317
345
|
// them client-side.
|
|
@@ -520,10 +548,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
520
548
|
handler: async (c, { app, clientIp }) => {
|
|
521
549
|
const url = new URL(c.req.url);
|
|
522
550
|
const parsedBody = await c.req.json().catch(() => null);
|
|
523
|
-
const token =
|
|
524
|
-
parsedBody && typeof parsedBody === "object" && "token" in parsedBody
|
|
525
|
-
? (parsedBody as { token?: unknown }).token
|
|
526
|
-
: undefined;
|
|
551
|
+
const token = isPlainObject(parsedBody) ? parsedBody["token"] : undefined;
|
|
527
552
|
if (typeof token !== "string" || token.length === 0) {
|
|
528
553
|
return c.json({ error: "missing_token" }, 400);
|
|
529
554
|
}
|
|
@@ -605,28 +630,25 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
605
630
|
// versendet der Cron die Export-Notifications selbst (Default-Templates).
|
|
606
631
|
// Der Resolver baut den per-Tenant-Transport aus configResolver — gleiche
|
|
607
632
|
// Bruecke wie buildStorageProvider.
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
handlerName: "user-data-rights:run-export-jobs",
|
|
616
|
-
})
|
|
617
|
-
: undefined;
|
|
633
|
+
// The cron runs across tenants: each mail needs a notify bound to the
|
|
634
|
+
// recipient's tenant (template override, channel config).
|
|
635
|
+
const notifyFactory = ctx._notifyFactory;
|
|
636
|
+
const exportNotifyFor =
|
|
637
|
+
notifyFactory !== undefined && isGdprMailDeliveryAvailable(exportRegistry)
|
|
638
|
+
? (tenantId: TenantId) => notifyFactory(createSystemUser(tenantId), tenantId)
|
|
639
|
+
: undefined;
|
|
618
640
|
const sendExportReadyEmail =
|
|
619
641
|
opts.sendExportReadyEmail ??
|
|
620
|
-
(
|
|
621
|
-
? makeDefaultExportReadyEmail(
|
|
642
|
+
(exportNotifyFor && opts.appExportDownloadUrl !== undefined
|
|
643
|
+
? makeDefaultExportReadyEmail(exportNotifyFor, opts.mailDefaults)
|
|
622
644
|
: undefined);
|
|
623
645
|
const sendExportFailedEmail =
|
|
624
646
|
opts.sendExportFailedEmail ??
|
|
625
|
-
(
|
|
626
|
-
? makeDefaultExportFailedEmail(
|
|
647
|
+
(exportNotifyFor
|
|
648
|
+
? makeDefaultExportFailedEmail(exportNotifyFor, opts.mailDefaults)
|
|
627
649
|
: undefined);
|
|
628
650
|
if (
|
|
629
|
-
|
|
651
|
+
exportNotifyFor &&
|
|
630
652
|
!opts.sendExportReadyEmail &&
|
|
631
653
|
opts.appExportDownloadUrl === undefined &&
|
|
632
654
|
!warnedMissingExportUrl
|
|
@@ -634,7 +656,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
634
656
|
warnedMissingExportUrl = true;
|
|
635
657
|
// biome-ignore lint/suspicious/noConsole: one-shot operator visibility for misconfig
|
|
636
658
|
console.warn(
|
|
637
|
-
"[user-data-rights:run-export-jobs] mail
|
|
659
|
+
"[user-data-rights:run-export-jobs] mail delivery mounted but appExportDownloadUrl unset — default export-ready emails disabled (export-failed + deletion mails still send)",
|
|
638
660
|
);
|
|
639
661
|
}
|
|
640
662
|
|
|
@@ -697,22 +719,37 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
697
719
|
userId: forgetUserId,
|
|
698
720
|
});
|
|
699
721
|
|
|
700
|
-
//
|
|
701
|
-
// mail
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
registry: forgetRegistry,
|
|
705
|
-
configResolver: ctx.configResolver,
|
|
706
|
-
secrets: ctx.secrets,
|
|
707
|
-
db: forgetDb,
|
|
708
|
-
userId: forgetUserId,
|
|
709
|
-
handlerName: "user-data-rights:run-forget-cleanup",
|
|
710
|
-
})
|
|
711
|
-
: undefined;
|
|
722
|
+
// Without an app callback the default mail goes out when a mail transport and a
|
|
723
|
+
// mail renderer are mounted. The user is already erased here, so it bypasses the
|
|
724
|
+
// delivery log: an attempt row would be new personal data.
|
|
725
|
+
const forgetConfigResolver = ctx.configResolver;
|
|
712
726
|
const sendDeletionExecutedEmail =
|
|
713
727
|
opts.sendDeletionExecutedEmail ??
|
|
714
|
-
(
|
|
715
|
-
? makeDefaultDeletionExecutedEmail(
|
|
728
|
+
(isMailTransportAvailable(forgetRegistry) && isMailRendererAvailable(forgetRegistry)
|
|
729
|
+
? makeDefaultDeletionExecutedEmail(
|
|
730
|
+
{
|
|
731
|
+
registry: forgetRegistry,
|
|
732
|
+
db: forgetDb,
|
|
733
|
+
resolveTransport: makeTenantMailTransportResolver({
|
|
734
|
+
registry: forgetRegistry,
|
|
735
|
+
configResolver: forgetConfigResolver,
|
|
736
|
+
secrets: ctx.secrets,
|
|
737
|
+
db: forgetDb,
|
|
738
|
+
userId: forgetUserId,
|
|
739
|
+
handlerName: "user-data-rights:run-forget-cleanup",
|
|
740
|
+
}),
|
|
741
|
+
configFor: (tenantId) =>
|
|
742
|
+
forgetConfigResolver &&
|
|
743
|
+
createConfigAccessor(
|
|
744
|
+
forgetRegistry,
|
|
745
|
+
forgetConfigResolver,
|
|
746
|
+
tenantId,
|
|
747
|
+
forgetUserId,
|
|
748
|
+
forgetDb,
|
|
749
|
+
),
|
|
750
|
+
},
|
|
751
|
+
opts.mailDefaults,
|
|
752
|
+
)
|
|
716
753
|
: undefined);
|
|
717
754
|
|
|
718
755
|
const forgetResult = await runForgetCleanup({
|
|
@@ -71,12 +71,13 @@ export const cancelDeletionWrite = defineWriteHandler({
|
|
|
71
71
|
|
|
72
72
|
// gracePeriodEnd=null im Response symmetrisch zu request-deletion's
|
|
73
73
|
// ISO-Timestamp — Frontend kann beide Endpoints uniform behandeln.
|
|
74
|
+
const gracePeriodEnd: string | null = null;
|
|
74
75
|
return {
|
|
75
76
|
isSuccess: true as const,
|
|
76
77
|
data: {
|
|
77
78
|
userId: event.user.id,
|
|
78
79
|
status: USER_STATUS.Active,
|
|
79
|
-
gracePeriodEnd
|
|
80
|
+
gracePeriodEnd,
|
|
80
81
|
},
|
|
81
82
|
};
|
|
82
83
|
},
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { createTransportForTenant } from "@cosmicdrift/kumiko-bundled-features/mail-foundation";
|
|
2
1
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
2
|
import { writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
3
|
import * as z from "zod";
|
|
5
4
|
import { USER_STATUS } from "../../user/index.js";
|
|
6
5
|
import {
|
|
7
6
|
type GdprMailDefaults,
|
|
8
|
-
|
|
7
|
+
isGdprMailDeliveryAvailable,
|
|
9
8
|
makeDefaultDeletionRequestedEmail,
|
|
10
9
|
} from "../lib/default-mailers.js";
|
|
11
10
|
import { resolveGracePeriod, startDeletionGracePeriod } from "./deletion-grace-period.js";
|
|
@@ -71,17 +70,12 @@ export function createRequestDeletionHandler(opts: RequestDeletionOptions = {})
|
|
|
71
70
|
if (!res.ok) return writeFailure(res.error);
|
|
72
71
|
const { gracePeriodEnd, userEmail, userLocale } = res;
|
|
73
72
|
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// Ohne gemounteten mail-transport bleibt send undefined → keine Mail.
|
|
73
|
+
// The app callback wins; otherwise the default mail goes through ctx.notify after
|
|
74
|
+
// the commit. Without delivery and an email channel there is no sender and no mail.
|
|
77
75
|
const send =
|
|
78
76
|
opts.sendDeletionRequestedEmail ??
|
|
79
|
-
(
|
|
80
|
-
? makeDefaultDeletionRequestedEmail(
|
|
81
|
-
(tenantId) =>
|
|
82
|
-
createTransportForTenant(ctx, tenantId, "user-data-rights:request-deletion"),
|
|
83
|
-
opts.mailDefaults,
|
|
84
|
-
)
|
|
77
|
+
(ctx.notify !== undefined && isGdprMailDeliveryAvailable(ctx.registry)
|
|
78
|
+
? makeDefaultDeletionRequestedEmail(ctx.notify, opts.mailDefaults)
|
|
85
79
|
: undefined);
|
|
86
80
|
|
|
87
81
|
// Best-effort Email-Notification. Send-Failure darf das Write nicht
|