@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
@@ -16,6 +16,12 @@ import type {
16
16
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
17
17
  import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
18
18
  import * as z from "zod";
19
+ import { fileContextForTenant } from "../file-foundation/index.js";
20
+ import {
21
+ emailAttachmentRefSchema,
22
+ MAX_EMAIL_ATTACHMENTS,
23
+ toAttachmentRef,
24
+ } from "./attachment-ref.js";
19
25
  import { appendAttemptEvent } from "./attempt-log.js";
20
26
  import { buildChannelContext } from "./channel-context.js";
21
27
  import { DeliveryJobs, deliveryIdempotencyRedisKey, deliveryPriorityRank } from "./constants.js";
@@ -49,7 +55,14 @@ const renderJobPayloadSchema = z.object({
49
55
 
50
56
  const sendJobPayloadSchema = renderJobPayloadSchema.extend({
51
57
  rendered: z
52
- .object({ html: z.string(), subject: z.string(), text: z.string().optional() })
58
+ .object({
59
+ html: z.string(),
60
+ subject: z.string(),
61
+ text: z.string().optional(),
62
+ fromName: z.string().optional(),
63
+ replyTo: z.string().optional(),
64
+ attachments: z.array(emailAttachmentRefSchema).max(MAX_EMAIL_ATTACHMENTS).optional(),
65
+ })
53
66
  .optional(),
54
67
  });
55
68
 
@@ -63,18 +76,36 @@ function requireTenantScopedDeps(
63
76
  if (!registry) throw new Error("delivery job: missing registry in job context");
64
77
  // delivery is r.systemScope()'d, so JobContext.systemDb is guaranteed; unsafeRaw
65
78
  // hands the underlying DbConnection to both — safe since they filter by tenantId themselves.
79
+ // Attachment lookups read only this tenant's FileRefs, plus the system tenant's for inline images.
66
80
  if (!ctx.systemDb) {
67
81
  throw new InternalError({
68
82
  message: "delivery job: ctx.systemDb missing on a system-scoped job",
69
83
  });
70
84
  }
85
+ // Also makes ctx.config (built for the job's tenant) the accessor of the payload tenant;
86
+ // job contexts carry no configFor.
71
87
  ctx.systemDb.assertTenantMatch(tenantId);
72
88
  const db = ctx.systemDb.unsafeRaw(
73
- "delivery attempt append and channel context filter by this job's tenantId themselves",
89
+ "attempt append and channel context filter by this job's tenantId; attachment lookup reads only the job tenant's FileRefs plus, for inline images, the system tenant's",
74
90
  ) as DbConnection; // @cast-boundary db-operator — DbRunner narrows to DbConnection, jobs never run inside a DbTx
75
91
  return { db, registry };
76
92
  }
77
93
 
94
+ function toSendRendered(
95
+ rendered: RenderedMessage,
96
+ ): z.infer<typeof sendJobPayloadSchema>["rendered"] {
97
+ return {
98
+ html: rendered.html,
99
+ subject: rendered.subject,
100
+ ...(rendered.text !== undefined && { text: rendered.text }),
101
+ ...(rendered.fromName !== undefined && { fromName: rendered.fromName }),
102
+ ...(rendered.replyTo !== undefined && { replyTo: rendered.replyTo }),
103
+ ...(rendered.attachments !== undefined && {
104
+ attachments: rendered.attachments.map(toAttachmentRef),
105
+ }),
106
+ };
107
+ }
108
+
78
109
  function resolveChannel(registry: Registry, name: string): DeliveryChannel {
79
110
  const channel = collectChannels(registry).find((c) => c.name === name);
80
111
  if (!channel) throw new Error(`delivery job: unknown channel "${name}"`);
@@ -111,15 +142,11 @@ function entryFor(
111
142
  };
112
143
  }
113
144
 
114
- // Only the final retry frees the key: earlier failures are retried by the runner and
115
- // must still dedup. Assumes ctx.redis is the handle the service claimed on; runProdApp
116
- // wires the same instance into both.
117
- async function releaseIdempotencyOnTerminalFailure(
118
- ctx: JobContext,
119
- p: RenderJobPayload,
120
- ): Promise<void> {
121
- // skip: nothing to release before the last attempt or without an idempotency key
122
- if (!ctx.finalAttempt || !p.idempotencyKey) return;
145
+ // Assumes ctx.redis is the handle the service claimed on; runProdApp wires the same
146
+ // instance into both.
147
+ async function releaseIdempotencyKey(ctx: JobContext, p: RenderJobPayload): Promise<void> {
148
+ // skip: no idempotency key was claimed for this notification
149
+ if (!p.idempotencyKey) return;
123
150
  if (!ctx.redis) {
124
151
  ctx.log.warn("delivery: idempotency key not released, job context has no redis", {
125
152
  notificationType: p.notificationType,
@@ -135,6 +162,33 @@ async function releaseIdempotencyOnTerminalFailure(
135
162
  }
136
163
  }
137
164
 
165
+ // Only the final retry frees the key: earlier failures are retried by the runner and
166
+ // must still dedup.
167
+ async function releaseIdempotencyOnTerminalFailure(
168
+ ctx: JobContext,
169
+ p: RenderJobPayload,
170
+ ): Promise<void> {
171
+ // skip: not the last attempt, the runner retries and the key must keep deduplicating
172
+ if (!ctx.finalAttempt) return;
173
+ await releaseIdempotencyKey(ctx, p);
174
+ }
175
+
176
+ // Covers every throw after the payload parsed, not just the attempt-logged ones: a
177
+ // missing registry or unknown channel on the last attempt would otherwise hold the key
178
+ // until its TTL.
179
+ async function releasingKeyOnTerminalFailure<T>(
180
+ ctx: JobContext,
181
+ p: RenderJobPayload,
182
+ run: () => Promise<T>,
183
+ ): Promise<T> {
184
+ try {
185
+ return await run();
186
+ } catch (err) {
187
+ await releaseIdempotencyOnTerminalFailure(ctx, p);
188
+ throw err;
189
+ }
190
+ }
191
+
138
192
  // The job runner stores err.message in the run row, and BullMQ keeps failedReason and
139
193
  // the stack trace in Redis in plain text, so the rethrown error carries only the code.
140
194
  // The redacted message goes to the log.
@@ -157,7 +211,6 @@ async function failAttempt(
157
211
  p.deliveryAttemptId,
158
212
  entryFor(p, "failed", code, p.address),
159
213
  );
160
- await releaseIdempotencyOnTerminalFailure(ctx, p);
161
214
  throw new Error(code);
162
215
  }
163
216
 
@@ -166,10 +219,22 @@ async function failAttempt(
166
219
  // the send step is never reached, so a stuck render can't half-send.
167
220
  export const deliveryRenderJob: JobHandlerFn = async (payload, ctx) => {
168
221
  const p = renderJobPayloadSchema.parse(payload);
222
+ return releasingKeyOnTerminalFailure(ctx, p, () => renderStep(p, ctx));
223
+ };
224
+
225
+ async function renderStep(p: RenderJobPayload, ctx: JobContext): Promise<void> {
169
226
  const tenantId = p.tenantId as TenantId; // @cast-boundary engine-payload — stream tenant
170
227
  const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
171
228
  const channel = resolveChannel(registry, p.channelName);
172
- const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets);
229
+ const channelCtx = buildChannelContext(
230
+ db,
231
+ registry,
232
+ undefined,
233
+ tenantId,
234
+ ctx.secrets,
235
+ ctx.config,
236
+ (fileTenantId) => fileContextForTenant(ctx, ctx.systemUser.tenantId, fileTenantId),
237
+ );
173
238
 
174
239
  let rendered: RenderedMessage;
175
240
  try {
@@ -184,23 +249,35 @@ export const deliveryRenderJob: JobHandlerFn = async (payload, ctx) => {
184
249
  const jobRunner = ctx["jobRunner"] as JobRunner; // @cast-boundary dynamic-key — dispatch lives on the concrete runner
185
250
  await jobRunner.dispatch(
186
251
  DeliveryJobs.send,
187
- { ...p, rendered },
252
+ { ...p, rendered: toSendRendered(rendered) },
188
253
  { priority: deliveryPriorityRank[p.priority] },
189
254
  );
190
255
  } catch (err) {
191
256
  return failAttempt(ctx, db, registry, p, "channel_error", err);
192
257
  }
193
- };
258
+ }
194
259
 
195
260
  // Deliver via the channel using the rendered payload (if any). On failure:
196
261
  // record the terminal failed event and rethrow so BullMQ retries the send with
197
262
  // the same already-rendered HTML — no re-render needed.
198
263
  export const deliverySendJob: JobHandlerFn = async (payload, ctx) => {
199
264
  const p = sendJobPayloadSchema.parse(payload);
265
+ return releasingKeyOnTerminalFailure(ctx, p, () => sendStep(p, ctx));
266
+ };
267
+
268
+ async function sendStep(p: z.infer<typeof sendJobPayloadSchema>, ctx: JobContext): Promise<void> {
200
269
  const tenantId = p.tenantId as TenantId; // @cast-boundary engine-payload — stream tenant
201
270
  const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
202
271
  const channel = resolveChannel(registry, p.channelName);
203
- const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets);
272
+ const channelCtx = buildChannelContext(
273
+ db,
274
+ registry,
275
+ undefined,
276
+ tenantId,
277
+ ctx.secrets,
278
+ ctx.config,
279
+ (fileTenantId) => fileContextForTenant(ctx, ctx.systemUser.tenantId, fileTenantId),
280
+ );
204
281
 
205
282
  try {
206
283
  const result = await channel.send(p.address, toMessage(p), channelCtx, p.rendered);
@@ -216,7 +293,10 @@ export const deliverySendJob: JobHandlerFn = async (payload, ctx) => {
216
293
  result.confirmed,
217
294
  ),
218
295
  );
296
+ // A soft failure ends the job successfully, so no retry follows and the caller's
297
+ // retry must not be skipped as a duplicate.
298
+ if (result.status === "failed") await releaseIdempotencyKey(ctx, p);
219
299
  } catch (err) {
220
300
  return failAttempt(ctx, db, registry, p, "send_failed", err);
221
301
  }
222
- };
302
+ }
@@ -1,4 +1,4 @@
1
- import type { NotifyFactory } from "@cosmicdrift/kumiko-framework/engine";
1
+ import type { NotifyFactory, NotifyFn, NotifyOptions } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import type { DeliveryService } from "./types.js";
3
3
 
4
4
  // The same binding runProdApp wires into the app context: the calling context's
@@ -8,12 +8,17 @@ export function createDeliveryNotifyFactory(
8
8
  deliveryService: Pick<DeliveryService, "notify">,
9
9
  options: { readonly deliverQueuedInline?: boolean } = {},
10
10
  ): NotifyFactory {
11
- return (user, tenantId, jobDispatcher) => (notificationType, notifyOptions) =>
12
- deliveryService.notify(
13
- notificationType,
14
- notifyOptions,
15
- user,
16
- tenantId,
17
- options.deliverQueuedInline === true ? undefined : jobDispatcher,
18
- );
11
+ return (user, tenantId, jobDispatcher, deliveryDb, fileProviderResolver) => {
12
+ const notify: NotifyFn = (notificationType: string, notifyOptions: NotifyOptions) =>
13
+ deliveryService.notify(
14
+ notificationType,
15
+ notifyOptions,
16
+ user,
17
+ tenantId,
18
+ options.deliverQueuedInline === true ? undefined : jobDispatcher,
19
+ deliveryDb,
20
+ fileProviderResolver,
21
+ );
22
+ return notify;
23
+ };
19
24
  }
@@ -1,6 +1,12 @@
1
1
  import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
2
2
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
- import type { EscapeHatchAuditSink, Registry } from "@cosmicdrift/kumiko-framework/engine";
3
+ import type {
4
+ ConfigAccessor,
5
+ EscapeHatchAuditSink,
6
+ Registry,
7
+ TenantId,
8
+ } from "@cosmicdrift/kumiko-framework/engine";
9
+ import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
4
10
  import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
5
11
  import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
6
12
  import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
@@ -19,6 +25,8 @@ export type CreateDeliveryTestContextOptions = {
19
25
  readonly jobRunner?: JobRunner;
20
26
  readonly secrets?: SecretsContext;
21
27
  readonly idempotencyRedis?: Redis;
28
+ readonly filesForTenant?: (tenantId: string) => FileContext | undefined;
29
+ readonly tenantConfigFor?: (tenantId: TenantId) => ConfigAccessor;
22
30
  };
23
31
 
24
32
  /**
@@ -1,21 +1,36 @@
1
1
  import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
2
- import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
3
  import type {
4
4
  DeliveryErrorCode,
5
5
  NotifyDeliveryStatus,
6
+ NotifyDispatched,
6
7
  NotifyJobDispatcher,
7
8
  NotifyOptions,
8
9
  NotifyPriority,
9
- NotifyResult,
10
10
  Registry,
11
11
  SessionUser,
12
12
  TenantId,
13
13
  } from "@cosmicdrift/kumiko-framework/engine";
14
+ import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
14
15
  import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
16
+ import type { MailPayload, RenderedTemplateRef } from "../renderer-foundation/index.js";
17
+ import type { EmailAttachmentRef } from "./attachment-ref.js";
15
18
  import { DELIVERY_CHANNEL_EXTENSION } from "./constants.js";
16
19
 
17
20
  // --- Channel Interface ---
18
21
 
22
+ export type RenderedMail = {
23
+ readonly subject: string;
24
+ readonly html: string;
25
+ readonly text: string;
26
+ readonly previewText?: string;
27
+ readonly fromName?: string;
28
+ readonly replyTo?: string;
29
+ readonly attachments?: readonly EmailAttachmentRef[];
30
+ /** The layout version the html was rendered through; the editor preview names it. */
31
+ readonly layout?: RenderedTemplateRef;
32
+ };
33
+
19
34
  export type ChannelContext = {
20
35
  readonly db: TenantDb;
21
36
  readonly registry: Registry;
@@ -24,8 +39,19 @@ export type ChannelContext = {
24
39
  // Chat channels read their per-tenant webhook URL / bot token from it.
25
40
  // Absent when no secrets feature/KEK is wired.
26
41
  readonly secrets?: SecretsContext | undefined;
42
+ // Renders a stored mail template, the tenant's own or the system fallback.
43
+ readonly renderMail?: (payload: MailPayload) => Promise<RenderedMail>;
44
+ // Reads the bytes behind an attachment ref at send time. Absent when no file storage is wired.
45
+ readonly readAttachment?: (
46
+ ref: EmailAttachmentRef,
47
+ origin?: AttachmentOrigin,
48
+ ) => Promise<{ readonly content: Uint8Array; readonly contentType: string } | null>;
27
49
  };
28
50
 
51
+ // "template": the renderer returned the ref from the stored template (own or system). "message":
52
+ // the ref came from notify data, so a caller chose it. Unset means "message".
53
+ export type AttachmentOrigin = "template" | "message";
54
+
29
55
  export type ChannelMessage = {
30
56
  readonly notificationType: string;
31
57
  readonly title: string;
@@ -49,6 +75,9 @@ export type RenderedMessage = {
49
75
  readonly html: string;
50
76
  readonly subject: string;
51
77
  readonly text?: string;
78
+ readonly fromName?: string;
79
+ readonly replyTo?: string;
80
+ readonly attachments?: readonly EmailAttachmentRef[];
52
81
  };
53
82
 
54
83
  // `mode` decides how the delivery-service dispatches a channel:
@@ -76,21 +105,6 @@ export type DeliveryChannel = {
76
105
  ): Promise<ChannelResult>;
77
106
  };
78
107
 
79
- // --- Notification Renderer ---
80
-
81
- export type RendererInput = {
82
- readonly template: string;
83
- readonly variables: Readonly<Record<string, unknown>>;
84
- readonly locale?: string | undefined;
85
- };
86
-
87
- export type NotificationRenderer = {
88
- readonly name: string;
89
- render(input: RendererInput): Promise<string>;
90
- /** Plain-text version of the same mail; the email channel sends it as the text part of a multipart/alternative mail. */
91
- renderText?(input: RendererInput): Promise<string>;
92
- };
93
-
94
108
  // --- Delivery Log Entry ---
95
109
 
96
110
  export type DeliveryLogEntry = {
@@ -114,7 +128,9 @@ export type DeliveryService = {
114
128
  user: SessionUser,
115
129
  tenantId: TenantId,
116
130
  jobDispatcher?: NotifyJobDispatcher,
117
- ): Promise<NotifyResult>;
131
+ deliveryDb?: DbConnection,
132
+ fileProviderResolver?: FileProviderResolver,
133
+ ): Promise<NotifyDispatched>;
118
134
  };
119
135
 
120
136
  // r.useExtension options-shape: `name` is NOT part of the registration
@@ -7,6 +7,7 @@ import {
7
7
  } from "@cosmicdrift/kumiko-framework/api";
8
8
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
9
9
  import { escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
10
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
10
11
  import * as jose from "jose";
11
12
  import * as z from "zod";
12
13
  import { hashUnsubscribeAddress } from "./address-opt-out.js";
@@ -412,8 +413,10 @@ function tokenFromRequestBody(request: SignatureExtraRouteVerifyRequest): string
412
413
  }
413
414
  if (contentType?.includes("application/json")) {
414
415
  try {
415
- const parsed = JSON.parse(request.rawBody) as { token?: unknown };
416
- return typeof parsed.token === "string" ? parsed.token : undefined;
416
+ const parsed: unknown = JSON.parse(request.rawBody);
417
+ return isPlainObject(parsed) && typeof parsed["token"] === "string"
418
+ ? parsed["token"]
419
+ : undefined;
417
420
  } catch {
418
421
  return undefined;
419
422
  }
@@ -13,13 +13,10 @@ import type {
13
13
  import QRCode from "qrcode";
14
14
  import type { OverlayOptions, ResizeOptions, Sharp } from "sharp";
15
15
  import sharp from "sharp";
16
+ import { assertNotSvg, SHARP_INPUT_OPTIONS } from "../shared/index.js";
16
17
 
17
18
  const MIN_SHARP_SIGMA = 0.3;
18
19
  const MAX_SHARP_SIGMA = 1000;
19
- // Above this, a decoded pixel buffer runs into hundreds of MB before any
20
- // resize even happens — sharp's own default (~268MP) is still large enough
21
- // to be an OOM primitive for a single request.
22
- const MAX_INPUT_PIXELS = 100_000_000;
23
20
  // Longest output edge a caller may request via `size`/`maxEdge`. Without a
24
21
  // cap, `{ maxEdge: 50000 }` allocates a multi-gigabyte buffer per encoder
25
22
  // step before any client sees a byte back.
@@ -38,12 +35,6 @@ const MAX_QR_DATA_LENGTH = 1024;
38
35
  // camera capture — the AC claims phone-scannable, so an under-sized layer
39
36
  // throws instead of caching a silently unusable image.
40
37
  const MIN_QR_PIXEL_WIDTH = 160;
41
- // sequentialRead trades random pixel access for lower peak memory on
42
- // streamed formats — applied at every decode entry point below since none
43
- // of them need non-sequential access to the *original* bytes (extract runs
44
- // against a single already-buffered region per call).
45
- const SHARP_INPUT_OPTIONS = { limitInputPixels: MAX_INPUT_PIXELS, sequentialRead: true } as const;
46
-
47
38
  function normalizeMimeType(mimeType: string): string {
48
39
  return mimeType.toLowerCase().split(";")[0]?.trim() ?? "";
49
40
  }
@@ -52,18 +43,6 @@ function clampSigma(sigma: number): number {
52
43
  return Math.min(MAX_SHARP_SIGMA, Math.max(MIN_SHARP_SIGMA, sigma));
53
44
  }
54
45
 
55
- // Shared trust boundary for every image byte source this renderer decodes —
56
- // the original upload (renderImage) and an overlay layer's bytes (applyOverlays)
57
- // alike: a base64 string in a field declaration isn't more trustworthy than
58
- // an upload just because it came from app code.
59
- function assertNotSvg(format: string | undefined): void {
60
- if (format === "svg") {
61
- throw new Error(
62
- "derivatives-sharp: SVG is not supported — rendering untrusted SVG through libvips is a trust boundary this renderer doesn't need to cross.",
63
- );
64
- }
65
- }
66
-
67
46
  function assertValidRegion(region: BlurRegion): void {
68
47
  const values = [region.x, region.y, region.width, region.height];
69
48
  if (values.some((v) => !Number.isFinite(v)) || region.width <= 0 || region.height <= 0) {
@@ -4,6 +4,7 @@
4
4
  // the codec here keeps liteparse + recipe consumers lockstep with the
5
5
  // entity shape (kumiko-framework#1549/1).
6
6
 
7
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
7
8
  import type { IngestPage } from "./entity.js";
8
9
 
9
10
  export function writeIngestPages(pages: readonly IngestPage[]): string {
@@ -28,7 +29,9 @@ function parseJson(raw: string): unknown {
28
29
  }
29
30
 
30
31
  function isIngestPage(value: unknown): value is IngestPage {
31
- if (value === null || typeof value !== "object") return false;
32
- const page = value as { pageNumber?: unknown; text?: unknown };
33
- return typeof page.pageNumber === "number" && typeof page.text === "string";
32
+ return (
33
+ isPlainObject(value) &&
34
+ typeof value["pageNumber"] === "number" &&
35
+ typeof value["text"] === "string"
36
+ );
34
37
  }
@@ -5,6 +5,7 @@ import {
5
5
  type RedisClientOptions,
6
6
  redisChannelPrefixFromEnv,
7
7
  } from "@cosmicdrift/kumiko-framework/redis";
8
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
8
9
  import type { ToggleSyncSignal } from "./toggle-runtime.js";
9
10
 
10
11
  // Single fixed channel — unlike the SSE broker there's no per-tenant/
@@ -20,10 +21,9 @@ export type RedisToggleSyncSignal = ToggleSyncSignal & {
20
21
 
21
22
  function isTogglePayload(value: unknown): value is { featureName: string; enabled: boolean } {
22
23
  return (
23
- typeof value === "object" &&
24
- value !== null &&
25
- typeof (value as { featureName?: unknown }).featureName === "string" &&
26
- typeof (value as { enabled?: unknown }).enabled === "boolean"
24
+ isPlainObject(value) &&
25
+ typeof value["featureName"] === "string" &&
26
+ typeof value["enabled"] === "boolean"
27
27
  );
28
28
  }
29
29
 
@@ -147,7 +147,7 @@ export const publicVariantQuery = defineQueryHandler({
147
147
  const spec = resolveFieldVariant(ctx.registry, entityType, fieldName, query.payload.variant);
148
148
  if (!spec) return null;
149
149
  const result = await derivatives.variant(query.payload.fileRefId, spec, query.payload.variant);
150
- const bytes = await files.ref(result.storageKey).read();
150
+ const bytes = await files.refFromFileRef(result).read();
151
151
 
152
152
  // ponytail: fully buffered (no streaming), +33% over the wire from
153
153
  // base64 — HttpRouteHandlerDeps only exposes {app, systemQuery} (no
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "readFileRefContent reads handed-over files; fileContextForTenant takes the context tenant",
6
+ "detail": "readFileRefContent, readTemplateResource and the public variant query read bytes through FileContext.refFromFileRef, so a fileRef moved by a tenant handover is readable again. fileContextForTenant gains a `contextTenantId` argument: without a provider resolver it returns undefined for any other tenant instead of the context's files, which are bound to a different key space."
7
+ },
2
8
  {
3
9
  "version": "0.292.0",
4
10
  "type": "improvement",
@@ -7,3 +7,10 @@ export {
7
7
  fileFoundationFeature,
8
8
  isFileProviderPlugin,
9
9
  } from "./feature.js";
10
+ export {
11
+ type FileRefContent,
12
+ FileRefTooLargeError,
13
+ fileContextForTenant,
14
+ fileContextFromResolver,
15
+ readFileRefContent,
16
+ } from "./read-file-ref-content.js";
@@ -0,0 +1,89 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
+ import { isUuid, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
+ import {
5
+ createFileContext,
6
+ type FileContext,
7
+ type FileProviderResolver,
8
+ fileRefsTable,
9
+ } from "@cosmicdrift/kumiko-framework/files";
10
+
11
+ export type FileRefContent = {
12
+ readonly content: Uint8Array;
13
+ readonly contentType: string;
14
+ readonly tenantId: string;
15
+ readonly size: number;
16
+ };
17
+
18
+ type FileRefRow = {
19
+ readonly storageKey: string;
20
+ readonly mimeType: string;
21
+ readonly tenantId: string;
22
+ readonly size: number | bigint | string;
23
+ };
24
+
25
+ // Tenant-bound on purpose: without a resolver `ctx.files` only reaches the storage of the
26
+ // context's own tenant (`contextTenantId`), so a request for another tenant's files is
27
+ // undefined instead of silently reading through the wrong tenant's storage.
28
+ export function fileContextForTenant(
29
+ ctx: {
30
+ readonly _fileProviderResolver?: FileProviderResolver | undefined;
31
+ readonly files?: FileContext | undefined;
32
+ },
33
+ contextTenantId: string,
34
+ tenantId: string,
35
+ ): FileContext | undefined {
36
+ const resolver = ctx._fileProviderResolver;
37
+ if (resolver === undefined) return tenantId === contextTenantId ? ctx.files : undefined;
38
+ return fileContextFromResolver(resolver, tenantId);
39
+ }
40
+
41
+ export function fileContextFromResolver(
42
+ resolver: FileProviderResolver,
43
+ tenantId: string,
44
+ ): FileContext {
45
+ // @cast-boundary engine-payload — tenantId comes from a trusted job payload or session
46
+ const brandedTenantId = tenantId as TenantId;
47
+ return createFileContext(() => resolver(brandedTenantId), tenantId);
48
+ }
49
+
50
+ export class FileRefTooLargeError extends Error {
51
+ constructor(readonly size: number) {
52
+ super(`file ref exceeds the size limit (${size} bytes)`);
53
+ }
54
+ }
55
+
56
+ // fileName of the row is never used: it is personal data and may be a PII ciphertext.
57
+ export async function readFileRefContent(args: {
58
+ readonly db: DbRunner | TenantDb;
59
+ readonly filesForTenant: (tenantId: string) => FileContext | undefined;
60
+ readonly fileRefId: string;
61
+ readonly allowedTenantIds: readonly string[];
62
+ /** Only accept this user's own upload that is not bound to any entity yet. */
63
+ readonly unclaimedUploadOf?: string;
64
+ /** Rejects before any bytes are read when the stored size exceeds the limit. */
65
+ readonly maxBytes?: number;
66
+ }): Promise<FileRefContent | null> {
67
+ if (!isUuid(args.fileRefId)) return null;
68
+ const [row] = await selectMany<FileRefRow>(args.db, fileRefsTable, {
69
+ id: args.fileRefId,
70
+ tenantId: { in: args.allowedTenantIds },
71
+ isDeleted: false,
72
+ ...(args.unclaimedUploadOf !== undefined && {
73
+ insertedById: args.unclaimedUploadOf,
74
+ entityType: null,
75
+ entityId: null,
76
+ }),
77
+ });
78
+ if (row === undefined) return null;
79
+ const size = Number(row.size);
80
+ if (args.maxBytes !== undefined && size > args.maxBytes) {
81
+ throw new FileRefTooLargeError(size);
82
+ }
83
+ const files = args.filesForTenant(row.tenantId);
84
+ if (files === undefined) {
85
+ throw new Error("readFileRefContent: no file storage is available for the file's tenant");
86
+ }
87
+ const content = await files.refFromFileRef(row).read();
88
+ return { content, contentType: row.mimeType, tenantId: row.tenantId, size };
89
+ }
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "Tenant destroy hooks make progress even when the deadline already passed",
6
+ "detail": "The chunked migration takes a minBatches option and the file destroy hooks process one chunk per tick, so a slow list call no longer makes every run time out empty."
7
+ },
2
8
  {
3
9
  "version": "0.354.0",
4
10
  "type": "fix",
@@ -129,6 +129,7 @@ export const fileRefTenantDestroyHook: TenantDataDestroyHook = async (ctx) => {
129
129
  },
130
130
  maxFailures: 1,
131
131
  deadlineAt: ctx.deadlineAt ?? Number.POSITIVE_INFINITY,
132
+ minBatches: 1,
132
133
  onRowError: (_row, err) => {
133
134
  firstFailure = err instanceof Error ? err.message : String(err);
134
135
  },
@@ -222,6 +223,9 @@ export const fileRefStorageDestroyHook: StorageProviderDestroyTenantHook = async
222
223
  },
223
224
  maxFailures: 1,
224
225
  deadlineAt: ctx.deadlineAt ?? Number.POSITIVE_INFINITY,
226
+ // list() and the survivor lookup above can use up the whole budget; one chunk per tick
227
+ // still deletes, so the stage never reports "no progress".
228
+ minBatches: 1,
225
229
  onRowError: (_key, err) => {
226
230
  firstFailure = err instanceof Error ? err.message : String(err);
227
231
  },
@@ -74,7 +74,10 @@ const loadCollapsed = (): ReadonlySet<string> => {
74
74
  if (typeof window === "undefined") return new Set();
75
75
  try {
76
76
  const raw = window.localStorage.getItem(COLLAPSED_KEY);
77
- return new Set(raw ? (JSON.parse(raw) as string[]) : []);
77
+ const parsed: unknown = raw ? JSON.parse(raw) : [];
78
+ return new Set(
79
+ Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === "string") : [],
80
+ );
78
81
  } catch {
79
82
  return new Set();
80
83
  }
@@ -273,7 +276,7 @@ export function FolderManager({
273
276
  setDragOverKey(key);
274
277
  },
275
278
  onDragLeave: (e: DragEvent<HTMLDivElement>) => {
276
- if (!e.currentTarget.contains(e.relatedTarget as Node))
279
+ if (!(e.relatedTarget instanceof Node && e.currentTarget.contains(e.relatedTarget)))
277
280
  setDragOverKey((cur) => (cur === key ? null : cur));
278
281
  },
279
282
  onDrop: (e: DragEvent<HTMLDivElement>) => {