@cosmicdrift/kumiko-bundled-features 0.363.0 → 0.365.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 (361) hide show
  1. package/dist/audit/feature.js +1 -1
  2. package/dist/audit/handlers/list.query.js +9 -5
  3. package/dist/audit/run-escape-hatch-retention.d.ts +1 -0
  4. package/dist/audit/run-escape-hatch-retention.js +39 -12
  5. package/dist/auth-email-password/bootstrap.d.ts +9 -8
  6. package/dist/auth-email-password/bootstrap.js +12 -4
  7. package/dist/auth-email-password/email-rate-limits.d.ts +4 -0
  8. package/dist/auth-email-password/email-rate-limits.js +13 -0
  9. package/dist/auth-email-password/feature.js +2 -0
  10. package/dist/auth-email-password/handlers/change-password.write.d.ts +1 -1
  11. package/dist/auth-email-password/handlers/change-password.write.js +6 -2
  12. package/dist/auth-email-password/handlers/confirm-account-unlock.write.d.ts +1 -1
  13. package/dist/auth-email-password/handlers/invite-accept-with-login.write.d.ts +1 -1
  14. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -3
  15. package/dist/auth-email-password/handlers/invite-accept.write.d.ts +1 -1
  16. package/dist/auth-email-password/handlers/invite-create.write.d.ts +4 -5
  17. package/dist/auth-email-password/handlers/invite-create.write.js +4 -1
  18. package/dist/auth-email-password/handlers/invite-signup-complete.write.d.ts +1 -1
  19. package/dist/auth-email-password/handlers/login.write.d.ts +1 -1
  20. package/dist/auth-email-password/handlers/logout.write.d.ts +1 -1
  21. package/dist/auth-email-password/handlers/logout.write.js +6 -2
  22. package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -1
  23. package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -1
  24. package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -1
  25. package/dist/auth-email-password/handlers/reset-password.write.d.ts +1 -1
  26. package/dist/auth-email-password/handlers/signup-confirm.write.d.ts +1 -1
  27. package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -1
  28. package/dist/auth-email-password/handlers/signup-request.write.js +2 -1
  29. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.d.ts +2 -1
  30. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +1 -1
  31. package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -1
  32. package/dist/auth-email-password/handlers/token-request-handler.js +2 -1
  33. package/dist/auth-email-password/handlers/verify-email.write.d.ts +1 -1
  34. package/dist/auth-email-password/magic-link-mail.js +6 -3
  35. package/dist/auth-email-password/mail-i18n.d.ts +3 -0
  36. package/dist/auth-email-password/mail-i18n.js +10 -0
  37. package/dist/auth-email-password/web/auth-gate.js +16 -4
  38. package/dist/auth-mfa/handlers/disable.write.d.ts +1 -1
  39. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.d.ts +1 -1
  40. package/dist/auth-mfa/handlers/enable-confirm.write.d.ts +1 -1
  41. package/dist/auth-mfa/handlers/enable-start-preauth.write.d.ts +1 -1
  42. package/dist/auth-mfa/handlers/enable-start.write.d.ts +1 -1
  43. package/dist/auth-mfa/handlers/regenerate-recovery.write.d.ts +1 -1
  44. package/dist/auth-mfa/handlers/verify.write.d.ts +1 -1
  45. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +52 -5
  46. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +3 -0
  47. package/dist/billing-foundation/consumer-protection/consent-gate.js +21 -2
  48. package/dist/billing-foundation/consumer-protection/mail-notifications.js +1 -1
  49. package/dist/billing-foundation/consumer-protection/register-termination.js +11 -1
  50. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +5 -3
  51. package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -2
  52. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +6 -0
  53. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +34 -0
  54. package/dist/billing-foundation/consumer-protection/termination-record.d.ts +2 -0
  55. package/dist/billing-foundation/consumer-protection/termination-record.js +15 -2
  56. package/dist/billing-foundation/consumer-protection/termination-request.js +30 -20
  57. package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
  58. package/dist/billing-foundation/consumer-protection/termination-texts.js +6 -0
  59. package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +10 -0
  60. package/dist/billing-foundation/consumer-protection/withdrawal-period.js +26 -0
  61. package/dist/billing-foundation/handlers/create-checkout-session.write.js +11 -5
  62. package/dist/billing-foundation/handlers/process-event.write.js +10 -6
  63. package/dist/billing-foundation/i18n.js +9 -0
  64. package/dist/billing-foundation/subscription-tier-sync.d.ts +7 -1
  65. package/dist/billing-foundation/subscription-tier-sync.js +21 -7
  66. package/dist/billing-foundation/types.d.ts +7 -2
  67. package/dist/cap-counter/book-cap-usage.js +8 -0
  68. package/dist/cap-counter/calendar-cap-usage-reservation.js +2 -0
  69. package/dist/cap-counter/cap-reservation.js +7 -3
  70. package/dist/cap-counter/enforce-cap.d.ts +18 -1
  71. package/dist/cap-counter/enforce-cap.js +49 -6
  72. package/dist/cap-counter/handlers/increment-rolling.write.d.ts +1 -0
  73. package/dist/cap-counter/handlers/increment-rolling.write.js +2 -0
  74. package/dist/cap-counter/index.d.ts +1 -1
  75. package/dist/cap-counter/index.js +1 -1
  76. package/dist/cap-counter/with-cap-enforcement.d.ts +2 -0
  77. package/dist/cap-overview/handlers/tenant-caps-list.query.js +3 -1
  78. package/dist/cap-overview/handlers/tenant-options.query.js +3 -1
  79. package/dist/channel-discord/discord-channel.js +3 -2
  80. package/dist/channel-email/email-channel.js +40 -23
  81. package/dist/channel-email/smtp-transport.js +6 -1
  82. package/dist/channel-in-app/handlers/mark-all-read.write.d.ts +1 -1
  83. package/dist/channel-in-app/handlers/mark-read.write.d.ts +1 -1
  84. package/dist/channel-slack/slack-channel.js +3 -2
  85. package/dist/channel-teams/teams-channel.js +3 -2
  86. package/dist/compliance-profiles/handlers/set-profile.write.d.ts +1 -1
  87. package/dist/config/handlers/reset.write.d.ts +1 -1
  88. package/dist/config/handlers/set.write.d.ts +1 -1
  89. package/dist/config/handlers/set.write.js +2 -2
  90. package/dist/config/resolver.d.ts +1 -0
  91. package/dist/config/resolver.js +11 -14
  92. package/dist/config/write-helpers.d.ts +2 -0
  93. package/dist/config/write-helpers.js +19 -0
  94. package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
  95. package/dist/crypto-shredding/handlers/forget-subject.write.js +30 -20
  96. package/dist/custom-fields/db/queries/retention.d.ts +1 -1
  97. package/dist/custom-fields/db/queries/retention.js +2 -1
  98. package/dist/custom-fields/handlers/update-tenant-field.write.js +0 -1
  99. package/dist/data-retention/run-retention-cleanup.js +4 -5
  100. package/dist/delivery/attachment-ref.d.ts +2 -0
  101. package/dist/delivery/attachment-ref.js +30 -1
  102. package/dist/delivery/channel-context.js +19 -1
  103. package/dist/delivery/delivery-service.js +71 -82
  104. package/dist/delivery/handlers/resubscribe-address.write.d.ts +1 -1
  105. package/dist/delivery/handlers/resubscribe-user.write.d.ts +1 -1
  106. package/dist/delivery/handlers/set-preference.write.d.ts +1 -1
  107. package/dist/delivery/handlers/unsubscribe-address.write.d.ts +1 -1
  108. package/dist/delivery/handlers/unsubscribe-user.write.d.ts +1 -1
  109. package/dist/delivery/index.d.ts +1 -1
  110. package/dist/delivery/index.js +1 -1
  111. package/dist/delivery/jobs.js +43 -11
  112. package/dist/delivery/types.d.ts +7 -1
  113. package/dist/feature-toggles/handlers/set.write.d.ts +1 -1
  114. package/dist/feature-toggles/toggle-runtime.d.ts +5 -3
  115. package/dist/feature-toggles/toggle-runtime.js +25 -12
  116. package/dist/file-derivatives/handlers/public-variant.query.js +1 -1
  117. package/dist/file-foundation/read-file-ref-content.d.ts +1 -1
  118. package/dist/file-foundation/read-file-ref-content.js +6 -4
  119. package/dist/files-tenant-data/hooks.js +4 -0
  120. package/dist/files-tenant-data/index.d.ts +1 -0
  121. package/dist/files-tenant-data/index.js +2 -1
  122. package/dist/form-draft/db/queries/owned-file-refs.d.ts +1 -1
  123. package/dist/form-draft/db/queries/owned-file-refs.js +9 -2
  124. package/dist/form-draft/handlers/discard.write.d.ts +1 -1
  125. package/dist/form-draft/handlers/discard.write.js +1 -1
  126. package/dist/form-draft/handlers/save.write.d.ts +1 -1
  127. package/dist/inbound-mail-foundation/oauth-access-token.js +8 -3
  128. package/dist/inbound-mail-foundation/watch-supervisor.js +3 -1
  129. package/dist/jobs/db/queries/stale-run-sweep.d.ts +1 -1
  130. package/dist/jobs/db/queries/stale-run-sweep.js +12 -5
  131. package/dist/jobs/feature.js +5 -3
  132. package/dist/jobs/handlers/list.query.js +3 -1
  133. package/dist/jobs/handlers/retry.write.d.ts +1 -1
  134. package/dist/jobs/handlers/stale-run-sweep.job.d.ts +1 -1
  135. package/dist/jobs/handlers/stale-run-sweep.job.js +2 -2
  136. package/dist/jobs/handlers/trigger.write.d.ts +1 -1
  137. package/dist/jobs/job-run-logger.js +35 -10
  138. package/dist/jobs/tenant-job-run-table.js +3 -2
  139. package/dist/ledger/entity.js +3 -1
  140. package/dist/managed-pages/handlers/set.write.d.ts +1 -1
  141. package/dist/metrics/filters.d.ts +16 -1
  142. package/dist/metrics/filters.js +27 -11
  143. package/dist/personal-access-tokens/handlers/create.write.d.ts +1 -1
  144. package/dist/personal-access-tokens/handlers/revoke.write.d.ts +1 -1
  145. package/dist/personal-access-tokens/revoke-for-user.js +3 -7
  146. package/dist/renderer-foundation/types.d.ts +2 -0
  147. package/dist/renderer-pdf/render-table-pdf.d.ts +27 -33
  148. package/dist/renderer-pdf/render-table-pdf.js +95 -34
  149. package/dist/secrets/feature.js +4 -3
  150. package/dist/secrets/handlers/delete.write.d.ts +1 -1
  151. package/dist/secrets/handlers/delete.write.js +3 -1
  152. package/dist/secrets/handlers/set.write.d.ts +1 -1
  153. package/dist/secrets/secrets-context.js +15 -3
  154. package/dist/secrets/write-gate.d.ts +3 -1
  155. package/dist/secrets/write-gate.js +6 -3
  156. package/dist/sessions/handlers/revoke-all-for-user.write.d.ts +1 -1
  157. package/dist/sessions/handlers/revoke-all-others.write.d.ts +1 -1
  158. package/dist/sessions/handlers/revoke.write.d.ts +1 -1
  159. package/dist/shared/chunked-entity-migration.d.ts +1 -0
  160. package/dist/shared/chunked-entity-migration.js +1 -1
  161. package/dist/shared/index.d.ts +1 -0
  162. package/dist/shared/index.js +1 -0
  163. package/dist/shared/mail-template-body.d.ts +3 -0
  164. package/dist/shared/mail-template-body.js +10 -0
  165. package/dist/shared/resource-image-formats.d.ts +23 -0
  166. package/dist/shared/resource-image-formats.js +21 -0
  167. package/dist/shared/sharp-input-limits.js +7 -8
  168. package/dist/step-dispatcher/webhook-runner.js +2 -2
  169. package/dist/subscription-stripe/plugin-methods.js +18 -8
  170. package/dist/subscription-stripe/verify-webhook.d.ts +1 -1
  171. package/dist/template-resolver/api.js +3 -2
  172. package/dist/template-resolver/compile-document-body.d.ts +7 -0
  173. package/dist/template-resolver/compile-document-body.js +114 -21
  174. package/dist/template-resolver/compile-steps-block.d.ts +10 -0
  175. package/dist/template-resolver/compile-steps-block.js +45 -0
  176. package/dist/template-resolver/constants.d.ts +6 -3
  177. package/dist/template-resolver/constants.js +13 -2
  178. package/dist/template-resolver/document-sanitizer.js +3 -0
  179. package/dist/template-resolver/document-schema.d.ts +3 -2
  180. package/dist/template-resolver/document-schema.js +29 -4
  181. package/dist/template-resolver/document-steps.d.ts +17 -0
  182. package/dist/template-resolver/document-steps.js +93 -0
  183. package/dist/template-resolver/feature.d.ts +2 -0
  184. package/dist/template-resolver/feature.js +13 -1
  185. package/dist/template-resolver/handlers/bundle-import.d.ts +7 -1
  186. package/dist/template-resolver/handlers/bundle-import.js +63 -38
  187. package/dist/template-resolver/handlers/collection-set.write.d.ts +1 -1
  188. package/dist/template-resolver/handlers/collection-set.write.js +4 -3
  189. package/dist/template-resolver/handlers/copy-locale.write.d.ts +1 -1
  190. package/dist/template-resolver/handlers/copy-locale.write.js +95 -49
  191. package/dist/template-resolver/handlers/diff-to-system.query.js +5 -38
  192. package/dist/template-resolver/handlers/discard-draft.write.d.ts +13 -0
  193. package/dist/template-resolver/handlers/discard-draft.write.js +37 -0
  194. package/dist/template-resolver/handlers/export-bundle.query.js +51 -1
  195. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
  196. package/dist/template-resolver/handlers/find-by-id.query.js +6 -3
  197. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +1 -1
  198. package/dist/template-resolver/handlers/import-bundle-system.write.js +16 -5
  199. package/dist/template-resolver/handlers/import-bundle.write.d.ts +1 -1
  200. package/dist/template-resolver/handlers/import-bundle.write.js +11 -1
  201. package/dist/template-resolver/handlers/layout-source.query.d.ts +5 -0
  202. package/dist/template-resolver/handlers/layout-source.query.js +46 -23
  203. package/dist/template-resolver/handlers/list.query.js +4 -1
  204. package/dist/template-resolver/handlers/override-diff.d.ts +20 -0
  205. package/dist/template-resolver/handlers/override-diff.js +41 -0
  206. package/dist/template-resolver/handlers/overview.query.d.ts +9 -0
  207. package/dist/template-resolver/handlers/overview.query.js +60 -12
  208. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +1 -1
  209. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +1 -1
  210. package/dist/template-resolver/handlers/resource-remove.write.d.ts +1 -1
  211. package/dist/template-resolver/handlers/resource-upload.write.d.ts +2 -2
  212. package/dist/template-resolver/handlers/resource-upload.write.js +36 -6
  213. package/dist/template-resolver/handlers/send-test.write.d.ts +3 -1
  214. package/dist/template-resolver/handlers/send-test.write.js +37 -10
  215. package/dist/template-resolver/handlers/set.write.d.ts +1 -1
  216. package/dist/template-resolver/handlers/template-gates.js +13 -2
  217. package/dist/template-resolver/handlers/template-write.js +36 -4
  218. package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +32 -0
  219. package/dist/template-resolver/handlers/tenant-overrides.query.js +80 -16
  220. package/dist/template-resolver/handlers/toggle-status.write.d.ts +17 -4
  221. package/dist/template-resolver/handlers/toggle-status.write.js +35 -19
  222. package/dist/template-resolver/handlers/upsert-system.write.d.ts +1 -1
  223. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +1 -1
  224. package/dist/template-resolver/handlers/upsert-tenant.write.js +1 -1
  225. package/dist/template-resolver/handlers/usage.query.js +11 -2
  226. package/dist/template-resolver/handlers/validate.query.js +4 -1
  227. package/dist/template-resolver/index.d.ts +5 -4
  228. package/dist/template-resolver/index.js +5 -4
  229. package/dist/template-resolver/layout-appearance.d.ts +10 -0
  230. package/dist/template-resolver/layout-appearance.js +36 -0
  231. package/dist/template-resolver/mail-constants.d.ts +2 -0
  232. package/dist/template-resolver/mail-constants.js +3 -0
  233. package/dist/template-resolver/pdf-constants.d.ts +7 -0
  234. package/dist/template-resolver/pdf-constants.js +8 -0
  235. package/dist/template-resolver/qualified-names.d.ts +4 -0
  236. package/dist/template-resolver/qualified-names.js +4 -0
  237. package/dist/template-resolver/resource-image.d.ts +2 -9
  238. package/dist/template-resolver/resource-image.js +2 -11
  239. package/dist/template-resolver/seeding.js +23 -4
  240. package/dist/template-resolver/store-template-resource.d.ts +53 -9
  241. package/dist/template-resolver/store-template-resource.js +250 -97
  242. package/dist/template-resolver/table.d.ts +103 -0
  243. package/dist/template-resolver/table.js +9 -2
  244. package/dist/template-resolver/template-bundle.js +4 -0
  245. package/dist/template-resolver/template-draft.d.ts +54 -0
  246. package/dist/template-resolver/template-draft.js +72 -0
  247. package/dist/template-resolver/template-meta.d.ts +35 -6
  248. package/dist/template-resolver/template-meta.js +67 -16
  249. package/dist/template-resolver/template-preview.d.ts +19 -2
  250. package/dist/template-resolver/template-preview.js +98 -55
  251. package/dist/template-resolver/template-resources.js +3 -4
  252. package/dist/template-resolver/template-usage.d.ts +5 -1
  253. package/dist/template-resolver/template-usage.js +14 -9
  254. package/dist/template-resolver/template-validation.js +13 -9
  255. package/dist/template-resolver/web/client-plugin.js +4 -2
  256. package/dist/template-resolver/web/document-editor-extensions.d.ts +5 -1
  257. package/dist/template-resolver/web/document-editor-extensions.js +76 -13
  258. package/dist/template-resolver/web/document-editor.d.ts +2 -1
  259. package/dist/template-resolver/web/document-editor.js +12 -2
  260. package/dist/template-resolver/web/document-tiptap-editor.d.ts +1 -1
  261. package/dist/template-resolver/web/document-tiptap-editor.js +173 -34
  262. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +13 -5
  263. package/dist/template-resolver/web/document-toolbar-blocks.js +6 -0
  264. package/dist/template-resolver/web/i18n.js +24 -4
  265. package/dist/template-resolver/web/index.d.ts +2 -0
  266. package/dist/template-resolver/web/index.js +2 -0
  267. package/dist/template-resolver/web/placeholder-line-editor.d.ts +6 -2
  268. package/dist/template-resolver/web/placeholder-line-editor.js +9 -7
  269. package/dist/tenant/handlers/add-member.write.d.ts +1 -1
  270. package/dist/tenant/handlers/cancel-invitation.write.d.ts +1 -1
  271. package/dist/tenant/handlers/create.write.d.ts +1 -1
  272. package/dist/tenant/handlers/remove-member.write.d.ts +1 -1
  273. package/dist/tenant/handlers/toggle-enabled.write.d.ts +2 -2
  274. package/dist/tenant/handlers/update-member-roles.write.d.ts +1 -1
  275. package/dist/tenant/handlers/update-member-roles.write.js +3 -6
  276. package/dist/tenant/handlers/update.write.d.ts +1 -1
  277. package/dist/tenant/seeding.js +23 -24
  278. package/dist/tenant-handover/handlers/claim.write.d.ts +1 -1
  279. package/dist/tenant-handover/move-entity-graph.js +14 -4
  280. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
  281. package/dist/tenant-lifecycle/handlers/request-destruction.write.d.ts +1 -1
  282. package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +1 -0
  283. package/dist/tenant-lifecycle/run-tenant-destroy.js +8 -1
  284. package/dist/tenant-lifecycle/stages.js +3 -2
  285. package/dist/tier-engine/constants.d.ts +2 -0
  286. package/dist/tier-engine/constants.js +4 -0
  287. package/dist/tier-engine/entity.d.ts +15 -14
  288. package/dist/tier-engine/entity.js +4 -3
  289. package/dist/tier-engine/feature.js +9 -3
  290. package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +6 -1
  291. package/dist/tier-engine/handlers/set-tenant-tier.write.js +7 -4
  292. package/dist/tier-engine/index.d.ts +1 -1
  293. package/dist/tier-engine/index.js +1 -1
  294. package/dist/tier-engine/tier-resolver.d.ts +13 -13
  295. package/dist/user/constants.d.ts +1 -0
  296. package/dist/user/handlers/create.write.d.ts +1 -1
  297. package/dist/user/handlers/update.write.d.ts +1 -1
  298. package/dist/user/invited-global-roles.d.ts +2 -4
  299. package/dist/user/resolve-display-names.js +5 -9
  300. package/dist/user/seeding.js +20 -1
  301. package/dist/user-data-rights/email-templates.js +0 -1
  302. package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
  303. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.d.ts +1 -1
  304. package/dist/user-data-rights/handlers/lift-restriction.write.d.ts +1 -1
  305. package/dist/user-data-rights/handlers/request-deletion-by-email.write.d.ts +1 -1
  306. package/dist/user-data-rights/handlers/request-deletion.write.d.ts +1 -1
  307. package/dist/user-data-rights/handlers/request-export.write.d.ts +1 -1
  308. package/dist/user-data-rights/handlers/restrict-account.write.d.ts +1 -1
  309. package/dist/user-data-rights/handlers/run-forget-cleanup.write.d.ts +1 -1
  310. package/dist/user-data-rights/i18n.js +5 -0
  311. package/dist/user-data-rights/lib/update-user-lifecycle.d.ts +2 -0
  312. package/dist/user-data-rights/lib/update-user-lifecycle.js +6 -2
  313. package/dist/user-data-rights/run-forget-cleanup.js +2 -2
  314. package/dist/user-profile/handlers/change-email.write.d.ts +1 -1
  315. package/dist/waitlist/feature.js +1 -1
  316. package/dist/waitlist/handlers/invite.write.d.ts +1 -1
  317. package/dist/waitlist/handlers/invite.write.js +21 -3
  318. package/dist/waitlist/handlers/reject.write.d.ts +1 -1
  319. package/dist/waitlist/handlers/reject.write.js +3 -16
  320. package/dist/waitlist/handlers/submit.write.d.ts +1 -1
  321. package/dist/waitlist/handlers/submit.write.js +29 -7
  322. package/dist/waitlist/i18n.js +9 -0
  323. package/dist/waitlist/lib.d.ts +2 -1
  324. package/dist/waitlist/lib.js +15 -0
  325. package/dist/waitlist/options.js +1 -1
  326. package/dist/workflow-runner/event-trigger.js +2 -2
  327. package/dist/workflow-runner/feature.js +1 -1
  328. package/dist/workflow-runner/handlers/resume-run.write.js +7 -2
  329. package/package.json +19 -10
  330. package/src/audit/changes.json +18 -0
  331. package/src/auth-email-password/changes.json +55 -0
  332. package/src/billing-foundation/changes.json +48 -0
  333. package/src/cap-counter/changes.json +19 -0
  334. package/src/cap-overview/changes.json +6 -0
  335. package/src/channel-email/changes.json +18 -0
  336. package/src/config/changes.json +6 -0
  337. package/src/crypto-shredding/changes.json +6 -0
  338. package/src/custom-fields/changes.json +6 -0
  339. package/src/delivery/changes.json +30 -0
  340. package/src/document-ingest-foundation/changes.json +6 -0
  341. package/src/feature-toggles/changes.json +7 -0
  342. package/src/file-foundation/changes.json +6 -0
  343. package/src/files-tenant-data/changes.json +6 -0
  344. package/src/inbound-mail-foundation/changes.json +6 -0
  345. package/src/jobs/changes.json +31 -0
  346. package/src/ledger/changes.json +7 -0
  347. package/src/metrics/changes.json +6 -0
  348. package/src/personal-access-tokens/changes.json +6 -0
  349. package/src/renderer-pdf/changes.json +12 -0
  350. package/src/secrets/changes.json +12 -0
  351. package/src/shared/changes.json +6 -0
  352. package/src/subscription-stripe/changes.json +6 -0
  353. package/src/template-resolver/changes.json +149 -0
  354. package/src/tenant/changes.json +6 -0
  355. package/src/tenant-lifecycle/changes.json +6 -0
  356. package/src/tier-engine/changes.json +18 -0
  357. package/src/user/changes.json +6 -0
  358. package/src/user-data-rights/changes.json +12 -0
  359. package/src/user-profile/changes.json +1 -1
  360. package/src/waitlist/changes.json +18 -0
  361. package/src/workflow-runner/changes.json +6 -0
