@cosmicdrift/kumiko-samples 0.361.0 → 0.363.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +17 -6
  3. package/packages/bundled-features/src/admin-shell/changes.json +6 -0
  4. package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
  5. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
  6. package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
  7. package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
  8. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +14 -0
  10. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  11. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  12. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  13. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  14. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  15. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  16. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  17. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +3 -12
  18. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  19. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  20. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  21. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  22. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  23. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  24. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  25. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  26. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  27. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  28. package/packages/bundled-features/src/billing-foundation/changes.json +7 -0
  29. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  30. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +38 -44
  31. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  32. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +115 -0
  33. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  34. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +30 -30
  35. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  36. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  37. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +2 -2
  38. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  39. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  40. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  41. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +2 -2
  42. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  43. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +3 -6
  44. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  45. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  46. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  47. package/packages/bundled-features/src/channel-email/email-channel.ts +122 -29
  48. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  49. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  50. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  51. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  52. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  53. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  54. package/packages/bundled-features/src/config/handlers/set.write.ts +1 -5
  55. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  56. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  57. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  58. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  59. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  60. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  61. package/packages/bundled-features/src/delivery/attachment-ref.ts +84 -0
  62. package/packages/bundled-features/src/delivery/changes.json +48 -0
  63. package/packages/bundled-features/src/delivery/channel-context.ts +129 -4
  64. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  65. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  66. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  67. package/packages/bundled-features/src/delivery/index.ts +17 -2
  68. package/packages/bundled-features/src/delivery/jobs.ts +48 -5
  69. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  70. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  71. package/packages/bundled-features/src/delivery/types.ts +28 -18
  72. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  73. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  74. package/packages/bundled-features/src/document-ingest-foundation/aggregate-id.ts +10 -0
  75. package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
  76. package/packages/bundled-features/src/document-ingest-foundation/events.ts +3 -1
  77. package/packages/bundled-features/src/document-ingest-foundation/feature.ts +5 -3
  78. package/packages/bundled-features/src/document-ingest-foundation/index.ts +1 -0
  79. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  80. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  81. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  82. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -0
  83. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  84. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  85. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  86. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  87. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  88. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  89. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  90. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  91. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  92. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  93. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  94. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  95. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  96. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  97. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +40 -25
  98. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  99. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  100. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  101. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  102. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  103. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  104. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  105. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  106. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  107. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  108. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  109. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  110. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  111. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  112. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  113. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  114. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  115. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  116. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  117. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  118. package/packages/bundled-features/src/renderer-foundation/types.ts +132 -35
  119. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  120. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  121. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  122. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  123. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  124. package/packages/bundled-features/src/shared/index.ts +6 -0
  125. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  126. package/packages/bundled-features/src/shared/mail-template-body.ts +25 -0
  127. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  128. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  129. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  130. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  131. package/packages/bundled-features/src/template-resolver/README.md +63 -7
  132. package/packages/bundled-features/src/template-resolver/api.ts +83 -8
  133. package/packages/bundled-features/src/template-resolver/changes.json +118 -0
  134. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
  135. package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
  136. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  137. package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
  138. package/packages/bundled-features/src/template-resolver/feature.ts +56 -6
  139. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  140. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  141. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +142 -0
  142. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
  143. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
  144. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
  145. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  146. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  147. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +108 -0
  148. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
  149. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -0
  150. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  151. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  152. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  153. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  154. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  155. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  156. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +106 -0
  157. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  158. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  159. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +184 -0
  160. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
  161. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
  162. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
  163. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  164. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +15 -76
  165. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
  166. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  167. package/packages/bundled-features/src/template-resolver/index.ts +100 -0
  168. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  169. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  170. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  171. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  172. package/packages/bundled-features/src/template-resolver/qualified-names.ts +30 -0
  173. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  174. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  175. package/packages/bundled-features/src/template-resolver/seeding.ts +147 -17
  176. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  177. package/packages/bundled-features/src/template-resolver/table.ts +14 -0
  178. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  179. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  180. package/packages/bundled-features/src/template-resolver/template-meta.ts +129 -0
  181. package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
  182. package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
  183. package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
  184. package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
  185. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  186. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
  187. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
  188. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
  189. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
  190. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
  191. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  192. package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
  193. package/packages/bundled-features/src/template-resolver/web/index.ts +19 -0
  194. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  195. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +169 -0
  196. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  197. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  198. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  199. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  200. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  201. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  202. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  203. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  204. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  205. package/packages/bundled-features/src/user-data-rights/changes.json +14 -0
  206. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  207. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  208. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  209. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  210. package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +5 -1
  211. package/packages/bundled-features/src/user-data-rights/handlers/list-download-attempts.query.ts +13 -1
  212. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  213. package/packages/bundled-features/src/user-data-rights/index.ts +1 -0
  214. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  215. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  216. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  217. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  218. package/packages/bundled-features/src/user-data-rights/schema/download-attempt.ts +6 -12
  219. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  220. package/packages/bundled-features/src/user-data-rights-defaults/feature.ts +10 -1
  221. package/packages/bundled-features/src/user-data-rights-defaults/hooks/download-attempt.userdata-hook.ts +58 -0
  222. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  223. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  224. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  225. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  226. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  227. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +1 -2
  228. package/samples/apps/ui-walkthrough/package.json +2 -0
  229. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  230. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  231. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +154 -0
  232. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  233. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  234. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +354 -0
  235. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -0
  236. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  237. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  238. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  239. package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +1 -1
  240. package/samples/apps/ui-walkthrough/src/run-config.ts +37 -1
  241. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  242. package/samples/apps/use-all-bundled/feature-manifest.json +123 -22
  243. package/samples/apps/use-all-bundled/kumiko/migrations/0044_download-attempt-ip-bidx.rebuild.json +4 -0
  244. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  245. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  246. package/samples/apps/use-all-bundled/package.json +2 -0
  247. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  248. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  249. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  250. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  251. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  252. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  253. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  254. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  255. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  256. package/samples/recipes/delivery-notifications/README.md +26 -25
  257. package/samples/recipes/delivery-notifications/package.json +2 -1
  258. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  259. package/samples/recipes/seed-migration/README.md +1 -1
  260. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  261. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  262. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  263. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  264. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  265. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -0,0 +1,61 @@
