@cosmicdrift/kumiko-samples 0.362.0 → 0.364.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +23 -6
  3. package/packages/bundled-features/src/admin-shell/changes.json +6 -0
  4. package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
  5. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
  6. package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
  7. package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
  8. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
  9. package/packages/bundled-features/src/audit/changes.json +6 -0
  10. package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
  11. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
  12. package/packages/bundled-features/src/auth-email-password/changes.json +51 -0
  13. package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
  14. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  15. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  16. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
  17. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  18. package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
  19. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  20. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  21. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  22. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  23. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
  24. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +5 -13
  25. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  26. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  27. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  28. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  29. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
  30. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  31. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  32. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  33. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  34. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  35. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  36. package/packages/bundled-features/src/billing-foundation/changes.json +43 -0
  37. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  38. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +90 -44
  39. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
  40. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  41. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +118 -0
  42. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  43. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
  44. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +39 -33
  45. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  46. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  47. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
  48. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
  49. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +36 -20
  50. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
  51. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
  52. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  53. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
  54. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  55. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  56. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +12 -6
  57. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  58. package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
  59. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +20 -11
  60. package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
  61. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
  62. package/packages/bundled-features/src/cap-counter/changes.json +6 -0
  63. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
  64. package/packages/bundled-features/src/cap-counter/index.ts +1 -0
  65. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
  66. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  67. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  68. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  69. package/packages/bundled-features/src/channel-email/email-channel.ts +135 -29
  70. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  71. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  72. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  73. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  74. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  75. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  76. package/packages/bundled-features/src/config/changes.json +6 -0
  77. package/packages/bundled-features/src/config/handlers/set.write.ts +3 -7
  78. package/packages/bundled-features/src/config/resolver.ts +15 -12
  79. package/packages/bundled-features/src/config/write-helpers.ts +23 -0
  80. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  81. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
  82. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  83. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  84. package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
  85. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  86. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  87. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  88. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  89. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  90. package/packages/bundled-features/src/delivery/attachment-ref.ts +105 -0
  91. package/packages/bundled-features/src/delivery/changes.json +66 -0
  92. package/packages/bundled-features/src/delivery/channel-context.ts +152 -4
  93. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  94. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  95. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  96. package/packages/bundled-features/src/delivery/index.ts +19 -2
  97. package/packages/bundled-features/src/delivery/jobs.ts +97 -17
  98. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  99. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  100. package/packages/bundled-features/src/delivery/types.ts +34 -18
  101. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  102. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  103. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  104. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  105. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  106. package/packages/bundled-features/src/file-foundation/changes.json +6 -0
  107. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  108. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +89 -0
  109. package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
  110. package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
  111. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  112. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  113. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  114. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  115. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  116. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
  117. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  118. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  119. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  120. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  121. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  122. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
  123. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  124. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  125. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  126. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  127. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +42 -25
  128. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  129. package/packages/bundled-features/src/jobs/changes.json +19 -0
  130. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
  131. package/packages/bundled-features/src/jobs/feature.ts +7 -4
  132. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  133. package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
  134. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
  135. package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
  136. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
  137. package/packages/bundled-features/src/ledger/changes.json +7 -0
  138. package/packages/bundled-features/src/ledger/entity.ts +3 -1
  139. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  140. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  141. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  142. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  143. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  144. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  145. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  146. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  147. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  148. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  149. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  150. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  151. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  152. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  153. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  154. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  155. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  156. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  157. package/packages/bundled-features/src/renderer-foundation/types.ts +134 -35
  158. package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
  159. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  160. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
  161. package/packages/bundled-features/src/secrets/changes.json +6 -0
  162. package/packages/bundled-features/src/secrets/feature.ts +4 -3
  163. package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
  164. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  165. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  166. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  167. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  168. package/packages/bundled-features/src/shared/changes.json +6 -0
  169. package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
  170. package/packages/bundled-features/src/shared/index.ts +6 -0
  171. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  172. package/packages/bundled-features/src/shared/mail-template-body.ts +42 -0
  173. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  174. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  175. package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
  176. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
  177. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
  178. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  179. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  180. package/packages/bundled-features/src/template-resolver/README.md +66 -8
  181. package/packages/bundled-features/src/template-resolver/api.ts +85 -9
  182. package/packages/bundled-features/src/template-resolver/changes.json +186 -0
  183. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +278 -0
  184. package/packages/bundled-features/src/template-resolver/constants.ts +49 -11
  185. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  186. package/packages/bundled-features/src/template-resolver/document-schema.ts +345 -0
  187. package/packages/bundled-features/src/template-resolver/feature.ts +59 -6
  188. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  189. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  190. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +160 -0
  191. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +33 -0
  192. package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
  193. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +138 -0
  194. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -6
  195. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  196. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  197. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +151 -0
  198. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +19 -18
  199. package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
  200. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +323 -0
  201. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  202. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  203. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  204. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  205. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  206. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  207. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +135 -0
  208. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  209. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  210. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +196 -0
  211. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +233 -0
  212. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +137 -0
  213. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +91 -17
  214. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  215. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +16 -77
  216. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +31 -0
  217. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  218. package/packages/bundled-features/src/template-resolver/index.ts +111 -0
  219. package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
  220. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  221. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  222. package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
  223. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  224. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  225. package/packages/bundled-features/src/template-resolver/qualified-names.ts +34 -0
  226. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  227. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  228. package/packages/bundled-features/src/template-resolver/seeding.ts +171 -20
  229. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  230. package/packages/bundled-features/src/template-resolver/table.ts +27 -1
  231. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  232. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  233. package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
  234. package/packages/bundled-features/src/template-resolver/template-meta.ts +192 -0
  235. package/packages/bundled-features/src/template-resolver/template-preview.ts +676 -0
  236. package/packages/bundled-features/src/template-resolver/template-resources.ts +114 -0
  237. package/packages/bundled-features/src/template-resolver/template-usage.ts +68 -0
  238. package/packages/bundled-features/src/template-resolver/template-validation.ts +230 -0
  239. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  240. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +20 -13
  241. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +215 -0
  242. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +37 -0
  243. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1065 -0
  244. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +72 -0
  245. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  246. package/packages/bundled-features/src/template-resolver/web/i18n.ts +121 -0
  247. package/packages/bundled-features/src/template-resolver/web/index.ts +26 -0
  248. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  249. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +176 -0
  250. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  251. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  252. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  253. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  254. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  255. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  256. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  257. package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
  258. package/packages/bundled-features/src/user/changes.json +6 -0
  259. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  260. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  261. package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
  262. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  263. package/packages/bundled-features/src/user/seeding.ts +19 -5
  264. package/packages/bundled-features/src/user-data-rights/changes.json +13 -0
  265. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  266. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  267. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  268. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  269. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  270. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  271. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  272. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  273. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  274. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  275. package/packages/bundled-features/src/user-profile/changes.json +1 -1
  276. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  277. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  278. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  279. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  280. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  281. package/packages/bundled-features/src/waitlist/options.ts +1 -1
  282. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  283. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +3 -3
  284. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
  285. package/samples/apps/ui-walkthrough/package.json +11 -0
  286. package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
  287. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  288. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  289. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +239 -0
  290. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  291. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  292. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +570 -0
  293. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +72 -0
  294. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  295. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  296. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  297. package/samples/apps/ui-walkthrough/src/run-config.ts +41 -1
  298. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  299. package/samples/apps/use-all-bundled/feature-manifest.json +118 -21
  300. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  301. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  302. package/samples/apps/use-all-bundled/package.json +2 -0
  303. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  304. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  305. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  306. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  307. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  308. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  309. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  310. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  311. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  312. package/samples/recipes/delivery-notifications/README.md +26 -25
  313. package/samples/recipes/delivery-notifications/package.json +2 -1
  314. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  315. package/samples/recipes/seed-migration/README.md +1 -1
  316. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  317. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  318. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  319. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  320. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  321. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -83,21 +83,22 @@ export function requireSecretsContext(
83
83
  "createSecretsContext({db, masterKeyProvider, registry}) via extraContext.secrets at boot.",
84
84
  });
