@cosmicdrift/kumiko-samples 0.362.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 (252) 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/pages.ts +6 -3
  75. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  76. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  77. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -0
  78. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  79. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  80. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  81. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  82. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  83. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  84. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  85. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  86. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  87. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  88. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  89. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  90. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  91. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  92. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +40 -25
  93. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  94. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  95. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  96. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  97. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  98. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  99. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  100. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  101. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  102. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  103. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  104. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  105. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  106. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  107. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  108. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  109. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  110. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  111. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  112. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  113. package/packages/bundled-features/src/renderer-foundation/types.ts +132 -35
  114. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  115. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  116. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  117. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  118. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  119. package/packages/bundled-features/src/shared/index.ts +6 -0
  120. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  121. package/packages/bundled-features/src/shared/mail-template-body.ts +25 -0
  122. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  123. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  124. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  125. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  126. package/packages/bundled-features/src/template-resolver/README.md +63 -7
  127. package/packages/bundled-features/src/template-resolver/api.ts +83 -8
  128. package/packages/bundled-features/src/template-resolver/changes.json +118 -0
  129. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
  130. package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
  131. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  132. package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
  133. package/packages/bundled-features/src/template-resolver/feature.ts +56 -6
  134. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  135. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  136. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +142 -0
  137. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
  138. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
  139. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
  140. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  141. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  142. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +108 -0
  143. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
  144. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -0
  145. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  146. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  147. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  148. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  149. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  150. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  151. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +106 -0
  152. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  153. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  154. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +184 -0
  155. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
  156. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
  157. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
  158. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  159. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +15 -76
  160. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
  161. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  162. package/packages/bundled-features/src/template-resolver/index.ts +100 -0
  163. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  164. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  165. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  166. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  167. package/packages/bundled-features/src/template-resolver/qualified-names.ts +30 -0
  168. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  169. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  170. package/packages/bundled-features/src/template-resolver/seeding.ts +147 -17
  171. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  172. package/packages/bundled-features/src/template-resolver/table.ts +14 -0
  173. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  174. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  175. package/packages/bundled-features/src/template-resolver/template-meta.ts +129 -0
  176. package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
  177. package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
  178. package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
  179. package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
  180. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  181. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
  182. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
  183. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
  184. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
  185. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
  186. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  187. package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
  188. package/packages/bundled-features/src/template-resolver/web/index.ts +19 -0
  189. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  190. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +169 -0
  191. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  192. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  193. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  194. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  195. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  196. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  197. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  198. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  199. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  200. package/packages/bundled-features/src/user-data-rights/changes.json +7 -0
  201. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  202. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  203. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  204. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  205. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  206. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  207. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  208. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  209. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  210. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  211. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  212. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  213. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  214. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  215. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  216. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +1 -2
  217. package/samples/apps/ui-walkthrough/package.json +2 -0
  218. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  219. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  220. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +154 -0
  221. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  222. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  223. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +354 -0
  224. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -0
  225. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  226. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  227. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  228. package/samples/apps/ui-walkthrough/src/run-config.ts +37 -1
  229. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  230. package/samples/apps/use-all-bundled/feature-manifest.json +115 -20
  231. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  232. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  233. package/samples/apps/use-all-bundled/package.json +2 -0
  234. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  235. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  236. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  237. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  238. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  239. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  240. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  241. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  242. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  243. package/samples/recipes/delivery-notifications/README.md +26 -25
  244. package/samples/recipes/delivery-notifications/package.json +2 -1
  245. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  246. package/samples/recipes/seed-migration/README.md +1 -1
  247. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  248. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  249. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  250. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  251. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  252. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
14
14
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
15
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
15
16
 
16
17
  const STATE_PURPOSE = "inbound-mail-oauth-connect";
17
18
 
@@ -91,13 +92,12 @@ export function verifyOAuthState(
91
92
  }
92
93
 
93
94
  function isOAuthStatePayload(v: unknown): v is OAuthStatePayload {
94
- if (typeof v !== "object" || v === null) return false;
95
- const o = v as Record<string, unknown>;
95
+ if (!isPlainObject(v)) return false;
96
96
  return (
97
- typeof o["tenantId"] === "string" &&
98
- (o["ownerUserId"] === null || typeof o["ownerUserId"] === "string") &&
99
- typeof o["providerKey"] === "string" &&
100
- typeof o["mailbox"] === "string" &&
101
- typeof o["nonce"] === "string"
97
+ typeof v["tenantId"] === "string" &&
98
+ (v["ownerUserId"] === null || typeof v["ownerUserId"] === "string") &&
99
+ typeof v["providerKey"] === "string" &&
100
+ typeof v["mailbox"] === "string" &&
101
+ typeof v["nonce"] === "string"
102
102
  );
103
103
  }