1
+ // @runtime client
2
+ import type { ContentEditorProps } from "@cosmicdrift/kumiko-renderer";
3
+
4
+ export type DocumentToolbarBlock =
5
+ | "image"
6
+ | "table"
7
+ | "button"
8
+ | "divider"
9
+ | "repeat"
10
+ | "pageBreak"
11
+ | "align";
12
+
13
+ /** Imperative access for an outside panel (placeholder list, resource list). */
14
+ export type DocumentEditorHandle = {
15
+ readonly insertPlaceholder: (name: string) => void;
16
+ readonly insertResource: (key: string, alt?: string) => void;
17
+ readonly focus: () => void;
18
+ };
19
+
20
+ export type DocumentResourcePreview = {
21
+ /** Shown as the image source in the editor only; never written to the document. */
22
+ readonly src: string;
23
+ readonly caption?: string;
24
+ };
25
+
26
+ /** "default" keeps the compact bordered editor; "canvas" renders a flat text
27
+ * toolbar and a paper-like page with block labels. */
28
+ export type DocumentEditorChrome = "default" | "canvas";
29
+
30
+ export type DocumentEditorProps = ContentEditorProps & {
31
+ readonly blocks?: readonly DocumentToolbarBlock[];
32
+ /** Display label per variable name, as inserted (list item fields use the
33
+ * dotted form, e.g. "lines.description"). A chip shows label and name. */
34
+ readonly variableLabels?: Readonly<Record<string, string>>;
35
+ /** Variable names rendered as system chips (a different tint). */
36
+ readonly systemVariables?: readonly string[];
37
+ /** Image previews by resource key; resources without one show a key placeholder. */
38
+ readonly resourcePreviews?: Readonly<Record<string, DocumentResourcePreview>>;
39
+ readonly chrome?: DocumentEditorChrome;
40
+ /** "compact" renders the placeholder and block buttons icon-only with a tooltip, for narrow columns. Default "full". */
41
+ readonly toolbar?: "full" | "compact";
42
+ /** Called with the handle once the editor exists and with null when it is destroyed. */
43
+ readonly onEditorReady?: (handle: DocumentEditorHandle) => void;
44
+ };
45
+
46
+ export const MAIL_DOCUMENT_BLOCKS = [
47
+ "image",
48
+ "table",
49
+ "button",
50
+ "divider",
51
+ "repeat",
52
+ ] as const satisfies readonly DocumentToolbarBlock[];
53
+
54
+ export const PDF_DOCUMENT_BLOCKS = [
55
+ "align",
56
+ "image",
57
+ "table",
58
+ "divider",
59
+ "repeat",
60
+ "pageBreak",
61
+ ] as const satisfies readonly DocumentToolbarBlock[];
@@ -0,0 +1,26 @@
1
+ // @runtime client
2
+ import type { Editor } from "@tiptap/core";
3
+ import { isDocumentReferenceName, isDocumentResourceKey } from "../document-schema.js";
4
+ import type { DocumentEditorHandle } from "./document-toolbar-blocks.js";
5
+
6
+ export function createEditorHandle(editor: Editor): DocumentEditorHandle {
7
+ return {
8
+ insertPlaceholder: (name) => {
9
+ // skip: an invalid variable name is not insertable
10
+ if (!isDocumentReferenceName(name)) return;
11
+ editor.chain().focus().insertContent({ type: "variable", attrs: { name } }).run();
12
+ },
13
+ insertResource: (key, alt) => {
14
+ // skip: an invalid resource key is not insertable
15
+ if (!isDocumentResourceKey(key)) return;
16
+ editor
17
+ .chain()
18
+ .focus()
19
+ .insertContent({ type: "resource", attrs: { resource: key, alt: alt ?? null } })
20
+ .run();
21
+ },
22
+ focus: () => {
23
+ editor.commands.focus();
24
+ },
25
+ };
26
+ }
@@ -9,12 +9,112 @@ import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
9
9
 
