@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.
Files changed (307) hide show
  1. package/dist/admin-shell/feature.d.ts +3 -0
  2. package/dist/admin-shell/feature.js +6 -1
  3. package/dist/agent-tools/agent-manifest.js +2 -1
  4. package/dist/agent-tools/filter-ops.d.ts +1 -1
  5. package/dist/agent-tools/filter-ops.js +1 -3
  6. package/dist/agent-tools/tool-dispatch.js +2 -1
  7. package/dist/auth-email-password/email-templates.d.ts +4 -26
  8. package/dist/auth-email-password/email-templates.js +9 -101
  9. package/dist/auth-email-password/feature.js +24 -6
  10. package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
  11. package/dist/auth-email-password/handlers/request-account-unlock.write.js +2 -4
  12. package/dist/auth-email-password/handlers/request-email-verification.write.js +2 -4
  13. package/dist/auth-email-password/handlers/request-password-reset.write.js +2 -4
  14. package/dist/auth-email-password/handlers/signup-confirm.write.js +3 -4
  15. package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -2
  16. package/dist/auth-email-password/handlers/token-request-handler.js +0 -1
  17. package/dist/auth-email-password/index.d.ts +2 -2
  18. package/dist/auth-email-password/index.js +1 -1
  19. package/dist/auth-email-password/magic-link-mail.d.ts +3 -2
  20. package/dist/auth-email-password/magic-link-mail.js +19 -14
  21. package/dist/auth-email-password/mail-notifications.d.ts +32 -0
  22. package/dist/auth-email-password/mail-notifications.js +116 -0
  23. package/dist/auth-email-password/signup-activation.d.ts +1 -1
  24. package/dist/auth-email-password/signup-activation.js +2 -3
  25. package/dist/auth-foundation/types.js +2 -8
  26. package/dist/auth-mfa/feature.js +4 -4
  27. package/dist/auth-mfa/hmac-token-codec.js +4 -4
  28. package/dist/auth-mfa/mfa-challenge-token.js +3 -3
  29. package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -3
  30. package/dist/auth-mfa/mfa-setup-token.js +8 -8
  31. package/dist/billing-foundation/constants.d.ts +3 -3
  32. package/dist/billing-foundation/constants.js +5 -4
  33. package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +32 -14
  34. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +27 -32
  35. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +3 -3
  36. package/dist/billing-foundation/consumer-protection/mail-notifications.d.ts +8 -0
  37. package/dist/billing-foundation/consumer-protection/mail-notifications.js +95 -0
  38. package/dist/billing-foundation/consumer-protection/register-consent.js +2 -0
  39. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +20 -10
  40. package/dist/billing-foundation/consumer-protection/termination-mail.js +10 -21
  41. package/dist/billing-foundation/consumer-protection/termination-notify.js +2 -2
  42. package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -3
  43. package/dist/billing-foundation/consumer-protection/termination-request.js +2 -2
  44. package/dist/billing-foundation/handlers/process-event.write.js +2 -2
  45. package/dist/billing-foundation/handlers/process-payment-event.write.js +1 -1
  46. package/dist/billing-foundation/subscription-tier-sync.js +3 -4
  47. package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -1
  48. package/dist/cap-overview/web/cap-usage-cell.js +5 -4
  49. package/dist/channel-email/email-channel.d.ts +2 -2
  50. package/dist/channel-email/email-channel.js +89 -26
  51. package/dist/channel-email/feature.js +3 -3
  52. package/dist/channel-email/index.d.ts +1 -1
  53. package/dist/channel-email/index.js +1 -1
  54. package/dist/channel-email/pii-guard.js +10 -0
  55. package/dist/channel-email/smtp-transport.js +13 -0
  56. package/dist/channel-email/types.d.ts +8 -0
  57. package/dist/config/handlers/set.write.js +1 -2
  58. package/dist/custom-fields/lib/value-schema.js +10 -11
  59. package/dist/custom-fields/wire-user-data-rights.js +7 -6
  60. package/dist/data-retention/index.d.ts +1 -0
  61. package/dist/data-retention/index.js +1 -0
  62. package/dist/data-retention/run-retention-cleanup.js +6 -6
  63. package/dist/delivery/attachment-ref.d.ts +39 -0
  64. package/dist/delivery/attachment-ref.js +64 -0
  65. package/dist/delivery/channel-context.d.ts +8 -4
  66. package/dist/delivery/channel-context.js +76 -2
  67. package/dist/delivery/constants.d.ts +1 -0
  68. package/dist/delivery/constants.js +1 -0
  69. package/dist/delivery/delivery-service.d.ts +4 -1
  70. package/dist/delivery/delivery-service.js +38 -32
  71. package/dist/delivery/feature.js +1 -1
  72. package/dist/delivery/index.d.ts +5 -2
  73. package/dist/delivery/index.js +4 -1
  74. package/dist/delivery/jobs.js +25 -5
  75. package/dist/delivery/notify-factory.js +4 -1
  76. package/dist/delivery/testing.d.ts +4 -1
  77. package/dist/delivery/types.d.ts +20 -14
  78. package/dist/delivery/unsubscribe.js +4 -1
  79. package/dist/derivatives-sharp/render.js +1 -18
  80. package/dist/document-ingest-foundation/pages.js +4 -4
  81. package/dist/feature-toggles/toggle-sync-signal.js +4 -4
  82. package/dist/file-foundation/index.d.ts +1 -0
  83. package/dist/file-foundation/index.js +1 -0
  84. package/dist/file-foundation/read-file-ref-content.d.ts +27 -0
  85. package/dist/file-foundation/read-file-ref-content.js +49 -0
  86. package/dist/folders/web/folder-manager.js +3 -2
  87. package/dist/form-draft/db/queries/cleanup.js +4 -4
  88. package/dist/form-draft/db/queries/draft-count.js +2 -2
  89. package/dist/form-draft/db/queries/owned-file-refs.js +2 -2
  90. package/dist/form-draft/release-file-refs.js +3 -2
  91. package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
  92. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +6 -6
  93. package/dist/inbound-mail-foundation/handlers/scope-visibility.d.ts +1 -1
  94. package/dist/inbound-mail-foundation/oauth-state.js +7 -7
  95. package/dist/inbound-mail-foundation/types.js +4 -4
  96. package/dist/inbound-mail-foundation/watch-supervisor.js +22 -13
  97. package/dist/inbound-provider-imap/imap-client.js +4 -3
  98. package/dist/ledger/recurring.js +1 -3
  99. package/dist/managed-pages/branding.d.ts +2 -0
  100. package/dist/managed-pages/branding.js +24 -2
  101. package/dist/managed-pages/feature.js +1 -0
  102. package/dist/managed-pages/i18n.js +1 -0
  103. package/dist/managed-pages/screens/branding-screen.js +8 -1
  104. package/dist/page-render/branding.d.ts +1 -0
  105. package/dist/page-render/branding.js +1 -0
  106. package/dist/page-render/markdown.js +1 -1
  107. package/dist/personal-access-tokens/feature.js +1 -1
  108. package/dist/personal-access-tokens/handlers/list.query.js +2 -1
  109. package/dist/personal-access-tokens/revoke-for-user.js +2 -1
  110. package/dist/renderer-foundation/api.d.ts +13 -13
  111. package/dist/renderer-foundation/api.js +1 -1
  112. package/dist/renderer-foundation/constants.js +2 -4
  113. package/dist/renderer-foundation/feature.js +7 -7
  114. package/dist/renderer-foundation/index.d.ts +2 -1
  115. package/dist/renderer-foundation/index.js +1 -0
  116. package/dist/renderer-foundation/types.d.ts +92 -32
  117. package/dist/renderer-pdf/feature.js +7 -1
  118. package/dist/sessions/db/queries/cleanup.js +2 -2
  119. package/dist/sessions/feature.js +1 -1
  120. package/dist/sessions/handlers/list.query.js +2 -1
  121. package/dist/shared/attachment-file-name.d.ts +3 -0
  122. package/dist/shared/attachment-file-name.js +35 -0
  123. package/dist/shared/index.d.ts +3 -0
  124. package/dist/shared/index.js +3 -0
  125. package/dist/shared/is-tenant-db.js +1 -1
  126. package/dist/shared/mail-template-body.d.ts +4 -0
  127. package/dist/shared/mail-template-body.js +17 -0
  128. package/dist/shared/sharp-input-limits.d.ts +6 -0
  129. package/dist/shared/sharp-input-limits.js +22 -0
  130. package/dist/shared/signup-handover.js +2 -2
  131. package/dist/tags/handlers/assign-tag.write.js +3 -1
  132. package/dist/tags/web/tag-chip.js +2 -0
  133. package/dist/template-resolver/api.d.ts +30 -2
  134. package/dist/template-resolver/api.js +40 -5
  135. package/dist/template-resolver/compile-document-body.d.ts +8 -0
  136. package/dist/template-resolver/compile-document-body.js +163 -0
  137. package/dist/template-resolver/constants.d.ts +13 -4
  138. package/dist/template-resolver/constants.js +30 -10
  139. package/dist/template-resolver/document-sanitizer.d.ts +32 -0
  140. package/dist/template-resolver/document-sanitizer.js +201 -0
  141. package/dist/template-resolver/document-schema.d.ts +21 -0
  142. package/dist/template-resolver/document-schema.js +296 -0
  143. package/dist/template-resolver/feature.d.ts +18 -0
  144. package/dist/template-resolver/feature.js +50 -6
  145. package/dist/template-resolver/handlers/bundle-import.d.ts +20 -0
  146. package/dist/template-resolver/handlers/bundle-import.js +147 -0
  147. package/dist/template-resolver/handlers/collection-set.write.js +25 -2
  148. package/dist/template-resolver/handlers/copy-locale.write.d.ts +19 -0
  149. package/dist/template-resolver/handlers/copy-locale.write.js +116 -0
  150. package/dist/template-resolver/handlers/diff-to-system.query.d.ts +22 -0
  151. package/dist/template-resolver/handlers/diff-to-system.query.js +64 -0
  152. package/dist/template-resolver/handlers/export-bundle.query.d.ts +35 -0
  153. package/dist/template-resolver/handlers/export-bundle.query.js +68 -0
  154. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
  155. package/dist/template-resolver/handlers/find-by-id.query.js +3 -3
  156. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +39 -0
  157. package/dist/template-resolver/handlers/import-bundle-system.write.js +36 -0
  158. package/dist/template-resolver/handlers/import-bundle.write.d.ts +40 -0
  159. package/dist/template-resolver/handlers/import-bundle.write.js +19 -0
  160. package/dist/template-resolver/handlers/layout-source.query.d.ts +26 -0
  161. package/dist/template-resolver/handlers/layout-source.query.js +64 -0
  162. package/dist/template-resolver/handlers/list.query.d.ts +4 -1
  163. package/dist/template-resolver/handlers/list.query.js +5 -7
  164. package/dist/template-resolver/handlers/overview.query.d.ts +59 -0
  165. package/dist/template-resolver/handlers/overview.query.js +165 -0
  166. package/dist/template-resolver/handlers/preview.query.d.ts +12 -0
  167. package/dist/template-resolver/handlers/preview.query.js +24 -0
  168. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +8 -0
  169. package/dist/template-resolver/handlers/reset-to-system.write.js +50 -0
  170. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +8 -0
  171. package/dist/template-resolver/handlers/resource-link-pdf.write.js +32 -0
  172. package/dist/template-resolver/handlers/resource-remove.write.d.ts +9 -0
  173. package/dist/template-resolver/handlers/resource-remove.write.js +44 -0
  174. package/dist/template-resolver/handlers/resource-shared.d.ts +9 -0
  175. package/dist/template-resolver/handlers/resource-shared.js +14 -0
  176. package/dist/template-resolver/handlers/resource-upload.write.d.ts +13 -0
  177. package/dist/template-resolver/handlers/resource-upload.write.js +98 -0
  178. package/dist/template-resolver/handlers/send-test.write.d.ts +11 -0
  179. package/dist/template-resolver/handlers/send-test.write.js +81 -0
  180. package/dist/template-resolver/handlers/set.write.d.ts +4 -1
  181. package/dist/template-resolver/handlers/set.write.js +25 -3
  182. package/dist/template-resolver/handlers/shared.d.ts +9 -5
  183. package/dist/template-resolver/handlers/shared.js +6 -2
  184. package/dist/template-resolver/handlers/template-gates.d.ts +36 -0
  185. package/dist/template-resolver/handlers/template-gates.js +131 -0
  186. package/dist/template-resolver/handlers/template-write.d.ts +45 -0
  187. package/dist/template-resolver/handlers/template-write.js +109 -0
  188. package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +17 -0
  189. package/dist/template-resolver/handlers/tenant-overrides.query.js +48 -0
  190. package/dist/template-resolver/handlers/toggle-status.write.d.ts +12 -0
  191. package/dist/template-resolver/handlers/toggle-status.write.js +27 -11
  192. package/dist/template-resolver/handlers/upsert-system.write.d.ts +5 -8
  193. package/dist/template-resolver/handlers/upsert-system.write.js +14 -59
  194. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +5 -8
  195. package/dist/template-resolver/handlers/upsert-tenant.write.js +10 -61
  196. package/dist/template-resolver/handlers/usage.query.d.ts +17 -0
  197. package/dist/template-resolver/handlers/usage.query.js +15 -0
  198. package/dist/template-resolver/handlers/validate.query.d.ts +74 -0
  199. package/dist/template-resolver/handlers/validate.query.js +80 -0
  200. package/dist/template-resolver/index.d.ts +18 -1
  201. package/dist/template-resolver/index.js +16 -1
  202. package/dist/template-resolver/layout-regions.d.ts +23 -0
  203. package/dist/template-resolver/layout-regions.js +125 -0
  204. package/dist/template-resolver/linked-resources.d.ts +17 -0
  205. package/dist/template-resolver/linked-resources.js +75 -0
  206. package/dist/template-resolver/preview-display-html.d.ts +7 -0
  207. package/dist/template-resolver/preview-display-html.js +27 -0
  208. package/dist/template-resolver/print-document.d.ts +1 -0
  209. package/dist/template-resolver/print-document.js +55 -0
  210. package/dist/template-resolver/qualified-names.d.ts +30 -0
  211. package/dist/template-resolver/qualified-names.js +30 -0
  212. package/dist/template-resolver/record-mapping.d.ts +2 -0
  213. package/dist/template-resolver/record-mapping.js +21 -0
  214. package/dist/template-resolver/resource-image.d.ts +15 -0
  215. package/dist/template-resolver/resource-image.js +34 -0
  216. package/dist/template-resolver/seeding.d.ts +14 -1
  217. package/dist/template-resolver/seeding.js +105 -15
  218. package/dist/template-resolver/store-template-resource.d.ts +64 -0
  219. package/dist/template-resolver/store-template-resource.js +264 -0
  220. package/dist/template-resolver/table.d.ts +47 -8
  221. package/dist/template-resolver/table.js +11 -0
  222. package/dist/template-resolver/template-bundle.d.ts +36 -0
  223. package/dist/template-resolver/template-bundle.js +60 -0
  224. package/dist/template-resolver/template-diff.d.ts +7 -0
  225. package/dist/template-resolver/template-diff.js +68 -0
  226. package/dist/template-resolver/template-meta.d.ts +60 -0
  227. package/dist/template-resolver/template-meta.js +101 -0
  228. package/dist/template-resolver/template-preview.d.ts +86 -0
  229. package/dist/template-resolver/template-preview.js +374 -0
  230. package/dist/template-resolver/template-resources.d.ts +14 -0
  231. package/dist/template-resolver/template-resources.js +42 -0
  232. package/dist/template-resolver/template-usage.d.ts +12 -0
  233. package/dist/template-resolver/template-usage.js +30 -0
  234. package/dist/template-resolver/template-validation.d.ts +22 -0
  235. package/dist/template-resolver/template-validation.js +172 -0
  236. package/dist/template-resolver/testing.js +5 -5
  237. package/dist/template-resolver/user-content-table.d.ts +14 -8
  238. package/dist/template-resolver/web/client-plugin.d.ts +1 -0
  239. package/dist/template-resolver/web/client-plugin.js +9 -6
  240. package/dist/template-resolver/web/document-editor-extensions.d.ts +15 -0
  241. package/dist/template-resolver/web/document-editor-extensions.js +107 -0
  242. package/dist/template-resolver/web/document-editor.d.ts +3 -0
  243. package/dist/template-resolver/web/document-editor.js +11 -0
  244. package/dist/template-resolver/web/document-tiptap-editor.d.ts +3 -0
  245. package/dist/template-resolver/web/document-tiptap-editor.js +376 -0
  246. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +33 -0
  247. package/dist/template-resolver/web/document-toolbar-blocks.js +15 -0
  248. package/dist/template-resolver/web/editor-handle.d.ts +3 -0
  249. package/dist/template-resolver/web/editor-handle.js +24 -0
  250. package/dist/template-resolver/web/i18n.js +76 -0
  251. package/dist/template-resolver/web/index.d.ts +4 -0
  252. package/dist/template-resolver/web/index.js +4 -0
  253. package/dist/template-resolver/web/placeholder-line-editor-lazy.d.ts +4 -0
  254. package/dist/template-resolver/web/placeholder-line-editor-lazy.js +16 -0
  255. package/dist/template-resolver/web/placeholder-line-editor.d.ts +15 -0
  256. package/dist/template-resolver/web/placeholder-line-editor.js +94 -0
  257. package/dist/template-resolver/web/placeholder-text.d.ts +4 -0
  258. package/dist/template-resolver/web/placeholder-text.js +34 -0
  259. package/dist/template-resolver/web/use-stable-by-key.d.ts +1 -0
  260. package/dist/template-resolver/web/use-stable-by-key.js +13 -0
  261. package/dist/tenant/handlers/team-list.query.js +3 -2
  262. package/dist/tenant/seeding.js +31 -6
  263. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
  264. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +2 -1
  265. package/dist/user/db/queries/stream-tenant-backfill.js +4 -4
  266. package/dist/user/handlers/detail.query.js +2 -1
  267. package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
  268. package/dist/user-data-rights/email-templates.d.ts +1 -29
  269. package/dist/user-data-rights/email-templates.js +5 -106
  270. package/dist/user-data-rights/feature.js +53 -35
  271. package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
  272. package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
  273. package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
  274. package/dist/user-data-rights/lib/default-mailers.d.ts +15 -7
  275. package/dist/user-data-rights/lib/default-mailers.js +100 -48
  276. package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
  277. package/dist/user-data-rights/mail-notifications.d.ts +13 -0
  278. package/dist/user-data-rights/mail-notifications.js +118 -0
  279. package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
  280. package/dist/user-profile/web/profile-screen.js +2 -1
  281. package/dist/waitlist/feature.js +10 -0
  282. package/dist/waitlist/handlers/submit.write.js +3 -4
  283. package/dist/waitlist/mail.d.ts +19 -6
  284. package/dist/waitlist/mail.js +50 -14
  285. package/package.json +28 -17
  286. package/src/admin-shell/changes.json +6 -0
  287. package/src/auth-email-password/changes.json +14 -0
  288. package/src/billing-foundation/changes.json +7 -0
  289. package/src/channel-email/changes.json +19 -0
  290. package/src/data-retention/changes.json +6 -0
  291. package/src/delivery/changes.json +48 -0
  292. package/src/managed-pages/changes.json +6 -0
  293. package/src/renderer-foundation/changes.json +39 -0
  294. package/src/template-resolver/changes.json +118 -0
  295. package/src/user-data-rights/changes.json +7 -0
  296. package/src/waitlist/changes.json +7 -0
  297. package/dist/renderer-simple/feature.d.ts +0 -10
  298. package/dist/renderer-simple/feature.js +0 -42
  299. package/dist/renderer-simple/index.d.ts +0 -3
  300. package/dist/renderer-simple/index.js +0 -2
  301. package/dist/renderer-simple/mail-blocks.d.ts +0 -21
  302. package/dist/renderer-simple/mail-blocks.js +0 -107
  303. package/dist/renderer-simple/resolve-variables.d.ts +0 -7
  304. package/dist/renderer-simple/resolve-variables.js +0 -47
  305. package/dist/renderer-simple/simple-renderer.d.ts +0 -24
  306. package/dist/renderer-simple/simple-renderer.js +0 -200
  307. package/src/renderer-simple/changes.json +0 -27