@@ -157,12 +157,8 @@ async function purgeExpiredSeenAnchors(
157
157
  // direkt tenant-scoped, kein Account-Join. Trifft auch Anker, deren Account
158
158
  // schon disconnected/geloescht ist.
159
159
  const where = { tenantId, seenAt: { lt: cutoff } };
160
- const expired = await selectMany<{ accountId: string }>(
161
- db,
162
- seenMessageTable as EntityTableMeta,
163
- where,
164
- );
160
+ const expired = await selectMany<{ accountId: string }>(db, seenMessageTable, where);
165
161
  if (expired.length === 0) return 0;
166
- await deleteMany(db, seenMessageTable as EntityTableMeta, where);
162
+ await deleteMany(db, seenMessageTable, where);
167
163
  return expired.length;
168
164
  }
@@ -62,27 +62,19 @@ async function archiveAndDeleteRows(
62
62
  export async function mailAccountTenantDestroyHook(ctx: TenantDataHookCtx): Promise<void> {
63
63
  const accountIds = await archiveAndDeleteRows(
64
64
  ctx,
65
- mailAccountsProjectionTable as EntityTableMeta,
65
+ mailAccountsProjectionTable,
66
66
  MAIL_ACCOUNT_AGGREGATE_TYPE,
67
67
  );
68
68
  for (const accountId of accountIds) {
69
- await ctx.db.deleteMany(syncCursorTable as EntityTableMeta, { accountId });
70
- await ctx.db.deleteMany(seenMessageTable as EntityTableMeta, { accountId });
69
+ await ctx.db.deleteMany(syncCursorTable, { accountId });
70
+ await ctx.db.deleteMany(seenMessageTable, { accountId });
71
71
  }
72
72
  }
73
73
 
74
74
  export async function inboundMessageTenantDestroyHook(ctx: TenantDataHookCtx): Promise<void> {
75
- await archiveAndDeleteRows(
76
- ctx,
77
- inboundMessagesProjectionTable as EntityTableMeta,
78
- INBOUND_MESSAGE_AGGREGATE_TYPE,
79
- );
75
+ await archiveAndDeleteRows(ctx, inboundMessagesProjectionTable, INBOUND_MESSAGE_AGGREGATE_TYPE);
80
76
  }
81
77
 
82
78
  export async function mailThreadTenantDestroyHook(ctx: TenantDataHookCtx): Promise<void> {
83
- await archiveAndDeleteRows(
84
- ctx,
85
- mailThreadsProjectionTable as EntityTableMeta,
86
- MAIL_THREAD_AGGREGATE_TYPE,
87
- );
79
+ await archiveAndDeleteRows(ctx, mailThreadsProjectionTable, MAIL_THREAD_AGGREGATE_TYPE);
88
80
  }
@@ -257,17 +257,15 @@ export class InboundTransientError extends Error {
257
257
  }
258
258
  }
259
259
 
260
- type KindCarrier = { readonly kind?: unknown };
261
-
262
260
  export function isInboundAuthError(e: unknown): e is InboundAuthError {
263
- return e instanceof Error && (e as KindCarrier).kind === "inbound-auth-error";
261
+ return e instanceof Error && "kind" in e && e.kind === "inbound-auth-error";
264
262
  }
265
263
  export function isInboundRateLimitError(e: unknown): e is InboundRateLimitError {
266
- return e instanceof Error && (e as KindCarrier).kind === "inbound-rate-limit-error";
264
+ return e instanceof Error && "kind" in e && e.kind === "inbound-rate-limit-error";
267
265
  }
268
266
  export function isInboundCursorInvalidError(e: unknown): e is InboundCursorInvalidError {
269
- return e instanceof Error && (e as KindCarrier).kind === "inbound-cursor-invalid-error";
267
+ return e instanceof Error && "kind" in e && e.kind === "inbound-cursor-invalid-error";
270
268
  }
271
269
  export function isInboundTransientError(e: unknown): e is InboundTransientError {
272
- return e instanceof Error && (e as KindCarrier).kind === "inbound-transient-error";
270
+ return e instanceof Error && "kind" in e && e.kind === "inbound-transient-error";
273
271
  }
@@ -49,9 +49,10 @@ import {
49
49
  configuredPiiSubjectKms,
50
50
  decryptPiiFieldValues,
51
51
  } from "@cosmicdrift/kumiko-framework/crypto";
