@cosmicdrift/kumiko-bundled-features 0.362.0 → 0.363.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/dist/admin-shell/feature.d.ts +3 -0
- package/dist/admin-shell/feature.js +6 -1
- package/dist/agent-tools/agent-manifest.js +2 -1
- package/dist/agent-tools/filter-ops.d.ts +1 -1
- package/dist/agent-tools/filter-ops.js +1 -3
- package/dist/agent-tools/tool-dispatch.js +2 -1
- package/dist/auth-email-password/email-templates.d.ts +4 -26
- package/dist/auth-email-password/email-templates.js +9 -101
- package/dist/auth-email-password/feature.js +24 -6
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
- package/dist/auth-email-password/handlers/request-account-unlock.write.js +2 -4
- package/dist/auth-email-password/handlers/request-email-verification.write.js +2 -4
- package/dist/auth-email-password/handlers/request-password-reset.write.js +2 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +3 -4
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -2
- package/dist/auth-email-password/handlers/token-request-handler.js +0 -1
- package/dist/auth-email-password/index.d.ts +2 -2
- package/dist/auth-email-password/index.js +1 -1
- package/dist/auth-email-password/magic-link-mail.d.ts +3 -2
- package/dist/auth-email-password/magic-link-mail.js +19 -14
- package/dist/auth-email-password/mail-notifications.d.ts +32 -0
- package/dist/auth-email-password/mail-notifications.js +116 -0
- package/dist/auth-email-password/signup-activation.d.ts +1 -1
- package/dist/auth-email-password/signup-activation.js +2 -3
- package/dist/auth-foundation/types.js +2 -8
- package/dist/auth-mfa/feature.js +4 -4
- package/dist/auth-mfa/hmac-token-codec.js +4 -4
- package/dist/auth-mfa/mfa-challenge-token.js +3 -3
- package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -3
- package/dist/auth-mfa/mfa-setup-token.js +8 -8
- package/dist/billing-foundation/constants.d.ts +3 -3
- package/dist/billing-foundation/constants.js +5 -4
- package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +32 -14
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +27 -32
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +3 -3
- package/dist/billing-foundation/consumer-protection/mail-notifications.d.ts +8 -0
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +95 -0
- package/dist/billing-foundation/consumer-protection/register-consent.js +2 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +20 -10
- package/dist/billing-foundation/consumer-protection/termination-mail.js +10 -21
- package/dist/billing-foundation/consumer-protection/termination-notify.js +2 -2
- package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -3
- package/dist/billing-foundation/consumer-protection/termination-request.js +2 -2
- package/dist/billing-foundation/handlers/process-event.write.js +2 -2
- package/dist/billing-foundation/handlers/process-payment-event.write.js +1 -1
- package/dist/billing-foundation/subscription-tier-sync.js +3 -4
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -1
- package/dist/cap-overview/web/cap-usage-cell.js +5 -4
- package/dist/channel-email/email-channel.d.ts +2 -2
- package/dist/channel-email/email-channel.js +89 -26
- package/dist/channel-email/feature.js +3 -3
- package/dist/channel-email/index.d.ts +1 -1
- package/dist/channel-email/index.js +1 -1
- package/dist/channel-email/pii-guard.js +10 -0
- package/dist/channel-email/smtp-transport.js +13 -0
- package/dist/channel-email/types.d.ts +8 -0
- package/dist/config/handlers/set.write.js +1 -2
- package/dist/custom-fields/lib/value-schema.js +10 -11
- package/dist/custom-fields/wire-user-data-rights.js +7 -6
- package/dist/data-retention/index.d.ts +1 -0
- package/dist/data-retention/index.js +1 -0
- package/dist/data-retention/run-retention-cleanup.js +6 -6
- package/dist/delivery/attachment-ref.d.ts +39 -0
- package/dist/delivery/attachment-ref.js +64 -0
- package/dist/delivery/channel-context.d.ts +8 -4
- package/dist/delivery/channel-context.js +76 -2
- package/dist/delivery/constants.d.ts +1 -0
- package/dist/delivery/constants.js +1 -0
- package/dist/delivery/delivery-service.d.ts +4 -1
- package/dist/delivery/delivery-service.js +38 -32
- package/dist/delivery/feature.js +1 -1
- package/dist/delivery/index.d.ts +5 -2
- package/dist/delivery/index.js +4 -1
- package/dist/delivery/jobs.js +25 -5
- package/dist/delivery/notify-factory.js +4 -1
- package/dist/delivery/testing.d.ts +4 -1
- package/dist/delivery/types.d.ts +20 -14
- package/dist/delivery/unsubscribe.js +4 -1
- package/dist/derivatives-sharp/render.js +1 -18
- package/dist/document-ingest-foundation/pages.js +4 -4
- package/dist/feature-toggles/toggle-sync-signal.js +4 -4
- package/dist/file-foundation/index.d.ts +1 -0
- package/dist/file-foundation/index.js +1 -0
- package/dist/file-foundation/read-file-ref-content.d.ts +27 -0
- package/dist/file-foundation/read-file-ref-content.js +49 -0
- package/dist/folders/web/folder-manager.js +3 -2
- package/dist/form-draft/db/queries/cleanup.js +4 -4
- package/dist/form-draft/db/queries/draft-count.js +2 -2
- package/dist/form-draft/db/queries/owned-file-refs.js +2 -2
- package/dist/form-draft/release-file-refs.js +3 -2
- package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +6 -6
- package/dist/inbound-mail-foundation/handlers/scope-visibility.d.ts +1 -1
- package/dist/inbound-mail-foundation/oauth-state.js +7 -7
- package/dist/inbound-mail-foundation/types.js +4 -4
- package/dist/inbound-mail-foundation/watch-supervisor.js +22 -13
- package/dist/inbound-provider-imap/imap-client.js +4 -3
- package/dist/ledger/recurring.js +1 -3
- package/dist/managed-pages/branding.d.ts +2 -0
- package/dist/managed-pages/branding.js +24 -2
- package/dist/managed-pages/feature.js +1 -0
- package/dist/managed-pages/i18n.js +1 -0
- package/dist/managed-pages/screens/branding-screen.js +8 -1
- package/dist/page-render/branding.d.ts +1 -0
- package/dist/page-render/branding.js +1 -0
- package/dist/page-render/markdown.js +1 -1
- package/dist/personal-access-tokens/feature.js +1 -1
- package/dist/personal-access-tokens/handlers/list.query.js +2 -1
- package/dist/personal-access-tokens/revoke-for-user.js +2 -1
- package/dist/renderer-foundation/api.d.ts +13 -13
- package/dist/renderer-foundation/api.js +1 -1
- package/dist/renderer-foundation/constants.js +2 -4
- package/dist/renderer-foundation/feature.js +7 -7
- package/dist/renderer-foundation/index.d.ts +2 -1
- package/dist/renderer-foundation/index.js +1 -0
- package/dist/renderer-foundation/types.d.ts +92 -32
- package/dist/renderer-pdf/feature.js +7 -1
- package/dist/sessions/db/queries/cleanup.js +2 -2
- package/dist/sessions/feature.js +1 -1
- package/dist/sessions/handlers/list.query.js +2 -1
- package/dist/shared/attachment-file-name.d.ts +3 -0
- package/dist/shared/attachment-file-name.js +35 -0
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.js +3 -0
- package/dist/shared/is-tenant-db.js +1 -1
- package/dist/shared/mail-template-body.d.ts +4 -0
- package/dist/shared/mail-template-body.js +17 -0
- package/dist/shared/sharp-input-limits.d.ts +6 -0
- package/dist/shared/sharp-input-limits.js +22 -0
- package/dist/shared/signup-handover.js +2 -2
- package/dist/tags/handlers/assign-tag.write.js +3 -1
- package/dist/tags/web/tag-chip.js +2 -0
- package/dist/template-resolver/api.d.ts +30 -2
- package/dist/template-resolver/api.js +40 -5
- package/dist/template-resolver/compile-document-body.d.ts +8 -0
- package/dist/template-resolver/compile-document-body.js +163 -0
- package/dist/template-resolver/constants.d.ts +13 -4
- package/dist/template-resolver/constants.js +30 -10
- package/dist/template-resolver/document-sanitizer.d.ts +32 -0
- package/dist/template-resolver/document-sanitizer.js +201 -0
- package/dist/template-resolver/document-schema.d.ts +21 -0
- package/dist/template-resolver/document-schema.js +296 -0
- package/dist/template-resolver/feature.d.ts +18 -0
- package/dist/template-resolver/feature.js +50 -6
- package/dist/template-resolver/handlers/bundle-import.d.ts +20 -0
- package/dist/template-resolver/handlers/bundle-import.js +147 -0
- package/dist/template-resolver/handlers/collection-set.write.js +25 -2
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +19 -0
- package/dist/template-resolver/handlers/copy-locale.write.js +116 -0
- package/dist/template-resolver/handlers/diff-to-system.query.d.ts +22 -0
- package/dist/template-resolver/handlers/diff-to-system.query.js +64 -0
- package/dist/template-resolver/handlers/export-bundle.query.d.ts +35 -0
- package/dist/template-resolver/handlers/export-bundle.query.js +68 -0
- package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
- package/dist/template-resolver/handlers/find-by-id.query.js +3 -3
- package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +39 -0
- package/dist/template-resolver/handlers/import-bundle-system.write.js +36 -0
- package/dist/template-resolver/handlers/import-bundle.write.d.ts +40 -0
- package/dist/template-resolver/handlers/import-bundle.write.js +19 -0
- package/dist/template-resolver/handlers/layout-source.query.d.ts +26 -0
- package/dist/template-resolver/handlers/layout-source.query.js +64 -0
- package/dist/template-resolver/handlers/list.query.d.ts +4 -1
- package/dist/template-resolver/handlers/list.query.js +5 -7
- package/dist/template-resolver/handlers/overview.query.d.ts +59 -0
- package/dist/template-resolver/handlers/overview.query.js +165 -0
- package/dist/template-resolver/handlers/preview.query.d.ts +12 -0
- package/dist/template-resolver/handlers/preview.query.js +24 -0
- package/dist/template-resolver/handlers/reset-to-system.write.d.ts +8 -0
- package/dist/template-resolver/handlers/reset-to-system.write.js +50 -0
- package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +8 -0
- package/dist/template-resolver/handlers/resource-link-pdf.write.js +32 -0
- package/dist/template-resolver/handlers/resource-remove.write.d.ts +9 -0
- package/dist/template-resolver/handlers/resource-remove.write.js +44 -0
- package/dist/template-resolver/handlers/resource-shared.d.ts +9 -0
- package/dist/template-resolver/handlers/resource-shared.js +14 -0
- package/dist/template-resolver/handlers/resource-upload.write.d.ts +13 -0
- package/dist/template-resolver/handlers/resource-upload.write.js +98 -0
- package/dist/template-resolver/handlers/send-test.write.d.ts +11 -0
- package/dist/template-resolver/handlers/send-test.write.js +81 -0
- package/dist/template-resolver/handlers/set.write.d.ts +4 -1
- package/dist/template-resolver/handlers/set.write.js +25 -3
- package/dist/template-resolver/handlers/shared.d.ts +9 -5
- package/dist/template-resolver/handlers/shared.js +6 -2
- package/dist/template-resolver/handlers/template-gates.d.ts +36 -0
- package/dist/template-resolver/handlers/template-gates.js +131 -0
- package/dist/template-resolver/handlers/template-write.d.ts +45 -0
- package/dist/template-resolver/handlers/template-write.js +109 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +17 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.js +48 -0
- package/dist/template-resolver/handlers/toggle-status.write.d.ts +12 -0
- package/dist/template-resolver/handlers/toggle-status.write.js +27 -11
- package/dist/template-resolver/handlers/upsert-system.write.d.ts +5 -8
- package/dist/template-resolver/handlers/upsert-system.write.js +14 -59
- package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +5 -8
- package/dist/template-resolver/handlers/upsert-tenant.write.js +10 -61
- package/dist/template-resolver/handlers/usage.query.d.ts +17 -0
- package/dist/template-resolver/handlers/usage.query.js +15 -0
- package/dist/template-resolver/handlers/validate.query.d.ts +74 -0
- package/dist/template-resolver/handlers/validate.query.js +80 -0
- package/dist/template-resolver/index.d.ts +18 -1
- package/dist/template-resolver/index.js +16 -1
- package/dist/template-resolver/layout-regions.d.ts +23 -0
- package/dist/template-resolver/layout-regions.js +125 -0
- package/dist/template-resolver/linked-resources.d.ts +17 -0
- package/dist/template-resolver/linked-resources.js +75 -0
- package/dist/template-resolver/preview-display-html.d.ts +7 -0
- package/dist/template-resolver/preview-display-html.js +27 -0
- package/dist/template-resolver/print-document.d.ts +1 -0
- package/dist/template-resolver/print-document.js +55 -0
- package/dist/template-resolver/qualified-names.d.ts +30 -0
- package/dist/template-resolver/qualified-names.js +30 -0
- package/dist/template-resolver/record-mapping.d.ts +2 -0
- package/dist/template-resolver/record-mapping.js +21 -0
- package/dist/template-resolver/resource-image.d.ts +15 -0
- package/dist/template-resolver/resource-image.js +34 -0
- package/dist/template-resolver/seeding.d.ts +14 -1
- package/dist/template-resolver/seeding.js +105 -15
- package/dist/template-resolver/store-template-resource.d.ts +64 -0
- package/dist/template-resolver/store-template-resource.js +264 -0
- package/dist/template-resolver/table.d.ts +47 -8
- package/dist/template-resolver/table.js +11 -0
- package/dist/template-resolver/template-bundle.d.ts +36 -0
- package/dist/template-resolver/template-bundle.js +60 -0
- package/dist/template-resolver/template-diff.d.ts +7 -0
- package/dist/template-resolver/template-diff.js +68 -0
- package/dist/template-resolver/template-meta.d.ts +60 -0
- package/dist/template-resolver/template-meta.js +101 -0
- package/dist/template-resolver/template-preview.d.ts +86 -0
- package/dist/template-resolver/template-preview.js +374 -0
- package/dist/template-resolver/template-resources.d.ts +14 -0
- package/dist/template-resolver/template-resources.js +42 -0
- package/dist/template-resolver/template-usage.d.ts +12 -0
- package/dist/template-resolver/template-usage.js +30 -0
- package/dist/template-resolver/template-validation.d.ts +22 -0
- package/dist/template-resolver/template-validation.js +172 -0
- package/dist/template-resolver/testing.js +5 -5
- package/dist/template-resolver/user-content-table.d.ts +14 -8
- package/dist/template-resolver/web/client-plugin.d.ts +1 -0
- package/dist/template-resolver/web/client-plugin.js +9 -6
- package/dist/template-resolver/web/document-editor-extensions.d.ts +15 -0
- package/dist/template-resolver/web/document-editor-extensions.js +107 -0
- package/dist/template-resolver/web/document-editor.d.ts +3 -0
- package/dist/template-resolver/web/document-editor.js +11 -0
- package/dist/template-resolver/web/document-tiptap-editor.d.ts +3 -0
- package/dist/template-resolver/web/document-tiptap-editor.js +376 -0
- package/dist/template-resolver/web/document-toolbar-blocks.d.ts +33 -0
- package/dist/template-resolver/web/document-toolbar-blocks.js +15 -0
- package/dist/template-resolver/web/editor-handle.d.ts +3 -0
- package/dist/template-resolver/web/editor-handle.js +24 -0
- package/dist/template-resolver/web/i18n.js +76 -0
- package/dist/template-resolver/web/index.d.ts +4 -0
- package/dist/template-resolver/web/index.js +4 -0
- package/dist/template-resolver/web/placeholder-line-editor-lazy.d.ts +4 -0
- package/dist/template-resolver/web/placeholder-line-editor-lazy.js +16 -0
- package/dist/template-resolver/web/placeholder-line-editor.d.ts +15 -0
- package/dist/template-resolver/web/placeholder-line-editor.js +94 -0
- package/dist/template-resolver/web/placeholder-text.d.ts +4 -0
- package/dist/template-resolver/web/placeholder-text.js +34 -0
- package/dist/template-resolver/web/use-stable-by-key.d.ts +1 -0
- package/dist/template-resolver/web/use-stable-by-key.js +13 -0
- package/dist/tenant/handlers/team-list.query.js +3 -2
- package/dist/tenant/seeding.js +31 -6
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +2 -1
- package/dist/user/db/queries/stream-tenant-backfill.js +4 -4
- package/dist/user/handlers/detail.query.js +2 -1
- package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
- package/dist/user-data-rights/email-templates.d.ts +1 -29
- package/dist/user-data-rights/email-templates.js +5 -106
- package/dist/user-data-rights/feature.js +53 -35
- package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
- package/dist/user-data-rights/lib/default-mailers.d.ts +15 -7
- package/dist/user-data-rights/lib/default-mailers.js +100 -48
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/user-data-rights/mail-notifications.d.ts +13 -0
- package/dist/user-data-rights/mail-notifications.js +118 -0
- package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
- package/dist/user-profile/web/profile-screen.js +2 -1
- package/dist/waitlist/feature.js +10 -0
- package/dist/waitlist/handlers/submit.write.js +3 -4
- package/dist/waitlist/mail.d.ts +19 -6
- package/dist/waitlist/mail.js +50 -14
- package/package.json +28 -17
- package/src/admin-shell/changes.json +6 -0
- package/src/auth-email-password/changes.json +14 -0
- package/src/billing-foundation/changes.json +7 -0
- package/src/channel-email/changes.json +19 -0
- package/src/data-retention/changes.json +6 -0
- package/src/delivery/changes.json +48 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/renderer-foundation/changes.json +39 -0
- package/src/template-resolver/changes.json +118 -0
- package/src/user-data-rights/changes.json +7 -0
- package/src/waitlist/changes.json +7 -0
- package/dist/renderer-simple/feature.d.ts +0 -10
- package/dist/renderer-simple/feature.js +0 -42
- package/dist/renderer-simple/index.d.ts +0 -3
- package/dist/renderer-simple/index.js +0 -2
- package/dist/renderer-simple/mail-blocks.d.ts +0 -21
- package/dist/renderer-simple/mail-blocks.js +0 -107
- package/dist/renderer-simple/resolve-variables.d.ts +0 -7
- package/dist/renderer-simple/resolve-variables.js +0 -47
- package/dist/renderer-simple/simple-renderer.d.ts +0 -24
- package/dist/renderer-simple/simple-renderer.js +0 -200
- package/src/renderer-simple/changes.json +0 -27
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// kumiko-lint-ignore app-feature-structure Phase-3 conversion tracked in #2312 (feature.ts > 300 lines)
|
|
2
2
|
import { UNKNOWN_CLIENT_IP } from "@cosmicdrift/kumiko-framework/api";
|
|
3
|
-
import { createSystemConfig, defineFeature, EXT_USER_DATA, i18nKey, SYSTEM_USER_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { createSystemConfig, createSystemUser, defineFeature, EXT_USER_DATA, i18nKey, SYSTEM_USER_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
4
5
|
import { createMetricsHandle, createNoopMetricsHandle, } from "@cosmicdrift/kumiko-framework/observability";
|
|
6
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
7
|
+
import { createConfigAccessor } from "../config/index.js";
|
|
5
8
|
import { validateGdprHookCompleteness, validateGdprPiiHookCoverage } from "./boot-checks.js";
|
|
6
9
|
import { EXPORT_SECTION_EXTENSION_NAME, PRIVACY_CENTER_SCREEN_ID, STATUS_OPTION_KEY_PREFIX, USER_ME_QUERY, UserDataRightsHandlers, } from "./constants.js";
|
|
7
10
|
import { cancelDeletionWrite } from "./handlers/cancel-deletion.write.js";
|
|
@@ -21,21 +24,30 @@ import { requestExportWrite } from "./handlers/request-export.write.js";
|
|
|
21
24
|
import { restrictAccountWrite } from "./handlers/restrict-account.write.js";
|
|
22
25
|
import { createRunForgetCleanupHandler } from "./handlers/run-forget-cleanup.write.js";
|
|
23
26
|
import { USER_DATA_RIGHTS_I18N } from "./i18n.js";
|
|
24
|
-
import { isMailTransportAvailable, makeDefaultDeletionExecutedEmail, makeDefaultExportFailedEmail, makeDefaultExportReadyEmail, } from "./lib/default-mailers.js";
|
|
27
|
+
import { isGdprMailDeliveryAvailable, isMailRendererAvailable, isMailTransportAvailable, makeDefaultDeletionExecutedEmail, makeDefaultExportFailedEmail, makeDefaultExportReadyEmail, } from "./lib/default-mailers.js";
|
|
25
28
|
import { makeTenantMailTransportResolver } from "./lib/mail-transport-resolver.js";
|
|
26
29
|
import { resolveAppTenantModel } from "./lib/resolve-tenant-model.js";
|
|
27
30
|
import { makeTenantStorageProviderResolver } from "./lib/storage-provider-resolver.js";
|
|
31
|
+
import { registerGdprMails } from "./mail-notifications.js";
|
|
28
32
|
import { EXPORT_CLEANUP_BACKLOG_AGE_METRIC, EXPORT_CLEANUP_LAST_RUN_TIMESTAMP_METRIC, runExportJobs, } from "./run-export-jobs.js";
|
|
29
33
|
import { runForgetCleanup, sanitizeReasonForLog, } from "./run-forget-cleanup.js";
|
|
30
34
|
import { downloadAttemptEntity } from "./schema/download-attempt.js";
|
|
31
35
|
import { exportDownloadTokenEntity } from "./schema/download-token.js";
|
|
32
36
|
import { exportJobEntity } from "./schema/export-job.js";
|
|
33
37
|
import { downloadAttemptListScreen, exportJobDetailScreen, exportJobListScreen, } from "./screens.js";
|
|
38
|
+
function isMailTransportMountedWithoutDelivery(features) {
|
|
39
|
+
const names = new Set(features.map((feature) => feature.name));
|
|
40
|
+
const hasTransport = [...names].some((name) => name.startsWith("mail-transport-"));
|
|
41
|
+
return (names.has("mail-foundation") &&
|
|
42
|
+
hasTransport &&
|
|
43
|
+
!(names.has("delivery") && names.has("channel-email")));
|
|
44
|
+
}
|
|
34
45
|
export function createUserDataRightsFeature(opts = {}) {
|
|
35
46
|
// One-shot operator warning (the export cron fires daily — warn once per
|
|
36
47
|
// process, not every run). Lives in the factory scope so the cron closure
|
|
37
48
|
// shares it across runs.
|
|
38
49
|
let warnedMissingExportUrl = false;
|
|
50
|
+
let warnedMissingMailDelivery = false;
|
|
39
51
|
const showDeletion = opts.privacyCenterShowDeletion ?? true;
|
|
40
52
|
return defineFeature("user-data-rights", (r) => {
|
|
41
53
|
r.describe('Implements GDPR Art. 15 (access / `my-audit-log` query), Art. 17 (erasure / `request-deletion` + `cancel-deletion`, plus the anonymous email-verified `request-deletion-by-email` + `confirm-deletion-by-token` flow for lockout-safe self-service, + cron cleanup with grace period), Art. 18 (restriction / `restrict-account` + `lift-restriction`), and Art. 20 (portability / async `request-export` \u2192 ZIP via `file-foundation`, Magic-Link download) as first-class HTTP handlers and cron jobs. Each domain feature opts in by calling `r.useExtension(EXT_USER_DATA, "<entity>", { export, delete })` \u2014 the feature then orchestrates the export and forget pipelines across all registered hooks automatically. When `mail-foundation` and a `mail-transport-*` are mounted, it also sends the four GDPR notifications (export ready/failed, deletion requested/executed) itself with no app callback code, rendered in each recipient’s locale. Requires `user`, `data-retention`, `compliance-profiles`, and `sessions`.');
|
|
@@ -61,7 +73,13 @@ export function createUserDataRightsFeature(opts = {}) {
|
|
|
61
73
|
// EXT_USER_DATA channel owner: any r.useExtension(EXT_USER_DATA, ...)
|
|
62
74
|
// usage already requires user-data-rights to be mounted.
|
|
63
75
|
r.bootCheck(({ features }) => validateGdprHookCompleteness(features));
|
|
64
|
-
r.bootCheck(({ features }) =>
|
|
76
|
+
r.bootCheck(({ features }) => {
|
|
77
|
+
validateGdprPiiHookCoverage(features);
|
|
78
|
+
if (!warnedMissingMailDelivery && isMailTransportMountedWithoutDelivery(features)) {
|
|
79
|
+
warnedMissingMailDelivery = true;
|
|
80
|
+
createFallbackLogger("user-data-rights").warn("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");
|
|
81
|
+
}
|
|
82
|
+
});
|
|
65
83
|
// App-level tenant-occupancy model. Default "multi-user" → tenant-scoped
|
|
66
84
|
// contributors (e.g. credit) NEVER erase tenant data on a per-user forget
|
|
67
85
|
// (would harm co-members). An app with one user per tenant sets this to
|
|
@@ -147,6 +165,7 @@ export function createUserDataRightsFeature(opts = {}) {
|
|
|
147
165
|
access: { roles: ["SystemAdmin"] },
|
|
148
166
|
});
|
|
149
167
|
r.translations({ keys: USER_DATA_RIGHTS_I18N });
|
|
168
|
+
registerGdprMails(r);
|
|
150
169
|
// Privacy-center dot-form labels (i18nKey()-marked below), duplicated in
|
|
151
170
|
// web/i18n.ts's client bundle since privacy-center-screen.tsx renders
|
|
152
171
|
// them client-side.
|
|
@@ -350,9 +369,7 @@ export function createUserDataRightsFeature(opts = {}) {
|
|
|
350
369
|
handler: async (c, { app, clientIp }) => {
|
|
351
370
|
const url = new URL(c.req.url);
|
|
352
371
|
const parsedBody = await c.req.json().catch(() => null);
|
|
353
|
-
const token = parsedBody
|
|
354
|
-
? parsedBody.token
|
|
355
|
-
: undefined;
|
|
372
|
+
const token = isPlainObject(parsedBody) ? parsedBody["token"] : undefined;
|
|
356
373
|
if (typeof token !== "string" || token.length === 0) {
|
|
357
374
|
return c.json({ error: "missing_token" }, 400);
|
|
358
375
|
}
|
|
@@ -426,31 +443,27 @@ export function createUserDataRightsFeature(opts = {}) {
|
|
|
426
443
|
// versendet der Cron die Export-Notifications selbst (Default-Templates).
|
|
427
444
|
// Der Resolver baut den per-Tenant-Transport aus configResolver — gleiche
|
|
428
445
|
// Bruecke wie buildStorageProvider.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
db: exportDb,
|
|
435
|
-
userId: exportUserId,
|
|
436
|
-
handlerName: "user-data-rights:run-export-jobs",
|
|
437
|
-
})
|
|
446
|
+
// The cron runs across tenants: each mail needs a notify bound to the
|
|
447
|
+
// recipient's tenant (template override, channel config).
|
|
448
|
+
const notifyFactory = ctx._notifyFactory;
|
|
449
|
+
const exportNotifyFor = notifyFactory !== undefined && isGdprMailDeliveryAvailable(exportRegistry)
|
|
450
|
+
? (tenantId) => notifyFactory(createSystemUser(tenantId), tenantId)
|
|
438
451
|
: undefined;
|
|
439
452
|
const sendExportReadyEmail = opts.sendExportReadyEmail ??
|
|
440
|
-
(
|
|
441
|
-
? makeDefaultExportReadyEmail(
|
|
453
|
+
(exportNotifyFor && opts.appExportDownloadUrl !== undefined
|
|
454
|
+
? makeDefaultExportReadyEmail(exportNotifyFor, opts.mailDefaults)
|
|
442
455
|
: undefined);
|
|
443
456
|
const sendExportFailedEmail = opts.sendExportFailedEmail ??
|
|
444
|
-
(
|
|
445
|
-
? makeDefaultExportFailedEmail(
|
|
457
|
+
(exportNotifyFor
|
|
458
|
+
? makeDefaultExportFailedEmail(exportNotifyFor, opts.mailDefaults)
|
|
446
459
|
: undefined);
|
|
447
|
-
if (
|
|
460
|
+
if (exportNotifyFor &&
|
|
448
461
|
!opts.sendExportReadyEmail &&
|
|
449
462
|
opts.appExportDownloadUrl === undefined &&
|
|
450
463
|
!warnedMissingExportUrl) {
|
|
451
464
|
warnedMissingExportUrl = true;
|
|
452
465
|
// biome-ignore lint/suspicious/noConsole: one-shot operator visibility for misconfig
|
|
453
|
-
console.warn("[user-data-rights:run-export-jobs] mail
|
|
466
|
+
console.warn("[user-data-rights:run-export-jobs] mail delivery mounted but appExportDownloadUrl unset — default export-ready emails disabled (export-failed + deletion mails still send)");
|
|
454
467
|
}
|
|
455
468
|
await runExportJobs({
|
|
456
469
|
db: exportDb,
|
|
@@ -508,21 +521,26 @@ export function createUserDataRightsFeature(opts = {}) {
|
|
|
508
521
|
db: forgetDb,
|
|
509
522
|
userId: forgetUserId,
|
|
510
523
|
});
|
|
511
|
-
//
|
|
512
|
-
// mail
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
registry: forgetRegistry,
|
|
516
|
-
configResolver: ctx.configResolver,
|
|
517
|
-
secrets: ctx.secrets,
|
|
518
|
-
db: forgetDb,
|
|
519
|
-
userId: forgetUserId,
|
|
520
|
-
handlerName: "user-data-rights:run-forget-cleanup",
|
|
521
|
-
})
|
|
522
|
-
: undefined;
|
|
524
|
+
// Without an app callback the default mail goes out when a mail transport and a
|
|
525
|
+
// mail renderer are mounted. The user is already erased here, so it bypasses the
|
|
526
|
+
// delivery log: an attempt row would be new personal data.
|
|
527
|
+
const forgetConfigResolver = ctx.configResolver;
|
|
523
528
|
const sendDeletionExecutedEmail = opts.sendDeletionExecutedEmail ??
|
|
524
|
-
(
|
|
525
|
-
? makeDefaultDeletionExecutedEmail(
|
|
529
|
+
(isMailTransportAvailable(forgetRegistry) && isMailRendererAvailable(forgetRegistry)
|
|
530
|
+
? makeDefaultDeletionExecutedEmail({
|
|
531
|
+
registry: forgetRegistry,
|
|
532
|
+
db: forgetDb,
|
|
533
|
+
resolveTransport: makeTenantMailTransportResolver({
|
|
534
|
+
registry: forgetRegistry,
|
|
535
|
+
configResolver: forgetConfigResolver,
|
|
536
|
+
secrets: ctx.secrets,
|
|
537
|
+
db: forgetDb,
|
|
538
|
+
userId: forgetUserId,
|
|
539
|
+
handlerName: "user-data-rights:run-forget-cleanup",
|
|
540
|
+
}),
|
|
541
|
+
configFor: (tenantId) => forgetConfigResolver &&
|
|
542
|
+
createConfigAccessor(forgetRegistry, forgetConfigResolver, tenantId, forgetUserId, forgetDb),
|
|
543
|
+
}, opts.mailDefaults)
|
|
526
544
|
: undefined);
|
|
527
545
|
const forgetResult = await runForgetCleanup({
|
|
528
546
|
db: forgetDb,
|
|
@@ -2,5 +2,5 @@ import * as z from "zod";
|
|
|
2
2
|
export declare const cancelDeletionWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"cancel-deletion", z.ZodObject<{}, z.core.$strip>, {
|
|
3
3
|
userId: string;
|
|
4
4
|
status: "active";
|
|
5
|
-
gracePeriodEnd:
|
|
5
|
+
gracePeriodEnd: null;
|
|
6
6
|
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
@@ -54,12 +54,13 @@ export const cancelDeletionWrite = defineWriteHandler({
|
|
|
54
54
|
});
|
|
55
55
|
// gracePeriodEnd=null im Response symmetrisch zu request-deletion's
|
|
56
56
|
// ISO-Timestamp — Frontend kann beide Endpoints uniform behandeln.
|
|
57
|
+
const gracePeriodEnd = null;
|
|
57
58
|
return {
|
|
58
59
|
isSuccess: true,
|
|
59
60
|
data: {
|
|
60
61
|
userId: event.user.id,
|
|
61
62
|
status: USER_STATUS.Active,
|
|
62
|
-
gracePeriodEnd
|
|
63
|
+
gracePeriodEnd,
|
|
63
64
|
},
|
|
64
65
|
};
|
|
65
66
|
},
|
|
@@ -1,9 +1,8 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import { isGdprMailDeliveryAvailable, makeDefaultDeletionRequestedEmail, } from "../lib/default-mailers.js";
|
|
7
6
|
import { resolveGracePeriod, startDeletionGracePeriod } from "./deletion-grace-period.js";
|
|
8
7
|
// POST /api/user/request-deletion (S2.U5a) — DSGVO Art. 17 Forget-Antrag.
|
|
9
8
|
// Flippt status=Active → deletionRequested, setzt gracePeriodEnd aus dem
|
|
@@ -34,12 +33,11 @@ export function createRequestDeletionHandler(opts = {}) {
|
|
|
34
33
|
if (!res.ok)
|
|
35
34
|
return writeFailure(res.error);
|
|
36
35
|
const { gracePeriodEnd, userEmail, userLocale } = res;
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// Ohne gemounteten mail-transport bleibt send undefined → keine Mail.
|
|
36
|
+
// The app callback wins; otherwise the default mail goes through ctx.notify after
|
|
37
|
+
// the commit. Without delivery and an email channel there is no sender and no mail.
|
|
40
38
|
const send = opts.sendDeletionRequestedEmail ??
|
|
41
|
-
(
|
|
42
|
-
? makeDefaultDeletionRequestedEmail(
|
|
39
|
+
(ctx.notify !== undefined && isGdprMailDeliveryAvailable(ctx.registry)
|
|
40
|
+
? makeDefaultDeletionRequestedEmail(ctx.notify, opts.mailDefaults)
|
|
43
41
|
: undefined);
|
|
44
42
|
// Best-effort Email-Notification. Send-Failure darf das Write nicht
|
|
45
43
|
// killen — siehe Type-Doc oben.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EmailTransport } from "@cosmicdrift/kumiko-bundled-features/channel-email";
|
|
2
|
-
import type {
|
|
2
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { ConfigAccessor, NotifyFn, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
4
|
import { type GdprMailLocale } from "../email-templates.js";
|
|
4
5
|
import type { SendDeletionRequestedEmailFn } from "../handlers/request-deletion.write.js";
|
|
5
6
|
import type { SendExportFailedEmailFn, SendExportReadyEmailFn } from "../run-export-jobs.js";
|
|
@@ -8,10 +9,17 @@ export type GdprMailDefaults = {
|
|
|
8
9
|
readonly locale?: GdprMailLocale;
|
|
9
10
|
readonly appName?: string;
|
|
10
11
|
};
|
|
12
|
+
export declare function isGdprMailDeliveryAvailable(registry: Registry): boolean;
|
|
11
13
|
export declare function isMailTransportAvailable(registry: Registry): boolean;
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
-
export declare function
|
|
15
|
-
export declare function
|
|
16
|
-
export declare function
|
|
17
|
-
export {
|
|
14
|
+
export declare function isMailRendererAvailable(registry: Registry): boolean;
|
|
15
|
+
export type NotifyForTenant = (tenantId: TenantId) => NotifyFn;
|
|
16
|
+
export declare function makeDefaultExportReadyEmail(notifyFor: NotifyForTenant, defaults?: GdprMailDefaults): SendExportReadyEmailFn;
|
|
17
|
+
export declare function makeDefaultExportFailedEmail(notifyFor: NotifyForTenant, defaults?: GdprMailDefaults): SendExportFailedEmailFn;
|
|
18
|
+
export declare function makeDefaultDeletionRequestedEmail(notify: NotifyFn, defaults?: GdprMailDefaults): SendDeletionRequestedEmailFn;
|
|
19
|
+
export type DeletionExecutedMailDeps = {
|
|
20
|
+
readonly registry: Registry;
|
|
21
|
+
readonly db: DbConnection;
|
|
22
|
+
readonly resolveTransport: (tenantId: string) => Promise<EmailTransport>;
|
|
23
|
+
readonly configFor: (tenantId: TenantId) => ConfigAccessor | undefined;
|
|
24
|
+
};
|
|
25
|
+
export declare function makeDefaultDeletionExecutedEmail(deps: DeletionExecutedMailDeps, defaults?: GdprMailDefaults): SendDeletionExecutedEmailFn;
|
|
@@ -1,65 +1,94 @@
|
|
|
1
|
-
// Default send*Email
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// send*Email-Opts, greifen diese Defaults nicht (Opt-Override im feature.ts).
|
|
1
|
+
// Default send*Email callbacks for the GDPR notifications. The app writes no
|
|
2
|
+
// callback code: it mounts delivery + channel-email (+ the template stack) and
|
|
3
|
+
// sets appExportDownloadUrl for export mails. Own send*Email options win over
|
|
4
|
+
// these defaults (opt-override in feature.ts).
|
|
6
5
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// Export-ready/-failed and deletion-requested go through ctx.notify addressed
|
|
7
|
+
// to the user, so delivery resolves the address and the attempt row carries
|
|
8
|
+
// the recipientId the forget path erases. Deletion-executed cannot: the user
|
|
9
|
+
// is gone by then, and an attempt row written after the erasure would be new
|
|
10
|
+
// personal data. It renders the seeded template and sends over the tenant's
|
|
11
|
+
// mail transport, with no delivery log.
|
|
11
12
|
import { MAIL_TRANSPORT_EXTENSION } from "@cosmicdrift/kumiko-bundled-features/mail-foundation";
|
|
12
|
-
import { hasMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
13
|
+
import { hasMailTranslations, mailT } from "@cosmicdrift/kumiko-framework/i18n";
|
|
14
|
+
import { DELIVERY_CHANNEL_EXTENSION } from "../../delivery/index.js";
|
|
15
|
+
import { collectRendererPlugins, createRendererFoundationApi, } from "../../renderer-foundation/index.js";
|
|
16
|
+
import { formatGdprMailTimestamp, normalizeGdprMailLocale, } from "../email-templates.js";
|
|
17
|
+
import { GdprMailNotificationTypes, gdprMailTemplateLocale } from "../mail-notifications.js";
|
|
18
|
+
// The notify-based defaults need the delivery pipeline with an email channel.
|
|
19
|
+
export function isGdprMailDeliveryAvailable(registry) {
|
|
20
|
+
return registry
|
|
21
|
+
.getExtensionUsages(DELIVERY_CHANNEL_EXTENSION)
|
|
22
|
+
.some((usage) => usage.entityName === "email");
|
|
23
|
+
}
|
|
18
24
|
export function isMailTransportAvailable(registry) {
|
|
19
25
|
return registry.getExtensionUsages(MAIL_TRANSPORT_EXTENSION).length > 0;
|
|
20
26
|
}
|
|
27
|
+
export function isMailRendererAvailable(registry) {
|
|
28
|
+
return collectRendererPlugins(registry).some((plugin) => plugin.kinds.includes("mail"));
|
|
29
|
+
}
|
|
21
30
|
// Per-recipient locale wins (user.locale); mailDefaults.locale is the fallback
|
|
22
|
-
// for unknown/unsupported user.locale values;
|
|
23
|
-
|
|
31
|
+
// for unknown/unsupported user.locale values; a locale without a template for
|
|
32
|
+
// this notification falls back to en.
|
|
33
|
+
function mailLocaleFor(notificationType, userLocale, defaults) {
|
|
24
34
|
const user = normalizeGdprMailLocale(userLocale);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
const preferred = user !== undefined && hasMailTranslations(user) ? user : defaults.locale;
|
|
36
|
+
return gdprMailTemplateLocale(notificationType, preferred ?? "en");
|
|
37
|
+
}
|
|
38
|
+
function appNameFor(locale, defaults) {
|
|
39
|
+
return defaults.appName ?? mailT(locale, "gdpr.mail.appNameDefault");
|
|
28
40
|
}
|
|
29
|
-
|
|
41
|
+
// Delivery failures come back as results, not throws; the callers' best-effort
|
|
42
|
+
// handling only sees them if they throw.
|
|
43
|
+
async function notifyImmediately(notify, notificationType, options) {
|
|
44
|
+
const result = await notify(notificationType, { ...options, immediate: true });
|
|
45
|
+
const failed = result.deliveries.find((delivery) => delivery.status === "failed");
|
|
46
|
+
if (failed) {
|
|
47
|
+
throw new Error(`${notificationType} delivery failed: ${failed.error ?? "unknown"}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function makeDefaultExportReadyEmail(notifyFor, defaults = {}) {
|
|
30
51
|
return async (args) => {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
const type = GdprMailNotificationTypes.exportReady;
|
|
53
|
+
const locale = mailLocaleFor(type, args.userLocale, defaults);
|
|
54
|
+
// immediate: the plain magic-link token must not land in a delivery job payload.
|
|
55
|
+
await notifyImmediately(notifyFor(args.tenantId), type, {
|
|
56
|
+
to: args.userId,
|
|
57
|
+
locale,
|
|
58
|
+
data: {
|
|
59
|
+
appName: appNameFor(locale, defaults),
|
|
60
|
+
actionUrl: args.downloadUrl,
|
|
61
|
+
validUntil: formatGdprMailTimestamp(args.expiresAt),
|
|
62
|
+
},
|
|
37
63
|
});
|
|
38
|
-
await transport.send({ to: args.userEmail, subject, html, text });
|
|
39
64
|
};
|
|
40
65
|
}
|
|
41
|
-
export function makeDefaultExportFailedEmail(
|
|
66
|
+
export function makeDefaultExportFailedEmail(notifyFor, defaults = {}) {
|
|
42
67
|
return async (args) => {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
const type = GdprMailNotificationTypes.exportFailed;
|
|
69
|
+
const locale = mailLocaleFor(type, args.userLocale, defaults);
|
|
70
|
+
await notifyImmediately(notifyFor(args.tenantId), type, {
|
|
71
|
+
to: args.userId,
|
|
72
|
+
locale,
|
|
73
|
+
data: { appName: appNameFor(locale, defaults) },
|
|
47
74
|
});
|
|
48
|
-
await transport.send({ to: args.userEmail, subject, html, text });
|
|
49
75
|
};
|
|
50
76
|
}
|
|
51
|
-
export function makeDefaultDeletionRequestedEmail(
|
|
77
|
+
export function makeDefaultDeletionRequestedEmail(notify, defaults = {}) {
|
|
52
78
|
return async (args) => {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
79
|
+
const type = GdprMailNotificationTypes.deletionRequested;
|
|
80
|
+
const locale = mailLocaleFor(type, args.userLocale, defaults);
|
|
81
|
+
await notify(type, {
|
|
82
|
+
to: args.userId,
|
|
83
|
+
locale,
|
|
84
|
+
data: {
|
|
85
|
+
appName: appNameFor(locale, defaults),
|
|
86
|
+
deletionDate: formatGdprMailTimestamp(args.gracePeriodEnd),
|
|
87
|
+
},
|
|
58
88
|
});
|
|
59
|
-
await transport.send({ to: args.userEmail, subject, html, text });
|
|
60
89
|
};
|
|
61
90
|
}
|
|
62
|
-
export function makeDefaultDeletionExecutedEmail(
|
|
91
|
+
export function makeDefaultDeletionExecutedEmail(deps, defaults = {}) {
|
|
63
92
|
return async (args) => {
|
|
64
93
|
// Der User ist global, der Mail-Transport per-Tenant — wir senden ueber den
|
|
65
94
|
// Transport des ersten Memberships. Orphan-User (0 Memberships) liefert
|
|
@@ -69,12 +98,35 @@ export function makeDefaultDeletionExecutedEmail(resolveTransport, defaults = {}
|
|
|
69
98
|
// skip: orphan user has no tenant whose transport could send the mail
|
|
70
99
|
return;
|
|
71
100
|
}
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
101
|
+
const type = GdprMailNotificationTypes.deletionExecuted;
|
|
102
|
+
const locale = mailLocaleFor(type, args.userLocale, defaults);
|
|
103
|
+
const plugin = createRendererFoundationApi(collectRendererPlugins(deps.registry)).createRendererForTenant({ tenantId, kind: "mail" });
|
|
104
|
+
const config = deps.configFor(tenantId);
|
|
105
|
+
const rendered = await plugin.render({
|
|
106
|
+
kind: "mail",
|
|
107
|
+
payload: {
|
|
108
|
+
slug: type,
|
|
109
|
+
locale,
|
|
110
|
+
variables: {
|
|
111
|
+
appName: appNameFor(locale, defaults),
|
|
112
|
+
executedAt: formatGdprMailTimestamp(args.executedAt),
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
}, {
|
|
116
|
+
db: deps.db,
|
|
117
|
+
registry: deps.registry,
|
|
118
|
+
tenantId,
|
|
119
|
+
...(config !== undefined && { config }),
|
|
120
|
+
});
|
|
121
|
+
if (rendered.kind !== "mail") {
|
|
122
|
+
throw new Error(`deletion-executed mail: renderer answered with kind "${rendered.kind}"`);
|
|
123
|
+
}
|
|
124
|
+
const transport = await deps.resolveTransport(tenantId);
|
|
125
|
+
await transport.send({
|
|
126
|
+
to: args.userEmail,
|
|
127
|
+
subject: rendered.subject,
|
|
128
|
+
html: rendered.html,
|
|
129
|
+
text: rendered.text,
|
|
77
130
|
});
|
|
78
|
-
await transport.send({ to: args.userEmail, subject, html, text });
|
|
79
131
|
};
|
|
80
132
|
}
|
|
@@ -51,7 +51,7 @@ export async function updateUserLifecycle(conn, userId, changes, options) {
|
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
export async function backfillUserLifecycleEvents(conn) {
|
|
54
|
-
const rows =
|
|
54
|
+
const rows = await selectMany(conn, userTable, {});
|
|
55
55
|
let backfilled = 0;
|
|
56
56
|
const failed = [];
|
|
57
57
|
for (const row of rows) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type FeatureRegistrar, type NotificationLocalizedTemplate, type TemplateVariables } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
declare const ALL_GDPR_MAIL_KINDS: readonly ["exportReady", "exportFailed", "deletionRequested", "deletionExecuted"];
|
|
3
|
+
export type GdprMailKind = (typeof ALL_GDPR_MAIL_KINDS)[number];
|
|
4
|
+
export declare const GDPR_MAIL_KINDS: Readonly<Record<GdprMailKind, {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly translationKey: string;
|
|
7
|
+
}>>;
|
|
8
|
+
export declare const GdprMailNotificationTypes: Readonly<Record<GdprMailKind, string>>;
|
|
9
|
+
export declare function gdprMailTemplateLocale(notificationType: string, requestedLocale: string): string;
|
|
10
|
+
export declare const GDPR_MAIL_VARIABLES: Readonly<Record<GdprMailKind, TemplateVariables>>;
|
|
11
|
+
export declare function gdprMailLocalizedTemplate(kind: GdprMailKind): () => NotificationLocalizedTemplate;
|
|
12
|
+
export declare function registerGdprMails(r: FeatureRegistrar): void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { QnTypes, qn, templateVariable, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { hasMailTranslationKey, listMailLocales, mailT, resolveMailLocale, } from "@cosmicdrift/kumiko-framework/i18n";
|
|
3
|
+
import { escapeHtml } from "@cosmicdrift/kumiko-headless";
|
|
4
|
+
import { mailParagraph } from "../shared/index.js";
|
|
5
|
+
import { USER_DATA_RIGHTS_FEATURE } from "./constants.js";
|
|
6
|
+
const ALL_GDPR_MAIL_KINDS = [
|
|
7
|
+
"exportReady",
|
|
8
|
+
"exportFailed",
|
|
9
|
+
"deletionRequested",
|
|
10
|
+
"deletionExecuted",
|
|
11
|
+
];
|
|
12
|
+
export const GDPR_MAIL_KINDS = {
|
|
13
|
+
exportReady: { name: "export-ready", translationKey: "exportReady" },
|
|
14
|
+
exportFailed: { name: "export-failed", translationKey: "exportFailed" },
|
|
15
|
+
deletionRequested: { name: "deletion-requested", translationKey: "deletionRequested" },
|
|
16
|
+
deletionExecuted: { name: "deletion-executed", translationKey: "deletionExecuted" },
|
|
17
|
+
};
|
|
18
|
+
export const GdprMailNotificationTypes = {
|
|
19
|
+
exportReady: gdprMailType("exportReady"),
|
|
20
|
+
exportFailed: gdprMailType("exportFailed"),
|
|
21
|
+
deletionRequested: gdprMailType("deletionRequested"),
|
|
22
|
+
deletionExecuted: gdprMailType("deletionExecuted"),
|
|
23
|
+
};
|
|
24
|
+
function gdprMailType(kind) {
|
|
25
|
+
return qn(USER_DATA_RIGHTS_FEATURE, QnTypes.notify, GDPR_MAIL_KINDS[kind].name);
|
|
26
|
+
}
|
|
27
|
+
function subjectKey(kind) {
|
|
28
|
+
return `gdpr.mail.${GDPR_MAIL_KINDS[kind].translationKey}.subject`;
|
|
29
|
+
}
|
|
30
|
+
const SUBJECT_KEY_BY_TYPE = new Map(ALL_GDPR_MAIL_KINDS.map((kind) => [GdprMailNotificationTypes[kind], subjectKey(kind)]));
|
|
31
|
+
// A registered locale whose bundle lacks the subject key has no template (see
|
|
32
|
+
// gdprMailLocalizedTemplate); the renderer has no locale fallback, so the
|
|
33
|
+
// sender must pick a locale that has one or the mail would not go out at all.
|
|
34
|
+
export function gdprMailTemplateLocale(notificationType, requestedLocale) {
|
|
35
|
+
const locale = resolveMailLocale(requestedLocale);
|
|
36
|
+
const key = SUBJECT_KEY_BY_TYPE.get(notificationType);
|
|
37
|
+
return key !== undefined && hasMailTranslationKey(locale, key) ? locale : "en";
|
|
38
|
+
}
|
|
39
|
+
const appName = () => templateVariable.text({ required: true });
|
|
40
|
+
export const GDPR_MAIL_VARIABLES = {
|
|
41
|
+
exportReady: {
|
|
42
|
+
appName: appName(),
|
|
43
|
+
actionUrl: templateVariable.link({ required: true }),
|
|
44
|
+
validUntil: templateVariable.text({ required: true }),
|
|
45
|
+
},
|
|
46
|
+
exportFailed: { appName: appName() },
|
|
47
|
+
deletionRequested: {
|
|
48
|
+
appName: appName(),
|
|
49
|
+
deletionDate: templateVariable.text({ required: true }),
|
|
50
|
+
},
|
|
51
|
+
deletionExecuted: { appName: appName(), executedAt: templateVariable.text({ required: true }) },
|
|
52
|
+
};
|
|
53
|
+
// Translation placeholder -> template variable per kind.
|
|
54
|
+
const PLACEHOLDER_SLOTS = {
|
|
55
|
+
exportReady: { app: "{appName}", when: "{validUntil}" },
|
|
56
|
+
exportFailed: { app: "{appName}" },
|
|
57
|
+
deletionRequested: { app: "{appName}", when: "{deletionDate}" },
|
|
58
|
+
deletionExecuted: { app: "{appName}", when: "{executedAt}" },
|
|
59
|
+
};
|
|
60
|
+
function paragraph(kind, locale, part) {
|
|
61
|
+
const { translationKey } = GDPR_MAIL_KINDS[kind];
|
|
62
|
+
return mailParagraph(mailT(locale, `gdpr.mail.${translationKey}.${part}`, PLACEHOLDER_SLOTS[kind]));
|
|
63
|
+
}
|
|
64
|
+
function greeting(locale) {
|
|
65
|
+
return mailParagraph(mailT(locale, "gdpr.mail.greeting"));
|
|
66
|
+
}
|
|
67
|
+
function mailBody(kind, locale) {
|
|
68
|
+
switch (kind) {
|
|
69
|
+
case "exportReady":
|
|
70
|
+
return [
|
|
71
|
+
greeting(locale),
|
|
72
|
+
paragraph(kind, locale, "intro"),
|
|
73
|
+
`<p><a data-button href="{{actionUrl}}">${escapeHtml(mailT(locale, "gdpr.mail.exportReady.button"))}</a></p>`,
|
|
74
|
+
paragraph(kind, locale, "expiry"),
|
|
75
|
+
].join("");
|
|
76
|
+
case "exportFailed":
|
|
77
|
+
return [greeting(locale), paragraph(kind, locale, "intro")].join("");
|
|
78
|
+
case "deletionRequested":
|
|
79
|
+
return [
|
|
80
|
+
greeting(locale),
|
|
81
|
+
paragraph(kind, locale, "intro"),
|
|
82
|
+
paragraph(kind, locale, "cancel"),
|
|
83
|
+
].join("");
|
|
84
|
+
case "deletionExecuted":
|
|
85
|
+
return [greeting(locale), paragraph(kind, locale, "intro")].join("");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Evaluated when the registry is built, after every locale feature has
|
|
89
|
+
// registered its mail translations. English always ships; another locale only
|
|
90
|
+
// gets a template when it has its own subject, so a missing translation never
|
|
91
|
+
// silently becomes an English mail in that locale's slot.
|
|
92
|
+
export function gdprMailLocalizedTemplate(kind) {
|
|
93
|
+
const kindSubjectKey = subjectKey(kind);
|
|
94
|
+
return () => {
|
|
95
|
+
const locales = [
|
|
96
|
+
"en",
|
|
97
|
+
...listMailLocales().filter((locale) => locale !== "en" && hasMailTranslationKey(locale, kindSubjectKey)),
|
|
98
|
+
];
|
|
99
|
+
return Object.fromEntries(locales.map((locale) => [
|
|
100
|
+
locale,
|
|
101
|
+
{
|
|
102
|
+
body: mailBody(kind, locale),
|
|
103
|
+
meta: {
|
|
104
|
+
subject: mailT(locale, kindSubjectKey, { app: "{{appName}}" }),
|
|
105
|
+
category: "transactional",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
]));
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function registerGdprMails(r) {
|
|
112
|
+
for (const kind of ALL_GDPR_MAIL_KINDS) {
|
|
113
|
+
r.notification(GDPR_MAIL_KINDS[kind].name, {
|
|
114
|
+
variables: GDPR_MAIL_VARIABLES[kind],
|
|
115
|
+
localizedTemplate: gdprMailLocalizedTemplate(kind),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -12,6 +12,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
12
12
|
// declarative fields/actions instead (feature.ts).
|
|
13
13
|
import { useDispatcher, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
14
14
|
import { postWithDownload } from "@cosmicdrift/kumiko-renderer-web";
|
|
15
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
15
16
|
import { useEffect, useState } from "react";
|
|
16
17
|
import { EXPORT_JOB_STATUS, UserDataRightsHandlers, UserDataRightsQueries, } from "../constants.js";
|
|
17
18
|
// Export-Job läuft async (worker-Lane-Cron, ~1 Min). Solange er pending/running
|
|
@@ -21,7 +22,7 @@ const EXPORT_POLL_MS = 4000;
|
|
|
21
22
|
// Dispatcher-Failures tragen i18nKey nur wenn der Handler einen setzt —
|
|
22
23
|
// Boundary-Read mit generischem Fallback.
|
|
23
24
|
function failureKey(error) {
|
|
24
|
-
const key = error
|
|
25
|
+
const key = propertyOf(error, "i18nKey");
|
|
25
26
|
return typeof key === "string" ? key : "userDataRights.privacyCenter.errors.generic";
|
|
26
27
|
}
|
|
27
28
|
// Roher ISO-Instant → nur der Datums-Teil ist relevant; reiner String-Slice,
|
|
@@ -8,6 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
// (../../user-data-rights/web/privacy-center-screen.tsx), which this file
|
|
9
9
|
// mirrors.
|
|
10
10
|
import { useDispatcher, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
11
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
11
12
|
import { useState } from "react";
|
|
12
13
|
import { AuthHandlers } from "../../auth-email-password/constants.js";
|
|
13
14
|
import { requestEmailVerification } from "../../auth-email-password/web/index.js";
|
|
@@ -15,7 +16,7 @@ import { UserProfileHandlers } from "../constants.js";
|
|
|
15
16
|
// Dispatcher failures carry an i18nKey only when the handler sets one —
|
|
16
17
|
// boundary read with a generic fallback.
|
|
17
18
|
function failureKey(error) {
|
|
18
|
-
const key = error
|
|
19
|
+
const key = propertyOf(error, "i18nKey");
|
|
19
20
|
return typeof key === "string" ? key : "profile.errors.generic";
|
|
20
21
|
}
|
|
21
22
|
function StatusBanner({ status }) {
|