@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
@@ -1,13 +1,22 @@
1
1
  import type { ContractTerminationDeclarationType, ContractTerminationKind, ProviderCancelOutcome } from "../events.js";
2
2
  import type { ConsentLocale } from "./consent-text.js";
3
3
  import { type OperatorNoticeReason } from "./termination-texts.js";
4
- export type TerminationMailContent = {
4
+ type TextRow = {
5
+ readonly text: string;
6
+ };
7
+ /** Variables of the termination receipt template. */
8
+ export type TerminationReceiptVariables = {
9
+ readonly subject: string;
10
+ readonly receivedAt: string;
11
+ readonly requestId: string;
12
+ readonly details: readonly TextRow[];
13
+ readonly operatorEmail: string;
14
+ };
15
+ /** Variables of the operator notice template. */
16
+ export type OperatorNoticeVariables = {
5
17
  readonly subject: string;
6
- readonly header: string;
7
- readonly sections: readonly {
8
- readonly text: string;
9
- }[];
10
- readonly footer: string;
18
+ readonly lines: readonly TextRow[];
19
+ readonly operatorEmail: string;
11
20
  };
12
21
  /** What the declarant entered; the only input of the receipt. */
13
22
  export type TerminationDeclaration = {
@@ -21,7 +30,7 @@ export type TerminationDeclaration = {
21
30
  /** The operator notice for a provider problem has no declarant data at all. */
22
31
  export type OperatorNoticeDeclaration = Pick<TerminationDeclaration, "declarationType" | "terminationKind"> & Partial<Omit<TerminationDeclaration, "declarationType" | "terminationKind">>;
23
32
  export declare function formatReceivedAt(iso: string, locale: ConsentLocale): string;
24
- export type RenderTerminationReceiptArgs = {
33
+ export type TerminationReceiptArgs = {
25
34
  readonly locale: ConsentLocale;
26
35
  readonly requestId: string;
27
36
  readonly receivedAtIso: string;
@@ -30,8 +39,8 @@ export type RenderTerminationReceiptArgs = {
30
39
  };
31
40
  /** Built from the declarant's own input only: the content must not depend on
32
41
  * whether (or how) the declaration matched a contract. */
33
- export declare function renderTerminationReceipt(args: RenderTerminationReceiptArgs): TerminationMailContent;
34
- export type RenderOperatorNoticeArgs = {
42
+ export declare function terminationReceiptVariables(args: TerminationReceiptArgs): TerminationReceiptVariables;
43
+ export type OperatorNoticeArgs = {
35
44
  readonly locale: ConsentLocale;
36
45
  readonly requestId: string;
37
46
  readonly receivedAtIso: string;
@@ -42,4 +51,5 @@ export type RenderOperatorNoticeArgs = {
42
51
  readonly providerCancel?: ProviderCancelOutcome;
43
52
  readonly operatorEmail: string;
44
53
  };
45
- export declare function renderOperatorNotice(args: RenderOperatorNoticeArgs): TerminationMailContent;
54
+ export declare function operatorNoticeVariables(args: OperatorNoticeArgs): OperatorNoticeVariables;
55
+ export {};
@@ -34,32 +34,21 @@ function declarationLines(declaration, locale) {
34
34
  }
35
35
  /** Built from the declarant's own input only: the content must not depend on
36
36
  * whether (or how) the declaration matched a contract. */
37
- export function renderTerminationReceipt(args) {
37
+ export function terminationReceiptVariables(args) {
38
38
  const texts = TERMINATION_TEXTS[args.locale];
39
- const subject = texts.receiptSubject[args.declaration.declarationType];
40
39
  return {
41
- subject,
42
- header: subject,
43
- sections: [
44
- { text: texts.receiptGreeting },
45
- { text: texts.receiptIntro },
46
- { text: `${texts.receivedAt}: ${formatReceivedAt(args.receivedAtIso, args.locale)}` },
47
- { text: `${texts.requestId}: ${args.requestId}` },
48
- ...declarationLines(args.declaration, args.locale),
49
- { text: texts.receiptNextSteps },
50
- { text: texts.receiptRevoke(args.operatorEmail) },
51
- ],
52
- footer: `${texts.contact} ${args.operatorEmail}`,
40
+ subject: texts.receiptSubject[args.declaration.declarationType],
41
+ receivedAt: formatReceivedAt(args.receivedAtIso, args.locale),
42
+ requestId: args.requestId,
43
+ details: declarationLines(args.declaration, args.locale),
44
+ operatorEmail: args.operatorEmail,
53
45
  };
54
46
  }
55
- export function renderOperatorNotice(args) {
47
+ export function operatorNoticeVariables(args) {
56
48
  const texts = TERMINATION_TEXTS[args.locale];
57
- const subject = texts.operatorSubject[args.declaration.declarationType];
58
49
  return {
59
- subject,
60
- header: subject,
61
- sections: [
62
- { text: texts.operatorIntro },
50
+ subject: texts.operatorSubject[args.declaration.declarationType],
51
+ lines: [
63
52
  ...args.reasons.map((reason) => ({
64
53
  text: `${texts.noticeReasons}: ${texts.operatorReasons[reason]}`,
65
54
  })),
@@ -72,6 +61,6 @@ export function renderOperatorNotice(args) {
72
61
  : []),
73
62
  ...declarationLines(args.declaration, args.locale),
74
63
  ],
75
- footer: args.operatorEmail,
64
+ operatorEmail: args.operatorEmail,
76
65
  };
77
66
  }
@@ -1,6 +1,6 @@
1
1
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
2
2
  import { CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE } from "../constants.js";
3
- import { renderOperatorNotice } from "./termination-mail.js";
3
+ import { operatorNoticeVariables } from "./termination-mail.js";
4
4
  export function requireNotify(ctx) {
5
5
  if (!ctx.notify) {
6
6
  throw new InternalError({
@@ -12,7 +12,7 @@ export function requireNotify(ctx) {
12
12
  export async function notifyOperator(ctx, consumerProtection, plan) {
13
13
  await requireNotify(ctx)(CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE, {
14
14
  route: { email: consumerProtection.operatorEmail },
15
- data: renderOperatorNotice({
15
+ data: operatorNoticeVariables({
16
16
  ...plan,
17
17
  operatorEmail: consumerProtection.operatorEmail,
18
18
  }),
@@ -2,6 +2,7 @@
2
2
  // no JavaScript. Every echoed value goes through escapeHtml/escapeHtmlAttr.
3
3
  import { getSessionCookies } from "@cosmicdrift/kumiko-framework/api";
4
4
  import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
5
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
5
6
  import { getCookie } from "hono/cookie";
6
7
  import { redirectToCanonicalPath, securePageHeaders, slashVariantOf, wrapInLayout, } from "../../page-render/index.js";
7
8
  import { SubscriptionFoundationHandlers } from "../constants.js";
@@ -54,9 +55,6 @@ function htmlResponse(ctx, title, bodyHtml, status) {
54
55
  alternates: ctx.alternates,
55
56
  }), { status, headers: pageHeaders() });
56
57
  }
57
- function isOneOf(options, value) {
58
- return options.includes(value);
59
- }
60
58
  async function readForm(c) {
61
59
  const body = await c.req.parseBody();
62
60
  const text = (key) => {
@@ -12,7 +12,7 @@ import { CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, SubscriptionCancelTimin
12
12
  import { CONTRACT_TERMINATION_DECLARATION_TYPES, CONTRACT_TERMINATION_KINDS, } from "../events.js";
13
13
  import { purchaseRolesOf } from "../plan-catalog.js";
14
14
  import { resolveConsentLocale } from "./consent-text.js";
15
- import { renderTerminationReceipt } from "./termination-mail.js";
15
+ import { terminationReceiptVariables } from "./termination-mail.js";
16
16
  import { notifyOperator, requireNotify } from "./termination-notify.js";
17
17
  const TENANT_ADMIN_ROLE = "TenantAdmin";
18
18
  const EXTRAORDINARY_REASON_MESSAGE = "reason is required for an extraordinary termination";
@@ -55,7 +55,7 @@ function needsOperatorNotice(declaration) {
55
55
  async function sendTerminationMails(ctx, consumerProtection, plan) {
56
56
  await requireNotify(ctx)(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
57
57
  route: { email: plan.declaration.email },
58
- data: renderTerminationReceipt({
58
+ data: terminationReceiptVariables({
59
59
  locale: plan.locale,
60
60
  requestId: plan.requestId,
61
61
  receivedAtIso: plan.receivedAtIso,
@@ -124,8 +124,8 @@ export async function appendSubscriptionEvent(ctx, tenantId, payload) {
124
124
  // ---------------------------------------------------------------
125
125
  const eventPayload = {
126
126
  providerName: payload.providerName,
127
- providerCustomerId: encryptedFields["providerCustomerId"],
128
- providerSubscriptionId: encryptedFields["providerSubscriptionId"],
127
+ providerCustomerId: encryptedFields.providerCustomerId,
128
+ providerSubscriptionId: encryptedFields.providerSubscriptionId,
129
129
  status: payload.status,
130
130
  tier: payload.tier,
131
131
  currentPeriodEndIso: payload.currentPeriodEndIso,
@@ -70,7 +70,7 @@ export const processPaymentEventHandler = {
70
70
  // ---------------------------------------------------------------
71
71
  const eventPayload = {
72
72
  providerName: payload.providerName,
73
- providerCustomerId: encryptedFields["providerCustomerId"],
73
+ providerCustomerId: encryptedFields.providerCustomerId,
74
74
  priceId: payload.priceId,
75
75
  ...(payload.consentId !== undefined && { consentId: payload.consentId }),
76
76
  };
@@ -3,6 +3,7 @@
3
3
  // (infra#446) — the only per-app variables were the TierName union/default,
4
4
  // so it is now a factory parameter.
5
5
  import { TierAssignmentSources, TierEngineHandlers, TierEngineQueries, tierAssignmentAggregateId, } from "@cosmicdrift/kumiko-bundled-features/tier-engine";
6
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
6
7
  import { subscriptionAggregateId } from "./aggregate-id.js";
7
8
  import { SubscriptionFoundationQueries, SubscriptionStatuses } from "./constants.js";
8
9
  import { createSubscriptionWebhookRoute } from "./webhook-handler.js";
@@ -18,10 +19,8 @@ export function effectiveTierFromSubscription(status, tier, isTierName, defaultT
18
19
  // (handler bug, or the handlerQn resolving to a non-list query) fails
19
20
  // loudly instead of crashing on `.find` against `undefined`.
20
21
  function asRows(result) {
21
- if (typeof result === "object" &&
22
- result !== null &&
23
- Array.isArray(result.rows)) {
24
- return result.rows;
22
+ if (isPlainObject(result) && Array.isArray(result["rows"])) {
23
+ return result["rows"];
25
24
  }
26
25
  throw new Error("expected a { rows: [...] } list-query result");
27
26
  }
@@ -2,6 +2,7 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { buildEntityTable, decodeCursor, encodeCursor } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { definePagedQueryHandler, MAX_LIST_LIMIT, } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import { InternalError, ValidationError } from "@cosmicdrift/kumiko-framework/errors";
5
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
5
6
  import * as z from "zod";
6
7
  import { subscriptionsProjectionTable } from "../../billing-foundation/index.js";
7
8
  import { decryptTenantNames, tenantTable } from "../../tenant/index.js";
@@ -34,7 +35,7 @@ async function resolveLimitsByTenantAndCap(caps, tenants, configFor) {
34
35
  return new Map(entries);
35
36
  }
36
37
  function isSortableField(field) {
37
- return SORTABLE_FIELDS.includes(field);
38
+ return isOneOf(SORTABLE_FIELDS, field);
38
39
  }
39
40
  // tier is a plain string with no defined ordering — lt/gt have no meaning
40
41
  // on it and are rejected rather than silently treated as "no filter".
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
2
3
  import { CapUsageBar } from "./cap-usage-bar.js";
3
4
  function isCapUsage(value) {
4
- if (typeof value !== "object" || value === null)
5
+ if (!isPlainObject(value))
5
6
  return false;
6
- const used = value.used;
7
- const limit = value.limit;
7
+ const used = value["used"];
8
+ const limit = value["limit"];
8
9
  return ((typeof used === "number" || used === null) &&
9
10
  (typeof limit === "number" || limit === null) &&
10
- typeof value.fraction === "number");
11
+ typeof value["fraction"] === "number");
11
12
  }
12
13
  export function CapUsageCell({ value }) {
13
14
  if (!isCapUsage(value))
@@ -1,9 +1,9 @@
1
1
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
- import { type DeliveryChannel, type NotificationRenderer } from "../delivery/index.js";
2
+ import { type DeliveryChannel, type EmailAttachmentRef } from "../delivery/index.js";
3
3
  import type { EmailTransport } from "./types.js";
4
+ export declare function fileAttachmentsFrom(data: Readonly<Record<string, unknown>> | undefined, messageLocale?: string): readonly EmailAttachmentRef[];
4
5
  export type EmailChannelOptions = {
5
6
  readonly transport: EmailTransport;
6
- readonly renderer: NotificationRenderer;
7
7
  readonly resolveEmail: (userId: string, ctx: {
8
8
  db: unknown;
9
9
  tenantId: TenantId;
@@ -1,5 +1,6 @@
1
1
  import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
2
- import { DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE, DELIVERY_UNSUBSCRIBE_PATH, } from "../delivery/index.js";
2
+ import * as z from "zod";
3
+ import { DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE, DELIVERY_UNSUBSCRIBE_PATH, EMAIL_CHANNEL_NAME, fileAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, renderedPdfAttachmentRefSchema, } from "../delivery/index.js";
3
4
  import { decryptStoredPii } from "../shared/index.js";
4
5
  import { guardEmailMessage } from "./pii-guard.js";
5
6
  // Envelope (From / Reply-To / threading headers) rides on the channel data —
@@ -65,31 +66,88 @@ function emailEnvelopeFrom(data) {
65
66
  ...(headers && { headers }),
66
67
  };
67
68
  }
69
+ const fileAttachmentsSchema = z
70
+ .array(z.discriminatedUnion("kind", [fileAttachmentRefSchema, renderedPdfAttachmentRefSchema]))
71
+ .max(MAX_EMAIL_ATTACHMENTS);
72
+ // Only kinds "file" and "rendered-pdf" may come from notify data; inline images are created by the renderer alone.
73
+ // Invalid input throws instead of dropping the attachment silently.
74
+ export function fileAttachmentsFrom(data, messageLocale) {
75
+ const raw = data?.["attachments"];
76
+ if (raw === undefined)
77
+ return [];
78
+ return fileAttachmentsSchema
79
+ .parse(raw)
80
+ .map((ref) => ref.kind === "rendered-pdf" && ref.locale === undefined && messageLocale !== undefined
81
+ ? { ...ref, locale: messageLocale }
82
+ : ref);
83
+ }
84
+ const INLINE_EXTENSION_BY_CONTENT_TYPE = {
85
+ "image/png": "png",
86
+ "image/jpeg": "jpg",
87
+ "image/gif": "gif",
88
+ "image/webp": "webp",
89
+ };
90
+ async function resolveAttachments(sources, ctx) {
91
+ if (sources.length === 0)
92
+ return [];
93
+ if (ctx.readAttachment === undefined) {
94
+ throw new Error("email channel: the message has attachments but the channel context cannot read files");
95
+ }
96
+ const resolved = [];
97
+ for (const { ref, origin } of sources) {
98
+ const found = await ctx.readAttachment(ref, origin);
99
+ if (ref.kind === "rendered-pdf") {
100
+ if (found === null) {
101
+ throw new Error(`email channel: rendering attachment "${ref.fileName}" returned nothing`);
102
+ }
103
+ resolved.push({
104
+ kind: "rendered-pdf",
105
+ templateSlug: ref.templateSlug,
106
+ fileName: ref.fileName,
107
+ content: found.content,
108
+ contentType: found.contentType,
109
+ });
110
+ continue;
111
+ }
112
+ if (found === null) {
113
+ // A missing inline image only degrades the look; a missing file is content the recipient expects.
114
+ if (ref.kind === "inline")
115
+ continue;
116
+ throw new Error(`email channel: attachment file ${ref.fileRefId} was not found`);
117
+ }
118
+ const fileName = ref.kind === "file"
119
+ ? ref.fileName
120
+ : INLINE_EXTENSION_BY_CONTENT_TYPE[found.contentType]
121
+ ? `${ref.cid}.${INLINE_EXTENSION_BY_CONTENT_TYPE[found.contentType]}`
122
+ : ref.cid;
123
+ resolved.push({ ...ref, fileName, content: found.content, contentType: found.contentType });
124
+ }
125
+ return resolved;
126
+ }
68
127
  export function createEmailChannel(options) {
69
- const { transport, renderer, resolveEmail } = options;
70
- // Render is the expensive step (template engine, possibly a remote service)
71
- // and runs in the delivery.render job, decoupled from the SMTP send so each
72
- // can retry independently. Extracted so the inline fallback (no job runner)
128
+ const { transport, resolveEmail } = options;
129
+ // Render runs in the delivery.render job, decoupled from the SMTP send so
130
+ // each can retry independently. Extracted so the inline path (no job runner)
73
131
  // can reuse it without going through the channel's own render() indirection.
74
- async function renderMessage(message) {
75
- // Build renderer input: per-channel template data (if any) or fall back
76
- // to title/body from the message. Renderer handles both cases.
77
- const variables = message.data ?? {
78
- title: message.title,
79
- body: message.body,
80
- };
81
- const rendererInput = {
82
- template: message.notificationType,
83
- variables,
84
- locale: message.locale,
85
- };
86
- const html = await renderer.render(rendererInput);
87
- const text = renderer.renderText ? await renderer.renderText(rendererInput) : undefined;
88
- const subject = variables["subject"] ?? message.title; // @cast-boundary dynamic-key
89
- return { html, subject, ...(text !== undefined && { text }) };
132
+ async function renderMessage(message, ctx) {
133
+ const notificationType = message.notificationType;
134
+ if (ctx.registry.getAllNotifications().get(notificationType)?.template === undefined) {
135
+ throw new Error(`email channel: notification "${notificationType}" has no mail template, declare template + meta on r.notification`);
136
+ }
137
+ if (ctx.renderMail === undefined) {
138
+ throw new Error(`email channel: notification "${notificationType}" has a mail template but the channel context carries no renderMail`);
139
+ }
140
+ const unsubscribeUrl = message.data?.["unsubscribeUrl"];
141
+ const { subject, html, text, attachments } = await ctx.renderMail({
142
+ slug: notificationType,
143
+ locale: message.locale ?? "en",
144
+ variables: message.data ?? {},
145
+ ...(typeof unsubscribeUrl === "string" && { unsubscribeUrl }),
146
+ });
147
+ return { html, subject, text, ...(attachments !== undefined && { attachments }) };
90
148
  }
91
149
  return {
92
- name: "email",
150
+ name: EMAIL_CHANNEL_NAME,
93
151
  mode: "queued",
94
152
  // resolveEmail typically returns the raw stored user.email, which is ciphertext under an active KMS.
95
153
  async resolve(userId, ctx) {
@@ -99,18 +157,23 @@ export function createEmailChannel(options) {
99
157
  const address = await decryptStoredPii(stored, "email", "channel-email:resolve");
100
158
  return address === PII_ERASED_SENTINEL ? null : address;
101
159
  },
102
- render(message, _ctx) {
103
- return renderMessage(message);
160
+ render(message, ctx) {
161
+ return renderMessage(message, ctx);
104
162
  },
105
- async send(address, message, _ctx, rendered) {
106
- const { html, subject, text } = rendered ?? (await renderMessage(message));
163
+ async send(address, message, ctx, rendered) {
164
+ const { html, subject, text, attachments } = rendered ?? (await renderMessage(message, ctx));
107
165
  const envelope = emailEnvelopeFrom(message.data);
166
+ const resolvedAttachments = await resolveAttachments([
167
+ ...(attachments ?? []).map((ref) => ({ ref, origin: "template" })),
168
+ ...fileAttachmentsFrom(message.data, message.locale).map((ref) => ({ ref, origin: "message" })),
169
+ ], ctx);
108
170
  await transport.send(guardEmailMessage({
109
171
  to: address,
110
172
  subject,
111
173
  html,
112
174
  ...(text !== undefined && { text }),
113
175
  ...envelope,
176
+ ...(resolvedAttachments.length > 0 && { attachments: resolvedAttachments }),
114
177
  }));
115
178
  return { status: "sent", address };
116
179
  },
@@ -1,10 +1,10 @@
1
- import { DELIVERY_CHANNEL_EXTENSION } from "@cosmicdrift/kumiko-bundled-features/delivery";
1
+ import { DELIVERY_CHANNEL_EXTENSION, EMAIL_CHANNEL_NAME, } from "@cosmicdrift/kumiko-bundled-features/delivery";
2
2
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { createEmailChannel } from "./email-channel.js";
4
4
  export function createChannelEmailFeature(options) {
5
5
  const channel = createEmailChannel(options);
6
6
  return defineFeature("channel-email", (r) => {
7
- r.describe("Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport`, a `renderer: NotificationRenderer` (e.g. backed by `renderer-simple`), and a `resolveEmail` function that maps a user ID to their email address.");
7
+ r.describe("Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails.");
8
8
  r.uiHints({
9
9
  displayLabel: "Email Channel",
10
10
  category: "notifications",
@@ -12,7 +12,7 @@ export function createChannelEmailFeature(options) {
12
12
  });
13
13
  r.requires("delivery");
14
14
  r.translations({ keys: { "delivery.channel.email": { en: "Email" } } });
15
- r.useExtension(DELIVERY_CHANNEL_EXTENSION, "email", {
15
+ r.useExtension(DELIVERY_CHANNEL_EXTENSION, EMAIL_CHANNEL_NAME, {
16
16
  mode: channel.mode,
17
17
  resolve: channel.resolve,
18
18
  render: channel.render,
@@ -2,4 +2,4 @@ export { createEmailChannel, type EmailChannelOptions } from "./email-channel.js
2
2
  export { createChannelEmailFeature } from "./feature.js";
3
3
  export { guardEmailMessage, withPiiCiphertextGuard } from "./pii-guard.js";
4
4
  export { createSmtpTransport, createSmtpTransportFromEnv, type SmtpEnv, type SmtpTransportOptions, } from "./smtp-transport.js";
5
- export { createInMemoryTransport, type EmailMessage, type EmailTransport } from "./types.js";
5
+ export { createInMemoryTransport, type EmailAttachment, type EmailAttachmentRef, type EmailMessage, type EmailTransport, } from "./types.js";
@@ -2,4 +2,4 @@ export { createEmailChannel } from "./email-channel.js";
2
2
  export { createChannelEmailFeature } from "./feature.js";
3
3
  export { guardEmailMessage, withPiiCiphertextGuard } from "./pii-guard.js";
4
4
  export { createSmtpTransport, createSmtpTransportFromEnv, } from "./smtp-transport.js";
5
- export { createInMemoryTransport } from "./types.js";
5
+ export { createInMemoryTransport, } from "./types.js";
@@ -8,6 +8,15 @@ const CIPHERTEXT_RE = /kumiko-pii:v\d+:[^"\s<>\\]*/g;
8
8
  // is always refused (the address is garbage — better no mail than a leaked
9
9
  // blob); ciphertext in subject/body fails loud in dev and is redacted+logged
10
10
  // in prod.
11
+ function assertAttachmentNamesArePlain(message) {
12
+ for (const attachment of message.attachments ?? []) {
13
+ const fields = [attachment.fileName, attachment.kind === "inline" ? attachment.cid : undefined];
14
+ if (fields.some((field) => field?.includes(CIPHERTEXT_MARKER))) {
15
+ throw new Error(`[channel-email] refusing to send: attachment file name is a PII ciphertext ` +
16
+ `("${PII_CIPHERTEXT_PREFIX}…") — decrypt the stored value before mailing (decryptStoredPii).`);
17
+ }
18
+ }
19
+ }
11
20
  export function guardEmailMessage(message) {
12
21
  // Every envelope address, not just `to`: a ciphertext From, From-Name or Reply-To is
13
22
  // the same garbage as a ciphertext recipient — refuse rather than mail it.
@@ -28,6 +37,7 @@ export function guardEmailMessage(message) {
28
37
  `("${PII_CIPHERTEXT_PREFIX}…") — decrypt the stored value before mailing (decryptStoredPii).`);
29
38
  }
30
39
  }
40
+ assertAttachmentNamesArePlain(message);
31
41
  const leaking = message.subject.includes(CIPHERTEXT_MARKER) ||
32
42
  message.html.includes(CIPHERTEXT_MARKER) ||
33
43
  message.text?.includes(CIPHERTEXT_MARKER) === true;
@@ -37,6 +37,16 @@ function fromHeader(address, fromName) {
37
37
  const [parsed] = addressparser(address, { flatten: true });
38
38
  return parsed?.address ? { name, address: parsed.address } : address;
39
39
  }
40
+ function toNodemailerAttachment(attachment) {
41
+ const base = {
42
+ filename: attachment.fileName,
43
+ content: Buffer.from(attachment.content),
44
+ contentType: attachment.contentType,
45
+ };
46
+ return attachment.kind === "inline"
47
+ ? { ...base, cid: attachment.cid, contentDisposition: "inline" }
48
+ : base;
49
+ }
40
50
  export function createSmtpTransport(options) {
41
51
  const transporter = createTransport({
42
52
  host: options.host,
@@ -57,6 +67,9 @@ export function createSmtpTransport(options) {
57
67
  ...(message.text !== undefined && { text: message.text }),
58
68
  ...(message.replyTo && { replyTo: message.replyTo }),
59
69
  ...(message.headers && { headers: message.headers }),
70
+ ...(message.attachments && {
71
+ attachments: message.attachments.map(toNodemailerAttachment),
72
+ }),
60
73
  });
61
74
  },
62
75
  };
@@ -1,3 +1,10 @@
1
+ import type { EmailAttachmentRef, FileAttachmentRef, InlineAttachmentRef, RenderedPdfAttachmentRef } from "../delivery/index.js";
2
+ export type { EmailAttachmentRef };
3
+ export type EmailAttachment = (InlineAttachmentRef | FileAttachmentRef | Pick<RenderedPdfAttachmentRef, "kind" | "templateSlug">) & {
4
+ readonly content: Uint8Array;
5
+ readonly contentType: string;
6
+ readonly fileName: string;
7
+ };
1
8
  export type EmailMessage = {
2
9
  readonly to: string;
3
10
  readonly subject: string;
@@ -7,6 +14,7 @@ export type EmailMessage = {
7
14
  readonly fromName?: string;
8
15
  readonly replyTo?: string;
9
16
  readonly headers?: Readonly<Record<string, string>>;
17
+ readonly attachments?: readonly EmailAttachment[];
10
18
  };
11
19
  export type EmailTransport = {
12
20
  send(message: EmailMessage): Promise<void>;
@@ -120,8 +120,7 @@ export const setWrite = defineWriteHandler({
120
120
  });
121
121
  }
122
122
  // scope="system" was already rejected above — narrow accordingly.
123
- const subject = resolvePiiSubject(scope, // @cast-boundary runtime-checked above
124
- tenantId, userId);
123
+ const subject = resolvePiiSubject(scope, tenantId, userId);
125
124
  const kmsCtx = {
126
125
  requestId: requestContext.get()?.requestId ?? "config:write:set",
127
126
  tenantId,
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_CURRENCIES, fieldToZod, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
2
3
  import { SUPPORTED_FIELD_TYPES } from "../constants.js";
3
4
  // Builds a Zod schema that validates a custom-field VALUE against its
4
5
  // fieldDefinition. Reuses the framework's `fieldToZod` (Builder-Reuse /
@@ -29,10 +30,9 @@ const SUPPORTED_EMBEDDED_SUB_TYPES = new Set([
29
30
  // resulting schema validates the type-shape only, not the constraint.
30
31
  const CONSTRAINT_KEYS = ["required", "maxLength", "format", "default"];
31
32
  export function buildCustomFieldValueSchema(parsedField) {
32
- if (!parsedField || typeof parsedField !== "object")
33
+ if (!isPlainObject(parsedField))
33
34
  return null;
34
- const obj = parsedField;
35
- const rawType = obj["type"];
35
+ const rawType = parsedField["type"];
36
36
  if (typeof rawType !== "string")
37
37
  return null;
38
38
  if (!SUPPORTED_TYPES_SET.has(rawType))
@@ -47,37 +47,36 @@ export function buildCustomFieldValueSchema(parsedField) {
47
47
  // drops everywhere else.
48
48
  let strippedSubSchema;
49
49
  if (rawType === "embedded") {
50
- const schema = obj["schema"];
50
+ const schema = parsedField["schema"];
51
51
  if (!schema || typeof schema !== "object")
52
52
  return null;
53
53
  strippedSubSchema = {};
54
54
  for (const [subKey, sub] of Object.entries(schema)) {
55
- if (!sub || typeof sub !== "object")
55
+ if (!isPlainObject(sub))
56
56
  return null;
57
- const subObj = sub;
58
- const subType = subObj["type"];
57
+ const subType = sub["type"];
59
58
  if (typeof subType !== "string" || !SUPPORTED_EMBEDDED_SUB_TYPES.has(subType)) {
60
59
  return null;
61
60
  }
62
- const strippedSub = { ...subObj };
61
+ const strippedSub = { ...sub };
63
62
  for (const k of CONSTRAINT_KEYS)
64
63
  delete strippedSub[k];
65
64
  strippedSubSchema[subKey] = strippedSub;
66
65
  }
67
66
  }
68
- const fieldDef = { ...obj };
67
+ const fieldDef = { ...parsedField };
69
68
  for (const k of CONSTRAINT_KEYS)
70
69
  delete fieldDef[k];
71
70
  if (strippedSubSchema !== undefined)
72
71
  fieldDef["schema"] = strippedSubSchema;
73
72
  if (rawType === "enum") {
74
73
  fieldDef["type"] = "select";
75
- fieldDef["options"] = obj["values"] ?? obj["options"] ?? [];
74
+ fieldDef["options"] = parsedField["values"] ?? parsedField["options"] ?? [];
76
75
  }
77
76
  // fieldToZod's money case validates `currency` against the passed list, not
78
77
  // a field-level key — so surface the field's own currency when it declares
79
78
  // one, else fall back to the framework defaults.
80
- const currencies = typeof obj["currency"] === "string" ? [obj["currency"]] : DEFAULT_CURRENCIES;
79
+ const currencies = typeof parsedField["currency"] === "string" ? [parsedField["currency"]] : DEFAULT_CURRENCIES;
81
80
  // @cast-boundary serialized-field is the dehydrated r.field.X() output =
82
81
  // a FieldDefinition; fieldToZod reads only its type-specific keys (the
83
82
  // extra fieldAccess/retention/label keys are ignored).
@@ -1,21 +1,22 @@
1
1
  // T1.5c — user-data-rights wiring for custom-fields.
2
2
  import { extractTableName } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { EXT_USER_DATA } from "@cosmicdrift/kumiko-framework/engine";
4
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
4
5
  import { selectCustomFieldsHostRows } from "./db/queries/user-data-rights.js";
5
6
  function asCustomFieldsHostRow(value) {
6
- if (!value || typeof value !== "object" || Array.isArray(value))
7
+ if (!isPlainObject(value))
7
8
  return null;
8
- if (!("id" in value) || typeof value.id !== "string")
9
+ const id = value["id"];
10
+ if (typeof id !== "string")
9
11
  return null;
10
- const record = value;
11
- const cf = record["custom_fields"] ?? record["customFields"];
12
+ const cf = value["custom_fields"] ?? value["customFields"];
12
13
  if (cf === undefined)
13
14
  return null;
14
15
  if (cf === null)
15
- return { id: value.id, customFields: null };
16
+ return { id, customFields: null };
16
17
  if (!cf || typeof cf !== "object" || Array.isArray(cf))
17
18
  return null;
18
- return { id: value.id, customFields: Object.fromEntries(Object.entries(cf)) };
19
+ return { id, customFields: Object.fromEntries(Object.entries(cf)) };
19
20
  }
20
21
  // fw#2914 — selectCustomFieldsHostRows runs raw SQL against a dynamic host
21
22
  // table name (opts.entityTable varies per call site), so it can't go through
@@ -1,2 +1,3 @@
1
1
  export type { EffectiveRetentionPolicy, ResolveForTenantArgs, ResolveRetentionPolicyArgs, ResolveTenantPresetArgs, RetentionOverride, RetentionPreset, RetentionPresetKey, } from "./feature.js";
2
2
  export { createDataRetentionFeature, RETENTION_PRESETS, resolveRetentionPolicy, resolveRetentionPolicyForTenant, resolveTenantRetentionPreset, retentionOverrideSchema, SELECTABLE_RETENTION_PRESETS, tenantRetentionOverrideEntity, tenantRetentionOverrideTable, } from "./feature.js";
3
+ export { computeCutoff, type Instant } from "./keep-for.js";
@@ -1 +1,2 @@
1
1
  export { createDataRetentionFeature, RETENTION_PRESETS, resolveRetentionPolicy, resolveRetentionPolicyForTenant, resolveTenantRetentionPreset, retentionOverrideSchema, SELECTABLE_RETENTION_PRESETS, tenantRetentionOverrideEntity, tenantRetentionOverrideTable, } from "./feature.js";
2
+ export { computeCutoff } from "./keep-for.js";