@cosmicdrift/kumiko-bundled-features 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 (307) hide show
  1. package/dist/admin-shell/feature.d.ts +3 -0
  2. package/dist/admin-shell/feature.js +6 -1
  3. package/dist/agent-tools/agent-manifest.js +2 -1
  4. package/dist/agent-tools/filter-ops.d.ts +1 -1
  5. package/dist/agent-tools/filter-ops.js +1 -3
  6. package/dist/agent-tools/tool-dispatch.js +2 -1
  7. package/dist/auth-email-password/email-templates.d.ts +4 -26
  8. package/dist/auth-email-password/email-templates.js +9 -101
  9. package/dist/auth-email-password/feature.js +24 -6
  10. package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
  11. package/dist/auth-email-password/handlers/request-account-unlock.write.js +2 -4
  12. package/dist/auth-email-password/handlers/request-email-verification.write.js +2 -4
  13. package/dist/auth-email-password/handlers/request-password-reset.write.js +2 -4
  14. package/dist/auth-email-password/handlers/signup-confirm.write.js +3 -4
  15. package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -2
  16. package/dist/auth-email-password/handlers/token-request-handler.js +0 -1
  17. package/dist/auth-email-password/index.d.ts +2 -2
  18. package/dist/auth-email-password/index.js +1 -1
  19. package/dist/auth-email-password/magic-link-mail.d.ts +3 -2
  20. package/dist/auth-email-password/magic-link-mail.js +19 -14
  21. package/dist/auth-email-password/mail-notifications.d.ts +32 -0
  22. package/dist/auth-email-password/mail-notifications.js +116 -0
  23. package/dist/auth-email-password/signup-activation.d.ts +1 -1
  24. package/dist/auth-email-password/signup-activation.js +2 -3
  25. package/dist/auth-foundation/types.js +2 -8
  26. package/dist/auth-mfa/feature.js +4 -4
  27. package/dist/auth-mfa/hmac-token-codec.js +4 -4
  28. package/dist/auth-mfa/mfa-challenge-token.js +3 -3
  29. package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -3
  30. package/dist/auth-mfa/mfa-setup-token.js +8 -8
  31. package/dist/billing-foundation/constants.d.ts +3 -3
  32. package/dist/billing-foundation/constants.js +5 -4
  33. package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +32 -14
  34. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +27 -32
  35. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +3 -3
  36. package/dist/billing-foundation/consumer-protection/mail-notifications.d.ts +8 -0
  37. package/dist/billing-foundation/consumer-protection/mail-notifications.js +95 -0
  38. package/dist/billing-foundation/consumer-protection/register-consent.js +2 -0
  39. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +20 -10
  40. package/dist/billing-foundation/consumer-protection/termination-mail.js +10 -21
  41. package/dist/billing-foundation/consumer-protection/termination-notify.js +2 -2
  42. package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -3
  43. package/dist/billing-foundation/consumer-protection/termination-request.js +2 -2
  44. package/dist/billing-foundation/handlers/process-event.write.js +2 -2
  45. package/dist/billing-foundation/handlers/process-payment-event.write.js +1 -1
  46. package/dist/billing-foundation/subscription-tier-sync.js +3 -4
  47. package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -1
  48. package/dist/cap-overview/web/cap-usage-cell.js +5 -4
  49. package/dist/channel-email/email-channel.d.ts +2 -2
  50. package/dist/channel-email/email-channel.js +89 -26
  51. package/dist/channel-email/feature.js +3 -3
  52. package/dist/channel-email/index.d.ts +1 -1
  53. package/dist/channel-email/index.js +1 -1
  54. package/dist/channel-email/pii-guard.js +10 -0
  55. package/dist/channel-email/smtp-transport.js +13 -0
  56. package/dist/channel-email/types.d.ts +8 -0
  57. package/dist/config/handlers/set.write.js +1 -2
  58. package/dist/custom-fields/lib/value-schema.js +10 -11
  59. package/dist/custom-fields/wire-user-data-rights.js +7 -6
  60. package/dist/data-retention/index.d.ts +1 -0
  61. package/dist/data-retention/index.js +1 -0
  62. package/dist/data-retention/run-retention-cleanup.js +6 -6
  63. package/dist/delivery/attachment-ref.d.ts +39 -0
  64. package/dist/delivery/attachment-ref.js +64 -0
  65. package/dist/delivery/channel-context.d.ts +8 -4
  66. package/dist/delivery/channel-context.js +76 -2
  67. package/dist/delivery/constants.d.ts +1 -0
  68. package/dist/delivery/constants.js +1 -0
  69. package/dist/delivery/delivery-service.d.ts +4 -1
  70. package/dist/delivery/delivery-service.js +38 -32
  71. package/dist/delivery/feature.js +1 -1
  72. package/dist/delivery/index.d.ts +5 -2
  73. package/dist/delivery/index.js +4 -1
  74. package/dist/delivery/jobs.js +25 -5
  75. package/dist/delivery/notify-factory.js +4 -1
  76. package/dist/delivery/testing.d.ts +4 -1
  77. package/dist/delivery/types.d.ts +20 -14
  78. package/dist/delivery/unsubscribe.js +4 -1
  79. package/dist/derivatives-sharp/render.js +1 -18
  80. package/dist/document-ingest-foundation/pages.js +4 -4
  81. package/dist/feature-toggles/toggle-sync-signal.js +4 -4
  82. package/dist/file-foundation/index.d.ts +1 -0
  83. package/dist/file-foundation/index.js +1 -0
  84. package/dist/file-foundation/read-file-ref-content.d.ts +27 -0
  85. package/dist/file-foundation/read-file-ref-content.js +49 -0
  86. package/dist/folders/web/folder-manager.js +3 -2
  87. package/dist/form-draft/db/queries/cleanup.js +4 -4
  88. package/dist/form-draft/db/queries/draft-count.js +2 -2
  89. package/dist/form-draft/db/queries/owned-file-refs.js +2 -2
  90. package/dist/form-draft/release-file-refs.js +3 -2
  91. package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
  92. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +6 -6
  93. package/dist/inbound-mail-foundation/handlers/scope-visibility.d.ts +1 -1
  94. package/dist/inbound-mail-foundation/oauth-state.js +7 -7
  95. package/dist/inbound-mail-foundation/types.js +4 -4
  96. package/dist/inbound-mail-foundation/watch-supervisor.js +22 -13
  97. package/dist/inbound-provider-imap/imap-client.js +4 -3
  98. package/dist/ledger/recurring.js +1 -3
  99. package/dist/managed-pages/branding.d.ts +2 -0
  100. package/dist/managed-pages/branding.js +24 -2
  101. package/dist/managed-pages/feature.js +1 -0
  102. package/dist/managed-pages/i18n.js +1 -0
  103. package/dist/managed-pages/screens/branding-screen.js +8 -1
  104. package/dist/page-render/branding.d.ts +1 -0
  105. package/dist/page-render/branding.js +1 -0
  106. package/dist/page-render/markdown.js +1 -1
  107. package/dist/personal-access-tokens/feature.js +1 -1
  108. package/dist/personal-access-tokens/handlers/list.query.js +2 -1
  109. package/dist/personal-access-tokens/revoke-for-user.js +2 -1
  110. package/dist/renderer-foundation/api.d.ts +13 -13
  111. package/dist/renderer-foundation/api.js +1 -1
  112. package/dist/renderer-foundation/constants.js +2 -4
  113. package/dist/renderer-foundation/feature.js +7 -7
  114. package/dist/renderer-foundation/index.d.ts +2 -1
  115. package/dist/renderer-foundation/index.js +1 -0
  116. package/dist/renderer-foundation/types.d.ts +92 -32
  117. package/dist/renderer-pdf/feature.js +7 -1
  118. package/dist/sessions/db/queries/cleanup.js +2 -2
  119. package/dist/sessions/feature.js +1 -1
  120. package/dist/sessions/handlers/list.query.js +2 -1
  121. package/dist/shared/attachment-file-name.d.ts +3 -0
  122. package/dist/shared/attachment-file-name.js +35 -0
  123. package/dist/shared/index.d.ts +3 -0
  124. package/dist/shared/index.js +3 -0
  125. package/dist/shared/is-tenant-db.js +1 -1
  126. package/dist/shared/mail-template-body.d.ts +4 -0
  127. package/dist/shared/mail-template-body.js +17 -0
  128. package/dist/shared/sharp-input-limits.d.ts +6 -0
  129. package/dist/shared/sharp-input-limits.js +22 -0
  130. package/dist/shared/signup-handover.js +2 -2
  131. package/dist/tags/handlers/assign-tag.write.js +3 -1
  132. package/dist/tags/web/tag-chip.js +2 -0
  133. package/dist/template-resolver/api.d.ts +30 -2
  134. package/dist/template-resolver/api.js +40 -5
  135. package/dist/template-resolver/compile-document-body.d.ts +8 -0
  136. package/dist/template-resolver/compile-document-body.js +163 -0
  137. package/dist/template-resolver/constants.d.ts +13 -4
  138. package/dist/template-resolver/constants.js +30 -10
  139. package/dist/template-resolver/document-sanitizer.d.ts +32 -0
  140. package/dist/template-resolver/document-sanitizer.js +201 -0
  141. package/dist/template-resolver/document-schema.d.ts +21 -0
  142. package/dist/template-resolver/document-schema.js +296 -0
  143. package/dist/template-resolver/feature.d.ts +18 -0
  144. package/dist/template-resolver/feature.js +50 -6
  145. package/dist/template-resolver/handlers/bundle-import.d.ts +20 -0
  146. package/dist/template-resolver/handlers/bundle-import.js +147 -0
  147. package/dist/template-resolver/handlers/collection-set.write.js +25 -2
  148. package/dist/template-resolver/handlers/copy-locale.write.d.ts +19 -0
  149. package/dist/template-resolver/handlers/copy-locale.write.js +116 -0
  150. package/dist/template-resolver/handlers/diff-to-system.query.d.ts +22 -0
  151. package/dist/template-resolver/handlers/diff-to-system.query.js +64 -0
  152. package/dist/template-resolver/handlers/export-bundle.query.d.ts +35 -0
  153. package/dist/template-resolver/handlers/export-bundle.query.js +68 -0
  154. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
  155. package/dist/template-resolver/handlers/find-by-id.query.js +3 -3
  156. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +39 -0
  157. package/dist/template-resolver/handlers/import-bundle-system.write.js +36 -0
  158. package/dist/template-resolver/handlers/import-bundle.write.d.ts +40 -0
  159. package/dist/template-resolver/handlers/import-bundle.write.js +19 -0
  160. package/dist/template-resolver/handlers/layout-source.query.d.ts +26 -0
  161. package/dist/template-resolver/handlers/layout-source.query.js +64 -0
  162. package/dist/template-resolver/handlers/list.query.d.ts +4 -1
  163. package/dist/template-resolver/handlers/list.query.js +5 -7
  164. package/dist/template-resolver/handlers/overview.query.d.ts +59 -0
  165. package/dist/template-resolver/handlers/overview.query.js +165 -0
  166. package/dist/template-resolver/handlers/preview.query.d.ts +12 -0
  167. package/dist/template-resolver/handlers/preview.query.js +24 -0
  168. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +8 -0
  169. package/dist/template-resolver/handlers/reset-to-system.write.js +50 -0
  170. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +8 -0
  171. package/dist/template-resolver/handlers/resource-link-pdf.write.js +32 -0
  172. package/dist/template-resolver/handlers/resource-remove.write.d.ts +9 -0
  173. package/dist/template-resolver/handlers/resource-remove.write.js +44 -0
  174. package/dist/template-resolver/handlers/resource-shared.d.ts +9 -0
  175. package/dist/template-resolver/handlers/resource-shared.js +14 -0
  176. package/dist/template-resolver/handlers/resource-upload.write.d.ts +13 -0
  177. package/dist/template-resolver/handlers/resource-upload.write.js +98 -0
  178. package/dist/template-resolver/handlers/send-test.write.d.ts +11 -0
  179. package/dist/template-resolver/handlers/send-test.write.js +81 -0
  180. package/dist/template-resolver/handlers/set.write.d.ts +4 -1
  181. package/dist/template-resolver/handlers/set.write.js +25 -3
  182. package/dist/template-resolver/handlers/shared.d.ts +9 -5
  183. package/dist/template-resolver/handlers/shared.js +6 -2
  184. package/dist/template-resolver/handlers/template-gates.d.ts +36 -0
  185. package/dist/template-resolver/handlers/template-gates.js +131 -0
  186. package/dist/template-resolver/handlers/template-write.d.ts +45 -0
  187. package/dist/template-resolver/handlers/template-write.js +109 -0
  188. package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +17 -0
  189. package/dist/template-resolver/handlers/tenant-overrides.query.js +48 -0
  190. package/dist/template-resolver/handlers/toggle-status.write.d.ts +12 -0
  191. package/dist/template-resolver/handlers/toggle-status.write.js +27 -11
  192. package/dist/template-resolver/handlers/upsert-system.write.d.ts +5 -8
  193. package/dist/template-resolver/handlers/upsert-system.write.js +14 -59
  194. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +5 -8
  195. package/dist/template-resolver/handlers/upsert-tenant.write.js +10 -61
  196. package/dist/template-resolver/handlers/usage.query.d.ts +17 -0
  197. package/dist/template-resolver/handlers/usage.query.js +15 -0
  198. package/dist/template-resolver/handlers/validate.query.d.ts +74 -0
  199. package/dist/template-resolver/handlers/validate.query.js +80 -0
  200. package/dist/template-resolver/index.d.ts +18 -1
  201. package/dist/template-resolver/index.js +16 -1
  202. package/dist/template-resolver/layout-regions.d.ts +23 -0
  203. package/dist/template-resolver/layout-regions.js +125 -0
  204. package/dist/template-resolver/linked-resources.d.ts +17 -0
  205. package/dist/template-resolver/linked-resources.js +75 -0
  206. package/dist/template-resolver/preview-display-html.d.ts +7 -0
  207. package/dist/template-resolver/preview-display-html.js +27 -0
  208. package/dist/template-resolver/print-document.d.ts +1 -0
  209. package/dist/template-resolver/print-document.js +55 -0
  210. package/dist/template-resolver/qualified-names.d.ts +30 -0
  211. package/dist/template-resolver/qualified-names.js +30 -0
  212. package/dist/template-resolver/record-mapping.d.ts +2 -0
  213. package/dist/template-resolver/record-mapping.js +21 -0
  214. package/dist/template-resolver/resource-image.d.ts +15 -0
  215. package/dist/template-resolver/resource-image.js +34 -0
  216. package/dist/template-resolver/seeding.d.ts +14 -1
  217. package/dist/template-resolver/seeding.js +105 -15
  218. package/dist/template-resolver/store-template-resource.d.ts +64 -0
  219. package/dist/template-resolver/store-template-resource.js +264 -0
  220. package/dist/template-resolver/table.d.ts +47 -8
  221. package/dist/template-resolver/table.js +11 -0
  222. package/dist/template-resolver/template-bundle.d.ts +36 -0
  223. package/dist/template-resolver/template-bundle.js +60 -0
  224. package/dist/template-resolver/template-diff.d.ts +7 -0
  225. package/dist/template-resolver/template-diff.js +68 -0
  226. package/dist/template-resolver/template-meta.d.ts +60 -0
  227. package/dist/template-resolver/template-meta.js +101 -0
  228. package/dist/template-resolver/template-preview.d.ts +86 -0
  229. package/dist/template-resolver/template-preview.js +374 -0
  230. package/dist/template-resolver/template-resources.d.ts +14 -0
  231. package/dist/template-resolver/template-resources.js +42 -0
  232. package/dist/template-resolver/template-usage.d.ts +12 -0
  233. package/dist/template-resolver/template-usage.js +30 -0
  234. package/dist/template-resolver/template-validation.d.ts +22 -0
  235. package/dist/template-resolver/template-validation.js +172 -0
  236. package/dist/template-resolver/testing.js +5 -5
  237. package/dist/template-resolver/user-content-table.d.ts +14 -8
  238. package/dist/template-resolver/web/client-plugin.d.ts +1 -0
  239. package/dist/template-resolver/web/client-plugin.js +9 -6
  240. package/dist/template-resolver/web/document-editor-extensions.d.ts +15 -0
  241. package/dist/template-resolver/web/document-editor-extensions.js +107 -0
  242. package/dist/template-resolver/web/document-editor.d.ts +3 -0
  243. package/dist/template-resolver/web/document-editor.js +11 -0
  244. package/dist/template-resolver/web/document-tiptap-editor.d.ts +3 -0
  245. package/dist/template-resolver/web/document-tiptap-editor.js +376 -0
  246. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +33 -0
  247. package/dist/template-resolver/web/document-toolbar-blocks.js +15 -0
  248. package/dist/template-resolver/web/editor-handle.d.ts +3 -0
  249. package/dist/template-resolver/web/editor-handle.js +24 -0
  250. package/dist/template-resolver/web/i18n.js +76 -0
  251. package/dist/template-resolver/web/index.d.ts +4 -0
  252. package/dist/template-resolver/web/index.js +4 -0
  253. package/dist/template-resolver/web/placeholder-line-editor-lazy.d.ts +4 -0
  254. package/dist/template-resolver/web/placeholder-line-editor-lazy.js +16 -0
  255. package/dist/template-resolver/web/placeholder-line-editor.d.ts +15 -0
  256. package/dist/template-resolver/web/placeholder-line-editor.js +94 -0
  257. package/dist/template-resolver/web/placeholder-text.d.ts +4 -0
  258. package/dist/template-resolver/web/placeholder-text.js +34 -0
  259. package/dist/template-resolver/web/use-stable-by-key.d.ts +1 -0
  260. package/dist/template-resolver/web/use-stable-by-key.js +13 -0
  261. package/dist/tenant/handlers/team-list.query.js +3 -2
  262. package/dist/tenant/seeding.js +31 -6
  263. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
  264. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +2 -1
  265. package/dist/user/db/queries/stream-tenant-backfill.js +4 -4
  266. package/dist/user/handlers/detail.query.js +2 -1
  267. package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
  268. package/dist/user-data-rights/email-templates.d.ts +1 -29
  269. package/dist/user-data-rights/email-templates.js +5 -106
  270. package/dist/user-data-rights/feature.js +53 -35
  271. package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
  272. package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
  273. package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
  274. package/dist/user-data-rights/lib/default-mailers.d.ts +15 -7
  275. package/dist/user-data-rights/lib/default-mailers.js +100 -48
  276. package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
  277. package/dist/user-data-rights/mail-notifications.d.ts +13 -0
  278. package/dist/user-data-rights/mail-notifications.js +118 -0
  279. package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
  280. package/dist/user-profile/web/profile-screen.js +2 -1
  281. package/dist/waitlist/feature.js +10 -0
  282. package/dist/waitlist/handlers/submit.write.js +3 -4
  283. package/dist/waitlist/mail.d.ts +19 -6
  284. package/dist/waitlist/mail.js +50 -14
  285. package/package.json +28 -17
  286. package/src/admin-shell/changes.json +6 -0
  287. package/src/auth-email-password/changes.json +14 -0
  288. package/src/billing-foundation/changes.json +7 -0
  289. package/src/channel-email/changes.json +19 -0
  290. package/src/data-retention/changes.json +6 -0
  291. package/src/delivery/changes.json +48 -0
  292. package/src/managed-pages/changes.json +6 -0
  293. package/src/renderer-foundation/changes.json +39 -0
  294. package/src/template-resolver/changes.json +118 -0
  295. package/src/user-data-rights/changes.json +7 -0
  296. package/src/waitlist/changes.json +7 -0
  297. package/dist/renderer-simple/feature.d.ts +0 -10
  298. package/dist/renderer-simple/feature.js +0 -42
  299. package/dist/renderer-simple/index.d.ts +0 -3
  300. package/dist/renderer-simple/index.js +0 -2
  301. package/dist/renderer-simple/mail-blocks.d.ts +0 -21
  302. package/dist/renderer-simple/mail-blocks.js +0 -107
  303. package/dist/renderer-simple/resolve-variables.d.ts +0 -7
  304. package/dist/renderer-simple/resolve-variables.js +0 -47
  305. package/dist/renderer-simple/simple-renderer.d.ts +0 -24
  306. package/dist/renderer-simple/simple-renderer.js +0 -200
  307. package/src/renderer-simple/changes.json +0 -27
