@cosmicdrift/kumiko-samples 0.362.0 → 0.364.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 (321) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +23 -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/audit/changes.json +6 -0
  10. package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
  11. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
  12. package/packages/bundled-features/src/auth-email-password/changes.json +51 -0
  13. package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
  14. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  15. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  16. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
  17. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  18. package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
  19. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  20. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  21. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  22. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  23. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
  24. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +5 -13
  25. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  26. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  27. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  28. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  29. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
  30. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  31. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  32. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  33. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  34. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  35. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  36. package/packages/bundled-features/src/billing-foundation/changes.json +43 -0
  37. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  38. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +90 -44
  39. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
  40. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  41. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +118 -0
  42. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  43. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
  44. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +39 -33
  45. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  46. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  47. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
  48. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
  49. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +36 -20
  50. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
  51. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
  52. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  53. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
  54. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  55. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  56. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +12 -6
  57. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  58. package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
  59. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +20 -11
  60. package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
  61. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
  62. package/packages/bundled-features/src/cap-counter/changes.json +6 -0
  63. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
  64. package/packages/bundled-features/src/cap-counter/index.ts +1 -0
  65. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
  66. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  67. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  68. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  69. package/packages/bundled-features/src/channel-email/email-channel.ts +135 -29
  70. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  71. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  72. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  73. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  74. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  75. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  76. package/packages/bundled-features/src/config/changes.json +6 -0
  77. package/packages/bundled-features/src/config/handlers/set.write.ts +3 -7
  78. package/packages/bundled-features/src/config/resolver.ts +15 -12
  79. package/packages/bundled-features/src/config/write-helpers.ts +23 -0
  80. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  81. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
  82. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  83. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  84. package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
  85. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  86. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  87. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  88. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  89. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  90. package/packages/bundled-features/src/delivery/attachment-ref.ts +105 -0
  91. package/packages/bundled-features/src/delivery/changes.json +66 -0
  92. package/packages/bundled-features/src/delivery/channel-context.ts +152 -4
  93. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  94. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  95. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  96. package/packages/bundled-features/src/delivery/index.ts +19 -2
  97. package/packages/bundled-features/src/delivery/jobs.ts +97 -17
  98. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  99. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  100. package/packages/bundled-features/src/delivery/types.ts +34 -18
  101. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  102. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  103. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  104. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  105. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  106. package/packages/bundled-features/src/file-foundation/changes.json +6 -0
  107. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  108. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +89 -0
  109. package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
  110. package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
  111. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  112. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  113. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  114. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  115. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  116. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
  117. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  118. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  119. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  120. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  121. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  122. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
  123. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  124. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  125. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  126. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  127. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +42 -25
  128. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  129. package/packages/bundled-features/src/jobs/changes.json +19 -0
  130. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
  131. package/packages/bundled-features/src/jobs/feature.ts +7 -4
  132. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  133. package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
  134. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
  135. package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
  136. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
  137. package/packages/bundled-features/src/ledger/changes.json +7 -0
  138. package/packages/bundled-features/src/ledger/entity.ts +3 -1
  139. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  140. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  141. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  142. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  143. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  144. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  145. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  146. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  147. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  148. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  149. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  150. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  151. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  152. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  153. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  154. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  155. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  156. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  157. package/packages/bundled-features/src/renderer-foundation/types.ts +134 -35
  158. package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
  159. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  160. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
  161. package/packages/bundled-features/src/secrets/changes.json +6 -0
  162. package/packages/bundled-features/src/secrets/feature.ts +4 -3
  163. package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
  164. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  165. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  166. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  167. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  168. package/packages/bundled-features/src/shared/changes.json +6 -0
  169. package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
  170. package/packages/bundled-features/src/shared/index.ts +6 -0
  171. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  172. package/packages/bundled-features/src/shared/mail-template-body.ts +42 -0
  173. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  174. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  175. package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
  176. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
  177. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
  178. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  179. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  180. package/packages/bundled-features/src/template-resolver/README.md +66 -8
  181. package/packages/bundled-features/src/template-resolver/api.ts +85 -9
  182. package/packages/bundled-features/src/template-resolver/changes.json +186 -0
  183. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +278 -0
  184. package/packages/bundled-features/src/template-resolver/constants.ts +49 -11
  185. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  186. package/packages/bundled-features/src/template-resolver/document-schema.ts +345 -0
  187. package/packages/bundled-features/src/template-resolver/feature.ts +59 -6
  188. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  189. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  190. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +160 -0
  191. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +33 -0
  192. package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
  193. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +138 -0
  194. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -6
  195. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  196. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  197. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +151 -0
  198. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +19 -18
  199. package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
  200. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +323 -0
  201. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  202. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  203. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  204. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  205. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  206. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  207. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +135 -0
  208. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  209. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  210. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +196 -0
  211. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +233 -0
  212. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +137 -0
  213. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +91 -17
  214. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  215. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +16 -77
  216. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +31 -0
  217. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  218. package/packages/bundled-features/src/template-resolver/index.ts +111 -0
  219. package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
  220. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  221. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  222. package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
  223. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  224. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  225. package/packages/bundled-features/src/template-resolver/qualified-names.ts +34 -0
  226. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  227. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  228. package/packages/bundled-features/src/template-resolver/seeding.ts +171 -20
  229. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  230. package/packages/bundled-features/src/template-resolver/table.ts +27 -1
  231. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  232. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  233. package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
  234. package/packages/bundled-features/src/template-resolver/template-meta.ts +192 -0
  235. package/packages/bundled-features/src/template-resolver/template-preview.ts +676 -0
  236. package/packages/bundled-features/src/template-resolver/template-resources.ts +114 -0
  237. package/packages/bundled-features/src/template-resolver/template-usage.ts +68 -0
  238. package/packages/bundled-features/src/template-resolver/template-validation.ts +230 -0
  239. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  240. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +20 -13
  241. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +215 -0
  242. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +37 -0
  243. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1065 -0
  244. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +72 -0
  245. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  246. package/packages/bundled-features/src/template-resolver/web/i18n.ts +121 -0
  247. package/packages/bundled-features/src/template-resolver/web/index.ts +26 -0
  248. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  249. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +176 -0
  250. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  251. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  252. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  253. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  254. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  255. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  256. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  257. package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
  258. package/packages/bundled-features/src/user/changes.json +6 -0
  259. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  260. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  261. package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
  262. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  263. package/packages/bundled-features/src/user/seeding.ts +19 -5
  264. package/packages/bundled-features/src/user-data-rights/changes.json +13 -0
  265. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  266. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  267. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  268. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  269. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  270. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  271. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  272. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  273. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  274. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  275. package/packages/bundled-features/src/user-profile/changes.json +1 -1
  276. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  277. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  278. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  279. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  280. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  281. package/packages/bundled-features/src/waitlist/options.ts +1 -1
  282. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  283. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +3 -3
  284. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
  285. package/samples/apps/ui-walkthrough/package.json +11 -0
  286. package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
  287. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  288. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  289. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +239 -0
  290. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  291. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  292. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +570 -0
  293. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +72 -0
  294. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  295. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  296. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  297. package/samples/apps/ui-walkthrough/src/run-config.ts +41 -1
  298. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  299. package/samples/apps/use-all-bundled/feature-manifest.json +118 -21
  300. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  301. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  302. package/samples/apps/use-all-bundled/package.json +2 -0
  303. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  304. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  305. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  306. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  307. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  308. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  309. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  310. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  311. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  312. package/samples/recipes/delivery-notifications/README.md +26 -25
  313. package/samples/recipes/delivery-notifications/package.json +2 -1
  314. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  315. package/samples/recipes/seed-migration/README.md +1 -1
  316. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  317. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  318. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  319. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  320. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  321. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { createTransport, type Transporter } from "nodemailer";