@@ -10,7 +10,7 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
10
10
  // (nur `{ children }`-Prop). Der Sample kann so einen eigenen Login-
11
11
  // Screen rein konfigurieren, ohne den Gate selbst ersetzen zu müssen.
12
12
  import { useLocale } from "@cosmicdrift/kumiko-renderer";
13
- import { useEffect, useState } from "react";
13
+ import { useEffect, useMemo, useState } from "react";
14
14
  import { assertNavigableUrl, buildLoginRedirectUrl, followNextOrLandingAfterLogin, } from "./auth-redirect.js";
15
15
  import { LoginScreen } from "./login-screen.js";
16
16
  import { SessionProvider, useSession } from "./session.js";
@@ -102,6 +102,17 @@ export function resolveLoginRedirect(loginUrl, locale, { origin, pathname, searc
102
102
  url: typeof loginUrl === "function" ? target : buildLoginRedirectUrl(target, returnPath, origin),
103
103
  };
104
104
  }
105
+ // A throwing loginUrl builder must not take the whole app tree down in render;
106
+ // null falls through to the built-in login screen.
107
+ function resolveLoginRedirectOrNull(loginUrl, locale, location) {
108
+ try {
109
+ return resolveLoginRedirect(loginUrl, locale, location);
110
+ }
111
+ catch (error) {
112
+ console.error("Auth gate: loginUrl could not be resolved, showing the built-in login", error);
113
+ return null;
114
+ }
115
+ }
105
116
  export function makeAuthGate(opts = {}) {
106
117
  const { loginUrl } = opts;
107
118
  if (typeof loginUrl === "string")
@@ -110,9 +121,10 @@ export function makeAuthGate(opts = {}) {
110
121
  function AuthGate({ children }) {
111
122
  const { status } = useSession();
112
123
  const locale = useLocale().locale();
113
- const loginRedirect = loginUrl !== undefined && status === "unauthenticated"
114
- ? resolveLoginRedirect(loginUrl, locale, window.location)
115
- : null;
124
+ const { origin, pathname, search, hash } = window.location;
125
+ const loginRedirect = useMemo(() => loginUrl !== undefined && status === "unauthenticated"
126
+ ? resolveLoginRedirectOrNull(loginUrl, locale, { origin, pathname, search, hash })
127
+ : null, [status, locale, origin, pathname, search, hash]);
116
128
  const redirectUrl = loginRedirect?.kind === "redirect" ? loginRedirect.url : null;
117
129
  // kumiko-lint-ignore no-raw-hooks Phase-3 conversion tracked in #2312
118
130
  useEffect(() => {
@@ -7,4 +7,4 @@ export declare function createDisableHandler(opts: DisableOptions): import("@cos
7
7
  code: z.ZodString;
8
8
  }, z.core.$strip>, {
9
9
  disabled: boolean;
10
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
10
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -13,4 +13,4 @@ export declare function createEnableConfirmPreauthHandler(opts: EnableConfirmPre
13
13
  }, z.core.$strip>, {
14
14
  kind: string;
15
15
  session: SessionUser;
16
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
16
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -9,4 +9,4 @@ export declare function createEnableConfirmHandler(opts: EnableConfirmOptions):
9
9
  code: z.ZodString;
10
10
  }, z.core.$strip>, {
11
11
  enabled: boolean;
12
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
12
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -11,4 +11,4 @@ export declare function createEnableStartPreauthHandler(opts: EnableStartPreauth
11
11
  setupToken: string;
12
12
  otpauthUri: string;
13
13
  recoveryCodes: string[];
14
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
14
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -10,4 +10,4 @@ export declare function createEnableStartHandler(opts: EnableStartOptions): impo
10
10
  otpauthUri: string;
11
11
  totpSecret: string;
12
12
  recoveryCodes: string[];
13
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
13
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -6,4 +6,4 @@ export declare function createRegenerateRecoveryHandler(opts: RegenerateRecovery
6
6
  code: z.ZodString;
7
7
  }, z.core.$strip>, {
8
8
  recoveryCodes: string[];
9
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
9
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -11,4 +11,4 @@ export declare function createMfaVerifyHandler(opts: MfaVerifyOptions): import("
11
11
  }, z.core.$strip>, {
12
12
  kind: string;
13
13
  session: SessionUser;
14
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
14
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -1,4 +1,7 @@
1
+ import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
1
2
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
+ import { Marked } from "marked";
4
+ import { isAllowedDocumentLinkHref, isDocumentPlaceholderHref, } from "../../template-resolver/index.js";
2
5
  import { resolveConsentLocale } from "./consent-text.js";
3
6
  export const CONFIRMATION_LABELS = {
4
7
  de: {
@@ -89,13 +92,57 @@ function formatDate(iso, locale, timeZone) {
89
92
  timeZoneName: "short",
90
93
  }).format(epochMilliseconds);
91
94
  }
92
- // The mail template only knows plain text variables, so the Markdown terms
93
- // become one row per paragraph; heading markers are dropped.
95
+ // One richText row of inline HTML per Markdown block. The sanitizer shows a rejected
96
+ // row as escaped text, so author HTML, images and refused links stay plain text.
97
+ const ROW_END = "\n\n";
98
+ function isMailLinkHref(href) {
99
+ return isAllowedDocumentLinkHref(href) && !isDocumentPlaceholderHref(href);
100
+ }
101
+ const inlineHtmlMarkdown = new Marked({ gfm: false, breaks: false });
102
+ inlineHtmlMarkdown.use({
103
+ renderer: {
104
+ heading({ tokens }) {
105
+ const headingHtml = this.parser.parseInline(tokens);
106
+ return `<strong>${headingHtml}</strong>${ROW_END}`;
107
+ },
108
+ paragraph({ tokens }) {
109
+ return `${this.parser.parseInline(tokens)}${ROW_END}`;
110
+ },
111
+ code: ({ text }) => `${escapeHtml(text)}${ROW_END}`,
112
+ blockquote({ tokens }) {
113
+ return this.parser.parse(tokens);
114
+ },
115
+ list({ items }) {
116
+ return items.map((item) => this.parser.parse(item.tokens)).join("");
117
+ },
118
+ hr: () => "",
119
+ space: () => "",
120
+ del({ tokens }) {
121
+ return this.parser.parseInline(tokens);
122
+ },
123
+ br: () => " ",
124
+ html: ({ text }) => escapeHtml(text),
125
+ image: ({ text }) => escapeHtml(text),
126
+ text(token) {
127
+ // Only block-level text (a list item's own text) carries inline child tokens.
128
+ return "tokens" in token && token.tokens
129
+ ? `${this.parser.parseInline(token.tokens)}${ROW_END}`
130
+ : escapeHtml(token.text);
131
+ },
132
+ link({ href, tokens }) {
133
+ const labelHtml = this.parser.parseInline(tokens);
134
+ if (isMailLinkHref(href))
135
+ return `<a href="${escapeHtmlAttr(href)}">${labelHtml}</a>`;
136
+ return labelHtml === escapeHtml(href) ? labelHtml : `${labelHtml} (${escapeHtml(href)})`;
137
+ },
138
+ },
139
+ });
94
140
  function termsParagraphsOf(markdown) {
95
- return markdown
141
+ return inlineHtmlMarkdown
142
+ .parse(markdown, { async: false })
96
143
  .split(/\n\s*\n/)
97
- .map((paragraph) => paragraph.replace(/^#{1,6}\s+/gm, "").trim())
98
- .filter((paragraph) => paragraph.length > 0)
144
+ .map((row) => row.replace(/\s+/g, " ").trim())
145
+ .filter((row) => row.length > 0)
99
146
  .map((text) => ({ text }));
100
147
  }
101
148
  export function contractConfirmationVariables(args) {
@@ -22,6 +22,9 @@ export type OrderItem = {
22
22
  readonly labelKey: string;
23
23
  readonly params?: Readonly<Record<string, string | number>>;
24
24
  };
25
+ /** Throws an InternalError (an app configuration fault, not a buyer error) for a
26
+ * label the consent event could not store. */
27
+ export declare function assertValidOrderItem(item: OrderItem | undefined, priceId: string): void;
25
28
  export type ConsentPriceDetails = {
26
29
  readonly tier: string | null;
27
30
  readonly priceId: string;
@@ -2,12 +2,12 @@
2
2
  // validate + prepare before the provider call, record only after it succeeded.
3
3
  import { createHash } from "node:crypto";
4
4
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
5
- import { UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
5
+ import { InternalError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
6
6
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
7
7
  import * as z from "zod";
8
8
  import { requireTemplateResolver, TEXT_BLOCK_KIND, TemplateNotFoundError, } from "../../template-resolver/index.js";
9
9
  import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id.js";
10
- import { CHECKOUT_CONSENT_RECORDED_EVENT_QN, PAYMENT_AGGREGATE_TYPE, SUBSCRIPTION_AGGREGATE_TYPE, } from "../events.js";
10
+ import { CHECKOUT_CONSENT_RECORDED_EVENT_QN, checkoutConsentRecordedPayloadSchema, PAYMENT_AGGREGATE_TYPE, SUBSCRIPTION_AGGREGATE_TYPE, } from "../events.js";
11
11
  import { consentTextVersion, resolveConsentLocale, submitMessageFor } from "./consent-text.js";
12
12
  // Optional at the schema level so a missing/unchecked consent reaches
13
13
  // prepareConsent and fails as 422 consent_required, not as a zod 400.
@@ -19,6 +19,25 @@ export const consentPayloadSchema = z
19
19
  locale: z.string().min(2).max(35),
20
20
  })
21
21
  .strict();
22
+ // Bounds come from the event schema the label ends up in, so an app label that
23
+ // passes here cannot fail the append after the provider session exists.
24
+ const orderItemSchema = z.object({
25
+ labelKey: checkoutConsentRecordedPayloadSchema.shape.itemLabelKey.unwrap(),
26
+ params: checkoutConsentRecordedPayloadSchema.shape.itemLabelParams,
27
+ });
28
+ /** Throws an InternalError (an app configuration fault, not a buyer error) for a
29
+ * label the consent event could not store. */
30
+ export function assertValidOrderItem(item, priceId) {
31
+ // skip: subscription checkouts have no one-off order item
32
+ if (item === undefined)
33
+ return;
34
+ const parsed = orderItemSchema.safeParse(item);
35
+ if (!parsed.success) {
36
+ throw new InternalError({
37
+ message: `billing-foundation: consumerProtection.oneOffItemLabel returned an invalid label for priceId "${priceId}": ${parsed.error.message}`,
38
+ });
39
+ }
40
+ }
22
41
  export async function prepareConsent(ctx, options, consent, mode) {
23
42
  if (!consent?.earlyPerformanceRequested || !consent.withdrawalLossAcknowledged) {
24
43
  throw new UnprocessableError("consent_required", {
@@ -21,7 +21,7 @@ export const OPERATOR_NOTICE_VARIABLES = {
21
21
  export const CONTRACT_CONFIRMATION_VARIABLES = {
22
22
  contractLines: textRows(),
23
23
  consentGivenAt: templateVariable.text({ required: true }),
24
- termsParagraphs: textRows(),
24
+ termsParagraphs: templateVariable.list({ text: templateVariable.richText({ required: true }) }, { required: true }),
25
25
  operatorEmail: templateVariable.text({ required: true }),
26
26
  };
27
27
  function perLocale(build) {
@@ -16,7 +16,17 @@ export function registerContractTermination(r, options, consumerProtection) {
16
16
  // request: matched and unmatched requests then do the same work and the
17
17
  // response time does not tell whether the email belongs to a customer.
18
18
  // The tenant comes from the stored trigger event (the job's system user).
19
- r.job("cancel-on-public-termination-declared", { trigger: { on: CONTRACT_TERMINATION_DECLARED_EVENT_QN }, runIn: "worker" }, async (payload, ctx) => {
19
+ // Every declaration appends to the same subscription stream, so parallel runs
20
+ // and a declaration arriving mid-run hit version conflicts. Sequential runs
21
+ // rule out the first; a retry re-reads the stream and the requestId check in
22
+ // record-contract-termination keeps it from recording twice.
23
+ r.job("cancel-on-public-termination-declared", {
24
+ trigger: { on: CONTRACT_TERMINATION_DECLARED_EVENT_QN },
25
+ runIn: "worker",
26
+ concurrency: "sequential",
27
+ retries: 3,
28
+ backoff: "exponential",
29
+ }, async (payload, ctx) => {
20
30
  const declared = contractTerminationDeclaredPayloadSchema.safeParse(payload);
21
31
  // skip: trigger event carries no usable declaration
22
32
  if (!declared.success)
@@ -30,15 +30,17 @@ export type TerminationDeclaration = {
30
30
  /** The operator notice for a provider problem has no declarant data at all. */
31
31
  export type OperatorNoticeDeclaration = Pick<TerminationDeclaration, "declarationType" | "terminationKind"> & Partial<Omit<TerminationDeclaration, "declarationType" | "terminationKind">>;
32
32
  export declare function formatReceivedAt(iso: string, locale: ConsentLocale): string;
33
+ /** The receipt carries enum fields only: free text from an anonymous caller
34
+ * must not be echoed to an arbitrary address. */
35
+ export type ReceiptDeclaration = Pick<TerminationDeclaration, "declarationType" | "terminationKind">;
33
36
  export type TerminationReceiptArgs = {
34
37
  readonly locale: ConsentLocale;
35
38
  readonly requestId: string;
36
39
  readonly receivedAtIso: string;
37
- readonly declaration: TerminationDeclaration;
40
+ readonly declaration: ReceiptDeclaration;
38
41
  readonly operatorEmail: string;
39
42
  };
40
- /** Built from the declarant's own input only: the content must not depend on
41
- * whether (or how) the declaration matched a contract. */
43
+ /** The content must not depend on whether (or how) the declaration matched a contract. */
42
44
  export declare function terminationReceiptVariables(args: TerminationReceiptArgs): TerminationReceiptVariables;
43
45
  export type OperatorNoticeArgs = {
44
46
  readonly locale: ConsentLocale;
@@ -32,8 +32,7 @@ function declarationLines(declaration, locale) {
32
32
  ];
33
33
  return lines.map((text) => ({ text }));
34
34
  }
35
- /** Built from the declarant's own input only: the content must not depend on
36
- * whether (or how) the declaration matched a contract. */
35
+ /** The content must not depend on whether (or how) the declaration matched a contract. */
37
36
  export function terminationReceiptVariables(args) {
38
37
  const texts = TERMINATION_TEXTS[args.locale];
39
38
  return {
@@ -0,0 +1,6 @@
1
+ import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare const RECEIPT_MAILS_PER_RECIPIENT = 3;
3
+ export declare const RECEIPT_WINDOW_SECONDS: number;
4
+ /** Consumes one receipt token for the recipient. Fails closed when the limiter
5
+ * errors or no digest key is wired: the declaration is already recorded, a missing receipt is the lesser harm. */
6
+ export declare function mayMailReceipt(ctx: HandlerContext, email: string): Promise<boolean>;
@@ -0,0 +1,34 @@
1
+ import { normalizePayloadBucketValue } from "@cosmicdrift/kumiko-framework/pipeline";
2
+ // The declaration itself is a statutory duty and is never refused. Only the
3
+ // receipt mail is capped, so the endpoint cannot be used to mail one inbox
4
+ // from the operator's sender domain.
5
+ export const RECEIPT_MAILS_PER_RECIPIENT = 3;
6
+ export const RECEIPT_WINDOW_SECONDS = 24 * 60 * 60;
7
+ const RECEIPT_BUCKET_PREFIX = "termination-receipt";
8
+ function receiptBucketKey(ctx, email) {
9
+ const digest = ctx.rateLimitDigest?.(normalizePayloadBucketValue(email, "email"));
10
+ return digest === undefined ? undefined : `${RECEIPT_BUCKET_PREFIX}:${digest}`;
11
+ }
12
+ /** Consumes one receipt token for the recipient. Fails closed when the limiter
13
+ * errors or no digest key is wired: the declaration is already recorded, a missing receipt is the lesser harm. */
14
+ export async function mayMailReceipt(ctx, email) {
15
+ // skip: no rate limiter wired, nothing to count against
16
+ if (!ctx.rateLimit)
17
+ return true;
18
+ try {
19
+ const bucketKey = receiptBucketKey(ctx, email);
20
+ if (bucketKey === undefined) {
21
+ ctx.log?.warn("[billing-foundation:termination] no rate-limit digest available, receipt mail withheld");
22
+ return false;
23
+ }
24
+ const decision = await ctx.rateLimit.check(bucketKey, {
25
+ limit: RECEIPT_MAILS_PER_RECIPIENT,
26
+ windowSeconds: RECEIPT_WINDOW_SECONDS,
27
+ });
28
+ return decision.allowed;
29
+ }
30
+ catch (error) {
31
+ ctx.log?.warn(`[billing-foundation:termination] receipt rate limiter failed: ${error instanceof Error ? error.message : String(error)}`);
32
+ return false;
33
+ }
34
+ }
@@ -49,6 +49,8 @@ export type RecordContractTerminationResult = {
49
49
  readonly effectiveAtIso: string | null;
50
50
  readonly providerCancel: ProviderCancelOutcome;
51
51
  readonly noticeReason: Extract<OperatorNoticeReason, "provider_cannot_cancel" | "provider_error" | "no_active_subscription"> | null;
52
+ /** An account withdrawal arrived after the period: cancelled at period end instead. */
53
+ readonly withdrawalPeriodExpired: boolean;
52
54
  };
53
55
  export declare const recordUnmatchedContractTerminationSchema: z.ZodObject<{
54
56
  requestId: z.ZodString;
@@ -12,6 +12,7 @@ import { CONTRACT_TERMINATION_CHANNELS, CONTRACT_TERMINATION_DECLARATION_TYPES,
12
12
  import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
13
13
  import { CONSENT_LOCALES, FALLBACK_CONSENT_LOCALE } from "./consent-locale.js";
14
14
  import { notifyOperator } from "./termination-notify.js";
15
+ import { isWithinWithdrawalPeriod } from "./withdrawal-period.js";
15
16
  const REQUESTED_CANCEL_TIMINGS = [
16
17
  SubscriptionCancelTimings.periodEnd,
17
18
  SubscriptionCancelTimings.immediately,
@@ -148,10 +149,14 @@ export function createRecordContractTerminationHandler(options, consumerProtecti
148
149
  effectiveAtIso: alreadyRecorded.effectiveAtIso,
149
150
  providerCancel: alreadyRecorded.providerCancel,
150
151
  noticeReason: null,
152
+ withdrawalPeriodExpired: false,
151
153
  },
152
154
  };
153
155
  }
154
- const { providerCancel, effectiveAtIso, noticeReason } = await cancelAtProvider(ctx, tenantId, payload.when, receivedAtIso);
156
+ const withdrawalPeriodExpired = payload.declarationType === "withdrawal" &&
157
+ payload.when === SubscriptionCancelTimings.immediately &&
158
+ !isWithinWithdrawalPeriod(stream.events, options.now());
159
+ const { providerCancel, effectiveAtIso, noticeReason } = await cancelAtProvider(ctx, tenantId, withdrawalPeriodExpired ? SubscriptionCancelTimings.periodEnd : payload.when, receivedAtIso);
155
160
  const recorded = {
156
161
  requestId: payload.requestId,
157
162
  declarationType: payload.declarationType,
@@ -161,7 +166,14 @@ export function createRecordContractTerminationHandler(options, consumerProtecti
161
166
  effectiveAtIso,
162
167
  providerCancel,
163
168
  };
164
- await stream.appendOne({
169
+ // Re-fetch after the external call: a webhook appended meanwhile would
170
+ // otherwise fail the append on a stale expectedVersion although the
171
+ // provider already cancelled.
172
+ const streamAfterProviderCall = await ctx.fetchForWriting({
173
+ aggregateId: subscriptionAggregateId(tenantId),
174
+ aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
175
+ });
176
+ await streamAfterProviderCall.appendOne({
165
177
  type: CONTRACT_TERMINATION_REQUESTED_EVENT_QN,
166
178
  payload: recorded,
167
179
  });
@@ -189,6 +201,7 @@ export function createRecordContractTerminationHandler(options, consumerProtecti
189
201
  effectiveAtIso,
190
202
  providerCancel,
191
203
  noticeReason,
204
+ withdrawalPeriodExpired,
192
205
  },
193
206
  };
194
207
  },
@@ -14,7 +14,7 @@ import { purchaseRolesOf } from "../plan-catalog.js";
14
14
  import { resolveConsentLocale } from "./consent-text.js";
15
15
  import { terminationReceiptVariables } from "./termination-mail.js";
16
16
  import { notifyOperator, requireNotify } from "./termination-notify.js";
17
- const TENANT_ADMIN_ROLE = "TenantAdmin";
17
+ import { mayMailReceipt } from "./termination-receipt-limit.js";
18
18
  const EXTRAORDINARY_REASON_MESSAGE = "reason is required for an extraordinary termination";
19
19
  const authUserRowSchema = z.object({
20
20
  id: z.string().min(1),
@@ -53,27 +53,36 @@ function needsOperatorNotice(declaration) {
53
53
  ];
54
54
  }
55
55
  async function sendTerminationMails(ctx, consumerProtection, plan) {
56
- await requireNotify(ctx)(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
57
- route: { email: plan.declaration.email },
58
- data: terminationReceiptVariables({
56
+ const receiptAllowed = await mayMailReceipt(ctx, plan.declaration.email);
57
+ if (receiptAllowed) {
58
+ await requireNotify(ctx)(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
59
+ route: { email: plan.declaration.email },
60
+ data: terminationReceiptVariables({
61
+ locale: plan.locale,
62
+ requestId: plan.requestId,
63
+ receivedAtIso: plan.receivedAtIso,
64
+ declaration: {
65
+ declarationType: plan.declaration.declarationType,
66
+ terminationKind: plan.declaration.terminationKind,
67
+ },
68
+ operatorEmail: consumerProtection.operatorEmail,
69
+ }),
59
70
  locale: plan.locale,
60
- requestId: plan.requestId,
61
- receivedAtIso: plan.receivedAtIso,
62
- declaration: plan.declaration,
63
- operatorEmail: consumerProtection.operatorEmail,
64
- }),
65
- locale: plan.locale,
66
- priority: "critical",
67
- });
71
+ priority: "critical",
72
+ });
73
+ }
74
+ const reasons = receiptAllowed
75
+ ? plan.reasons
76
+ : [...plan.reasons, "receipt_rate_limited"];
68
77
  // skip: no operator notice needed for this declaration
69
- if (plan.reasons.length === 0)
78
+ if (reasons.length === 0)
70
79
  return;
71
- await notifyOperator(ctx, consumerProtection, plan);
80
+ await notifyOperator(ctx, consumerProtection, { ...plan, reasons });
72
81
  }
73
82
  // Tenants the entered email can terminate a contract for: tenants where the
74
- // user is TenantAdmin and the subscription is not terminal. Every identity
83
+ // user holds a purchase role and the subscription is not terminal. Every identity
75
84
  // below comes from the trusted lookups, never from the payload.
76
- async function findTerminableTenantIds(ctx, requestTenantId, email) {
85
+ async function findTerminableTenantIds(ctx, options, requestTenantId, email) {
77
86
  declareEscapeHatch({
78
87
  reason: "anonymous declarant has no session — finds the contract by email via user:find-for-auth and tenant:memberships as a system user; the calling handler declares its own escapeHatch",
79
88
  });
@@ -85,9 +94,10 @@ async function findTerminableTenantIds(ctx, requestTenantId, email) {
85
94
  }));
86
95
  if (!memberships.success)
87
96
  return [];
97
+ const purchaseRoles = purchaseRolesOf(options.catalog);
88
98
  const adminTenantIds = [
89
99
  ...new Set(memberships.data
90
- .filter((membership) => membership.roles.includes(TENANT_ADMIN_ROLE))
100
+ .filter((membership) => membership.roles.some((role) => purchaseRoles.includes(role)))
91
101
  .map((membership) => membership.tenantId)),
92
102
  ];
93
103
  const terminable = [];
@@ -105,7 +115,6 @@ export function createRequestContractTerminationHandler(options, consumerProtect
105
115
  schema: requestContractTerminationSchema,
106
116
  access: { roles: ["anonymous"] },
107
117
  rateLimit: { per: "ip+handler", limit: 5, windowSeconds: 600 },
108
- additionalRateLimits: [{ per: { payloadField: "email" }, limit: 3, windowSeconds: 86400 }],
109
118
  ...(consumerProtection.terminationScope === "platform" && { tenantlessAnonymous: true }),
110
119
  escapeHatch: {
111
120
  reason: "Anonymous declarant has no session — finds the contract by email via ctx.queryAs(system, user:find-for-auth) and tenant:memberships; the tenant written to comes only from those lookups.",
@@ -119,7 +128,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
119
128
  const reasons = [];
120
129
  const locale = resolveConsentLocale(payload.locale);
121
130
  let matchedTenantId;
122
- const candidates = await findTerminableTenantIds(ctx, event.user.tenantId, payload.email);
131
+ const candidates = await findTerminableTenantIds(ctx, options, event.user.tenantId, payload.email);
123
132
  const [onlyCandidate] = candidates;
124
133
  if (candidates.length === 1 && onlyCandidate !== undefined) {
125
134
  matchedTenantId = onlyCandidate;
@@ -168,7 +177,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
168
177
  export function createTerminateContractHandler(options, consumerProtection) {
169
178
  return {
170
179
  name: "terminate-contract",
171
- description: "Terminates the tenant's subscription at the end of the billing period, or withdraws from the contract and cancels it immediately; sends a receipt to the caller's email.",
180
+ description: "Terminates the tenant's subscription at the end of the billing period, or withdraws from the contract and cancels it immediately within the 14-day withdrawal period (at period end after it); sends a receipt to the caller's email.",
172
181
  schema: terminateContractSchema,
173
182
  access: { roles: purchaseRolesOf(options.catalog) },
174
183
  escapeHatch: {
@@ -214,6 +223,7 @@ export function createTerminateContractHandler(options, consumerProtection) {
214
223
  },
215
224
  reasons: [
216
225
  ...(data.noticeReason !== null ? [data.noticeReason] : []),
226
+ ...(data.withdrawalPeriodExpired ? ["withdrawal_period_expired"] : []),
217
227
  ...needsOperatorNotice(payload),
218
228
  ],
219
229
  tenantId,
@@ -58,6 +58,6 @@ export type PageTexts = {
58
58
  readonly rateLimitedTitle: string;
59
59
  readonly rateLimitedBody: string;
60
60
  };
61
- export declare const OPERATOR_NOTICE_REASONS: readonly ["unmatched", "ambiguous", "withdrawal", "extraordinary", "provider_cannot_cancel", "provider_error", "no_active_subscription", "recording_failed"];
61
+ export declare const OPERATOR_NOTICE_REASONS: readonly ["unmatched", "ambiguous", "withdrawal", "extraordinary", "provider_cannot_cancel", "provider_error", "no_active_subscription", "recording_failed", "withdrawal_period_expired", "receipt_rate_limited"];
62
62
  export type OperatorNoticeReason = (typeof OPERATOR_NOTICE_REASONS)[number];
63
63
  export declare const TERMINATION_TEXTS: Readonly<Record<ConsentLocale, TerminationTexts>>;
@@ -7,6 +7,8 @@ export const OPERATOR_NOTICE_REASONS = [
7
7
  "provider_error",
8
8
  "no_active_subscription",
9
9
  "recording_failed",
10
+ "withdrawal_period_expired",
11
+ "receipt_rate_limited",
10
12
  ];
11
13
  export const TERMINATION_TEXTS = {
12
14
  de: {
@@ -46,6 +48,8 @@ export const TERMINATION_TEXTS = {
46
48
  provider_error: "Die Kündigung beim Zahlungsanbieter ist fehlgeschlagen",
47
49
  no_active_subscription: "Keine laufende Subscription beim Anbieter",
48
50
  recording_failed: "Die Erklärung konnte nicht gespeichert werden",
51
+ withdrawal_period_expired: "Widerruf nach Ablauf der Widerrufsfrist: zum Periodenende gekündigt, Anspruch prüfen",
52
+ receipt_rate_limited: "Keine Eingangsbestätigung versendet: Limit pro Empfängeradresse erreicht",
49
53
  },
50
54
  noticeReasons: "Grund der Benachrichtigung",
51
55
  channel: "Kanal",
@@ -125,6 +129,8 @@ export const TERMINATION_TEXTS = {
125
129
  provider_error: "Cancelling at the payment provider failed",
126
130
  no_active_subscription: "No running subscription at the provider",
127
131
  recording_failed: "The declaration could not be stored",
132
+ withdrawal_period_expired: "Withdrawal after the withdrawal period: cancelled at period end, review the claim",
133
+ receipt_rate_limited: "No receipt mail sent: the per-recipient limit was reached",
128
134
  },
129
135
  noticeReasons: "Reason for this notice",
130
136
  channel: "Channel",
@@ -0,0 +1,10 @@
1
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
+ export declare const WITHDRAWAL_PERIOD_DAYS = 14;
3
+ type PeriodStartEvent = {
4
+ readonly type: string;
5
+ readonly createdAt: Temporal.Instant;
6
+ };
7
+ /** The period runs from the latest consent or subscription start on the stream.
8
+ * A stream with neither has no provable start and counts as expired. */
9
+ export declare function isWithinWithdrawalPeriod(events: readonly PeriodStartEvent[], now: Temporal.Instant): boolean;
10
+ export {};
@@ -0,0 +1,26 @@
1
+ import { CHECKOUT_CONSENT_RECORDED_EVENT_QN, SUBSCRIPTION_CREATED_EVENT_QN } from "../events.js";
2
+ // § 355 BGB: the withdrawal period is 14 days from conclusion of the contract.
3
+ export const WITHDRAWAL_PERIOD_DAYS = 14;
4
+ const HOURS_PER_DAY = 24;
5
+ const PERIOD_START_EVENT_TYPES = new Set([
6
+ CHECKOUT_CONSENT_RECORDED_EVENT_QN,
7
+ SUBSCRIPTION_CREATED_EVENT_QN,
8
+ ]);
9
+ /** The period runs from the latest consent or subscription start on the stream.
10
+ * A stream with neither has no provable start and counts as expired. */
11
+ export function isWithinWithdrawalPeriod(events, now) {
12
+ let latestStart;
13
+ for (const event of events) {
14
+ if (!PERIOD_START_EVENT_TYPES.has(event.type))
15
+ continue;
16
+ if (latestStart === undefined ||
17
+ event.createdAt.epochMilliseconds > latestStart.epochMilliseconds) {
18
+ latestStart = event.createdAt;
19
+ }
20
+ }
21
+ // skip: no consent or subscription-created event, nothing proves a running period
22
+ if (latestStart === undefined)
23
+ return false;
24
+ const periodEnd = latestStart.add({ hours: WITHDRAWAL_PERIOD_DAYS * HOURS_PER_DAY });
25
+ return now.epochMilliseconds <= periodEnd.epochMilliseconds;
26
+ }
@@ -21,10 +21,10 @@
21
21
  // billing-foundation:write:switch-plan instead. Plan-driven callers should
22
22
  // prefer start-plan-checkout/switch-plan over calling this handler with a
23
23
  // hand-picked priceId.
24
- import { UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
24
+ import { InternalError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
25
25
  import * as z from "zod";
26
26
  import { assertCheckoutAllowed, openCheckout, startProviderCheckout, } from "../checkout-core.js";
27
- import { consentCheckoutFields, consentPayloadSchema, prepareConsent, recordConsent, } from "../consumer-protection/consent-gate.js";
27
+ import { assertValidOrderItem, consentCheckoutFields, consentPayloadSchema, prepareConsent, recordConsent, } from "../consumer-protection/consent-gate.js";
28
28
  const createCheckoutSessionSchema = z.object({
29
29
  /** Welcher Provider — entityName eines registrierten subscription-
30
30
  * Plugins ("stripe" / "paypal" / ...). */
@@ -75,10 +75,11 @@ export function createCheckoutSessionHandler(options) {
75
75
  }
76
76
  const mode = payload.mode ?? "subscription";
77
77
  const plugin = await assertCheckoutAllowed(ctx, openOptions, checkoutInput);
78
- const price = await lookupPrice(ctx, plugin, payload.priceId);
78
+ const price = await lookupPrice(ctx, plugin, payload.providerName, payload.priceId);
79
79
  const orderItem = mode === "payment"
80
80
  ? await consumerProtection.oneOffItemLabel?.(ctx, payload.priceId)
81
81
  : undefined;
82
+ assertValidOrderItem(orderItem, payload.priceId);
82
83
  const consent = await prepareConsent(ctx, consumerProtection, payload.consent, mode);
83
84
  const result = await startProviderCheckout(ctx, plugin, {
84
85
  ...checkoutInput,
@@ -99,10 +100,15 @@ export function createCheckoutSessionHandler(options) {
99
100
  }
100
101
  // The price is part of the consent record, so it must be known before the
101
102
  // buyer is sent to the provider.
102
- async function lookupPrice(ctx, plugin, priceId) {
103
+ async function lookupPrice(ctx, plugin, providerName, priceId) {
104
+ if (!plugin.retrievePrices) {
105
+ throw new InternalError({
106
+ message: `billing-foundation: provider "${providerName}" lacks retrievePrices, required by consumerProtection`,
107
+ });
108
+ }
103
109
  let price;
104
110
  try {
105
- const prices = (await plugin.retrievePrices?.(ctx, [priceId])) ?? [];
111
+ const prices = await plugin.retrievePrices(ctx, [priceId]);
106
112
  price = prices.find((p) => p.priceId === priceId);
107
113
  }
108
114
  catch (cause) {