85
85
  }
86
- const raw = ctx.secrets;
87
86
  const userId = ctx._userId;
88
87
  if (!userId) {
89
88
  throw new InternalError({
90
89
  message: `[${handlerName}] ctx._userId missing — cannot audit secret reads without a caller identity.`,
91
90
  });
92
91
  }
93
- return {
92
+ const withAudit = (raw: SecretsContext): SecretsContext => ({
93
+ withRunner: (runner) => withAudit(raw.withRunner(runner)),
94
94
  get: (tenantId, key, overrideAudit) =>
95
95
  raw.get(tenantId, key, overrideAudit ?? { userId, handlerName }),
96
96
  // No audit injection: has() is metadata-only and never logs a read.
97
97
  has: raw.has.bind(raw),
98
98
  set: raw.set.bind(raw),
99
99
  delete: raw.delete.bind(raw),
100
- };
100
+ });
101
+ return withAudit(ctx.secrets);
101
102
  }
102
103
 
103
104
  export type SecretsFeatureOptions = {
@@ -17,6 +17,7 @@ import {
17
17
  createEventStoreExecutor,
18
18
  createTenantDb,
19
19
  type DbConnection,
20
+ type DbRunner,
20
21
  } from "@cosmicdrift/kumiko-framework/db";
21
22
  import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
22
23
  import { InternalError, type WriteErrorInfo } from "@cosmicdrift/kumiko-framework/errors";
@@ -91,8 +92,20 @@ function resolveKey(keyOrHandle: string | { readonly name: string }): string {
91
92
  }
92
93
 
93
94
  export function createSecretsContext(opts: SecretsContextOptions): SecretsContext {
94
- const { db, masterKeyProvider } = opts;
95
+ const { masterKeyProvider } = opts;
95
96
  const provider = withDekCache(masterKeyProvider, opts.dekCache ?? createDekCache());
97
+ return buildSecretsContext(opts, provider, opts.db, false);
98
+ }
99
+
100
+ // `runnerIsTransaction`: the runner is a caller-owned transaction, so audited reads join it
101
+ // instead of opening a second connection (which would deadlock a small pool).
102
+ function buildSecretsContext(
103
+ opts: SecretsContextOptions,
104
+ provider: MasterKeyProvider,
105
+ db: DbRunner,
106
+ runnerIsTransaction: boolean,
107
+ ): SecretsContext {
108
+ const { masterKeyProvider } = opts;
96
109
 
97
110
  type SecretLookupRow = {
98
111
  readonly id: string;
@@ -105,6 +118,10 @@ export function createSecretsContext(opts: SecretsContextOptions): SecretsContex
105
118
  }
106
119
 
107
120
  return {
121
+ withRunner(runner) {
122
+ return buildSecretsContext(opts, provider, runner, true);
123
+ },
124
+
108
125
  async get(tenantId, keyOrHandle, auditCtx) {
109
126
  const key = resolveKey(keyOrHandle);
110
127
  // Atomic audit + read: a decrypt that "escaped" the audit trail
@@ -120,7 +137,7 @@ export function createSecretsContext(opts: SecretsContextOptions): SecretsContex
120
137
  return createSecret(plaintext);
121
138
  }
122
139
 
123
- const plaintext = await transaction(db, async (tx) => {
140
+ const readAndAudit = async (tx: DbRunner): Promise<string | undefined> => {
124
141
  // Inline select inside the TX via raw client — fetchOne's connection
125
142
  // type doesn't widen to the transaction object cleanly.
126
143
  const envelope = await selectTenantSecretEnvelope(tx, tenantId, key);
@@ -154,7 +171,10 @@ export function createSecretsContext(opts: SecretsContextOptions): SecretsContex
154
171
  metadata,
155
172
  });
156
173
  return pt;
157
- });
174
+ };
175
+ const plaintext = runnerIsTransaction
176
+ ? await readAndAudit(db)
177
+ : await transaction(db, readAndAudit);
158
178
 
159
179
  if (plaintext === undefined) return undefined;
160
180
  // Brand the plaintext only after audit committed. The response
@@ -6,7 +6,7 @@ export async function deleteStaleSessionsBatch(
6
6
  olderThanDays: number,
7
7
  batchSize: number,
8
8
  ): Promise<number> {
9
- const rows = (await asRawClient(db).unsafe(
9
+ const rows = await asRawClient(db).unsafe<{ id: string }>(
10
10
  `DELETE FROM "store_user_sessions"
11
11
  WHERE "id" IN (
12
12
  SELECT "id" FROM "store_user_sessions"
@@ -16,6 +16,6 @@ export async function deleteStaleSessionsBatch(
16
16
  )
17
17
  RETURNING "id"`,
18
18
  [olderThanDays, batchSize],
19
- )) as readonly { id: string }[];
19
+ );
20
20
  return rows.length;
21
21
  }
@@ -71,11 +71,9 @@ export function bindAutoRevokeFromFeature(
71
71
  ): BindAutoRevokeOnPasswordChange | undefined {
72
72
  const exports = feature.exports;
73
73
  if (exports && typeof exports === "object" && "bindAutoRevokeOnPasswordChange" in exports) {
74
- const { bindAutoRevokeOnPasswordChange } = exports as {
75
- bindAutoRevokeOnPasswordChange: unknown;
76
- };
74
+ const { bindAutoRevokeOnPasswordChange } = exports;
77
75
  if (typeof bindAutoRevokeOnPasswordChange === "function") {
78
- // @cast-boundary exports-walk — feature.exports is untyped by design
76
+ // @cast-boundary feature-exports — feature.exports is untyped by design
79
77
  return bindAutoRevokeOnPasswordChange as BindAutoRevokeOnPasswordChange;
80
78
  }
81
79
  }
@@ -4,6 +4,7 @@ import {
4
4
  definePagedQueryHandler,
5
5
  MAX_LIST_LIMIT,
6
6
  } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
7
8
  import * as z from "zod";
8
9
  import { decryptStoredPii } from "../../shared/index.js";
9
10
  import { userSessionTable } from "../schema/user-session.js";
@@ -17,7 +18,7 @@ const SORTABLE_COLUMNS = ["id", "userId", "createdAt", "expiresAt", "revokedAt"]
17
18
  type SortableColumn = (typeof SORTABLE_COLUMNS)[number];
18
19
 
19
20
  function isSortableColumn(value: string): value is SortableColumn {
20
- return (SORTABLE_COLUMNS as readonly string[]).includes(value);
21
+ return isOneOf(SORTABLE_COLUMNS, value);
21
22
  }
22
23
 
23
24
  // Admin view of every session in the active tenant. ctx.db (TenantDb)
@@ -0,0 +1,39 @@
1
+ import * as z from "zod";
2
+
3
+ const MAX_FILE_NAME_LENGTH = 255;
4
+
5
+ function hasControlCharacter(value: string): boolean {
6
+ return value.split("").some((char) => {
7
+ const code = char.charCodeAt(0);
8
+ return code <= 0x1f || code === 0x7f;
9
+ });
10
+ }
11
+
12
+ export const attachmentFileNameSchema = z
13
+ .string()
14
+ .min(1)
15
+ .max(MAX_FILE_NAME_LENGTH)
16
+ .refine((name) => !name.includes("/") && !name.includes("\\") && !hasControlCharacter(name), {
17
+ message: "file name must not contain path separators or control characters",
18
+ });
19
+
20
+ const PDF_FILE_NAME_MAX_LENGTH = 200;
21
+ const PDF_EXTENSION = ".pdf";
22
+
23
+ // Shared by everything that names a rendered PDF, so a mail attachment and a download of the same
24
+ // template get the same name. The cut leaves room for the extension: the attachment schema caps
25
+ // rendered-pdf names, and a name that only fits without ".pdf" would fail it.
26
+ export function sanitizePdfAttachmentFileName(raw: string, templateSlug: string): string {
27
+ const cleaned = raw
28
+ .split("")
29
+ .filter((char) => !hasControlCharacter(char))
30
+ .join("")
31
+ .replace(/[/\\";]/g, "-")
32
+ .replaceAll("..", "-")
33
+ .trim()
34
+ .slice(0, PDF_FILE_NAME_MAX_LENGTH - PDF_EXTENSION.length)
35
+ .trim();
36
+ const lastSegment = templateSlug.split(":").pop() ?? templateSlug;
37
+ if (cleaned === "") return `${lastSegment}${PDF_EXTENSION}`;
38
+ return cleaned.toLowerCase().endsWith(PDF_EXTENSION) ? cleaned : `${cleaned}${PDF_EXTENSION}`;
39
+ }
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "improvement",
5
+ "title": "Mail template body helpers exported as `./shared/mail-template-body`",
6
+ "detail": "Apps that register their own notification templates can build the body with the same helpers the framework uses: `mailParagraph`, `mailHeading`, `mailRepeatedRows`, `mailTextWithSlotsHtml`, plus the new `mailButton`, `mailParagraphWithLink` and `mailSubject`. The subpath only depends on kumiko-headless and is safe on the server. Apps that copied these helpers can import them instead."
7
+ },
2
8
  {
3
9
  "version": "0.335.0",
4
10
  "type": "fix",
@@ -33,6 +33,9 @@ export type ChunkedMigrationOptions<Row> = {
33
33
  readonly maxFailures: number;
34
34
  // Epoch ms; Number.POSITIVE_INFINITY for no time bound.
35
35
  readonly deadlineAt: number;
36
+ // Batches processed even when the deadline already passed. Callers whose setup before the
37
+ // loop can eat the whole budget set 1, so every run makes progress instead of timing out empty.
38
+ readonly minBatches?: number;
36
39
  readonly signal?: AbortSignal | undefined;
37
40
  readonly onRowError?: (row: Row, err: unknown) => void;
38
41
  };
@@ -51,7 +54,7 @@ export async function runChunkedMigration<Row>(
51
54
  stoppedReason = "signal";
52
55
  break;
53
56
  }
54
- if (Date.now() >= opts.deadlineAt) {
57
+ if (batchesProcessed >= (opts.minBatches ?? 0) && Date.now() >= opts.deadlineAt) {
55
58
  stoppedReason = "timeout";
56
59
  break;
57
60
  }
@@ -1,5 +1,9 @@
1
1
  export { collectErasureFailure, throwIfErasureFailed } from "./assert-erased.js";
2
2
  export { assertNoWhereRuleInOwnershipWrite } from "./assert-no-where-rule-in-ownership-write.js";
3
+ export {
4
+ attachmentFileNameSchema,
5
+ sanitizePdfAttachmentFileName,
6
+ } from "./attachment-file-name.js";
3
7
  export {
4
8
  type ChunkedMigrationOptions,
5
9
  type ChunkedMigrationResult,
@@ -18,6 +22,7 @@ export { isWithinGracePeriod } from "./grace-period.js";
18
22
  export { isIdentityV3Hash, verifyIdentityV3Hash } from "./identity-v3-hash.js";
19
23
  export { isTenantDb } from "./is-tenant-db.js";
20
24
  export { createLockoutCounter, type LockoutCounterState } from "./lockout-counter.js";
25
+ export { mailHeading, mailParagraph, mailRepeatedRows } from "./mail-template-body.js";
21
26
  export { mapWithConcurrency } from "./map-with-concurrency.js";
22
27
  export {
23
28
  denyUnlessJoinRowParentVisible,
@@ -43,6 +48,7 @@ export {
43
48
  export { runInSubTransaction } from "./run-in-sub-transaction.js";
44
49
  export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
45
50
  export { sessionTimezoneField } from "./session-timezone-field.js";
51
+ export { assertNotSvg, MAX_INPUT_PIXELS, SHARP_INPUT_OPTIONS } from "./sharp-input-limits.js";
46
52
  export {
47
53
  peekTokenSubject,
48
54
  signToken,
@@ -5,5 +5,5 @@ import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
5
5
  // Shared so retention/compliance-profile resolvers that accept either (bulk cron
6
6
  // vs. per-hook TenantDb) branch through one predicate instead of duplicating it.
7
7
  export function isTenantDb(db: DbRunner | TenantDb): db is TenantDb {
8
- return typeof (db as Partial<TenantDb>).unsafeRaw === "function";
8
+ return "unsafeRaw" in db && typeof db.unsafeRaw === "function";
9
9
  }
@@ -0,0 +1,42 @@
1
+ import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
2
+
3
+ const SLOT_PATTERN = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
4
+
5
+ // Author text uses `{name}` slots. The text is escaped first and the slots are
6
+ // swapped for variable nodes afterwards, so a slot can never smuggle markup.
7
+ export function mailTextWithSlotsHtml(text: string): string {
8
+ return escapeHtml(text).replace(
9
+ SLOT_PATTERN,
10
+ (_, name: string) => `<span data-variable="${escapeHtmlAttr(name)}"></span>`,
11
+ );
12
+ }
13
+
14
+ export function mailParagraph(text: string): string {
15
+ return `<p>${mailTextWithSlotsHtml(text)}</p>`;
16
+ }
17
+
18
+ export function mailHeading(text: string): string {
19
+ return `<h2>${mailTextWithSlotsHtml(text)}</h2>`;
20
+ }
21
+
22
+ export function mailButton(label: string, linkVariable: string): string {
23
+ return `<p><a data-button href="{{${escapeHtmlAttr(linkVariable)}}}">${escapeHtml(label)}</a></p>`;
24
+ }
25
+
26
+ export function mailParagraphWithLink(
27
+ text: string,
28
+ linkLabel: string,
29
+ linkVariable: string,
30
+ ): string {
31
+ return `<p>${mailTextWithSlotsHtml(text)} <a href="{{${escapeHtmlAttr(linkVariable)}}}">${escapeHtml(linkLabel)}</a></p>`;
32
+ }
33
+
34
+ // One table row per list entry; the cell shows the entry's `field`.
35
+ export function mailRepeatedRows(listName: string, field: string): string {
36
+ return `<table><tbody><tr data-each="${escapeHtmlAttr(listName)}"><td><p><span data-variable="${escapeHtmlAttr(field)}"></span></p></td></tr></tbody></table>`;
37
+ }
38
+
39
+ // Subjects are Handlebars text, so the same `{name}` slots become `{{name}}`.
40
+ export function mailSubject(text: string): string {
41
+ return text.replace(SLOT_PATTERN, "{{$1}}");
42
+ }
@@ -0,0 +1,26 @@
1
+ // Sharp-free so callers can import the limits without loading the native binding.
2
+
3
+ // Above this, a decoded pixel buffer runs into hundreds of MB before any
4
+ // resize even happens — sharp's own default (~268MP) is still large enough
5
+ // to be an OOM primitive for a single request.
6
+ export const MAX_INPUT_PIXELS = 100_000_000;
7
+ // sequentialRead trades random pixel access for lower peak memory on
8
+ // streamed formats — applied at every decode entry point below since none
9
+ // of them need non-sequential access to the *original* bytes (extract runs
10
+ // against a single already-buffered region per call).
11
+ export const SHARP_INPUT_OPTIONS = {
12
+ limitInputPixels: MAX_INPUT_PIXELS,
13
+ sequentialRead: true,
14
+ } as const;
15
+
16
+ // Shared trust boundary for every image byte source this renderer decodes —
17
+ // the original upload (renderImage) and an overlay layer's bytes (applyOverlays)
18
+ // alike: a base64 string in a field declaration isn't more trustworthy than
19
+ // an upload just because it came from app code.
20
+ export function assertNotSvg(format: string | undefined): void {
21
+ if (format === "svg") {
22
+ throw new Error(
23
+ "derivatives-sharp: SVG is not supported — rendering untrusted SVG through libvips is a trust boundary this renderer doesn't need to cross.",
24
+ );
25
+ }
26
+ }
@@ -13,6 +13,7 @@
13
13
 
14
14
  import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
15
15
  import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
16
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
16
17
 
17
18
  export const EXT_SIGNUP_HANDOVER = "signupHandover" as const;
18
19
 
@@ -46,10 +47,9 @@ export type SignupHandoverProvider = {
46
47
 
47
48
  export function isSignupHandoverProvider(options: unknown): options is SignupHandoverProvider {
48
49
  return (
49
- typeof options === "object" &&
50
- options !== null &&
51
- typeof (options as Record<string, unknown>)["verifyGrant"] === "function" &&
52
- typeof (options as Record<string, unknown>)["mintClaim"] === "function"
50
+ isPlainObject(options) &&
51
+ typeof options["verifyGrant"] === "function" &&
52
+ typeof options["mintClaim"] === "function"
53
53
  );
54
54
  }
55
55
 
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "A stale Stripe customer id no longer turns the customer portal into a 500",
6
+ "detail": "createPortalSession now maps Stripe's \"No such customer\" rejection (account switch, customer deleted in the dashboard) to an UnprocessableError with the i18n key billing-foundation.errors.providerCustomerUnknown, translated in German and Spanish, instead of letting the raw Stripe error surface as a server error."
7
+ },
2
8
  {
3
9
  "version": "0.338.0",
4
10
  "type": "improvement",
@@ -109,12 +109,10 @@ export function mapAppLocaleToStripeLocale(
109
109
  return knownLocales.find((known) => known === language) ?? "auto";
110
110
  }
111
111
 
112
- function isUnknownCustomerStripeError(error: unknown): boolean {
113
- return (
114
- isResourceMissingStripeError(error) &&
115
- error instanceof Stripe.errors.StripeInvalidRequestError &&
116
- error.param === "customer"
117
- );
112
+ function isUnknownCustomerStripeError(
113
+ error: unknown,
114
+ ): error is Stripe.errors.StripeInvalidRequestError {
115
+ return isResourceMissingStripeError(error) && error.param === "customer";
118
116
  }
119
117
 
120
118
  export function createStripeCheckoutSession(
@@ -203,11 +201,21 @@ export type StripePortalOptions = Parameters<
203
201
  export function createStripePortalSession(runtime: StripeCtxRuntime) {
204
202
  return async (ctx: HandlerContext, options: StripePortalOptions): Promise<{ url: string }> => {
205
203
  const stripe = await runtime.clientForCtx(ctx);
206
- const session = await stripe.billingPortal.sessions.create({
207
- customer: options.providerCustomerId,
208
- return_url: options.returnUrl,
209
- });
210
- return { url: session.url };
204
+ try {
205
+ const session = await stripe.billingPortal.sessions.create({
206
+ customer: options.providerCustomerId,
207
+ return_url: options.returnUrl,
208
+ });
209
+ return { url: session.url };
210
+ } catch (error) {
211
+ // A portal needs an existing customer, so unlike checkout there is no retry without one.
212
+ if (!isUnknownCustomerStripeError(error)) throw error;
213
+ throw new UnprocessableError("provider_customer_unknown", {
214
+ i18nKey: "billing-foundation.errors.providerCustomerUnknown",
215
+ message: `subscription-stripe: Stripe does not know customer "${options.providerCustomerId}" (account switch or customer deleted).`,
216
+ cause: error,
217
+ });
218
+ }
211
219
  };
212
220
  }
213
221
 
@@ -54,7 +54,9 @@ import type { StripeWebhookRuntime } from "./runtime.js";
54
54
 
55
55
  const log = createFallbackLogger("subscription-stripe");
56
56
 
57
- export function isResourceMissingStripeError(error: unknown): boolean {
57
+ export function isResourceMissingStripeError(
58
+ error: unknown,
59
+ ): error is Stripe.errors.StripeInvalidRequestError {
58
60
  return (
59
61
  error instanceof Stripe.errors.StripeInvalidRequestError && error.code === "resource_missing"
60
62
  );
@@ -1,5 +1,6 @@
1
1
  import type { AccessRule, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
3
4
  import { denyUnlessJoinRowParentVisible } from "../../shared/index.js";
4
5
  import { tagAssignmentAggregateId } from "../aggregate-id.js";
5
6
  import { DEFAULT_TAG_ACCESS } from "../constants.js";
@@ -67,7 +68,10 @@ export function createAssignTagHandler(
67
68
  // above and this restore() call, so restore() correctly refuses with
68
69
  // "not_deleted" — but the desired end state is already true. Converge
69
70
  // like the create() race below instead of surfacing a spurious error.
70
- if ((restored.error.details as { reason?: string } | undefined)?.reason === "not_deleted") {
71
+ if (
72
+ isPlainObject(restored.error.details) &&
73
+ restored.error.details["reason"] === "not_deleted"
74
+ ) {
71
75
  return { isSuccess: true as const, data: { id } };
72
76
  }
73
77
  if (restored.error.code !== "not_found") return restored;
@@ -21,7 +21,8 @@ export function contrastText(color: string): "#000000" | "#ffffff" | null {
21
21
  function hexToRgb(color: string): { r: number; g: number; b: number } | null {
22
22
  const match = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.exec(color.trim());
23
23
  if (match === null) return null;
24
- const short = match[1] as string;
24
+ const short = match[1];
25
+ if (short === undefined) return null;
25
26
  const hex =
26
27
  short.length === 3
27
28
  ? `${short[0]}${short[0]}${short[1]}${short[1]}${short[2]}${short[2]}`
@@ -1,6 +1,6 @@
1
1
  # template-resolver
2
2
 
3
- Strukturierter Content-Storage mit Tenant-Override-Hierarchie, Locale-Fallback und Resource-Linking via `file-foundation`. Eine Entity (`template-resource`) für alle Content-Arten — Render-Templates (`mail-html`, `document-pdf`, `notification`, `image-snapshot`), Markdown-Textblöcke (`text-block`, ex `text-content`-Feature) und AI-Prompts (`ai-prompt`).
3
+ Strukturierter Content-Storage mit Tenant-Override-Hierarchie, Locale-Fallback und Resource-Linking via `file-foundation`. Eine Entity (`template-resource`) für alle Content-Arten — Render-Templates (`mail`, `document-pdf`, `notification`, `image-snapshot`), Rahmen (`mail-layout`, `pdf-layout`), Markdown-Textblöcke (`text-block`, ex `text-content`-Feature) und AI-Prompts (`ai-prompt`).
4
4
 
5
5
  **Plan-Doc:** [`kumiko-platform/docs/plans/features/template-resolver.md`](../../../../../../kumiko-platform/docs/plans/features/template-resolver.md)
6
6
 
@@ -38,14 +38,51 @@ async function someHandler(ctx) {
38
38
  const template = await templateResolver.resolveTemplate({
39
39
  tenantId: ctx.user.tenantId,
40
40
  slug: "nka-versand",
41
- kind: "mail-html",
41
+ kind: "mail",
42
42
  locale: "de",
43
43
  });
44
- // template.content + template.variableSchema + template.linkedResources verwenden
44
+ // template.content + template.meta + template.variableSchema + template.linkedResources verwenden
45
45
  // ...
46
46
  }
47
47
  ```
48
48
 
49
+ ## Kinds, Format und Meta
50
+
51
+ | kind | contentFormat | meta |
52
+ |---|---|---|
53
+ | `mail` | `html` (Dokument-Schema, wird serverseitig neu aufgebaut) | Pflicht: `subject`; optional `previewText`, `layoutSlug`, `category` (`transactional`/`marketing`), `senderName`, `replyTo` |
54
+ | `mail-layout` | `mjml` | leer (`{}`) oder fehlend |
55
+ | `pdf-layout` | `html` | leer (`{}`) oder fehlend |
56
+ | `document-pdf` | `html` | fehlt |
57
+ | übrige | frei | fehlt |
58
+
59
+ `meta` liegt als JSON in der Spalte `meta` und wird beim Schreiben pro kind gegen
60
+ `template-meta.ts` geprüft (Fehler als Field-Issues auf `meta.*`). Subject und
61
+ Preview-Text erlauben nur einfache Platzhalter (`{{name}}`, `{{name.feld}}`), keine
62
+ Zeilenumbrüche.
63
+
64
+ Slugs sind kebab-Segmente, mit `:` verbunden (`templateSlugSchema`); Notification-Templates
65
+ liegen unter ihrem qualifizierten Namen `feature:notify:name`.
66
+
67
+ ## Platzhalter-Validierung (kind `mail`)
68
+
69
+ Die Variablen kommen aus der Notification mit gleichem qualifiziertem Namen
70
+ (`r.notification({ variables })`), sonst aus `variableSchema` der Zeile. Systemplatzhalter
71
+ (`tenantName`, `signature`, `imprint`, `unsubscribeUrl`) und der Resource-Key `logo` sind immer erlaubt.
72
+
73
+ - `upsertSystem` und `upsertTenant` lehnen unbekannte Platzhalter ab (Field-Issue auf `content`).
74
+ - `upsertSystem` und `upsertTenant` mit `status: "active"` verlangen zusätzlich, dass jede `required`-Variable benutzt wird. Ein Tenant-Draft darf unvollständig sein und liefert `validation` zurück.
75
+ - `publish` prüft die gespeicherte Zeile erneut.
76
+ - `seedSystemTemplate` wirft bei beiden Problemen.
77
+
78
+ ## Notification-Templates seeden
79
+
80
+ `r.notification({ variables, template, meta })` bringt Text und Meta mit dem Code mit.
81
+ `seedNotificationTemplates(db, registry)` (aus `.../template-resolver/seeding`) schreibt sie
82
+ pro Locale als System-Template; `applyBootSeeds` ruft das beim Boot auf. Der Code ist
83
+ maßgeblich: geänderte Texte aktualisieren die Zeile, unveränderte erzeugen kein Event.
84
+ Tenant-Overrides bleiben unberührt.
85
+
49
86
  ## Resolver-Reihenfolge (4-Stufen-Fallback)
50
87
 
51
88
  1. `tenantId` + requested locale
@@ -63,10 +100,10 @@ import { seedSystemTemplate } from "@cosmicdrift/kumiko-bundled-features/templat
63
100
  // runProdApp seeds — idempotent, default skip if slug exists
64
101
  await seedSystemTemplate(db, {
65
102
  slug: "welcome-email",
66
- kind: "notification",
103
+ kind: "mail",
67
104
  locale: "de",
68
- content: JSON.stringify({ header: "Willkommen", sections: [{ text: "…" }] }),
69
- contentFormat: "plain",
105
+ content: '<p>Willkommen, <span data-variable="name"></span></p>',
106
+ contentFormat: "html",
70
107
  });
71
108
  ```
72
109
 
@@ -80,8 +117,16 @@ await seedSystemTemplate(db, {
80
117
  | `TemplateResolverHandlers.upsertTenant` | `template-resolver:write:upsert-tenant` | TenantAdmin (eigener Tenant) + SystemAdmin via `tenantIdOverride` | Erstellt/Updated Tenant-Overrides (scope='tenant'), default-status='draft' |
81
118
  | `TemplateResolverHandlers.publish` | `template-resolver:write:publish` | TenantAdmin (eigener Tenant) | Setzt status='active' |
82
119
  | `TemplateResolverHandlers.archive` | `template-resolver:write:archive` | TenantAdmin (eigener Tenant) | Setzt status='archived' (Resolver ignoriert es danach) |
120
+ | `TemplateResolverHandlers.publishSystem` | `template-resolver:write:publish-system` | SystemAdmin | Setzt status='active' auf einem System-Default (gleiche Mail-Prüfung wie `publish`) |
121
+ | `TemplateResolverHandlers.archiveSystem` | `template-resolver:write:archive-system` | SystemAdmin | Setzt status='archived' auf einem System-Default; Tenants ohne Override fallen durch |
122
+ | `TemplateResolverHandlers.resetToSystem` | `template-resolver:write:reset-to-system` | TenantAdmin (eigener Tenant) | Archiviert den eigenen Override, sodass wieder die aktive System-Vorlage greift; ohne aktive System-Vorlage `no_system_template` |
83
123
  | `TemplateResolverQueries.findById` | `template-resolver:query:find-by-id` | TenantAdmin + User (eigener Tenant + system-templates sichtbar) | Raw-Lookup für Edit-UI |
84
124
  | `TemplateResolverQueries.list` | `template-resolver:query:list` | gleich | Filter nach kind/locale/status, optional includeSystem |
125
+ | `TemplateResolverQueries.usage` | `template-resolver:query:usage` | TenantAdmin | Welche Notifications eine Mail-Vorlage bzw. ein Mail-Layout verwenden (aus der Registry) |
126
+ | `TemplateResolverQueries.overview` | `template-resolver:query:overview` | TenantAdmin + SystemAdmin | Eine Zeile je Slug und Art (Mail, PDF, Layouts) mit Herkunft, Sprachen, Status, Version, Layout und Verwendung; durchsuch-, filter-, sortier- und seitenweise lesbar |
127
+ | `TemplateResolverQueries.layoutSource` | `template-resolver:query:layout-source` | TenantAdmin + SystemAdmin | Eigene und System-Fassung eines Layouts, Kopf/Signatur/Fuß-Bereiche der wirksamen html-Fassung, Speicher-Locale; MJML-Quelle nur für SystemAdmin |
128
+ | `TemplateResolverQueries.tenantOverrides` | `template-resolver:query:tenant-overrides` | SystemAdmin | Anzahl aktiver/Entwurf-Overrides einer System-Vorlage und aktive Tenants gesamt, nur Zahlen |
129
+ | `TemplateResolverQueries.diffToSystem` | `template-resolver:query:diff-to-system` | TenantAdmin (eigener Tenant) | Block- und Meta-Vergleich eines Tenant-Overrides mit der System-Vorlage |
85
130
 
86
131
  **SystemAdmin-Cross-Tenant für publish/archive/findById:** deferred (Task 8) — `upsertTenant` hat `tenantIdOverride` bereits; publish/archive brauchen das erst wenn ein Sysadmin-Panel fremde Tenant-Templates kuratiert. `ctx.db` ist tenant-scoped, fremde IDs → NotFound ohne Override.
87
132
 
@@ -92,10 +137,12 @@ upsertSystem ──┐
92
137
  ├──► status: "active" (System-Default sofort aktiv)
93
138
  upsertTenant ──┴──► status: "draft" (Default) | "active" (explizit)
94
139
 
95
- publish ───────► status: "active"
140
+ publish ───────► status: "active" (übernimmt einen offenen Entwurf)
96
141
  archive ───────► status: "archived"
142
+ discardDraft ──► verwirft den Entwurf neben der aktiven Fassung
97
143
  ```
98
144
 
145
+ Ein Entwurf über einer aktiven Zeile (`upsertTenant` ohne `status`, `copy-locale`, `import-bundle`) liegt als JSON in `draft` neben der Zeile; die Hauptfelder bleiben die ausgelieferte Fassung. `find-by-id` und `layout-source` liefern die Arbeitsfassung (Entwurf, sonst gespeichert) mit `active { version, since }`.
99
146
  Resolver returnt **nur** Templates mit `status: "active"`. draft/archived werden ignoriert.
100
147
 
101
148
  ## Consumer Conformance
@@ -162,7 +209,7 @@ createTemplateResolverFeature({
162
209
  collections: [
163
210
  {
164
211
  id: "reply-snippets",
165
- kind: "mail-html",
212
+ kind: "mail",
166
213
  access: { roles: ["Agent", "TenantAdmin"] },
167
214
  nav: { label: "mail:nav.snippets", parent: "mail:nav:root" },
168
215
  },
@@ -245,6 +292,17 @@ nebeneinander und führt sie in der UI zusammen.
245
292
  Unter `tenantIdOverride` (SystemAdmin) bleibt die Zeile die **eigene** des
246
293
  Aufrufers im fremden Tenant — ein Admin editiert nie die Signatur eines anderen.
247
294
 
295
+ ## Validierung, Layouts, Bundles
296
+
297
+ - `validate` (Query) trocken-läuft die Schreib-Pipeline: `canSave`, `canPublish`, verwendete/unbekannte/fehlende Platzhalter, verfügbare Variablen. `mail`, `document-pdf`, `pdf-layout` und `mail-layout` laufen durch dasselbe Platzhalter-Gate; `page`/`pages` gibt es nur in `document-pdf` und `pdf-layout`.
298
+ - `preview` (Query, TenantAdmin/SystemAdmin) rendert einen Entwurf oder, ohne `content`, die gespeicherte Vorlage mit Beispielwerten aus dem `example` jeder deklarierten Variable (Listen: zwei Zeilen, ohne `example` feste Ersatzwerte). Der Entwurf läuft durch dieselben Gates wie ein Speichern, eine Mail durch den echten Mail-Renderer. Antwort: Betreff, HTML, `displayHtml` (nur Mail: `cid:`-Bilder als Data-URI für den Editor, 5 MiB Budget), Text, Anhänge (`kind: "inline"` mit `cid` oder `kind: "file"`, jeweils mit Größe) und `checks` (`resources_resolved`, `text_present`, `size_under_cap` mit 10 MiB, `required_placeholders`). `document-pdf` liefert den bereinigten Body und die Layout-Regionen, kein PDF.
299
+ - `send-test` (Write, TenantAdmin/SystemAdmin) schickt die gerenderte Mail nur an die eigene Adresse des Aufrufers, Betreff mit `[TEST] `, höchstens 10 pro Nutzer und Stunde. Es gibt keine Empfänger-Eingabe. Braucht `delivery`, `channel-email` und `renderer-mail-html`.
300
+ - Layouts: Tenant-`mail-layout` und `pdf-layout` sind strukturiertes HTML mit `data-content`-Slot und Regionen (Header, Signatur, Footer). Rohes MJML mit `{{{content}}}` genau einmal darf nur das System schreiben.
301
+ - `export-bundle`, `import-bundle`, `import-bundle-system`: ein Template mit allen Locales und Ressourcen-Dateien (base64) als eigenständiges Bundle; bestehende Ziele nur mit `overwrite`. Systemimport mit Ressourcen braucht einen Caller im System-Tenant.
302
+ - `copy-locale`: Startpunkt für Übersetzungen, kopiert als Tenant-Draft. Mit `scope: "system"` (nur SystemAdmin) wird eine Systemvorlage als System-Draft kopiert; andere Aufrufer bekommen 403.
303
+ - Mail-Anhänge: `resource-upload` mit `purpose: "attachment"` und `fileName` legt eine PDF-, PNG- oder JPEG-Datei (geprüft am Inhalt, Originalbytes, höchstens 10 pro Mail-Vorlage) neben den Inline-Bildern ab. `resource-remove`, `copy-locale` und die Bundles tragen sie mit (Bundle-Eintrag `purpose: "attachment"`). Der Mail-Renderer hängt sie nach den Inline-Bildern an.
304
+ - `r.notification(..., { record: { entity, map } })` mappt eine Entity-Zeile auf die deklarierten Variablen (`mapRecordToVariables`); Boot prüft Entity, Variablen und Feldpfade.
305
+
248
306
  ## Out-of-Scope
249
307
 
250
308
  - Rendering (Markdown/MJML → HTML/PDF) — siehe `renderer-foundation`