@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.
Files changed (252) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +17 -6
  3. package/packages/bundled-features/src/admin-shell/changes.json +6 -0
  4. package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
  5. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
  6. package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
  7. package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
  8. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +14 -0
  10. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  11. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  12. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  13. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  14. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  15. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  16. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  17. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +3 -12
  18. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  19. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  20. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  21. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  22. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  23. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  24. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  25. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  26. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  27. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  28. package/packages/bundled-features/src/billing-foundation/changes.json +7 -0
  29. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  30. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +38 -44
  31. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  32. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +115 -0
  33. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  34. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +30 -30
  35. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  36. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  37. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +2 -2
  38. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  39. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  40. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  41. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +2 -2
  42. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  43. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +3 -6
  44. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  45. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  46. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  47. package/packages/bundled-features/src/channel-email/email-channel.ts +122 -29
  48. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  49. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  50. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  51. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  52. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  53. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  54. package/packages/bundled-features/src/config/handlers/set.write.ts +1 -5
  55. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  56. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  57. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  58. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  59. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  60. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  61. package/packages/bundled-features/src/delivery/attachment-ref.ts +84 -0
  62. package/packages/bundled-features/src/delivery/changes.json +48 -0
  63. package/packages/bundled-features/src/delivery/channel-context.ts +129 -4
  64. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  65. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  66. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  67. package/packages/bundled-features/src/delivery/index.ts +17 -2
  68. package/packages/bundled-features/src/delivery/jobs.ts +48 -5
  69. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  70. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  71. package/packages/bundled-features/src/delivery/types.ts +28 -18
  72. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  73. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  74. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  75. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  76. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  77. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -0
  78. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  79. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  80. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  81. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  82. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  83. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  84. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  85. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  86. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  87. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  88. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  89. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  90. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  91. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  92. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +40 -25
  93. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  94. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  95. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  96. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  97. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  98. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  99. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  100. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  101. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  102. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  103. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  104. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  105. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  106. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  107. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  108. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  109. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  110. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  111. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  112. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  113. package/packages/bundled-features/src/renderer-foundation/types.ts +132 -35
  114. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  115. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  116. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  117. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  118. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  119. package/packages/bundled-features/src/shared/index.ts +6 -0
  120. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  121. package/packages/bundled-features/src/shared/mail-template-body.ts +25 -0
  122. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  123. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  124. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  125. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  126. package/packages/bundled-features/src/template-resolver/README.md +63 -7
  127. package/packages/bundled-features/src/template-resolver/api.ts +83 -8
  128. package/packages/bundled-features/src/template-resolver/changes.json +118 -0
  129. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
  130. package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
  131. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  132. package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
  133. package/packages/bundled-features/src/template-resolver/feature.ts +56 -6
  134. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  135. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  136. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +142 -0
  137. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
  138. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
  139. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
  140. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  141. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  142. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +108 -0
  143. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
  144. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -0
  145. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  146. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  147. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  148. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  149. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  150. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  151. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +106 -0
  152. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  153. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  154. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +184 -0
  155. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
  156. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
  157. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
  158. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  159. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +15 -76
  160. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
  161. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  162. package/packages/bundled-features/src/template-resolver/index.ts +100 -0
  163. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  164. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  165. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  166. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  167. package/packages/bundled-features/src/template-resolver/qualified-names.ts +30 -0
  168. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  169. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  170. package/packages/bundled-features/src/template-resolver/seeding.ts +147 -17
  171. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  172. package/packages/bundled-features/src/template-resolver/table.ts +14 -0
  173. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  174. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  175. package/packages/bundled-features/src/template-resolver/template-meta.ts +129 -0
  176. package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
  177. package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
  178. package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
  179. package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
  180. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  181. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
  182. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
  183. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
  184. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
  185. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
  186. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  187. package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
  188. package/packages/bundled-features/src/template-resolver/web/index.ts +19 -0
  189. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  190. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +169 -0
  191. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  192. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  193. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  194. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  195. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  196. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  197. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  198. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  199. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  200. package/packages/bundled-features/src/user-data-rights/changes.json +7 -0
  201. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  202. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  203. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  204. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  205. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  206. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  207. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  208. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  209. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  210. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  211. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  212. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  213. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  214. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  215. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  216. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +1 -2
  217. package/samples/apps/ui-walkthrough/package.json +2 -0
  218. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  219. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  220. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +154 -0
  221. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  222. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  223. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +354 -0
  224. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -0
  225. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  226. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  227. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  228. package/samples/apps/ui-walkthrough/src/run-config.ts +37 -1
  229. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  230. package/samples/apps/use-all-bundled/feature-manifest.json +115 -20
  231. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  232. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  233. package/samples/apps/use-all-bundled/package.json +2 -0
  234. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  235. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  236. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  237. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  238. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  239. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  240. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  241. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  242. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  243. package/samples/recipes/delivery-notifications/README.md +26 -25
  244. package/samples/recipes/delivery-notifications/package.json +2 -1
  245. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  246. package/samples/recipes/seed-migration/README.md +1 -1
  247. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  248. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  249. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  250. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  251. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  252. 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({ html: z.string(), subject: z.string(), text: z.string().optional() })
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
- "delivery attempt append and channel context filter by this job's tenantId themselves",
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(db, registry, undefined, tenantId, ctx.secrets);
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(db, registry, undefined, tenantId, ctx.secrets);
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) => (notificationType, notifyOptions) =>
12
- deliveryService.notify(
13
- notificationType,
14
- notifyOptions,
15
- user,
16
- tenantId,
17
- options.deliverQueuedInline === true ? undefined : jobDispatcher,
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 { EscapeHatchAuditSink, Registry } from "@cosmicdrift/kumiko-framework/engine";
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
- ): Promise<NotifyResult>;
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) as { token?: unknown };
416
- return typeof parsed.token === "string" ? parsed.token : undefined;
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
- if (value === null || typeof value !== "object") return false;
32
- const page = value as { pageNumber?: unknown; text?: unknown };
33
- return typeof page.pageNumber === "number" && typeof page.text === "string";
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
- typeof value === "object" &&
24
- value !== null &&
25
- typeof (value as { featureName?: unknown }).featureName === "string" &&
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
 