20
20
  import addressparser from "nodemailer/lib/addressparser";
21
- import type { EmailMessage, EmailTransport } from "./types.js";
21
+ import type { EmailAttachment, EmailMessage, EmailTransport } from "./types.js";
22
22
 
23
23
  export type SmtpTransportOptions = {
24
24
  /** SMTP-Server-Host (z.B. "smtp.gmail.com", "in-v3.mailjet.com",
@@ -74,6 +74,17 @@ function fromHeader(
74
74
  return parsed?.address ? { name, address: parsed.address } : address;
75
75
  }
76
76
 
77
+ function toNodemailerAttachment(attachment: EmailAttachment) {
78
+ const base = {
79
+ filename: attachment.fileName,
80
+ content: Buffer.from(attachment.content),
81
+ contentType: attachment.contentType,
82
+ };
83
+ return attachment.kind === "inline"
84
+ ? { ...base, cid: attachment.cid, contentDisposition: "inline" as const }
85
+ : base;
86
+ }
87
+
77
88
  export function createSmtpTransport(options: SmtpTransportOptions): EmailTransport {
78
89
  const transporter: Transporter = createTransport({
79
90
  host: options.host,
@@ -95,6 +106,9 @@ export function createSmtpTransport(options: SmtpTransportOptions): EmailTranspo
95
106
  ...(message.text !== undefined && { text: message.text }),
96
107
  ...(message.replyTo && { replyTo: message.replyTo }),
97
108
  ...(message.headers && { headers: message.headers }),
109
+ ...(message.attachments && {
110
+ attachments: message.attachments.map(toNodemailerAttachment),
111
+ }),
98
112
  });
99
113
  },
100
114
  };
@@ -1,3 +1,24 @@
1
+ import type {
2
+ EmailAttachmentRef,
3
+ FileAttachmentRef,
4
+ InlineAttachmentRef,
5
+ RenderedPdfAttachmentRef,
6
+ } from "../delivery/index.js";
7
+
8
+ export type { EmailAttachmentRef };
9
+
10
+ // What a transport receives: the reference plus the bytes the send job read. A rendered
11
+ // PDF keeps no variables here, they may be personal data and the transport has no use for them.
12
+ export type EmailAttachment = (
13
+ | InlineAttachmentRef
14
+ | FileAttachmentRef
15
+ | Pick<RenderedPdfAttachmentRef, "kind" | "templateSlug">
16
+ ) & {
17
+ readonly content: Uint8Array;
18
+ readonly contentType: string;
19
+ readonly fileName: string;
20
+ };
21
+
1
22
  // Transport interface — SMTP in prod, InMemory in tests
2
23
  export type EmailMessage = {
3
24
  readonly to: string;
@@ -19,6 +40,8 @@ export type EmailMessage = {
19
40
  // Extra RFC-5322 headers — In-Reply-To / References thread a reply into the
20
41
  // conversation it answers.
21
42
  readonly headers?: Readonly<Record<string, string>>;
43
+ // Refs travel through jobs; the bytes are only read at send time.
44
+ readonly attachments?: readonly EmailAttachment[];
22
45
  };
23
46
 
24
47
  export type EmailTransport = {
@@ -20,7 +20,7 @@ export const markReadWrite = defineWriteHandler({
20
20
  description:
21
21
  "Marks one in-app message of the calling user as read by its numeric id, failing with not-found when no such message belongs to the caller; use it when a user opens a single notification.",
22
22
  handler: async (event, ctx) => {
23
- const rows = await updateMany(
23
+ const rows = await updateMany<{ id: number }>(
24
24
  ctx.db,
25
25
  inAppMessagesTable,
26
26
  { isRead: true, readAt: Temporal.Now.instant() },
@@ -29,6 +29,6 @@ export const markReadWrite = defineWriteHandler({
29
29
  if (rows.length === 0) {
30
30
  return writeFailure(new NotFoundError("inAppMessage", event.payload.id));
31
31
  }
32
- return { isSuccess: true, data: { id: (rows[0] as { id: number } | undefined)?.id } };
32
+ return { isSuccess: true, data: { id: rows[0]?.id } };
33
33
  },
34
34
  });
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "Config key validate() receives a db scoped to the written tenant",
6
+ "detail": "A SystemAdmin writing a config value for another tenant previously handed the validator a system-mode db that could read every tenant. `ConfigValidateContext.db` is now a `TenantDb` locked to the tenant the value is written for. The validator runs inside the write transaction, so a slow query there should set its own timeout."
7
+ },
2
8
  {
3
9
  "version": "0.338.0",
4
10
  "type": "improvement",
@@ -4,7 +4,7 @@ import {
4
4
  encryptPiiValueForSubject,
5
5
  type KmsContext,
6
6
  } from "@cosmicdrift/kumiko-framework/crypto";
7
- import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
7
+ import { createEventStoreExecutor, scopeTenantDbToTenant } from "@cosmicdrift/kumiko-framework/db";
8
8
  import {
9
9
  ConfigScopes,
10
10
  defineWriteHandler,
@@ -111,7 +111,7 @@ export const setWrite = defineWriteHandler({
111
111
  scope,
112
112
  tenantId,
113
113
  userId,
114
- db,
114
+ db: scopeTenantDbToTenant(db, tenantId),
115
115
  });
116
116
  } catch (error) {
117
117
  if (isKumikoError(error)) return writeFailure(error);
@@ -164,11 +164,7 @@ export const setWrite = defineWriteHandler({
164
164
  });
165
165
  }
166
166
  // scope="system" was already rejected above — narrow accordingly.
167
- const subject = resolvePiiSubject(
168
- scope as Exclude<typeof scope, "system">, // @cast-boundary runtime-checked above
169
- tenantId,
170
- userId,
171
- );
167
+ const subject = resolvePiiSubject(scope as Exclude<typeof scope, "system">, tenantId, userId);
172
168
  const kmsCtx: KmsContext = {
173
169
  requestId: requestContext.get()?.requestId ?? "config:write:set",
174
170
  tenantId,
@@ -119,6 +119,19 @@ async function decryptPiiEncrypted(raw: string, qualifiedKey: string): Promise<s
119
119
  return decryptPiiValueForSubject(kms, raw, kmsCtx, qualifiedKey);
120
120
  }
121
121
 
122
+ // Plaintext form of a stored config_values.value for every read path (resolver,
123
+ // bootstrap idempotency check).
124
+ export async function decryptStoredValue(
125
+ raw: string,
126
+ keyDef: ConfigKeyDefinition,
127
+ cipher: EnvelopeCipher | undefined,
128
+ qualifiedKey: string,
129
+ ): Promise<string> {
130
+ if (keyDef.encrypted) return decryptEncrypted(cipher, raw, qualifiedKey);
131
+ if (keyDef.piiEncrypted) return decryptPiiEncrypted(raw, qualifiedKey);
132
+ return raw;
133
+ }
134
+
122
135
  // backing="secrets" keys store their value in the secrets store (flat per
123
136
  // (tenant,key) at SYSTEM_TENANT_ID), not in config_values. Both read paths
124
137
  // (getWithSource + buildCascade) route the system rung here. Missing reader =
@@ -230,12 +243,7 @@ async function buildCascade(
230
243
 
231
244
  const row = await fetchRow(lookup.tenantId, lookup.userId);
232
245
  if (row?.value !== null && row?.value !== undefined) {
233
- let raw = row.value;
234
- if (keyDef.encrypted) {
235
- raw = await decryptEncrypted(cipher, raw, qualifiedKey);
236
- } else if (keyDef.piiEncrypted) {
237
- raw = await decryptPiiEncrypted(raw, qualifiedKey);
238
- }
246
+ const raw = await decryptStoredValue(row.value, keyDef, cipher, qualifiedKey);
239
247
  if (activeIndex === -1) activeIndex = levels.length;
240
248
  levels.push({
241
249
  label: lookup.label,
@@ -409,12 +417,7 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
409
417
  for (const lookup of lookups) {
410
418
  const row = await findRow(qualifiedKey, lookup.tenantId, lookup.userId, db);
411
419
  if (row?.value !== null && row?.value !== undefined) {
412
- let raw = row.value;
413
- if (keyDef.encrypted) {
414
- raw = await decryptEncrypted(cipher, raw, qualifiedKey);
415
- } else if (keyDef.piiEncrypted) {
416
- raw = await decryptPiiEncrypted(raw, qualifiedKey);
417
- }
420
+ const raw = await decryptStoredValue(row.value, keyDef, cipher, qualifiedKey);
418
421
  return { value: deserializeValue(raw, keyDef.type), source: lookup.source };
419
422
  }
420
423
  }
@@ -28,8 +28,10 @@ import {
28
28
  type WriteFailure,
29
29
  writeFailure,
30
30
  } from "@cosmicdrift/kumiko-framework/errors";
31
+ import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
31
32
  import { assertUnreachable } from "@cosmicdrift/kumiko-framework/utils";
32
33
  import { ConfigErrors } from "./constants.js";
34
+ import { decryptStoredValue } from "./resolver.js";
33
35
  import { configValuesTable } from "./table.js";
34
36
 
35
37
  export type ConfigRowLookup = {
@@ -62,6 +64,27 @@ export async function findConfigRow(
62
64
  };
63
65
  }
64
66
 
67
+ // Idempotency check for declarative writers: does the stored row already hold
68
+ // `serializedValue` (JSON of the plaintext)? Encrypted rows are decrypted
69
+ // first, since their ciphertext differs on every encrypt.
70
+ export async function configRowHoldsValue(
71
+ row: ConfigRowLookup | null,
72
+ keyDef: ConfigKeyDefinition | undefined,
73
+ cipher: EnvelopeCipher | undefined,
74
+ qualifiedKey: string,
75
+ serializedValue: string,
76
+ ): Promise<boolean> {
77
+ if (row?.value === null || row?.value === undefined) return false;
78
+ if (!keyDef?.encrypted && !keyDef?.piiEncrypted) return row.value === serializedValue;
79
+ if (!keyDef) return false;
80
+ try {
81
+ return (await decryptStoredValue(row.value, keyDef, cipher, qualifiedKey)) === serializedValue;
82
+ } catch {
83
+ // Unreadable (rotated key, missing cipher/KMS): the caller rewrites the row, which is the repair.
84
+ return false;
85
+ }
86
+ }
87
+
65
88
  // Three-stage pre-write gate that set + reset both need: resolve the key
66
89
  // definition (404 when unknown), check the user's roles can write it (403),
67
90
  // and map the target scope to the (tenantId | null, userId | null) pair the
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "forget-subject keeps the user status flip and PAT revocation when the sweep fails",
6
+ "detail": "The user is set to Deleted and its personal access tokens are revoked right after the key erase, in their own transaction on the outside-transaction db. A failing blind-index or search sweep still makes the handler throw so the cleanup can be retried, but it no longer rolls the login door back. Without an outside-transaction db the previous in-transaction savepoint path is used. A failure while closing the login door of an existing user now fails the forget (retry is safe) instead of passing silently; subjects without a user row are skipped."
7
+ },
2
8
  {
3
9
  "version": "0.357.0",
4
10
  "type": "improvement",
@@ -1,6 +1,11 @@
1
1
  import { requestContext } from "@cosmicdrift/kumiko-framework/api";
2
2
  import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
3
- import { runInSavepointIfSupported } from "@cosmicdrift/kumiko-framework/bun-db";
3
+ import {
4
+ fetchOne,
5
+ runInSavepointIfSupported,
6
+ runnerCanBeginTransaction,
7
+ transaction,
8
+ } from "@cosmicdrift/kumiko-framework/bun-db";
4
9
  import {
5
10
  computeBlindIndex,
6
11
  configuredBlindIndexKey,
@@ -42,7 +47,7 @@ import * as z from "zod";
42
47
  import { resolveRetentionPolicyForTenant } from "../../data-retention/index.js";
43
48
  import { eraseNoteBodiesReachingSubject } from "../../notes-history-user-data/index.js";
44
49
  import { revokeAllPatTokensForUser } from "../../personal-access-tokens/index.js";
45
- import { USER_STATUS } from "../../user/index.js";
50
+ import { USER_STATUS, userTable } from "../../user/index.js";
46
51
  import {
47
52
  denyIfTargetOutsideAdminTenant,
48
53
  isSystemAdminActor,
@@ -337,25 +342,33 @@ async function sweepDerivedSubjectData(
337
342
  }
338
343
  }
339
344
 
340
- // Graceful fail: email-subscribers and other non-user entities may use
341
- // user-style subject keys without having an actual user row — the key erase is
342
- // the important part for GDPR compliance; the lifecycle update is best-effort for
343
- // real users. The savepoint confines the failure so the handler transaction
344
- // stays usable for the audit event.
345
- async function closeUserLoginDoorBestEffort(
345
+ // Email subscribers and other non-user entities use user-style subject keys
346
+ // without a read_users row — the key erase is the GDPR-relevant part, so no row
347
+ // means nothing to close. For an existing row, failures propagate: the handler
348
+ // throws and the retry re-runs the (idempotent) erase plus the door close. With
349
+ // an outside-transaction runner that can open its own transaction, the door
350
+ // closes there and stays committed when the handler throws afterwards (sweep
351
+ // error). Otherwise a savepoint confines a failure to the handler transaction.
352
+ async function closeUserLoginDoor(
346
353
  ctx: HandlerContext,
347
354
  userId: string,
348
- lifecycleRunner: DbRunner,
355
+ outsideRunner: DbRunner | undefined,
356
+ handlerTxRunner: DbRunner,
349
357
  ): Promise<void> {
350
- try {
351
- await runInSavepointIfSupported(lifecycleRunner, async (userLifecycleRunner) => {
352
- await updateUserLifecycle(userLifecycleRunner, userId, { status: USER_STATUS.Deleted });
353
- if (ctx.registry.features.has("personal-access-tokens")) {
354
- await revokeAllPatTokensForUser(userLifecycleRunner, userId);
355
- }
356
- });
357
- } catch {
358
- // skip: no user row for this subject key (e.g. email subscribers); key erase and sweeps already ran.
358
+ const userRow = await fetchOne(handlerTxRunner, userTable, { id: userId });
359
+ // skip: no user row for this subject key (e.g. email subscribers); there is no door to close.
360
+ if (!userRow) return;
361
+
362
+ const closeDoor = async (runner: DbRunner): Promise<void> => {
363
+ await updateUserLifecycle(runner, userId, { status: USER_STATUS.Deleted });
364
+ if (ctx.registry.features.has("personal-access-tokens")) {
365
+ await revokeAllPatTokensForUser(runner, userId);
366
+ }
367
+ };
368
+ if (outsideRunner && runnerCanBeginTransaction(outsideRunner)) {
369
+ await transaction(outsideRunner, closeDoor);
370
+ } else {
371
+ await runInSavepointIfSupported(handlerTxRunner, closeDoor);
359
372
  }
360
373
  }
361
374
 
@@ -490,14 +503,6 @@ export const forgetSubjectWrite = defineWriteHandler({
490
503
  eraseReason: event.payload.reason,
491
504
  });
492
505
 
493
- const cleanupError = await sweepDerivedSubjectData(
494
- ctx,
495
- subject,
496
- subjectKey,
497
- ctx.db.unsafeRaw(),
498
- { requestId, erasedBy: event.user.id },
499
- );
500
-
501
506
  // User subject: close the login door. DEK-erase makes the passwordHash
502
507
  // ciphertext unreadable, but status + PATs are standalone credentials —
503
508
  // the PAT resolver only checks revokedAt/expiresAt, NOT user.status
@@ -513,10 +518,26 @@ export const forgetSubjectWrite = defineWriteHandler({
513
518
  // crash recovery is safe. User-feature guard: without the user feature
514
519
  // read_users doesn't exist (crypto-only stack). Tenant subjects have no
515
520
  // credentials.
521
+ // Runs before the sweep and in its own transaction: a failing sweep rolls the
522
+ // handler transaction back, and the handler tx holds no lock on this user's
523
+ // read_users row yet (only read gates ran), so there is no lock conflict.
516
524
  if (raw.kind === "user" && ctx.registry.features.has("user")) {
517
- await closeUserLoginDoorBestEffort(ctx, raw.userId, ctx.db.unsafeRaw());
525
+ await closeUserLoginDoor(
526
+ ctx,
527
+ raw.userId,
528
+ ctx.dbOutsideTransaction?.unsafeRaw(),
529
+ ctx.db.unsafeRaw(),
530
+ );
518
531
  }
519
532
 
533
+ const cleanupError = await sweepDerivedSubjectData(
534
+ ctx,
535
+ subject,
536
+ subjectKey,
537
+ ctx.db.unsafeRaw(),
538
+ { requestId, erasedBy: event.user.id },
539
+ );
540
+
520
541
  await appendSubjectForgotten(
521
542
  ctx,
522
543
  event,
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "update-tenant-field no longer hides `type` from the handler schema",
6
+ "detail": "The `type` field is part of the documented payload; the 422 check against changing the stored type remains the protection."
7
+ },
2
8
  {
3
9
  "version": "0.359.0",
4
10
  "type": "improvement",
@@ -31,9 +31,9 @@ export async function selectFieldDefinitionsForEntity(
31
31
  entityName: string,
32
32
  tenantId: string,
33
33
  ): Promise<readonly { field_key: string; serialized_field: unknown }[]> {
34
- return unsafeReadRetrying(
34
+ return unsafeReadRetrying<{ field_key: string; serialized_field: unknown }>(
35
35
  db,
36
36
  "SELECT field_key, serialized_field FROM read_custom_field_definitions WHERE entity_name = $1 AND tenant_id = $2",
37
37
  [entityName, tenantId],
38
- ) as Promise<readonly { field_key: string; serialized_field: unknown }[]>;
38
+ );
39
39
  }
@@ -46,7 +46,6 @@ export function createUpdateTenantFieldHandler(
46
46
  access: { roles: opts.roles ?? DEFAULT_FIELD_DEFINITION_WRITE_ROLES },
47
47
  description:
48
48
  "Replaces a tenant-owned custom-field definition with a complete new spec; serializedField.type may be omitted, in which case the stored type applies, and a different type is refused. Use it to relabel or re-constrain an existing field rather than to create or remove one.",
49
- excludedFields: ["type"],
50
49
  handler: async (event, ctx) => {
51
50
  const payload = event.payload as UpdateFieldPayload; // @cast-boundary engine-payload
52
51
  const tenantId = event.user.tenantId;
@@ -3,6 +3,7 @@ import {
3
3
  type FieldDefinition,
4
4
  fieldToZod,
5
5
  } from "@cosmicdrift/kumiko-framework/engine";
6
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
6
7
  import type * as z from "zod";
7
8
  import { SUPPORTED_FIELD_TYPES } from "../constants.js";
8
9
 
@@ -38,9 +39,8 @@ const SUPPORTED_EMBEDDED_SUB_TYPES: ReadonlySet<string> = new Set([
38
39
  const CONSTRAINT_KEYS = ["required", "maxLength", "format", "default"] as const;
39
40
 
40
41
  export function buildCustomFieldValueSchema(parsedField: unknown): z.ZodTypeAny | null {
41
- if (!parsedField || typeof parsedField !== "object") return null;
42
- const obj = parsedField as Record<string, unknown>;
43
- const rawType = obj["type"];
42
+ if (!isPlainObject(parsedField)) return null;
43
+ const rawType = parsedField["type"];
44
44
  if (typeof rawType !== "string") return null;
45
45
  if (!SUPPORTED_TYPES_SET.has(rawType)) return null;
46
46
 
@@ -54,35 +54,34 @@ export function buildCustomFieldValueSchema(parsedField: unknown): z.ZodTypeAny
54
54
  // drops everywhere else.
55
55
  let strippedSubSchema: Record<string, Record<string, unknown>> | undefined;
56
56
  if (rawType === "embedded") {
57
- const schema = obj["schema"];
57
+ const schema = parsedField["schema"];
58
58
  if (!schema || typeof schema !== "object") return null;
59
59
  strippedSubSchema = {};
60
60
  for (const [subKey, sub] of Object.entries(schema)) {
61
- if (!sub || typeof sub !== "object") return null;
62
- const subObj = sub as Record<string, unknown>;
63
- const subType = subObj["type"];
61
+ if (!isPlainObject(sub)) return null;
62
+ const subType = sub["type"];
64
63
  if (typeof subType !== "string" || !SUPPORTED_EMBEDDED_SUB_TYPES.has(subType)) {
65
64
  return null;
66
65
  }
67
- const strippedSub = { ...subObj };
66
+ const strippedSub = { ...sub };
68
67
  for (const k of CONSTRAINT_KEYS) delete strippedSub[k];
69
68
  strippedSubSchema[subKey] = strippedSub;
70
69
  }
71
70
  }
72
71
 
73
- const fieldDef: Record<string, unknown> = { ...obj };
72
+ const fieldDef: Record<string, unknown> = { ...parsedField };
74
73
  for (const k of CONSTRAINT_KEYS) delete fieldDef[k];
75
74
  if (strippedSubSchema !== undefined) fieldDef["schema"] = strippedSubSchema;
76
75
  if (rawType === "enum") {
77
76
  fieldDef["type"] = "select";
78
- fieldDef["options"] = obj["values"] ?? obj["options"] ?? [];
77
+ fieldDef["options"] = parsedField["values"] ?? parsedField["options"] ?? [];
79
78
  }
80
79
 
81
80
  // fieldToZod's money case validates `currency` against the passed list, not
82
81
  // a field-level key — so surface the field's own currency when it declares
83
82
  // one, else fall back to the framework defaults.
84
83
  const currencies =
85
- typeof obj["currency"] === "string" ? [obj["currency"] as string] : DEFAULT_CURRENCIES;
84
+ typeof parsedField["currency"] === "string" ? [parsedField["currency"]] : DEFAULT_CURRENCIES;
86
85
 
87
86
  // @cast-boundary serialized-field is the dehydrated r.field.X() output =
88
87
  // a FieldDefinition; fieldToZod reads only its type-specific keys (the
@@ -3,6 +3,7 @@
3
3
  import { type DbRunner, extractTableName } from "@cosmicdrift/kumiko-framework/db";
4
4
  import type { UserDataExportSnippet, UserDataHookCtx } from "@cosmicdrift/kumiko-framework/engine";
5
5
  import { EXT_USER_DATA, type FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
6
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
6
7
  import { selectCustomFieldsHostRows } from "./db/queries/user-data-rights.js";
7
8
 
8
9
  export interface WireCustomFieldsUserDataRightsOptions {
@@ -17,14 +18,14 @@ interface CustomFieldsHostRow {
17
18
  }
18
19
 
19
20
  function asCustomFieldsHostRow(value: unknown): CustomFieldsHostRow | null {
20
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
21
- if (!("id" in value) || typeof value.id !== "string") return null;
22
- const record = value as Record<string, unknown>;
23
- const cf = record["custom_fields"] ?? record["customFields"];
21
+ if (!isPlainObject(value)) return null;
22
+ const id = value["id"];
23
+ if (typeof id !== "string") return null;
24
+ const cf = value["custom_fields"] ?? value["customFields"];
24
25
  if (cf === undefined) return null;
25
- if (cf === null) return { id: value.id, customFields: null };
26
+ if (cf === null) return { id, customFields: null };
26
27
  if (!cf || typeof cf !== "object" || Array.isArray(cf)) return null;
27
- return { id: value.id, customFields: Object.fromEntries(Object.entries(cf)) };
28
+ return { id, customFields: Object.fromEntries(Object.entries(cf)) };
28
29
  }
29
30
 
30
31
  // fw#2914 — selectCustomFieldsHostRows runs raw SQL against a dynamic host
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.363.0",
4
+ "type": "improvement",
5
+ "title": "data-retention exports computeCutoff",
6
+ "detail": "computeCutoff(keepFor, now) and its Instant type are exported from @cosmicdrift/kumiko-bundled-features/data-retention, so apps that compute a retention cutoff themselves (for example a backfill) use the same keepFor parsing as the cleanup job instead of a copy."
7
+ },
2
8
  {
3
9
  "version": "0.335.0",
4
10
  "type": "fix",
@@ -18,3 +18,4 @@ export {
18
18
  tenantRetentionOverrideEntity,
19
19
  tenantRetentionOverrideTable,
20
20
  } from "./feature.js";
21
+ export { computeCutoff, type Instant } from "./keep-for.js";
@@ -72,6 +72,7 @@ import {
72
72
  fileRefEntity,
73
73
  fileRefsTable,
74
74
  } from "@cosmicdrift/kumiko-framework/files";
75
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
75
76
  import { runInSubTransaction } from "../shared/index.js";
76
77
  import { computeCutoff, type Instant } from "./keep-for.js";
77
78
  import type { RetentionPresetKey } from "./presets.js";
@@ -347,10 +348,9 @@ function collectSingleFileHolders(registry: Registry): readonly SingleFileHolder
347
348
  if (!entity) continue;
348
349
  const { singleFields } = planFileFields(entity.fields);
349
350
  if (singleFields.length === 0) continue;
350
- const table = proj.table as Record<string, unknown>; // @cast-boundary column-presence probe
351
351
  holders.push({
352
352
  table: proj.table,
353
- tableHasTenantId: table["tenantId"] !== undefined,
353
+ tableHasTenantId: propertyOf(proj.table, "tenantId") !== undefined,
354
354
  singleFields,
355
355
  });
356
356
  }
@@ -699,11 +699,10 @@ export async function runRetentionCleanup(
699
699
  const entity = registry.getEntity(entityName);
700
700
  if (!entity) continue;
701
701
 
702
- const table = proj.table as Record<string, unknown>; // @cast-boundary column-presence probe
703
702
  const declaredReference = policy.reference ?? DEFAULT_REFERENCE_FIELD;
704
703
  const referenceField = FRAMEWORK_REFERENCE_ALIAS[declaredReference] ?? declaredReference;
705
704
 
706
- if (table[referenceField] === undefined) {
705
+ if (propertyOf(proj.table, referenceField) === undefined) {
707
706
  skipped.push({ entityName, reason: "missing_reference_column" });
708
707
  continue;
709
708
  }
@@ -713,7 +712,7 @@ export async function runRetentionCleanup(
713
712
  // Tenant-Scope nur wenn die Tabelle eine tenantId-Spalte hat — identisch zu
714
713
  // soft-delete-cleanup. Ohne diesen Filter wuerde ein Tenant die Rows eines
715
714
  // anderen treffen.
716
- if (table["tenantId"] !== undefined) {
715
+ if (propertyOf(proj.table, "tenantId") !== undefined) {
717
716
  where["tenantId"] = tenantId;
718
717
  }
719
718
 
@@ -762,7 +761,10 @@ export async function runRetentionCleanup(
762
761
  break;
763
762
  }
764
763
  case "softDelete": {
765
- if (table["isDeleted"] === undefined || table["deletedAt"] === undefined) {
764
+ if (
765
+ propertyOf(proj.table, "isDeleted") === undefined ||
766
+ propertyOf(proj.table, "deletedAt") === undefined
767
+ ) {
766
768
  skipped.push({ entityName, reason: "missing_softdelete_columns" });
767
769
  break;
768
770
  }
@@ -0,0 +1,105 @@
1
+ import * as z from "zod";
2
+ import type {
3
+ FileAttachmentRef,
4
+ InlineAttachmentRef,
5
+ RenderedPdfAttachmentRef,
6
+ } from "../renderer-foundation/index.js";
7
+ import { attachmentFileNameSchema } from "../shared/index.js";
8
+ import { isDocumentResourceKey, templateSlugSchema } from "../template-resolver/index.js";
9
+
10
+ export type { FileAttachmentRef, InlineAttachmentRef, RenderedPdfAttachmentRef };
11
+ export type EmailAttachmentRef = InlineAttachmentRef | FileAttachmentRef | RenderedPdfAttachmentRef;
12
+
13
+ export const MAX_EMAIL_ATTACHMENTS = 20;
14
+
15
+ export const MAX_RENDERED_PDF_VARIABLES_BYTES = 64_000;
16
+
17
+ // Channel-data keys that steer delivery itself (envelope, headers, attachments);
18
+ // they are not document variables and must not be copied into attached PDF refs.
19
+ export const DELIVERY_ENVELOPE_DATA_KEYS = [
20
+ "attachments",
21
+ "from",
22
+ "fromName",
23
+ "replyTo",
24
+ "headers",
25
+ "unsubscribeUrl",
26
+ ] as const;
27
+
28
+ const DELIVERY_ENVELOPE_DATA_KEY_SET: ReadonlySet<string> = new Set(DELIVERY_ENVELOPE_DATA_KEYS);
29
+
30
+ export function withoutDeliveryEnvelopeKeys(
31
+ variables: Readonly<Record<string, unknown>>,
32
+ ): Readonly<Record<string, unknown>> {
33
+ return Object.fromEntries(
34
+ Object.entries(variables).filter(([key]) => !DELIVERY_ENVELOPE_DATA_KEY_SET.has(key)),
35
+ );
36
+ }
37
+
38
+ const MAX_ID_LENGTH = 64;
39
+
40
+ const fileRefIdSchema = z.string().min(1).max(MAX_ID_LENGTH);
41
+
42
+ export const inlineAttachmentRefSchema = z
43
+ .object({
44
+ kind: z.literal("inline"),
45
+ cid: z.string().min(1).max(MAX_ID_LENGTH).refine(isDocumentResourceKey),
46
+ fileRefId: fileRefIdSchema,
47
+ })
48
+ .strict();
49
+
50
+ export const fileAttachmentRefSchema = z
51
+ .object({
52
+ kind: z.literal("file"),
53
+ fileRefId: fileRefIdSchema,
54
+ fileName: attachmentFileNameSchema,
55
+ })
56
+ .strict();
57
+
58
+ export const renderedPdfAttachmentRefSchema = z
59
+ .object({
60
+ kind: z.literal("rendered-pdf"),
61
+ templateSlug: templateSlugSchema,
62
+ variables: z
63
+ .record(z.string(), z.unknown())
64
+ .refine(
65
+ (value) => Buffer.byteLength(JSON.stringify(value)) <= MAX_RENDERED_PDF_VARIABLES_BYTES,
66
+ {
67
+ message: `variables must not exceed ${MAX_RENDERED_PDF_VARIABLES_BYTES} bytes as JSON`,
68
+ },
69
+ ),
70
+ fileName: attachmentFileNameSchema.refine((name) => name.toLowerCase().endsWith(".pdf"), {
71
+ message: "file name must end with .pdf",
72
+ }),
73
+ locale: z
74
+ .string()
75
+ .min(2)
76
+ .max(8)
77
+ .regex(/^[a-z]{2}(-[a-z]{2})?$/i, "locale must be ISO 639-1 (e.g. de, en, en-us)")
78
+ .optional(),
79
+ })
80
+ .strict();
81
+
82
+ export const emailAttachmentRefSchema = z.discriminatedUnion("kind", [
83
+ inlineAttachmentRefSchema,
84
+ fileAttachmentRefSchema,
85
+ renderedPdfAttachmentRefSchema,
86
+ ]);
87
+
88
+ // Rebuilds only the contract fields so bytes a caller attached to a ref object
89
+ // can never ride into a job payload.
90
+ export function toAttachmentRef(ref: EmailAttachmentRef): EmailAttachmentRef {
91
+ switch (ref.kind) {
92
+ case "inline":
93
+ return { kind: "inline", cid: ref.cid, fileRefId: ref.fileRefId };
94
+ case "file":
95
+ return { kind: "file", fileRefId: ref.fileRefId, fileName: ref.fileName };
96
+ case "rendered-pdf":
97
+ return {
98
+ kind: "rendered-pdf",
99
+ templateSlug: ref.templateSlug,
100
+ variables: ref.variables,
101
+ fileName: ref.fileName,
102
+ ...(ref.locale !== undefined && { locale: ref.locale }),
103
+ };
104
+ }
105
+ }