10
10
  export const defaultTranslations: TranslationsByLocale = {
11
11
  en: {
12
+ "template-resolver.documentEditor.button": "Button",
13
+ "template-resolver.documentEditor.buttonDefaultText": "Confirm",
14
+ "template-resolver.documentEditor.buttonTarget": "Button target",
15
+ "template-resolver.documentEditor.buttonText": "Button text",
16
+ "template-resolver.documentEditor.cancel": "Cancel",
17
+ "template-resolver.documentEditor.divider": "Divider",
18
+ "template-resolver.documentEditor.heading3": "Heading 3",
19
+ "template-resolver.documentEditor.image": "Image",
20
+ "template-resolver.documentEditor.imageAlt": "Alt text (optional)",
21
+ "template-resolver.documentEditor.insert": "Insert",
22
+ "template-resolver.documentEditor.insertPlaceholder": "Placeholder",
23
+ "template-resolver.documentEditor.invalidImageAlt": "The alt text must not contain < or >.",
24
+ "template-resolver.documentEditor.invalidLink":
25
+ "This link target is not allowed. Use https, http, mailto, tel or a placeholder.",
26
+ "template-resolver.documentEditor.invalidResourceKey": "This resource key is not valid.",
27
+ "template-resolver.documentEditor.link": "Link",
28
+ "template-resolver.documentEditor.linkHref": "Link target",
29
+ "template-resolver.documentEditor.linkHrefPlaceholder": "https://",
30
+ "template-resolver.documentEditor.noVariables": "No matching variables are available here.",
31
+ "template-resolver.documentEditor.pageBreak": "Page break",
32
+ "template-resolver.documentEditor.align": "Align",
33
+ "template-resolver.documentEditor.alignLeft": "Left",
34
+ "template-resolver.documentEditor.alignCenter": "Centered",
35
+ "template-resolver.documentEditor.alignRight": "Right",
36
+ "template-resolver.documentEditor.alignJustify": "Justified",
37
+ "template-resolver.documentEditor.repeatBarList": "Repeat · {list}",
38
+ "template-resolver.documentEditor.repeatHeader": "Repeat header row",
39
+ "template-resolver.documentEditor.keepRowsTogether": "Keep rows together",
40
+ "template-resolver.documentEditor.paragraph": "Paragraph",
41
+ "template-resolver.documentEditor.paragraphStyle": "Paragraph style",
42
+ "template-resolver.documentEditor.placeholder": "Write your text…",
43
+ "template-resolver.documentEditor.removeLink": "Remove link",
44
+ "template-resolver.documentEditor.repeat": "Repeat",
45
+ "template-resolver.documentEditor.repeatList": "List to repeat",
46
+ "template-resolver.documentEditor.resourceKey": "Resource key",
47
+ "template-resolver.documentEditor.table": "Table",
12
48
  "template-resolver.editor.titleLabel": "Title",
13
49
  "template-resolver.editor.contentLabel": "Content",
14
50
  "template-resolver.editor.save": "Save",
15
51
  "template-resolver.editor.saving": "Saving…",
16
52
  "template-resolver.editor.created": "Created.",
17
53
  "template-resolver.editor.saved": "Saved.",
54
+ "templateResolver.errors.document.buttonHrefNotPlaceholder":
55
+ "The button target “{detail}” must be a placeholder.",
56
+ "templateResolver.errors.document.disallowedLinkHref":
57
+ "Link target “{detail}” is not allowed; use https, http, mailto, tel or a placeholder.",
58
+ "templateResolver.errors.document.documentRequiresHtmlFormat":
59
+ "This template kind requires the html format, got “{detail}”.",
60
+ "templateResolver.errors.document.layoutRequiresMjmlFormat":
61
+ "Mail layouts require the mjml format, got “{detail}”.",
62
+ "templateResolver.errors.document.layoutInvalidStructure":
63
+ "The layout may only contain the header, content, signature and footer regions: {detail}.",
64
+ "templateResolver.errors.document.layoutMissingContentSlot":
65
+ "The layout needs its content slot: {detail}.",
66
+ "templateResolver.errors.document.layoutSourceSystemOnly":
67
+ "Only system layouts may use {detail} source; use the structured html layout.",
68
+ "templateResolver.errors.template.targetExists":
69
+ "A template for locale “{detail}” already exists; overwrite it explicitly to replace it.",
70
+ "templateResolver.warnings.resourceNotUploaded": "Image not uploaded: {detail}",
71
+ "templateResolver.checks.resourcesResolved": "All images are available",
72
+ "templateResolver.checks.textPresent": "A plain-text version is present",
73
+ "templateResolver.checks.sizeUnderCap": "The mail is within the 10 MiB size limit",
74
+ "templateResolver.checks.requiredPlaceholders": "All required placeholders are used",
75
+ "templateResolver.errors.preview.renderFailed": "The preview could not be rendered.",
76
+ "templateResolver.errors.testMail.noChannel":
77
+ "No email channel is set up, so no test mail can be sent.",
78
+ "templateResolver.errors.testMail.noAddress":
79
+ "Your account has no email address to send the test mail to.",
80
+ "templateResolver.errors.testMail.failed": "The test mail could not be sent.",
81
+ "templateResolver.errors.resource.tooLarge": "The image is too large.",
82
+ "templateResolver.errors.resource.unsupportedImage":
83
+ "This file is not a supported image. Use PNG, JPEG, WebP or GIF.",
84
+ "templateResolver.errors.template.systemResourcesNeedSystemTenant":
85
+ "Resource files can only be imported as system templates by a user of the system tenant.",
86
+ "templateResolver.errors.collection.kindNotAllowed":
87
+ "“{detail}” templates are written through their own editor, not through a collection.",
88
+ "templateResolver.errors.noSystemTemplate": "There is no active system template to reset to.",
89
+ "templateResolver.errors.template.invalidMeta": "Invalid template settings: {detail}",
90
+ "templateResolver.errors.template.unknownPlaceholders": "Unknown placeholders: {detail}",
91
+ "templateResolver.errors.template.missingRequiredPlaceholders":
92
+ "Required placeholders are not used: {detail}",
93
+ "templateResolver.errors.document.embeddedContent":
94
+ "<{detail}> elements are not allowed in template bodies.",
95
+ "templateResolver.errors.document.unsafeAttributeValue":
96
+ "The attribute value “{detail}” must not contain < or >.",
97
+ "templateResolver.errors.document.eventHandlerAttribute":
98
+ "Inline event handler “{detail}” is not allowed.",
99
+ "templateResolver.errors.document.externalImageSource":
100
+ "Images must reference an uploaded resource; image source “{detail}” is not allowed.",
101
+ "templateResolver.errors.document.imageWithoutResource":
102
+ "Images must reference an uploaded resource.",
103
+ "templateResolver.errors.document.invalidReferenceName": "“{detail}” is not a valid name.",
104
+ "templateResolver.errors.document.scriptElement":
105
+ "<script> elements are not allowed in template bodies.",
106
+ "templateResolver.errors.attachment.unsupportedType":
107
+ "Attachments must be a PDF, PNG or JPEG file.",
108
+ "templateResolver.errors.attachment.limit": "A template can have at most {max} attachments.",
109
+ "templateResolver.errors.attachment.invalid":
110
+ "The attachment needs a template, a key and a valid file name.",
111
+ "templateResolver.errors.attachment.pdfTemplateMissing":
112
+ "There is no PDF template “{slug}” for this tenant or the system.",
113
+ "template-resolver.documentEditor.htmlSource": "HTML source",
114
+ "template-resolver.documentEditor.removeBlock": "Remove block",
115
+ "template-resolver.documentEditor.selectedBlock": "Selected block",
116
+ "template-resolver.documentEditor.showHtml": "HTML",
117
+ "template-resolver.documentEditor.underline": "Underline",
18
118
  "template-resolver.editor.saveFailed": "Save failed.",
19
119
  "template-resolver.editor.networkError": "Network error while saving.",
20
120
  "template-resolver.editor.loading": "Loading current version…",
@@ -5,4 +5,23 @@
5
5
  // `@cosmicdrift/kumiko-bundled-features/template-resolver` and has no React or
6
6
  // DOM deps.
7
7
 
8
+ export {
9
+ TemplateResolverErrors,
10
+ TemplateResolverHandlers,
11
+ TemplateResolverQueries,
12
+ } from "../qualified-names.js";
8
13
  export { textBlocksClient } from "./client-plugin.js";
14
+ export { DocumentContentEditor } from "./document-editor.js";
15
+ export {
16
+ type DocumentEditorChrome,
17
+ type DocumentEditorHandle,
18
+ type DocumentEditorProps,
19
+ type DocumentResourcePreview,
20
+ type DocumentToolbarBlock,
21
+ MAIL_DOCUMENT_BLOCKS,
22
+ PDF_DOCUMENT_BLOCKS,
23
+ } from "./document-toolbar-blocks.js";
24
+ export {
25
+ PlaceholderLineEditor,
26
+ type PlaceholderLineEditorProps,
27
+ } from "./placeholder-line-editor-lazy.js";
@@ -0,0 +1,36 @@
1
+ // @runtime client
2
+ //
3
+ // Lazy boundary for the placeholder line editor: tiptap loads with the chunk,
4
+ // the fallback is a plain text input holding the same `{{name}}` text.
5
+ // Never import placeholder-line-editor statically here.
6
+
7
+ import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
8
+ import { lazy, type ReactNode, Suspense } from "react";
9
+ import type { PlaceholderLineEditorProps } from "./placeholder-line-editor.js";
10
+
11
+ export type { PlaceholderLineEditorProps } from "./placeholder-line-editor.js";
12
+
13
+ const LazyPlaceholderLineEditor = lazy(() => import("./placeholder-line-editor.js"));
14
+
15
+ function PlainLineFallback({
16
+ id,
17
+ value,
18
+ onChange,
19
+ readOnly = false,
20
+ "aria-label": ariaLabel,
21
+ }: PlaceholderLineEditorProps): ReactNode {
22
+ const { Field, Input } = usePrimitives();
23
+ return (
24
+ <Field id={id} label={ariaLabel} hideLabel>
25
+ <Input kind="text" id={id} name={id} value={value} onChange={onChange} disabled={readOnly} />
26
+ </Field>
27
+ );
28
+ }
29
+
30
+ export function PlaceholderLineEditor(props: PlaceholderLineEditorProps): ReactNode {
31
+ return (
32
+ <Suspense fallback={<PlainLineFallback {...props} />}>
33
+ <LazyPlaceholderLineEditor {...props} />
34
+ </Suspense>
35
+ );
36
+ }
@@ -0,0 +1,169 @@
1
+ // @runtime client
2
+ //
3
+ // Single-line editor for plain-text fields that carry {{placeholders}}
4
+ // (subject, preheader). The stored value stays plain text; placeholders show
5
+ // as chips while editing. Lives in the same lazy chunk as the document editor.
6
+
7
+ import { cn } from "@cosmicdrift/kumiko-renderer-web";
8
+ import { Extension, Node } from "@tiptap/core";
9
+ import { UndoRedo } from "@tiptap/extensions";
10
+ import { EditorContent, useEditor } from "@tiptap/react";
11
+ import StarterKit from "@tiptap/starter-kit";
12
+ import { type ReactNode, useMemo, useState } from "react";
13
+ import { DocumentVariable } from "../document-schema.js";
14
+ import { createVariableView } from "./document-editor-extensions.js";
15
+ import type { DocumentEditorHandle } from "./document-toolbar-blocks.js";
16
+ import { createEditorHandle } from "./editor-handle.js";
17
+ import { contentToLine, lineToContent, singleLine } from "./placeholder-text.js";
18
+ import { useStableByKey } from "./use-stable-by-key.js";
19
+
20
+ export type PlaceholderLineEditorProps = {
21
+ readonly id: string;
22
+ /** Plain text with `{{name}}` placeholders. */
23
+ readonly value: string;
24
+ readonly onChange: (value: string) => void;
25
+ readonly variableLabels?: Readonly<Record<string, string>>;
26
+ /** Placeholders shown as system chips (a different tint). */
27
+ readonly systemVariables?: readonly string[];
28
+ readonly readOnly?: boolean;
29
+ readonly onEditorReady?: (handle: DocumentEditorHandle) => void;
30
+ readonly "aria-label": string;
31
+ };
32
+
33
+ const LineDocument = Node.create({ name: "doc", topNode: true, content: "paragraph" });
34
+
35
+ // One line only: Enter must not split the paragraph.
36
+ const BlockLineBreaks = Extension.create({
37
+ name: "blockLineBreaks",
38
+ addKeyboardShortcuts() {
39
+ return { Enter: () => true, "Shift-Enter": () => true, "Mod-Enter": () => true };
40
+ },
41
+ });
42
+
43
+ const NO_LABELS: Readonly<Record<string, string>> = {};
44
+ const NO_NAMES: readonly string[] = [];
45
+
46
+ function LineEditorBody({
47
+ id,
48
+ initialValue,
49
+ onChange,
50
+ variableLabels,
51
+ systemVariables,
52
+ readOnly,
53
+ onEditorReady,
54
+ ariaLabel,
55
+ }: {
56
+ readonly id: string;
57
+ readonly initialValue: string;
58
+ readonly onChange: (value: string) => void;
59
+ readonly variableLabels: Readonly<Record<string, string>>;
60
+ readonly systemVariables: readonly string[];
61
+ readonly readOnly: boolean;
62
+ readonly onEditorReady: ((handle: DocumentEditorHandle) => void) | undefined;
63
+ readonly ariaLabel: string;
64
+ }): ReactNode {
65
+ const extensions = useMemo(
66
+ () => [
67
+ LineDocument,
68
+ StarterKit.configure({
69
+ document: false,
70
+ blockquote: false,
71
+ bold: false,
72
+ bulletList: false,
73
+ code: false,
74
+ codeBlock: false,
75
+ dropcursor: false,
76
+ gapcursor: false,
77
+ hardBreak: false,
78
+ heading: false,
79
+ horizontalRule: false,
80
+ italic: false,
81
+ link: false,
82
+ listItem: false,
83
+ listKeymap: false,
84
+ orderedList: false,
85
+ strike: false,
86
+ trailingNode: false,
87
+ underline: false,
88
+ undoRedo: false,
89
+ }),
90
+ DocumentVariable.extend({
91
+ addNodeView: () => createVariableView(variableLabels, systemVariables),
92
+ }),
93
+ UndoRedo,
94
+ BlockLineBreaks,
95
+ ],
96
+ [variableLabels, systemVariables],
97
+ );
98
+
99
+ const editor = useEditor({
100
+ extensions,
101
+ content: {
102
+ type: "doc",
103
+ content: [{ type: "paragraph", content: lineToContent(initialValue) }],
104
+ },
105
+ editable: !readOnly,
106
+ immediatelyRender: false,
107
+ editorProps: {
108
+ attributes: { id, role: "textbox", "aria-label": ariaLabel, "aria-multiline": "false" },
109
+ transformPastedText: singleLine,
110
+ // Rich pastes would bring several paragraphs; only their text is kept.
111
+ transformPastedHTML: (html) => singleLine(html.replace(/<[^>]*>/g, " ")),
112
+ },
113
+ onCreate: ({ editor: created }) => onEditorReady?.(createEditorHandle(created)),
114
+ onUpdate: ({ editor: e }) => onChange(contentToLine(e.getJSON().content?.[0]?.content)),
115
+ });
116
+
117
+ return (
118
+ <EditorContent
119
+ editor={editor}
120
+ className={cn(
121
+ "flex min-h-9 items-center rounded-md border border-input bg-transparent px-3 text-base md:text-sm",
122
+ "[&_.ProseMirror]:w-full [&_.ProseMirror]:whitespace-nowrap [&_.ProseMirror]:outline-none",
123
+ "[&_p]:m-0",
124
+ readOnly && "opacity-60",
125
+ )}
126
+ />
127
+ );
128
+ }
129
+
130
+ export default function PlaceholderLineEditor({
131
+ id,
132
+ value,
133
+ onChange,
134
+ variableLabels = NO_LABELS,
135
+ systemVariables = NO_NAMES,
136
+ readOnly = false,
137
+ onEditorReady,
138
+ "aria-label": ariaLabel,
139
+ }: PlaceholderLineEditorProps): ReactNode {
140
+ const stableLabels = useStableByKey(variableLabels, JSON.stringify(variableLabels));
141
+ const stableSystem = useStableByKey(systemVariables, systemVariables.join("\n"));
142
+
143
+ // The text is the source of truth. Typing reports it through onChange and
144
+ // keeps `emitted` equal to `value`; any other value (async load, reset)
145
+ // remounts the editor body on the new text.
146
+ const [emitted, setEmitted] = useState(value);
147
+ const [epoch, setEpoch] = useState(0);
148
+ if (value !== emitted) {
149
+ setEmitted(value);
150
+ setEpoch((current) => current + 1);
151
+ }
152
+
153
+ return (
154
+ <LineEditorBody
155
+ key={`${epoch}-${readOnly}-${JSON.stringify(stableLabels)}`}
156
+ id={id}
157
+ initialValue={value}
158
+ onChange={(next) => {
159
+ setEmitted(next);
160
+ onChange(next);
161
+ }}
162
+ variableLabels={stableLabels}
163
+ systemVariables={stableSystem}
164
+ readOnly={readOnly}
165
+ onEditorReady={onEditorReady}
166
+ ariaLabel={ariaLabel}
167
+ />
168
+ );
169
+ }
@@ -0,0 +1,37 @@
1
+ // @runtime client
2
+ import type { JSONContent } from "@tiptap/core";
3
+ import { isDocumentReferenceName } from "../document-schema.js";
4
+
5
+ const PLACEHOLDER_TOKEN = /\{\{([^{}]*)\}\}/g;
6
+
7
+ // Names the schema refuses stay plain text, so a typo never becomes a chip
8
+ // that the server would reject.
9
+ export function lineToContent(text: string): JSONContent[] {
10
+ const content: JSONContent[] = [];
11
+ let cursor = 0;
12
+ const pushText = (value: string): void => {
13
+ if (value !== "") content.push({ type: "text", text: value });
14
+ };
15
+ for (const match of text.matchAll(PLACEHOLDER_TOKEN)) {
16
+ const name = match[1] ?? "";
17
+ if (!isDocumentReferenceName(name)) continue;
18
+ pushText(text.slice(cursor, match.index));
19
+ content.push({ type: "variable", attrs: { name } });
20
+ cursor = match.index + match[0].length;
21
+ }
22
+ pushText(text.slice(cursor));
23
+ return content;
24
+ }
25
+
26
+ export function contentToLine(nodes: readonly JSONContent[] | undefined): string {
27
+ return (nodes ?? [])
28
+ .map((node) => {
29
+ if (node.type === "variable") return `{{${String(node.attrs?.["name"])}}}`;
30
+ return node.type === "text" ? (node.text ?? "") : "";
31
+ })
32
+ .join("");
33
+ }
34
+
35
+ export function singleLine(text: string): string {
36
+ return text.replace(/\s*[\r\n]+\s*/g, " ");
37
+ }
@@ -0,0 +1,14 @@
1
+ // @runtime client
2
+ import { useState } from "react";
3
+
4
+ // Callers often pass fresh object literals every render; this keeps the first
5
+ // value for as long as the content key is unchanged, so memoized extension
6
+ // lists (and the editor built from them) are not rebuilt on every render.
7
+ export function useStableByKey<T>(value: T, key: string): T {
8
+ const [stored, setStored] = useState({ key, value });
9
+ if (stored.key !== key) {
10
+ setStored({ key, value });
11
+ return value;
12
+ }
13
+ return stored.value;
14
+ }
@@ -7,6 +7,7 @@ import {
7
7
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
8
8
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
9
9
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
10
+ import { errorCodeOf, isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
10
11
  import * as z from "zod";
11
12
  import { userSessionTable } from "../../sessions/index.js";
12
13
  import { decryptStoredPii, mapWithConcurrency } from "../../shared/index.js";
@@ -37,7 +38,7 @@ const SORT_FIELDS = ["email", "status", "createdAt", "lastSeenAt"] as const;
37
38
  type SortField = (typeof SORT_FIELDS)[number];
38
39
 
39
40
  function isSortField(value: string): value is SortField {
40
- return (SORT_FIELDS as readonly string[]).includes(value);
41
+ return isOneOf(SORT_FIELDS, value);
41
42
  }
42
43
 
43
44
  // Nulls (no session yet, invitation rows) sort last regardless of
@@ -141,7 +142,7 @@ export const teamListQuery = definePagedQueryHandler({
141
142
  // ponytail: lean sample apps (admin-console) mount auth without
142
143
  // sessions — store_user_sessions never gets pushed. Integration tests
143
144
  // may seed the table without mounting the feature; prod mounts both.
144
- if ((err as { code?: string }).code !== "42P01") throw err;
145
+ if (errorCodeOf(err) !== "42P01") throw err;
145
146
  }
146
147
  }
147
148
 
@@ -44,7 +44,10 @@ import {
44
44
  type TenantId,
45
45
  } from "@cosmicdrift/kumiko-framework/engine";
46
46
  import { getUnscopedAggregateStreamMaxVersion } from "@cosmicdrift/kumiko-framework/event-store";
47
- import { createLifecycleHooks } from "@cosmicdrift/kumiko-framework/pipeline";
47
+ import {
48
+ createDeferringNotify,
49
+ createLifecycleHooks,
50
+ } from "@cosmicdrift/kumiko-framework/pipeline";
48
51
  import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
49
52
  import { assertAssignableMembershipRoles } from "./membership-roles.js";
50
53
  import { tenantMembershipEntity, tenantMembershipsTable } from "./membership-table.js";
@@ -118,12 +121,40 @@ export type SeedTenantHooks = {
118
121
  // dispatcher's sink and flush post-commit. Without a sink (fixture / plain
119
122
  // Connection) they still fire immediately — there is no outer TX to wait for.
120
123
  //
124
+ function buildSeedHookNotify(
125
+ context: AppContext,
126
+ scopedUser: SessionUser,
127
+ targetTenantId: TenantId,
128
+ notifyTiming: "deferUntilCommit" | "inline",
129
+ ): AppContext["notify"] {
130
+ const notifyFactory = context._notifyFactory;
131
+ if (!notifyFactory) return context.notify;
132
+ if (notifyTiming === "inline") return notifyFactory(scopedUser, targetTenantId);
133
+ // Still inside the caller's write tx: delivery DB work must not wait for an app-pool slot.
134
+ const notifyInsideTransaction = (context._notifyInTransactionFactory ?? notifyFactory)(
135
+ scopedUser,
136
+ targetTenantId,
137
+ );
138
+ const scheduleAfterCommit = context.scheduleAfterCommit;
139
+ if (!scheduleAfterCommit) return notifyInsideTransaction;
140
+ return createDeferringNotify({
141
+ registry: context.registry,
142
+ tenantId: targetTenantId,
143
+ notifyAfterCommit: notifyFactory(scopedUser, targetTenantId),
144
+ notifyNow: notifyInsideTransaction,
145
+ scheduleAfterCommit,
146
+ });
147
+ }
148
+
121
149
  // `runner` comes from the caller, which already holds a declared DbRunner —
122
150
  // a TenantDb exposes no runner back out to feature code.
123
151
  function scopeSeedHookContext(
124
152
  context: AppContext,
125
153
  targetTenantId: TenantId,
126
154
  runner: DbRunner,
155
+ // The afterCommit sink is snapshotted when it flushes, so a hook scheduled from inside an
156
+ // afterCommit-phase hook would be dropped: that phase must notify inline.
157
+ notifyTiming: "deferUntilCommit" | "inline",
127
158
  ): AppContext {
128
159
  const db = createTenantDb(
129
160
  runner,
@@ -142,10 +173,9 @@ function scopeSeedHookContext(
142
173
  ? { ...baseUser, tenantId: targetTenantId }
143
174
  : undefined;
144
175
 
145
- const notify =
146
- context._notifyFactory && scopedUser
147
- ? context._notifyFactory(scopedUser, targetTenantId)
148
- : context.notify;
176
+ const notify = scopedUser
177
+ ? buildSeedHookNotify(context, scopedUser, targetTenantId, notifyTiming)
178
+ : context.notify;
149
179
 
150
180
  const features = context.effectiveFeatures as
151
181
  | (((tenantId: TenantId) => ReadonlySet<string>) & {
@@ -188,10 +218,14 @@ export async function fireEntityPostSave(
188
218
  const scopedContext =
189
219
  target === undefined
190
220
  ? hooks.context
191
- : scopeSeedHookContext(hooks.context, target.tenantId, target.db);
221
+ : scopeSeedHookContext(hooks.context, target.tenantId, target.db, "deferUntilCommit");
222
+ const afterCommitContext =
223
+ target === undefined
224
+ ? hooks.context
225
+ : scopeSeedHookContext(hooks.context, target.tenantId, target.db, "inline");
192
226
  await lifecycle.runPostSave(pseudoType, entityData, scopedContext, HookPhases.inTransaction);
193
227
  const runAfterCommit = () =>
194
- lifecycle.runPostSave(pseudoType, entityData, scopedContext, HookPhases.afterCommit);
228
+ lifecycle.runPostSave(pseudoType, entityData, afterCommitContext, HookPhases.afterCommit);
195
229
  if (scopedContext.scheduleAfterCommit) {
196
230
  scopedContext.scheduleAfterCommit(runAfterCommit);
197
231
  } else {
@@ -27,7 +27,7 @@ function hasTenantOwned(field: unknown): field is ResolvedPiiFlags {
27
27
  typeof field === "object" &&
28
28
  field !== null &&
29
29
  "tenantOwned" in field &&
30
- (field as { tenantOwned?: unknown }).tenantOwned === true
30
+ field.tenantOwned === true
31
31
  );
32
32
  }
33
33
 
@@ -72,12 +72,13 @@ export const cancelDestructionWrite = defineWriteHandler({
72
72
  payload: { cancelledBy: event.user.id },
73
73
  });
74
74
 
75
+ const gracePeriodEnd: string | null = null;
75
76
  return {
76
77
  isSuccess: true as const,
77
78
  data: {
78
79
  tenantId,
79
80
  status: "active" as const,
80
- gracePeriodEnd: null as string | null,
81
+ gracePeriodEnd,
81
82
  },
82
83
  };
83
84
  },
@@ -37,12 +37,12 @@ export type UserStreamBackfillResult = {
37
37
  export async function backfillUserStreamTenants(
38
38
  db: DbConnection,
39
39
  ): Promise<UserStreamBackfillResult> {
40
- const candidates = (await unsafeReadRetrying(
40
+ const candidates = await unsafeReadRetrying<{ aggregate_id: string }>(
41
41
  db,
42
42
  `SELECT DISTINCT "aggregate_id" FROM "kumiko_events"
43
43
  WHERE "aggregate_type" = 'user' AND "tenant_id" <> $1::uuid`,
44
44
  [SYSTEM_TENANT_ID],
45
- )) as ReadonlyArray<{ aggregate_id: string }>;
45
+ );
46
46
 
47
47
  let aggregatesMigrated = 0;
48
48
  let eventsMigrated = 0;
@@ -67,13 +67,13 @@ async function migrateAggregate(db: DbConnection, aggregateId: string): Promise<
67
67
  const raw = asRawClient(tx);
68
68
  // Global id order = commit-adjacent replay order; merges a split stream
69
69
  // (legacy tenant + SYSTEM) into one consistent version sequence.
70
- const events = (await raw.unsafe(
70
+ const events = await raw.unsafe<{ id: bigint | string }>(
71
71
  `SELECT "id" FROM "kumiko_events"
72
72
  WHERE "aggregate_type" = 'user' AND "aggregate_id" = $1::uuid
73
73
  ORDER BY "id" ASC
74
74
  FOR UPDATE`,
75
75
  [aggregateId],
76
- )) as ReadonlyArray<{ id: bigint | string }>;
76
+ );
77
77
 
78
78
  // Phase 1: negative interim versions — real versions are always >= 1, so
79
79
  // nothing can collide with events_aggregate_version_uq mid-migration.