@cosmicdrift/kumiko-samples 0.388.0 → 0.390.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 (385) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +25 -13
  3. package/packages/bundled-features/src/agent-tools/changes.json +6 -0
  4. package/packages/bundled-features/src/agent-tools/index.ts +1 -1
  5. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +42 -26
  6. package/packages/bundled-features/src/audit/feature.ts +2 -1
  7. package/packages/bundled-features/src/auth-email-password/auth-user-row.ts +1 -1
  8. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +13 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +18 -0
  10. package/packages/bundled-features/src/auth-email-password/feature.ts +2 -2
  11. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +7 -7
  12. package/packages/bundled-features/src/auth-email-password/handlers/confirm-account-unlock.write.ts +3 -3
  13. package/packages/bundled-features/src/auth-email-password/handlers/confirm-token-flow.ts +5 -5
  14. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +9 -7
  15. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +11 -7
  16. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -4
  17. package/packages/bundled-features/src/auth-email-password/handlers/invite-info.query.ts +2 -2
  18. package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +9 -7
  19. package/packages/bundled-features/src/auth-email-password/handlers/login.write.ts +23 -16
  20. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
  21. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
  22. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
  23. package/packages/bundled-features/src/auth-email-password/handlers/reset-password.write.ts +3 -2
  24. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +12 -12
  25. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +16 -8
  26. package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +4 -2
  27. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +10 -2
  28. package/packages/bundled-features/src/auth-email-password/handlers/verify-email.write.ts +3 -2
  29. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
  30. package/packages/bundled-features/src/auth-email-password/seeding.ts +14 -9
  31. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
  32. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
  33. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
  34. package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
  35. package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
  36. package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
  37. package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
  38. package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
  39. package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
  40. package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
  41. package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
  42. package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
  43. package/packages/bundled-features/src/auth-foundation/feature.ts +13 -4
  44. package/packages/bundled-features/src/auth-mfa/changes.json +6 -0
  45. package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
  46. package/packages/bundled-features/src/auth-mfa/handlers/disable.write.ts +5 -5
  47. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +24 -14
  48. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm.write.ts +9 -3
  49. package/packages/bundled-features/src/auth-mfa/handlers/enable-start-preauth.write.ts +6 -1
  50. package/packages/bundled-features/src/auth-mfa/handlers/enable-start.write.ts +3 -1
  51. package/packages/bundled-features/src/auth-mfa/handlers/reencrypt.job.ts +14 -9
  52. package/packages/bundled-features/src/auth-mfa/handlers/regenerate-recovery.write.ts +5 -4
  53. package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +25 -14
  54. package/packages/bundled-features/src/auth-mfa/mfa-code-verifier.ts +10 -4
  55. package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +17 -8
  56. package/packages/bundled-features/src/auth-mfa/verify-factor.ts +1 -1
  57. package/packages/bundled-features/src/billing-foundation/billing-info-query.ts +2 -2
  58. package/packages/bundled-features/src/billing-foundation/changes.json +6 -0
  59. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +1 -1
  60. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +5 -5
  61. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +10 -7
  62. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +15 -12
  63. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-open-cancels.ts +22 -0
  64. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-provider-lock.ts +4 -0
  65. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +4 -4
  66. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +17 -58
  67. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +10 -8
  68. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +18 -15
  69. package/packages/bundled-features/src/billing-foundation/events.ts +1 -1
  70. package/packages/bundled-features/src/billing-foundation/feature.ts +8 -3
  71. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +1 -1
  72. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +5 -5
  73. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +3 -3
  74. package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +2 -2
  75. package/packages/bundled-features/src/billing-foundation/projection.ts +1 -1
  76. package/packages/bundled-features/src/billing-foundation/types.ts +1 -1
  77. package/packages/bundled-features/src/cap-counter/aggregate-id.ts +1 -1
  78. package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +5 -4
  79. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +21 -8
  80. package/packages/bundled-features/src/cap-counter/changes.json +12 -0
  81. package/packages/bundled-features/src/cap-counter/constants.ts +1 -1
  82. package/packages/bundled-features/src/cap-counter/feature.ts +3 -2
  83. package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +1 -1
  84. package/packages/bundled-features/src/cap-counter/reservation-chain.ts +1 -1
  85. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +4 -4
  86. package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
  87. package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +1 -1
  88. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +1 -1
  89. package/packages/bundled-features/src/compliance-profiles/seeding.ts +12 -7
  90. package/packages/bundled-features/src/config/changes.json +6 -0
  91. package/packages/bundled-features/src/config/feature.ts +6 -6
  92. package/packages/bundled-features/src/config/handlers/cascade.query.ts +1 -1
  93. package/packages/bundled-features/src/config/handlers/readiness.query.ts +1 -1
  94. package/packages/bundled-features/src/config/handlers/reencrypt.job.ts +23 -13
  95. package/packages/bundled-features/src/config/handlers/reset.write.ts +3 -3
  96. package/packages/bundled-features/src/config/handlers/selected-extensions.query.ts +1 -1
  97. package/packages/bundled-features/src/config/handlers/set.write.ts +22 -5
  98. package/packages/bundled-features/src/config/handlers/values.query.ts +1 -1
  99. package/packages/bundled-features/src/crypto-shredding/changes.json +12 -0
  100. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +9 -9
  101. package/packages/bundled-features/src/crypto-shredding/resolve-file-bytes-deleter.ts +3 -3
  102. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  103. package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +1 -1
  104. package/packages/bundled-features/src/custom-fields/handlers/delete-system-field.write.ts +1 -1
  105. package/packages/bundled-features/src/custom-fields/handlers/delete-tenant-field.write.ts +1 -1
  106. package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +1 -1
  107. package/packages/bundled-features/src/custom-fields/index.ts +4 -0
  108. package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
  109. package/packages/bundled-features/src/data-retention/feature.ts +1 -1
  110. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +12 -2
  111. package/packages/bundled-features/src/delivery/attempt-log.ts +1 -1
  112. package/packages/bundled-features/src/delivery/changes.json +12 -0
  113. package/packages/bundled-features/src/delivery/delivery-service.ts +6 -5
  114. package/packages/bundled-features/src/delivery/feature.ts +3 -2
  115. package/packages/bundled-features/src/delivery/index.ts +5 -0
  116. package/packages/bundled-features/src/delivery/jobs.ts +11 -11
  117. package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
  118. package/packages/bundled-features/src/delivery/testing.ts +1 -1
  119. package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
  120. package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
  121. package/packages/bundled-features/src/document-ingest-foundation/events.ts +16 -1
  122. package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
  123. package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
  124. package/packages/bundled-features/src/feature-toggles/handlers/registered.query.ts +1 -1
  125. package/packages/bundled-features/src/feature-toggles/handlers/set.write.ts +1 -1
  126. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +1 -1
  127. package/packages/bundled-features/src/file-derivatives/feature.ts +2 -2
  128. package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +5 -1
  129. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +7 -5
  130. package/packages/bundled-features/src/file-foundation/feature.ts +1 -1
  131. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -5
  132. package/packages/bundled-features/src/file-provider-s3/feature.ts +4 -1
  133. package/packages/bundled-features/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +8 -8
  134. package/packages/bundled-features/src/form-draft/feature.ts +1 -1
  135. package/packages/bundled-features/src/form-draft/handlers/cleanup.job.ts +22 -12
  136. package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +3 -3
  137. package/packages/bundled-features/src/form-draft/handlers/get.query.ts +1 -1
  138. package/packages/bundled-features/src/foundation-shared/config-helpers.ts +1 -1
  139. package/packages/bundled-features/src/host-routing/changes.json +6 -0
  140. package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
  141. package/packages/bundled-features/src/inbound-mail-foundation/events.ts +1 -1
  142. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +2 -1
  143. package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-state.ts +2 -2
  144. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  145. package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +1 -1
  146. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +10 -10
  147. package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +1 -1
  148. package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +1 -1
  149. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +1 -1
  150. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +4 -1
  151. package/packages/bundled-features/src/jobs/changes.json +6 -0
  152. package/packages/bundled-features/src/jobs/events.ts +1 -1
  153. package/packages/bundled-features/src/jobs/feature.ts +1 -0
  154. package/packages/bundled-features/src/jobs/handlers/consumer.write.ts +7 -4
  155. package/packages/bundled-features/src/jobs/handlers/projection-rebuild.job.ts +1 -1
  156. package/packages/bundled-features/src/jobs/handlers/reindex-entity.job.ts +11 -5
  157. package/packages/bundled-features/src/jobs/handlers/retention-cleanup.job.ts +1 -1
  158. package/packages/bundled-features/src/jobs/handlers/retry.write.ts +1 -1
  159. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +1 -1
  160. package/packages/bundled-features/src/jobs/handlers/trigger.write.ts +2 -2
  161. package/packages/bundled-features/src/jobs/job-run-logger.ts +139 -90
  162. package/packages/bundled-features/src/ledger/web/index.ts +1 -1
  163. package/packages/bundled-features/src/legal-pages/README.md +1 -1
  164. package/packages/bundled-features/src/legal-pages/lib/boot-check.ts +12 -8
  165. package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
  166. package/packages/bundled-features/src/mail-foundation/feature.ts +2 -2
  167. package/packages/bundled-features/src/mail-transport-smtp/changes.json +12 -0
  168. package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
  169. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +13 -12
  170. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  171. package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
  172. package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
  173. package/packages/bundled-features/src/managed-pages/index.ts +1 -0
  174. package/packages/bundled-features/src/managed-pages/seeding.ts +13 -8
  175. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +2 -2
  176. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -1
  177. package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +2 -2
  178. package/packages/bundled-features/src/personal-access-tokens/handlers/revoke.write.ts +1 -1
  179. package/packages/bundled-features/src/rate-limiting/handlers/status.query.ts +2 -2
  180. package/packages/bundled-features/src/readiness/handlers/status.query.ts +4 -1
  181. package/packages/bundled-features/src/renderer-foundation/api.ts +3 -3
  182. package/packages/bundled-features/src/secrets/announce-secret-change.ts +4 -4
  183. package/packages/bundled-features/src/secrets/changes.json +6 -0
  184. package/packages/bundled-features/src/secrets/feature.ts +9 -9
  185. package/packages/bundled-features/src/secrets/handlers/delete.write.ts +4 -3
  186. package/packages/bundled-features/src/secrets/handlers/rotate.job.ts +13 -8
  187. package/packages/bundled-features/src/secrets/handlers/set.write.ts +4 -3
  188. package/packages/bundled-features/src/secrets/secrets-context.ts +16 -6
  189. package/packages/bundled-features/src/secrets/write-gate.ts +10 -3
  190. package/packages/bundled-features/src/seo/feature.ts +1 -1
  191. package/packages/bundled-features/src/sessions/changes.json +19 -0
  192. package/packages/bundled-features/src/sessions/feature.ts +2 -1
  193. package/packages/bundled-features/src/sessions/handlers/cleanup.job.ts +2 -2
  194. package/packages/bundled-features/src/sessions/handlers/detail.query.ts +5 -2
  195. package/packages/bundled-features/src/sessions/handlers/list.query.ts +5 -2
  196. package/packages/bundled-features/src/sessions/handlers/mine.query.ts +5 -2
  197. package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +3 -3
  198. package/packages/bundled-features/src/sessions/handlers/revoke-all-others.write.ts +1 -1
  199. package/packages/bundled-features/src/sessions/handlers/revoke.write.ts +1 -1
  200. package/packages/bundled-features/src/sessions/index.ts +1 -1
  201. package/packages/bundled-features/src/sessions/screens.ts +8 -4
  202. package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
  203. package/packages/bundled-features/src/sessions/session-revoked-event.ts +1 -1
  204. package/packages/bundled-features/src/sessions/web/session-device-cell.tsx +10 -12
  205. package/packages/bundled-features/src/sessions/web/user-agent.ts +12 -0
  206. package/packages/bundled-features/src/shared/index.ts +15 -0
  207. package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +36 -4
  208. package/packages/bundled-features/src/shared/require-for-tenant.ts +7 -5
  209. package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
  210. package/packages/bundled-features/src/shared/stream-tenant-backfill.ts +201 -0
  211. package/packages/bundled-features/src/shared/stream-tenant-derived-state.ts +107 -0
  212. package/packages/bundled-features/src/step-dispatcher/changes.json +6 -0
  213. package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
  214. package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -1
  215. package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
  216. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +1 -1
  217. package/packages/bundled-features/src/subscription-dodo/runtime.ts +7 -7
  218. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +5 -3
  219. package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -7
  220. package/packages/bundled-features/src/tags/feature.ts +1 -0
  221. package/packages/bundled-features/src/template-resolver/api.ts +3 -3
  222. package/packages/bundled-features/src/template-resolver/changes.json +54 -0
  223. package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
  224. package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
  225. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +5 -4
  226. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +2 -2
  227. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +1 -1
  228. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
  229. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +32 -7
  230. package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
  231. package/packages/bundled-features/src/template-resolver/handlers/send-test-shared.ts +1 -1
  232. package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
  233. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +2 -2
  234. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +6 -2
  235. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +4 -2
  236. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +1 -1
  237. package/packages/bundled-features/src/template-resolver/seeding.ts +22 -5
  238. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +68 -19
  239. package/packages/bundled-features/src/template-resolver/template-resources.ts +4 -4
  240. package/packages/bundled-features/src/template-resolver/testing.ts +2 -2
  241. package/packages/bundled-features/src/template-resolver/web/document-insert-fields.tsx +222 -0
  242. package/packages/bundled-features/src/template-resolver/web/document-insert-model.ts +164 -0
  243. package/packages/bundled-features/src/template-resolver/web/document-insert-panel.tsx +115 -0
  244. package/packages/bundled-features/src/template-resolver/web/document-tiptap-bubble.tsx +177 -0
  245. package/packages/bundled-features/src/template-resolver/web/document-tiptap-content.tsx +38 -0
  246. package/packages/bundled-features/src/template-resolver/web/document-tiptap-context.ts +119 -0
  247. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +51 -1098
  248. package/packages/bundled-features/src/template-resolver/web/document-tiptap-helpers.ts +161 -0
  249. package/packages/bundled-features/src/template-resolver/web/document-tiptap-layout.tsx +83 -0
  250. package/packages/bundled-features/src/template-resolver/web/document-tiptap-toolbar-groups.tsx +153 -0
  251. package/packages/bundled-features/src/template-resolver/web/document-tiptap-toolbar.tsx +182 -0
  252. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +18 -0
  253. package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
  254. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +19 -6
  255. package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
  256. package/packages/bundled-features/src/template-resolver/web/use-document-tiptap-model.ts +138 -0
  257. package/packages/bundled-features/src/tenant/changes.json +19 -0
  258. package/packages/bundled-features/src/tenant/feature.ts +1 -0
  259. package/packages/bundled-features/src/tenant/handlers/cancel-invitation.write.ts +1 -1
  260. package/packages/bundled-features/src/tenant/handlers/memberships.query.ts +1 -1
  261. package/packages/bundled-features/src/tenant/handlers/remove-member.write.ts +2 -2
  262. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
  263. package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +1 -1
  264. package/packages/bundled-features/src/tenant/index.ts +5 -0
  265. package/packages/bundled-features/src/tenant/invited-membership.ts +6 -1
  266. package/packages/bundled-features/src/tenant/schema/tenant.ts +4 -1
  267. package/packages/bundled-features/src/tenant/seeding.ts +65 -62
  268. package/packages/bundled-features/src/tenant/stream-tenant-backfill.ts +115 -0
  269. package/packages/bundled-features/src/tenant/testing.ts +77 -0
  270. package/packages/bundled-features/src/tenant-handover/events.ts +1 -1
  271. package/packages/bundled-features/src/tenant-handover/handlers/claim.write.ts +1 -1
  272. package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
  273. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +5 -5
  274. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +22 -10
  275. package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +22 -11
  276. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +23 -12
  277. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +42 -4
  278. package/packages/bundled-features/src/tenant-lifecycle/testing.ts +9 -8
  279. package/packages/bundled-features/src/tier-engine/entity.ts +1 -1
  280. package/packages/bundled-features/src/tier-engine/feature.ts +8 -3
  281. package/packages/bundled-features/src/tier-engine/handlers/active-tier.query.ts +3 -3
  282. package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +7 -1
  283. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +3 -3
  284. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +71 -97
  285. package/packages/bundled-features/src/user/feature.ts +1 -0
  286. package/packages/bundled-features/src/user/handlers/find-for-auth.query.ts +2 -2
  287. package/packages/bundled-features/src/user/handlers/update-roles.ts +2 -2
  288. package/packages/bundled-features/src/user/index.ts +2 -0
  289. package/packages/bundled-features/src/user/invited-global-roles.ts +6 -1
  290. package/packages/bundled-features/src/user/principal-status.ts +1 -1
  291. package/packages/bundled-features/src/user/seeding.ts +24 -6
  292. package/packages/bundled-features/src/user/testing.ts +22 -0
  293. package/packages/bundled-features/src/user-data-rights/COMPLIANCE.md +1 -1
  294. package/packages/bundled-features/src/user-data-rights/README.md +1 -1
  295. package/packages/bundled-features/src/user-data-rights/audit-download.ts +12 -2
  296. package/packages/bundled-features/src/user-data-rights/changes.json +36 -0
  297. package/packages/bundled-features/src/user-data-rights/feature.ts +30 -24
  298. package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +1 -1
  299. package/packages/bundled-features/src/user-data-rights/handlers/deletion-grace-period.ts +1 -1
  300. package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +2 -2
  301. package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +2 -2
  302. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
  303. package/packages/bundled-features/src/user-data-rights/handlers/restrict-account.write.ts +4 -4
  304. package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +12 -6
  305. package/packages/bundled-features/src/user-data-rights/index.ts +8 -1
  306. package/packages/bundled-features/src/user-data-rights/lib/mail-transport-resolver.ts +1 -1
  307. package/packages/bundled-features/src/user-data-rights/lib/storage-provider-resolver.ts +1 -1
  308. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +6 -1
  309. package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +8 -2
  310. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +14 -3
  311. package/packages/bundled-features/src/user-data-rights/run-user-export.ts +11 -1
  312. package/packages/bundled-features/src/user-data-rights/web/constants.ts +16 -0
  313. package/packages/bundled-features/src/user-data-rights/web/index.ts +6 -0
  314. package/packages/bundled-features/src/user-profile/handlers/change-email.write.ts +8 -6
  315. package/packages/bundled-features/src/waitlist/changes.json +18 -0
  316. package/packages/bundled-features/src/waitlist/feature.ts +1 -0
  317. package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +1 -1
  318. package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +2 -2
  319. package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +1 -1
  320. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +34 -10
  321. package/packages/bundled-features/src/waitlist/import.ts +23 -3
  322. package/packages/bundled-features/src/waitlist/legacy-migrate.ts +8 -2
  323. package/packages/bundled-features/src/waitlist/link-on-signup.ts +7 -1
  324. package/packages/bundled-features/src/waitlist/options.ts +2 -1
  325. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  326. package/packages/bundled-features/src/workflow-runner/db/queries/due-runs.ts +1 -1
  327. package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +4 -3
  328. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
  329. package/packages/bundled-features/src/workflow-runner/feature.ts +2 -2
  330. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +8 -8
  331. package/packages/bundled-features/src/workflow-runner/runner.ts +6 -4
  332. package/samples/apps/cap-billing-demo/src/feature.ts +19 -5
  333. package/samples/apps/config-demo/src/app/server.ts +1 -1
  334. package/samples/apps/styleguide/src/app/client.tsx +2 -0
  335. package/samples/apps/styleguide/src/app/seed.ts +6 -4
  336. package/samples/apps/styleguide/src/app/server.ts +6 -3
  337. package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
  338. package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
  339. package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
  340. package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
  341. package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
  342. package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
  343. package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
  344. package/samples/apps/use-all-bundled/bin/main.ts +2 -0
  345. package/samples/apps/use-all-bundled/feature-manifest.json +17 -1
  346. package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
  347. package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
  348. package/samples/apps/use-all-bundled/package.json +2 -1
  349. package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
  350. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
  351. package/samples/apps/use-all-bundled/src/app/seed.ts +12 -2
  352. package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
  353. package/samples/apps/user-data-rights-demo/README.md +1 -1
  354. package/samples/apps/user-data-rights-demo/src/feature.ts +3 -2
  355. package/samples/recipes/api-worker-split/README.md +2 -1
  356. package/samples/recipes/api-worker-split/src/feature.ts +2 -1
  357. package/samples/recipes/cross-feature-events/README.md +5 -5
  358. package/samples/recipes/cross-feature-events/package.json +1 -1
  359. package/samples/recipes/cross-feature-events/src/feature.ts +4 -4
  360. package/samples/recipes/event-sourcing/README.md +5 -5
  361. package/samples/recipes/event-sourcing/package.json +1 -1
  362. package/samples/recipes/event-sourcing/src/feature.ts +15 -15
  363. package/samples/recipes/files-post-processing/README.md +1 -1
  364. package/samples/recipes/files-post-processing/package.json +1 -1
  365. package/samples/recipes/files-post-processing/src/feature.ts +6 -4
  366. package/samples/recipes/golden-path/src/feature.ts +2 -1
  367. package/samples/recipes/lane-routing/package.json +1 -1
  368. package/samples/recipes/legal-pages/README.md +2 -2
  369. package/samples/recipes/lifecycle-hooks/README.md +4 -0
  370. package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
  371. package/samples/recipes/list-table-options/README.md +50 -0
  372. package/samples/recipes/list-table-options/package.json +11 -0
  373. package/samples/recipes/list-table-options/src/feature.ts +221 -0
  374. package/samples/recipes/managed-config/README.md +3 -3
  375. package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
  376. package/samples/recipes/search/README.md +1 -1
  377. package/samples/recipes/search/src/feature.ts +3 -3
  378. package/samples/recipes/secrets-demo/README.md +1 -1
  379. package/samples/recipes/secrets-demo/src/feature.ts +5 -2
  380. package/samples/recipes/state-machine/src/events.ts +4 -3
  381. package/samples/recipes/state-machine/src/reducer.ts +1 -1
  382. package/samples/recipes/tenant-isolation/README.md +1 -0
  383. package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
  384. package/samples/recipes/workflow-engine/src/resume-loop.ts +4 -4
  385. package/samples/recipes/workflow-engine/src/workflow-runner.ts +2 -2