@@ -23,9 +23,7 @@ export function scheduleReference(scheduleId, period) {
23
23
  // can't land in one call-site and drift from the other.
24
24
  export function findReversedIds(rows) {
25
25
  const ids = new Set(rows.map((r) => r.id));
26
- return new Set(rows
27
- .filter((r) => r.reference !== null && ids.has(r.reference))
28
- .map((r) => r.reference));
26
+ return new Set(rows.flatMap((r) => (r.reference !== null && ids.has(r.reference) ? [r.reference] : [])));
29
27
  }
30
28
  // "YYYY-MM" ⇄ a month index (year*12 + monthOfYear), so window math is pure
31
29
  // integer arithmetic — no Date object (no-date-api guard) and no DST/timezone
@@ -8,6 +8,7 @@ export declare const BRANDING_KEYS: {
8
8
  brandingSiteUrl: ConfigKeyDefinition<"text">;
9
9
  brandingAccentColor: ConfigKeyDefinition<"text">;
10
10
  brandingLogoUrl: ConfigKeyDefinition<"text">;
11
+ brandingLogoFile: ConfigKeyDefinition<"text">;
11
12
  brandingLayoutPreset: ConfigKeyDefinition<"select">;
12
13
  };
13
14
  export declare const CUSTOM_CSS_KEY: {
@@ -19,6 +20,7 @@ export declare const BRANDING_QN: {
19
20
  readonly siteUrl: "managed-pages:config:branding-site-url";
20
21
  readonly accentColor: "managed-pages:config:branding-accent-color";
21
22
  readonly logoUrl: "managed-pages:config:branding-logo-url";
23
+ readonly logoFile: "managed-pages:config:branding-logo-file";
22
24
  readonly layoutPreset: "managed-pages:config:branding-layout-preset";
23
25
  readonly customCss: "managed-pages:config:branding-custom-css";
24
26
  };
@@ -42,6 +42,10 @@ const DESCRIPTION_PATTERN = { regex: "^[\\s\\S]{0,500}$" };
42
42
  // render (page-render/css-sanitize.ts). Keep this cap in sync with MAX_CSS_LENGTH
43
43
  // there.
44
44
  const CUSTOM_CSS_PATTERN = { regex: "^[\\s\\S]{0,8000}$" };
45
+ // Empty or a FileRef id (UUID); the upload UI that fills it ships separately.
46
+ const FILE_REF_ID_PATTERN = {
47
+ 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}$",
48
+ };
45
49
  export const LAYOUT_PRESETS = ["minimal", "centered", "wide"];
46
50
  // Companion toggle feature name. managed-pages reads ctx.hasFeature() against it
47
51
  // in the branding query to per-tenant-gate custom-CSS emission. Declared as a
@@ -80,6 +84,12 @@ export const BRANDING_KEYS = {
80
84
  pattern: HTTPS_PATTERN,
81
85
  write: BRANDING_WRITE,
82
86
  }),
