@cosmicdrift/kumiko-bundled-features 0.386.0 → 0.388.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/LICENSE +21 -57
- package/dist/agent-tools/tool-catalog.js +1 -1
- package/dist/agent-tools/tool-dispatch.d.ts +7 -7
- package/dist/agent-tools/tool-dispatch.js +9 -9
- package/dist/audit/handlers/audit-events.js +1 -1
- package/dist/audit/screens.js +1 -1
- package/dist/auth-email-password/feature.js +1 -1
- package/dist/auth-email-password/handlers/change-password.write.js +3 -3
- package/dist/auth-email-password/handlers/confirm-token-flow.js +5 -5
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +1 -1
- package/dist/auth-email-password/handlers/invite-accept.write.js +18 -18
- package/dist/auth-email-password/handlers/invite-create.write.d.ts +2 -2
- package/dist/auth-email-password/handlers/invite-create.write.js +10 -10
- package/dist/auth-email-password/handlers/invite-info.query.js +1 -1
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +24 -26
- package/dist/auth-email-password/handlers/login.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/login.write.js +1 -1
- package/dist/auth-email-password/handlers/self-registration-status.query.js +1 -1
- package/dist/auth-email-password/handlers/signup-confirm.write.js +34 -37
- package/dist/auth-email-password/handlers/signup-request.write.js +6 -7
- package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +1 -4
- package/dist/auth-email-password/index.js +5 -5
- package/dist/auth-email-password/lockout-store.js +1 -1
- package/dist/auth-email-password/seeding.d.ts +42 -45
- package/dist/auth-email-password/seeding.js +51 -53
- package/dist/auth-email-password/signup-token-store.js +8 -8
- package/dist/auth-email-password/testing.js +3 -3
- package/dist/auth-email-password/web/auth-client.d.ts +2 -2
- package/dist/auth-email-password/web/auth-client.js +36 -37
- package/dist/auth-email-password/web/auth-form-primitives.js +10 -10
- package/dist/auth-email-password/web/auth-gate.js +15 -16
- package/dist/auth-email-password/web/client-plugin.d.ts +7 -7
- package/dist/auth-email-password/web/client-plugin.js +4 -5
- package/dist/auth-email-password/web/confirm-account-unlock-screen.js +1 -1
- package/dist/auth-email-password/web/constants.js +3 -3
- package/dist/auth-email-password/web/default-topbar-actions.d.ts +8 -8
- package/dist/auth-email-password/web/default-topbar-actions.js +6 -6
- package/dist/auth-email-password/web/forgot-password-screen.d.ts +2 -2
- package/dist/auth-email-password/web/forgot-password-screen.js +9 -9
- package/dist/auth-email-password/web/index.js +5 -5
- package/dist/auth-email-password/web/invite-accept-screen.d.ts +1 -1
- package/dist/auth-email-password/web/invite-accept-screen.js +16 -16
- package/dist/auth-email-password/web/login-screen.d.ts +1 -1
- package/dist/auth-email-password/web/login-screen.js +2 -3
- package/dist/auth-email-password/web/reset-password-screen.d.ts +4 -4
- package/dist/auth-email-password/web/reset-password-screen.js +12 -13
- package/dist/auth-email-password/web/session.d.ts +10 -9
- package/dist/auth-email-password/web/session.js +28 -29
- package/dist/auth-email-password/web/signup-complete-screen.js +3 -3
- package/dist/auth-email-password/web/signup-screen.d.ts +1 -1
- package/dist/auth-email-password/web/signup-screen.js +13 -13
- package/dist/auth-email-password/web/tenant-switcher.d.ts +4 -4
- package/dist/auth-email-password/web/tenant-switcher.js +21 -22
- package/dist/auth-email-password/web/use-shell-user.js +12 -13
- package/dist/auth-email-password/web/user-menu.d.ts +4 -4
- package/dist/auth-email-password/web/user-menu.js +12 -13
- package/dist/auth-email-password/web/verify-email-screen.d.ts +2 -2
- package/dist/auth-email-password/web/verify-email-screen.js +9 -10
- package/dist/auth-foundation/boot-checks.js +4 -4
- package/dist/auth-foundation/resolve-anonymous-access.d.ts +1 -1
- package/dist/auth-foundation/resolve-anonymous-access.js +3 -3
- package/dist/auth-foundation/types.js +2 -2
- package/dist/auth-mfa/feature-exports.d.ts +2 -2
- package/dist/auth-mfa/feature.js +8 -0
- package/dist/auth-mfa/handlers/disable.write.js +3 -3
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +4 -4
- package/dist/auth-mfa/handlers/enable-confirm.write.js +2 -2
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/reencrypt.job.js +1 -1
- package/dist/auth-mfa/handlers/regenerate-recovery.write.js +5 -5
- package/dist/auth-mfa/handlers/verify.write.js +1 -1
- package/dist/auth-mfa/token-secrets.js +1 -1
- package/dist/auth-mfa/web/client-plugin.d.ts +1 -1
- package/dist/auth-mfa/web/client-plugin.js +1 -1
- package/dist/auth-mfa/web/i18n.js +8 -4
- package/dist/auth-mfa/web/index.js +4 -4
- package/dist/auth-mfa/web/mfa-client.js +17 -16
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.d.ts +11 -0
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.js +12 -0
- package/dist/auth-mfa/web/mfa-disable-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-regenerate-recovery-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +10 -5
- package/dist/auth-mfa/web/mfa-verify-screen.js +32 -11
- package/dist/billing-foundation/aggregate-id.d.ts +5 -5
- package/dist/billing-foundation/aggregate-id.js +10 -11
- package/dist/billing-foundation/checkout-core.d.ts +9 -14
- package/dist/billing-foundation/checkout-core.js +24 -39
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +8 -2
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +1 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +9 -3
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +3 -1
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +4 -2
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +6 -5
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +5 -1
- package/dist/billing-foundation/consumer-protection/termination-record.js +27 -17
- package/dist/billing-foundation/consumer-protection/termination-request.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-revoke.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -0
- package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +5 -2
- package/dist/billing-foundation/consumer-protection/withdrawal-period.js +8 -9
- package/dist/billing-foundation/entities.js +27 -27
- package/dist/billing-foundation/events.d.ts +1 -0
- package/dist/billing-foundation/events.js +19 -17
- package/dist/billing-foundation/feature.js +45 -44
- package/dist/billing-foundation/get-subscription-for-tenant.d.ts +3 -3
- package/dist/billing-foundation/get-subscription-for-tenant.js +3 -5
- package/dist/billing-foundation/handlers/billing-plans.query.js +35 -5
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +19 -17
- package/dist/billing-foundation/handlers/create-portal-session.write.js +11 -12
- package/dist/billing-foundation/handlers/list-subscriptions.query.js +4 -4
- package/dist/billing-foundation/handlers/plan-switch-preview.query.d.ts +13 -0
- package/dist/billing-foundation/handlers/plan-switch-preview.query.js +48 -0
- package/dist/billing-foundation/handlers/process-event.write.d.ts +6 -0
- package/dist/billing-foundation/handlers/process-event.write.js +28 -22
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +32 -5
- package/dist/billing-foundation/handlers/switch-plan.write.d.ts +2 -2
- package/dist/billing-foundation/handlers/switch-plan.write.js +56 -64
- package/dist/billing-foundation/handlers/sync-subscription.write.js +12 -1
- package/dist/billing-foundation/i18n.js +22 -0
- package/dist/billing-foundation/index.d.ts +1 -1
- package/dist/billing-foundation/plan-catalog.d.ts +21 -5
- package/dist/billing-foundation/plan-catalog.js +25 -16
- package/dist/billing-foundation/plan-switch.d.ts +12 -0
- package/dist/billing-foundation/plan-switch.js +60 -0
- package/dist/billing-foundation/projection.d.ts +15 -11
- package/dist/billing-foundation/projection.js +38 -37
- package/dist/billing-foundation/subscription-tier-sync.js +3 -3
- package/dist/billing-foundation/tenant-destroy-hook.d.ts +2 -2
- package/dist/billing-foundation/tenant-destroy-hook.js +7 -7
- package/dist/billing-foundation/types.d.ts +97 -42
- package/dist/billing-foundation/types.js +29 -20
- package/dist/billing-foundation/validate-options.js +17 -0
- package/dist/billing-foundation/web/billing-plans-panel.js +82 -36
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +17 -3
- package/dist/billing-foundation/web/checkout-consent-dialog.js +14 -10
- package/dist/billing-foundation/web/constants.d.ts +4 -0
- package/dist/billing-foundation/web/constants.js +4 -0
- package/dist/billing-foundation/web/plan-price.d.ts +15 -0
- package/dist/billing-foundation/web/plan-price.js +32 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.d.ts +15 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.js +66 -0
- package/dist/cap-counter/aggregate-id.js +8 -9
- package/dist/cap-counter/cap-reservation.js +5 -1
- package/dist/cap-counter/constants.js +11 -12
- package/dist/cap-counter/enforce-cap.d.ts +59 -62
- package/dist/cap-counter/enforce-cap.js +49 -49
- package/dist/cap-counter/entity.js +29 -29
- package/dist/cap-counter/feature.js +34 -35
- package/dist/cap-counter/handlers/increment-rolling.write.d.ts +2 -2
- package/dist/cap-counter/handlers/increment-rolling.write.js +31 -31
- package/dist/cap-counter/handlers/mark-soft-warned.write.js +4 -5
- package/dist/cap-counter/stock-cap-guard.js +8 -2
- package/dist/cap-counter/with-cap-enforcement.d.ts +3 -5
- package/dist/channel-email/feature.js +1 -1
- package/dist/channel-email/smtp-transport.d.ts +18 -17
- package/dist/channel-email/smtp-transport.js +24 -21
- package/dist/channel-in-app/handlers/unread-count.query.js +3 -4
- package/dist/compliance-profiles/feature.js +4 -4
- package/dist/compliance-profiles/handlers/for-tenant.query.js +3 -6
- package/dist/compliance-profiles/handlers/list-profiles.query.js +5 -5
- package/dist/compliance-profiles/handlers/needs-profile.query.js +4 -4
- package/dist/compliance-profiles/handlers/set-profile.write.js +13 -14
- package/dist/compliance-profiles/handlers/sub-processors.query.js +9 -11
- package/dist/compliance-profiles/resolve-for-tenant.js +7 -7
- package/dist/compliance-profiles/schema/profile-selection.js +18 -23
- package/dist/compliance-profiles/seeding.js +14 -15
- package/dist/compliance-profiles-ops/handlers/tenants-missing-profile.query.js +3 -3
- package/dist/compliance-profiles-ops/index.js +2 -2
- package/dist/config/handlers/cascade.query.d.ts +5 -0
- package/dist/config/handlers/cascade.query.js +4 -3
- package/dist/config/handlers/readiness.query.d.ts +4 -4
- package/dist/config/handlers/readiness.query.js +1 -1
- package/dist/config/handlers/reencrypt.job.js +4 -5
- package/dist/config/handlers/set.write.js +4 -4
- package/dist/config/handlers/values.query.d.ts +5 -0
- package/dist/config/handlers/values.query.js +4 -3
- package/dist/config/resolver.js +4 -4
- package/dist/config/table.js +10 -10
- package/dist/config/write-helpers.d.ts +1 -0
- package/dist/config/write-helpers.js +19 -10
- package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +2 -2
- package/dist/crypto-shredding/handlers/forget-subject.write.js +25 -21
- package/dist/custom-fields/aggregate-id.d.ts +12 -13
- package/dist/custom-fields/aggregate-id.js +16 -17
- package/dist/custom-fields/entity.js +49 -47
- package/dist/custom-fields/events.js +7 -8
- package/dist/custom-fields/feature.d.ts +14 -14
- package/dist/custom-fields/feature.js +53 -54
- package/dist/custom-fields/handlers/clear-custom-field.write.d.ts +3 -2
- package/dist/custom-fields/handlers/clear-custom-field.write.js +9 -8
- package/dist/custom-fields/handlers/define-tenant-field.write.d.ts +1 -1
- package/dist/custom-fields/handlers/define-tenant-field.write.js +2 -2
- package/dist/custom-fields/handlers/delete-system-field.write.js +6 -7
- package/dist/custom-fields/handlers/delete-tenant-field.write.d.ts +1 -1
- package/dist/custom-fields/handlers/delete-tenant-field.write.js +3 -3
- package/dist/custom-fields/handlers/set-custom-field.write.d.ts +3 -3
- package/dist/custom-fields/handlers/set-custom-field.write.js +25 -28
- package/dist/custom-fields/handlers/update-tenant-field.write.d.ts +1 -1
- package/dist/custom-fields/handlers/update-tenant-field.write.js +1 -1
- package/dist/custom-fields/lib/field-access.js +2 -2
- package/dist/custom-fields/lib/parse-serialized-field.js +1 -1
- package/dist/custom-fields/lib/quota.js +1 -1
- package/dist/custom-fields/lib/value-schema.js +10 -12
- package/dist/custom-fields/run-retention.js +3 -3
- package/dist/custom-fields/schemas.js +14 -14
- package/dist/custom-fields/web/client-plugin.js +2 -5
- package/dist/custom-fields/web/constants.js +2 -2
- package/dist/custom-fields/web/custom-fields-form-section.d.ts +4 -4
- package/dist/custom-fields/web/custom-fields-form-section.js +22 -22
- package/dist/custom-fields/wire-for-entity.js +65 -67
- package/dist/custom-fields/wire-user-data-rights.js +3 -3
- package/dist/data-retention/_internal/parse-override.js +2 -3
- package/dist/data-retention/feature.js +11 -18
- package/dist/data-retention/handlers/policy-for.query.js +6 -6
- package/dist/data-retention/keep-for.d.ts +7 -7
- package/dist/data-retention/keep-for.js +15 -15
- package/dist/data-retention/override-schema.d.ts +3 -4
- package/dist/data-retention/override-schema.js +13 -15
- package/dist/data-retention/presets.d.ts +4 -5
- package/dist/data-retention/presets.js +18 -23
- package/dist/data-retention/resolve-tenant-preset.js +17 -16
- package/dist/data-retention/resolver.d.ts +12 -12
- package/dist/data-retention/resolver.js +24 -23
- package/dist/data-retention/run-retention-cleanup.d.ts +8 -3
- package/dist/data-retention/run-retention-cleanup.js +61 -51
- package/dist/data-retention/schema/tenant-retention-override.js +13 -11
- package/dist/delivery/channel-context.js +6 -3
- package/dist/delivery/db/queries/preferences.js +2 -2
- package/dist/delivery/delivery-service.js +22 -7
- package/dist/delivery/feature.js +2 -2
- package/dist/delivery/handlers/log.query.js +1 -1
- package/dist/delivery/handlers/preferences.query.d.ts +1 -1
- package/dist/delivery/handlers/preferences.query.js +20 -1
- package/dist/delivery/handlers/unsubscribe-page-chrome.query.js +1 -1
- package/dist/delivery/tables.js +9 -8
- package/dist/delivery/unsubscribe-link.d.ts +3 -1
- package/dist/delivery/unsubscribe-page-strings.d.ts +1 -0
- package/dist/delivery/unsubscribe-page-strings.js +1 -0
- package/dist/delivery/unsubscribe-page.d.ts +2 -0
- package/dist/delivery/unsubscribe-page.js +5 -0
- package/dist/delivery/unsubscribe.js +29 -11
- package/dist/document-ingest-foundation/entity.js +4 -4
- package/dist/document-ingest-foundation/events.js +2 -2
- package/dist/document-ingest-foundation/feature.js +2 -2
- package/dist/document-ingest-foundation/pages.js +1 -1
- package/dist/document-ingest-foundation/tenant-destroy-hook.js +4 -4
- package/dist/feature-toggles/compose-tier-resolver.js +1 -1
- package/dist/feature-toggles/feature.js +1 -1
- package/dist/feature-toggles/global-feature-state-table.js +1 -1
- package/dist/feature-toggles/handlers/registered.query.js +5 -5
- package/dist/feature-toggles/toggle-sync-signal.js +1 -1
- package/dist/feature-toggles/web/constants.js +1 -1
- package/dist/file-derivatives/feature.d.ts +1 -1
- package/dist/file-derivatives/feature.js +1 -1
- package/dist/file-derivatives/handlers/public-variant.query.js +1 -1
- package/dist/file-derivatives/presets.js +1 -1
- package/dist/file-foundation/feature.js +6 -6
- package/dist/file-foundation/index.d.ts +1 -1
- package/dist/file-foundation/index.js +1 -1
- package/dist/file-foundation/read-file-ref-content.d.ts +3 -0
- package/dist/file-foundation/read-file-ref-content.js +6 -1
- package/dist/file-provider-inmemory/feature.d.ts +1 -1
- package/dist/file-provider-inmemory/feature.js +16 -16
- package/dist/file-provider-s3/env-named-feature.d.ts +0 -2
- package/dist/file-provider-s3/feature.js +16 -16
- package/dist/file-provider-s3/lib/s3-provider.js +6 -6
- package/dist/files/feature.js +6 -6
- package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +4 -4
- package/dist/files-tenant-data/hooks.js +24 -26
- package/dist/files-tenant-data/index.js +1 -1
- package/dist/folders/feature.d.ts +1 -1
- package/dist/folders/web/folder-manager.js +2 -2
- package/dist/folders/web/tree.js +5 -5
- package/dist/form-draft/db/queries/cleanup.js +3 -3
- package/dist/form-draft/db/queries/owned-file-refs.js +1 -1
- package/dist/form-draft/decrypt-draft.js +5 -1
- package/dist/form-draft/entity.js +3 -3
- package/dist/form-draft/feature.js +4 -5
- package/dist/form-draft/handlers/discard.write.js +1 -1
- package/dist/form-draft/release-file-refs.js +6 -6
- package/dist/form-draft/schemas.js +3 -3
- package/dist/form-draft/web/constants.js +0 -2
- package/dist/host-routing/dispatch.js +11 -20
- package/dist/inbound-mail-foundation/aggregate-id.d.ts +13 -13
- package/dist/inbound-mail-foundation/aggregate-id.js +19 -19
- package/dist/inbound-mail-foundation/connect-routes.js +4 -3
- package/dist/inbound-mail-foundation/constants.d.ts +22 -24
- package/dist/inbound-mail-foundation/constants.js +30 -33
- package/dist/inbound-mail-foundation/entities.d.ts +12 -12
- package/dist/inbound-mail-foundation/entities.js +47 -48
- package/dist/inbound-mail-foundation/events.d.ts +4 -3
- package/dist/inbound-mail-foundation/events.js +50 -47
- package/dist/inbound-mail-foundation/feature.js +39 -39
- package/dist/inbound-mail-foundation/handlers/account-state.js +8 -8
- package/dist/inbound-mail-foundation/handlers/connect-account.write.js +25 -26
- package/dist/inbound-mail-foundation/handlers/disconnect-account.write.js +7 -7
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +50 -51
- package/dist/inbound-mail-foundation/handlers/list-accounts.query.js +8 -8
- package/dist/inbound-mail-foundation/handlers/list-messages.query.js +10 -10
- package/dist/inbound-mail-foundation/handlers/scope-visibility.js +3 -4
- package/dist/inbound-mail-foundation/handlers/update-account.write.d.ts +2 -2
- package/dist/inbound-mail-foundation/handlers/update-account.write.js +22 -14
- package/dist/inbound-mail-foundation/i18n.js +0 -1
- package/dist/inbound-mail-foundation/oauth-state.d.ts +5 -5
- package/dist/inbound-mail-foundation/oauth-state.js +11 -10
- package/dist/inbound-mail-foundation/projection.d.ts +4 -4
- package/dist/inbound-mail-foundation/projection.js +31 -29
- package/dist/inbound-mail-foundation/provider-factory.d.ts +4 -4
- package/dist/inbound-mail-foundation/provider-factory.js +9 -9
- package/dist/inbound-mail-foundation/retention-sweep.d.ts +5 -3
- package/dist/inbound-mail-foundation/retention-sweep.js +38 -36
- package/dist/inbound-mail-foundation/screens.js +0 -3
- package/dist/inbound-mail-foundation/tenant-destroy-hook.js +14 -16
- package/dist/inbound-mail-foundation/types.d.ts +49 -49
- package/dist/inbound-mail-foundation/types.js +21 -21
- package/dist/inbound-mail-foundation/watch-supervisor.d.ts +10 -10
- package/dist/inbound-mail-foundation/watch-supervisor.js +85 -67
- package/dist/inbound-provider-imap/credential-document.js +13 -13
- package/dist/inbound-provider-imap/feature.d.ts +2 -2
- package/dist/inbound-provider-imap/feature.js +42 -40
- package/dist/inbound-provider-imap/imap-client.d.ts +12 -11
- package/dist/inbound-provider-imap/imap-client.js +20 -19
- package/dist/inbound-provider-inmemory/feature.d.ts +9 -9
- package/dist/inbound-provider-inmemory/feature.js +19 -19
- package/dist/jobs/db/queries/retention.js +2 -2
- package/dist/jobs/db/queries/stale-run-sweep.js +3 -3
- package/dist/jobs/events.js +1 -1
- package/dist/jobs/feature.d.ts +1 -0
- package/dist/jobs/feature.js +13 -10
- package/dist/jobs/handlers/detail.query.js +4 -4
- package/dist/jobs/handlers/list.query.js +1 -1
- package/dist/jobs/handlers/projection-rebuild.job.js +1 -1
- package/dist/jobs/handlers/reindex-entity.job.js +1 -1
- package/dist/jobs/handlers/retry.write.js +1 -1
- package/dist/jobs/handlers/tenant-failures.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-failures.query.js +45 -41
- package/dist/jobs/handlers/tenant-runs.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-runs.query.js +54 -50
- package/dist/jobs/job-instant.js +1 -1
- package/dist/jobs/job-run-logger.js +15 -15
- package/dist/jobs/job-run-table.js +3 -3
- package/dist/jobs/screens.js +1 -1
- package/dist/jobs/tenant-job-failure-table.js +1 -1
- package/dist/jobs/tenant-job-run-table.js +1 -1
- package/dist/ledger/entity.js +5 -5
- package/dist/ledger/feature.js +3 -3
- package/dist/ledger/handlers/confirm-schedule-period.write.js +3 -3
- package/dist/ledger/recurring.js +12 -12
- package/dist/ledger/schemas.js +1 -1
- package/dist/ledger/web/constants.js +2 -2
- package/dist/ledger/web/index.js +7 -7
- package/dist/legal-pages/feature.d.ts +4 -4
- package/dist/legal-pages/feature.js +25 -26
- package/dist/legal-pages/markdown.js +3 -3
- package/dist/legal-pages/web/client-plugin.js +23 -24
- package/dist/legal-pages/web/index.js +0 -1
- package/dist/mail-foundation/feature.d.ts +5 -5
- package/dist/mail-foundation/feature.js +27 -25
- package/dist/mail-transport-inmemory/feature.d.ts +4 -4
- package/dist/mail-transport-inmemory/feature.js +27 -28
- package/dist/mail-transport-smtp/feature.js +20 -20
- package/dist/managed-pages/branding.js +6 -6
- package/dist/managed-pages/feature.d.ts +26 -26
- package/dist/managed-pages/feature.js +34 -34
- package/dist/managed-pages/handlers/by-slug.query.js +5 -6
- package/dist/managed-pages/handlers/by-tenant-published.query.js +2 -2
- package/dist/managed-pages/handlers/set.write.js +5 -7
- package/dist/managed-pages/screens/page-screens.js +11 -11
- package/dist/managed-pages/seeding.js +8 -8
- package/dist/managed-pages/table.js +13 -12
- package/dist/managed-pages/web/client-plugin.d.ts +1 -1
- package/dist/managed-pages/web/client-plugin.js +5 -5
- package/dist/managed-pages/web/i18n.js +1 -1
- package/dist/notes-history/entity.js +4 -4
- package/dist/notes-history/feature.d.ts +2 -2
- package/dist/notes-history-user-data/hooks.js +1 -1
- package/dist/page-render/layout.js +9 -9
- package/dist/page-render/markdown.js +11 -11
- package/dist/page-render/security-headers.js +7 -6
- package/dist/personal-access-tokens/feature.d.ts +1 -1
- package/dist/personal-access-tokens/feature.js +9 -5
- package/dist/personal-access-tokens/roles.js +2 -2
- package/dist/readiness/handlers/status.query.js +3 -3
- package/dist/renderer-foundation/api.js +12 -13
- package/dist/renderer-foundation/constants.js +2 -2
- package/dist/renderer-foundation/feature.js +7 -7
- package/dist/renderer-pdf/render-table-pdf.d.ts +1 -1
- package/dist/renderer-pdf/render-table-pdf.js +22 -10
- package/dist/secrets/feature.js +9 -9
- package/dist/secrets/web/constants.js +2 -2
- package/dist/seo/feature.js +3 -3
- package/dist/sessions/constants.d.ts +1 -1
- package/dist/sessions/constants.js +1 -1
- package/dist/sessions/feature.d.ts +2 -2
- package/dist/sessions/feature.js +12 -8
- package/dist/sessions/handlers/revoke-all-for-user.write.js +2 -2
- package/dist/sessions/handlers/revoke-all-others.write.js +2 -2
- package/dist/sessions/handlers/revoke.write.js +1 -1
- package/dist/sessions/schema/user-session.js +4 -4
- package/dist/sessions/screens.js +2 -2
- package/dist/sessions/session-callbacks.js +2 -2
- package/dist/sessions/session-revoked-event.js +2 -2
- package/dist/shared/assert-no-where-rule-in-ownership-write.js +1 -1
- package/dist/shared/decrypt-stored-pii.d.ts +1 -0
- package/dist/shared/decrypt-stored-pii.js +7 -2
- package/dist/shared/encrypt-for-direct-write.js +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/mail-template-body.d.ts +6 -2
- package/dist/shared/mail-template-body.js +7 -4
- package/dist/shared/map-with-concurrency.js +5 -4
- package/dist/shared/parent-visibility.js +2 -2
- package/dist/shared/password-hashing.js +1 -1
- package/dist/shared/signup-handover.js +1 -2
- package/dist/shared/single-use-token-store.js +1 -1
- package/dist/step-dispatcher/feature.js +1 -0
- package/dist/subscription-dodo/client.d.ts +17 -0
- package/dist/subscription-dodo/client.js +31 -0
- package/dist/subscription-dodo/constants.d.ts +12 -0
- package/dist/subscription-dodo/constants.js +19 -0
- package/dist/subscription-dodo/feature.d.ts +37 -0
- package/dist/subscription-dodo/feature.js +144 -0
- package/dist/subscription-dodo/index.d.ts +2 -0
- package/dist/subscription-dodo/index.js +4 -0
- package/dist/subscription-dodo/plugin-methods.d.ts +23 -0
- package/dist/subscription-dodo/plugin-methods.js +260 -0
- package/dist/subscription-dodo/runtime.d.ts +27 -0
- package/dist/subscription-dodo/runtime.js +89 -0
- package/dist/subscription-dodo/verify-webhook.d.ts +37 -0
- package/dist/subscription-dodo/verify-webhook.js +189 -0
- package/dist/subscription-mollie/constants.js +15 -17
- package/dist/subscription-mollie/feature.d.ts +17 -18
- package/dist/subscription-mollie/feature.js +24 -24
- package/dist/subscription-mollie/index.js +3 -3
- package/dist/subscription-mollie/plugin-methods.d.ts +2 -2
- package/dist/subscription-mollie/plugin-methods.js +15 -14
- package/dist/subscription-mollie/verify-webhook.d.ts +10 -10
- package/dist/subscription-mollie/verify-webhook.js +27 -27
- package/dist/subscription-stripe/constants.js +4 -6
- package/dist/subscription-stripe/feature.d.ts +14 -14
- package/dist/subscription-stripe/feature.js +36 -34
- package/dist/subscription-stripe/index.js +6 -6
- package/dist/subscription-stripe/plugin-methods.js +18 -18
- package/dist/subscription-stripe/runtime.d.ts +1 -1
- package/dist/subscription-stripe/runtime.js +8 -7
- package/dist/subscription-stripe/verify-webhook.d.ts +11 -11
- package/dist/subscription-stripe/verify-webhook.js +59 -61
- package/dist/tags/feature.d.ts +3 -3
- package/dist/tags/screens.js +1 -1
- package/dist/template-resolver/api.d.ts +3 -2
- package/dist/template-resolver/api.js +34 -12
- package/dist/template-resolver/compile-document-body.js +1 -0
- package/dist/template-resolver/feature.js +2 -2
- package/dist/template-resolver/handlers/bundle-import.js +3 -3
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +5 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +22 -7
- package/dist/template-resolver/handlers/diff-to-system.query.js +3 -2
- package/dist/template-resolver/handlers/document-base.js +3 -2
- package/dist/template-resolver/handlers/find-by-id.query.js +8 -7
- package/dist/template-resolver/handlers/list.query.js +6 -7
- package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
- package/dist/template-resolver/handlers/overview.query.js +6 -3
- package/dist/template-resolver/handlers/shared.js +2 -2
- package/dist/template-resolver/handlers/template-write.js +3 -3
- package/dist/template-resolver/handlers/tenant-override-counts.query.js +1 -3
- package/dist/template-resolver/handlers/tenant-override-selection.d.ts +2 -1
- package/dist/template-resolver/handlers/tenant-overrides.query.js +28 -5
- package/dist/template-resolver/handlers/upsert-system.write.js +3 -4
- package/dist/template-resolver/handlers/upsert-tenant.write.js +4 -5
- package/dist/template-resolver/handlers/usage.query.d.ts +2 -1
- package/dist/template-resolver/handlers/usage.query.js +26 -16
- package/dist/template-resolver/handlers/validate.query.js +7 -2
- package/dist/template-resolver/seeding.js +30 -14
- package/dist/template-resolver/store-template-resource.d.ts +1 -0
- package/dist/template-resolver/store-template-resource.js +19 -4
- package/dist/template-resolver/table.js +10 -11
- package/dist/template-resolver/template-delivery-policy.d.ts +0 -5
- package/dist/template-resolver/template-delivery-policy.js +0 -6
- package/dist/template-resolver/template-meta.js +7 -4
- package/dist/template-resolver/template-preview.js +4 -3
- package/dist/template-resolver/web/client-plugin.js +2 -2
- package/dist/template-resolver/web/constants.d.ts +2 -2
- package/dist/template-resolver/web/constants.js +3 -3
- package/dist/template-resolver/web/index.d.ts +1 -0
- package/dist/template-resolver/web/index.js +1 -0
- package/dist/template-resolver/web/mail-layout-constants.d.ts +5 -0
- package/dist/template-resolver/web/mail-layout-constants.js +7 -0
- package/dist/tenant/feature.js +7 -7
- package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/member-directory.query.js +11 -4
- package/dist/tenant/handlers/members.query.js +1 -1
- package/dist/tenant/handlers/team-list.query.js +1 -1
- package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/tenant-directory.query.js +9 -5
- package/dist/tenant/invitation-table.js +37 -40
- package/dist/tenant/membership-table.js +3 -3
- package/dist/tenant/schema/tenant.js +7 -9
- package/dist/tenant/screens.js +1 -1
- package/dist/tenant/seeding.d.ts +6 -5
- package/dist/tenant/seeding.js +12 -12
- package/dist/tenant/testing.js +5 -6
- package/dist/tenant/web/constants.js +2 -2
- package/dist/tenant-handover/events.js +5 -7
- package/dist/tenant-handover/feature.js +1 -1
- package/dist/tenant-handover/grant.js +1 -1
- package/dist/tenant-handover/handlers/claim.write.js +1 -1
- package/dist/tenant-handover/move-entity-graph.js +8 -9
- package/dist/tenant-handover/signup-handover-provider.js +1 -2
- package/dist/tenant-handover/transfer-graph.js +4 -5
- package/dist/tenant-lifecycle/boot-checks.js +1 -1
- package/dist/tenant-lifecycle/constants.d.ts +1 -0
- package/dist/tenant-lifecycle/constants.js +4 -0
- package/dist/tenant-lifecycle/feature.js +1 -1
- package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
- package/dist/tenant-settings/constants.js +1 -1
- package/dist/tenant-settings/tenant-defaults.d.ts +1 -1
- package/dist/tenant-settings/tenant-defaults.js +9 -2
- package/dist/tier-engine/aggregate-id.d.ts +11 -11
- package/dist/tier-engine/aggregate-id.js +15 -16
- package/dist/tier-engine/compose-app.d.ts +14 -14
- package/dist/tier-engine/compose-app.js +1 -1
- package/dist/tier-engine/entity.js +18 -19
- package/dist/tier-engine/feature.d.ts +19 -17
- package/dist/tier-engine/feature.js +68 -69
- package/dist/tier-engine/handlers/active-tier.query.js +6 -7
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +4 -4
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +7 -8
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +7 -4
- package/dist/tier-engine/tier-resolver.js +2 -2
- package/dist/tier-engine/trial.js +8 -8
- package/dist/tier-engine/web/constants.js +2 -2
- package/dist/tier-engine/web/index.js +4 -4
- package/dist/user/db/queries/stream-tenant-backfill.js +8 -8
- package/dist/user/handlers/create.write.js +4 -4
- package/dist/user/schema/user.d.ts +11 -11
- package/dist/user/schema/user.js +45 -44
- package/dist/user/seeding.d.ts +15 -15
- package/dist/user/seeding.js +28 -28
- package/dist/user/testing.js +3 -3
- package/dist/user-data-rights/audit-download.d.ts +8 -8
- package/dist/user-data-rights/audit-download.js +19 -20
- package/dist/user-data-rights/boot-checks.js +1 -1
- package/dist/user-data-rights/deletion-token.js +1 -1
- package/dist/user-data-rights/feature.d.ts +49 -50
- package/dist/user-data-rights/feature.js +54 -53
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +9 -9
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +8 -8
- package/dist/user-data-rights/handlers/download-by-job.query.js +29 -30
- package/dist/user-data-rights/handlers/download-by-token.query.js +40 -42
- package/dist/user-data-rights/handlers/export-status.query.js +8 -9
- package/dist/user-data-rights/handlers/lift-restriction.write.js +7 -8
- package/dist/user-data-rights/handlers/list-download-attempts.query.js +2 -2
- package/dist/user-data-rights/handlers/my-audit-log.query.js +4 -4
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.d.ts +5 -5
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +16 -15
- package/dist/user-data-rights/handlers/request-deletion.write.d.ts +2 -2
- package/dist/user-data-rights/handlers/request-deletion.write.js +7 -9
- package/dist/user-data-rights/handlers/request-export.write.js +42 -41
- package/dist/user-data-rights/handlers/restrict-account.write.js +22 -19
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +8 -8
- package/dist/user-data-rights/index.js +8 -10
- package/dist/user-data-rights/lib/default-mailers.js +3 -3
- package/dist/user-data-rights/lib/update-user-lifecycle.js +14 -17
- package/dist/user-data-rights/run-export-jobs.d.ts +38 -42
- package/dist/user-data-rights/run-export-jobs.js +130 -237
- package/dist/user-data-rights/run-forget-cleanup.d.ts +16 -20
- package/dist/user-data-rights/run-forget-cleanup.js +65 -83
- package/dist/user-data-rights/run-user-export.d.ts +34 -33
- package/dist/user-data-rights/run-user-export.js +55 -57
- package/dist/user-data-rights/schema/download-attempt.js +11 -11
- package/dist/user-data-rights/schema/download-token.js +37 -41
- package/dist/user-data-rights/schema/export-job.d.ts +4 -4
- package/dist/user-data-rights/schema/export-job.js +65 -67
- package/dist/user-data-rights/token-helpers.d.ts +8 -9
- package/dist/user-data-rights/token-helpers.js +21 -25
- package/dist/user-data-rights/web/client-plugin.d.ts +5 -5
- package/dist/user-data-rights/web/client-plugin.js +1 -1
- package/dist/user-data-rights/web/confirm-deletion-screen.js +6 -5
- package/dist/user-data-rights/web/constants.js +1 -1
- package/dist/user-data-rights/web/index.js +5 -5
- package/dist/user-data-rights/web/privacy-center-screen.js +14 -13
- package/dist/user-data-rights/web/public-deletion-gate.d.ts +4 -4
- package/dist/user-data-rights/web/request-deletion-screen.js +6 -6
- package/dist/user-data-rights/zip-path.d.ts +15 -15
- package/dist/user-data-rights/zip-path.js +41 -40
- package/dist/user-data-rights-defaults/feature.js +15 -15
- package/dist/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.js +3 -5
- package/dist/user-data-rights-defaults/hooks/file-ref.userdata-hook.js +45 -51
- package/dist/user-data-rights-defaults/hooks/job-run.userdata-hook.js +2 -2
- package/dist/user-data-rights-defaults/hooks/notification-preference.userdata-hook.js +1 -1
- package/dist/user-data-rights-defaults/hooks/user-session.userdata-hook.js +3 -3
- package/dist/user-data-rights-defaults/hooks/user.userdata-hook.js +32 -31
- package/dist/user-profile/handlers/change-email.write.js +13 -13
- package/dist/user-profile/index.js +3 -3
- package/dist/user-profile/web/client-plugin.js +1 -1
- package/dist/user-profile/web/constants.js +1 -1
- package/dist/user-profile/web/profile-screen.js +1 -1
- package/dist/waitlist/handlers/export-purge.write.js +2 -1
- package/dist/waitlist/handlers/submit.write.js +5 -5
- package/dist/waitlist/import.d.ts +1 -0
- package/dist/waitlist/import.js +4 -0
- package/dist/waitlist/legacy-migrate.js +17 -6
- package/dist/waitlist/link-on-signup.js +18 -6
- package/dist/waitlist/options.d.ts +3 -0
- package/dist/workflow-runner/aggregate-id.d.ts +1 -1
- package/dist/workflow-runner/aggregate-id.js +1 -1
- package/dist/workflow-runner/event-subscriber.js +4 -5
- package/dist/workflow-runner/event-trigger.js +4 -17
- package/dist/workflow-runner/feature.js +2 -2
- package/dist/workflow-runner/handlers/resume-run.write.js +4 -4
- package/dist/workflow-runner/pending-projection.js +6 -7
- package/dist/workflow-runner/runner.js +6 -6
- package/dist/workflow-runner/tables.js +6 -6
- package/dist/workflow-runner/workflow-registry.js +1 -1
- package/package.json +20 -10
- package/src/agent-tools/changes.json +6 -0
- package/src/audit/changes.json +12 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +55 -0
- package/src/cap-counter/changes.json +18 -0
- package/src/channel-email/changes.json +6 -0
- package/src/config/changes.json +12 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/delivery/changes.json +30 -0
- package/src/form-draft/changes.json +6 -0
- package/src/host-routing/changes.json +6 -0
- package/src/inbound-mail-foundation/changes.json +18 -0
- package/src/jobs/changes.json +13 -0
- package/src/mail-foundation/changes.json +6 -0
- package/src/renderer-pdf/changes.json +6 -0
- package/src/step-dispatcher/changes.json +6 -0
- package/src/subscription-dodo/changes.json +14 -0
- package/src/subscription-stripe/changes.json +12 -0
- package/src/template-resolver/changes.json +48 -0
- package/src/tenant/changes.json +6 -0
- package/src/tenant-lifecycle/changes.json +6 -0
- package/src/tier-engine/changes.json +6 -0
- package/src/waitlist/changes.json +18 -0
- package/dist/billing-foundation/consumer-protection/stored-user-email.d.ts +0 -1
- package/dist/billing-foundation/consumer-protection/stored-user-email.js +0 -8
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
// Retention-Cleanup-Runner
|
|
2
|
-
//
|
|
1
|
+
// Retention-Cleanup-Runner — pure function, called by the retention-cleanup cron
|
|
2
|
+
// for each fan-out tenant.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// ist:
|
|
4
|
+
// Iterates all implicit entity projections, resolves the effective retention
|
|
5
|
+
// policy per entity (3-layer resolver, see resolver.ts), and applies the
|
|
6
|
+
// strategy to rows whose reference timestamp is older than the keepFor cutoff:
|
|
8
7
|
//
|
|
9
|
-
// - hardDelete → executor.forget
|
|
8
|
+
// - hardDelete → executor.forget per row (event → rebuild-safe hard purge).
|
|
10
9
|
// Entities with file/image/files/images fields: bytes first
|
|
11
10
|
// (incl. derivatives), then the fileRef row(s), then the row;
|
|
12
11
|
// shared fileRefs (other owner, other row of the same table)
|
|
13
12
|
// stay untouched.
|
|
14
|
-
// - softDelete → executor.delete
|
|
15
|
-
//
|
|
16
|
-
// - anonymize → executor.update
|
|
17
|
-
// anonymize
|
|
18
|
-
// - blockDelete →
|
|
19
|
-
// user-forget
|
|
20
|
-
// anonymize
|
|
21
|
-
//
|
|
13
|
+
// - softDelete → executor.delete per row (event → rebuild-safe soft-delete),
|
|
14
|
+
// only on not-yet-deleted rows (isDeleted:false filter)
|
|
15
|
+
// - anonymize → executor.update per row with the values of the per-field
|
|
16
|
+
// anonymize functions; row remains (fields.ts contract)
|
|
17
|
+
// - blockDelete → untouched during keepFor (retention requirement;
|
|
18
|
+
// user-forget triggers anonymize). AFTER expiration, the
|
|
19
|
+
// anonymize functions run — business data remains, the person-
|
|
20
|
+
// identifying link is removed (fields.ts:44-47).
|
|
22
21
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
22
|
+
// Via the executor instead of batch-deleteMany/updateMany: an eventless batch write
|
|
23
|
+
// to the (erased) projection table is wiped/resurrected during a projection rebuild.
|
|
24
|
+
// Cost: N events per cleanup instead of one batch statement (per-row ceiling, with
|
|
25
|
+
// a batch-event variant later).
|
|
27
26
|
//
|
|
28
|
-
// **
|
|
29
|
-
// (keyed
|
|
30
|
-
//
|
|
31
|
-
// reference
|
|
32
|
-
//
|
|
27
|
+
// **Stricter than soft-delete-cleanup:** this cron hard-deletes LIVE rows
|
|
28
|
+
// (keyed on reference, default createdAt), not already-soft-deleted ones.
|
|
29
|
+
// Therefore the column-existence check comes before every WHERE — a missing/mistyped
|
|
30
|
+
// reference column would otherwise produce a malformed/all-matching WHERE and
|
|
31
|
+
// delete everything.
|
|
33
32
|
//
|
|
34
|
-
// **anonymize
|
|
35
|
-
// row-
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
33
|
+
// **anonymize idempotence without a marker column:** the anonymize functions are
|
|
34
|
+
// row-independent (`() => unknown`) — evaluated once per entity, they produce
|
|
35
|
+
// the target values. A row that already carries them is skipped, so the
|
|
36
|
+
// daily rerun appends zero events. The WHERE continues to match anonymized
|
|
37
|
+
// rows (reference remains old), hence id-cursor paging instead of a single
|
|
38
|
+
// page — otherwise completed rows clog the batchLimit window.
|
|
40
39
|
import { collectPiiSubjectFields, configuredPiiSubjectKms, isRecordKeyedField, isSelfPiiField, resolveSubjectForField, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
41
40
|
import { createEventStoreExecutor, createTenantDb, nullBlindIndexesForSubject, selectMany, WritePurposes, } from "@cosmicdrift/kumiko-framework/db";
|
|
42
41
|
import { deleteFileRefBytes } from "@cosmicdrift/kumiko-framework/derivatives";
|
|
43
42
|
import { createSystemUser, entityHasAnonymizableSubjectField, } from "@cosmicdrift/kumiko-framework/engine";
|
|
43
|
+
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
44
44
|
import { fileRefEntity, fileRefsTable, } from "@cosmicdrift/kumiko-framework/files";
|
|
45
45
|
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
46
46
|
import { collectSingleFileHolders, planFileFields, purgeRowFileRefs, runInSubTransaction, } from "../shared/index.js";
|
|
@@ -49,13 +49,13 @@ import { resolveRetentionPolicyForTenant } from "./resolve-for-tenant.js";
|
|
|
49
49
|
import { tenantRetentionOverrideTable } from "./schema/tenant-retention-override.js";
|
|
50
50
|
const DEFAULT_BATCH_LIMIT = 1000;
|
|
51
51
|
const DEFAULT_REFERENCE_FIELD = "createdAt";
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// entity.fields
|
|
55
|
-
// (table-builder.ts: inserted_at/modified_at).
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
52
|
+
// The boot validator (boot-validator/pii-retention.ts FRAMEWORK_TIMESTAMP_FIELDS)
|
|
53
|
+
// permits these aliases as retention.reference even when they are not declared in
|
|
54
|
+
// entity.fields, but the physical columns use different names
|
|
55
|
+
// (table-builder.ts: inserted_at/modified_at). Map them to the actual entity field
|
|
56
|
+
// during cleanup; otherwise the column existence check below would cause the cron
|
|
57
|
+
// to do nothing silently. deletedAt/lastSeenAt are real fields (softDelete and
|
|
58
|
+
// session respectively) and need no translation.
|
|
59
59
|
const FRAMEWORK_REFERENCE_ALIAS = {
|
|
60
60
|
createdAt: "insertedAt",
|
|
61
61
|
updatedAt: "modifiedAt",
|
|
@@ -72,7 +72,7 @@ async function purgeMatchingRows(db, table, where, batchLimit, op) {
|
|
|
72
72
|
}
|
|
73
73
|
return count;
|
|
74
74
|
}
|
|
75
|
-
// --- hardDelete crypto-shredding
|
|
75
|
+
// --- hardDelete crypto-shredding ---
|
|
76
76
|
//
|
|
77
77
|
// recordOwned/self-pii resolution (subject-resolver.ts) only ever reads
|
|
78
78
|
// `row.id` for these two kinds — resolveRecordSubject uses opts.entityName +
|
|
@@ -137,7 +137,7 @@ async function eraseHardDeletedRowKeys(args) {
|
|
|
137
137
|
async function forgetRowAndEraseKeys(args) {
|
|
138
138
|
return runInSubTransaction(args.db, async (sp) => {
|
|
139
139
|
const tdb = createTenantDb({ db: sp, tenantId: args.tenantId, mode: "system" });
|
|
140
|
-
const result = await args.executor.forget({ id: args.
|
|
140
|
+
const result = await args.executor.forget({ id: args.rowId }, args.systemUser, tdb);
|
|
141
141
|
if (!result.isSuccess)
|
|
142
142
|
return { isSuccess: false };
|
|
143
143
|
await eraseHardDeletedRowKeys({
|
|
@@ -152,7 +152,7 @@ async function forgetRowAndEraseKeys(args) {
|
|
|
152
152
|
return { isSuccess: true };
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
|
-
// --- hardDelete + file-fields
|
|
155
|
+
// --- hardDelete + file-fields ---
|
|
156
156
|
// Throws (unsafe key, missing list permission) must fail this row closed,
|
|
157
157
|
// not abort the whole tenant run.
|
|
158
158
|
async function deleteFileFailingClosed(key, files, logContext) {
|
|
@@ -169,8 +169,7 @@ async function deleteFileFailingClosed(key, files, logContext) {
|
|
|
169
169
|
// fields: purge the un-shared fileRefs (bytes + derivatives, then the fileRef
|
|
170
170
|
// rows), then the entity row itself.
|
|
171
171
|
async function purgeHardDeleteRowWithFiles(args) {
|
|
172
|
-
const
|
|
173
|
-
const logContext = `tenant=${args.tenantId} entity=${args.entityName} row=${rowId}`;
|
|
172
|
+
const logContext = `tenant=${args.tenantId} entity=${args.entityName} row=${String(args.rowId)}`;
|
|
174
173
|
const files = args.files;
|
|
175
174
|
const outcome = await purgeRowFileRefs({
|
|
176
175
|
db: args.db,
|
|
@@ -198,6 +197,7 @@ async function purgeHardDeleteRowWithFiles(args) {
|
|
|
198
197
|
entityName: args.entityName,
|
|
199
198
|
tenantId: args.tenantId,
|
|
200
199
|
systemUser: args.systemUser,
|
|
200
|
+
rowId: args.rowId,
|
|
201
201
|
row: args.row,
|
|
202
202
|
kms: args.kms,
|
|
203
203
|
registry: args.registry,
|
|
@@ -230,7 +230,14 @@ async function purgeHardDeleteRowsWithFiles(args) {
|
|
|
230
230
|
if (rows.length === 0)
|
|
231
231
|
break;
|
|
232
232
|
for (const row of rows) {
|
|
233
|
-
|
|
233
|
+
const rawId = row["id"];
|
|
234
|
+
if (typeof rawId !== "string" && typeof rawId !== "number") {
|
|
235
|
+
throw new InternalError({
|
|
236
|
+
message: `data-retention: ${args.entityName} row has no usable id (got ${typeof rawId})`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const rowId = rawId;
|
|
240
|
+
cursor = rowId;
|
|
234
241
|
const ok = await purgeHardDeleteRowWithFiles({
|
|
235
242
|
db: args.db,
|
|
236
243
|
table: args.table,
|
|
@@ -238,6 +245,7 @@ async function purgeHardDeleteRowsWithFiles(args) {
|
|
|
238
245
|
tenantId: args.tenantId,
|
|
239
246
|
entityName: args.entityName,
|
|
240
247
|
entity: args.entity,
|
|
248
|
+
rowId,
|
|
241
249
|
row,
|
|
242
250
|
files: args.files,
|
|
243
251
|
entityExecutor: args.entityExecutor,
|
|
@@ -311,9 +319,9 @@ export async function runRetentionCleanup(args) {
|
|
|
311
319
|
const skipped = [];
|
|
312
320
|
// Retention writes go through the executor (events) so a projection rebuild
|
|
313
321
|
// replays the cleanup — a batch deleteMany/updateMany on the (erased)
|
|
314
|
-
// projection table is eventless and gets wiped/resurrected on rebuild
|
|
315
|
-
//
|
|
316
|
-
//
|
|
322
|
+
// projection table is eventless and gets wiped/resurrected on rebuild.
|
|
323
|
+
// The cron acts as the system actor; the WHERE already tenant-scopes, so a
|
|
324
|
+
// system-mode TenantDb (no extra filter) is correct.
|
|
317
325
|
// ponytail: per-row events, one batchLimit-sized page per entity per run —
|
|
318
326
|
// the daily cron converges (hardDelete removes rows, softDelete's isDeleted
|
|
319
327
|
// filter shrinks the set). A single batched forget/delete event is a follow-up.
|
|
@@ -326,8 +334,9 @@ export async function runRetentionCleanup(args) {
|
|
|
326
334
|
const overrideRows = await selectMany(db, tenantRetentionOverrideTable, { tenantId });
|
|
327
335
|
const overrideByEntity = new Map(overrideRows.map((r) => [r.entityName, { config: r.config }]));
|
|
328
336
|
for (const proj of registry.getAllProjections().values()) {
|
|
329
|
-
//
|
|
330
|
-
//
|
|
337
|
+
// Only implicit entity projections with a table are targets, as in
|
|
338
|
+
// soft-delete-cleanup. Custom projections and unmanaged tables (such as
|
|
339
|
+
// sessions) are excluded.
|
|
331
340
|
if (proj.isImplicit !== true || typeof proj.source !== "string" || !proj.table)
|
|
332
341
|
continue;
|
|
333
342
|
const entityName = proj.source;
|
|
@@ -353,9 +362,9 @@ export async function runRetentionCleanup(args) {
|
|
|
353
362
|
}
|
|
354
363
|
const cutoff = computeCutoff(policy.keepFor, now);
|
|
355
364
|
const where = { [referenceField]: { lt: cutoff } };
|
|
356
|
-
//
|
|
357
|
-
// soft-delete-cleanup.
|
|
358
|
-
//
|
|
365
|
+
// Apply tenant scoping only when the table has a tenantId column, as in
|
|
366
|
+
// soft-delete-cleanup. Without this filter, one tenant could affect another
|
|
367
|
+
// tenant's rows.
|
|
359
368
|
if (propertyOf(proj.table, "tenantId") !== undefined) {
|
|
360
369
|
where["tenantId"] = tenantId;
|
|
361
370
|
}
|
|
@@ -371,6 +380,7 @@ export async function runRetentionCleanup(args) {
|
|
|
371
380
|
entityName,
|
|
372
381
|
tenantId,
|
|
373
382
|
systemUser,
|
|
383
|
+
rowId: id,
|
|
374
384
|
row: { id },
|
|
375
385
|
kms,
|
|
376
386
|
registry,
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { createEntity, createLongTextField, createTextField, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
// tenantRetentionOverride — Layer 3
|
|
3
|
+
// tenantRetentionOverride — Layer 3 in the three-layer resolver.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// -
|
|
8
|
-
// - Pilot
|
|
9
|
-
// -
|
|
5
|
+
// One optional override config per (tenantId, entityName) replaces the
|
|
6
|
+
// preset (Layer 2) for that entity only. Use cases:
|
|
7
|
+
// - Law firm in DE: caseFile 6y blockDelete (not in the preset)
|
|
8
|
+
// - Pilot tenant retaining data longer for testing
|
|
9
|
+
// - Industry-specific shorter retention periods
|
|
10
10
|
//
|
|
11
|
-
// reason
|
|
11
|
+
// reason is required so the decision is auditable by the DPO and supervisory
|
|
12
|
+
// authorities.
|
|
12
13
|
//
|
|
13
|
-
// config
|
|
14
|
-
//
|
|
14
|
+
// config is a JSON string with `{ keepFor, strategy, reference? }`. The Zod
|
|
15
|
+
// schema validates it when set-override is called.
|
|
15
16
|
//
|
|
16
|
-
// Tenant-
|
|
17
|
-
//
|
|
17
|
+
// Tenant one-to-many: each tenant can have any number of entity overrides.
|
|
18
|
+
// A UNIQUE index on (tenantId, entityName) allows at most one override per
|
|
19
|
+
// entity.
|
|
18
20
|
export const tenantRetentionOverrideEntity = createEntity({
|
|
19
21
|
table: "read_tenant_retention_overrides",
|
|
20
22
|
fields: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
4
|
-
import { readFileRefContent } from "../file-foundation/index.js";
|
|
4
|
+
import { FileStorageUnavailableError, readFileRefContent, } from "../file-foundation/index.js";
|
|
5
5
|
import { collectRendererPlugins, createRendererFoundationApi, RendererError, } from "../renderer-foundation/index.js";
|
|
6
6
|
import { FALLBACK_LOCALE } from "../template-resolver/index.js";
|
|
7
7
|
export function createTemplateMailRenderer(db, registry, tenantId, config) {
|
|
@@ -44,15 +44,18 @@ function createPdfRenderer(db, registry, tenantId, config, filesForTenant) {
|
|
|
44
44
|
return { content: response.pdfBytes, contentType: PDF_CONTENT_TYPE };
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
-
// A decorative inline image (logo)
|
|
47
|
+
// A decorative inline image (logo) of a tenant without file storage must not fail the whole mail;
|
|
48
|
+
// transient read errors rethrow so the send job retries.
|
|
48
49
|
async function readInlineImageOrNull(fileRefId, read) {
|
|
49
50
|
try {
|
|
50
51
|
return await read();
|
|
51
52
|
}
|
|
52
53
|
catch (err) {
|
|
54
|
+
if (!(err instanceof FileStorageUnavailableError))
|
|
55
|
+
throw err;
|
|
53
56
|
createFallbackLogger("delivery").warn("inline image could not be read, sending without it", {
|
|
54
57
|
fileRefId,
|
|
55
|
-
errorName: err
|
|
58
|
+
errorName: err.name,
|
|
56
59
|
});
|
|
57
60
|
return null;
|
|
58
61
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { unsafeReadRetrying } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import { toPostgresArrayLiteral, unsafeReadRetrying } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
// @wrapper-known semantic-alias
|
|
3
3
|
export async function selectNotificationPreferences(db, tenantId, userId, notificationTypeNames, channelName) {
|
|
4
4
|
return unsafeReadRetrying(db, `SELECT notification_type AS "notificationType", channel, enabled
|
|
@@ -9,5 +9,5 @@ export async function selectNotificationPreferences(db, tenantId, userId, notifi
|
|
|
9
9
|
(notification_type = ANY($3::text[]) AND channel = $4)
|
|
10
10
|
OR (notification_type = '*' AND channel = $4)
|
|
11
11
|
OR (notification_type = ANY($3::text[]) AND channel = '*')
|
|
12
|
-
)`, [tenantId, userId, notificationTypeNames, channelName]);
|
|
12
|
+
)`, [tenantId, userId, toPostgresArrayLiteral(notificationTypeNames), channelName]);
|
|
13
13
|
}
|
|
@@ -266,9 +266,27 @@ export function createDeliveryService(options) {
|
|
|
266
266
|
});
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
|
+
// The app's tenantOrigin callback typically hits the DB: once per run and tenant, not per recipient.
|
|
270
|
+
const unsubscribeOriginsByRun = new WeakMap();
|
|
271
|
+
function unsubscribeOriginOnce(run, links, channelCtx, notificationType) {
|
|
272
|
+
let originsByTenant = unsubscribeOriginsByRun.get(run);
|
|
273
|
+
if (originsByTenant === undefined) {
|
|
274
|
+
originsByTenant = new Map();
|
|
275
|
+
unsubscribeOriginsByRun.set(run, originsByTenant);
|
|
276
|
+
}
|
|
277
|
+
let origin = originsByTenant.get(channelCtx.tenantId);
|
|
278
|
+
if (origin === undefined) {
|
|
279
|
+
origin = unsubscribeLinkOrigin(links, channelCtx, (reason) => logError.warn(`unsubscribe link falls back to the app origin: ${reason}`, {
|
|
280
|
+
tenantId: channelCtx.tenantId,
|
|
281
|
+
notificationType,
|
|
282
|
+
}));
|
|
283
|
+
originsByTenant.set(channelCtx.tenantId, origin);
|
|
284
|
+
}
|
|
285
|
+
return origin;
|
|
286
|
+
}
|
|
269
287
|
// A caller-supplied unsubscribeUrl wins; the recipient comes from the server-side notify
|
|
270
288
|
// context, never from the notification data.
|
|
271
|
-
async function withUnsubscribeUrl(message, channelName, recipient, channelCtx) {
|
|
289
|
+
async function withUnsubscribeUrl(run, message, channelName, recipient, channelCtx) {
|
|
272
290
|
if (!unsubscribeLinks || channelName !== EMAIL_CHANNEL_NAME)
|
|
273
291
|
return message;
|
|
274
292
|
if (registry.getAllNotifications().get(message.notificationType)?.unsubscribe !== true) {
|
|
@@ -277,10 +295,7 @@ export function createDeliveryService(options) {
|
|
|
277
295
|
const supplied = message.data?.["unsubscribeUrl"];
|
|
278
296
|
if (typeof supplied === "string" && supplied !== "")
|
|
279
297
|
return message;
|
|
280
|
-
const origin = await
|
|
281
|
-
tenantId: channelCtx.tenantId,
|
|
282
|
-
notificationType: message.notificationType,
|
|
283
|
-
}));
|
|
298
|
+
const origin = await unsubscribeOriginOnce(run, unsubscribeLinks, channelCtx, message.notificationType);
|
|
284
299
|
const unsubscribeUrl = await buildUnsubscribeUrl(unsubscribeLinks, origin, recipient, {
|
|
285
300
|
tenantId: channelCtx.tenantId,
|
|
286
301
|
notificationType: message.notificationType,
|
|
@@ -476,7 +491,7 @@ export function createDeliveryService(options) {
|
|
|
476
491
|
run,
|
|
477
492
|
channel,
|
|
478
493
|
address,
|
|
479
|
-
message: await withUnsubscribeUrl(message, channel.name, { kind: "user", userId }, channelCtx),
|
|
494
|
+
message: await withUnsubscribeUrl(run, message, channel.name, { kind: "user", userId }, channelCtx),
|
|
480
495
|
channelCtx,
|
|
481
496
|
tenantId,
|
|
482
497
|
recipientId: userId,
|
|
@@ -549,7 +564,7 @@ export function createDeliveryService(options) {
|
|
|
549
564
|
run,
|
|
550
565
|
channel,
|
|
551
566
|
address,
|
|
552
|
-
message: await withUnsubscribeUrl(message, channel.name, { kind: "address", address }, channelCtx),
|
|
567
|
+
message: await withUnsubscribeUrl(run, message, channel.name, { kind: "address", address }, channelCtx),
|
|
553
568
|
channelCtx,
|
|
554
569
|
tenantId,
|
|
555
570
|
recipientId,
|
package/dist/delivery/feature.js
CHANGED
|
@@ -55,10 +55,10 @@ export function createDeliveryFeature(options) {
|
|
|
55
55
|
// boot-validator accepts the projection below because at least one
|
|
56
56
|
// apply-key is a registered domain-event (DELIVERY_ATTEMPT_EVENT).
|
|
57
57
|
// recipientAddress is the real PII (email address); recipientId stays
|
|
58
|
-
// plaintext — pseudonymous fk, same line as config.userId
|
|
58
|
+
// plaintext — pseudonymous fk, same line as config.userId.
|
|
59
59
|
// recipientId is nullable (system-triggered sends to non-users), so the
|
|
60
60
|
// address falls back to the tenant key instead of the plaintext it used
|
|
61
|
-
// to be — still shreddable, one tenant wide
|
|
61
|
+
// to be — still shreddable, one tenant wide.
|
|
62
62
|
r.defineEvent("attempt", deliveryAttemptSchema, {
|
|
63
63
|
piiFields: {
|
|
64
64
|
recipientAddress: { personal: { of: "recipientId", whenAbsent: "tenant" } },
|
|
@@ -135,7 +135,7 @@ export const logQuery = definePagedQueryHandler({
|
|
|
135
135
|
const rows = [...tiedRows, ...beyondRows];
|
|
136
136
|
const lastRow = rows[rows.length - 1];
|
|
137
137
|
const nextCursor = rows.length === limit && lastRow ? encodeSortCursor(sortColumn, lastRow) : null;
|
|
138
|
-
// recipientAddress is stored encrypted under the recipient's DEK
|
|
138
|
+
// recipientAddress is stored encrypted under the recipient's DEK
|
|
139
139
|
// — decrypt for the admin log view; forgotten subjects show [[erased]].
|
|
140
140
|
// The notificationType/recipientAddress → type/recipient rename happens
|
|
141
141
|
// here, not in the client: a projectionList screen has no entity to
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
export declare const preferencesQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"preferences", z.ZodObject<{}, z.core.$strip>, {
|
|
3
|
-
rows:
|
|
3
|
+
rows: any[];
|
|
4
4
|
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
@@ -2,6 +2,7 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import * as z from "zod";
|
|
5
|
+
import { currentNotificationType } from "../notification-type-aliases.js";
|
|
5
6
|
import { notificationPreferencesTable } from "../tables.js";
|
|
6
7
|
export const preferencesQuery = defineQueryHandler({
|
|
7
8
|
name: "preferences",
|
|
@@ -31,6 +32,24 @@ export const preferencesQuery = defineQueryHandler({
|
|
|
31
32
|
tenantId: query.user.tenantId,
|
|
32
33
|
userId: query.user.id,
|
|
33
34
|
});
|
|
34
|
-
|
|
35
|
+
// Rows stored under a former notification name are reported under the current
|
|
36
|
+
// one, collapsed with the same precedence as delivery: an exact row wins,
|
|
37
|
+
// otherwise any disabled former-name row disables.
|
|
38
|
+
const collapsed = new Map();
|
|
39
|
+
for (const row of rows) {
|
|
40
|
+
const notificationType = currentNotificationType(ctx.registry, row.notificationType);
|
|
41
|
+
const key = `${notificationType}\u0000${row.channel}`;
|
|
42
|
+
const isExact = row.notificationType === notificationType;
|
|
43
|
+
const existing = collapsed.get(key);
|
|
44
|
+
if (existing?.isExact)
|
|
45
|
+
continue;
|
|
46
|
+
const formerNameRowDisablesAlready = existing !== undefined &&
|
|
47
|
+
!isExact &&
|
|
48
|
+
(existing.row.enabled === false || row.enabled !== false);
|
|
49
|
+
if (formerNameRowDisablesAlready)
|
|
50
|
+
continue;
|
|
51
|
+
collapsed.set(key, { row: { ...row, notificationType }, isExact });
|
|
52
|
+
}
|
|
53
|
+
return { rows: [...collapsed.values()].map((entry) => entry.row) };
|
|
35
54
|
},
|
|
36
55
|
});
|
|
@@ -23,7 +23,7 @@ export const unsubscribePageChromeQuery = defineQueryHandler({
|
|
|
23
23
|
// Only reachable via a system dispatch: the signed unsubscribe routes after verify(),
|
|
24
24
|
// or createUnsubscribePageChromeReader for a tenant the app resolved server-side.
|
|
25
25
|
agent: { expose: false },
|
|
26
|
-
description: "Returns the tenant's mail brand row and footer resolved for one locale, or null without a mail renderer; dispatched by the signed unsubscribe route to dress its pages like the mail
|
|
26
|
+
description: "Returns the tenant's mail brand row and footer resolved for one locale, or null without a mail renderer; dispatched by the signed unsubscribe route or by createUnsubscribePageChromeReader for a tenant the app resolved server-side, to dress its pages like the mail; not meant for UI callers.",
|
|
27
27
|
handler: async (query, ctx) => {
|
|
28
28
|
const { tenantId } = query.user;
|
|
29
29
|
if (!ctx.systemDb) {
|
package/dist/delivery/tables.js
CHANGED
|
@@ -30,14 +30,15 @@ export const deliveryAttemptsTable = pgTable("store_delivery_attempts", {
|
|
|
30
30
|
priority: text("priority").notNull().default("normal").$type(),
|
|
31
31
|
createdAt: instant("created_at").default(sql `now()`).notNull(),
|
|
32
32
|
}, (t) => [index(DELIVERY_ATTEMPTS_TENANT_CREATED_INDEX).on(t.tenantId, t.createdAt)]);
|
|
33
|
-
// **Unmanaged table** —
|
|
34
|
-
// - id
|
|
35
|
-
// -
|
|
36
|
-
//
|
|
37
|
-
// - created_at
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
33
|
+
// **Unmanaged table** — deliberately has no createEntity because:
|
|
34
|
+
// - id comes from the aggregate stream (no gen_random_uuid()-DEFAULT)
|
|
35
|
+
// - no version/inserted_by/modified_by/modified_at: no in-place edits or
|
|
36
|
+
// audit columns are needed (idempotent-on-replay via PK conflict)
|
|
37
|
+
// - created_at rather than inserted_at reflects historical naming drift, not
|
|
38
|
+
// a bug
|
|
39
|
+
// The app is responsible for tenant scoping in queries and replay idempotency.
|
|
40
|
+
// pgTable remains the source of truth for the query API; the pgTable is derived
|
|
41
|
+
// from this metadata.
|
|
41
42
|
export const deliveryAttemptsTableMeta = defineUnmanagedTable({
|
|
42
43
|
tableName: "store_delivery_attempts",
|
|
43
44
|
indexes: [{ name: DELIVERY_ATTEMPTS_TENANT_CREATED_INDEX, columns: ["tenant_id", "created_at"] }],
|
|
@@ -5,7 +5,9 @@ export type UnsubscribeLinkConfig = {
|
|
|
5
5
|
readonly secret: string;
|
|
6
6
|
readonly baseUrl: string;
|
|
7
7
|
/** The sending tenant's own public origin (subdomain or custom domain), null when it has
|
|
8
|
-
* none. The host must serve this app's `/api` routes
|
|
8
|
+
* none. The host must serve this app's `/api` routes, and the callback must only return a
|
|
9
|
+
* verified, active domain of the tenant (the check that lets the app serve under it), else null:
|
|
10
|
+
* the link carries a bearer token that unsubscribes and yields a resubscribe token. Null, a throw or an unusable value
|
|
9
11
|
* fall back to `baseUrl`, so a missing tenant origin never fails a send. */
|
|
10
12
|
readonly tenantOrigin?: (ctx: ChannelContext) => Promise<string | null | undefined>;
|
|
11
13
|
};
|
|
@@ -10,6 +10,7 @@ export declare const UNSUBSCRIBE_PAGE_EN: {
|
|
|
10
10
|
readonly "mail.unsubscribe.resubscribedMessage": "You have been subscribed again.";
|
|
11
11
|
readonly "mail.unsubscribe.failed": "That did not work. Please try again later.";
|
|
12
12
|
readonly "mail.unsubscribe.failedTitle": "Something went wrong";
|
|
13
|
+
readonly "mail.unsubscribe.resubscribeLimitReached": "This address cannot be subscribed again here.";
|
|
13
14
|
readonly "mail.unsubscribe.invalidTitle": "Link no longer valid";
|
|
14
15
|
readonly "mail.unsubscribe.invalidMessage": "This link is invalid or has expired. Please use the unsubscribe link in a more recent email.";
|
|
15
16
|
};
|
|
@@ -13,6 +13,7 @@ export const UNSUBSCRIBE_PAGE_EN = {
|
|
|
13
13
|
"mail.unsubscribe.resubscribedMessage": "You have been subscribed again.",
|
|
14
14
|
"mail.unsubscribe.failed": "That did not work. Please try again later.",
|
|
15
15
|
"mail.unsubscribe.failedTitle": "Something went wrong",
|
|
16
|
+
"mail.unsubscribe.resubscribeLimitReached": "This address cannot be subscribed again here.",
|
|
16
17
|
"mail.unsubscribe.invalidTitle": "Link no longer valid",
|
|
17
18
|
"mail.unsubscribe.invalidMessage": "This link is invalid or has expired. Please use the unsubscribe link in a more recent email.",
|
|
18
19
|
};
|
|
@@ -135,6 +135,11 @@ ${tokenForm(DELIVERY_RESUBSCRIBE_PATH, state.resubscribeToken, mailT(lang, "mail
|
|
|
135
135
|
title: mailT(lang, "mail.unsubscribe.failedTitle"),
|
|
136
136
|
bodyHtml: paragraph("mail.unsubscribe.failed"),
|
|
137
137
|
};
|
|
138
|
+
case "resubscribeLimitReached":
|
|
139
|
+
return {
|
|
140
|
+
title: mailT(lang, "mail.unsubscribe.failedTitle"),
|
|
141
|
+
bodyHtml: paragraph("mail.unsubscribe.resubscribeLimitReached"),
|
|
142
|
+
};
|
|
138
143
|
case "invalid":
|
|
139
144
|
return {
|
|
140
145
|
title: mailT(lang, "mail.unsubscribe.invalidTitle"),
|
|
@@ -13,13 +13,13 @@ const localeClaimSchema = z
|
|
|
13
13
|
.string()
|
|
14
14
|
.regex(/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{1,8})*$/)
|
|
15
15
|
.optional();
|
|
16
|
-
// Shape
|
|
17
|
-
//
|
|
16
|
+
// Shape of verified JWT payloads. `tenantId` arrives from jose as a string and
|
|
17
|
+
// becomes a branded `TenantId` only after successful `parse()`—never via a blind cast.
|
|
18
18
|
//
|
|
19
19
|
// `kind` is explicitly typed as "absent or undefined, nothing else" here
|
|
20
20
|
// (not bare `.optional()`, which would also accept any other value on an
|
|
21
|
-
// unrelated key) so an address-token payload
|
|
22
|
-
// `kind: "address"
|
|
21
|
+
// unrelated key) so an address-token payload—which always carries
|
|
22
|
+
// `kind: "address"`—fails this schema instead of silently parsing with
|
|
23
23
|
// `addressHash` mistaken for a userId.
|
|
24
24
|
const unsubscribeJwtPayloadSchema = z.object({
|
|
25
25
|
kind: z.undefined().optional(),
|
|
@@ -373,10 +373,16 @@ async function verifyForPage(token, encodedSecret, purpose, headers) {
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
async function verifyPost(request, token, encodedSecret, purpose) {
|
|
376
|
-
return isFormRequest(request.headers["content-type"])
|
|
376
|
+
return isFormRequest(request.headers["content-type"]) && !isOneClickPost(request)
|
|
377
377
|
? verifyForPage(token, encodedSecret, purpose, request.headers)
|
|
378
378
|
: verifyUnsubscribeToken(token, encodedSecret, purpose);
|
|
379
379
|
}
|
|
380
|
+
// RFC 8058 mail-provider POSTs are form-encoded too, but nobody looks at the answer: they keep
|
|
381
|
+
// the JSON rejection and get no branded page.
|
|
382
|
+
function isOneClickPost(request) {
|
|
383
|
+
return (isFormRequest(request.headers["content-type"]) &&
|
|
384
|
+
request.rawBody.trim() === DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE);
|
|
385
|
+
}
|
|
380
386
|
function isFormRequest(contentType) {
|
|
381
387
|
return contentType?.includes("application/x-www-form-urlencoded") === true;
|
|
382
388
|
}
|
|
@@ -435,22 +441,31 @@ export function createUnsubscribeRoutes(options) {
|
|
|
435
441
|
method: "POST",
|
|
436
442
|
path: DELIVERY_UNSUBSCRIBE_PATH,
|
|
437
443
|
entry: "signature",
|
|
438
|
-
verify: async (request) =>
|
|
444
|
+
verify: async (request) => {
|
|
445
|
+
const verified = await verifyPost(request, tokenFromPostRequest(request), encodedSecret, "unsubscribe");
|
|
446
|
+
return verified.kind === "rejected"
|
|
447
|
+
? verified
|
|
448
|
+
: { ...verified, oneClick: isOneClickPost(request) };
|
|
449
|
+
},
|
|
439
450
|
handler: async (c, verified, deps) => {
|
|
440
451
|
if (verified.kind === "rejected") {
|
|
441
452
|
return pageResponse(c, deps, { kind: "invalid" }, verified, 400);
|
|
442
453
|
}
|
|
454
|
+
// No chrome lookup for a one-click client: it never renders the page.
|
|
455
|
+
const pageScope = verified.oneClick
|
|
456
|
+
? { tenantId: undefined, locale: verified.locale }
|
|
457
|
+
: verified;
|
|
443
458
|
// Token-verify passed — everything below is a legitimate write. Don't
|
|
444
459
|
// swallow write-errors as "invalid token", that would mask real bugs
|
|
445
460
|
// (e.g. events-table missing, DB down) behind a misleading 400.
|
|
446
461
|
const succeeded = await dispatchUnsubscribeWrite(verified, deps);
|
|
447
462
|
if (!succeeded) {
|
|
448
|
-
return pageResponse(c, deps, { kind: "failed" },
|
|
463
|
+
return pageResponse(c, deps, { kind: "failed" }, pageScope, 500);
|
|
449
464
|
}
|
|
450
465
|
return pageResponse(c, deps, {
|
|
451
466
|
kind: "unsubscribed",
|
|
452
467
|
resubscribeToken: await signResubscribeToken(verified, encodedSecret),
|
|
453
|
-
},
|
|
468
|
+
}, pageScope, 200);
|
|
454
469
|
},
|
|
455
470
|
});
|
|
456
471
|
// Undo direction: token only from the JSON/form body, never the query
|
|
@@ -468,9 +483,12 @@ export function createUnsubscribeRoutes(options) {
|
|
|
468
483
|
const outcome = await dispatchResubscribeWrite(verified, deps);
|
|
469
484
|
// The undo button on the unsubscribed page posts a form; a browser should land on a page.
|
|
470
485
|
if (isFormRequest(c.req.header("content-type"))) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
486
|
+
if (outcome === "success") {
|
|
487
|
+
return pageResponse(c, deps, { kind: "resubscribed" }, verified, 200);
|
|
488
|
+
}
|
|
489
|
+
return outcome === "limit_reached"
|
|
490
|
+
? pageResponse(c, deps, { kind: "resubscribeLimitReached" }, verified, 409)
|
|
491
|
+
: pageResponse(c, deps, { kind: "failed" }, verified, 500);
|
|
474
492
|
}
|
|
475
493
|
if (outcome === "limit_reached") {
|
|
476
494
|
return c.json({ error: { code: DeliveryErrors.resubscribeLimitReached } }, 409, NO_STORE_HEADERS);
|