@@ -114,11 +114,11 @@ export function createStripeRuntimes(deps: StripeRuntimeDeps): StripeRuntimes {
114
114
 
115
115
  async function ctxApiKey(ctx: HandlerContext): Promise<string> {
116
116
  let key: string | undefined;
117
- if (ctx.secrets) {
118
- const got = await requireSecretsContext(ctx, SUBSCRIPTION_STRIPE_FEATURE).get(
119
- SYSTEM_TENANT_ID,
120
- deps.apiKeyHandle.name,
121
- );
117
+ if (ctx.services.secrets) {
118
+ const got = await requireSecretsContext(
119
+ { secrets: ctx.services.secrets, user: ctx.user },
120
+ SUBSCRIPTION_STRIPE_FEATURE,
121
+ ).get(SYSTEM_TENANT_ID, deps.apiKeyHandle.name);
122
122
  key = parseStoredSecret(got?.reveal());
123
123
  }
124
124
  if (!key && deps.fallback.apiKey && deps.fallback.apiKey.length > 0) {
@@ -167,8 +167,8 @@ export function createStripeRuntimes(deps: StripeRuntimeDeps): StripeRuntimes {
167
167
  if (hasFallbackKey) return true;
168
168
  // Existence-only probe — called on every billing-plans page render,
169
169
  // must stay un-audited (see SecretsContext.has's own doc comment).
170
- return ctx.secrets
171
- ? await ctx.secrets.has(SYSTEM_TENANT_ID, deps.apiKeyHandle.name)
170
+ return ctx.services.secrets
171
+ ? await ctx.services.secrets.has(SYSTEM_TENANT_ID, deps.apiKeyHandle.name)
172
172
  : false;
173
173
  },
174
174
  },
@@ -54,6 +54,7 @@ function registerTags(
54
54
  ownership: EntityDefinition["access"] | undefined,
55
55
  parents: readonly string[] | undefined,
56
56
  ): void {
57
+ r.adoptsBundledLists();
57
58
  r.describe(
58
59
  "Generic, host-agnostic tagging for any entity. Owns two event-sourced entities — the per-tenant `tag` catalog (`read_tags`, with optional `color` and `scope`) and `tag-assignment` join rows keyed by (entityType, entityId) (`read_tag_assignments`) — so tagging adds NO column to the host entity and needs no relational pivot or JOIN. Catalog screens are declarative (`entityList` + `entityEdit`) and use convention QNs `tag:{create,update,delete}`; TagManager/TagPicker keep `create-tag`/`update-tag`/`delete-tag`. Also: `assign-tag` (idempotent), `remove-tag` (idempotent) and list queries for the catalog and the assignments. Read which tags an entity has, or which entities carry a tag, by listing `tag-assignment` filtered on `entityId` or `tagId` and composing in the read-layer. A tag with empty `scope` is global; a `scope` of an entityType restricts it to that type in the picker. Every path uses one access rule — adopt the host's model with createTagsFeature({ access: { openToAll: { reason } } }) or pin roles with createTagsFeature({ roles }). Pass { toggleable: { default: false } } to make the whole feature tier-gatable via the tier-engine (no host hook).",
59
60
  );
@@ -332,12 +332,12 @@ export class TemplateNotFoundError extends Error {
332
332
  // Single source of truth for "this handler requires template-resolver".
333
333
  // Pattern symmetric with requireTextContent.
334
334
  export function requireTemplateResolver(
335
- ctx: { readonly templateResolver?: TemplateResolverApi } | object,
335
+ ctx: { readonly services: { readonly extensions: Readonly<Record<string, unknown>> } },
336
336
  callerName: string,
337
337
  ): TemplateResolverApi {
338
338
  // @cast-boundary engine-bridge — templateResolver kommt per extraContext
339
- // from App-Bootstrap; the framework container does not know this field on its own.
340
- const api = (ctx as { templateResolver?: TemplateResolverApi }).templateResolver;
339
+ // from App-Bootstrap and lands in services.extensions; the framework does not know its type.
340
+ const api = ctx.services.extensions["templateResolver"] as TemplateResolverApi | undefined;
341
341
  if (!api) {
342
342
  throw new InternalError({
343
343
  message:
@@ -1,4 +1,58 @@
1
1
  [
2
+ {
3
+ "version": "0.390.0",
4
+ "type": "fix",
5
+ "title": "Rail inserts no longer do nothing after the placeholder labels load",
6
+ "detail": "Inserting a placeholder into the subject, preheader, header chip or footer field could silently do nothing once the placeholder labels had loaded, because the replaced editor unregistered the handle of its remounted successor. The editors now report their handle from the React commit instead of tiptap's create and destroy callbacks."
7
+ },
8
+ {
9
+ "version": "0.390.0",
10
+ "type": "fix",
11
+ "title": "Placeholder inserts keep working after a line editor remounts",
12
+ "detail": "When a placeholder line editor (subject, preheader, header chip, footer note) remounted (labels arriving from the async validate query, or a new external value), TipTap could destroy the replaced editor after the new one was created. Its late null report then removed the live handle and placeholder inserts into that field did nothing. A null from a superseded editor is now ignored."
13
+ },
14
+ {
15
+ "version": "0.390.0",
16
+ "type": "improvement",
17
+ "title": "Split DocumentTiptapEditor into a model hook and render functions",
18
+ "detail": "DocumentTiptapEditor and its insert panel compose a model hook, pure helpers and render functions for toolbar, bubble menu, content and insert fields. Rendered markup and behavior are unchanged."
19
+ },
20
+ {
21
+ "version": "0.389.0",
22
+ "type": "fix",
23
+ "title": "copy-locale reads source files from the owning tenant's storage",
24
+ "detail": "The copy of linked files uses readFileRefContent with the provider of the fileRef's tenant. A source file whose fileRef row exists but whose bytes cannot be read fails the copy with template_resource_source_unreadable and releases the files already copied. A deleted fileRef is still left out of the copy."
25
+ },
26
+ {
27
+ "version": "0.389.0",
28
+ "type": "improvement",
29
+ "title": "replaceText explains finds across formatting or placeholders",
30
+ "detail": "replaceText matches inside single text nodes so formatting stays intact. A find that appears in the block line but crosses a formatting change now fails with text_spans_formatting, and a find with a {{placeholder}} fails with placeholder_not_replaceable, both with translated messages. Only a string that is not in the block at all still gives text_not_found."
31
+ },
32
+ {
33
+ "version": "0.389.0",
34
+ "type": "fix",
35
+ "title": "send-test-status follows the tenantEditable roles",
36
+ "detail": "send-test-status takes its access from templateEditAccess(policy.tenantEditable), like send-test. Roles that may send a test mail now get the status, and a TenantAdmin without an edit opt-in gets a 403 on both."
37
+ },
38
+ {
39
+ "version": "0.389.0",
40
+ "type": "fix",
41
+ "title": "overview follows the tenantEditable roles",
42
+ "detail": "The overview query takes its access from templateEditAccess(policy.tenantEditable). An opted-in role lists exactly the released templates, and a TenantAdmin without an opt-in is refused with a 403, matching the screen visibility and the write handlers."
43
+ },
44
+ {
45
+ "version": "0.389.0",
46
+ "type": "fix",
47
+ "title": "overview search and sort use the translated title",
48
+ "detail": "The overview query translates a seeded titleI18nKey with the request locale before it filters by search and sorts by title. A search for the visible title now matches, and the raw key is still searchable."
49
+ },
50
+ {
51
+ "version": "0.389.0",
52
+ "type": "fix",
53
+ "title": "legalContentSeed keeps and can set the content folder",
54
+ "detail": "seedTextBlock no longer resets the folder of an existing block to null when the seed passes no folder. legalContentSeed takes an optional folder, so apps do not need their own seed loop to keep legal blocks in the SystemAdmin content tree."
55
+ },
2
56
  {
3
57
  "version": "0.388.0",
4
58
  "type": "fix",
@@ -389,6 +389,23 @@ function removeBlockOp(
389
389
  return null;
390
390
  }
391
391
 
392
+ export const REPLACE_TEXT_ISSUE_CODES = {
393
+ notFound: "text_not_found",
394
+ spansFormatting: "text_spans_formatting",
395
+ placeholderNotReplaceable: "placeholder_not_replaceable",
396
+ } as const;
397
+
398
+ // document-blocks shows the line across formatting runs with placeholders as {{name}},
399
+ // so a find that is visible there but lies in no single text node needs its own explanation.
400
+ function textNotMatchedCode(block: JSONContent, find: string): string {
401
+ if (!textLinesOf(block).some((line) => line.includes(find))) {
402
+ return REPLACE_TEXT_ISSUE_CODES.notFound;
403
+ }
404
+ return find.includes("{{")
405
+ ? REPLACE_TEXT_ISSUE_CODES.placeholderNotReplaceable
406
+ : REPLACE_TEXT_ISSUE_CODES.spansFormatting;
407
+ }
408
+
392
409
  function replaceTextOp(
393
410
  state: OpState,
394
411
  op: Extract<DocumentOp, { op: "replaceText" }>,
@@ -401,7 +418,9 @@ function replaceTextOp(
401
418
  (sum, node) => sum + occurrencesOf(node.text ?? "", op.find),
402
419
  0,
403
420
  );
404
- if (count === 0) return patchIssue(`${path}.find`, "text_not_found", { find: op.find });
421
+ if (count === 0) {
422
+ return patchIssue(`${path}.find`, textNotMatchedCode(entry.node, op.find), { find: op.find });
423
+ }
405
424
  if (count > 1) return patchIssue(`${path}.find`, "text_ambiguous", { find: op.find, count });
406
425
  entry.node = replaceInContent([entry.node], op.find, op.replace)[0] ?? entry.node;
407
426
  return null;
@@ -29,7 +29,7 @@ import { makeResourceLinkPdfWrite } from "./handlers/resource-link-pdf.write.ts"
29
29
  import { makeResourceRemoveWrite } from "./handlers/resource-remove.write.ts";
30
30
  import { makeResourceUploadWrite } from "./handlers/resource-upload.write.ts";
31
31
  import { makeSendTestWrite } from "./handlers/send-test.write.ts";
32
- import { sendTestStatusQuery } from "./handlers/send-test-status.query.ts";
32
+ import { makeSendTestStatusQuery } from "./handlers/send-test-status.query.ts";
33
33
  import { makeSetWrite } from "./handlers/set.write.ts";
34
34
  import { tenantOverrideCountsQuery } from "./handlers/tenant-override-counts.query.ts";
35
35
  import { tenantOverridesQuery } from "./handlers/tenant-overrides.query.ts";
@@ -191,7 +191,7 @@ export function createTemplateResolverFeature(opts: TemplateResolverOptions = {}
191
191
  preview: r.queryHandler(previewQuery),
192
192
  documentBlocks: r.queryHandler(documentBlocksQuery),
193
193
  exportBundle: r.queryHandler(exportBundleQuery),
194
- sendTestStatus: r.queryHandler(sendTestStatusQuery),
194
+ sendTestStatus: r.queryHandler(makeSendTestStatusQuery(policy)),
195
195
  };
196
196
 
197
197
  for (const collection of collections) {
@@ -14,6 +14,7 @@ import {
14
14
  import { requireForTenant } from "../../shared/index.ts";
15
15
  import type { TemplateWriteScope } from "../layout-regions.ts";
16
16
  import type { LinkedResourceEntry } from "../linked-resources.ts";
17
+ import type { PrepareContext } from "../store-template-resource.ts";
17
18
  import {
18
19
  attachmentLimitFailure,
19
20
  attachmentNeedsMailTemplate,
@@ -48,8 +49,8 @@ export function targetExistsIssue(locale: string): FieldIssue {
48
49
  };
49
50
  }
50
51
 
51
- type BundleImport = {
52
- readonly ctx: HandlerContext;
52
+ type BundleImport<TCtx extends PrepareContext = HandlerContext> = {
53
+ readonly ctx: TCtx;
53
54
  readonly user: SessionUser;
54
55
  readonly tenantId: TenantId;
55
56
  readonly scope: TemplateWriteScope;
@@ -60,7 +61,7 @@ type BundleImport = {
60
61
  type BundleImportStatus = "draft" | "active";
61
62
 
62
63
  async function collectBundleIssues(
63
- args: BundleImport,
64
+ args: BundleImport<PrepareContext>,
64
65
  status: BundleImportStatus,
65
66
  ): Promise<readonly FieldIssue[]> {
66
67
  const { ctx, tenantId, scope, bundle, overwrite } = args;
@@ -113,7 +114,7 @@ function checkBundleAttachments(bundle: TemplateBundle): WriteResult<null> {
113
114
  // beforeTransaction half: validates the bundle and uploads every resource file to the target
114
115
  // tenant's storage. A failed or throwing upload releases the ones before it.
115
116
  export async function prepareTemplateBundleBeforeTransaction(
116
- args: BundleImport & { readonly status: BundleImportStatus },
117
+ args: BundleImport<PrepareContext> & { readonly status: BundleImportStatus },
117
118
  ): Promise<BeforeTransactionResult<PreparedBundleResources>> {
118
119
  const refused =
119
120
  refuseTenantScopeUnderSystemTenant(args.scope, args.tenantId) ??
@@ -1,7 +1,6 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
2
2
  import {
3
3
  defineWriteHandler,
4
- type HandlerContext,
5
4
  type SessionUser,
6
5
  SYSTEM_TENANT_ID,
7
6
  type TenantId,
@@ -18,6 +17,7 @@ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
18
17
  import * as z from "zod";
19
18
  import { requireForTenant } from "../../shared/index.ts";
20
19
  import { type LinkedResourceEntry, parseLinkedResourceEntries } from "../linked-resources.ts";
20
+ import type { PrepareContext } from "../store-template-resource.ts";
21
21
  import {
22
22
  createLinkedResourceCopies,
23
23
  prepareLinkedResourceCopiesBeforeTransaction,
@@ -43,7 +43,7 @@ import {
43
43
  } from "./template-write.ts";
44
44
 
45
45
  type CopyScope = "tenant" | "system";
46
- type WriteCtx = HandlerContext;
46
+ type WriteCtx = PrepareContext;
47
47
 
48
48
  function readDbFor(ctx: WriteCtx, scope: CopyScope) {
49
49
  return scope === "system" ? requireForTenant(ctx, SYSTEM_TENANT_ID).db : ctx.db;
@@ -124,7 +124,7 @@ export const exportBundleQuery = defineQueryHandler({
124
124
  }
125
125
  const attachment = isFileAttachmentEntry(entry);
126
126
  const file = await readTemplateResource(
127
- { db: ctx.db, files: ctx.files, user: query.user },
127
+ { db: ctx.db, services: { files: ctx.services.files, extensions: {} }, user: query.user },
128
128
  attachment ? entry.fileRefId : entry,
129
129
  );
130
130
  if (!file) throw new NotFoundError("template-resource-file", key);
@@ -1,5 +1,6 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { AccessDeniedError } from "@cosmicdrift/kumiko-framework/errors";
3
4
  import * as z from "zod";
4
5
  import { filterDeliveredTo } from "../api.ts";
5
6
  import { type LayoutRegions, readLayoutRegions } from "../layout-regions.ts";
@@ -14,6 +15,7 @@ import {
14
15
  } from "../template-draft.ts";
15
16
  import { type MailLayoutMeta, readMailLayoutMeta } from "../template-meta.ts";
16
17
  import { FALLBACK_LOCALE } from "../web/constants.ts";
18
+ import { TemplateResolverErrors } from "../web/qualified-names.ts";
17
19
  import { localeSchema, templateSlugSchema } from "./shared.ts";
18
20
  import { parseStoredJson } from "./template-gates.ts";
19
21
 
@@ -66,20 +68,29 @@ function layoutFormat(row: WorkingRow): "html" | "mjml" | null {
66
68
  return row.contentFormat === "html" || row.contentFormat === "mjml" ? row.contentFormat : null;
67
69
  }
68
70
 
71
+ type EditScope = "tenant" | "system";
72
+
69
73
  function pickEditedRows(
70
74
  rows: readonly TemplateResourceRow[],
71
- user: { readonly tenantId: string; readonly roles: readonly string[] },
75
+ caller: { readonly tenantId: string; readonly roles: readonly string[] },
76
+ scope: EditScope,
72
77
  locale: string | undefined,
73
78
  ): {
74
79
  readonly tenantRow: WorkingRow | null;
75
80
  readonly systemRow: WorkingRow | null;
76
81
  } {
77
- const tenantRow = user.roles.includes("SystemAdmin")
78
- ? null
79
- : pickByLocale(
80
- rows.filter((row) => row.scope === "tenant"),
81
- locale,
82
- );
82
+ // A SystemAdmin editing the tenant layout sees the system version like a tenant does: served only, no draft.
83
+ const user =
84
+ scope === "system"
85
+ ? caller
86
+ : { ...caller, roles: caller.roles.filter((role) => role !== "SystemAdmin") };
87
+ const tenantRow =
88
+ scope === "system"
89
+ ? null
90
+ : pickByLocale(
91
+ rows.filter((row) => row.scope === "tenant"),
92
+ locale,
93
+ );
83
94
  const systemRow = pickByLocale(
84
95
  rows.filter((row) => row.scope === "system" && !isHiddenSystemDraft(user, row)),
85
96
  locale,
@@ -136,13 +147,21 @@ export const layoutSourceQuery = defineQueryHandler({
136
147
  kind: z.enum(["mail-layout", "pdf-layout"]),
137
148
  slug: templateSlugSchema.default("default"),
138
149
  locale: localeSchema.optional(),
150
+ scope: z.enum(["tenant", "system"]).optional(),
139
151
  }),
140
152
  access: { roles: ["TenantAdmin", "SystemAdmin"] },
141
153
  description:
142
- "Reads the layout the caller edits: the tenant's own version and the system version of one layout slug (each the open draft if there is one, else the stored version, with `active` naming the served version), the header, signature and footer regions of the effective html version, and the locale a save has to use; a SystemAdmin edits the system version and also receives its mjml source. Use it to load the layout editor.",
154
+ "Reads the layout the caller edits: the tenant's own version and the system version of one layout slug (each the open draft if there is one, else the stored version, with `active` naming the served version), the header, signature and footer regions of the effective html version, and the locale a save has to use; a SystemAdmin edits the system version by default and also receives its mjml source; scope tenant loads the SystemAdmin's own tenant layout instead, and scope system is refused for everyone else. Use it to load the layout editor.",
143
155
  handler: async (query, ctx): Promise<LayoutSourceResponse> => {
144
156
  const { kind, slug, locale } = query.payload;
145
- const isSystemAdmin = query.user.roles.includes("SystemAdmin");
157
+ const callerIsSystemAdmin = query.user.roles.includes("SystemAdmin");
158
+ const scope = query.payload.scope ?? (callerIsSystemAdmin ? "system" : "tenant");
159
+ if (scope === "system" && !callerIsSystemAdmin) {
160
+ throw new AccessDeniedError({
161
+ i18nKey: "errors.access.denied",
162
+ details: { reason: TemplateResolverErrors.systemAdminRequired },
163
+ });
164
+ }
146
165
  const policy = deliveryPolicyOf(ctx.registry);
147
166
  const rows = await filterDeliveredTo(
148
167
  ctx.db,
@@ -150,7 +169,13 @@ export const layoutSourceQuery = defineQueryHandler({
150
169
  query.user,
151
170
  await selectMany<TemplateResourceRow>(ctx.db, templateResourcesTable, { kind, slug }),
152
171
  );
153
- const { tenantRow, systemRow } = pickEditedRows(rows, query.user, locale);
154
- return buildLayoutSource({ kind, locale, isSystemAdmin, tenantRow, systemRow });
172
+ const { tenantRow, systemRow } = pickEditedRows(rows, query.user, scope, locale);
173
+ return buildLayoutSource({
174
+ kind,
175
+ locale,
176
+ isSystemAdmin: scope === "system",
177
+ tenantRow,
178
+ systemRow,
179
+ });
155
180
  },
156
181
  });
@@ -1,5 +1,6 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { createI18n } from "@cosmicdrift/kumiko-framework/i18n";
3
4
  import * as z from "zod";
4
5
  import { type TemplateResourceRow, templateResourcesTable } from "../table.ts";
5
6
  import {
@@ -16,7 +17,11 @@ import {
16
17
  parseTemplateDraft,
17
18
  servedVersionOf,
18
19
  } from "../template-draft.ts";
19
- import { mayEditTemplate, type TemplateEditPolicy } from "../template-edit-policy.ts";
20
+ import {
21
+ mayEditTemplate,
22
+ type TemplateEditPolicy,
23
+ templateEditAccess,
24
+ } from "../template-edit-policy.ts";
20
25
  import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.ts";
21
26
  import { collectTemplateUsage, type EffectiveMailLayoutSlugs } from "../template-usage.ts";
22
27
  import {
@@ -204,7 +209,15 @@ function splitDocumentSlug(slug: string): { feature: string; notification: strin
204
209
  : { feature: slug.slice(0, separator), notification: slug.slice(separator + 1) };
205
210
  }
206
211
 
207
- function compareBy(field: SortField, a: OverviewRow, b: OverviewRow): number {
212
+ type ShownTitle = (row: OverviewRow) => string;
213
+
214
+ function compareBy(
215
+ field: SortField,
216
+ a: OverviewRow,
217
+ b: OverviewRow,
218
+ shownTitle: ShownTitle,
219
+ ): number {
220
+ if (field === "title") return shownTitle(a).localeCompare(shownTitle(b));
208
221
  return String(a[field]).localeCompare(String(b[field]));
209
222
  }
210
223
 
@@ -351,11 +364,17 @@ function filterRows(
351
364
  search: string | undefined,
352
365
  filters: readonly OverviewFilter[],
353
366
  layoutSlug: string | undefined,
367
+ shownTitle: ShownTitle,
354
368
  ): OverviewRow[] {
355
369
  const needle = search?.trim().toLowerCase() ?? "";
356
370
  return rows.filter((row) => {
357
371
  if (layoutSlug !== undefined && row.layoutSlug !== layoutSlug) return false;
358
- if (needle !== "" && !`${row.title} ${row.slug}`.toLowerCase().includes(needle)) return false;
372
+ if (
373
+ needle !== "" &&
374
+ !`${shownTitle(row)} ${row.title} ${row.slug}`.toLowerCase().includes(needle)
375
+ ) {
376
+ return false;
377
+ }
359
378
  return filters.every((filter) => filter.value.includes(String(row[filter.field])));
360
379
  });
361
380
  }
@@ -379,7 +398,7 @@ export function makeOverviewQuery(policy: TemplateEditPolicy) {
379
398
  offset: z.number().int().min(0).optional(),
380
399
  totalCount: z.boolean().optional(),
381
400
  }),
382
- access: { roles: ["TenantAdmin", "SystemAdmin"] },
401
+ access: templateEditAccess(policy.tenantEditable),
383
402
  description:
384
403
  "Lists mail, PDF and layout templates as one row per slug and kind for the caller's tenant (optionally only the mails and PDFs using one layout slug): where the effective version comes from (own override or system), its locales, status and version (the served one; `draftVersion` is set when an unpublished draft waits beside it, `activeSince` says since when the served version applies), the system version it overrides, the layout it uses and which notifications or templates use it; templates of notifications registered with `systemOnly` are listed for SystemAdmins only, and a TenantAdmin sees only the templates the app lists in tenantEditable; a tenant override of a template that is not listed is not delivered, so the row shows the system version with `lockedOverride` true (SystemAdmin only); use it to browse and filter all templates at a glance.",
385
404
  handler: async (query, ctx) => {
@@ -393,7 +412,7 @@ export function makeOverviewQuery(policy: TemplateEditPolicy) {
393
412
  const truncated = fetchedRows.length > OVERVIEW_ROW_LIMIT;
394
413
  const rows = truncated ? fetchedRows.slice(0, OVERVIEW_ROW_LIMIT) : fetchedRows;
395
414
  if (truncated) {
396
- ctx.log?.warn("template overview: row limit reached, result is incomplete", {
415
+ ctx.obs.log?.warn("template overview: row limit reached, result is incomplete", {
397
416
  limit: OVERVIEW_ROW_LIMIT,
398
417
  });
399
418
  }
@@ -415,21 +434,27 @@ export function makeOverviewQuery(policy: TemplateEditPolicy) {
415
434
  ? withOverriddenBy(
416
435
  rowsWithUsage,
417
436
  tenantOverrideCountsResultSchema.parse(
418
- await ctx.query(TemplateResolverQueries.tenantOverrideCounts, {}),
437
+ await ctx.dispatch.query(TemplateResolverQueries.tenantOverrideCounts, {}),
419
438
  ),
420
439
  )
421
440
  : rowsWithUsage;
441
+ // A seeded title can be an i18n key; search and sort must see what the admin sees.
442
+ const i18n = createI18n(ctx.registry, { defaultLocale: "en" });
443
+ const shownTitle: ShownTitle = (row) => i18n.t(row.title, ctx.locale);
422
444
  const filtered = filterRows(
423
445
  overviewRows,
424
446
  payload.search,
425
447
  payload.filters ?? [],
426
448
  payload.layoutSlug,
449
+ shownTitle,
427
450
  );
428
451
 
429
452
  const sortField: SortField =
430
453
  payload.sort !== undefined && isSortField(payload.sort) ? payload.sort : "modifiedAt";
431
454
  const direction = (payload.sortDirection ?? "desc") === "desc" ? -1 : 1;
432
- filtered.sort((a, b) => direction * compareBy(sortField, a, b) || a.id.localeCompare(b.id));
455
+ filtered.sort(
456
+ (a, b) => direction * compareBy(sortField, a, b, shownTitle) || a.id.localeCompare(b.id),
457
+ );
433
458
 
434
459
  const offset = payload.offset ?? 0;
435
460
  return {
@@ -40,7 +40,7 @@ export function makePatchDraftWrite(policy: TemplateEditPolicy) {
40
40
  }),
41
41
  access: templateEditAccess(policy.tenantEditable),
42
42
  description:
43
- "Changes a mail or PDF document template with structured operations instead of a full rewrite and saves the result as the caller's tenant draft (never published, an active version keeps being served until publish): insertBlock after a block id (null = at the start; paragraph, heading, image, button, divider, pageBreak), replaceText inside one block (the text must occur exactly once), removeBlock, setResource on an image block, setMeta (flat merge, null removes a key) and setVariable (adds, replaces or with null removes a variable definition; refused when a notification declares the variables). Block ids and base come from document-blocks and refer to the state that was read; if the template changed since, the call fails with version_conflict and nothing is written. A patch that removes a required placeholder is refused. A TenantAdmin may patch only templates the app lists in tenantEditable, otherwise SystemAdmin only. Returns the written row.",
43
+ "Changes a mail or PDF document template with structured operations instead of a full rewrite and saves the result as the caller's tenant draft (never published, an active version keeps being served until publish): insertBlock after a block id (null = at the start; paragraph, heading, image, button, divider, pageBreak), replaceText inside one block (the text must occur exactly once within one formatting run and cannot contain a {{placeholder}}: a find across a formatting change fails with text_spans_formatting, a find with a placeholder with placeholder_not_replaceable, and the formatting of the replaced text is kept), removeBlock, setResource on an image block, setMeta (flat merge, null removes a key) and setVariable (adds, replaces or with null removes a variable definition; refused when a notification declares the variables). Block ids and base come from document-blocks and refer to the state that was read; if the template changed since, the call fails with version_conflict and nothing is written. A patch that removes a required placeholder is refused. A TenantAdmin may patch only templates the app lists in tenantEditable, otherwise SystemAdmin only. Returns the written row.",
44
44
  handler: async (event, ctx) => {
45
45
  const { slug, kind, locale, base, ops } = event.payload;
46
46
  const refused = refuseTemplateEdit(policy, event.user, { slug, kind });
@@ -17,7 +17,7 @@ export function testMailChannelContext(ctx: HandlerContext, user: SessionUser):
17
17
  registry: ctx.registry,
18
18
  sseBroker: undefined,
19
19
  tenantId: user.tenantId,
20
- secrets: ctx.secrets,
20
+ secrets: ctx.services.secrets,
21
21
  config: ctx.config,
22
22
  readAttachment: createEmailAttachmentReader(
23
23
  ctx.db,
@@ -1,5 +1,6 @@
1
1
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import * as z from "zod";
3
+ import { type TemplateEditPolicy, templateEditAccess } from "../template-edit-policy.ts";
3
4
  import {
4
5
  emailChannelOf,
5
6
  resolveTestMailAddress,
@@ -10,22 +11,24 @@ export type SendTestStatus =
10
11
  | { readonly canSend: true; readonly reason: null }
11
12
  | { readonly canSend: false; readonly reason: "no_channel" | "no_address" };
12
13
 
13
- export const sendTestStatusQuery = defineQueryHandler({
14
- name: "send-test-status",
15
- schema: z.object({}),
16
- access: { roles: ["TenantAdmin", "SystemAdmin"] },
17
- description:
18
- "Tells whether send-test can reach the calling user: canSend is true when an email channel is mounted and resolves an address for the caller, otherwise reason is no_channel or no_address; the address itself is never returned.",
19
- handler: async (query, ctx): Promise<SendTestStatus> => {
20
- const channel = emailChannelOf(ctx.registry);
21
- if (channel === undefined) return { canSend: false, reason: "no_channel" };
22
- const address = await resolveTestMailAddress(
23
- channel,
24
- testMailChannelContext(ctx, query.user),
25
- query.user,
26
- );
27
- return address === null
28
- ? { canSend: false, reason: "no_address" }
29
- : { canSend: true, reason: null };
30
- },
31
- });
14
+ export function makeSendTestStatusQuery(policy: TemplateEditPolicy) {
15
+ return defineQueryHandler({
16
+ name: "send-test-status",
17
+ schema: z.object({}),
18
+ access: templateEditAccess(policy.tenantEditable),
19
+ description:
20
+ "Tells whether send-test can reach the calling user: canSend is true when an email channel is mounted and resolves an address for the caller, otherwise reason is no_channel or no_address; the address itself is never returned.",
21
+ handler: async (query, ctx): Promise<SendTestStatus> => {
22
+ const channel = emailChannelOf(ctx.registry);
23
+ if (channel === undefined) return { canSend: false, reason: "no_channel" };
24
+ const address = await resolveTestMailAddress(
25
+ channel,
26
+ testMailChannelContext(ctx, query.user),
27
+ query.user,
28
+ );
29
+ return address === null
30
+ ? { canSend: false, reason: "no_address" }
31
+ : { canSend: true, reason: null };
32
+ },
33
+ });
34
+ }
@@ -112,7 +112,7 @@ export function makeSendTestWrite(policy: TemplateEditPolicy) {
112
112
  sentStatus = result.status;
113
113
  } catch (error) {
114
114
  // Class and code only: the message can carry the address or a provider response body.
115
- ctx.log?.warn("template test mail: channel threw", {
115
+ ctx.obs.log?.warn("template test mail: channel threw", {
116
116
  tenantId: event.user.tenantId,
117
117
  errorClass: error instanceof Error ? error.name : typeof error,
118
118
  errorCode: errorCodeOf(error),
@@ -120,7 +120,7 @@ export function makeSendTestWrite(policy: TemplateEditPolicy) {
120
120
  return testMailFailed("channel_error");
121
121
  }
122
122
  if (sentStatus !== "sent") {
123
- ctx.log?.warn("template test mail: channel did not send", {
123
+ ctx.obs.log?.warn("template test mail: channel did not send", {
124
124
  tenantId: event.user.tenantId,
125
125
  status: sentStatus,
126
126
  });
@@ -10,7 +10,9 @@ const tenantIdsSchema = z.array(z.string());
10
10
 
11
11
  type TenantIdQueryContext = {
12
12
  readonly registry: { getQueryHandler(name: string): unknown };
13
- readonly query: (name: string, payload: Record<string, never>) => Promise<unknown>;
13
+ readonly dispatch: {
14
+ readonly query: (name: string, payload: Record<string, never>) => Promise<unknown>;
15
+ };
14
16
  };
15
17
 
16
18
  /** Null when the tenant feature is not installed: every tenant row then counts. */
@@ -18,7 +20,9 @@ export async function loadEnabledTenantIds(
18
20
  ctx: TenantIdQueryContext,
19
21
  ): Promise<ReadonlySet<string> | null> {
20
22
  if (ctx.registry.getQueryHandler(TenantQueries.activeTenantIds) === undefined) return null;
21
- return new Set(tenantIdsSchema.parse(await ctx.query(TenantQueries.activeTenantIds, {})));
23
+ return new Set(
24
+ tenantIdsSchema.parse(await ctx.dispatch.query(TenantQueries.activeTenantIds, {})),
25
+ );
22
26
  }
23
27
 
24
28
  export type OverrideCandidate = Pick<TemplateResourceRow, "tenantId" | "status" | "version">;
@@ -31,7 +31,9 @@ function chunksOf<T>(items: readonly T[], size: number): T[][] {
31
31
  }
32
32
 
33
33
  type DirectoryQueryContext = {
34
- readonly query: (name: string, payload: { ids: string[] }) => Promise<unknown>;
34
+ readonly dispatch: {
35
+ readonly query: (name: string, payload: { ids: string[] }) => Promise<unknown>;
36
+ };
35
37
  };
36
38
 
37
39
  // Looks up only the ids the overrides need, so labels never depend on the directory's default cap.
@@ -42,7 +44,7 @@ async function labelsFor(
42
44
  ): Promise<ReadonlyMap<string, string>> {
43
45
  const labels = new Map<string, string>();
44
46
  for (const chunk of chunksOf([...ids], MAX_LIST_LIMIT)) {
45
- const raw = await ctx.query(queryName, { ids: chunk });
47
+ const raw = await ctx.dispatch.query(queryName, { ids: chunk });
46
48
  for (const row of directorySchema.parse(raw).rows) labels.set(row.id, row.label);
47
49
  }
48
50
  return labels;
@@ -28,7 +28,7 @@ export function makeUsageQuery(policy: TemplateEditPolicy) {
28
28
  { orderBy: { col: "id", direction: "asc" }, limit: MAIL_ROW_LIMIT + 1 },
29
29
  );
30
30
  if (fetchedRows.length > MAIL_ROW_LIMIT) {
31
- ctx.log?.warn("template usage: row limit reached, result is incomplete", {
31
+ ctx.obs.log?.warn("template usage: row limit reached, result is incomplete", {
32
32
  limit: MAIL_ROW_LIMIT,
33
33
  });
34
34
  }