@@ -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({ html: z.string(), subject: z.string(), text: z.string().optional() })
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("delivery attempt append and channel context filter by this job's tenantId themselves"); // @cast-boundary db-operator — DbRunner narrows to DbConnection, jobs never run inside a DbTx
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) => (notificationType, notifyOptions) => deliveryService.notify(notificationType, notifyOptions, user, tenantId, options.deliverQueuedInline === true ? undefined : 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.
@@ -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, NotifyResult, Registry, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
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<NotifyResult>;
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.token === "string" ? parsed.token : undefined;
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
- if (value === null || typeof value !== "object")
29
- return false;
30
- const page = value;
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 (typeof value === "object" &&
7
- value !== null &&
8
- typeof value.featureName === "string" &&
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
- return new Set(raw ? JSON.parse(raw) : []);
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 = (await unsafeReadRetrying(db, `SELECT "id", "tenant_id", "owner_id", "draft_key", "draft", "inserted_at" FROM "read_form_drafts"
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 = (await unsafeReadRetrying(db, `SELECT 1 FROM "read_form_drafts"
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 = (await unsafeReadRetrying(db, `SELECT count(*)::int AS "count" FROM "read_form_drafts"
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 = (await unsafeReadRetrying(db, `SELECT "id", "storage_key" FROM "file_refs"
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 (typeof value !== "object" || value === null)
10
+ if (!isPlainObject(value))
10
11
  return false;
11
- const storageKey = value.storageKey;
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["address"],
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["from"],
169
- to: encryptedMessageFields["to"],
170
- cc: encryptedMessageFields["cc"],
171
- subject: encryptedMessageFields["subject"],
172
- snippet: encryptedMessageFields["snippet"],
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["subject"],
249
+ subject: encryptedThreadFields.subject,
250
250
  lastMessageAtIso: newLastAt,
251
251
  messageCount,
252
252
  };
@@ -1,4 +1,4 @@
1
- export declare function isVisibleToCaller(row: unknown, user: {
1
+ export declare function isVisibleToCaller(row: Readonly<Record<string, unknown>>, user: {
2
2
  readonly id: string;
3
3
  readonly roles: readonly string[];
4
4
  }): boolean;
@@ -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 (typeof v !== "object" || v === null)
64
+ if (!isPlainObject(v))
64
65
  return false;
65
- const o = v;
66
- return (typeof o["tenantId"] === "string" &&
67
- (o["ownerUserId"] === null || typeof o["ownerUserId"] === "string") &&
68
- typeof o["providerKey"] === "string" &&
69
- typeof o["mailbox"] === "string" &&
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 raw of rows) {
115
- const row = piiKms
116
- ? await decryptPiiFieldValues(raw, MAIL_ACCOUNT_PII_FIELDS, piiKms, { requestId: "inbound-mail-foundation:supervisor:list-accounts" })
117
- : raw;
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["id"],
120
- tenantId: row["tenantId"],
121
- provider: row["provider"],
122
- authMethod: row["authMethod"],
123
- ownerUserId: row["ownerUserId"] ?? null,
124
- address: row["address"],
125
- displayName: row["displayName"] ?? "",
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["watchState"] ?? "idle",
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
- return JSON.parse(row.cursor); // @cast-boundary eigene JSON-Persistenz
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.responseText : "";
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
  }