52
- import type { DbConnection, EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
52
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
53
53
  import type { DistributedLock } from "@cosmicdrift/kumiko-framework/pipeline";
54
54
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
55
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
55
56
  import { InboundMailAccountStatuses, InboundMailFoundationHandlers } from "./constants.js";
56
57
  import { MAIL_ACCOUNT_PII_FIELDS, syncCursorTable } from "./entities.js";
57
58
  import {
@@ -158,6 +159,17 @@ export type InboundMailSupervisor = {
158
159
  readonly stop: () => Promise<void>;
159
160
  };
160
161
 
162
+ type ActiveMailAccountRow = {
163
+ readonly id: string;
164
+ readonly tenantId: string;
165
+ readonly provider: string;
166
+ readonly authMethod: string;
167
+ readonly ownerUserId?: string | null;
168
+ readonly address: string;
169
+ readonly displayName?: string;
170
+ readonly watchState?: string;
171
+ };
172
+
161
173
  export function createInboundMailSupervisor(
162
174
  deps: InboundMailSupervisorDeps,
163
175
  ): InboundMailSupervisor {
@@ -206,30 +218,32 @@ export function createInboundMailSupervisor(
206
218
  // (Provider arbeiten nie mit Ciphertext).
207
219
  // ---------------------------------------------------------------
208
220
  async function listActiveAccounts(): Promise<readonly MailAccountRecord[]> {
209
- const rows = await selectMany(deps.db, mailAccountsProjectionTable, {
221
+ const rows = await selectMany<ActiveMailAccountRow>(deps.db, mailAccountsProjectionTable, {
210
222
  status: InboundMailAccountStatuses.active,
211
223
  });
212
224
  const piiKms = configuredPiiSubjectKms();
213
225
  const records: MailAccountRecord[] = [];
214
- for (const raw of rows) {
215
- const row = piiKms
216
- ? await decryptPiiFieldValues(
217
- raw as Record<string, unknown>,
218
- MAIL_ACCOUNT_PII_FIELDS,
219
- piiKms,
220
- { requestId: "inbound-mail-foundation:supervisor:list-accounts" },
221
- )
222
- : (raw as Record<string, unknown>);
226
+ for (const row of rows) {
227
+ const decrypted = piiKms
228
+ ? await decryptPiiFieldValues(row, MAIL_ACCOUNT_PII_FIELDS, piiKms, {
229
+ requestId: "inbound-mail-foundation:supervisor:list-accounts",
230
+ })
231
+ : row;
232
+ const decryptedAddress = decrypted["address"];
233
+ // Falling back to row.address would hand the ciphertext to a provider.
234
+ if (typeof decryptedAddress !== "string") {
235
+ throw new Error(`inbound-mail: address of mail account ${row.id} did not decrypt to text`);
236
+ }
223
237
  records.push({
224
- id: row["id"] as string,
225
- tenantId: row["tenantId"] as string,
226
- provider: row["provider"] as string,
227
- authMethod: row["authMethod"] as string,
228
- ownerUserId: (row["ownerUserId"] as string | null | undefined) ?? null,
229
- address: row["address"] as string,
230
- displayName: (row["displayName"] as string | undefined) ?? "",
238
+ id: row.id,
239
+ tenantId: row.tenantId,
240
+ provider: row.provider,
241
+ authMethod: row.authMethod,
242
+ ownerUserId: row.ownerUserId ?? null,
243
+ address: decryptedAddress,
244
+ displayName: row.displayName ?? "",
231
245
  status: InboundMailAccountStatuses.active,
232
- watchState: (row["watchState"] as string | undefined) ?? "idle",
246
+ watchState: row.watchState ?? "idle",
233
247
  });
234
248
  }
235
249
  return records;
@@ -239,13 +253,14 @@ export function createInboundMailSupervisor(
239
253
  // Cursor-Persistenz (unmanaged direct-write store).
240
254
  // ---------------------------------------------------------------
241
255
  async function loadCursor(accountId: string): Promise<SyncCursorPayload | null> {
242
- const row = await fetchOne<{ cursor: string }>(deps.db, syncCursorTable as EntityTableMeta, {
256
+ const row = await fetchOne<{ cursor: string }>(deps.db, syncCursorTable, {
243
257
  accountId,
244
258
  scope: CURSOR_SCOPE,
245
259
  });
246
260
  if (!row) return null;
247
261
  try {
248
- return JSON.parse(row.cursor) as SyncCursorPayload; // @cast-boundary eigene JSON-Persistenz
262
+ const parsed: unknown = JSON.parse(row.cursor);
263
+ return isPlainObject(parsed) ? parsed : null;
249
264
  } catch {
250
265
  return null; // korrupter Cursor = wie kein Cursor → Backfill, Dedup fängt Dubletten
251
266
  }
@@ -258,21 +273,21 @@ export function createInboundMailSupervisor(
258
273
  const accountId = account.id;
259
274
  const now = Temporal.Now.instant().toString();
260
275
  const serialized = JSON.stringify(cursor);
261
- const existing = await fetchOne<{ id: string }>(deps.db, syncCursorTable as EntityTableMeta, {
276
+ const existing = await fetchOne<{ id: string }>(deps.db, syncCursorTable, {
262
277
  accountId,
263
278
  scope: CURSOR_SCOPE,
264
279
  });
265
280
  if (existing) {
266
281
  await updateMany(
267
282
  deps.db,
268
- syncCursorTable as EntityTableMeta,
283
+ syncCursorTable,
269
284
  { cursor: serialized, updatedAt: now },
270
285
  { accountId, scope: CURSOR_SCOPE },
271
286
  );
272
287
  // skip: update-Pfad fertig — nicht in den insert-Zweig durchfallen.
273
288
  return;
274
289
  }
275
- await insertOne(deps.db, syncCursorTable as EntityTableMeta, {
290
+ await insertOne(deps.db, syncCursorTable, {
276
291
  id: crypto.randomUUID(),
277
292
  // Direct-write store: no executor stamps the NOT NULL tenant_id for us.
278
293
  tenantId: account.tenantId,
@@ -288,7 +303,7 @@ export function createInboundMailSupervisor(
288
303
  // → nächster fetch läuft als Backfill.
289
304
  await updateMany(
290
305
  deps.db,
291
- syncCursorTable as EntityTableMeta,
306
+ syncCursorTable,
292
307
  { cursor: "", updatedAt: Temporal.Now.instant().toString() },
293
308
  { accountId, scope: CURSOR_SCOPE },
294
309
  );
@@ -18,6 +18,7 @@ import {
18
18
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
19
19
  import { legacyDateToInstant } from "@cosmicdrift/kumiko-framework/time";
20
20
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
21
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
21
22
  import { ImapFlow } from "imapflow";
22
23
  import { type AddressObject, type ParsedMail, simpleParser } from "mailparser";
23
24
  import type { ImapCredentialDocument } from "./credential-document.js";
@@ -97,11 +98,11 @@ export function mapImapError(err: unknown, host: string): Error {
97
98
  // imapflow-Fehler tragen die Server-Antwort in responseText, die
98
99
  // message ist nur "Command failed" — beides in den Match einbeziehen;
99
100
  // authenticationFailed ist das verlässliche Flag.
100
- const carrier = err as { authenticationFailed?: unknown; responseText?: unknown };
101
- const responseText = typeof carrier?.responseText === "string" ? carrier.responseText : "";
101
+ const carrier = isPlainObject(err) ? err : undefined;
102
+ const responseText = typeof carrier?.["responseText"] === "string" ? carrier["responseText"] : "";
102
103
  const msg = `${err instanceof Error ? err.message : String(err)} ${responseText}`.trim();
103
104
  if (
104
- carrier?.authenticationFailed === true ||
105
+ carrier?.["authenticationFailed"] === true ||
105
106
  /auth(entication)? failed|invalid credentials|invalid login|LOGIN failed|AUTHENTICATIONFAILED/i.test(
106
107
  msg,
107
108
  )
@@ -12,7 +12,7 @@ export type ManualJobCatalogEntry = {
12
12
  function payloadSchemaJson(job: JobDefinition): Record<string, unknown> | null {
13
13
  if (job.schema === undefined) return null;
14
14
  try {
15
- return z.toJSONSchema(job.schema) as Record<string, unknown>;
15
+ return z.toJSONSchema(job.schema);
16
16
  } catch {
17
17
  // Non-JSON-Schema-able Zod types (transforms/effects) — UI still lists the job.
18
18
  return null;
@@ -63,9 +63,7 @@ export function findReversedIds(
63
63
  ): Set<string> {
64
64
  const ids = new Set(rows.map((r) => r.id));
65
65
  return new Set(
66
- rows
67
- .filter((r) => r.reference !== null && ids.has(r.reference))
68
- .map((r) => r.reference as string),
66
+ rows.flatMap((r) => (r.reference !== null && ids.has(r.reference) ? [r.reference] : [])),
69
67
  );
70
68
  }
71
69
 
@@ -50,6 +50,11 @@ const DESCRIPTION_PATTERN = { regex: "^[\\s\\S]{0,500}$" } as const;
50
50
  // there.
51
51
  const CUSTOM_CSS_PATTERN = { regex: "^[\\s\\S]{0,8000}$" } as const;
52
52
 
53
+ // Empty or a FileRef id (UUID); the upload UI that fills it ships separately.
54
+ const FILE_REF_ID_PATTERN = {
55
+ regex: "^$|^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
56
+ } as const;
57
+
53
58
  export const LAYOUT_PRESETS = ["minimal", "centered", "wide"] as const;
54
59
 
55
60
  // Companion toggle feature name. managed-pages reads ctx.hasFeature() against it
@@ -91,6 +96,12 @@ export const BRANDING_KEYS = {
91
96
  pattern: HTTPS_PATTERN,
92
97
  write: BRANDING_WRITE,
93
98
  }),
99
+ // Ownership is checked where it is read (renderer-mail-html: FileRef must belong to the rendering tenant).
100
+ brandingLogoFile: createTenantConfig("text", {
101
+ default: "",
102
+ pattern: FILE_REF_ID_PATTERN,
103
+ write: BRANDING_WRITE,
104
+ }),
94
105
  brandingLayoutPreset: createTenantConfig("select", {
95
106
  default: "centered",
96
107
  options: LAYOUT_PRESETS,
@@ -112,6 +123,7 @@ export const BRANDING_QN = {
112
123
  siteUrl: "managed-pages:config:branding-site-url",
113
124
  accentColor: "managed-pages:config:branding-accent-color",
114
125
  logoUrl: "managed-pages:config:branding-logo-url",
126
+ logoFile: "managed-pages:config:branding-logo-file",
115
127
  layoutPreset: "managed-pages:config:branding-layout-preset",
116
128
  customCss: "managed-pages:config:branding-custom-css",
117
129
  } as const;
@@ -129,17 +141,28 @@ async function readText(config: ConfigAccessor, qualifiedKey: string): Promise<s
129
141
  // publicstatus' branding read), not a crash.
130
142
  export async function readBranding(config: ConfigAccessor | undefined): Promise<BrandingTokens> {
131
143
  if (!config) return EMPTY_BRANDING;
132
- const [title, description, siteUrl, accentColor, logoUrl, layoutPreset] = await Promise.all([
133
- readText(config, BRANDING_QN.title),
134
- readText(config, BRANDING_QN.description),
135
- readText(config, BRANDING_QN.siteUrl),
136
- readText(config, BRANDING_QN.accentColor),
137
- readText(config, BRANDING_QN.logoUrl),
138
- readText(config, BRANDING_QN.layoutPreset),
139
- ]);
144
+ const [title, description, siteUrl, accentColor, logoUrl, logoFile, layoutPreset] =
145
+ await Promise.all([
146
+ readText(config, BRANDING_QN.title),
147
+ readText(config, BRANDING_QN.description),
148
+ readText(config, BRANDING_QN.siteUrl),
149
+ readText(config, BRANDING_QN.accentColor),
150
+ readText(config, BRANDING_QN.logoUrl),
151
+ readText(config, BRANDING_QN.logoFile),
152
+ readText(config, BRANDING_QN.layoutPreset),
153
+ ]);
140
154
  // customCss stays "" here — it is gated (allowCustomCss + per-tenant toggle)
141
155
  // and read separately by the branding query via readCustomCss.
142
- return { title, description, siteUrl, accentColor, logoUrl, layoutPreset, customCss: "" };
156
+ return {
157
+ title,
158
+ description,
159
+ siteUrl,
160
+ accentColor,
161
+ logoUrl,
162
+ logoFile,
163
+ layoutPreset,
164
+ customCss: "",
165
+ };
143
166
  }
144
167
 
145
168
  // Read the raw, untrusted custom CSS for the request's tenant. The caller (the
@@ -167,6 +190,7 @@ export function coerceBranding(value: unknown): BrandingTokens {
167
190
  siteUrl: stringField(source, "siteUrl"),
168
191
  accentColor: stringField(source, "accentColor"),
169
192
  logoUrl: stringField(source, "logoUrl"),
193
+ logoFile: stringField(source, "logoFile"),
170
194
  layoutPreset: stringField(source, "layoutPreset"),
171
195
  customCss: stringField(source, "customCss"),
172
196
  };
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.363.0",
4
+ "type": "improvement",
5
+ "title": "Branding screen edits the logo file and readBranding returns logoFile",
6
+ "detail": "The branding settings screen has a logo upload for managed-pages:config:branding-logo-file, and readBranding and the branding query include logoFile next to logoUrl. The template resource errors template_resource_too_large and template_resource_unsupported_image have German and Spanish texts."
7
+ },
2
8
  {
3
9
  "version": "0.353.0",
4
10
  "type": "fix",
@@ -47,6 +47,7 @@ function brandingRevisionSeed(branding: BrandingTokens): string {
47
47
  branding.siteUrl,
48
48
  branding.accentColor,
49
49
  branding.logoUrl,
50
+ branding.logoFile,
50
51
  branding.layoutPreset,
51
52
  branding.customCss,
52
53
  ]);
@@ -42,6 +42,7 @@ export const MANAGED_PAGES_I18N: Readonly<Record<string, LocalizedString>> = {
42
42
  en: "Accent color",
43
43
  },
44
44
  "managed-pages:entity:__config-edit__:field:logoUrl": { en: "Logo URL" },
45
+ "managed-pages:entity:__config-edit__:field:logoFile": { en: "Logo file" },
45
46
  "managed-pages:entity:__config-edit__:field:layoutPreset": { en: "Layout" },
46
47
 
47
48
  // Section-Header
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  type ConfigEditScreenDefinition,
3
+ createImageField,
3
4
  createSelectField,
4
5
  createTextField,
5
6
  } from "@cosmicdrift/kumiko-framework/engine";
@@ -35,6 +36,7 @@ export function createBrandingSettingsScreen(opts: {
35
36
  siteUrl: BRANDING_QN.siteUrl,
36
37
  accentColor: BRANDING_QN.accentColor,
37
38
  logoUrl: BRANDING_QN.logoUrl,
39
+ logoFile: BRANDING_QN.logoFile,
38
40
  layoutPreset: BRANDING_QN.layoutPreset,
39
41
  },
40
42
  fields: {
@@ -62,6 +64,11 @@ export function createBrandingSettingsScreen(opts: {
62
64
  personal: false,
63
65
  reason: "is_business_data",
64
66
  }),
67
+ logoFile: createImageField({
68
+ accept: ["image/png", "image/jpeg", "image/webp"],
69
+ personal: false,
70
+ reason: "is_business_data",
71
+ }),
65
72
  layoutPreset: createSelectField({ options: LAYOUT_PRESETS }),
66
73
  },
67
74
  layout: {
@@ -75,6 +82,7 @@ export function createBrandingSettingsScreen(opts: {
75
82
  { field: "description", span: 2 },
76
83
  { field: "siteUrl", span: 1 },
77
84
  { field: "logoUrl", span: 1 },
85
+ { field: "logoFile", span: 1 },
78
86
  { field: "accentColor", span: 1 },
79
87
  ],
80
88
  },
@@ -13,6 +13,8 @@ export type BrandingTokens = {
13
13
  readonly siteUrl: string;
14
14
  readonly accentColor: string;
15
15
  readonly logoUrl: string;
16
+ // FileRef id of an uploaded logo ("" = none); the renderer resolves it, tenant ownership is checked there.
17
+ readonly logoFile: string;
16
18
  readonly layoutPreset: string;
17
19
  // RAW, untrusted tenant CSS — "" unless the custom-css capability is gated on
18
20
  // (managed-pages allowCustomCss + the per-tenant toggle). It is NOT pre-
@@ -29,6 +31,7 @@ export const EMPTY_BRANDING: BrandingTokens = {
29
31
  siteUrl: "",
30
32
  accentColor: "",
31
33
  logoUrl: "",
34
+ logoFile: "",
32
35
  layoutPreset: "",
33
36
  customCss: "",
34
37
  };
@@ -25,7 +25,7 @@ safeRenderer.use({
25
25
  });
26
26
 
27
27
  export function renderSafeMarkdown(markdown: string): string {
28
- // @cast-boundary marked.parse return-type ist `string | Promise<string>`;
28
+ // @cast-boundary third-party-type-gap marked.parse return-type ist `string | Promise<string>`;
29
29
  // `{ async: false }` garantiert sync (string) — Cast nur API-Vertragsfix.
30
30
  return safeRenderer.parse(markdown, { async: false }) as string;
31
31
  }
@@ -42,11 +42,9 @@ export function bindPatAutoRevokeOnPasswordChangeFromFeature(
42
42
  ): BindPatAutoRevokeOnPasswordChange | undefined {
43
43
  const exports = feature.exports;
44
44
  if (exports && typeof exports === "object" && "bindAutoRevokeOnPasswordChange" in exports) {
45
- const { bindAutoRevokeOnPasswordChange } = exports as {
46
- bindAutoRevokeOnPasswordChange: unknown;
47
- };
45
+ const { bindAutoRevokeOnPasswordChange } = exports;
48
46
  if (typeof bindAutoRevokeOnPasswordChange === "function") {
49
- // @cast-boundary exports-walk — feature.exports is untyped by design
47
+ // @cast-boundary feature-exports — feature.exports is untyped by design
50
48
  return bindAutoRevokeOnPasswordChange as BindPatAutoRevokeOnPasswordChange;
51
49
  }
52
50
  }
@@ -1,5 +1,6 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { definePagedQueryHandler, MAX_LIST_LIMIT } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
3
4
  import * as z from "zod";
4
5
  import { decryptStoredPii } from "../../shared/index.js";
5
6
  import { isExpiredAt } from "../expiry.js";
@@ -13,7 +14,7 @@ const SORTABLE_COLUMNS = ["id", "name", "createdAt", "expiresAt", "revokedAt"] a
13
14
  type SortableColumn = (typeof SORTABLE_COLUMNS)[number];
14
15
 
15
16
  function isSortableColumn(value: string): value is SortableColumn {
16
- return (SORTABLE_COLUMNS as readonly string[]).includes(value);
17
+ return isOneOf(SORTABLE_COLUMNS, value);
17
18
  }
18
19
 
19
20
  type PatStatus = "active" | "revoked" | "expired";
@@ -29,7 +29,7 @@ export { expandScopes } from "./scopes.js";
29
29
  export function patRateLimitFromFeature(feature: FeatureDefinition): PatRateLimit {
30
30
  const exports = feature.exports;
31
31
  if (exports && typeof exports === "object" && "rateLimit" in exports) {
32
- const { rateLimit } = exports as { rateLimit: unknown };
32
+ const { rateLimit } = exports;
33
33
  if (
34
34
  rateLimit &&
35
35
  typeof rateLimit === "object" &&
@@ -4,6 +4,7 @@ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import { append } from "@cosmicdrift/kumiko-framework/event-store";
5
5
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
6
6
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
7
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
7
8
  import {
8
9
  PAT_REVOKED_AGGREGATE_TYPE,
9
10
  PAT_REVOKED_EVENT_QN,
@@ -13,7 +14,7 @@ import { apiTokenTable } from "./schema/api-token.js";
13
14
 
14
15
  // A pool has begin(); a tx/savepoint handle does not and is already atomic.
15
16
  function canBeginTransaction(db: DbRunner): boolean {
16
- return typeof (db as { begin?: unknown }).begin === "function"; // @cast-boundary driver-handle-probe
17
+ return typeof propertyOf(db, "begin") === "function";
17
18
  }
18
19
 
19
20
  // Cross-tenant revoke: password-change and MFA-enable/disable are account-
@@ -1,6 +1,6 @@
1
1
  # renderer-foundation
2
2
 
3
- Plugin-Foundation für Renderer (Notification, HTML-Mail, PDF, Image). Plugins (`renderer-simple`, `renderer-mail-html`, `renderer-puppeteer-client`) registrieren sich via `r.useExtension("renderer", "<name>", { kinds, render })`.
3
+ Plugin-Foundation für Renderer (HTML-Mail, PDF, Image). Plugins (`renderer-mail-html`, `renderer-puppeteer-client`) registrieren sich via `r.useExtension("renderer", "<name>", { kinds, render })`.
4
4
 
5
5
  **Plan-Doc:** [`kumiko-platform/docs/plans/features/renderer-foundation.md`](../../../../../../kumiko-platform/docs/plans/features/renderer-foundation.md)
6
6
 
@@ -16,8 +16,7 @@ import {
16
16
  const features = [
17
17
  createTemplateResolverFeature(),
18
18
  createRendererFoundationFeature(),
19
- createRendererSimpleFeature(), // Plugin
20
- createRendererMailHtmlFeature(), // Plugin (enterprise)
19
+ createRendererMailHtmlFeature(), // Plugin: kind "mail"
21
20
  // weitere Plugins...
22
21
  ];
23
22
 
@@ -38,23 +37,25 @@ import { requireRendererFoundation } from "@cosmicdrift/kumiko-bundled-features/
38
37
 
39
38
  async function sendMail(ctx, tenantId) {
40
39
  const foundation = requireRendererFoundation(ctx, "sendMail");
41
- const renderer = foundation.createRendererForTenant({ tenantId, kind: "mail-html" });
40
+ const renderer = foundation.createRendererForTenant({ tenantId, kind: "mail" });
42
41
  const result = await renderer.render(
43
42
  {
44
- kind: "mail-html",
45
- payload: { content: "Hello {{name}}", contentFormat: "markdown", variables: { name: "Frau Schmidt" } },
43
+ kind: "mail",
44
+ payload: { slug: "billing:notify:invoice-ready", locale: "de", variables: { name: "Frau Schmidt" } },
46
45
  },
47
- { db: ctx.db, registry: ctx.registry, tenantId },
46
+ { db: ctx.db, registry: ctx.registry, tenantId, config: ctx.config },
48
47
  );
49
- // result.html, result.text
48
+ // result.subject, result.html, result.text, result.previewText
50
49
  }
51
50
  ```
52
51
 
53
- Plugins ohne Service-Deps (`renderer-simple`) ignorieren den zweiten `ctx`-Parameter.
52
+ Plugins ohne Service-Deps ignorieren den zweiten `ctx`-Parameter.
53
+
54
+ Mails einer `r.notification` rendert die Delivery selbst: `template` + `meta` an der Notification werden beim Boot gesät (`seedNotificationTemplates`) und über kind `"mail"` gerendert. Ohne `template` + `meta` scheitert der Versand über den Email-Channel.
54
55
 
55
56
  ## Plugin-Auswahl-Reihenfolge
56
57
 
57
- 1. Tenant-Override (Config-Key `rendererPluginByKind`, z.B. `{ "mail-html": "mail-html" }`)
58
+ 1. Tenant-Override (Config-Key `rendererPluginByKind`, z.B. `{ "mail": "mail-html" }`)
58
59
  2. `DEFAULT_PLUGIN_BY_KIND` aus constants
59
60
  3. Erstes Plugin im Pool das das kind bedient
60
61
  4. `RendererError("no_plugin_for_kind")` wenn nichts passt
@@ -10,21 +10,21 @@ import { RendererError, type RendererPlugin } from "./types.js";
10
10
 
11
11
  export type RendererFoundationApi = {
12
12
  /**
13
- * Wählt + returnt ein RendererPlugin für (tenantId × kind). Caller
14
- * rufen anschließend `plugin.render(req, ctx)`.
13
+ * Selects + returns a RendererPlugin for (tenantId × kind). Callers
14
+ * then invoke `plugin.render(req, ctx)`.
15
15
  *
16
- * **Auswahl-Reihenfolge:**
17
- * 1. Tenant-spezifische Config (config-Bundle: `rendererPluginByKind`)
18
- * 2. DEFAULT_PLUGIN_BY_KIND aus constants
19
- * 3. Erstes Plugin im Pool das das kind bedient
20
- * 4. `RendererError("no_plugin_for_kind")` wenn nichts passt
16
+ * **Selection order:**
17
+ * 1. Tenant-specific config (config bundle: `rendererPluginByKind`)
18
+ * 2. DEFAULT_PLUGIN_BY_KIND from constants
19
+ * 3. First plugin in the pool that serves the kind
20
+ * 4. `RendererError("no_plugin_for_kind")` if nothing matches
21
21
  *
22
- * **Caller-Pattern:** `tenantId` MUSS vom Server kommen (typisch
23
- * `ctx.user.tenantId`). Plugins, die Service-Access brauchen (z.B.
24
- * `renderer-mail-html` für Layout-Resolve via `template-resolver`),
25
- * erhalten `RendererContext` als zweites Argument zu `render()` —
26
- * matcht das Pattern von `DeliveryChannel.send(addr, msg, ctx)`.
27
- * Plugins ohne Service-Deps (z.B. `renderer-simple`) ignorieren ctx.
22
+ * **Caller pattern:** `tenantId` MUST come from the server (typically
23
+ * `ctx.user.tenantId`). Plugins that need service access (e.g.
24
+ * `@cosmicdrift/kumiko-renderer-mail-html` for layout resolution via `template-resolver`)
25
+ * receive `RendererContext` as the second argument to `render()` —
26
+ * matches the pattern of `DeliveryChannel.send(addr, msg, ctx)`.
27
+ * Plugins without service deps ignore ctx.
28
28
  */
29
29
  readonly createRendererForTenant: (args: {
30
30
  readonly tenantId: TenantId;
@@ -66,7 +66,7 @@ export function createRendererFoundationApi(
66
66
 
67
67
  // 4. Kein Plugin → Error
68
68
  throw new RendererError(
69
- `[renderer-foundation] no plugin registered for kind="${kind}". Mount at least one plugin (renderer-simple, renderer-mail-html, renderer-puppeteer-client) for this kind.`,
69
+ `[renderer-foundation] no plugin registered for kind="${kind}". Mount at least one plugin (renderer-mail-html, renderer-puppeteer-client) for this kind.`,
70
70
  "no_plugin_for_kind",
71
71
  );
72
72
  },