@cosmicdrift/kumiko-samples 0.362.0 → 0.363.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +17 -6
- package/packages/bundled-features/src/admin-shell/changes.json +6 -0
- package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
- package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
- package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +14 -0
- package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
- package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +3 -12
- package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
- package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
- package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
- package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
- package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
- package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
- package/packages/bundled-features/src/billing-foundation/changes.json +7 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +38 -44
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +115 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +30 -30
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
- package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +3 -6
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
- package/packages/bundled-features/src/channel-email/changes.json +19 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +122 -29
- package/packages/bundled-features/src/channel-email/feature.ts +6 -3
- package/packages/bundled-features/src/channel-email/index.ts +7 -1
- package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
- package/packages/bundled-features/src/channel-email/types.ts +23 -0
- package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
- package/packages/bundled-features/src/config/handlers/set.write.ts +1 -5
- package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
- package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
- package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
- package/packages/bundled-features/src/data-retention/changes.json +6 -0
- package/packages/bundled-features/src/data-retention/index.ts +1 -0
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
- package/packages/bundled-features/src/delivery/attachment-ref.ts +84 -0
- package/packages/bundled-features/src/delivery/changes.json +48 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +129 -4
- package/packages/bundled-features/src/delivery/constants.ts +2 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
- package/packages/bundled-features/src/delivery/feature.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +17 -2
- package/packages/bundled-features/src/delivery/jobs.ts +48 -5
- package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
- package/packages/bundled-features/src/delivery/testing.ts +9 -1
- package/packages/bundled-features/src/delivery/types.ts +28 -18
- package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
- package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
- package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
- package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
- package/packages/bundled-features/src/file-foundation/index.ts +7 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -0
- package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
- package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
- package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +40 -25
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
- package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
- package/packages/bundled-features/src/ledger/recurring.ts +1 -3
- package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
- package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
- package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
- package/packages/bundled-features/src/page-render/branding.ts +3 -0
- package/packages/bundled-features/src/page-render/markdown.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
- package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
- package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
- package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
- package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
- package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
- package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
- package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
- package/packages/bundled-features/src/renderer-foundation/types.ts +132 -35
- package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
- package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/sessions/feature.ts +2 -4
- package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
- package/packages/bundled-features/src/shared/index.ts +6 -0
- package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +25 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
- package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
- package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
- package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
- package/packages/bundled-features/src/template-resolver/README.md +63 -7
- package/packages/bundled-features/src/template-resolver/api.ts +83 -8
- package/packages/bundled-features/src/template-resolver/changes.json +118 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +56 -6
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +142 -0
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +108 -0
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -0
- package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
- package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +106 -0
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
- package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +184 -0
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
- package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +15 -76
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
- package/packages/bundled-features/src/template-resolver/index.ts +100 -0
- package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
- package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
- package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
- package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +30 -0
- package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +147 -17
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
- package/packages/bundled-features/src/template-resolver/table.ts +14 -0
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
- package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +129 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
- package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
- package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
- package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
- package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +19 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +169 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
- package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
- package/packages/bundled-features/src/tenant/seeding.ts +41 -7
- package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
- package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
- package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
- package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/changes.json +7 -0
- package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
- package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
- package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
- package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
- package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
- package/packages/bundled-features/src/waitlist/changes.json +7 -0
- package/packages/bundled-features/src/waitlist/feature.ts +17 -0
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
- package/packages/bundled-features/src/waitlist/mail.ts +69 -18
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +1 -2
- package/samples/apps/ui-walkthrough/package.json +2 -0
- package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
- package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +154 -0
- package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +354 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -0
- package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
- package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +37 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +115 -20
- package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +2 -0
- package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
- package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
- package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
- package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
- package/samples/recipes/delivery-notifications/README.md +26 -25
- package/samples/recipes/delivery-notifications/package.json +2 -1
- package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
- package/samples/recipes/seed-migration/README.md +1 -1
- package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
- package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
- package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
- package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
- package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
|
|
2
|
-
import type { DbRow } from "@cosmicdrift/kumiko-framework/db";
|
|
3
2
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import * as z from "zod";
|
|
4
4
|
import {
|
|
5
|
+
type AttachmentOrigin,
|
|
6
|
+
type ChannelContext,
|
|
5
7
|
type ChannelMessage,
|
|
6
8
|
DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE,
|
|
7
9
|
DELIVERY_UNSUBSCRIBE_PATH,
|
|
8
10
|
type DeliveryChannel,
|
|
9
|
-
|
|
11
|
+
EMAIL_CHANNEL_NAME,
|
|
12
|
+
type EmailAttachmentRef,
|
|
13
|
+
fileAttachmentRefSchema,
|
|
14
|
+
MAX_EMAIL_ATTACHMENTS,
|
|
10
15
|
type RenderedMessage,
|
|
16
|
+
renderedPdfAttachmentRefSchema,
|
|
11
17
|
} from "../delivery/index.js";
|
|
12
18
|
import { decryptStoredPii } from "../shared/index.js";
|
|
13
19
|
import { guardEmailMessage } from "./pii-guard.js";
|
|
14
|
-
import type { EmailTransport } from "./types.js";
|
|
20
|
+
import type { EmailAttachment, EmailTransport } from "./types.js";
|
|
15
21
|
|
|
16
22
|
// Envelope (From / Reply-To / threading headers) rides on the channel data —
|
|
17
23
|
// the notification's email template echoes it out of the notify() call, since
|
|
@@ -83,9 +89,80 @@ function emailEnvelopeFrom(data: Readonly<Record<string, unknown>> | undefined):
|
|
|
83
89
|
};
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
const fileAttachmentsSchema = z
|
|
93
|
+
.array(z.discriminatedUnion("kind", [fileAttachmentRefSchema, renderedPdfAttachmentRefSchema]))
|
|
94
|
+
.max(MAX_EMAIL_ATTACHMENTS);
|
|
95
|
+
|
|
96
|
+
// Only kinds "file" and "rendered-pdf" may come from notify data; inline images are created by the renderer alone.
|
|
97
|
+
// Invalid input throws instead of dropping the attachment silently.
|
|
98
|
+
export function fileAttachmentsFrom(
|
|
99
|
+
data: Readonly<Record<string, unknown>> | undefined,
|
|
100
|
+
messageLocale?: string,
|
|
101
|
+
): readonly EmailAttachmentRef[] {
|
|
102
|
+
const raw = data?.["attachments"];
|
|
103
|
+
if (raw === undefined) return [];
|
|
104
|
+
return fileAttachmentsSchema
|
|
105
|
+
.parse(raw)
|
|
106
|
+
.map((ref) =>
|
|
107
|
+
ref.kind === "rendered-pdf" && ref.locale === undefined && messageLocale !== undefined
|
|
108
|
+
? { ...ref, locale: messageLocale }
|
|
109
|
+
: ref,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const INLINE_EXTENSION_BY_CONTENT_TYPE: Readonly<Record<string, string>> = {
|
|
114
|
+
"image/png": "png",
|
|
115
|
+
"image/jpeg": "jpg",
|
|
116
|
+
"image/gif": "gif",
|
|
117
|
+
"image/webp": "webp",
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
type AttachmentSource = { readonly ref: EmailAttachmentRef; readonly origin: AttachmentOrigin };
|
|
121
|
+
|
|
122
|
+
async function resolveAttachments(
|
|
123
|
+
sources: readonly AttachmentSource[],
|
|
124
|
+
ctx: ChannelContext,
|
|
125
|
+
): Promise<readonly EmailAttachment[]> {
|
|
126
|
+
if (sources.length === 0) return [];
|
|
127
|
+
if (ctx.readAttachment === undefined) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
"email channel: the message has attachments but the channel context cannot read files",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const resolved: EmailAttachment[] = [];
|
|
133
|
+
for (const { ref, origin } of sources) {
|
|
134
|
+
const found = await ctx.readAttachment(ref, origin);
|
|
135
|
+
if (ref.kind === "rendered-pdf") {
|
|
136
|
+
if (found === null) {
|
|
137
|
+
throw new Error(`email channel: rendering attachment "${ref.fileName}" returned nothing`);
|
|
138
|
+
}
|
|
139
|
+
resolved.push({
|
|
140
|
+
kind: "rendered-pdf",
|
|
141
|
+
templateSlug: ref.templateSlug,
|
|
142
|
+
fileName: ref.fileName,
|
|
143
|
+
content: found.content,
|
|
144
|
+
contentType: found.contentType,
|
|
145
|
+
});
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (found === null) {
|
|
149
|
+
// A missing inline image only degrades the look; a missing file is content the recipient expects.
|
|
150
|
+
if (ref.kind === "inline") continue;
|
|
151
|
+
throw new Error(`email channel: attachment file ${ref.fileRefId} was not found`);
|
|
152
|
+
}
|
|
153
|
+
const fileName =
|
|
154
|
+
ref.kind === "file"
|
|
155
|
+
? ref.fileName
|
|
156
|
+
: INLINE_EXTENSION_BY_CONTENT_TYPE[found.contentType]
|
|
157
|
+
? `${ref.cid}.${INLINE_EXTENSION_BY_CONTENT_TYPE[found.contentType]}`
|
|
158
|
+
: ref.cid;
|
|
159
|
+
resolved.push({ ...ref, fileName, content: found.content, contentType: found.contentType });
|
|
160
|
+
}
|
|
161
|
+
return resolved;
|
|
162
|
+
}
|
|
163
|
+
|
|
86
164
|
export type EmailChannelOptions = {
|
|
87
165
|
readonly transport: EmailTransport;
|
|
88
|
-
readonly renderer: NotificationRenderer;
|
|
89
166
|
readonly resolveEmail: (
|
|
90
167
|
userId: string,
|
|
91
168
|
ctx: { db: unknown; tenantId: TenantId },
|
|
@@ -93,32 +170,38 @@ export type EmailChannelOptions = {
|
|
|
93
170
|
};
|
|
94
171
|
|
|
95
172
|
export function createEmailChannel(options: EmailChannelOptions): DeliveryChannel {
|
|
96
|
-
const { transport,
|
|
173
|
+
const { transport, resolveEmail } = options;
|
|
97
174
|
|
|
98
|
-
// Render
|
|
99
|
-
//
|
|
100
|
-
// can retry independently. Extracted so the inline fallback (no job runner)
|
|
175
|
+
// Render runs in the delivery.render job, decoupled from the SMTP send so
|
|
176
|
+
// each can retry independently. Extracted so the inline path (no job runner)
|
|
101
177
|
// can reuse it without going through the channel's own render() indirection.
|
|
102
|
-
async function renderMessage(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
178
|
+
async function renderMessage(
|
|
179
|
+
message: ChannelMessage,
|
|
180
|
+
ctx: ChannelContext,
|
|
181
|
+
): Promise<RenderedMessage> {
|
|
182
|
+
const notificationType = message.notificationType;
|
|
183
|
+
if (ctx.registry.getAllNotifications().get(notificationType)?.template === undefined) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`email channel: notification "${notificationType}" has no mail template, declare template + meta on r.notification`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (ctx.renderMail === undefined) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`email channel: notification "${notificationType}" has a mail template but the channel context carries no renderMail`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const unsubscribeUrl = message.data?.["unsubscribeUrl"];
|
|
194
|
+
const { subject, html, text, attachments } = await ctx.renderMail({
|
|
195
|
+
slug: notificationType,
|
|
196
|
+
locale: message.locale ?? "en",
|
|
197
|
+
variables: message.data ?? {},
|
|
198
|
+
...(typeof unsubscribeUrl === "string" && { unsubscribeUrl }),
|
|
199
|
+
});
|
|
200
|
+
return { html, subject, text, ...(attachments !== undefined && { attachments }) };
|
|
118
201
|
}
|
|
119
202
|
|
|
120
203
|
return {
|
|
121
|
-
name:
|
|
204
|
+
name: EMAIL_CHANNEL_NAME,
|
|
122
205
|
mode: "queued",
|
|
123
206
|
|
|
124
207
|
// resolveEmail typically returns the raw stored user.email, which is ciphertext under an active KMS.
|
|
@@ -129,13 +212,22 @@ export function createEmailChannel(options: EmailChannelOptions): DeliveryChanne
|
|
|
129
212
|
return address === PII_ERASED_SENTINEL ? null : address;
|
|
130
213
|
},
|
|
131
214
|
|
|
132
|
-
render(message,
|
|
133
|
-
return renderMessage(message);
|
|
215
|
+
render(message, ctx) {
|
|
216
|
+
return renderMessage(message, ctx);
|
|
134
217
|
},
|
|
135
218
|
|
|
136
|
-
async send(address, message,
|
|
137
|
-
const { html, subject, text } = rendered ?? (await renderMessage(message));
|
|
219
|
+
async send(address, message, ctx, rendered) {
|
|
220
|
+
const { html, subject, text, attachments } = rendered ?? (await renderMessage(message, ctx));
|
|
138
221
|
const envelope = emailEnvelopeFrom(message.data);
|
|
222
|
+
const resolvedAttachments = await resolveAttachments(
|
|
223
|
+
[
|
|
224
|
+
...(attachments ?? []).map((ref): AttachmentSource => ({ ref, origin: "template" })),
|
|
225
|
+
...fileAttachmentsFrom(message.data, message.locale).map(
|
|
226
|
+
(ref): AttachmentSource => ({ ref, origin: "message" }),
|
|
227
|
+
),
|
|
228
|
+
],
|
|
229
|
+
ctx,
|
|
230
|
+
);
|
|
139
231
|
await transport.send(
|
|
140
232
|
guardEmailMessage({
|
|
141
233
|
to: address,
|
|
@@ -143,6 +235,7 @@ export function createEmailChannel(options: EmailChannelOptions): DeliveryChanne
|
|
|
143
235
|
html,
|
|
144
236
|
...(text !== undefined && { text }),
|
|
145
237
|
...envelope,
|
|
238
|
+
...(resolvedAttachments.length > 0 && { attachments: resolvedAttachments }),
|
|
146
239
|
}),
|
|
147
240
|
);
|
|
148
241
|
return { status: "sent", address };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DELIVERY_CHANNEL_EXTENSION,
|
|
3
|
+
EMAIL_CHANNEL_NAME,
|
|
4
|
+
} from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
2
5
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
6
|
import { createEmailChannel, type EmailChannelOptions } from "./email-channel.js";
|
|
4
7
|
|
|
@@ -7,7 +10,7 @@ export function createChannelEmailFeature(options: EmailChannelOptions): Feature
|
|
|
7
10
|
|
|
8
11
|
return defineFeature("channel-email", (r) => {
|
|
9
12
|
r.describe(
|
|
10
|
-
"Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport
|
|
13
|
+
"Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails.",
|
|
11
14
|
);
|
|
12
15
|
r.uiHints({
|
|
13
16
|
displayLabel: "Email Channel",
|
|
@@ -17,7 +20,7 @@ export function createChannelEmailFeature(options: EmailChannelOptions): Feature
|
|
|
17
20
|
r.requires("delivery");
|
|
18
21
|
|
|
19
22
|
r.translations({ keys: { "delivery.channel.email": { en: "Email" } } });
|
|
20
|
-
r.useExtension(DELIVERY_CHANNEL_EXTENSION,
|
|
23
|
+
r.useExtension(DELIVERY_CHANNEL_EXTENSION, EMAIL_CHANNEL_NAME, {
|
|
21
24
|
mode: channel.mode,
|
|
22
25
|
resolve: channel.resolve,
|
|
23
26
|
render: channel.render,
|
|
@@ -7,4 +7,10 @@ export {
|
|
|
7
7
|
type SmtpEnv,
|
|
8
8
|
type SmtpTransportOptions,
|
|
9
9
|
} from "./smtp-transport.js";
|
|
10
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
createInMemoryTransport,
|
|
12
|
+
type EmailAttachment,
|
|
13
|
+
type EmailAttachmentRef,
|
|
14
|
+
type EmailMessage,
|
|
15
|
+
type EmailTransport,
|
|
16
|
+
} from "./types.js";
|
|
@@ -11,6 +11,18 @@ const CIPHERTEXT_RE = /kumiko-pii:v\d+:[^"\s<>\\]*/g;
|
|
|
11
11
|
// is always refused (the address is garbage — better no mail than a leaked
|
|
12
12
|
// blob); ciphertext in subject/body fails loud in dev and is redacted+logged
|
|
13
13
|
// in prod.
|
|
14
|
+
function assertAttachmentNamesArePlain(message: EmailMessage): void {
|
|
15
|
+
for (const attachment of message.attachments ?? []) {
|
|
16
|
+
const fields = [attachment.fileName, attachment.kind === "inline" ? attachment.cid : undefined];
|
|
17
|
+
if (fields.some((field) => field?.includes(CIPHERTEXT_MARKER))) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`[channel-email] refusing to send: attachment file name is a PII ciphertext ` +
|
|
20
|
+
`("${PII_CIPHERTEXT_PREFIX}…") — decrypt the stored value before mailing (decryptStoredPii).`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
export function guardEmailMessage(message: EmailMessage): EmailMessage {
|
|
15
27
|
// Every envelope address, not just `to`: a ciphertext From, From-Name or Reply-To is
|
|
16
28
|
// the same garbage as a ciphertext recipient — refuse rather than mail it.
|
|
@@ -35,6 +47,7 @@ export function guardEmailMessage(message: EmailMessage): EmailMessage {
|
|
|
35
47
|
);
|
|
36
48
|
}
|
|
37
49
|
}
|
|
50
|
+
assertAttachmentNamesArePlain(message);
|
|
38
51
|
const leaking =
|
|
39
52
|
message.subject.includes(CIPHERTEXT_MARKER) ||
|
|
40
53
|
message.html.includes(CIPHERTEXT_MARKER) ||
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { createTransport, type Transporter } from "nodemailer";
|
|
20
20
|
import addressparser from "nodemailer/lib/addressparser";
|
|
21
|
-
import type { EmailMessage, EmailTransport } from "./types.js";
|
|
21
|
+
import type { EmailAttachment, EmailMessage, EmailTransport } from "./types.js";
|
|
22
22
|
|
|
23
23
|
export type SmtpTransportOptions = {
|
|
24
24
|
/** SMTP-Server-Host (z.B. "smtp.gmail.com", "in-v3.mailjet.com",
|
|
@@ -74,6 +74,17 @@ function fromHeader(
|
|
|
74
74
|
return parsed?.address ? { name, address: parsed.address } : address;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function toNodemailerAttachment(attachment: EmailAttachment) {
|
|
78
|
+
const base = {
|
|
79
|
+
filename: attachment.fileName,
|
|
80
|
+
content: Buffer.from(attachment.content),
|
|
81
|
+
contentType: attachment.contentType,
|
|
82
|
+
};
|
|
83
|
+
return attachment.kind === "inline"
|
|
84
|
+
? { ...base, cid: attachment.cid, contentDisposition: "inline" as const }
|
|
85
|
+
: base;
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
export function createSmtpTransport(options: SmtpTransportOptions): EmailTransport {
|
|
78
89
|
const transporter: Transporter = createTransport({
|
|
79
90
|
host: options.host,
|
|
@@ -95,6 +106,9 @@ export function createSmtpTransport(options: SmtpTransportOptions): EmailTranspo
|
|
|
95
106
|
...(message.text !== undefined && { text: message.text }),
|
|
96
107
|
...(message.replyTo && { replyTo: message.replyTo }),
|
|
97
108
|
...(message.headers && { headers: message.headers }),
|
|
109
|
+
...(message.attachments && {
|
|
110
|
+
attachments: message.attachments.map(toNodemailerAttachment),
|
|
111
|
+
}),
|
|
98
112
|
});
|
|
99
113
|
},
|
|
100
114
|
};
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EmailAttachmentRef,
|
|
3
|
+
FileAttachmentRef,
|
|
4
|
+
InlineAttachmentRef,
|
|
5
|
+
RenderedPdfAttachmentRef,
|
|
6
|
+
} from "../delivery/index.js";
|
|
7
|
+
|
|
8
|
+
export type { EmailAttachmentRef };
|
|
9
|
+
|
|
10
|
+
// What a transport receives: the reference plus the bytes the send job read. A rendered
|
|
11
|
+
// PDF keeps no variables here, they may be personal data and the transport has no use for them.
|
|
12
|
+
export type EmailAttachment = (
|
|
13
|
+
| InlineAttachmentRef
|
|
14
|
+
| FileAttachmentRef
|
|
15
|
+
| Pick<RenderedPdfAttachmentRef, "kind" | "templateSlug">
|
|
16
|
+
) & {
|
|
17
|
+
readonly content: Uint8Array;
|
|
18
|
+
readonly contentType: string;
|
|
19
|
+
readonly fileName: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
1
22
|
// Transport interface — SMTP in prod, InMemory in tests
|
|
2
23
|
export type EmailMessage = {
|
|
3
24
|
readonly to: string;
|
|
@@ -19,6 +40,8 @@ export type EmailMessage = {
|
|
|
19
40
|
// Extra RFC-5322 headers — In-Reply-To / References thread a reply into the
|
|
20
41
|
// conversation it answers.
|
|
21
42
|
readonly headers?: Readonly<Record<string, string>>;
|
|
43
|
+
// Refs travel through jobs; the bytes are only read at send time.
|
|
44
|
+
readonly attachments?: readonly EmailAttachment[];
|
|
22
45
|
};
|
|
23
46
|
|
|
24
47
|
export type EmailTransport = {
|
|
@@ -20,7 +20,7 @@ export const markReadWrite = defineWriteHandler({
|
|
|
20
20
|
description:
|
|
21
21
|
"Marks one in-app message of the calling user as read by its numeric id, failing with not-found when no such message belongs to the caller; use it when a user opens a single notification.",
|
|
22
22
|
handler: async (event, ctx) => {
|
|
23
|
-
const rows = await updateMany(
|
|
23
|
+
const rows = await updateMany<{ id: number }>(
|
|
24
24
|
ctx.db,
|
|
25
25
|
inAppMessagesTable,
|
|
26
26
|
{ isRead: true, readAt: Temporal.Now.instant() },
|
|
@@ -29,6 +29,6 @@ export const markReadWrite = defineWriteHandler({
|
|
|
29
29
|
if (rows.length === 0) {
|
|
30
30
|
return writeFailure(new NotFoundError("inAppMessage", event.payload.id));
|
|
31
31
|
}
|
|
32
|
-
return { isSuccess: true, data: { id:
|
|
32
|
+
return { isSuccess: true, data: { id: rows[0]?.id } };
|
|
33
33
|
},
|
|
34
34
|
});
|
|
@@ -164,11 +164,7 @@ export const setWrite = defineWriteHandler({
|
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
166
|
// scope="system" was already rejected above — narrow accordingly.
|
|
167
|
-
const subject = resolvePiiSubject(
|
|
168
|
-
scope as Exclude<typeof scope, "system">, // @cast-boundary runtime-checked above
|
|
169
|
-
tenantId,
|
|
170
|
-
userId,
|
|
171
|
-
);
|
|
167
|
+
const subject = resolvePiiSubject(scope as Exclude<typeof scope, "system">, tenantId, userId);
|
|
172
168
|
const kmsCtx: KmsContext = {
|
|
173
169
|
requestId: requestContext.get()?.requestId ?? "config:write:set",
|
|
174
170
|
tenantId,
|
|
@@ -31,9 +31,9 @@ export async function selectFieldDefinitionsForEntity(
|
|
|
31
31
|
entityName: string,
|
|
32
32
|
tenantId: string,
|
|
33
33
|
): Promise<readonly { field_key: string; serialized_field: unknown }[]> {
|
|
34
|
-
return unsafeReadRetrying(
|
|
34
|
+
return unsafeReadRetrying<{ field_key: string; serialized_field: unknown }>(
|
|
35
35
|
db,
|
|
36
36
|
"SELECT field_key, serialized_field FROM read_custom_field_definitions WHERE entity_name = $1 AND tenant_id = $2",
|
|
37
37
|
[entityName, tenantId],
|
|
38
|
-
)
|
|
38
|
+
);
|
|
39
39
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type FieldDefinition,
|
|
4
4
|
fieldToZod,
|
|
5
5
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
6
7
|
import type * as z from "zod";
|
|
7
8
|
import { SUPPORTED_FIELD_TYPES } from "../constants.js";
|
|
8
9
|
|
|
@@ -38,9 +39,8 @@ const SUPPORTED_EMBEDDED_SUB_TYPES: ReadonlySet<string> = new Set([
|
|
|
38
39
|
const CONSTRAINT_KEYS = ["required", "maxLength", "format", "default"] as const;
|
|
39
40
|
|
|
40
41
|
export function buildCustomFieldValueSchema(parsedField: unknown): z.ZodTypeAny | null {
|
|
41
|
-
if (!parsedField
|
|
42
|
-
const
|
|
43
|
-
const rawType = obj["type"];
|
|
42
|
+
if (!isPlainObject(parsedField)) return null;
|
|
43
|
+
const rawType = parsedField["type"];
|
|
44
44
|
if (typeof rawType !== "string") return null;
|
|
45
45
|
if (!SUPPORTED_TYPES_SET.has(rawType)) return null;
|
|
46
46
|
|
|
@@ -54,35 +54,34 @@ export function buildCustomFieldValueSchema(parsedField: unknown): z.ZodTypeAny
|
|
|
54
54
|
// drops everywhere else.
|
|
55
55
|
let strippedSubSchema: Record<string, Record<string, unknown>> | undefined;
|
|
56
56
|
if (rawType === "embedded") {
|
|
57
|
-
const schema =
|
|
57
|
+
const schema = parsedField["schema"];
|
|
58
58
|
if (!schema || typeof schema !== "object") return null;
|
|
59
59
|
strippedSubSchema = {};
|
|
60
60
|
for (const [subKey, sub] of Object.entries(schema)) {
|
|
61
|
-
if (!sub
|
|
62
|
-
const
|
|
63
|
-
const subType = subObj["type"];
|
|
61
|
+
if (!isPlainObject(sub)) return null;
|
|
62
|
+
const subType = sub["type"];
|
|
64
63
|
if (typeof subType !== "string" || !SUPPORTED_EMBEDDED_SUB_TYPES.has(subType)) {
|
|
65
64
|
return null;
|
|
66
65
|
}
|
|
67
|
-
const strippedSub = { ...
|
|
66
|
+
const strippedSub = { ...sub };
|
|
68
67
|
for (const k of CONSTRAINT_KEYS) delete strippedSub[k];
|
|
69
68
|
strippedSubSchema[subKey] = strippedSub;
|
|
70
69
|
}
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
const fieldDef: Record<string, unknown> = { ...
|
|
72
|
+
const fieldDef: Record<string, unknown> = { ...parsedField };
|
|
74
73
|
for (const k of CONSTRAINT_KEYS) delete fieldDef[k];
|
|
75
74
|
if (strippedSubSchema !== undefined) fieldDef["schema"] = strippedSubSchema;
|
|
76
75
|
if (rawType === "enum") {
|
|
77
76
|
fieldDef["type"] = "select";
|
|
78
|
-
fieldDef["options"] =
|
|
77
|
+
fieldDef["options"] = parsedField["values"] ?? parsedField["options"] ?? [];
|
|
79
78
|
}
|
|
80
79
|
|
|
81
80
|
// fieldToZod's money case validates `currency` against the passed list, not
|
|
82
81
|
// a field-level key — so surface the field's own currency when it declares
|
|
83
82
|
// one, else fall back to the framework defaults.
|
|
84
83
|
const currencies =
|
|
85
|
-
typeof
|
|
84
|
+
typeof parsedField["currency"] === "string" ? [parsedField["currency"]] : DEFAULT_CURRENCIES;
|
|
86
85
|
|
|
87
86
|
// @cast-boundary serialized-field is the dehydrated r.field.X() output =
|
|
88
87
|
// a FieldDefinition; fieldToZod reads only its type-specific keys (the
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { type DbRunner, extractTableName } from "@cosmicdrift/kumiko-framework/db";
|
|
4
4
|
import type { UserDataExportSnippet, UserDataHookCtx } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
5
|
import { EXT_USER_DATA, type FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
6
7
|
import { selectCustomFieldsHostRows } from "./db/queries/user-data-rights.js";
|
|
7
8
|
|
|
8
9
|
export interface WireCustomFieldsUserDataRightsOptions {
|
|
@@ -17,14 +18,14 @@ interface CustomFieldsHostRow {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function asCustomFieldsHostRow(value: unknown): CustomFieldsHostRow | null {
|
|
20
|
-
if (!
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const cf =
|
|
21
|
+
if (!isPlainObject(value)) return null;
|
|
22
|
+
const id = value["id"];
|
|
23
|
+
if (typeof id !== "string") return null;
|
|
24
|
+
const cf = value["custom_fields"] ?? value["customFields"];
|
|
24
25
|
if (cf === undefined) return null;
|
|
25
|
-
if (cf === null) return { id
|
|
26
|
+
if (cf === null) return { id, customFields: null };
|
|
26
27
|
if (!cf || typeof cf !== "object" || Array.isArray(cf)) return null;
|
|
27
|
-
return { id
|
|
28
|
+
return { id, customFields: Object.fromEntries(Object.entries(cf)) };
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
// fw#2914 — selectCustomFieldsHostRows runs raw SQL against a dynamic host
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.363.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "data-retention exports computeCutoff",
|
|
6
|
+
"detail": "computeCutoff(keepFor, now) and its Instant type are exported from @cosmicdrift/kumiko-bundled-features/data-retention, so apps that compute a retention cutoff themselves (for example a backfill) use the same keepFor parsing as the cleanup job instead of a copy."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.335.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
fileRefEntity,
|
|
73
73
|
fileRefsTable,
|
|
74
74
|
} from "@cosmicdrift/kumiko-framework/files";
|
|
75
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
75
76
|
import { runInSubTransaction } from "../shared/index.js";
|
|
76
77
|
import { computeCutoff, type Instant } from "./keep-for.js";
|
|
77
78
|
import type { RetentionPresetKey } from "./presets.js";
|
|
@@ -347,10 +348,9 @@ function collectSingleFileHolders(registry: Registry): readonly SingleFileHolder
|
|
|
347
348
|
if (!entity) continue;
|
|
348
349
|
const { singleFields } = planFileFields(entity.fields);
|
|
349
350
|
if (singleFields.length === 0) continue;
|
|
350
|
-
const table = proj.table as Record<string, unknown>; // @cast-boundary column-presence probe
|
|
351
351
|
holders.push({
|
|
352
352
|
table: proj.table,
|
|
353
|
-
tableHasTenantId: table
|
|
353
|
+
tableHasTenantId: propertyOf(proj.table, "tenantId") !== undefined,
|
|
354
354
|
singleFields,
|
|
355
355
|
});
|
|
356
356
|
}
|
|
@@ -699,11 +699,10 @@ export async function runRetentionCleanup(
|
|
|
699
699
|
const entity = registry.getEntity(entityName);
|
|
700
700
|
if (!entity) continue;
|
|
701
701
|
|
|
702
|
-
const table = proj.table as Record<string, unknown>; // @cast-boundary column-presence probe
|
|
703
702
|
const declaredReference = policy.reference ?? DEFAULT_REFERENCE_FIELD;
|
|
704
703
|
const referenceField = FRAMEWORK_REFERENCE_ALIAS[declaredReference] ?? declaredReference;
|
|
705
704
|
|
|
706
|
-
if (table
|
|
705
|
+
if (propertyOf(proj.table, referenceField) === undefined) {
|
|
707
706
|
skipped.push({ entityName, reason: "missing_reference_column" });
|
|
708
707
|
continue;
|
|
709
708
|
}
|
|
@@ -713,7 +712,7 @@ export async function runRetentionCleanup(
|
|
|
713
712
|
// Tenant-Scope nur wenn die Tabelle eine tenantId-Spalte hat — identisch zu
|
|
714
713
|
// soft-delete-cleanup. Ohne diesen Filter wuerde ein Tenant die Rows eines
|
|
715
714
|
// anderen treffen.
|
|
716
|
-
if (table
|
|
715
|
+
if (propertyOf(proj.table, "tenantId") !== undefined) {
|
|
717
716
|
where["tenantId"] = tenantId;
|
|
718
717
|
}
|
|
719
718
|
|
|
@@ -762,7 +761,10 @@ export async function runRetentionCleanup(
|
|
|
762
761
|
break;
|
|
763
762
|
}
|
|
764
763
|
case "softDelete": {
|
|
765
|
-
if (
|
|
764
|
+
if (
|
|
765
|
+
propertyOf(proj.table, "isDeleted") === undefined ||
|
|
766
|
+
propertyOf(proj.table, "deletedAt") === undefined
|
|
767
|
+
) {
|
|
766
768
|
skipped.push({ entityName, reason: "missing_softdelete_columns" });
|
|
767
769
|
break;
|
|
768
770
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import type {
|
|
3
|
+
FileAttachmentRef,
|
|
4
|
+
InlineAttachmentRef,
|
|
5
|
+
RenderedPdfAttachmentRef,
|
|
6
|
+
} from "../renderer-foundation/index.js";
|
|
7
|
+
import { attachmentFileNameSchema } from "../shared/index.js";
|
|
8
|
+
import { isDocumentResourceKey, templateSlugSchema } from "../template-resolver/index.js";
|
|
9
|
+
|
|
10
|
+
export type { FileAttachmentRef, InlineAttachmentRef, RenderedPdfAttachmentRef };
|
|
11
|
+
export type EmailAttachmentRef = InlineAttachmentRef | FileAttachmentRef | RenderedPdfAttachmentRef;
|
|
12
|
+
|
|
13
|
+
export const MAX_EMAIL_ATTACHMENTS = 20;
|
|
14
|
+
|
|
15
|
+
export const MAX_RENDERED_PDF_VARIABLES_BYTES = 64_000;
|
|
16
|
+
|
|
17
|
+
const MAX_ID_LENGTH = 64;
|
|
18
|
+
|
|
19
|
+
const fileRefIdSchema = z.string().min(1).max(MAX_ID_LENGTH);
|
|
20
|
+
|
|
21
|
+
export const inlineAttachmentRefSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
kind: z.literal("inline"),
|
|
24
|
+
cid: z.string().min(1).max(MAX_ID_LENGTH).refine(isDocumentResourceKey),
|
|
25
|
+
fileRefId: fileRefIdSchema,
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
|
|
29
|
+
export const fileAttachmentRefSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
kind: z.literal("file"),
|
|
32
|
+
fileRefId: fileRefIdSchema,
|
|
33
|
+
fileName: attachmentFileNameSchema,
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
36
|
+
|
|
37
|
+
export const renderedPdfAttachmentRefSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
kind: z.literal("rendered-pdf"),
|
|
40
|
+
templateSlug: templateSlugSchema,
|
|
41
|
+
variables: z
|
|
42
|
+
.record(z.string(), z.unknown())
|
|
43
|
+
.refine(
|
|
44
|
+
(value) => Buffer.byteLength(JSON.stringify(value)) <= MAX_RENDERED_PDF_VARIABLES_BYTES,
|
|
45
|
+
{
|
|
46
|
+
message: `variables must not exceed ${MAX_RENDERED_PDF_VARIABLES_BYTES} bytes as JSON`,
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
fileName: attachmentFileNameSchema.refine((name) => name.toLowerCase().endsWith(".pdf"), {
|
|
50
|
+
message: "file name must end with .pdf",
|
|
51
|
+
}),
|
|
52
|
+
locale: z
|
|
53
|
+
.string()
|
|
54
|
+
.min(2)
|
|
55
|
+
.max(8)
|
|
56
|
+
.regex(/^[a-z]{2}(-[a-z]{2})?$/i, "locale must be ISO 639-1 (e.g. de, en, en-us)")
|
|
57
|
+
.optional(),
|
|
58
|
+
})
|
|
59
|
+
.strict();
|
|
60
|
+
|
|
61
|
+
export const emailAttachmentRefSchema = z.discriminatedUnion("kind", [
|
|
62
|
+
inlineAttachmentRefSchema,
|
|
63
|
+
fileAttachmentRefSchema,
|
|
64
|
+
renderedPdfAttachmentRefSchema,
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
// Rebuilds only the contract fields so bytes a caller attached to a ref object
|
|
68
|
+
// can never ride into a job payload.
|
|
69
|
+
export function toAttachmentRef(ref: EmailAttachmentRef): EmailAttachmentRef {
|
|
70
|
+
switch (ref.kind) {
|
|
71
|
+
case "inline":
|
|
72
|
+
return { kind: "inline", cid: ref.cid, fileRefId: ref.fileRefId };
|
|
73
|
+
case "file":
|
|
74
|
+
return { kind: "file", fileRefId: ref.fileRefId, fileName: ref.fileName };
|
|
75
|
+
case "rendered-pdf":
|
|
76
|
+
return {
|
|
77
|
+
kind: "rendered-pdf",
|
|
78
|
+
templateSlug: ref.templateSlug,
|
|
79
|
+
variables: ref.variables,
|
|
80
|
+
fileName: ref.fileName,
|
|
81
|
+
...(ref.locale !== undefined && { locale: ref.locale }),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|