@cosmicdrift/kumiko-samples 0.366.0 → 0.368.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +16 -2
- package/packages/bundled-features/src/audit/handlers/details.query.ts +1 -1
- package/packages/bundled-features/src/audit/handlers/list.query.ts +1 -1
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +5 -5
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +10 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +10 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-info.query.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +6 -2
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/seeding.ts +2 -2
- package/packages/bundled-features/src/auth-foundation/feature.ts +43 -34
- package/packages/bundled-features/src/auth-foundation/index.ts +1 -0
- package/packages/bundled-features/src/auth-mfa/db/queries.ts +5 -2
- package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/enable-start-preauth.write.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/reencrypt.job.ts +2 -2
- package/packages/bundled-features/src/auth-mfa/handlers/status.query.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/mfa-code-verifier.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +1 -1
- package/packages/bundled-features/src/auth-mfa-user-data/index.ts +19 -10
- package/packages/bundled-features/src/billing-foundation/billing-info-query.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/feature.ts +138 -135
- package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/projection.ts +3 -2
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +52 -1
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +203 -15
- package/packages/bundled-features/src/cap-counter/changes.json +13 -0
- package/packages/bundled-features/src/cap-counter/feature.ts +84 -73
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +3 -4
- package/packages/bundled-features/src/cap-counter/index.ts +9 -1
- package/packages/bundled-features/src/cap-counter/tables.ts +10 -3
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +1 -2
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +6 -2
- package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +1 -1
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +12 -2
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/handlers/inbox.query.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/handlers/mark-all-read.write.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/handlers/unread-count.query.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/tables.ts +1 -1
- package/packages/bundled-features/src/compliance-profiles/handlers/for-tenant.query.ts +1 -1
- package/packages/bundled-features/src/compliance-profiles/handlers/needs-profile.query.ts +1 -1
- package/packages/bundled-features/src/compliance-profiles/handlers/set-profile.write.ts +1 -2
- package/packages/bundled-features/src/compliance-profiles/resolve-for-tenant.ts +1 -1
- package/packages/bundled-features/src/compliance-profiles/seeding.ts +8 -4
- package/packages/bundled-features/src/compliance-profiles-ops/index.ts +26 -17
- package/packages/bundled-features/src/config/db/queries/resolver.ts +1 -1
- package/packages/bundled-features/src/config/handlers/reencrypt.job.ts +2 -2
- package/packages/bundled-features/src/config/resolver.ts +1 -1
- package/packages/bundled-features/src/config/write-helpers.ts +1 -1
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +9 -7
- package/packages/bundled-features/src/custom-fields/db/queries/field-access.ts +1 -1
- package/packages/bundled-features/src/custom-fields/db/queries/projection.ts +1 -1
- package/packages/bundled-features/src/custom-fields/db/queries/quota.ts +1 -1
- package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +6 -2
- package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +1 -1
- package/packages/bundled-features/src/custom-fields/feature.ts +11 -14
- package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/delete-system-field.write.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/delete-tenant-field.write.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +2 -2
- package/packages/bundled-features/src/custom-fields/wire-for-entity.ts +4 -4
- package/packages/bundled-features/src/data-retention/resolve-for-tenant.ts +1 -1
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +16 -10
- package/packages/bundled-features/src/delivery/address-opt-out.ts +5 -2
- package/packages/bundled-features/src/delivery/attempt-log-retention.ts +1 -1
- package/packages/bundled-features/src/delivery/changes.json +12 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +2 -1
- package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +1 -1
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +1 -1
- package/packages/bundled-features/src/delivery/delivery-service.ts +70 -8
- package/packages/bundled-features/src/delivery/feature.ts +2 -2
- package/packages/bundled-features/src/delivery/handlers/log.query.ts +3 -2
- package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +1 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-address.write.ts +10 -5
- package/packages/bundled-features/src/delivery/index.ts +6 -0
- package/packages/bundled-features/src/delivery/testing.ts +2 -0
- package/packages/bundled-features/src/delivery/types.ts +4 -0
- package/packages/bundled-features/src/delivery/unsubscribe-link.ts +73 -0
- package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +17 -0
- package/packages/bundled-features/src/delivery/unsubscribe.ts +94 -31
- package/packages/bundled-features/src/delivery/upsert-preference.ts +5 -2
- package/packages/bundled-features/src/derivatives-sharp/feature.ts +21 -12
- package/packages/bundled-features/src/derivatives-sharp/index.ts +1 -1
- package/packages/bundled-features/src/document-ingest-foundation/feature.ts +112 -103
- package/packages/bundled-features/src/document-ingest-foundation/forget-extract-with-file-ref.ts +3 -3
- package/packages/bundled-features/src/document-ingest-foundation/index.ts +4 -1
- package/packages/bundled-features/src/feature-toggles/global-feature-state-table.ts +1 -1
- package/packages/bundled-features/src/feature-toggles/handlers/set.write.ts +3 -5
- package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +1 -1
- package/packages/bundled-features/src/file-derivatives/feature.ts +134 -129
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +1 -1
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
- package/packages/bundled-features/src/file-foundation/feature.ts +38 -29
- package/packages/bundled-features/src/file-foundation/index.ts +1 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +1 -1
- package/packages/bundled-features/src/file-provider-inmemory/feature.ts +32 -21
- package/packages/bundled-features/src/file-provider-inmemory/index.ts +6 -1
- package/packages/bundled-features/src/file-provider-s3/changes.json +10 -1
- package/packages/bundled-features/src/file-provider-s3/env-feature.ts +62 -0
- package/packages/bundled-features/src/file-provider-s3/feature.ts +107 -73
- package/packages/bundled-features/src/file-provider-s3/index.ts +6 -1
- package/packages/bundled-features/src/file-provider-s3/lib/index.ts +3 -0
- package/packages/bundled-features/src/{files-provider-s3 → file-provider-s3/lib}/s3-provider.ts +19 -19
- package/packages/bundled-features/src/file-provider-s3-env/index.ts +7 -2
- package/packages/bundled-features/src/files-provider-s3/index.ts +11 -3
- package/packages/bundled-features/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +1 -1
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +1 -2
- package/packages/bundled-features/src/folders/feature.ts +8 -3
- package/packages/bundled-features/src/folders-user-data/index.ts +27 -18
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +1 -1
- package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +1 -1
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +3 -2
- package/packages/bundled-features/src/form-draft/feature.ts +6 -1
- package/packages/bundled-features/src/form-draft/handlers/cleanup.job.ts +2 -2
- package/packages/bundled-features/src/form-draft/index.ts +1 -1
- package/packages/bundled-features/src/form-draft/lookup.ts +1 -1
- package/packages/bundled-features/src/form-draft-user-data/index.ts +19 -10
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +165 -154
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -2
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +5 -2
- package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +3 -2
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +1 -1
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +22 -13
- package/packages/bundled-features/src/inbound-provider-imap/index.ts +6 -1
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +20 -12
- package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +1 -0
- package/packages/bundled-features/src/jobs/db/queries/retention.ts +1 -1
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/detail.query.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +7 -2
- package/packages/bundled-features/src/jobs/handlers/retry.write.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +1 -1
- package/packages/bundled-features/src/jobs/job-run-logger.ts +11 -10
- package/packages/bundled-features/src/jobs/tenant-job-failure-table.ts +1 -1
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +1 -1
- package/packages/bundled-features/src/ledger/feature.ts +6 -3
- package/packages/bundled-features/src/mail-foundation/feature.ts +107 -55
- package/packages/bundled-features/src/mail-foundation/index.ts +4 -0
- package/packages/bundled-features/src/mail-transport-inmemory/feature.ts +25 -17
- package/packages/bundled-features/src/mail-transport-inmemory/index.ts +6 -1
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +91 -79
- package/packages/bundled-features/src/mail-transport-smtp/index.ts +1 -0
- package/packages/bundled-features/src/managed-pages/handlers/by-slug.query.ts +1 -1
- package/packages/bundled-features/src/managed-pages/handlers/by-tenant-published.query.ts +1 -2
- package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +1 -2
- package/packages/bundled-features/src/managed-pages/seeding.ts +8 -4
- package/packages/bundled-features/src/metrics/compute.ts +1 -1
- package/packages/bundled-features/src/metrics/filters.ts +1 -1
- package/packages/bundled-features/src/metrics/validate.ts +1 -1
- package/packages/bundled-features/src/notes-history/feature.ts +6 -3
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
- package/packages/bundled-features/src/notes-history-user-data/hooks.ts +1 -1
- package/packages/bundled-features/src/notes-history-user-data/index.ts +27 -18
- package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +17 -2
- package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -2
- package/packages/bundled-features/src/personal-access-tokens/handlers/revoke.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/resolver.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -2
- package/packages/bundled-features/src/personal-access-tokens/roles.ts +8 -6
- package/packages/bundled-features/src/personal-access-tokens/screens.ts +2 -0
- package/packages/bundled-features/src/readiness/feature.ts +25 -16
- package/packages/bundled-features/src/readiness/index.ts +1 -1
- package/packages/bundled-features/src/secrets/db/queries/read.ts +1 -1
- package/packages/bundled-features/src/secrets/feature.ts +8 -3
- package/packages/bundled-features/src/secrets/handlers/rotate.job.ts +2 -2
- package/packages/bundled-features/src/secrets/secrets-context.ts +4 -3
- package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/detail.query.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -2
- package/packages/bundled-features/src/sessions/handlers/mine.query.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke-all-others.write.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke.write.ts +1 -1
- package/packages/bundled-features/src/sessions/session-callbacks.ts +14 -7
- package/packages/bundled-features/src/sessions/testing.ts +1 -1
- package/packages/bundled-features/src/tags/feature.ts +6 -3
- package/packages/bundled-features/src/template-resolver/api.ts +1 -1
- package/packages/bundled-features/src/template-resolver/changes.json +25 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +61 -9
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +19 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -0
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +10 -2
- package/packages/bundled-features/src/template-resolver/handlers/by-slug.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/by-tenant.query.ts +1 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +1 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +9 -3
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -2
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +7 -2
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +8 -3
- package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +22 -2
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/index.ts +4 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +1 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +57 -10
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +1 -1
- package/packages/bundled-features/src/template-resolver/template-preview.ts +3 -1
- package/packages/bundled-features/src/template-resolver/template-resources.ts +1 -1
- package/packages/bundled-features/src/template-resolver/template-system-only.ts +21 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +48 -24
- package/packages/bundled-features/src/template-resolver/testing.ts +1 -1
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +2 -0
- package/packages/bundled-features/src/template-resolver-user-data/index.ts +23 -14
- package/packages/bundled-features/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/add-member.write.ts +1 -2
- package/packages/bundled-features/src/tenant/handlers/cancel-invitation.write.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/invitations.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/me.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/members.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/memberships.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/remove-member.write.ts +2 -1
- package/packages/bundled-features/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +1 -2
- package/packages/bundled-features/src/tenant/invited-membership.ts +2 -2
- package/packages/bundled-features/src/tenant/last-tenant-admin.ts +1 -2
- package/packages/bundled-features/src/tenant/seeding.ts +12 -12
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +1 -2
- package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -2
- package/packages/bundled-features/src/tenant-lifecycle/lib/revoke-tenant-sessions.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/lifecycle-gate.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +3 -3
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +10 -5
- package/packages/bundled-features/src/tenant-lifecycle/testing.ts +3 -3
- package/packages/bundled-features/src/tenant-settings/feature.ts +34 -29
- package/packages/bundled-features/src/tier-engine/feature.ts +426 -428
- package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +2 -3
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +5 -2
- package/packages/bundled-features/src/user/changes.json +6 -0
- package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +1 -1
- package/packages/bundled-features/src/user/handlers/create.write.ts +1 -2
- package/packages/bundled-features/src/user/handlers/find-for-auth.query.ts +1 -1
- package/packages/bundled-features/src/user/handlers/list.query.ts +1 -1
- package/packages/bundled-features/src/user/handlers/update-roles.ts +2 -1
- package/packages/bundled-features/src/user/index.ts +5 -1
- package/packages/bundled-features/src/user/invited-global-roles.ts +2 -2
- package/packages/bundled-features/src/user/principal-status.ts +13 -3
- package/packages/bundled-features/src/user/resolve-display-names.ts +1 -1
- package/packages/bundled-features/src/user/seeding.ts +5 -5
- package/packages/bundled-features/src/user-data-rights/audit-download.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
- package/packages/bundled-features/src/user-data-rights/db/queries/export-jobs.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/export-status.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-export.write.ts +1 -2
- package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +3 -3
- package/packages/bundled-features/src/user-data-rights/lib/deny-if-target-outside-admin-tenant.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +7 -2
- package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +7 -3
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +11 -5
- package/packages/bundled-features/src/user-data-rights/run-user-export.ts +10 -5
- package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +1 -2
- package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +1 -1
- package/packages/bundled-features/src/waitlist/lib.ts +5 -2
- package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/feature.ts +58 -47
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/index.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/pending-projection.ts +1 -1
- package/samples/apps/admin-console/src/run-config.ts +2 -2
- package/samples/apps/cap-billing-demo/package.json +3 -0
- package/samples/apps/cap-billing-demo/src/feature.ts +2 -2
- package/samples/apps/cap-billing-demo/src/run-config.ts +8 -8
- package/samples/apps/marketing-demo/src/app/seed.ts +1 -1
- package/samples/apps/marketing-demo/src/db/queries/seed-counts.ts +1 -1
- package/samples/apps/showcase/package.json +3 -0
- package/samples/apps/showcase/src/app/seed-items.ts +1 -1
- package/samples/apps/styleguide/src/app/seed.ts +2 -2
- package/samples/apps/styleguide/src/features/ai-settings/seed.ts +2 -2
- package/samples/apps/ui-walkthrough/scripts/emit-e2e-data.ts +1 -1
- package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +1 -1
- package/samples/apps/ui-walkthrough/src/app/seed-rental.ts +2 -2
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +2 -0
- package/samples/apps/ui-walkthrough/src/app/seed-vehicles.ts +1 -1
- package/samples/apps/ui-walkthrough/src/app/seed.ts +1 -1
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +12 -0
- package/samples/apps/ui-walkthrough/src/db/queries/row-counts.ts +1 -1
- package/samples/apps/ui-walkthrough/src/db/queries/task-counts.ts +1 -1
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +7 -0
- package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +2 -0
- package/samples/apps/use-all-bundled/feature-manifest.json +4 -4
- package/samples/apps/use-all-bundled/package.json +4 -0
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +2 -2
- package/samples/apps/use-all-bundled/src/app/auth-constants.ts +1 -1
- package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +2 -2
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +1 -1
- package/samples/apps/use-all-bundled/src/app/seed-documents.ts +1 -1
- package/samples/apps/use-all-bundled/src/app/seed.ts +11 -5
- package/samples/apps/use-all-bundled/src/run-config.ts +55 -56
- package/samples/apps/user-data-rights-demo/README.md +2 -2
- package/samples/apps/user-data-rights-demo/package.json +3 -0
- package/samples/apps/user-data-rights-demo/src/run-config.ts +7 -7
- package/samples/apps/workspaces/package.json +3 -0
- package/samples/recipes/access-control/package.json +3 -0
- package/samples/recipes/anonymous-access/package.json +3 -0
- package/samples/recipes/anonymous-access-multitenant/package.json +3 -0
- package/samples/recipes/apex-landing/package.json +1 -0
- package/samples/recipes/apex-surface-auth/package.json +3 -0
- package/samples/recipes/apex-surface-auth/src/feature.ts +2 -2
- package/samples/recipes/api-worker-split/package.json +3 -0
- package/samples/recipes/api-worker-split/src/feature.ts +1 -2
- package/samples/recipes/auth-claims/package.json +3 -0
- package/samples/recipes/auth-foundation-providers/src/feature.ts +1 -1
- package/samples/recipes/auth-mfa/package.json +3 -0
- package/samples/recipes/basic-entity/package.json +3 -0
- package/samples/recipes/chat-channels/package.json +3 -0
- package/samples/recipes/compliance-profiles-demo/package.json +3 -0
- package/samples/recipes/cross-feature-events/package.json +3 -1
- package/samples/recipes/cross-feature-events/src/feature.ts +1 -1
- package/samples/recipes/crypto-shredding-hr/package.json +3 -0
- package/samples/recipes/currencies-global/package.json +3 -0
- package/samples/recipes/currencies-per-tenant/package.json +3 -0
- package/samples/recipes/custom-fields-basic/package.json +3 -0
- package/samples/recipes/custom-handlers/package.json +3 -0
- package/samples/recipes/dashboard-stats/package.json +3 -0
- package/samples/recipes/delivery-notifications/package.json +3 -0
- package/samples/recipes/e2e-generator/package.json +3 -0
- package/samples/recipes/embedded/package.json +3 -0
- package/samples/recipes/embedded-entity-form/package.json +1 -0
- package/samples/recipes/encrypted-tenant-config/README.md +1 -1
- package/samples/recipes/encrypted-tenant-config/package.json +3 -0
- package/samples/recipes/error-contract/package.json +3 -0
- package/samples/recipes/event-sourcing/package.json +3 -1
- package/samples/recipes/event-sourcing/src/feature.ts +16 -20
- package/samples/recipes/event-sourcing/tsconfig.json +2 -6
- package/samples/recipes/feature-toggles/package.json +3 -0
- package/samples/recipes/field-access/package.json +3 -0
- package/samples/recipes/files-post-processing/package.json +1 -0
- package/samples/recipes/golden-path/README.md +30 -0
- package/samples/recipes/golden-path/package.json +11 -0
- package/samples/recipes/golden-path/src/feature.ts +70 -0
- package/samples/recipes/idempotency/package.json +3 -0
- package/samples/recipes/inbound-mail-triage/package.json +3 -0
- package/samples/recipes/lane-routing/package.json +3 -0
- package/samples/recipes/ledger-banking/package.json +3 -0
- package/samples/recipes/ledger-invoicing/package.json +3 -0
- package/samples/recipes/legal-pages/package.json +3 -0
- package/samples/recipes/lifecycle-hooks/package.json +3 -0
- package/samples/recipes/managed-config/package.json +3 -0
- package/samples/recipes/managed-pages/package.json +3 -0
- package/samples/recipes/notes-history-basic/package.json +3 -0
- package/samples/recipes/ownership/package.json +3 -0
- package/samples/recipes/pdf-audit-export/package.json +1 -0
- package/samples/recipes/pipeline-basics/package.json +3 -0
- package/samples/recipes/public-share-token/package.json +3 -0
- package/samples/recipes/public-share-token/src/feature.ts +1 -1
- package/samples/recipes/rate-limiting/package.json +3 -0
- package/samples/recipes/realtime-sse/package.json +3 -0
- package/samples/recipes/record-detail-layout/package.json +1 -0
- package/samples/recipes/reference-data/package.json +3 -0
- package/samples/recipes/relations/package.json +3 -0
- package/samples/recipes/screens-nav/package.json +3 -0
- package/samples/recipes/search/package.json +3 -0
- package/samples/recipes/secrets-demo/package.json +3 -0
- package/samples/recipes/security-baseline/src/feature.ts +2 -2
- package/samples/recipes/session-revocation/README.md +1 -1
- package/samples/recipes/session-revocation/package.json +3 -0
- package/samples/recipes/session-revocation/src/feature.ts +1 -1
- package/samples/recipes/state-machine/package.json +3 -1
- package/samples/recipes/state-machine/src/entities/invoice.ts +2 -2
- package/samples/recipes/state-machine/src/events.ts +19 -0
- package/samples/recipes/state-machine/src/feature.ts +25 -30
- package/samples/recipes/state-machine/src/handlers/_helpers.ts +13 -14
- package/samples/recipes/state-machine/src/handlers/create.ts +1 -1
- package/samples/recipes/state-machine/src/handlers/transitions.ts +129 -122
- package/samples/recipes/state-machine/src/reducer.ts +1 -1
- package/samples/recipes/state-machine/tsconfig.json +2 -6
- package/samples/recipes/tags-basic/package.json +3 -0
- package/samples/recipes/tenant-isolation/package.json +3 -0
- package/samples/recipes/tenant-settings/package.json +3 -0
- package/samples/recipes/tier-admin/package.json +3 -0
- package/samples/recipes/timezones/package.json +3 -0
- package/samples/recipes/tsconfig.base.json +1 -1
- package/samples/recipes/user-data-rights/package.json +3 -0
- package/samples/recipes/user-data-rights/src/feature.ts +6 -2
- package/samples/recipes/user-profile/package.json +3 -0
- package/samples/recipes/user-profile/src/feature.ts +2 -2
- package/samples/recipes/webhook-step/package.json +3 -0
- package/samples/recipes/wizard-form/package.json +1 -0
- package/samples/recipes/workflow-engine/package.json +3 -0
- package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +5 -2
- package/samples/recipes/writeform-section/package.json +1 -0
- package/packages/bundled-features/src/file-provider-s3-env/feature.ts +0 -58
- /package/packages/bundled-features/src/{files-provider-s3 → file-provider-s3/lib}/env-helper.ts +0 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
1
|
import { computeBlindIndex, configuredBlindIndexKey } from "@cosmicdrift/kumiko-framework/crypto";
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createEventStoreExecutor,
|
|
4
|
+
fetchOne,
|
|
5
|
+
type TenantDb,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
4
7
|
import type { SessionUser, TenantId, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
8
|
import { ConflictError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
6
9
|
import { generateDeterministicId } from "@cosmicdrift/kumiko-framework/utils";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { deleteManyBatched } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
1
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { deleteManyBatched } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import { type PruneEventsOptions, pruneEvents } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
4
4
|
import { deliveryAttemptsTable } from "./tables.js";
|
|
5
5
|
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.367.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Delivery signs unsubscribe links itself",
|
|
6
|
+
"detail": "r.notification(name, { unsubscribe: true }) makes delivery put a signed data.unsubscribeUrl into every email of that notification when the service has unsubscribeLinks. User recipients get a user token, route recipients an address token (only with a blind-index key, otherwise no link). The token is scoped to tenant, notification and channel; a link the caller passes in data.unsubscribeUrl wins. runProdApp option unsubscribe: { secret } mounts createUnsubscribeRoutes and wires the links; the base URL comes from auth.publicOrigin, auth.mail.baseUrl or APP_BASE_URL, and without one boot warns and no link is added."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.367.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Unsubscribe links use the tenant's host and the mail's language, apps learn about address opt-outs",
|
|
12
|
+
"detail": "unsubscribeLinks and the runProdApp option unsubscribe take tenantOrigin, an app callback that returns the sending tenant's own origin (subdomain or custom domain) per send; null, a throw or a non-https value fall back to the app origin and never fail the send. The signed token now carries the mail's locale (notify option locale), so the confirmation, result and undo pages render in that language; tokens without it keep working and show English. A form post to the resubscribe route now answers with an HTML page, JSON callers are unchanged. After a committed address opt-out an app job declared with trigger on DeliveryHandlers.unsubscribeAddress receives addressHash, notificationType and channel (unsubscribeAddressPayloadSchema) with the tenant in ctx.triggeredBy.tenantId; hashUnsubscribeAddress(email) matches it to the app's own subscriber. UNSUBSCRIBE_PAGE_EN exports the English page texts, German and Spanish come with the locale packages."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.366.0",
|
|
4
16
|
"type": "improvement",
|
|
@@ -148,11 +148,12 @@ export function buildChannelContext(
|
|
|
148
148
|
filesForTenant?: (tenantId: string) => FileContext | undefined,
|
|
149
149
|
): ChannelContext {
|
|
150
150
|
return {
|
|
151
|
-
db: createTenantDb(db, tenantId),
|
|
151
|
+
db: createTenantDb({ db, tenantId }),
|
|
152
152
|
registry,
|
|
153
153
|
sseBroker,
|
|
154
154
|
tenantId,
|
|
155
155
|
secrets,
|
|
156
|
+
config: tenantConfig,
|
|
156
157
|
renderMail: createTemplateMailRenderer(db, registry, tenantId, tenantConfig),
|
|
157
158
|
...(filesForTenant !== undefined && {
|
|
158
159
|
readAttachment: createEmailAttachmentReader(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
1
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { notificationAddressOptOutsTable } from "../../tables.js";
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { unsafeReadRetrying } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
1
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { unsafeReadRetrying } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
|
|
5
5
|
export type NotificationPreferenceRow = {
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
16
16
|
import {
|
|
17
17
|
assertNotificationVariables,
|
|
18
|
+
bridgeStub,
|
|
18
19
|
createSystemUser,
|
|
19
20
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
20
21
|
import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
|
|
@@ -24,7 +25,6 @@ import {
|
|
|
24
25
|
createEscapeHatchReporter,
|
|
25
26
|
} from "@cosmicdrift/kumiko-framework/pipeline";
|
|
26
27
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
27
|
-
import { bridgeStub } from "@cosmicdrift/kumiko-framework/testing/handler-context";
|
|
28
28
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
29
29
|
import type { Redis } from "ioredis";
|
|
30
30
|
import { fileContextFromResolver } from "../file-foundation/index.js";
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
DeliveryJobs,
|
|
38
38
|
deliveryIdempotencyRedisKey,
|
|
39
39
|
deliveryPriorityRank,
|
|
40
|
+
EMAIL_CHANNEL_NAME,
|
|
40
41
|
} from "./constants.js";
|
|
41
42
|
import {
|
|
42
43
|
type AddressOptOutKey,
|
|
@@ -55,6 +56,12 @@ import {
|
|
|
55
56
|
isDeliveryChannelPlugin,
|
|
56
57
|
type RenderedMessage,
|
|
57
58
|
} from "./types.js";
|
|
59
|
+
import {
|
|
60
|
+
buildUnsubscribeUrl,
|
|
61
|
+
type UnsubscribeLinkConfig,
|
|
62
|
+
type UnsubscribeLinkRecipient,
|
|
63
|
+
unsubscribeLinkOrigin,
|
|
64
|
+
} from "./unsubscribe-link.js";
|
|
58
65
|
|
|
59
66
|
export function redactedMessageOf(err: unknown): string {
|
|
60
67
|
return redactErrorText(err instanceof Error ? err.message : String(err));
|
|
@@ -97,6 +104,9 @@ export type DeliveryServiceOptions = {
|
|
|
97
104
|
// absent means an unattributed warn log (see fallbackEscapeHatchReporter).
|
|
98
105
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
99
106
|
readonly log?: Logger;
|
|
107
|
+
// Signs `data.unsubscribeUrl` for email notifications declared with `unsubscribe: true`.
|
|
108
|
+
// The secret must be the one `createUnsubscribeRoutes` verifies with.
|
|
109
|
+
readonly unsubscribeLinks?: UnsubscribeLinkConfig;
|
|
100
110
|
};
|
|
101
111
|
|
|
102
112
|
// Build channel list from registry extension usages
|
|
@@ -128,6 +138,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
128
138
|
tenantConfigFor,
|
|
129
139
|
escapeHatchAuditSink,
|
|
130
140
|
log,
|
|
141
|
+
unsubscribeLinks,
|
|
131
142
|
} = options;
|
|
132
143
|
const idemRedis = idempotencyRedis ?? rateLimit?.redis;
|
|
133
144
|
const logError = createFallbackLogger("delivery", log);
|
|
@@ -262,17 +273,27 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
262
273
|
// path a cross-tenant read the dispatcher never allows. Cross-tenant reach only
|
|
263
274
|
// exists through the grant-carrying systemDb view below.
|
|
264
275
|
const isSystem = registry.isHandlerSystemScoped(tenantUserIdsQuery);
|
|
265
|
-
const tenantDb = createTenantDb(
|
|
266
|
-
|
|
276
|
+
const tenantDb = createTenantDb({
|
|
277
|
+
db: run.db,
|
|
278
|
+
tenantId,
|
|
279
|
+
mode: "tenant",
|
|
280
|
+
grants: {
|
|
281
|
+
report,
|
|
282
|
+
},
|
|
267
283
|
});
|
|
268
284
|
// The dispatcher grants a systemScope handler's ctx.systemDb.unsafeRaw() ungated —
|
|
269
285
|
// the grant comes from the handler's own r.systemScope() registration, not from this
|
|
270
286
|
// caller. Only the systemDb view's own source TenantDb carries the grant, so it
|
|
271
287
|
// can't leak onto ctx.db.
|
|
272
288
|
const systemDbSourceTenantDb = isSystem
|
|
273
|
-
? createTenantDb(
|
|
274
|
-
|
|
275
|
-
|
|
289
|
+
? createTenantDb({
|
|
290
|
+
db: run.db,
|
|
291
|
+
tenantId,
|
|
292
|
+
mode: "system",
|
|
293
|
+
grants: {
|
|
294
|
+
report,
|
|
295
|
+
unsafeRaw: { reason: `r.systemScope() handler "${tenantUserIdsQuery}"` },
|
|
296
|
+
},
|
|
276
297
|
})
|
|
277
298
|
: undefined;
|
|
278
299
|
// Hand-built context, not routed through the dispatcher — this needs both
|
|
@@ -454,6 +475,37 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
454
475
|
}
|
|
455
476
|
}
|
|
456
477
|
|
|
478
|
+
// A caller-supplied unsubscribeUrl wins; the recipient comes from the server-side notify
|
|
479
|
+
// context, never from the notification data.
|
|
480
|
+
async function withUnsubscribeUrl(
|
|
481
|
+
message: ChannelMessage,
|
|
482
|
+
channelName: string,
|
|
483
|
+
recipient: UnsubscribeLinkRecipient,
|
|
484
|
+
channelCtx: ChannelContext,
|
|
485
|
+
): Promise<ChannelMessage> {
|
|
486
|
+
if (!unsubscribeLinks || channelName !== EMAIL_CHANNEL_NAME) return message;
|
|
487
|
+
if (registry.getAllNotifications().get(message.notificationType)?.unsubscribe !== true) {
|
|
488
|
+
return message;
|
|
489
|
+
}
|
|
490
|
+
const supplied = message.data?.["unsubscribeUrl"];
|
|
491
|
+
if (typeof supplied === "string" && supplied !== "") return message;
|
|
492
|
+
const origin = await unsubscribeLinkOrigin(unsubscribeLinks, channelCtx, (reason) =>
|
|
493
|
+
logError.warn(`unsubscribe link falls back to the app origin: ${reason}`, {
|
|
494
|
+
tenantId: channelCtx.tenantId,
|
|
495
|
+
notificationType: message.notificationType,
|
|
496
|
+
}),
|
|
497
|
+
);
|
|
498
|
+
const unsubscribeUrl = await buildUnsubscribeUrl(unsubscribeLinks, origin, recipient, {
|
|
499
|
+
tenantId: channelCtx.tenantId,
|
|
500
|
+
notificationType: message.notificationType,
|
|
501
|
+
channel: channelName,
|
|
502
|
+
locale: message.locale,
|
|
503
|
+
});
|
|
504
|
+
return unsubscribeUrl === undefined
|
|
505
|
+
? message
|
|
506
|
+
: { ...message, data: { ...message.data, unsubscribeUrl } };
|
|
507
|
+
}
|
|
508
|
+
|
|
457
509
|
function buildMessage(
|
|
458
510
|
notificationType: string,
|
|
459
511
|
data: Readonly<Record<string, unknown>> | undefined,
|
|
@@ -744,7 +796,12 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
744
796
|
run,
|
|
745
797
|
channel,
|
|
746
798
|
address,
|
|
747
|
-
message
|
|
799
|
+
message: await withUnsubscribeUrl(
|
|
800
|
+
message,
|
|
801
|
+
channel.name,
|
|
802
|
+
{ kind: "user", userId },
|
|
803
|
+
channelCtx,
|
|
804
|
+
),
|
|
748
805
|
channelCtx,
|
|
749
806
|
tenantId,
|
|
750
807
|
recipientId: userId,
|
|
@@ -842,7 +899,12 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
842
899
|
run,
|
|
843
900
|
channel,
|
|
844
901
|
address,
|
|
845
|
-
message
|
|
902
|
+
message: await withUnsubscribeUrl(
|
|
903
|
+
message,
|
|
904
|
+
channel.name,
|
|
905
|
+
{ kind: "address", address },
|
|
906
|
+
channelCtx,
|
|
907
|
+
),
|
|
846
908
|
channelCtx,
|
|
847
909
|
tenantId,
|
|
848
910
|
recipientId,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { updateMany, upsertByPk } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
1
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { updateMany, upsertByPk } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import {
|
|
4
4
|
access,
|
|
5
5
|
defineFeature,
|
|
@@ -61,7 +61,7 @@ export function createDeliveryFeature(options?: DeliveryFeatureOptions): Feature
|
|
|
61
61
|
const attemptLogRetentionDays = resolveAttemptLogRetentionDays(options?.attemptLogRetentionDays);
|
|
62
62
|
return defineFeature("delivery", (r) => {
|
|
63
63
|
r.describe(
|
|
64
|
-
|
|
64
|
+
"The notification dispatch core: call `ctx.notify(notificationType, { to, route, data, priority, idempotencyKey })` from any handler to fan out a notification across all registered channels (email, in-app, push). Inside a write handler the delivery runs after the write has committed (the call returns `{ kind: \"deferred\" }`, a rolled-back write sends nothing); `immediate: true` delivers synchronously and returns `{ kind: \"dispatched\", deliveries }`. It stores per-user channel preferences in the `notification-preference` entity, opt-outs for no-account recipient addresses in `notification-address-opt-out` (keyed by a blind-index hash, never the plaintext address), logs every attempt to `store_delivery_attempts`, and enforces idempotency and rate-limiting \u2014 add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything. Unsubscribe/resubscribe links are served by `createUnsubscribeRoutes({ secret })` mounted via the app's `extraRoutes` at `/api/delivery/unsubscribe` and `/api/delivery/resubscribe`: `GET /unsubscribe?token=` renders a confirmation page (no write), `POST /unsubscribe` performs the opt-out (RFC 8058 one-click, token from the form body or query), `POST /resubscribe` undoes it (JSON `{token}` or form body only, never the query — reachable by a mail-client link prefetcher) — sign links with `signUnsubscribeToken` / `signAddressUnsubscribeToken` using the same secret. `channel-email` sets `List-Unsubscribe` / `List-Unsubscribe-Post` automatically when a message's `data.unsubscribeUrl` points at the unsubscribe route. A notification declared with `unsubscribe: true` gets that link from delivery itself when the service has `unsubscribeLinks` (`runProdApp` option `unsubscribe: { secret }` mounts the routes and wires it; base URL from `auth.publicOrigin`, `auth.mail.baseUrl` or `APP_BASE_URL`): user recipients get a user token, `route` recipients an address token (needs a blind-index key, otherwise no link); a `data.unsubscribeUrl` from the caller wins. `unsubscribe.tenantOrigin: (ctx) => origin | null` points the link at the tenant's own host (subdomain or custom domain, must serve this app's `/api`); null, a throw or a non-https value fall back to the app origin. The token carries the mail's locale, so the confirmation and result pages render in the mail's language. To react to a no-account opt-out (e.g. remove the app's own subscriber row), declare `r.job(name, { trigger: { on: DeliveryHandlers.unsubscribeAddress } }, handler)` on a lane the app consumes: it runs after each committed opt-out, also an idempotent repeat, with `unsubscribeAddressPayloadSchema` (`addressHash`, `notificationType`, `channel`) as payload and the tenant in `ctx.triggeredBy.tenantId`; match the subscriber via `hashUnsubscribeAddress(email)`. Declare `unsubscribeUrl` as an optional template variable if the mail body shows it.",
|
|
65
65
|
);
|
|
66
66
|
r.uiHints({
|
|
67
67
|
displayLabel: "Notifications \u00b7 Dispatch Core",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { selectMany, type WhereObject } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
1
|
import {
|
|
3
2
|
decodeKeysetCursor,
|
|
4
3
|
encodeKeysetCursor,
|
|
4
|
+
selectMany,
|
|
5
5
|
type TenantDb,
|
|
6
|
+
type WhereObject,
|
|
6
7
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
7
8
|
import {
|
|
8
9
|
access,
|
|
@@ -66,7 +67,7 @@ function isDeliveryLogSortField(value: string): value is DeliveryLogSortField {
|
|
|
66
67
|
type DeliveryLogSortColumn = (typeof DELIVERY_LOG_SORT_COLUMNS)[DeliveryLogSortField];
|
|
67
68
|
|
|
68
69
|
// createdAt round-trips through Temporal.Instant (matches how the WHERE
|
|
69
|
-
// builder coerces timestamptz values — see
|
|
70
|
+
// builder coerces timestamptz values — see db/query.ts prepareValue);
|
|
70
71
|
// every other whitelisted column is a plain text column. Keyed by the
|
|
71
72
|
// PHYSICAL column, not the display alias — row only has the DB field names
|
|
72
73
|
// (e.g. notificationType), never the alias (e.g. "type").
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { selectMany } from "@cosmicdrift/kumiko-framework/
|
|
1
|
+
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";
|
|
@@ -4,13 +4,18 @@ import * as z from "zod";
|
|
|
4
4
|
import { upsertAddressOptOut } from "../address-opt-out.js";
|
|
5
5
|
import { currentNotificationType } from "../notification-type-aliases.js";
|
|
6
6
|
|
|
7
|
+
// Also the payload an app job receives with `trigger: { on: DeliveryHandlers.unsubscribeAddress }`
|
|
8
|
+
// after a committed address opt-out (the tenant is the job's `ctx.triggeredBy.tenantId`).
|
|
9
|
+
export const unsubscribeAddressPayloadSchema = z.object({
|
|
10
|
+
addressHash: z.string().min(1),
|
|
11
|
+
notificationType: z.string().min(1),
|
|
12
|
+
channel: z.string().min(1),
|
|
13
|
+
});
|
|
14
|
+
export type UnsubscribeAddressPayload = z.infer<typeof unsubscribeAddressPayloadSchema>;
|
|
15
|
+
|
|
7
16
|
export const unsubscribeAddressWrite = defineWriteHandler({
|
|
8
17
|
name: "unsubscribeAddress",
|
|
9
|
-
schema:
|
|
10
|
-
addressHash: z.string().min(1),
|
|
11
|
-
notificationType: z.string().min(1),
|
|
12
|
-
channel: z.string().min(1),
|
|
13
|
-
}),
|
|
18
|
+
schema: unsubscribeAddressPayloadSchema,
|
|
14
19
|
access: { roles: access.systemAdmin },
|
|
15
20
|
// Only reachable via createUnsubscribeRoutes' POST dispatchSystemWrite, once
|
|
16
21
|
// verify() has already proven the address-token signature — not a surface
|
|
@@ -63,6 +63,10 @@ export {
|
|
|
63
63
|
type RateLimitConfig,
|
|
64
64
|
} from "./delivery-service.js";
|
|
65
65
|
export { createDeliveryFeature, type DeliveryFeatureOptions } from "./feature.js";
|
|
66
|
+
export {
|
|
67
|
+
type UnsubscribeAddressPayload,
|
|
68
|
+
unsubscribeAddressPayloadSchema,
|
|
69
|
+
} from "./handlers/unsubscribe-address.write.js";
|
|
66
70
|
export { createDeliveryNotifyFactory } from "./notify-factory.js";
|
|
67
71
|
export {
|
|
68
72
|
deliveryAttemptsTable,
|
|
@@ -94,3 +98,5 @@ export {
|
|
|
94
98
|
type UnsubscribeRouteOptions,
|
|
95
99
|
type UnsubscribeTokenPayload,
|
|
96
100
|
} from "./unsubscribe.js";
|
|
101
|
+
export type { UnsubscribeLinkConfig } from "./unsubscribe-link.js";
|
|
102
|
+
export { UNSUBSCRIBE_PAGE_EN } from "./unsubscribe-page-strings.js";
|
|
@@ -15,6 +15,7 @@ import type { KillSwitchResolver, RateLimitConfig } from "./delivery-service.js"
|
|
|
15
15
|
import { collectChannels, createDeliveryService } from "./delivery-service.js";
|
|
16
16
|
import { createDeliveryNotifyFactory } from "./notify-factory.js";
|
|
17
17
|
import type { DeliveryService } from "./types.js";
|
|
18
|
+
import type { UnsubscribeLinkConfig } from "./unsubscribe-link.js";
|
|
18
19
|
|
|
19
20
|
export type CreateDeliveryTestContextOptions = {
|
|
20
21
|
readonly tenantUserIdsQuery?: string;
|
|
@@ -27,6 +28,7 @@ export type CreateDeliveryTestContextOptions = {
|
|
|
27
28
|
readonly idempotencyRedis?: Redis;
|
|
28
29
|
readonly filesForTenant?: (tenantId: string) => FileContext | undefined;
|
|
29
30
|
readonly tenantConfigFor?: (tenantId: TenantId) => ConfigAccessor;
|
|
31
|
+
readonly unsubscribeLinks?: UnsubscribeLinkConfig;
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type {
|
|
4
|
+
ConfigAccessor,
|
|
4
5
|
DeliveryErrorCode,
|
|
5
6
|
NotifyDeliveryStatus,
|
|
6
7
|
NotifyDispatched,
|
|
@@ -39,6 +40,9 @@ export type ChannelContext = {
|
|
|
39
40
|
// Chat channels read their per-tenant webhook URL / bot token from it.
|
|
40
41
|
// Absent when no secrets feature/KEK is wired.
|
|
41
42
|
readonly secrets?: SecretsContext | undefined;
|
|
43
|
+
// The tenant's own config accessor, built server-side for this tenantId. Absent when the
|
|
44
|
+
// delivery service has no tenantConfigFor and the job context carries no config.
|
|
45
|
+
readonly config?: ConfigAccessor | undefined;
|
|
42
46
|
// Renders a stored mail template, the tenant's own or the system fallback.
|
|
43
47
|
readonly renderMail?: (payload: MailPayload) => Promise<RenderedMail>;
|
|
44
48
|
// Reads the bytes behind an attachment ref at send time. Absent when no file storage is wired.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { publicOriginOf } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
4
|
+
import { DELIVERY_UNSUBSCRIBE_PATH } from "./constants.js";
|
|
5
|
+
import type { ChannelContext } from "./types.js";
|
|
6
|
+
import { signAddressUnsubscribeToken, signUnsubscribeToken } from "./unsubscribe.js";
|
|
7
|
+
|
|
8
|
+
/** Secret shared with `createUnsubscribeRoutes` plus the public origin the links point at. */
|
|
9
|
+
export type UnsubscribeLinkConfig = {
|
|
10
|
+
readonly secret: string;
|
|
11
|
+
readonly baseUrl: string;
|
|
12
|
+
/** The sending tenant's own public origin (subdomain or custom domain), null when it has
|
|
13
|
+
* none. The host must serve this app's `/api` routes. Null, a throw or an unusable value
|
|
14
|
+
* fall back to `baseUrl`, so a missing tenant origin never fails a send. */
|
|
15
|
+
readonly tenantOrigin?: (ctx: ChannelContext) => Promise<string | null | undefined>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type UnsubscribeLinkRecipient =
|
|
19
|
+
| { readonly kind: "user"; readonly userId: string }
|
|
20
|
+
| { readonly kind: "address"; readonly address: string };
|
|
21
|
+
|
|
22
|
+
// https only, unless the app itself runs on plain http (local dev): a tenant origin must not
|
|
23
|
+
// downgrade the link that carries the token.
|
|
24
|
+
function isUsableTenantOrigin(origin: string, baseUrl: string): boolean {
|
|
25
|
+
return (
|
|
26
|
+
origin.startsWith("https://") || (baseUrl.startsWith("http://") && origin.startsWith("http://"))
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function unsubscribeLinkOrigin(
|
|
31
|
+
config: UnsubscribeLinkConfig,
|
|
32
|
+
ctx: ChannelContext,
|
|
33
|
+
onFallback: (reason: string) => void,
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
if (config.tenantOrigin === undefined) return config.baseUrl;
|
|
36
|
+
let resolved: string | null | undefined;
|
|
37
|
+
try {
|
|
38
|
+
resolved = await config.tenantOrigin(ctx);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
onFallback(`tenantOrigin threw (${err instanceof Error ? err.name : typeof err})`);
|
|
41
|
+
return config.baseUrl;
|
|
42
|
+
}
|
|
43
|
+
if (resolved === null || resolved === undefined) return config.baseUrl;
|
|
44
|
+
const origin = publicOriginOf(resolved);
|
|
45
|
+
if (origin === undefined || !isUsableTenantOrigin(origin, config.baseUrl)) {
|
|
46
|
+
onFallback("tenantOrigin returned no usable https origin");
|
|
47
|
+
return config.baseUrl;
|
|
48
|
+
}
|
|
49
|
+
return origin;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Undefined means "no link": without a blind-index key an address opt-out could never be
|
|
53
|
+
// matched again, and a mail without a link is better than a failed send.
|
|
54
|
+
export async function buildUnsubscribeUrl(
|
|
55
|
+
config: UnsubscribeLinkConfig,
|
|
56
|
+
origin: string,
|
|
57
|
+
recipient: UnsubscribeLinkRecipient,
|
|
58
|
+
scope: {
|
|
59
|
+
tenantId: TenantId;
|
|
60
|
+
notificationType: string;
|
|
61
|
+
channel: string;
|
|
62
|
+
locale: string | undefined;
|
|
63
|
+
},
|
|
64
|
+
): Promise<string | undefined> {
|
|
65
|
+
if (recipient.kind === "address" && hashUnsubscribeAddress(recipient.address) === undefined) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const token =
|
|
69
|
+
recipient.kind === "user"
|
|
70
|
+
? await signUnsubscribeToken({ ...scope, userId: recipient.userId }, config.secret)
|
|
71
|
+
: await signAddressUnsubscribeToken({ ...scope, address: recipient.address }, config.secret);
|
|
72
|
+
return `${origin}${DELIVERY_UNSUBSCRIBE_PATH}?token=${encodeURIComponent(token)}`;
|
|
73
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
|
|
2
|
+
|
|
3
|
+
// The pages behind a mailed unsubscribe link speak the mail's language, so their texts live in
|
|
4
|
+
// the mail translations: English here, other languages in the locale packages.
|
|
5
|
+
export const UNSUBSCRIBE_PAGE_EN = {
|
|
6
|
+
"mail.unsubscribe.confirmTitle": "Unsubscribe",
|
|
7
|
+
"mail.unsubscribe.confirmQuestion": "Unsubscribe from these emails?",
|
|
8
|
+
"mail.unsubscribe.confirmButton": "Unsubscribe",
|
|
9
|
+
"mail.unsubscribe.doneTitle": "Unsubscribed",
|
|
10
|
+
"mail.unsubscribe.doneMessage": "You have been unsubscribed.",
|
|
11
|
+
"mail.unsubscribe.undoButton": "Undo",
|
|
12
|
+
"mail.unsubscribe.resubscribedTitle": "Subscribed again",
|
|
13
|
+
"mail.unsubscribe.resubscribedMessage": "You have been subscribed again.",
|
|
14
|
+
"mail.unsubscribe.failed": "That did not work. Please try again later.",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
registerMailTranslations("en", UNSUBSCRIBE_PAGE_EN);
|