@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
|
@@ -16,6 +16,12 @@ import type {
|
|
|
16
16
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
17
17
|
import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
|
|
18
18
|
import * as z from "zod";
|
|
19
|
+
import { fileContextForTenant } from "../file-foundation/index.js";
|
|
20
|
+
import {
|
|
21
|
+
emailAttachmentRefSchema,
|
|
22
|
+
MAX_EMAIL_ATTACHMENTS,
|
|
23
|
+
toAttachmentRef,
|
|
24
|
+
} from "./attachment-ref.js";
|
|
19
25
|
import { appendAttemptEvent } from "./attempt-log.js";
|
|
20
26
|
import { buildChannelContext } from "./channel-context.js";
|
|
21
27
|
import { DeliveryJobs, deliveryIdempotencyRedisKey, deliveryPriorityRank } from "./constants.js";
|
|
@@ -49,7 +55,12 @@ const renderJobPayloadSchema = z.object({
|
|
|
49
55
|
|
|
50
56
|
const sendJobPayloadSchema = renderJobPayloadSchema.extend({
|
|
51
57
|
rendered: z
|
|
52
|
-
.object({
|
|
58
|
+
.object({
|
|
59
|
+
html: z.string(),
|
|
60
|
+
subject: z.string(),
|
|
61
|
+
text: z.string().optional(),
|
|
62
|
+
attachments: z.array(emailAttachmentRefSchema).max(MAX_EMAIL_ATTACHMENTS).optional(),
|
|
63
|
+
})
|
|
53
64
|
.optional(),
|
|
54
65
|
});
|
|
55
66
|
|
|
@@ -63,18 +74,34 @@ function requireTenantScopedDeps(
|
|
|
63
74
|
if (!registry) throw new Error("delivery job: missing registry in job context");
|
|
64
75
|
// delivery is r.systemScope()'d, so JobContext.systemDb is guaranteed; unsafeRaw
|
|
65
76
|
// hands the underlying DbConnection to both — safe since they filter by tenantId themselves.
|
|
77
|
+
// Attachment lookups read only this tenant's FileRefs, plus the system tenant's for inline images.
|
|
66
78
|
if (!ctx.systemDb) {
|
|
67
79
|
throw new InternalError({
|
|
68
80
|
message: "delivery job: ctx.systemDb missing on a system-scoped job",
|
|
69
81
|
});
|
|
70
82
|
}
|
|
83
|
+
// Also makes ctx.config (built for the job's tenant) the accessor of the payload tenant;
|
|
84
|
+
// job contexts carry no configFor.
|
|
71
85
|
ctx.systemDb.assertTenantMatch(tenantId);
|
|
72
86
|
const db = ctx.systemDb.unsafeRaw(
|
|
73
|
-
"
|
|
87
|
+
"attempt append and channel context filter by this job's tenantId; attachment lookup reads only the job tenant's FileRefs plus, for inline images, the system tenant's",
|
|
74
88
|
) as DbConnection; // @cast-boundary db-operator — DbRunner narrows to DbConnection, jobs never run inside a DbTx
|
|
75
89
|
return { db, registry };
|
|
76
90
|
}
|
|
77
91
|
|
|
92
|
+
function toSendRendered(
|
|
93
|
+
rendered: RenderedMessage,
|
|
94
|
+
): z.infer<typeof sendJobPayloadSchema>["rendered"] {
|
|
95
|
+
return {
|
|
96
|
+
html: rendered.html,
|
|
97
|
+
subject: rendered.subject,
|
|
98
|
+
...(rendered.text !== undefined && { text: rendered.text }),
|
|
99
|
+
...(rendered.attachments !== undefined && {
|
|
100
|
+
attachments: rendered.attachments.map(toAttachmentRef),
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
function resolveChannel(registry: Registry, name: string): DeliveryChannel {
|
|
79
106
|
const channel = collectChannels(registry).find((c) => c.name === name);
|
|
80
107
|
if (!channel) throw new Error(`delivery job: unknown channel "${name}"`);
|
|
@@ -169,7 +196,15 @@ export const deliveryRenderJob: JobHandlerFn = async (payload, ctx) => {
|
|
|
169
196
|
const tenantId = p.tenantId as TenantId; // @cast-boundary engine-payload — stream tenant
|
|
170
197
|
const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
|
|
171
198
|
const channel = resolveChannel(registry, p.channelName);
|
|
172
|
-
const channelCtx = buildChannelContext(
|
|
199
|
+
const channelCtx = buildChannelContext(
|
|
200
|
+
db,
|
|
201
|
+
registry,
|
|
202
|
+
undefined,
|
|
203
|
+
tenantId,
|
|
204
|
+
ctx.secrets,
|
|
205
|
+
ctx.config,
|
|
206
|
+
(fileTenantId) => fileContextForTenant(ctx, fileTenantId),
|
|
207
|
+
);
|
|
173
208
|
|
|
174
209
|
let rendered: RenderedMessage;
|
|
175
210
|
try {
|
|
@@ -184,7 +219,7 @@ export const deliveryRenderJob: JobHandlerFn = async (payload, ctx) => {
|
|
|
184
219
|
const jobRunner = ctx["jobRunner"] as JobRunner; // @cast-boundary dynamic-key — dispatch lives on the concrete runner
|
|
185
220
|
await jobRunner.dispatch(
|
|
186
221
|
DeliveryJobs.send,
|
|
187
|
-
{ ...p, rendered },
|
|
222
|
+
{ ...p, rendered: toSendRendered(rendered) },
|
|
188
223
|
{ priority: deliveryPriorityRank[p.priority] },
|
|
189
224
|
);
|
|
190
225
|
} catch (err) {
|
|
@@ -200,7 +235,15 @@ export const deliverySendJob: JobHandlerFn = async (payload, ctx) => {
|
|
|
200
235
|
const tenantId = p.tenantId as TenantId; // @cast-boundary engine-payload — stream tenant
|
|
201
236
|
const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
|
|
202
237
|
const channel = resolveChannel(registry, p.channelName);
|
|
203
|
-
const channelCtx = buildChannelContext(
|
|
238
|
+
const channelCtx = buildChannelContext(
|
|
239
|
+
db,
|
|
240
|
+
registry,
|
|
241
|
+
undefined,
|
|
242
|
+
tenantId,
|
|
243
|
+
ctx.secrets,
|
|
244
|
+
ctx.config,
|
|
245
|
+
(fileTenantId) => fileContextForTenant(ctx, fileTenantId),
|
|
246
|
+
);
|
|
204
247
|
|
|
205
248
|
try {
|
|
206
249
|
const result = await channel.send(p.address, toMessage(p), channelCtx, p.rendered);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NotifyFactory } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import type { NotifyFactory, NotifyFn, NotifyOptions } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import type { DeliveryService } from "./types.js";
|
|
3
3
|
|
|
4
4
|
// The same binding runProdApp wires into the app context: the calling context's
|
|
@@ -8,12 +8,17 @@ export function createDeliveryNotifyFactory(
|
|
|
8
8
|
deliveryService: Pick<DeliveryService, "notify">,
|
|
9
9
|
options: { readonly deliverQueuedInline?: boolean } = {},
|
|
10
10
|
): NotifyFactory {
|
|
11
|
-
return (user, tenantId, jobDispatcher
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
return (user, tenantId, jobDispatcher, deliveryDb, fileProviderResolver) => {
|
|
12
|
+
const notify: NotifyFn = (notificationType: string, notifyOptions: NotifyOptions) =>
|
|
13
|
+
deliveryService.notify(
|
|
14
|
+
notificationType,
|
|
15
|
+
notifyOptions,
|
|
16
|
+
user,
|
|
17
|
+
tenantId,
|
|
18
|
+
options.deliverQueuedInline === true ? undefined : jobDispatcher,
|
|
19
|
+
deliveryDb,
|
|
20
|
+
fileProviderResolver,
|
|
21
|
+
);
|
|
22
|
+
return notify;
|
|
23
|
+
};
|
|
19
24
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ConfigAccessor,
|
|
5
|
+
EscapeHatchAuditSink,
|
|
6
|
+
Registry,
|
|
7
|
+
TenantId,
|
|
8
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
+
import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
|
|
4
10
|
import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
|
|
5
11
|
import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
6
12
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
@@ -19,6 +25,8 @@ export type CreateDeliveryTestContextOptions = {
|
|
|
19
25
|
readonly jobRunner?: JobRunner;
|
|
20
26
|
readonly secrets?: SecretsContext;
|
|
21
27
|
readonly idempotencyRedis?: Redis;
|
|
28
|
+
readonly filesForTenant?: (tenantId: string) => FileContext | undefined;
|
|
29
|
+
readonly tenantConfigFor?: (tenantId: TenantId) => ConfigAccessor;
|
|
22
30
|
};
|
|
23
31
|
|
|
24
32
|
/**
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
-
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type {
|
|
4
4
|
DeliveryErrorCode,
|
|
5
5
|
NotifyDeliveryStatus,
|
|
6
|
+
NotifyDispatched,
|
|
6
7
|
NotifyJobDispatcher,
|
|
7
8
|
NotifyOptions,
|
|
8
9
|
NotifyPriority,
|
|
9
|
-
NotifyResult,
|
|
10
10
|
Registry,
|
|
11
11
|
SessionUser,
|
|
12
12
|
TenantId,
|
|
13
13
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
14
|
+
import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
|
|
14
15
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
16
|
+
import type { MailPayload } from "../renderer-foundation/index.js";
|
|
17
|
+
import type { EmailAttachmentRef } from "./attachment-ref.js";
|
|
15
18
|
import { DELIVERY_CHANNEL_EXTENSION } from "./constants.js";
|
|
16
19
|
|
|
17
20
|
// --- Channel Interface ---
|
|
18
21
|
|
|
22
|
+
export type RenderedMail = {
|
|
23
|
+
readonly subject: string;
|
|
24
|
+
readonly html: string;
|
|
25
|
+
readonly text: string;
|
|
26
|
+
readonly previewText?: string;
|
|
27
|
+
readonly attachments?: readonly EmailAttachmentRef[];
|
|
28
|
+
};
|
|
29
|
+
|
|
19
30
|
export type ChannelContext = {
|
|
20
31
|
readonly db: TenantDb;
|
|
21
32
|
readonly registry: Registry;
|
|
@@ -24,8 +35,19 @@ export type ChannelContext = {
|
|
|
24
35
|
// Chat channels read their per-tenant webhook URL / bot token from it.
|
|
25
36
|
// Absent when no secrets feature/KEK is wired.
|
|
26
37
|
readonly secrets?: SecretsContext | undefined;
|
|
38
|
+
// Renders a stored mail template, the tenant's own or the system fallback.
|
|
39
|
+
readonly renderMail?: (payload: MailPayload) => Promise<RenderedMail>;
|
|
40
|
+
// Reads the bytes behind an attachment ref at send time. Absent when no file storage is wired.
|
|
41
|
+
readonly readAttachment?: (
|
|
42
|
+
ref: EmailAttachmentRef,
|
|
43
|
+
origin?: AttachmentOrigin,
|
|
44
|
+
) => Promise<{ readonly content: Uint8Array; readonly contentType: string } | null>;
|
|
27
45
|
};
|
|
28
46
|
|
|
47
|
+
// "template": the renderer returned the ref from the stored template (own or system). "message":
|
|
48
|
+
// the ref came from notify data, so a caller chose it. Unset means "message".
|
|
49
|
+
export type AttachmentOrigin = "template" | "message";
|
|
50
|
+
|
|
29
51
|
export type ChannelMessage = {
|
|
30
52
|
readonly notificationType: string;
|
|
31
53
|
readonly title: string;
|
|
@@ -49,6 +71,7 @@ export type RenderedMessage = {
|
|
|
49
71
|
readonly html: string;
|
|
50
72
|
readonly subject: string;
|
|
51
73
|
readonly text?: string;
|
|
74
|
+
readonly attachments?: readonly EmailAttachmentRef[];
|
|
52
75
|
};
|
|
53
76
|
|
|
54
77
|
// `mode` decides how the delivery-service dispatches a channel:
|
|
@@ -76,21 +99,6 @@ export type DeliveryChannel = {
|
|
|
76
99
|
): Promise<ChannelResult>;
|
|
77
100
|
};
|
|
78
101
|
|
|
79
|
-
// --- Notification Renderer ---
|
|
80
|
-
|
|
81
|
-
export type RendererInput = {
|
|
82
|
-
readonly template: string;
|
|
83
|
-
readonly variables: Readonly<Record<string, unknown>>;
|
|
84
|
-
readonly locale?: string | undefined;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
export type NotificationRenderer = {
|
|
88
|
-
readonly name: string;
|
|
89
|
-
render(input: RendererInput): Promise<string>;
|
|
90
|
-
/** Plain-text version of the same mail; the email channel sends it as the text part of a multipart/alternative mail. */
|
|
91
|
-
renderText?(input: RendererInput): Promise<string>;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
102
|
// --- Delivery Log Entry ---
|
|
95
103
|
|
|
96
104
|
export type DeliveryLogEntry = {
|
|
@@ -114,7 +122,9 @@ export type DeliveryService = {
|
|
|
114
122
|
user: SessionUser,
|
|
115
123
|
tenantId: TenantId,
|
|
116
124
|
jobDispatcher?: NotifyJobDispatcher,
|
|
117
|
-
|
|
125
|
+
deliveryDb?: DbConnection,
|
|
126
|
+
fileProviderResolver?: FileProviderResolver,
|
|
127
|
+
): Promise<NotifyDispatched>;
|
|
118
128
|
};
|
|
119
129
|
|
|
120
130
|
// r.useExtension options-shape: `name` is NOT part of the registration
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@cosmicdrift/kumiko-framework/api";
|
|
8
8
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
9
|
import { escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
10
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
10
11
|
import * as jose from "jose";
|
|
11
12
|
import * as z from "zod";
|
|
12
13
|
import { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
@@ -412,8 +413,10 @@ function tokenFromRequestBody(request: SignatureExtraRouteVerifyRequest): string
|
|
|
412
413
|
}
|
|
413
414
|
if (contentType?.includes("application/json")) {
|
|
414
415
|
try {
|
|
415
|
-
const parsed = JSON.parse(request.rawBody)
|
|
416
|
-
return typeof parsed
|
|
416
|
+
const parsed: unknown = JSON.parse(request.rawBody);
|
|
417
|
+
return isPlainObject(parsed) && typeof parsed["token"] === "string"
|
|
418
|
+
? parsed["token"]
|
|
419
|
+
: undefined;
|
|
417
420
|
} catch {
|
|
418
421
|
return undefined;
|
|
419
422
|
}
|
|
@@ -13,13 +13,10 @@ import type {
|
|
|
13
13
|
import QRCode from "qrcode";
|
|
14
14
|
import type { OverlayOptions, ResizeOptions, Sharp } from "sharp";
|
|
15
15
|
import sharp from "sharp";
|
|
16
|
+
import { assertNotSvg, SHARP_INPUT_OPTIONS } from "../shared/index.js";
|
|
16
17
|
|
|
17
18
|
const MIN_SHARP_SIGMA = 0.3;
|
|
18
19
|
const MAX_SHARP_SIGMA = 1000;
|
|
19
|
-
// Above this, a decoded pixel buffer runs into hundreds of MB before any
|
|
20
|
-
// resize even happens — sharp's own default (~268MP) is still large enough
|
|
21
|
-
// to be an OOM primitive for a single request.
|
|
22
|
-
const MAX_INPUT_PIXELS = 100_000_000;
|
|
23
20
|
// Longest output edge a caller may request via `size`/`maxEdge`. Without a
|
|
24
21
|
// cap, `{ maxEdge: 50000 }` allocates a multi-gigabyte buffer per encoder
|
|
25
22
|
// step before any client sees a byte back.
|
|
@@ -38,12 +35,6 @@ const MAX_QR_DATA_LENGTH = 1024;
|
|
|
38
35
|
// camera capture — the AC claims phone-scannable, so an under-sized layer
|
|
39
36
|
// throws instead of caching a silently unusable image.
|
|
40
37
|
const MIN_QR_PIXEL_WIDTH = 160;
|
|
41
|
-
// sequentialRead trades random pixel access for lower peak memory on
|
|
42
|
-
// streamed formats — applied at every decode entry point below since none
|
|
43
|
-
// of them need non-sequential access to the *original* bytes (extract runs
|
|
44
|
-
// against a single already-buffered region per call).
|
|
45
|
-
const SHARP_INPUT_OPTIONS = { limitInputPixels: MAX_INPUT_PIXELS, sequentialRead: true } as const;
|
|
46
|
-
|
|
47
38
|
function normalizeMimeType(mimeType: string): string {
|
|
48
39
|
return mimeType.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
49
40
|
}
|
|
@@ -52,18 +43,6 @@ function clampSigma(sigma: number): number {
|
|
|
52
43
|
return Math.min(MAX_SHARP_SIGMA, Math.max(MIN_SHARP_SIGMA, sigma));
|
|
53
44
|
}
|
|
54
45
|
|
|
55
|
-
// Shared trust boundary for every image byte source this renderer decodes —
|
|
56
|
-
// the original upload (renderImage) and an overlay layer's bytes (applyOverlays)
|
|
57
|
-
// alike: a base64 string in a field declaration isn't more trustworthy than
|
|
58
|
-
// an upload just because it came from app code.
|
|
59
|
-
function assertNotSvg(format: string | undefined): void {
|
|
60
|
-
if (format === "svg") {
|
|
61
|
-
throw new Error(
|
|
62
|
-
"derivatives-sharp: SVG is not supported — rendering untrusted SVG through libvips is a trust boundary this renderer doesn't need to cross.",
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
46
|
function assertValidRegion(region: BlurRegion): void {
|
|
68
47
|
const values = [region.x, region.y, region.width, region.height];
|
|
69
48
|
if (values.some((v) => !Number.isFinite(v)) || region.width <= 0 || region.height <= 0) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// the codec here keeps liteparse + recipe consumers lockstep with the
|
|
5
5
|
// entity shape (kumiko-framework#1549/1).
|
|
6
6
|
|
|
7
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
7
8
|
import type { IngestPage } from "./entity.js";
|
|
8
9
|
|
|
9
10
|
export function writeIngestPages(pages: readonly IngestPage[]): string {
|
|
@@ -28,7 +29,9 @@ function parseJson(raw: string): unknown {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function isIngestPage(value: unknown): value is IngestPage {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
return (
|
|
33
|
+
isPlainObject(value) &&
|
|
34
|
+
typeof value["pageNumber"] === "number" &&
|
|
35
|
+
typeof value["text"] === "string"
|
|
36
|
+
);
|
|
34
37
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type RedisClientOptions,
|
|
6
6
|
redisChannelPrefixFromEnv,
|
|
7
7
|
} from "@cosmicdrift/kumiko-framework/redis";
|
|
8
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
8
9
|
import type { ToggleSyncSignal } from "./toggle-runtime.js";
|
|
9
10
|
|
|
10
11
|
// Single fixed channel — unlike the SSE broker there's no per-tenant/
|
|
@@ -20,10 +21,9 @@ export type RedisToggleSyncSignal = ToggleSyncSignal & {
|
|
|
20
21
|
|
|
21
22
|
function isTogglePayload(value: unknown): value is { featureName: string; enabled: boolean } {
|
|
22
23
|
return (
|
|
23
|
-
|
|
24
|
-
value
|
|
25
|
-
typeof
|
|
26
|
-
typeof (value as { enabled?: unknown }).enabled === "boolean"
|
|
24
|
+
isPlainObject(value) &&
|
|
25
|
+
typeof value["featureName"] === "string" &&
|
|
26
|
+
typeof value["enabled"] === "boolean"
|
|
27
27
|
);
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import { isUuid, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import {
|
|
5
|
+
createFileContext,
|
|
6
|
+
type FileContext,
|
|
7
|
+
type FileProviderResolver,
|
|
8
|
+
fileRefsTable,
|
|
9
|
+
} from "@cosmicdrift/kumiko-framework/files";
|
|
10
|
+
|
|
11
|
+
export type FileRefContent = {
|
|
12
|
+
readonly content: Uint8Array;
|
|
13
|
+
readonly contentType: string;
|
|
14
|
+
readonly tenantId: string;
|
|
15
|
+
readonly size: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type FileRefRow = {
|
|
19
|
+
readonly storageKey: string;
|
|
20
|
+
readonly mimeType: string;
|
|
21
|
+
readonly tenantId: string;
|
|
22
|
+
readonly size: number | bigint | string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Tenant-bound on purpose: the FileContext of the current request only reaches its own key space.
|
|
26
|
+
export function fileContextForTenant(
|
|
27
|
+
ctx: {
|
|
28
|
+
readonly _fileProviderResolver?: FileProviderResolver | undefined;
|
|
29
|
+
readonly files?: FileContext | undefined;
|
|
30
|
+
},
|
|
31
|
+
tenantId: string,
|
|
32
|
+
): FileContext | undefined {
|
|
33
|
+
const resolver = ctx._fileProviderResolver;
|
|
34
|
+
if (resolver === undefined) return ctx.files;
|
|
35
|
+
return fileContextFromResolver(resolver, tenantId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function fileContextFromResolver(
|
|
39
|
+
resolver: FileProviderResolver,
|
|
40
|
+
tenantId: string,
|
|
41
|
+
): FileContext {
|
|
42
|
+
// @cast-boundary engine-payload — tenantId comes from a trusted job payload or session
|
|
43
|
+
const brandedTenantId = tenantId as TenantId;
|
|
44
|
+
return createFileContext(() => resolver(brandedTenantId), tenantId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class FileRefTooLargeError extends Error {
|
|
48
|
+
constructor(readonly size: number) {
|
|
49
|
+
super(`file ref exceeds the size limit (${size} bytes)`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// fileName of the row is never used: it is personal data and may be a PII ciphertext.
|
|
54
|
+
export async function readFileRefContent(args: {
|
|
55
|
+
readonly db: DbRunner | TenantDb;
|
|
56
|
+
readonly filesForTenant: (tenantId: string) => FileContext | undefined;
|
|
57
|
+
readonly fileRefId: string;
|
|
58
|
+
readonly allowedTenantIds: readonly string[];
|
|
59
|
+
/** Only accept this user's own upload that is not bound to any entity yet. */
|
|
60
|
+
readonly unclaimedUploadOf?: string;
|
|
61
|
+
/** Rejects before any bytes are read when the stored size exceeds the limit. */
|
|
62
|
+
readonly maxBytes?: number;
|
|
63
|
+
}): Promise<FileRefContent | null> {
|
|
64
|
+
if (!isUuid(args.fileRefId)) return null;
|
|
65
|
+
const [row] = await selectMany<FileRefRow>(args.db, fileRefsTable, {
|
|
66
|
+
id: args.fileRefId,
|
|
67
|
+
tenantId: { in: args.allowedTenantIds },
|
|
68
|
+
isDeleted: false,
|
|
69
|
+
...(args.unclaimedUploadOf !== undefined && {
|
|
70
|
+
insertedById: args.unclaimedUploadOf,
|
|
71
|
+
entityType: null,
|
|
72
|
+
entityId: null,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
if (row === undefined) return null;
|
|
76
|
+
const size = Number(row.size);
|
|
77
|
+
if (args.maxBytes !== undefined && size > args.maxBytes) {
|
|
78
|
+
throw new FileRefTooLargeError(size);
|
|
79
|
+
}
|
|
80
|
+
const files = args.filesForTenant(row.tenantId);
|
|
81
|
+
if (files === undefined) {
|
|
82
|
+
throw new Error("readFileRefContent: no file storage is available for the file's tenant");
|
|
83
|
+
}
|
|
84
|
+
const content = await files.ref(row.storageKey).read();
|
|
85
|
+
return { content, contentType: row.mimeType, tenantId: row.tenantId, size };
|
|
86
|
+
}
|
|
@@ -74,7 +74,10 @@ const loadCollapsed = (): ReadonlySet<string> => {
|
|
|
74
74
|
if (typeof window === "undefined") return new Set();
|
|
75
75
|
try {
|
|
76
76
|
const raw = window.localStorage.getItem(COLLAPSED_KEY);
|
|
77
|
-
|
|
77
|
+
const parsed: unknown = raw ? JSON.parse(raw) : [];
|
|
78
|
+
return new Set(
|
|
79
|
+
Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === "string") : [],
|
|
80
|
+
);
|
|
78
81
|
} catch {
|
|
79
82
|
return new Set();
|
|
80
83
|
}
|
|
@@ -273,7 +276,7 @@ export function FolderManager({
|
|
|
273
276
|
setDragOverKey(key);
|
|
274
277
|
},
|
|
275
278
|
onDragLeave: (e: DragEvent<HTMLDivElement>) => {
|
|
276
|
-
if (!e.currentTarget.contains(e.relatedTarget
|
|
279
|
+
if (!(e.relatedTarget instanceof Node && e.currentTarget.contains(e.relatedTarget)))
|
|
277
280
|
setDragOverKey((cur) => (cur === key ? null : cur));
|
|
278
281
|
},
|
|
279
282
|
onDrop: (e: DragEvent<HTMLDivElement>) => {
|
|
@@ -29,21 +29,21 @@ export async function selectStaleDraftsBatch(
|
|
|
29
29
|
olderThanDays: number,
|
|
30
30
|
batchSize: number,
|
|
31
31
|
): Promise<readonly StaleDraftRow[]> {
|
|
32
|
-
const rows =
|
|
33
|
-
db,
|
|
34
|
-
`SELECT "id", "tenant_id", "owner_id", "draft_key", "draft", "inserted_at" FROM "read_form_drafts"
|
|
35
|
-
WHERE COALESCE("modified_at", "inserted_at") < now() - ($1::int * interval '1 day')
|
|
36
|
-
ORDER BY COALESCE("modified_at", "inserted_at") ASC
|
|
37
|
-
LIMIT $2`,
|
|
38
|
-
[olderThanDays, batchSize],
|
|
39
|
-
)) as readonly {
|
|
32
|
+
const rows = await unsafeReadRetrying<{
|
|
40
33
|
id: string;
|
|
41
34
|
tenant_id: string;
|
|
42
35
|
owner_id: string;
|
|
43
36
|
draft_key: string;
|
|
44
37
|
draft: FormDraftBlob;
|
|
45
38
|
inserted_at: Date;
|
|
46
|
-
}
|
|
39
|
+
}>(
|
|
40
|
+
db,
|
|
41
|
+
`SELECT "id", "tenant_id", "owner_id", "draft_key", "draft", "inserted_at" FROM "read_form_drafts"
|
|
42
|
+
WHERE COALESCE("modified_at", "inserted_at") < now() - ($1::int * interval '1 day')
|
|
43
|
+
ORDER BY COALESCE("modified_at", "inserted_at") ASC
|
|
44
|
+
LIMIT $2`,
|
|
45
|
+
[olderThanDays, batchSize],
|
|
46
|
+
);
|
|
47
47
|
return rows.map((row) => ({
|
|
48
48
|
id: row.id,
|
|
49
49
|
tenantId: row.tenant_id as TenantId, // @cast-boundary db-row
|
|
@@ -61,7 +61,7 @@ export async function isDraftStillStale(
|
|
|
61
61
|
id: string,
|
|
62
62
|
olderThanDays: number,
|
|
63
63
|
): Promise<boolean> {
|
|
64
|
-
const rows =
|
|
64
|
+
const rows = await unsafeReadRetrying<unknown>(
|
|
65
65
|
db,
|
|
66
66
|
`SELECT 1 FROM "read_form_drafts"
|
|
67
67
|
WHERE "id" = $1
|
|
@@ -69,6 +69,6 @@ export async function isDraftStillStale(
|
|
|
69
69
|
AND COALESCE("modified_at", "inserted_at") < now() - ($3::int * interval '1 day')
|
|
70
70
|
LIMIT 1`,
|
|
71
71
|
[id, tenantId, olderThanDays],
|
|
72
|
-
)
|
|
72
|
+
);
|
|
73
73
|
return rows.length > 0;
|
|
74
74
|
}
|
|
@@ -11,11 +11,11 @@ export async function countDraftsByOwner(
|
|
|
11
11
|
tenantId: TenantId,
|
|
12
12
|
ownerId: string,
|
|
13
13
|
): Promise<number> {
|
|
14
|
-
const rows =
|
|
14
|
+
const rows = await unsafeReadRetrying<{ count: number }>(
|
|
15
15
|
db,
|
|
16
16
|
`SELECT count(*)::int AS "count" FROM "read_form_drafts"
|
|
17
17
|
WHERE "tenant_id" = $1 AND "owner_id" = $2`,
|
|
18
18
|
[tenantId, ownerId],
|
|
19
|
-
)
|
|
19
|
+
);
|
|
20
20
|
return rows[0]?.count ?? 0;
|
|
21
21
|
}
|
|
@@ -62,13 +62,13 @@ export async function filterOwnedFileRefs(
|
|
|
62
62
|
isCreateMode: boolean,
|
|
63
63
|
): Promise<readonly OwnedFileRef[]> {
|
|
64
64
|
if (candidateKeys.length === 0) return [];
|
|
65
|
-
const rows =
|
|
65
|
+
const rows = await unsafeReadRetrying<{ id: string; storage_key: string }>(
|
|
66
66
|
db,
|
|
67
67
|
`SELECT "id", "storage_key" FROM "file_refs"
|
|
68
68
|
WHERE "tenant_id" = $1 AND "inserted_by_id" = $2
|
|
69
69
|
AND "storage_key" = ANY($3::text[]) AND "is_deleted" = false
|
|
70
70
|
AND ($5::boolean OR "inserted_at" > $4::timestamptz)`,
|
|
71
71
|
[tenantId, ownerId, candidateKeys, draftInsertedAt.toString(), isCreateMode],
|
|
72
|
-
)
|
|
72
|
+
);
|
|
73
73
|
return rows.map((row) => ({ id: row.id, storageKey: row.storage_key }));
|
|
74
74
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
2
|
import type { FormDraftBlob } from "./schemas.js";
|
|
2
3
|
|
|
3
4
|
// The draft blob's `values` is free-form by design (issue #1889: form-draft
|
|
@@ -8,8 +9,8 @@ import type { FormDraftBlob } from "./schemas.js";
|
|
|
8
9
|
// step writes into the draft, whether under a single "file" field or nested
|
|
9
10
|
// in a "files"/"images" array.
|
|
10
11
|
function isFileRefPointer(value: unknown): value is { readonly storageKey: string } {
|
|
11
|
-
if (
|
|
12
|
-
const storageKey =
|
|
12
|
+
if (!isPlainObject(value)) return false;
|
|
13
|
+
const storageKey = value["storageKey"];
|
|
13
14
|
return typeof storageKey === "string" && storageKey.length > 0;
|
|
14
15
|
}
|
|
15
16
|
|
package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts
CHANGED
|
@@ -95,7 +95,7 @@ export const connectAccountHandler: WriteHandlerDef = {
|
|
|
95
95
|
authMethod: payload.authMethod,
|
|
96
96
|
ownerUserId,
|
|
97
97
|
displayName: payload.displayName,
|
|
98
|
-
address: encryptedFields
|
|
98
|
+
address: encryptedFields.address,
|
|
99
99
|
// active ab Connect — der Supervisor greift den Account beim
|
|
100
100
|
// nächsten Tick auf und degradiert ihn bei Credential-Fehlern.
|
|
101
101
|
status: InboundMailAccountStatuses.active,
|
package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts
CHANGED
|
@@ -199,11 +199,11 @@ export const ingestMessageHandler: WriteHandlerDef = {
|
|
|
199
199
|
ownerUserId: payload.ownerUserId,
|
|
200
200
|
messageIdHeader: effectiveMessageIdHeader,
|
|
201
201
|
threadKey,
|
|
202
|
-
from: encryptedMessageFields
|
|
203
|
-
to: encryptedMessageFields
|
|
204
|
-
cc: encryptedMessageFields
|
|
205
|
-
subject: encryptedMessageFields
|
|
206
|
-
snippet: encryptedMessageFields
|
|
202
|
+
from: encryptedMessageFields.from,
|
|
203
|
+
to: encryptedMessageFields.to,
|
|
204
|
+
cc: encryptedMessageFields.cc,
|
|
205
|
+
subject: encryptedMessageFields.subject,
|
|
206
|
+
snippet: encryptedMessageFields.snippet,
|
|
207
207
|
receivedAtIso: payload.receivedAtIso,
|
|
208
208
|
bodyRef: payload.bodyRef,
|
|
209
209
|
scope: payload.scope,
|
|
@@ -296,7 +296,7 @@ export const ingestMessageHandler: WriteHandlerDef = {
|
|
|
296
296
|
|
|
297
297
|
const threadEventPayload: MailThreadEventPayload = {
|
|
298
298
|
threadKey,
|
|
299
|
-
subject: encryptedThreadFields
|
|
299
|
+
subject: encryptedThreadFields.subject,
|
|
300
300
|
lastMessageAtIso: newLastAt,
|
|
301
301
|
messageCount,
|
|
302
302
|
};
|
package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts
CHANGED
|
@@ -34,7 +34,7 @@ export const listAccountsQuery: QueryHandlerDef = {
|
|
|
34
34
|
if (!piiKms) return { rows };
|
|
35
35
|
const decryptedRows = await Promise.all(
|
|
36
36
|
rows.map((row) =>
|
|
37
|
-
decryptPiiFieldValues(row
|
|
37
|
+
decryptPiiFieldValues(row, MAIL_ACCOUNT_PII_FIELDS, piiKms, {
|
|
38
38
|
requestId: `inbound-mail-foundation:list-accounts:${ctx.user.tenantId}`,
|
|
39
39
|
}),
|
|
40
40
|
),
|
package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts
CHANGED
|
@@ -49,7 +49,7 @@ export const listMessagesQuery: QueryHandlerDef = {
|
|
|
49
49
|
if (!piiKms) return { rows };
|
|
50
50
|
const decryptedRows = await Promise.all(
|
|
51
51
|
rows.map((row) =>
|
|
52
|
-
decryptPiiFieldValues(row
|
|
52
|
+
decryptPiiFieldValues(row, INBOUND_MESSAGE_PII_FIELDS, piiKms, {
|
|
53
53
|
requestId: `inbound-mail-foundation:list-messages:${ctx.user.tenantId}`,
|
|
54
54
|
}),
|
|
55
55
|
),
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// für read_mail_accounts und read_inbound_messages (Scope-Vererbung).
|
|
5
5
|
|
|
6
6
|
export function isVisibleToCaller(
|
|
7
|
-
row: unknown
|
|
7
|
+
row: Readonly<Record<string, unknown>>,
|
|
8
8
|
user: { readonly id: string; readonly roles: readonly string[] },
|
|
9
9
|
): boolean {
|
|
10
|
-
const owner =
|
|
10
|
+
const owner = row["ownerUserId"];
|
|
11
11
|
if (owner == null || owner === "") return true;
|
|
12
12
|
if (owner === user.id) return true;
|
|
13
13
|
return user.roles.includes("TenantAdmin") || user.roles.includes("SystemAdmin");
|