87
+ // Ownership is checked where it is read (renderer-mail-html: FileRef must belong to the rendering tenant).
88
+ brandingLogoFile: createTenantConfig("text", {
89
+ default: "",
90
+ pattern: FILE_REF_ID_PATTERN,
91
+ write: BRANDING_WRITE,
92
+ }),
83
93
  brandingLayoutPreset: createTenantConfig("select", {
84
94
  default: "centered",
85
95
  options: LAYOUT_PRESETS,
@@ -99,6 +109,7 @@ export const BRANDING_QN = {
99
109
  siteUrl: "managed-pages:config:branding-site-url",
100
110
  accentColor: "managed-pages:config:branding-accent-color",
101
111
  logoUrl: "managed-pages:config:branding-logo-url",
112
+ logoFile: "managed-pages:config:branding-logo-file",
102
113
  layoutPreset: "managed-pages:config:branding-layout-preset",
103
114
  customCss: "managed-pages:config:branding-custom-css",
104
115
  };
@@ -114,17 +125,27 @@ async function readText(config, qualifiedKey) {
114
125
  export async function readBranding(config) {
115
126
  if (!config)
116
127
  return EMPTY_BRANDING;
117
- const [title, description, siteUrl, accentColor, logoUrl, layoutPreset] = await Promise.all([
128
+ const [title, description, siteUrl, accentColor, logoUrl, logoFile, layoutPreset] = await Promise.all([
118
129
  readText(config, BRANDING_QN.title),
119
130
  readText(config, BRANDING_QN.description),
120
131
  readText(config, BRANDING_QN.siteUrl),
121
132
  readText(config, BRANDING_QN.accentColor),
122
133
  readText(config, BRANDING_QN.logoUrl),
134
+ readText(config, BRANDING_QN.logoFile),
123
135
  readText(config, BRANDING_QN.layoutPreset),
124
136
  ]);
125
137
  // customCss stays "" here — it is gated (allowCustomCss + per-tenant toggle)
126
138
  // and read separately by the branding query via readCustomCss.
127
- return { title, description, siteUrl, accentColor, logoUrl, layoutPreset, customCss: "" };
139
+ return {
140
+ title,
141
+ description,
142
+ siteUrl,
143
+ accentColor,
144
+ logoUrl,
145
+ logoFile,
146
+ layoutPreset,
147
+ customCss: "",
148
+ };
128
149
  }
129
150
  // Read the raw, untrusted custom CSS for the request's tenant. The caller (the
130
151
  // branding query) only invokes this once the gate has passed; the value is
@@ -150,6 +171,7 @@ export function coerceBranding(value) {
150
171
  siteUrl: stringField(source, "siteUrl"),
151
172
  accentColor: stringField(source, "accentColor"),
152
173
  logoUrl: stringField(source, "logoUrl"),
174
+ logoFile: stringField(source, "logoFile"),
153
175
  layoutPreset: stringField(source, "layoutPreset"),
154
176
  customCss: stringField(source, "customCss"),
155
177
  };
@@ -26,6 +26,7 @@ function brandingRevisionSeed(branding) {
26
26
  branding.siteUrl,
27
27
  branding.accentColor,
28
28
  branding.logoUrl,
29
+ branding.logoFile,
29
30
  branding.layoutPreset,
30
31
  branding.customCss,
31
32
  ]);
@@ -37,6 +37,7 @@ export const MANAGED_PAGES_I18N = {
37
37
  en: "Accent color",
38
38
  },
39
39
  "managed-pages:entity:__config-edit__:field:logoUrl": { en: "Logo URL" },
40
+ "managed-pages:entity:__config-edit__:field:logoFile": { en: "Logo file" },
40
41
  "managed-pages:entity:__config-edit__:field:layoutPreset": { en: "Layout" },
41
42
  // Section-Header
42
43
  "managed-pages:section.meta": { en: "Metadata" },
@@ -1,4 +1,4 @@
1
- import { createSelectField, createTextField, } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { createImageField, createSelectField, createTextField, } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { BRANDING_QN, LAYOUT_PRESETS } from "../branding.js";
3
3
  // Tenant self-service branding editor. A `configEdit` screen (no entity
4
4
  // table) — the renderer loads config:query:values, maps the form fields via
@@ -27,6 +27,7 @@ export function createBrandingSettingsScreen(opts) {
27
27
  siteUrl: BRANDING_QN.siteUrl,
28
28
  accentColor: BRANDING_QN.accentColor,
29
29
  logoUrl: BRANDING_QN.logoUrl,
30
+ logoFile: BRANDING_QN.logoFile,
30
31
  layoutPreset: BRANDING_QN.layoutPreset,
31
32
  },
32
33
  fields: {
@@ -54,6 +55,11 @@ export function createBrandingSettingsScreen(opts) {
54
55
  personal: false,
55
56
  reason: "is_business_data",
56
57
  }),
58
+ logoFile: createImageField({
59
+ accept: ["image/png", "image/jpeg", "image/webp"],
60
+ personal: false,
61
+ reason: "is_business_data",
62
+ }),
57
63
  layoutPreset: createSelectField({ options: LAYOUT_PRESETS }),
58
64
  },
59
65
  layout: {
@@ -67,6 +73,7 @@ export function createBrandingSettingsScreen(opts) {
67
73
  { field: "description", span: 2 },
68
74
  { field: "siteUrl", span: 1 },
69
75
  { field: "logoUrl", span: 1 },
76
+ { field: "logoFile", span: 1 },
70
77
  { field: "accentColor", span: 1 },
71
78
  ],
72
79
  },
@@ -4,6 +4,7 @@ export type BrandingTokens = {
4
4
  readonly siteUrl: string;
5
5
  readonly accentColor: string;
6
6
  readonly logoUrl: string;
7
+ readonly logoFile: string;
7
8
  readonly layoutPreset: string;
8
9
  readonly customCss: string;
9
10
  };
@@ -5,6 +5,7 @@ export const EMPTY_BRANDING = {
5
5
  siteUrl: "",
6
6
  accentColor: "",
7
7
  logoUrl: "",
8
+ logoFile: "",
8
9
  layoutPreset: "",
9
10
  customCss: "",
10
11
  };
@@ -23,7 +23,7 @@ safeRenderer.use({
23
23
  },
24
24
  });
25
25
  export function renderSafeMarkdown(markdown) {
26
- // @cast-boundary marked.parse return-type ist `string | Promise<string>`;
26
+ // @cast-boundary third-party-type-gap marked.parse return-type ist `string | Promise<string>`;
27
27
  // `{ async: false }` garantiert sync (string) — Cast nur API-Vertragsfix.
28
28
  return safeRenderer.parse(markdown, { async: false });
29
29
  }
@@ -27,7 +27,7 @@ export function bindPatAutoRevokeOnPasswordChangeFromFeature(feature) {
27
27
  if (exports && typeof exports === "object" && "bindAutoRevokeOnPasswordChange" in exports) {
28
28
  const { bindAutoRevokeOnPasswordChange } = exports;
29
29
  if (typeof bindAutoRevokeOnPasswordChange === "function") {
30
- // @cast-boundary exports-walk — feature.exports is untyped by design
30
+ // @cast-boundary feature-exports — feature.exports is untyped by design
31
31
  return bindAutoRevokeOnPasswordChange;
32
32
  }
33
33
  }
@@ -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";
@@ -10,7 +11,7 @@ import { apiTokenTable } from "../schema/api-token.js";
10
11
  // client sorting by tokenHash or any other non-exposed column.
11
12
  const SORTABLE_COLUMNS = ["id", "name", "createdAt", "expiresAt", "revokedAt"];
12
13
  function isSortableColumn(value) {
13
- return SORTABLE_COLUMNS.includes(value);
14
+ return isOneOf(SORTABLE_COLUMNS, value);
14
15
  }
15
16
  function tokenStatus(row) {
16
17
  if (row.revokedAt !== null)
@@ -3,11 +3,12 @@ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { append } from "@cosmicdrift/kumiko-framework/event-store";
4
4
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
5
5
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
6
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
6
7
  import { PAT_REVOKED_AGGREGATE_TYPE, PAT_REVOKED_EVENT_QN, patRevokedSchema, } from "./pat-revoked-event.js";
7
8
  import { apiTokenTable } from "./schema/api-token.js";
8
9
  // A pool has begin(); a tx/savepoint handle does not and is already atomic.
9
10
  function canBeginTransaction(db) {
10
- return typeof db.begin === "function"; // @cast-boundary driver-handle-probe
11
+ return typeof propertyOf(db, "begin") === "function";
11
12
  }
12
13
  // Cross-tenant revoke: password-change and MFA-enable/disable are account-
13
14
  // level security events, not scoped to one tenant. Mirrors sessions'
@@ -3,21 +3,21 @@ import { type RenderKind } from "./constants.js";
3
3
  import { type RendererPlugin } from "./types.js";
4
4
  export type RendererFoundationApi = {
5
5
  /**
6
- * Wählt + returnt ein RendererPlugin für (tenantId × kind). Caller
7
- * rufen anschließend `plugin.render(req, ctx)`.
6
+ * Selects + returns a RendererPlugin for (tenantId × kind). Callers
7
+ * then invoke `plugin.render(req, ctx)`.
8
8
  *
9
- * **Auswahl-Reihenfolge:**
10
- * 1. Tenant-spezifische Config (config-Bundle: `rendererPluginByKind`)
11
- * 2. DEFAULT_PLUGIN_BY_KIND aus constants
12
- * 3. Erstes Plugin im Pool das das kind bedient
13
- * 4. `RendererError("no_plugin_for_kind")` wenn nichts passt
9
+ * **Selection order:**
10
+ * 1. Tenant-specific config (config bundle: `rendererPluginByKind`)
11
+ * 2. DEFAULT_PLUGIN_BY_KIND from constants
12
+ * 3. First plugin in the pool that serves the kind
13
+ * 4. `RendererError("no_plugin_for_kind")` if nothing matches
14
14
  *
15
- * **Caller-Pattern:** `tenantId` MUSS vom Server kommen (typisch
16
- * `ctx.user.tenantId`). Plugins, die Service-Access brauchen (z.B.
17
- * `renderer-mail-html` für Layout-Resolve via `template-resolver`),
18
- * erhalten `RendererContext` als zweites Argument zu `render()` —
19
- * matcht das Pattern von `DeliveryChannel.send(addr, msg, ctx)`.
20
- * Plugins ohne Service-Deps (z.B. `renderer-simple`) ignorieren ctx.
15
+ * **Caller pattern:** `tenantId` MUST come from the server (typically
16
+ * `ctx.user.tenantId`). Plugins that need service access (e.g.
17
+ * `@cosmicdrift/kumiko-renderer-mail-html` for layout resolution via `template-resolver`)
18
+ * receive `RendererContext` as the second argument to `render()` —
19
+ * matches the pattern of `DeliveryChannel.send(addr, msg, ctx)`.
20
+ * Plugins without service deps ignore ctx.
21
21
  */
22
22
  readonly createRendererForTenant: (args: {
23
23
  readonly tenantId: TenantId;
@@ -34,7 +34,7 @@ export function createRendererFoundationApi(plugins, tenantConfigLookup = () =>
34
34
  if (first)
35
35
  return first;
36
36
  // 4. Kein Plugin → Error
37
- throw new RendererError(`[renderer-foundation] no plugin registered for kind="${kind}". Mount at least one plugin (renderer-simple, renderer-mail-html, renderer-puppeteer-client) for this kind.`, "no_plugin_for_kind");
37
+ throw new RendererError(`[renderer-foundation] no plugin registered for kind="${kind}". Mount at least one plugin (renderer-mail-html, renderer-puppeteer-client) for this kind.`, "no_plugin_for_kind");
38
38
  },
39
39
  };
40
40
  }
@@ -1,12 +1,10 @@
1
- // Extension-point name for renderer plugins (renderer-simple,
2
- // renderer-mail-html, renderer-puppeteer-client, ...).
1
+ // Extension-point name for renderer plugins (@cosmicdrift/kumiko-renderer-mail-html, @cosmicdrift/kumiko-renderer-puppeteer-client, ...).
3
2
  export const RENDERER_EXTENSION = "renderer";
4
3
  export { CONTENT_FORMATS, RENDER_KINDS, } from "../template-resolver/index.js";
5
4
  // Standard-Default-Plugin pro Kind, wenn Tenant keine explizite Config
6
5
  // gesetzt hat. App-Bootstrap kann das via TenantConfigKey überschreiben.
7
6
  export const DEFAULT_PLUGIN_BY_KIND = {
8
- notification: "simple",
9
- "mail-html": "mail-html",
7
+ mail: "mail-html",
10
8
  "document-pdf": "puppeteer",
11
9
  "image-snapshot": "puppeteer",
12
10
  };
@@ -1,17 +1,17 @@
1
1
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { RENDERER_EXTENSION } from "./constants.js";
3
3
  import { isRendererRegistrationPlugin } from "./types.js";
4
- // renderer-foundation — Plugin-Foundation für Renderer (Notification,
5
- // HTML-Mail, PDF, Image). Plan-Doc:
4
+ // renderer-foundation — plugin foundation for renderers (HTML mail, PDF,
5
+ // image). Plan doc:
6
6
  // kumiko-platform/docs/plans/features/renderer-foundation.md
7
7
  //
8
- // Pattern symmetrisch zu ai-foundation: Foundation definiert den
9
- // Extension-Point `renderer`, Plugins (renderer-simple, renderer-mail-html,
10
- // renderer-puppeteer-client) registrieren sich via `r.useExtension`.
11
- // Konsumenten holen sich Plugin runtime via createRendererForTenant.
8
+ // Pattern symmetric to ai-foundation: the foundation defines the
9
+ // extension point `renderer`, plugins (@cosmicdrift/kumiko-renderer-mail-html,
10
+ // @cosmicdrift/kumiko-renderer-puppeteer-client) register via `r.useExtension`.
11
+ // Consumers get a plugin at runtime via createRendererForTenant.
12
12
  export function createRendererFoundationFeature() {
13
13
  return defineFeature("renderer-foundation", (r) => {
14
- r.describe('Plugin registry for content rendering (notification HTML, mail HTML, PDF, images): call `foundation.createRendererForTenant({ tenantId, kind })` at render time to get the right renderer plugin selected by kind, with tenant-level overrides via the `rendererPluginByKind` config key. Requires `template-resolver` (declared via `r.requires`). Low-level building block \u2014 add `renderer-simple` (or write a custom plugin via `r.useExtension("renderer", name, { kinds, render })`) rather than using this feature alone.');
14
+ r.describe('Plugin registry for content rendering (mail HTML, PDF, images): call `foundation.createRendererForTenant({ tenantId, kind })` at render time to get the right renderer plugin selected by kind, with tenant-level overrides via the `rendererPluginByKind` config key. Requires `template-resolver` (declared via `r.requires`). Low-level building block \u2014 add `renderer-mail-html` (or write a custom plugin via `r.useExtension("renderer", name, { kinds, render })`) rather than using this feature alone.');
15
15
  r.uiHints({
16
16
  displayLabel: "Renderer Foundation",
17
17
  category: "notifications",
@@ -1,4 +1,5 @@
1
+ export { attachmentFileNameSchema } from "../shared/index.js";
1
2
  export { createRendererFoundationApi, type RendererFoundationApi, requireRendererFoundation, } from "./api.js";
2
3
  export { CONTENT_FORMATS, type ContentFormat, DEFAULT_PLUGIN_BY_KIND, RENDER_KINDS, RENDERER_EXTENSION, type RenderKind, } from "./constants.js";
3
4
  export { collectRendererPlugins, createRendererFoundationFeature } from "./feature.js";
4
- export { type DocumentPayload, type ImageOptions, isRendererRegistrationPlugin, type MailHtmlPayload, type NotificationPayload, type PdfOptions, type RendererContext, RendererError, type RendererPlugin, type RendererRegistrationPlugin, type RenderRequest, type RenderResponse, } from "./types.js";
5
+ export { type DocumentPayload, type DocumentPdfDraft, type DocumentPdfPayload, type DocumentPreview, type DocumentPreviewRequest, type FileAttachmentRef, type ImageOptions, type InlineAttachmentRef, isRendererRegistrationPlugin, type MailAttachmentRef, type MailDraft, type MailPayload, type PdfOptions, type RenderedPdfAttachmentRef, type RenderedTemplateRef, type RendererContext, RendererError, type RendererPlugin, type RendererRegistrationPlugin, type RenderProvenance, type RenderRequest, type RenderResponse, } from "./types.js";
@@ -1,3 +1,4 @@
1
+ export { attachmentFileNameSchema } from "../shared/index.js";
1
2
  export { createRendererFoundationApi, requireRendererFoundation, } from "./api.js";
2
3
  export { CONTENT_FORMATS, DEFAULT_PLUGIN_BY_KIND, RENDER_KINDS, RENDERER_EXTENSION, } from "./constants.js";
3
4
  export { collectRendererPlugins, createRendererFoundationFeature } from "./feature.js";
@@ -1,55 +1,80 @@
1
- import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
- import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
1
+ import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { ConfigAccessor, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
+ import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
4
+ import type { LinkedResourceEntry, MailTemplateMeta, PdfPageFormat, PdfTemplateMeta, TemplateScope } from "../template-resolver/index.js";
3
5
  import { type ContentFormat, RENDERER_EXTENSION, type RenderKind } from "./constants.js";
4
6
  export type RenderRequest = {
5
- kind: "notification";
6
- payload: NotificationPayload;
7
- } | {
8
- kind: "mail-html";
9
- payload: MailHtmlPayload;
7
+ kind: "mail";
8
+ payload: MailPayload;
10
9
  } | {
11
10
  kind: "document-pdf";
12
- payload: DocumentPayload;
11
+ payload: DocumentPdfPayload;
13
12
  options?: PdfOptions;
14
13
  } | {
15
14
  kind: "image-snapshot";
16
15
  payload: DocumentPayload;
17
16
  options?: ImageOptions;
18
17
  };
19
- export type RenderResponse = {
20
- kind: "notification";
21
- html: string;
22
- text?: string;
23
- } | {
24
- kind: "mail-html";
18
+ export type RenderedTemplateRef = {
19
+ readonly templateId: string;
20
+ readonly templateVersion: number;
21
+ readonly scope: TemplateScope;
22
+ };
23
+ export type RenderProvenance = {
24
+ readonly template?: RenderedTemplateRef;
25
+ readonly layout?: RenderedTemplateRef;
26
+ };
27
+ export type InlineAttachmentRef = {
28
+ readonly kind: "inline";
29
+ readonly cid: string;
30
+ readonly fileRefId: string;
31
+ };
32
+ export type FileAttachmentRef = {
33
+ readonly kind: "file";
34
+ readonly fileRefId: string;
35
+ readonly fileName: string;
36
+ };
37
+ export type RenderedPdfAttachmentRef = {
38
+ readonly kind: "rendered-pdf";
39
+ readonly templateSlug: string;
40
+ readonly variables: Readonly<Record<string, unknown>>;
41
+ readonly fileName: string;
42
+ readonly locale?: string;
43
+ };
44
+ export type MailAttachmentRef = InlineAttachmentRef | FileAttachmentRef | RenderedPdfAttachmentRef;
45
+ export type RenderResponse = ({
46
+ kind: "mail";
47
+ subject: string;
25
48
  html: string;
26
49
  text: string;
50
+ previewText?: string;
51
+ attachments?: readonly MailAttachmentRef[];
27
52
  } | {
28
53
  kind: "document-pdf";
29
54
  pdfBytes: Uint8Array;
30
55
  pageCount: number;
31
56
  sizeBytes: number;
57
+ fileName: string;
32
58
  } | {
33
59
  kind: "image-snapshot";
34
60
  imageBytes: Uint8Array;
35
61
  format: "png" | "jpg";
36
62
  width: number;
37
63
  height: number;
64
+ }) & RenderProvenance;
65
+ export type MailDraft = {
66
+ readonly content: string;
67
+ readonly meta: MailTemplateMeta;
68
+ readonly variableSchema?: Readonly<Record<string, unknown>>;
69
+ /** Images and attachments of the row being edited; absent means those of the stored template. */
70
+ readonly linkedResources?: Readonly<Record<string, LinkedResourceEntry>>;
38
71
  };
39
- export type NotificationPayload = {
40
- readonly template?: string;
41
- readonly content?: string;
42
- readonly contentFormat?: ContentFormat;
43
- readonly variables?: Readonly<Record<string, unknown>>;
44
- readonly locale?: string;
45
- };
46
- export type MailHtmlPayload = {
47
- readonly template?: string;
48
- readonly content?: string;
49
- readonly contentFormat?: ContentFormat;
50
- readonly variables?: Readonly<Record<string, unknown>>;
51
- readonly locale?: string;
52
- readonly subject?: string;
72
+ export type MailPayload = {
73
+ readonly slug: string;
74
+ readonly locale: string;
75
+ readonly variables: Readonly<Record<string, unknown>>;
76
+ readonly unsubscribeUrl?: string;
77
+ readonly draft?: MailDraft;
53
78
  };
54
79
  export type DocumentPayload = {
55
80
  readonly template?: string;
@@ -58,8 +83,35 @@ export type DocumentPayload = {
58
83
  readonly variables?: Readonly<Record<string, unknown>>;
59
84
  readonly locale?: string;
60
85
  };
86
+ export type DocumentPdfPayload = {
87
+ readonly slug: string;
88
+ readonly locale: string;
89
+ readonly variables: Readonly<Record<string, unknown>>;
90
+ };
91
+ export type DocumentPdfDraft = {
92
+ readonly content: string;
93
+ readonly meta: PdfTemplateMeta | null;
94
+ readonly variableSchema?: Readonly<Record<string, unknown>>;
95
+ };
96
+ export type DocumentPreviewRequest = DocumentPdfPayload & {
97
+ readonly draft?: DocumentPdfDraft;
98
+ };
99
+ export type DocumentPreview = {
100
+ readonly title: string;
101
+ readonly fileName: string;
102
+ readonly bodyHtml: string;
103
+ readonly layout: {
104
+ readonly slug: string;
105
+ readonly scope: TemplateScope;
106
+ readonly headerHtml: string;
107
+ readonly signatureHtml: string;
108
+ readonly footerHtml: string;
109
+ } | null;
110
+ /** Image keys the body or the layout asks for that no file is linked to. */
111
+ readonly missingResourceKeys: readonly string[];
112
+ };
61
113
  export type PdfOptions = {
62
- readonly format?: "A4" | "Letter";
114
+ readonly format?: PdfPageFormat;
63
115
  readonly marginMm?: {
64
116
  top?: number;
65
117
  right?: number;
@@ -70,6 +122,7 @@ export type PdfOptions = {
70
122
  readonly footerTemplate?: string;
71
123
  readonly printBackground?: boolean;
72
124
  readonly displayHeaderFooter?: boolean;
125
+ readonly landscape?: boolean;
73
126
  };
74
127
  export type ImageOptions = {
75
128
  readonly width?: number;
@@ -78,18 +131,25 @@ export type ImageOptions = {
78
131
  readonly quality?: number;
79
132
  };
80
133
  export type RendererContext = {
81
- readonly db: DbConnection;
134
+ readonly db: DbRunner | TenantDb;
82
135
  readonly registry: Registry;
83
136
  readonly tenantId: TenantId;
137
+ /** Tenant-scoped config for plugins that read the tenant currency or
138
+ * branding; plugins fall back to request defaults when absent. */
139
+ readonly config?: ConfigAccessor;
140
+ /** Only plugins that embed resource bytes (PDF) need it; without it images are dropped. */
141
+ readonly filesForTenant?: (tenantId: string) => FileContext | undefined;
84
142
  };
85
143
  export type RendererPlugin = {
86
144
  readonly name: string;
87
145
  readonly kinds: ReadonlyArray<RenderKind>;
88
146
  render(req: RenderRequest, ctx: RendererContext): Promise<RenderResponse>;
147
+ /** Optional: plugins that print documents offer a preview that never reaches the print service. */
148
+ previewDocument?(req: DocumentPreviewRequest, ctx: RendererContext): Promise<DocumentPreview>;
89
149
  };
90
150
  export declare class RendererError extends Error {
91
- readonly code: "no_plugin_for_kind" | "invalid_payload" | "other";
92
- constructor(message: string, code: "no_plugin_for_kind" | "invalid_payload" | "other");
151
+ readonly code: "no_plugin_for_kind" | "invalid_payload" | "unavailable" | "other";
152
+ constructor(message: string, code: "no_plugin_for_kind" | "invalid_payload" | "unavailable" | "other");
93
153
  }
94
154
  export type RendererRegistrationPlugin = Omit<RendererPlugin, "name">;
95
155
  export declare function isRendererRegistrationPlugin(o: unknown): o is RendererRegistrationPlugin;
@@ -13,7 +13,13 @@ export async function renderTablePdfRequest(req) {
13
13
  ...(req.options?.format && { format: req.options.format }),
14
14
  ...(req.options?.marginMm && { marginMm: req.options.marginMm }),
15
15
  });
16
- return { kind: "document-pdf", pdfBytes, pageCount, sizeBytes: pdfBytes.byteLength };
16
+ return {
17
+ kind: "document-pdf",
18
+ pdfBytes,
19
+ pageCount,
20
+ sizeBytes: pdfBytes.byteLength,
21
+ fileName: `${req.payload.slug}.pdf`,
22
+ };
17
23
  }
18
24
  export function createRendererPdfFeature() {
19
25
  return defineFeature("renderer-pdf", (r) => {
@@ -1,12 +1,12 @@
1
1
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  export async function deleteStaleSessionsBatch(db, olderThanDays, batchSize) {
3
- const rows = (await asRawClient(db).unsafe(`DELETE FROM "store_user_sessions"
3
+ const rows = await asRawClient(db).unsafe(`DELETE FROM "store_user_sessions"
4
4
  WHERE "id" IN (
5
5
  SELECT "id" FROM "store_user_sessions"
6
6
  WHERE "expires_at" < now() - ($1::int * interval '1 day')
7
7
  OR "revoked_at" < now() - ($1::int * interval '1 day')
8
8
  LIMIT $2
9
9
  )
10
- RETURNING "id"`, [olderThanDays, batchSize]));
10
+ RETURNING "id"`, [olderThanDays, batchSize]);
11
11
  return rows.length;
12
12
  }
@@ -24,7 +24,7 @@ export function bindAutoRevokeFromFeature(feature) {
24
24
  if (exports && typeof exports === "object" && "bindAutoRevokeOnPasswordChange" in exports) {
25
25
  const { bindAutoRevokeOnPasswordChange } = exports;
26
26
  if (typeof bindAutoRevokeOnPasswordChange === "function") {
27
- // @cast-boundary exports-walk — feature.exports is untyped by design
27
+ // @cast-boundary feature-exports — feature.exports is untyped by design
28
28
  return bindAutoRevokeOnPasswordChange;
29
29
  }
30
30
  }
@@ -1,5 +1,6 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { access, 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 { userSessionTable } from "../schema/user-session.js";
@@ -10,7 +11,7 @@ import { userSessionTable } from "../schema/user-session.js";
10
11
  // non-exposed column.
11
12
  const SORTABLE_COLUMNS = ["id", "userId", "createdAt", "expiresAt", "revokedAt"];
12
13
  function isSortableColumn(value) {
13
- return SORTABLE_COLUMNS.includes(value);
14
+ return isOneOf(SORTABLE_COLUMNS, value);
14
15
  }
15
16
  // Admin view of every session in the active tenant. ctx.db (TenantDb)
16
17
  // applies tenant-scoping automatically on selects from tables with a
@@ -0,0 +1,3 @@
1
+ import * as z from "zod";
2
+ export declare const attachmentFileNameSchema: z.ZodString;
3
+ export declare function sanitizePdfAttachmentFileName(raw: string, templateSlug: string): string;
@@ -0,0 +1,35 @@
1
+ import * as z from "zod";
2
+ const MAX_FILE_NAME_LENGTH = 255;
3
+ function hasControlCharacter(value) {
4
+ return value.split("").some((char) => {
5
+ const code = char.charCodeAt(0);
6
+ return code <= 0x1f || code === 0x7f;
7
+ });
8
+ }
9
+ export const attachmentFileNameSchema = z
10
+ .string()
11
+ .min(1)
12
+ .max(MAX_FILE_NAME_LENGTH)
13
+ .refine((name) => !name.includes("/") && !name.includes("\\") && !hasControlCharacter(name), {
14
+ message: "file name must not contain path separators or control characters",
15
+ });
16
+ const PDF_FILE_NAME_MAX_LENGTH = 200;
17
+ const PDF_EXTENSION = ".pdf";
18
+ // Shared by everything that names a rendered PDF, so a mail attachment and a download of the same
19
+ // template get the same name. The cut leaves room for the extension: the attachment schema caps
20
+ // rendered-pdf names, and a name that only fits without ".pdf" would fail it.
21
+ export function sanitizePdfAttachmentFileName(raw, templateSlug) {
22
+ const cleaned = raw
23
+ .split("")
24
+ .filter((char) => !hasControlCharacter(char))
25
+ .join("")
26
+ .replace(/[/\\";]/g, "-")
27
+ .replaceAll("..", "-")
28
+ .trim()
29
+ .slice(0, PDF_FILE_NAME_MAX_LENGTH - PDF_EXTENSION.length)
30
+ .trim();
31
+ const lastSegment = templateSlug.split(":").pop() ?? templateSlug;
32
+ if (cleaned === "")
33
+ return `${lastSegment}${PDF_EXTENSION}`;
34
+ return cleaned.toLowerCase().endsWith(PDF_EXTENSION) ? cleaned : `${cleaned}${PDF_EXTENSION}`;
35
+ }
@@ -1,5 +1,6 @@
1
1
  export { collectErasureFailure, throwIfErasureFailed } from "./assert-erased.js";
2
2
  export { assertNoWhereRuleInOwnershipWrite } from "./assert-no-where-rule-in-ownership-write.js";
3
+ export { attachmentFileNameSchema, sanitizePdfAttachmentFileName, } from "./attachment-file-name.js";
3
4
  export { type ChunkedMigrationOptions, type ChunkedMigrationResult, type ChunkedMigrationStopReason, type MigrationRowOutcome, runChunkedMigration, } from "./chunked-entity-migration.js";
4
5
  export { classifyStoredEnvelope, type StoredEnvelopeClassification, } from "./classify-stored-envelope.js";
5
6
  export { decryptStoredPii } from "./decrypt-stored-pii.js";
@@ -9,6 +10,7 @@ export { isWithinGracePeriod } from "./grace-period.js";
9
10
  export { isIdentityV3Hash, verifyIdentityV3Hash } from "./identity-v3-hash.js";
10
11
  export { isTenantDb } from "./is-tenant-db.js";
11
12
  export { createLockoutCounter, type LockoutCounterState } from "./lockout-counter.js";
13
+ export { mailHeading, mailParagraph, mailRepeatedRows } from "./mail-template-body.js";
12
14
  export { mapWithConcurrency } from "./map-with-concurrency.js";
13
15
  export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
14
16
  export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
@@ -20,6 +22,7 @@ export { type RowBoundGrantResult, redeemRowBoundGrant, signRowBoundGrant, } fro
20
22
  export { runInSubTransaction } from "./run-in-sub-transaction.js";
21
23
  export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
22
24
  export { sessionTimezoneField } from "./session-timezone-field.js";
25
+ export { assertNotSvg, MAX_INPUT_PIXELS, SHARP_INPUT_OPTIONS } from "./sharp-input-limits.js";
23
26
  export { peekTokenSubject, signToken, TokenPurpose, type VerifyResult, verifyToken, } from "./signed-token.js";
24
27
  export { EXT_SIGNUP_HANDOVER, findSignupHandoverProvider, isSignupHandoverProvider, SIGNUP_HANDOVER_BENIGN_CLAIM_REJECTION_CODE, type SignupHandoverBinding, type SignupHandoverProvider, } from "./signup-handover.js";
25
28
  export { createSingleUseTokenStore, hashSingleUseToken } from "./single-use-token-store.js";