@@ -7,3 +7,10 @@ export {
7
7
  fileFoundationFeature,
8
8
  isFileProviderPlugin,
9
9
  } from "./feature.js";
10
+ export {
11
+ type FileRefContent,
12
+ FileRefTooLargeError,
13
+ fileContextForTenant,
14
+ fileContextFromResolver,
15
+ readFileRefContent,
16
+ } from "./read-file-ref-content.js";
@@ -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
- return new Set(raw ? (JSON.parse(raw) as string[]) : []);
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 as Node))
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 = (await unsafeReadRetrying(
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 = (await unsafeReadRetrying(
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
- )) as readonly unknown[];
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 = (await unsafeReadRetrying(
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
- )) as readonly { count: number }[];
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 = (await unsafeReadRetrying(
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
- )) as readonly { id: string; storage_key: string }[];
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 (typeof value !== "object" || value === null) return false;
12
- const storageKey = (value as { storageKey?: unknown }).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
 
@@ -95,7 +95,7 @@ export const connectAccountHandler: WriteHandlerDef = {
95
95
  authMethod: payload.authMethod,
96
96
  ownerUserId,
97
97
  displayName: payload.displayName,
98
- address: encryptedFields["address"] as string,
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,
@@ -199,11 +199,11 @@ export const ingestMessageHandler: WriteHandlerDef = {
199
199
  ownerUserId: payload.ownerUserId,
200
200
  messageIdHeader: effectiveMessageIdHeader,
201
201
  threadKey,
202
- from: encryptedMessageFields["from"] as string,
203
- to: encryptedMessageFields["to"] as string,
204
- cc: encryptedMessageFields["cc"] as string,
205
- subject: encryptedMessageFields["subject"] as string,
206
- snippet: encryptedMessageFields["snippet"] as string,
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["subject"] as string,
299
+ subject: encryptedThreadFields.subject,
300
300
  lastMessageAtIso: newLastAt,
301
301
  messageCount,
302
302
  };
@@ -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 as Record<string, unknown>, MAIL_ACCOUNT_PII_FIELDS, piiKms, {
37
+ decryptPiiFieldValues(row, MAIL_ACCOUNT_PII_FIELDS, piiKms, {
38
38
  requestId: `inbound-mail-foundation:list-accounts:${ctx.user.tenantId}`,
39
39
  }),
40
40
  ),
@@ -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 as Record<string, unknown>, INBOUND_MESSAGE_PII_FIELDS, piiKms, {
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 = (row as Record<string, unknown>)["ownerUserId"];
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");