@cosmicdrift/kumiko-bundled-features 0.362.0 → 0.363.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin-shell/feature.d.ts +3 -0
- package/dist/admin-shell/feature.js +6 -1
- package/dist/agent-tools/agent-manifest.js +2 -1
- package/dist/agent-tools/filter-ops.d.ts +1 -1
- package/dist/agent-tools/filter-ops.js +1 -3
- package/dist/agent-tools/tool-dispatch.js +2 -1
- package/dist/auth-email-password/email-templates.d.ts +4 -26
- package/dist/auth-email-password/email-templates.js +9 -101
- package/dist/auth-email-password/feature.js +24 -6
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
- package/dist/auth-email-password/handlers/request-account-unlock.write.js +2 -4
- package/dist/auth-email-password/handlers/request-email-verification.write.js +2 -4
- package/dist/auth-email-password/handlers/request-password-reset.write.js +2 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +3 -4
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -2
- package/dist/auth-email-password/handlers/token-request-handler.js +0 -1
- package/dist/auth-email-password/index.d.ts +2 -2
- package/dist/auth-email-password/index.js +1 -1
- package/dist/auth-email-password/magic-link-mail.d.ts +3 -2
- package/dist/auth-email-password/magic-link-mail.js +19 -14
- package/dist/auth-email-password/mail-notifications.d.ts +32 -0
- package/dist/auth-email-password/mail-notifications.js +116 -0
- package/dist/auth-email-password/signup-activation.d.ts +1 -1
- package/dist/auth-email-password/signup-activation.js +2 -3
- package/dist/auth-foundation/types.js +2 -8
- package/dist/auth-mfa/feature.js +4 -4
- package/dist/auth-mfa/hmac-token-codec.js +4 -4
- package/dist/auth-mfa/mfa-challenge-token.js +3 -3
- package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -3
- package/dist/auth-mfa/mfa-setup-token.js +8 -8
- package/dist/billing-foundation/constants.d.ts +3 -3
- package/dist/billing-foundation/constants.js +5 -4
- package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +32 -14
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +27 -32
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +3 -3
- package/dist/billing-foundation/consumer-protection/mail-notifications.d.ts +8 -0
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +95 -0
- package/dist/billing-foundation/consumer-protection/register-consent.js +2 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +20 -10
- package/dist/billing-foundation/consumer-protection/termination-mail.js +10 -21
- package/dist/billing-foundation/consumer-protection/termination-notify.js +2 -2
- package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -3
- package/dist/billing-foundation/consumer-protection/termination-request.js +2 -2
- package/dist/billing-foundation/handlers/process-event.write.js +2 -2
- package/dist/billing-foundation/handlers/process-payment-event.write.js +1 -1
- package/dist/billing-foundation/subscription-tier-sync.js +3 -4
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -1
- package/dist/cap-overview/web/cap-usage-cell.js +5 -4
- package/dist/channel-email/email-channel.d.ts +2 -2
- package/dist/channel-email/email-channel.js +89 -26
- package/dist/channel-email/feature.js +3 -3
- package/dist/channel-email/index.d.ts +1 -1
- package/dist/channel-email/index.js +1 -1
- package/dist/channel-email/pii-guard.js +10 -0
- package/dist/channel-email/smtp-transport.js +13 -0
- package/dist/channel-email/types.d.ts +8 -0
- package/dist/config/handlers/set.write.js +1 -2
- package/dist/custom-fields/lib/value-schema.js +10 -11
- package/dist/custom-fields/wire-user-data-rights.js +7 -6
- package/dist/data-retention/index.d.ts +1 -0
- package/dist/data-retention/index.js +1 -0
- package/dist/data-retention/run-retention-cleanup.js +6 -6
- package/dist/delivery/attachment-ref.d.ts +39 -0
- package/dist/delivery/attachment-ref.js +64 -0
- package/dist/delivery/channel-context.d.ts +8 -4
- package/dist/delivery/channel-context.js +76 -2
- package/dist/delivery/constants.d.ts +1 -0
- package/dist/delivery/constants.js +1 -0
- package/dist/delivery/delivery-service.d.ts +4 -1
- package/dist/delivery/delivery-service.js +38 -32
- package/dist/delivery/feature.js +1 -1
- package/dist/delivery/index.d.ts +5 -2
- package/dist/delivery/index.js +4 -1
- package/dist/delivery/jobs.js +25 -5
- package/dist/delivery/notify-factory.js +4 -1
- package/dist/delivery/testing.d.ts +4 -1
- package/dist/delivery/types.d.ts +20 -14
- package/dist/delivery/unsubscribe.js +4 -1
- package/dist/derivatives-sharp/render.js +1 -18
- package/dist/document-ingest-foundation/pages.js +4 -4
- package/dist/feature-toggles/toggle-sync-signal.js +4 -4
- package/dist/file-foundation/index.d.ts +1 -0
- package/dist/file-foundation/index.js +1 -0
- package/dist/file-foundation/read-file-ref-content.d.ts +27 -0
- package/dist/file-foundation/read-file-ref-content.js +49 -0
- package/dist/folders/web/folder-manager.js +3 -2
- package/dist/form-draft/db/queries/cleanup.js +4 -4
- package/dist/form-draft/db/queries/draft-count.js +2 -2
- package/dist/form-draft/db/queries/owned-file-refs.js +2 -2
- package/dist/form-draft/release-file-refs.js +3 -2
- package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +6 -6
- package/dist/inbound-mail-foundation/handlers/scope-visibility.d.ts +1 -1
- package/dist/inbound-mail-foundation/oauth-state.js +7 -7
- package/dist/inbound-mail-foundation/types.js +4 -4
- package/dist/inbound-mail-foundation/watch-supervisor.js +22 -13
- package/dist/inbound-provider-imap/imap-client.js +4 -3
- package/dist/ledger/recurring.js +1 -3
- package/dist/managed-pages/branding.d.ts +2 -0
- package/dist/managed-pages/branding.js +24 -2
- package/dist/managed-pages/feature.js +1 -0
- package/dist/managed-pages/i18n.js +1 -0
- package/dist/managed-pages/screens/branding-screen.js +8 -1
- package/dist/page-render/branding.d.ts +1 -0
- package/dist/page-render/branding.js +1 -0
- package/dist/page-render/markdown.js +1 -1
- package/dist/personal-access-tokens/feature.js +1 -1
- package/dist/personal-access-tokens/handlers/list.query.js +2 -1
- package/dist/personal-access-tokens/revoke-for-user.js +2 -1
- package/dist/renderer-foundation/api.d.ts +13 -13
- package/dist/renderer-foundation/api.js +1 -1
- package/dist/renderer-foundation/constants.js +2 -4
- package/dist/renderer-foundation/feature.js +7 -7
- package/dist/renderer-foundation/index.d.ts +2 -1
- package/dist/renderer-foundation/index.js +1 -0
- package/dist/renderer-foundation/types.d.ts +92 -32
- package/dist/renderer-pdf/feature.js +7 -1
- package/dist/sessions/db/queries/cleanup.js +2 -2
- package/dist/sessions/feature.js +1 -1
- package/dist/sessions/handlers/list.query.js +2 -1
- package/dist/shared/attachment-file-name.d.ts +3 -0
- package/dist/shared/attachment-file-name.js +35 -0
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.js +3 -0
- package/dist/shared/is-tenant-db.js +1 -1
- package/dist/shared/mail-template-body.d.ts +4 -0
- package/dist/shared/mail-template-body.js +17 -0
- package/dist/shared/sharp-input-limits.d.ts +6 -0
- package/dist/shared/sharp-input-limits.js +22 -0
- package/dist/shared/signup-handover.js +2 -2
- package/dist/tags/handlers/assign-tag.write.js +3 -1
- package/dist/tags/web/tag-chip.js +2 -0
- package/dist/template-resolver/api.d.ts +30 -2
- package/dist/template-resolver/api.js +40 -5
- package/dist/template-resolver/compile-document-body.d.ts +8 -0
- package/dist/template-resolver/compile-document-body.js +163 -0
- package/dist/template-resolver/constants.d.ts +13 -4
- package/dist/template-resolver/constants.js +30 -10
- package/dist/template-resolver/document-sanitizer.d.ts +32 -0
- package/dist/template-resolver/document-sanitizer.js +201 -0
- package/dist/template-resolver/document-schema.d.ts +21 -0
- package/dist/template-resolver/document-schema.js +296 -0
- package/dist/template-resolver/feature.d.ts +18 -0
- package/dist/template-resolver/feature.js +50 -6
- package/dist/template-resolver/handlers/bundle-import.d.ts +20 -0
- package/dist/template-resolver/handlers/bundle-import.js +147 -0
- package/dist/template-resolver/handlers/collection-set.write.js +25 -2
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +19 -0
- package/dist/template-resolver/handlers/copy-locale.write.js +116 -0
- package/dist/template-resolver/handlers/diff-to-system.query.d.ts +22 -0
- package/dist/template-resolver/handlers/diff-to-system.query.js +64 -0
- package/dist/template-resolver/handlers/export-bundle.query.d.ts +35 -0
- package/dist/template-resolver/handlers/export-bundle.query.js +68 -0
- package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
- package/dist/template-resolver/handlers/find-by-id.query.js +3 -3
- package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +39 -0
- package/dist/template-resolver/handlers/import-bundle-system.write.js +36 -0
- package/dist/template-resolver/handlers/import-bundle.write.d.ts +40 -0
- package/dist/template-resolver/handlers/import-bundle.write.js +19 -0
- package/dist/template-resolver/handlers/layout-source.query.d.ts +26 -0
- package/dist/template-resolver/handlers/layout-source.query.js +64 -0
- package/dist/template-resolver/handlers/list.query.d.ts +4 -1
- package/dist/template-resolver/handlers/list.query.js +5 -7
- package/dist/template-resolver/handlers/overview.query.d.ts +59 -0
- package/dist/template-resolver/handlers/overview.query.js +165 -0
- package/dist/template-resolver/handlers/preview.query.d.ts +12 -0
- package/dist/template-resolver/handlers/preview.query.js +24 -0
- package/dist/template-resolver/handlers/reset-to-system.write.d.ts +8 -0
- package/dist/template-resolver/handlers/reset-to-system.write.js +50 -0
- package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +8 -0
- package/dist/template-resolver/handlers/resource-link-pdf.write.js +32 -0
- package/dist/template-resolver/handlers/resource-remove.write.d.ts +9 -0
- package/dist/template-resolver/handlers/resource-remove.write.js +44 -0
- package/dist/template-resolver/handlers/resource-shared.d.ts +9 -0
- package/dist/template-resolver/handlers/resource-shared.js +14 -0
- package/dist/template-resolver/handlers/resource-upload.write.d.ts +13 -0
- package/dist/template-resolver/handlers/resource-upload.write.js +98 -0
- package/dist/template-resolver/handlers/send-test.write.d.ts +11 -0
- package/dist/template-resolver/handlers/send-test.write.js +81 -0
- package/dist/template-resolver/handlers/set.write.d.ts +4 -1
- package/dist/template-resolver/handlers/set.write.js +25 -3
- package/dist/template-resolver/handlers/shared.d.ts +9 -5
- package/dist/template-resolver/handlers/shared.js +6 -2
- package/dist/template-resolver/handlers/template-gates.d.ts +36 -0
- package/dist/template-resolver/handlers/template-gates.js +131 -0
- package/dist/template-resolver/handlers/template-write.d.ts +45 -0
- package/dist/template-resolver/handlers/template-write.js +109 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +17 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.js +48 -0
- package/dist/template-resolver/handlers/toggle-status.write.d.ts +12 -0
- package/dist/template-resolver/handlers/toggle-status.write.js +27 -11
- package/dist/template-resolver/handlers/upsert-system.write.d.ts +5 -8
- package/dist/template-resolver/handlers/upsert-system.write.js +14 -59
- package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +5 -8
- package/dist/template-resolver/handlers/upsert-tenant.write.js +10 -61
- package/dist/template-resolver/handlers/usage.query.d.ts +17 -0
- package/dist/template-resolver/handlers/usage.query.js +15 -0
- package/dist/template-resolver/handlers/validate.query.d.ts +74 -0
- package/dist/template-resolver/handlers/validate.query.js +80 -0
- package/dist/template-resolver/index.d.ts +18 -1
- package/dist/template-resolver/index.js +16 -1
- package/dist/template-resolver/layout-regions.d.ts +23 -0
- package/dist/template-resolver/layout-regions.js +125 -0
- package/dist/template-resolver/linked-resources.d.ts +17 -0
- package/dist/template-resolver/linked-resources.js +75 -0
- package/dist/template-resolver/preview-display-html.d.ts +7 -0
- package/dist/template-resolver/preview-display-html.js +27 -0
- package/dist/template-resolver/print-document.d.ts +1 -0
- package/dist/template-resolver/print-document.js +55 -0
- package/dist/template-resolver/qualified-names.d.ts +30 -0
- package/dist/template-resolver/qualified-names.js +30 -0
- package/dist/template-resolver/record-mapping.d.ts +2 -0
- package/dist/template-resolver/record-mapping.js +21 -0
- package/dist/template-resolver/resource-image.d.ts +15 -0
- package/dist/template-resolver/resource-image.js +34 -0
- package/dist/template-resolver/seeding.d.ts +14 -1
- package/dist/template-resolver/seeding.js +105 -15
- package/dist/template-resolver/store-template-resource.d.ts +64 -0
- package/dist/template-resolver/store-template-resource.js +264 -0
- package/dist/template-resolver/table.d.ts +47 -8
- package/dist/template-resolver/table.js +11 -0
- package/dist/template-resolver/template-bundle.d.ts +36 -0
- package/dist/template-resolver/template-bundle.js +60 -0
- package/dist/template-resolver/template-diff.d.ts +7 -0
- package/dist/template-resolver/template-diff.js +68 -0
- package/dist/template-resolver/template-meta.d.ts +60 -0
- package/dist/template-resolver/template-meta.js +101 -0
- package/dist/template-resolver/template-preview.d.ts +86 -0
- package/dist/template-resolver/template-preview.js +374 -0
- package/dist/template-resolver/template-resources.d.ts +14 -0
- package/dist/template-resolver/template-resources.js +42 -0
- package/dist/template-resolver/template-usage.d.ts +12 -0
- package/dist/template-resolver/template-usage.js +30 -0
- package/dist/template-resolver/template-validation.d.ts +22 -0
- package/dist/template-resolver/template-validation.js +172 -0
- package/dist/template-resolver/testing.js +5 -5
- package/dist/template-resolver/user-content-table.d.ts +14 -8
- package/dist/template-resolver/web/client-plugin.d.ts +1 -0
- package/dist/template-resolver/web/client-plugin.js +9 -6
- package/dist/template-resolver/web/document-editor-extensions.d.ts +15 -0
- package/dist/template-resolver/web/document-editor-extensions.js +107 -0
- package/dist/template-resolver/web/document-editor.d.ts +3 -0
- package/dist/template-resolver/web/document-editor.js +11 -0
- package/dist/template-resolver/web/document-tiptap-editor.d.ts +3 -0
- package/dist/template-resolver/web/document-tiptap-editor.js +376 -0
- package/dist/template-resolver/web/document-toolbar-blocks.d.ts +33 -0
- package/dist/template-resolver/web/document-toolbar-blocks.js +15 -0
- package/dist/template-resolver/web/editor-handle.d.ts +3 -0
- package/dist/template-resolver/web/editor-handle.js +24 -0
- package/dist/template-resolver/web/i18n.js +76 -0
- package/dist/template-resolver/web/index.d.ts +4 -0
- package/dist/template-resolver/web/index.js +4 -0
- package/dist/template-resolver/web/placeholder-line-editor-lazy.d.ts +4 -0
- package/dist/template-resolver/web/placeholder-line-editor-lazy.js +16 -0
- package/dist/template-resolver/web/placeholder-line-editor.d.ts +15 -0
- package/dist/template-resolver/web/placeholder-line-editor.js +94 -0
- package/dist/template-resolver/web/placeholder-text.d.ts +4 -0
- package/dist/template-resolver/web/placeholder-text.js +34 -0
- package/dist/template-resolver/web/use-stable-by-key.d.ts +1 -0
- package/dist/template-resolver/web/use-stable-by-key.js +13 -0
- package/dist/tenant/handlers/team-list.query.js +3 -2
- package/dist/tenant/seeding.js +31 -6
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +2 -1
- package/dist/user/db/queries/stream-tenant-backfill.js +4 -4
- package/dist/user/handlers/detail.query.js +2 -1
- package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
- package/dist/user-data-rights/email-templates.d.ts +1 -29
- package/dist/user-data-rights/email-templates.js +5 -106
- package/dist/user-data-rights/feature.js +53 -35
- package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
- package/dist/user-data-rights/lib/default-mailers.d.ts +15 -7
- package/dist/user-data-rights/lib/default-mailers.js +100 -48
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/user-data-rights/mail-notifications.d.ts +13 -0
- package/dist/user-data-rights/mail-notifications.js +118 -0
- package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
- package/dist/user-profile/web/profile-screen.js +2 -1
- package/dist/waitlist/feature.js +10 -0
- package/dist/waitlist/handlers/submit.write.js +3 -4
- package/dist/waitlist/mail.d.ts +19 -6
- package/dist/waitlist/mail.js +50 -14
- package/package.json +28 -17
- package/src/admin-shell/changes.json +6 -0
- package/src/auth-email-password/changes.json +14 -0
- package/src/billing-foundation/changes.json +7 -0
- package/src/channel-email/changes.json +19 -0
- package/src/data-retention/changes.json +6 -0
- package/src/delivery/changes.json +48 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/renderer-foundation/changes.json +39 -0
- package/src/template-resolver/changes.json +118 -0
- package/src/user-data-rights/changes.json +7 -0
- package/src/waitlist/changes.json +7 -0
- package/dist/renderer-simple/feature.d.ts +0 -10
- package/dist/renderer-simple/feature.js +0 -42
- package/dist/renderer-simple/index.d.ts +0 -3
- package/dist/renderer-simple/index.js +0 -2
- package/dist/renderer-simple/mail-blocks.d.ts +0 -21
- package/dist/renderer-simple/mail-blocks.js +0 -107
- package/dist/renderer-simple/resolve-variables.d.ts +0 -7
- package/dist/renderer-simple/resolve-variables.js +0 -47
- package/dist/renderer-simple/simple-renderer.d.ts +0 -24
- package/dist/renderer-simple/simple-renderer.js +0 -200
- package/src/renderer-simple/changes.json +0 -27
package/dist/delivery/jobs.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// these handlers append the terminal sent/failed event on the same stream.
|
|
7
7
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
8
8
|
import * as z from "zod";
|
|
9
|
+
import { fileContextForTenant } from "../file-foundation/index.js";
|
|
10
|
+
import { emailAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, toAttachmentRef, } from "./attachment-ref.js";
|
|
9
11
|
import { appendAttemptEvent } from "./attempt-log.js";
|
|
10
12
|
import { buildChannelContext } from "./channel-context.js";
|
|
11
13
|
import { DeliveryJobs, deliveryIdempotencyRedisKey, deliveryPriorityRank } from "./constants.js";
|
|
@@ -30,7 +32,12 @@ const renderJobPayloadSchema = z.object({
|
|
|
30
32
|
});
|
|
31
33
|
const sendJobPayloadSchema = renderJobPayloadSchema.extend({
|
|
32
34
|
rendered: z
|
|
33
|
-
.object({
|
|
35
|
+
.object({
|
|
36
|
+
html: z.string(),
|
|
37
|
+
subject: z.string(),
|
|
38
|
+
text: z.string().optional(),
|
|
39
|
+
attachments: z.array(emailAttachmentRefSchema).max(MAX_EMAIL_ATTACHMENTS).optional(),
|
|
40
|
+
})
|
|
34
41
|
.optional(),
|
|
35
42
|
});
|
|
36
43
|
function requireTenantScopedDeps(ctx, tenantId) {
|
|
@@ -39,15 +46,28 @@ function requireTenantScopedDeps(ctx, tenantId) {
|
|
|
39
46
|
throw new Error("delivery job: missing registry in job context");
|
|
40
47
|
// delivery is r.systemScope()'d, so JobContext.systemDb is guaranteed; unsafeRaw
|
|
41
48
|
// hands the underlying DbConnection to both — safe since they filter by tenantId themselves.
|
|
49
|
+
// Attachment lookups read only this tenant's FileRefs, plus the system tenant's for inline images.
|
|
42
50
|
if (!ctx.systemDb) {
|
|
43
51
|
throw new InternalError({
|
|
44
52
|
message: "delivery job: ctx.systemDb missing on a system-scoped job",
|
|
45
53
|
});
|
|
46
54
|
}
|
|
55
|
+
// Also makes ctx.config (built for the job's tenant) the accessor of the payload tenant;
|
|
56
|
+
// job contexts carry no configFor.
|
|
47
57
|
ctx.systemDb.assertTenantMatch(tenantId);
|
|
48
|
-
const db = ctx.systemDb.unsafeRaw("
|
|
58
|
+
const db = ctx.systemDb.unsafeRaw("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"); // @cast-boundary db-operator — DbRunner narrows to DbConnection, jobs never run inside a DbTx
|
|
49
59
|
return { db, registry };
|
|
50
60
|
}
|
|
61
|
+
function toSendRendered(rendered) {
|
|
62
|
+
return {
|
|
63
|
+
html: rendered.html,
|
|
64
|
+
subject: rendered.subject,
|
|
65
|
+
...(rendered.text !== undefined && { text: rendered.text }),
|
|
66
|
+
...(rendered.attachments !== undefined && {
|
|
67
|
+
attachments: rendered.attachments.map(toAttachmentRef),
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
51
71
|
function resolveChannel(registry, name) {
|
|
52
72
|
const channel = collectChannels(registry).find((c) => c.name === name);
|
|
53
73
|
if (!channel)
|
|
@@ -119,7 +139,7 @@ export const deliveryRenderJob = async (payload, ctx) => {
|
|
|
119
139
|
const tenantId = p.tenantId; // @cast-boundary engine-payload — stream tenant
|
|
120
140
|
const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
|
|
121
141
|
const channel = resolveChannel(registry, p.channelName);
|
|
122
|
-
const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets);
|
|
142
|
+
const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets, ctx.config, (fileTenantId) => fileContextForTenant(ctx, fileTenantId));
|
|
123
143
|
let rendered;
|
|
124
144
|
try {
|
|
125
145
|
if (!channel.render) {
|
|
@@ -132,7 +152,7 @@ export const deliveryRenderJob = async (payload, ctx) => {
|
|
|
132
152
|
}
|
|
133
153
|
try {
|
|
134
154
|
const jobRunner = ctx["jobRunner"]; // @cast-boundary dynamic-key — dispatch lives on the concrete runner
|
|
135
|
-
await jobRunner.dispatch(DeliveryJobs.send, { ...p, rendered }, { priority: deliveryPriorityRank[p.priority] });
|
|
155
|
+
await jobRunner.dispatch(DeliveryJobs.send, { ...p, rendered: toSendRendered(rendered) }, { priority: deliveryPriorityRank[p.priority] });
|
|
136
156
|
}
|
|
137
157
|
catch (err) {
|
|
138
158
|
return failAttempt(ctx, db, registry, p, "channel_error", err);
|
|
@@ -146,7 +166,7 @@ export const deliverySendJob = async (payload, ctx) => {
|
|
|
146
166
|
const tenantId = p.tenantId; // @cast-boundary engine-payload — stream tenant
|
|
147
167
|
const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
|
|
148
168
|
const channel = resolveChannel(registry, p.channelName);
|
|
149
|
-
const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets);
|
|
169
|
+
const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets, ctx.config, (fileTenantId) => fileContextForTenant(ctx, fileTenantId));
|
|
150
170
|
try {
|
|
151
171
|
const result = await channel.send(p.address, toMessage(p), channelCtx, p.rendered);
|
|
152
172
|
await appendAttemptEvent(db, registry, p.deliveryAttemptId, entryFor(p, result.status, result.error ?? null, result.address ?? p.address, result.confirmed));
|
|
@@ -2,5 +2,8 @@
|
|
|
2
2
|
// job dispatcher is handed to notify() per call, so queued channels go through
|
|
3
3
|
// the delivery jobs. deliverQueuedInline drops the dispatcher and sends inline.
|
|
4
4
|
export function createDeliveryNotifyFactory(deliveryService, options = {}) {
|
|
5
|
-
return (user, tenantId, jobDispatcher
|
|
5
|
+
return (user, tenantId, jobDispatcher, deliveryDb, fileProviderResolver) => {
|
|
6
|
+
const notify = (notificationType, notifyOptions) => deliveryService.notify(notificationType, notifyOptions, user, tenantId, options.deliverQueuedInline === true ? undefined : jobDispatcher, deliveryDb, fileProviderResolver);
|
|
7
|
+
return notify;
|
|
8
|
+
};
|
|
6
9
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import type { EscapeHatchAuditSink, Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import type { ConfigAccessor, EscapeHatchAuditSink, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
|
|
4
5
|
import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
|
|
5
6
|
import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
6
7
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
@@ -16,6 +17,8 @@ export type CreateDeliveryTestContextOptions = {
|
|
|
16
17
|
readonly jobRunner?: JobRunner;
|
|
17
18
|
readonly secrets?: SecretsContext;
|
|
18
19
|
readonly idempotencyRedis?: Redis;
|
|
20
|
+
readonly filesForTenant?: (tenantId: string) => FileContext | undefined;
|
|
21
|
+
readonly tenantConfigFor?: (tenantId: TenantId) => ConfigAccessor;
|
|
19
22
|
};
|
|
20
23
|
/**
|
|
21
24
|
* Helper for setupTestStack: creates a DeliveryService + _notifyFactory for the test context.
|
package/dist/delivery/types.d.ts
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
-
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import type { DeliveryErrorCode, NotifyDeliveryStatus, NotifyJobDispatcher, NotifyOptions, NotifyPriority,
|
|
2
|
+
import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { DeliveryErrorCode, NotifyDeliveryStatus, NotifyDispatched, NotifyJobDispatcher, NotifyOptions, NotifyPriority, Registry, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
|
|
4
5
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
6
|
+
import type { MailPayload } from "../renderer-foundation/index.js";
|
|
7
|
+
import type { EmailAttachmentRef } from "./attachment-ref.js";
|
|
5
8
|
import { DELIVERY_CHANNEL_EXTENSION } from "./constants.js";
|
|
9
|
+
export type RenderedMail = {
|
|
10
|
+
readonly subject: string;
|
|
11
|
+
readonly html: string;
|
|
12
|
+
readonly text: string;
|
|
13
|
+
readonly previewText?: string;
|
|
14
|
+
readonly attachments?: readonly EmailAttachmentRef[];
|
|
15
|
+
};
|
|
6
16
|
export type ChannelContext = {
|
|
7
17
|
readonly db: TenantDb;
|
|
8
18
|
readonly registry: Registry;
|
|
9
19
|
readonly sseBroker: SseBroker | undefined;
|
|
10
20
|
readonly tenantId: TenantId;
|
|
11
21
|
readonly secrets?: SecretsContext | undefined;
|
|
22
|
+
readonly renderMail?: (payload: MailPayload) => Promise<RenderedMail>;
|
|
23
|
+
readonly readAttachment?: (ref: EmailAttachmentRef, origin?: AttachmentOrigin) => Promise<{
|
|
24
|
+
readonly content: Uint8Array;
|
|
25
|
+
readonly contentType: string;
|
|
26
|
+
} | null>;
|
|
12
27
|
};
|
|
28
|
+
export type AttachmentOrigin = "template" | "message";
|
|
13
29
|
export type ChannelMessage = {
|
|
14
30
|
readonly notificationType: string;
|
|
15
31
|
readonly title: string;
|
|
@@ -27,6 +43,7 @@ export type RenderedMessage = {
|
|
|
27
43
|
readonly html: string;
|
|
28
44
|
readonly subject: string;
|
|
29
45
|
readonly text?: string;
|
|
46
|
+
readonly attachments?: readonly EmailAttachmentRef[];
|
|
30
47
|
};
|
|
31
48
|
export declare const DELIVERY_CHANNEL_MODES: readonly ["inline", "queued"];
|
|
32
49
|
export type DeliveryChannelMode = (typeof DELIVERY_CHANNEL_MODES)[number];
|
|
@@ -38,17 +55,6 @@ export type DeliveryChannel = {
|
|
|
38
55
|
render?(message: ChannelMessage, ctx: ChannelContext): Promise<RenderedMessage>;
|
|
39
56
|
send(address: string, message: ChannelMessage, ctx: ChannelContext, rendered?: RenderedMessage): Promise<ChannelResult>;
|
|
40
57
|
};
|
|
41
|
-
export type RendererInput = {
|
|
42
|
-
readonly template: string;
|
|
43
|
-
readonly variables: Readonly<Record<string, unknown>>;
|
|
44
|
-
readonly locale?: string | undefined;
|
|
45
|
-
};
|
|
46
|
-
export type NotificationRenderer = {
|
|
47
|
-
readonly name: string;
|
|
48
|
-
render(input: RendererInput): Promise<string>;
|
|
49
|
-
/** Plain-text version of the same mail; the email channel sends it as the text part of a multipart/alternative mail. */
|
|
50
|
-
renderText?(input: RendererInput): Promise<string>;
|
|
51
|
-
};
|
|
52
58
|
export type DeliveryLogEntry = {
|
|
53
59
|
readonly tenantId: TenantId;
|
|
54
60
|
readonly notificationType: string;
|
|
@@ -61,7 +67,7 @@ export type DeliveryLogEntry = {
|
|
|
61
67
|
readonly confirmed?: false;
|
|
62
68
|
};
|
|
63
69
|
export type DeliveryService = {
|
|
64
|
-
notify(notificationType: string, options: NotifyOptions, user: SessionUser, tenantId: TenantId, jobDispatcher?: NotifyJobDispatcher): Promise<
|
|
70
|
+
notify(notificationType: string, options: NotifyOptions, user: SessionUser, tenantId: TenantId, jobDispatcher?: NotifyJobDispatcher, deliveryDb?: DbConnection, fileProviderResolver?: FileProviderResolver): Promise<NotifyDispatched>;
|
|
65
71
|
};
|
|
66
72
|
export type DeliveryChannelPlugin = Omit<DeliveryChannel, "name">;
|
|
67
73
|
export declare function isDeliveryChannelPlugin(o: unknown): o is DeliveryChannelPlugin;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ExtraRouteRejection, signatureRoute, } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import { escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
3
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
4
|
import * as jose from "jose";
|
|
4
5
|
import * as z from "zod";
|
|
5
6
|
import { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
@@ -280,7 +281,9 @@ function tokenFromRequestBody(request) {
|
|
|
280
281
|
if (contentType?.includes("application/json")) {
|
|
281
282
|
try {
|
|
282
283
|
const parsed = JSON.parse(request.rawBody);
|
|
283
|
-
return typeof parsed
|
|
284
|
+
return isPlainObject(parsed) && typeof parsed["token"] === "string"
|
|
285
|
+
? parsed["token"]
|
|
286
|
+
: undefined;
|
|
284
287
|
}
|
|
285
288
|
catch {
|
|
286
289
|
return undefined;
|
|
@@ -3,12 +3,9 @@
|
|
|
3
3
|
// region blur (see BlurRegion). Server-only: sharp is a native binding.
|
|
4
4
|
import QRCode from "qrcode";
|
|
5
5
|
import sharp from "sharp";
|
|
6
|
+
import { assertNotSvg, SHARP_INPUT_OPTIONS } from "../shared/index.js";
|
|
6
7
|
const MIN_SHARP_SIGMA = 0.3;
|
|
7
8
|
const MAX_SHARP_SIGMA = 1000;
|
|
8
|
-
// Above this, a decoded pixel buffer runs into hundreds of MB before any
|
|
9
|
-
// resize even happens — sharp's own default (~268MP) is still large enough
|
|
10
|
-
// to be an OOM primitive for a single request.
|
|
11
|
-
const MAX_INPUT_PIXELS = 100_000_000;
|
|
12
9
|
// Longest output edge a caller may request via `size`/`maxEdge`. Without a
|
|
13
10
|
// cap, `{ maxEdge: 50000 }` allocates a multi-gigabyte buffer per encoder
|
|
14
11
|
// step before any client sees a byte back.
|
|
@@ -27,26 +24,12 @@ const MAX_QR_DATA_LENGTH = 1024;
|
|
|
27
24
|
// camera capture — the AC claims phone-scannable, so an under-sized layer
|
|
28
25
|
// throws instead of caching a silently unusable image.
|
|
29
26
|
const MIN_QR_PIXEL_WIDTH = 160;
|
|
30
|
-
// sequentialRead trades random pixel access for lower peak memory on
|
|
31
|
-
// streamed formats — applied at every decode entry point below since none
|
|
32
|
-
// of them need non-sequential access to the *original* bytes (extract runs
|
|
33
|
-
// against a single already-buffered region per call).
|
|
34
|
-
const SHARP_INPUT_OPTIONS = { limitInputPixels: MAX_INPUT_PIXELS, sequentialRead: true };
|
|
35
27
|
function normalizeMimeType(mimeType) {
|
|
36
28
|
return mimeType.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
37
29
|
}
|
|
38
30
|
function clampSigma(sigma) {
|
|
39
31
|
return Math.min(MAX_SHARP_SIGMA, Math.max(MIN_SHARP_SIGMA, sigma));
|
|
40
32
|
}
|
|
41
|
-
// Shared trust boundary for every image byte source this renderer decodes —
|
|
42
|
-
// the original upload (renderImage) and an overlay layer's bytes (applyOverlays)
|
|
43
|
-
// alike: a base64 string in a field declaration isn't more trustworthy than
|
|
44
|
-
// an upload just because it came from app code.
|
|
45
|
-
function assertNotSvg(format) {
|
|
46
|
-
if (format === "svg") {
|
|
47
|
-
throw new Error("derivatives-sharp: SVG is not supported — rendering untrusted SVG through libvips is a trust boundary this renderer doesn't need to cross.");
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
33
|
function assertValidRegion(region) {
|
|
51
34
|
const values = [region.x, region.y, region.width, region.height];
|
|
52
35
|
if (values.some((v) => !Number.isFinite(v)) || region.width <= 0 || region.height <= 0) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// JSON.stringify(IngestPage[]) and readers must parse back. Centralising
|
|
4
4
|
// the codec here keeps liteparse + recipe consumers lockstep with the
|
|
5
5
|
// entity shape (kumiko-framework#1549/1).
|
|
6
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
6
7
|
export function writeIngestPages(pages) {
|
|
7
8
|
return JSON.stringify(pages);
|
|
8
9
|
}
|
|
@@ -25,8 +26,7 @@ function parseJson(raw) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
function isIngestPage(value) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return typeof page.pageNumber === "number" && typeof page.text === "string";
|
|
29
|
+
return (isPlainObject(value) &&
|
|
30
|
+
typeof value["pageNumber"] === "number" &&
|
|
31
|
+
typeof value["text"] === "string");
|
|
32
32
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { CACHE_SYNC_TOPICS, createRedisPubSubSignal, redisChannelPrefixFromEnv, } from "@cosmicdrift/kumiko-framework/redis";
|
|
2
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
3
|
// Single fixed channel — unlike the SSE broker there's no per-tenant/
|
|
3
4
|
// per-user variance to multiplex, every toggle flip is global.
|
|
4
5
|
const TOGGLE_SYNC_CHANNEL_NAME = "kumiko:feature-toggles:cache-sync";
|
|
5
6
|
function isTogglePayload(value) {
|
|
6
|
-
return (
|
|
7
|
-
value
|
|
8
|
-
typeof value
|
|
9
|
-
typeof value.enabled === "boolean");
|
|
7
|
+
return (isPlainObject(value) &&
|
|
8
|
+
typeof value["featureName"] === "string" &&
|
|
9
|
+
typeof value["enabled"] === "boolean");
|
|
10
10
|
}
|
|
11
11
|
// fw#2625: gives toggle-cache-sync the cross-replica transport its shared
|
|
12
12
|
// dispatcher cursor needs — without this, only the one process that won
|
|
@@ -1 +1,2 @@
|
|
|
1
1
|
export { createFileProviderForTenant, type FileProviderContext, type FileProviderPlugin, fileFoundationFeature, isFileProviderPlugin, } from "./feature.js";
|
|
2
|
+
export { type FileRefContent, FileRefTooLargeError, fileContextForTenant, fileContextFromResolver, readFileRefContent, } from "./read-file-ref-content.js";
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
// Public API of the file-foundation bundled-feature.
|
|
2
2
|
export { createFileProviderForTenant, fileFoundationFeature, isFileProviderPlugin, } from "./feature.js";
|
|
3
|
+
export { FileRefTooLargeError, fileContextForTenant, fileContextFromResolver, readFileRefContent, } from "./read-file-ref-content.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { type FileContext, type FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
|
|
3
|
+
export type FileRefContent = {
|
|
4
|
+
readonly content: Uint8Array;
|
|
5
|
+
readonly contentType: string;
|
|
6
|
+
readonly tenantId: string;
|
|
7
|
+
readonly size: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function fileContextForTenant(ctx: {
|
|
10
|
+
readonly _fileProviderResolver?: FileProviderResolver | undefined;
|
|
11
|
+
readonly files?: FileContext | undefined;
|
|
12
|
+
}, tenantId: string): FileContext | undefined;
|
|
13
|
+
export declare function fileContextFromResolver(resolver: FileProviderResolver, tenantId: string): FileContext;
|
|
14
|
+
export declare class FileRefTooLargeError extends Error {
|
|
15
|
+
readonly size: number;
|
|
16
|
+
constructor(size: number);
|
|
17
|
+
}
|
|
18
|
+
export declare function readFileRefContent(args: {
|
|
19
|
+
readonly db: DbRunner | TenantDb;
|
|
20
|
+
readonly filesForTenant: (tenantId: string) => FileContext | undefined;
|
|
21
|
+
readonly fileRefId: string;
|
|
22
|
+
readonly allowedTenantIds: readonly string[];
|
|
23
|
+
/** Only accept this user's own upload that is not bound to any entity yet. */
|
|
24
|
+
readonly unclaimedUploadOf?: string;
|
|
25
|
+
/** Rejects before any bytes are read when the stored size exceeds the limit. */
|
|
26
|
+
readonly maxBytes?: number;
|
|
27
|
+
}): Promise<FileRefContent | null>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { isUuid } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { createFileContext, fileRefsTable, } from "@cosmicdrift/kumiko-framework/files";
|
|
4
|
+
// Tenant-bound on purpose: the FileContext of the current request only reaches its own key space.
|
|
5
|
+
export function fileContextForTenant(ctx, tenantId) {
|
|
6
|
+
const resolver = ctx._fileProviderResolver;
|
|
7
|
+
if (resolver === undefined)
|
|
8
|
+
return ctx.files;
|
|
9
|
+
return fileContextFromResolver(resolver, tenantId);
|
|
10
|
+
}
|
|
11
|
+
export function fileContextFromResolver(resolver, tenantId) {
|
|
12
|
+
// @cast-boundary engine-payload — tenantId comes from a trusted job payload or session
|
|
13
|
+
const brandedTenantId = tenantId;
|
|
14
|
+
return createFileContext(() => resolver(brandedTenantId), tenantId);
|
|
15
|
+
}
|
|
16
|
+
export class FileRefTooLargeError extends Error {
|
|
17
|
+
size;
|
|
18
|
+
constructor(size) {
|
|
19
|
+
super(`file ref exceeds the size limit (${size} bytes)`);
|
|
20
|
+
this.size = size;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// fileName of the row is never used: it is personal data and may be a PII ciphertext.
|
|
24
|
+
export async function readFileRefContent(args) {
|
|
25
|
+
if (!isUuid(args.fileRefId))
|
|
26
|
+
return null;
|
|
27
|
+
const [row] = await selectMany(args.db, fileRefsTable, {
|
|
28
|
+
id: args.fileRefId,
|
|
29
|
+
tenantId: { in: args.allowedTenantIds },
|
|
30
|
+
isDeleted: false,
|
|
31
|
+
...(args.unclaimedUploadOf !== undefined && {
|
|
32
|
+
insertedById: args.unclaimedUploadOf,
|
|
33
|
+
entityType: null,
|
|
34
|
+
entityId: null,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
if (row === undefined)
|
|
38
|
+
return null;
|
|
39
|
+
const size = Number(row.size);
|
|
40
|
+
if (args.maxBytes !== undefined && size > args.maxBytes) {
|
|
41
|
+
throw new FileRefTooLargeError(size);
|
|
42
|
+
}
|
|
43
|
+
const files = args.filesForTenant(row.tenantId);
|
|
44
|
+
if (files === undefined) {
|
|
45
|
+
throw new Error("readFileRefContent: no file storage is available for the file's tenant");
|
|
46
|
+
}
|
|
47
|
+
const content = await files.ref(row.storageKey).read();
|
|
48
|
+
return { content, contentType: row.mimeType, tenantId: row.tenantId, size };
|
|
49
|
+
}
|
|
@@ -42,7 +42,8 @@ const loadCollapsed = () => {
|
|
|
42
42
|
return new Set();
|
|
43
43
|
try {
|
|
44
44
|
const raw = window.localStorage.getItem(COLLAPSED_KEY);
|
|
45
|
-
|
|
45
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
46
|
+
return new Set(Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : []);
|
|
46
47
|
}
|
|
47
48
|
catch {
|
|
48
49
|
return new Set();
|
|
@@ -207,7 +208,7 @@ export function FolderManager({ renderMeta, filing, }) {
|
|
|
207
208
|
setDragOverKey(key);
|
|
208
209
|
},
|
|
209
210
|
onDragLeave: (e) => {
|
|
210
|
-
if (!e.currentTarget.contains(e.relatedTarget))
|
|
211
|
+
if (!(e.relatedTarget instanceof Node && e.currentTarget.contains(e.relatedTarget)))
|
|
211
212
|
setDragOverKey((cur) => (cur === key ? null : cur));
|
|
212
213
|
},
|
|
213
214
|
onDrop: (e) => {
|
|
@@ -12,10 +12,10 @@ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
|
12
12
|
// rebuild must not resurrect a "deleted" draft). Ordered oldest-first so a
|
|
13
13
|
// batch is a stable, deterministic slice across the two queries.
|
|
14
14
|
export async function selectStaleDraftsBatch(db, olderThanDays, batchSize) {
|
|
15
|
-
const rows =
|
|
15
|
+
const rows = await unsafeReadRetrying(db, `SELECT "id", "tenant_id", "owner_id", "draft_key", "draft", "inserted_at" FROM "read_form_drafts"
|
|
16
16
|
WHERE COALESCE("modified_at", "inserted_at") < now() - ($1::int * interval '1 day')
|
|
17
17
|
ORDER BY COALESCE("modified_at", "inserted_at") ASC
|
|
18
|
-
LIMIT $2`, [olderThanDays, batchSize])
|
|
18
|
+
LIMIT $2`, [olderThanDays, batchSize]);
|
|
19
19
|
return rows.map((row) => ({
|
|
20
20
|
id: row.id,
|
|
21
21
|
tenantId: row.tenant_id, // @cast-boundary db-row
|
|
@@ -27,10 +27,10 @@ export async function selectStaleDraftsBatch(db, olderThanDays, batchSize) {
|
|
|
27
27
|
}
|
|
28
28
|
/** Re-check staleness after the select→release window so a freshly saved draft is not deleted. */
|
|
29
29
|
export async function isDraftStillStale(db, tenantId, id, olderThanDays) {
|
|
30
|
-
const rows =
|
|
30
|
+
const rows = await unsafeReadRetrying(db, `SELECT 1 FROM "read_form_drafts"
|
|
31
31
|
WHERE "id" = $1
|
|
32
32
|
AND "tenant_id" = $2
|
|
33
33
|
AND COALESCE("modified_at", "inserted_at") < now() - ($3::int * interval '1 day')
|
|
34
|
-
LIMIT 1`, [id, tenantId, olderThanDays])
|
|
34
|
+
LIMIT 1`, [id, tenantId, olderThanDays]);
|
|
35
35
|
return rows.length > 0;
|
|
36
36
|
}
|
|
@@ -4,7 +4,7 @@ import { unsafeReadRetrying } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
|
4
4
|
// (64KB); loading up to FORM_DRAFT_MAX_PER_OWNER full rows just to count them
|
|
5
5
|
// would be several MB of wasted transfer per save.
|
|
6
6
|
export async function countDraftsByOwner(db, tenantId, ownerId) {
|
|
7
|
-
const rows =
|
|
8
|
-
WHERE "tenant_id" = $1 AND "owner_id" = $2`, [tenantId, ownerId])
|
|
7
|
+
const rows = await unsafeReadRetrying(db, `SELECT count(*)::int AS "count" FROM "read_form_drafts"
|
|
8
|
+
WHERE "tenant_id" = $1 AND "owner_id" = $2`, [tenantId, ownerId]);
|
|
9
9
|
return rows[0]?.count ?? 0;
|
|
10
10
|
}
|
|
@@ -29,9 +29,9 @@ export async function filterOwnedStorageKeys(db, tenantId, ownerId, candidateKey
|
|
|
29
29
|
export async function filterOwnedFileRefs(db, tenantId, ownerId, candidateKeys, draftInsertedAt, isCreateMode) {
|
|
30
30
|
if (candidateKeys.length === 0)
|
|
31
31
|
return [];
|
|
32
|
-
const rows =
|
|
32
|
+
const rows = await unsafeReadRetrying(db, `SELECT "id", "storage_key" FROM "file_refs"
|
|
33
33
|
WHERE "tenant_id" = $1 AND "inserted_by_id" = $2
|
|
34
34
|
AND "storage_key" = ANY($3::text[]) AND "is_deleted" = false
|
|
35
|
-
AND ($5::boolean OR "inserted_at" > $4::timestamptz)`, [tenantId, ownerId, candidateKeys, draftInsertedAt.toString(), isCreateMode])
|
|
35
|
+
AND ($5::boolean OR "inserted_at" > $4::timestamptz)`, [tenantId, ownerId, candidateKeys, draftInsertedAt.toString(), isCreateMode]);
|
|
36
36
|
return rows.map((row) => ({ id: row.id, storageKey: row.storage_key }));
|
|
37
37
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
2
|
// The draft blob's `values` is free-form by design (issue #1889: form-draft
|
|
2
3
|
// never knows the consuming form's field schema, only { values, stepIndex,
|
|
3
4
|
// savedAt }). A FileRef pointer is therefore recognised purely by shape —
|
|
@@ -6,9 +7,9 @@
|
|
|
6
7
|
// step writes into the draft, whether under a single "file" field or nested
|
|
7
8
|
// in a "files"/"images" array.
|
|
8
9
|
function isFileRefPointer(value) {
|
|
9
|
-
if (
|
|
10
|
+
if (!isPlainObject(value))
|
|
10
11
|
return false;
|
|
11
|
-
const storageKey = value
|
|
12
|
+
const storageKey = value["storageKey"];
|
|
12
13
|
return typeof storageKey === "string" && storageKey.length > 0;
|
|
13
14
|
}
|
|
14
15
|
function collectStorageKeys(value, keys) {
|
|
@@ -70,7 +70,7 @@ export const connectAccountHandler = {
|
|
|
70
70
|
authMethod: payload.authMethod,
|
|
71
71
|
ownerUserId,
|
|
72
72
|
displayName: payload.displayName,
|
|
73
|
-
address: encryptedFields
|
|
73
|
+
address: encryptedFields.address,
|
|
74
74
|
// active ab Connect — der Supervisor greift den Account beim
|
|
75
75
|
// nächsten Tick auf und degradiert ihn bei Credential-Fehlern.
|
|
76
76
|
status: InboundMailAccountStatuses.active,
|
|
@@ -165,11 +165,11 @@ export const ingestMessageHandler = {
|
|
|
165
165
|
ownerUserId: payload.ownerUserId,
|
|
166
166
|
messageIdHeader: effectiveMessageIdHeader,
|
|
167
167
|
threadKey,
|
|
168
|
-
from: encryptedMessageFields
|
|
169
|
-
to: encryptedMessageFields
|
|
170
|
-
cc: encryptedMessageFields
|
|
171
|
-
subject: encryptedMessageFields
|
|
172
|
-
snippet: encryptedMessageFields
|
|
168
|
+
from: encryptedMessageFields.from,
|
|
169
|
+
to: encryptedMessageFields.to,
|
|
170
|
+
cc: encryptedMessageFields.cc,
|
|
171
|
+
subject: encryptedMessageFields.subject,
|
|
172
|
+
snippet: encryptedMessageFields.snippet,
|
|
173
173
|
receivedAtIso: payload.receivedAtIso,
|
|
174
174
|
bodyRef: payload.bodyRef,
|
|
175
175
|
scope: payload.scope,
|
|
@@ -246,7 +246,7 @@ export const ingestMessageHandler = {
|
|
|
246
246
|
});
|
|
247
247
|
const threadEventPayload = {
|
|
248
248
|
threadKey,
|
|
249
|
-
subject: encryptedThreadFields
|
|
249
|
+
subject: encryptedThreadFields.subject,
|
|
250
250
|
lastMessageAtIso: newLastAt,
|
|
251
251
|
messageCount,
|
|
252
252
|
};
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// HMAC-Input, timing-safe compare, Expiry im Klartext-Segment).
|
|
12
12
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
13
13
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
14
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
14
15
|
const STATE_PURPOSE = "inbound-mail-oauth-connect";
|
|
15
16
|
function sign(input, secret) {
|
|
16
17
|
return createHmac("sha256", secret).update(input).digest("base64url");
|
|
@@ -60,12 +61,11 @@ export function verifyOAuthState(state, secret, now = Temporal.Now.instant()) {
|
|
|
60
61
|
return { ok: true, payload: parsed };
|
|
61
62
|
}
|
|
62
63
|
function isOAuthStatePayload(v) {
|
|
63
|
-
if (
|
|
64
|
+
if (!isPlainObject(v))
|
|
64
65
|
return false;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
typeof
|
|
69
|
-
typeof
|
|
70
|
-
typeof o["nonce"] === "string");
|
|
66
|
+
return (typeof v["tenantId"] === "string" &&
|
|
67
|
+
(v["ownerUserId"] === null || typeof v["ownerUserId"] === "string") &&
|
|
68
|
+
typeof v["providerKey"] === "string" &&
|
|
69
|
+
typeof v["mailbox"] === "string" &&
|
|
70
|
+
typeof v["nonce"] === "string");
|
|
71
71
|
}
|
|
@@ -64,14 +64,14 @@ export class InboundTransientError extends Error {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
export function isInboundAuthError(e) {
|
|
67
|
-
return e instanceof Error && e.kind === "inbound-auth-error";
|
|
67
|
+
return e instanceof Error && "kind" in e && e.kind === "inbound-auth-error";
|
|
68
68
|
}
|
|
69
69
|
export function isInboundRateLimitError(e) {
|
|
70
|
-
return e instanceof Error && e.kind === "inbound-rate-limit-error";
|
|
70
|
+
return e instanceof Error && "kind" in e && e.kind === "inbound-rate-limit-error";
|
|
71
71
|
}
|
|
72
72
|
export function isInboundCursorInvalidError(e) {
|
|
73
|
-
return e instanceof Error && e.kind === "inbound-cursor-invalid-error";
|
|
73
|
+
return e instanceof Error && "kind" in e && e.kind === "inbound-cursor-invalid-error";
|
|
74
74
|
}
|
|
75
75
|
export function isInboundTransientError(e) {
|
|
76
|
-
return e instanceof Error && e.kind === "inbound-transient-error";
|
|
76
|
+
return e instanceof Error && "kind" in e && e.kind === "inbound-transient-error";
|
|
77
77
|
}
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
import { fetchOne, insertOne, selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
47
47
|
import { configuredPiiSubjectKms, decryptPiiFieldValues, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
48
48
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
49
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
49
50
|
import { InboundMailAccountStatuses, InboundMailFoundationHandlers } from "./constants.js";
|
|
50
51
|
import { MAIL_ACCOUNT_PII_FIELDS, syncCursorTable } from "./entities.js";
|
|
51
52
|
import { createOAuthAccessTokenManager, usesFoundationManagedOAuth, } from "./oauth-access-token.js";
|
|
@@ -111,20 +112,27 @@ export function createInboundMailSupervisor(deps) {
|
|
|
111
112
|
});
|
|
112
113
|
const piiKms = configuredPiiSubjectKms();
|
|
113
114
|
const records = [];
|
|
114
|
-
for (const
|
|
115
|
-
const
|
|
116
|
-
? await decryptPiiFieldValues(
|
|
117
|
-
|
|
115
|
+
for (const row of rows) {
|
|
116
|
+
const decrypted = piiKms
|
|
117
|
+
? await decryptPiiFieldValues(row, MAIL_ACCOUNT_PII_FIELDS, piiKms, {
|
|
118
|
+
requestId: "inbound-mail-foundation:supervisor:list-accounts",
|
|
119
|
+
})
|
|
120
|
+
: row;
|
|
121
|
+
const decryptedAddress = decrypted["address"];
|
|
122
|
+
// Falling back to row.address would hand the ciphertext to a provider.
|
|
123
|
+
if (typeof decryptedAddress !== "string") {
|
|
124
|
+
throw new Error(`inbound-mail: address of mail account ${row.id} did not decrypt to text`);
|
|
125
|
+
}
|
|
118
126
|
records.push({
|
|
119
|
-
id: row
|
|
120
|
-
tenantId: row
|
|
121
|
-
provider: row
|
|
122
|
-
authMethod: row
|
|
123
|
-
ownerUserId: row
|
|
124
|
-
address:
|
|
125
|
-
displayName: row
|
|
127
|
+
id: row.id,
|
|
128
|
+
tenantId: row.tenantId,
|
|
129
|
+
provider: row.provider,
|
|
130
|
+
authMethod: row.authMethod,
|
|
131
|
+
ownerUserId: row.ownerUserId ?? null,
|
|
132
|
+
address: decryptedAddress,
|
|
133
|
+
displayName: row.displayName ?? "",
|
|
126
134
|
status: InboundMailAccountStatuses.active,
|
|
127
|
-
watchState: row
|
|
135
|
+
watchState: row.watchState ?? "idle",
|
|
128
136
|
});
|
|
129
137
|
}
|
|
130
138
|
return records;
|
|
@@ -140,7 +148,8 @@ export function createInboundMailSupervisor(deps) {
|
|
|
140
148
|
if (!row)
|
|
141
149
|
return null;
|
|
142
150
|
try {
|
|
143
|
-
|
|
151
|
+
const parsed = JSON.parse(row.cursor);
|
|
152
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
144
153
|
}
|
|
145
154
|
catch {
|
|
146
155
|
return null; // korrupter Cursor = wie kein Cursor → Backfill, Dedup fängt Dubletten
|
|
@@ -6,6 +6,7 @@ import { InboundAuthError, InboundCursorInvalidError, InboundTransientError, } f
|
|
|
6
6
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
7
7
|
import { legacyDateToInstant } from "@cosmicdrift/kumiko-framework/time";
|
|
8
8
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
9
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
9
10
|
import { ImapFlow } from "imapflow";
|
|
10
11
|
import { simpleParser } from "mailparser";
|
|
11
12
|
export const IMAP_MAILBOX = "INBOX";
|
|
@@ -73,10 +74,10 @@ export function mapImapError(err, host) {
|
|
|
73
74
|
// imapflow-Fehler tragen die Server-Antwort in responseText, die
|
|
74
75
|
// message ist nur "Command failed" — beides in den Match einbeziehen;
|
|
75
76
|
// authenticationFailed ist das verlässliche Flag.
|
|
76
|
-
const carrier = err;
|
|
77
|
-
const responseText = typeof carrier?.responseText === "string" ? carrier
|
|
77
|
+
const carrier = isPlainObject(err) ? err : undefined;
|
|
78
|
+
const responseText = typeof carrier?.["responseText"] === "string" ? carrier["responseText"] : "";
|
|
78
79
|
const msg = `${err instanceof Error ? err.message : String(err)} ${responseText}`.trim();
|
|
79
|
-
if (carrier?.authenticationFailed === true ||
|
|
80
|
+
if (carrier?.["authenticationFailed"] === true ||
|
|
80
81
|
/auth(entication)? failed|invalid credentials|invalid login|LOGIN failed|AUTHENTICATIONFAILED/i.test(msg)) {
|
|
81
82
|
return new InboundAuthError(`IMAP auth failed at ${host}: ${msg}`);
|
|
82
83
|
}
|