@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
@@ -14,7 +14,7 @@
14
14
  import { configuredPiiSubjectKms, encryptPiiFieldValues, } from "@cosmicdrift/kumiko-framework/crypto";
15
15
  import * as z from "zod";
16
16
  import { subscriptionAggregateId } from "../aggregate-id.js";
17
- import { isSubscriptionBlockingCheckout, SubscriptionEventTypes, SubscriptionStatuses, } from "../constants.js";
17
+ import { isSubscriptionBlockingCheckout, isSwitchableSubscriptionStatus, SubscriptionEventTypes, SubscriptionStatuses, } from "../constants.js";
18
18
  import { SUBSCRIPTION_PII_FIELDS, subscriptionEntity } from "../entities.js";
19
19
  import { INVOICE_PAID_EVENT_QN, INVOICE_PAYMENT_FAILED_EVENT_QN, SUBSCRIPTION_AGGREGATE_TYPE, SUBSCRIPTION_CANCELED_EVENT_QN, SUBSCRIPTION_CREATED_EVENT_QN, SUBSCRIPTION_UPDATED_EVENT_QN, } from "../events.js";
20
20
  import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
@@ -154,16 +154,20 @@ export async function appendSubscriptionEvent(ctx, tenantId, payload) {
154
154
  // Tenant-Admin direkt.
155
155
  // One subscription aggregate exists per tenant, but a tenant can legitimately hold two provider
156
156
  // subscriptions over time (a stale `incomplete` one superseded by a fresh checkout). A late or
157
- // retried event of the old subscription must not overwrite the row of the live one, so only a
158
- // `created` event may replace a blocking row that belongs to a different provider subscription.
157
+ // retried event of the old subscription must not overwrite the row of the live one, so an event of a
158
+ // different provider subscription only replaces a row that no longer blocks a checkout. The exception
159
+ // is a `created` of a subscription that is itself live (a provider switch); a late `created` of a
160
+ // never-completed `incomplete` one stays stale, since delivery order is not guaranteed.
159
161
  function isStaleSubscriptionEvent(current, payload, now) {
160
162
  if (current === null)
161
163
  return false;
162
- if (payload.type === SubscriptionEventTypes.created)
163
- return false;
164
164
  const sameSubscription = current.providerName === payload.providerName &&
165
165
  current.providerSubscriptionId === payload.providerSubscriptionId;
166
- return !sameSubscription && isSubscriptionBlockingCheckout(current, now);
166
+ if (sameSubscription || !isSubscriptionBlockingCheckout(current, now))
167
+ return false;
168
+ const supersedesCurrent = payload.type === SubscriptionEventTypes.created &&
169
+ isSwitchableSubscriptionStatus(payload.status);
170
+ return !supersedesCurrent;
167
171
  }
168
172
  export function createProcessEventHandler(now) {
169
173
  return {
@@ -1,4 +1,10 @@
1
1
  export const BILLING_FOUNDATION_I18N = {
2
+ "billing-foundation:feature.label": { en: "Billing" },
3
+ "billing-foundation:notify.contract-confirmation.label": { en: "Contract confirmation" },
4
+ "billing-foundation:notify.contract-termination-receipt.label": { en: "Termination receipt" },
5
+ "billing-foundation:notify.contract-termination-operator-notice.label": {
6
+ en: "Termination notice to operator",
7
+ },
2
8
  "screen:billing-plans.title": { en: "Billing" },
3
9
  "billing-foundation.plans.billingDisabled": {
4
10
  en: "Billing is not active yet. Your current plan stays in place.",
@@ -90,6 +96,9 @@ export const BILLING_FOUNDATION_I18N = {
90
96
  "billing-foundation.errors.redirectOriginNotAllowed": {
91
97
  en: "This redirect URL is not allowed for this app.",
92
98
  },
99
+ "billing-foundation.errors.providerCustomerUnknown": {
100
+ en: "The billing account for this subscription no longer exists. Subscribe again to continue.",
101
+ },
93
102
  "billing-foundation.errors.providerHasNoPriceCatalog": {
94
103
  en: "This provider has no known prices configured.",
95
104
  },
@@ -1,11 +1,17 @@
1
- import type { ExtraRouteDefinition } from "@cosmicdrift/kumiko-framework/api";
1
+ import type { ExtraRouteDefinition, SignatureExtraRouteDeps } from "@cosmicdrift/kumiko-framework/api";
2
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
3
  export declare const SUBSCRIPTION_WEBHOOK_PATH = "/webhooks/subscription/:providerName";
3
4
  export type SubscriptionTierSyncDeps<TTier extends string> = {
4
5
  readonly isTierName: (value: string) => value is TTier;
5
6
  readonly defaultTier: TTier;
6
7
  readonly onSyncError?: "log" | "fail-webhook";
7
8
  };
9
+ export declare const TIER_SYNC_MANUAL_GRANT_SKIPPED = "tier_sync_manual_grant_skipped";
8
10
  export declare function effectiveTierFromSubscription<TTier extends string>(status: string | undefined, tier: string | undefined, isTierName: (value: string) => value is TTier, defaultTier: TTier): TTier;
9
11
  export declare function createSubscriptionTierSync<TTier extends string>(deps: SubscriptionTierSyncDeps<TTier>): {
10
12
  createWebhookRoute: () => ExtraRouteDefinition;
13
+ syncTierFromSubscription: (tenantId: TenantId, routeDeps: Pick<SignatureExtraRouteDeps, "dispatchSystemQuery" | "dispatchSystemWrite">) => Promise<{
14
+ code: string;
15
+ message: string;
16
+ } | null>;
11
17
  };
@@ -2,7 +2,7 @@
2
2
  // near-identical webhook-route.ts app copies in show-pony and publicstatus
3
3
  // (infra#446) — the only per-app variables were the TierName union/default,
4
4
  // so it is now a factory parameter.
5
- import { TierAssignmentSources, TierEngineHandlers, TierEngineQueries, tierAssignmentAggregateId, } from "@cosmicdrift/kumiko-bundled-features/tier-engine";
5
+ import { isTierAssignmentSource, TierAssignmentSources, TierEngineHandlers, TierEngineQueries, tierAssignmentAggregateId, } from "@cosmicdrift/kumiko-bundled-features/tier-engine";
6
6
  import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
7
7
  import { subscriptionAggregateId } from "./aggregate-id.js";
8
8
  import { SubscriptionFoundationQueries, SubscriptionStatuses } from "./constants.js";
@@ -10,6 +10,9 @@ import { createSubscriptionWebhookRoute } from "./webhook-handler.js";
10
10
  // Outside /api — signature routes carry their own auth (verify()) and are
11
11
  // not JWT-guarded, so provider dashboards (Stripe, PayPal, ...) point here.
12
12
  export const SUBSCRIPTION_WEBHOOK_PATH = "/webhooks/subscription/:providerName";
13
+ // A manual grant that differs from what billing would assign is reported with this
14
+ // code but never fails the webhook: a provider retry cannot resolve it.
15
+ export const TIER_SYNC_MANUAL_GRANT_SKIPPED = "tier_sync_manual_grant_skipped";
13
16
  export function effectiveTierFromSubscription(status, tier, isTierName, defaultTier) {
14
17
  const usable = status === SubscriptionStatuses.active || status === SubscriptionStatuses.trialing;
15
18
  return usable && typeof tier === "string" && isTierName(tier) ? tier : defaultTier;
@@ -29,7 +32,13 @@ function findTierAssignment(rows, tenantId) {
29
32
  if (!row || typeof row["id"] !== "string" || typeof row["version"] !== "number") {
30
33
  return undefined;
31
34
  }
32
- return { id: row["id"], version: row["version"], tier: row["tier"], source: row["source"] };
35
+ const source = row["source"];
36
+ return {
37
+ id: row["id"],
38
+ version: row["version"],
39
+ tier: row["tier"],
40
+ source: isTierAssignmentSource(source) ? source : null,
41
+ };
33
42
  }
34
43
  export function createSubscriptionTierSync(deps) {
35
44
  async function syncTierFromSubscription(tenantId, routeDeps) {
@@ -68,10 +77,14 @@ export function createSubscriptionTierSync(deps) {
68
77
  }
69
78
  return null;
70
79
  }
71
- if (assignment.source === TierAssignmentSources.manual)
72
- return null;
73
80
  if (assignment.tier === effective)
74
81
  return null;
82
+ if (assignment.source === TierAssignmentSources.manual) {
83
+ return {
84
+ code: TIER_SYNC_MANUAL_GRANT_SKIPPED,
85
+ message: `manual tier grant "${String(assignment.tier)}" kept although the subscription yields "${effective}"`,
86
+ };
87
+ }
75
88
  const result = await routeDeps.dispatchSystemWrite({
76
89
  handlerQn: TierEngineHandlers.update,
77
90
  payload: {
@@ -108,8 +121,9 @@ export function createSubscriptionTierSync(deps) {
108
121
  const syncError = await syncTierFromSubscription(tenantId, routeDeps);
109
122
  if (syncError) {
110
123
  // biome-ignore lint/suspicious/noConsole: operator visibility for a post-commit sync failure
111
- console.warn(`[subscription-tier-sync] tier sync failed for tenant ${tenantId} after successful webhook write: ${syncError.code} ${syncError.message}`);
112
- if (deps.onSyncError === "fail-webhook") {
124
+ console.warn(`[subscription-tier-sync] tier sync for tenant ${tenantId} after successful webhook write: ${syncError.code} ${syncError.message}`);
125
+ if (deps.onSyncError === "fail-webhook" &&
126
+ syncError.code !== TIER_SYNC_MANUAL_GRANT_SKIPPED) {
113
127
  return {
114
128
  isSuccess: false,
115
129
  error: {
@@ -125,5 +139,5 @@ export function createSubscriptionTierSync(deps) {
125
139
  },
126
140
  });
127
141
  }
128
- return { createWebhookRoute };
142
+ return { createWebhookRoute, syncTierFromSubscription };
129
143
  }
@@ -289,7 +289,10 @@ export type LegalLinkSet = {
289
289
  export declare function isLegalLinkSet(links: LegalLinkSet | Readonly<Record<ConsentLocale, LegalLinkSet>>): links is LegalLinkSet;
290
290
  /** Consumer-protection consent gate (§ 312j / § 356 BGB style). When set,
291
291
  * `start-plan-checkout` and `create-checkout-session` require a `consent`
292
- * payload and record it as an event before the buyer reaches the provider. */
292
+ * payload and record it as an event before the buyer reaches the provider.
293
+ * Every provider used with `create-checkout-session` must implement
294
+ * `retrievePrices`: the live price is part of the consent record, and a
295
+ * provider without it fails the checkout with an InternalError. */
293
296
  export type ConsumerProtectionOptions = {
294
297
  /** Slug of the template-resolver `text-block` holding the terms text; its
295
298
  * content hash is stored with every recorded consent. */
@@ -304,7 +307,9 @@ export type ConsumerProtectionOptions = {
304
307
  readonly terminationScope?: TerminationScope;
305
308
  /** Names the item of a one-off payment (mode "payment") for the consent record
306
309
  * and the confirmation mail; the label key is resolved through the registry
307
- * i18n, so it must be qualified with the registering feature. */
310
+ * i18n, so it must be qualified with the registering feature. The label key
311
+ * is 1-200 characters, string params at most 100; a violation fails the
312
+ * checkout with an InternalError before the provider session is created. */
308
313
  readonly oneOffItemLabel?: (ctx: HandlerContext, priceId: string) => Promise<OrderItem | undefined>;
309
314
  /** Root-relative paths or absolute https URLs. One set for every locale, or
310
315
  * one set per consent locale (`de` and `en` required). */
@@ -173,6 +173,11 @@ export async function markCapSoftWarned(ctx, options) {
173
173
  }
174
174
  return retryCounterWriteOnVersionConflict(() => attemptMark(writeDb));
175
175
  }
176
+ // A release is written up to a reservation TTL after its increment; once the increment left the
177
+ // window the release must not subtract from other increments still inside it.
178
+ function isBefore(isoInstant, cutoff) {
179
+ return Temporal.Instant.compare(Temporal.Instant.from(isoInstant), cutoff) < 0;
180
+ }
176
181
  async function sumRollingEventAmounts(db, tenantId, options, type) {
177
182
  const aggregateId = rollingCapAggregateId(tenantId, options.capName);
178
183
  const cutoff = Temporal.Now.instant().subtract({ hours: options.windowDays * 24 });
@@ -189,6 +194,9 @@ async function sumRollingEventAmounts(db, tenantId, options, type) {
189
194
  // unknown by drizzle's $type<Record<string,unknown>>); narrowing the
190
195
  // shape here mirrors enforceRollingCap's own read-side contract.
191
196
  const payload = row["payload"];
197
+ if (typeof payload.incrementedAt === "string" && isBefore(payload.incrementedAt, cutoff)) {
198
+ continue;
199
+ }
192
200
  if (typeof payload.amount === "number") {
193
201
  total += payload.amount;
194
202
  }
@@ -12,6 +12,8 @@ export async function reserveEnforcedCalendarCap(ctx, cap) {
12
12
  profile: cap.profile,
13
13
  notify: cap.notify,
14
14
  amount: cap.amount,
15
+ ...(cap.periodEndIso !== undefined && { periodEndIso: cap.periodEndIso }),
16
+ ...(cap.i18nKey !== undefined && { i18nKey: cap.i18nKey }),
15
17
  markSoftWarnedOutsideTransaction: outsideTransaction,
16
18
  });
17
19
  }
@@ -61,13 +61,13 @@ export async function reserveCalendarCap(ctx, options, outsideTransaction = true
61
61
  counterValue: valueBeforeBooking + options.amount,
62
62
  };
63
63
  }
64
- function rollingEvent(user, capName, amount, expectedVersion, type) {
64
+ function rollingEvent(user, capName, amount, expectedVersion, type, extraPayload = {}) {
65
65
  return {
66
66
  aggregateId: rollingCapAggregateId(user.tenantId, capName),
67
67
  aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
68
68
  expectedVersion,
69
69
  type,
70
- payload: { capName, amount },
70
+ payload: { capName, amount, ...extraPayload },
71
71
  metadata: { userId: user.id },
72
72
  };
73
73
  }
@@ -104,7 +104,11 @@ async function giveBackCalendar(txDb, user, row) {
104
104
  }
105
105
  async function giveBackRolling(txDb, user, row) {
106
106
  const version = await getStreamVersionInTenantDb(txDb, rollingCapAggregateId(user.tenantId, row.capName));
107
- await appendEventInTenantDb(txDb, rollingEvent(user, row.capName, row.amount, version, ROLLING_RELEASED_EVENT_QN));
107
+ await appendEventInTenantDb(txDb,
108
+ // The window filter must use the increment's time: a sweep releases up to a TTL later.
109
+ rollingEvent(user, row.capName, row.amount, version, ROLLING_RELEASED_EVENT_QN, {
110
+ incrementedAt: row.createdAt.toString(),
111
+ }));
108
112
  return NOTHING_TO_RELEASE;
109
113
  }
110
114
  async function giveBackReservation(txDb, user, id) {
@@ -57,6 +57,9 @@ export declare function assertBelowHardCap(valueSeenByLastUnit: number, cap: {
57
57
  readonly capName: string;
58
58
  readonly limit: number;
59
59
  readonly profile: CapToleranceProfileName;
60
+ readonly periodStartIso?: string;
61
+ readonly periodEndIso?: string;
62
+ readonly i18nKey?: string;
60
63
  }): void;
61
64
  /**
62
65
  * Synchronous read-and-check of the calling tenant's counter for
@@ -85,6 +88,8 @@ export declare function enforceCap(ctx: CapCounterContext, options: {
85
88
  readonly limit: number;
86
89
  readonly profile: CapToleranceProfileName;
87
90
  readonly amount?: number;
91
+ readonly periodEndIso?: string;
92
+ readonly i18nKey?: string;
88
93
  }): Promise<EnforceCapResult>;
89
94
  /**
90
95
  * Synchronous read-and-check of the calling tenant's Rolling-Window-
@@ -120,6 +125,9 @@ export declare function enforceRollingCap(ctx: CapCounterContext, options: {
120
125
  * Thrown by enforceCap/enforceRollingCap when value ≥ hard-threshold.
121
126
  * Extends `KumikoError` — the dispatcher auto-maps this to HTTP 429
122
127
  * with `code: "cap_exceeded"`, no caller-side catch needed.
128
+ *
129
+ * Calendar caps carry `resetAt` (end of the period) and `retryAfterSeconds`, which the API
130
+ * answers as Retry-After. Rolling caps have no fixed reset instant, so both stay undefined.
123
131
  */
124
132
  export declare class CapExceededError extends KumikoError {
125
133
  readonly capName: string;
@@ -128,7 +136,13 @@ export declare class CapExceededError extends KumikoError {
128
136
  readonly tolerance: CapToleranceProfile;
129
137
  readonly code: "cap_exceeded";
130
138
  readonly httpStatus = 429;
131
- constructor(capName: string, limit: number, currentValue: number, tolerance: CapToleranceProfile);
139
+ readonly resetAt: string | undefined;
140
+ constructor(capName: string, limit: number, currentValue: number, tolerance: CapToleranceProfile, options?: {
141
+ readonly resetAt?: string;
142
+ readonly i18nKey?: string;
143
+ readonly cause?: Error;
144
+ readonly now?: Temporal.Instant;
145
+ });
132
146
  }
133
147
  /**
134
148
  * Calendar-month period start in UTC. Use this for monthly caps
@@ -137,6 +151,7 @@ export declare class CapExceededError extends KumikoError {
137
151
  * Returns ISO string for the 1st of the current month at 00:00 UTC.
138
152
  */
139
153
  export declare function currentCalendarMonthStartIso(now?: Temporal.Instant): string;
154
+ export declare function calendarMonthPeriodEndIso(periodStartIso: string): string;
140
155
  /**
141
156
  * Soft-hit-notifier callback. Caller liefert die Funktion die ein
142
157
  * delivery-event emittet (z.B. `delivery.send({to, template, payload})`).
@@ -169,6 +184,8 @@ export declare function enforceCapAndMaybeNotify(ctx: CapCounterContext, options
169
184
  readonly profile: CapToleranceProfileName;
170
185
  readonly notify: SoftHitNotifier;
171
186
  readonly amount?: number;
187
+ readonly periodEndIso?: string;
188
+ readonly i18nKey?: string;
172
189
  readonly markSoftWarnedOutsideTransaction?: boolean;
173
190
  }): Promise<EnforceCapResult>;
174
191
  /**
@@ -17,8 +17,24 @@ export const CAP_TOLERANCES = {
17
17
  export function assertBelowHardCap(valueSeenByLastUnit, cap) {
18
18
  const tolerance = CAP_TOLERANCES[cap.profile];
19
19
  if (valueSeenByLastUnit >= cap.limit * tolerance.hard) {
20
- throw new CapExceededError(cap.capName, cap.limit, valueSeenByLastUnit, tolerance);
20
+ const resetAt = resolvePeriodEndIso(cap.periodStartIso, cap.periodEndIso);
21
+ throw new CapExceededError(cap.capName, cap.limit, valueSeenByLastUnit, tolerance, {
22
+ ...(resetAt !== undefined && { resetAt }),
23
+ ...(cap.i18nKey !== undefined && { i18nKey: cap.i18nKey }),
24
+ });
25
+ }
26
+ }
27
+ function resolvePeriodEndIso(periodStartIso, periodEndIso) {
28
+ if (periodStartIso === undefined)
29
+ return periodEndIso;
30
+ if (periodEndIso === undefined)
31
+ return calendarMonthPeriodEndIso(periodStartIso);
32
+ const start = Temporal.Instant.from(periodStartIso);
33
+ const end = Temporal.Instant.from(periodEndIso);
34
+ if (Temporal.Instant.compare(end, start) <= 0) {
35
+ throw new Error(`cap-counter: periodEndIso (${periodEndIso}) must be after periodStartIso (${periodStartIso})`);
21
36
  }
37
+ return end.toString();
22
38
  }
23
39
  // =============================================================================
24
40
  // Enforce-Cap helper
@@ -115,6 +131,9 @@ export async function enforceRollingCap(ctx, options) {
115
131
  * Thrown by enforceCap/enforceRollingCap when value ≥ hard-threshold.
116
132
  * Extends `KumikoError` — the dispatcher auto-maps this to HTTP 429
117
133
  * with `code: "cap_exceeded"`, no caller-side catch needed.
134
+ *
135
+ * Calendar caps carry `resetAt` (end of the period) and `retryAfterSeconds`, which the API
136
+ * answers as Retry-After. Rolling caps have no fixed reset instant, so both stay undefined.
118
137
  */
119
138
  export class CapExceededError extends KumikoError {
120
139
  capName;
@@ -123,17 +142,32 @@ export class CapExceededError extends KumikoError {
123
142
  tolerance;
124
143
  code = "cap_exceeded";
125
144
  httpStatus = 429;
126
- constructor(capName, limit, currentValue, tolerance) {
145
+ resetAt;
146
+ constructor(capName, limit, currentValue, tolerance, options) {
147
+ const resetAt = options?.resetAt;
148
+ const retryAfterSeconds = resetAt === undefined
149
+ ? undefined
150
+ : Math.max(0, Math.ceil(Temporal.Instant.from(resetAt).epochMilliseconds / 1000 -
151
+ (options?.now ?? Temporal.Now.instant()).epochMilliseconds / 1000));
127
152
  super({
128
- message: `Cap "${capName}" exceeded: current=${currentValue}, limit=${limit}, hard-threshold=${limit * tolerance.hard}. Upgrade tier or wait for next period reset.`,
129
- i18nKey: "errors.cap.exceeded",
130
- i18nParams: { capName, limit, currentValue },
131
- details: { capName, limit, currentValue, tolerance },
153
+ message: `Cap "${capName}" exceeded: current=${currentValue}, limit=${limit}, hard-threshold=${limit * tolerance.hard}. Upgrade tier or wait for next period reset.${resetAt === undefined ? "" : ` Resets at ${resetAt}.`}`,
154
+ i18nKey: options?.i18nKey ?? "errors.cap.exceeded",
155
+ i18nParams: { capName, limit, currentValue, ...(resetAt !== undefined && { resetAt }) },
156
+ details: {
157
+ capName,
158
+ limit,
159
+ currentValue,
160
+ tolerance,
161
+ ...(resetAt !== undefined && { resetAt, retryAfterSeconds }),
162
+ },
163
+ ...(retryAfterSeconds !== undefined && { retryAfterSeconds }),
164
+ ...(options?.cause && { cause: options.cause }),
132
165
  });
133
166
  this.capName = capName;
134
167
  this.limit = limit;
135
168
  this.currentValue = currentValue;
136
169
  this.tolerance = tolerance;
170
+ this.resetAt = resetAt;
137
171
  }
138
172
  }
139
173
  // =============================================================================
@@ -158,6 +192,13 @@ export function currentCalendarMonthStartIso(now = Temporal.Now.instant()) {
158
192
  });
159
193
  return start.toInstant().toString();
160
194
  }
195
+ export function calendarMonthPeriodEndIso(periodStartIso) {
196
+ return Temporal.Instant.from(periodStartIso)
197
+ .toZonedDateTimeISO("UTC")
198
+ .add({ months: 1 })
199
+ .toInstant()
200
+ .toString();
201
+ }
161
202
  /**
162
203
  * Calendar-Period-enforcement + automatische soft-hit-Notification.
163
204
  * Ruft `enforceCap`, bei `crossed: true` den Notifier UND
@@ -175,6 +216,8 @@ export async function enforceCapAndMaybeNotify(ctx, options) {
175
216
  limit: options.limit,
176
217
  profile: options.profile,
177
218
  ...(options.amount !== undefined && { amount: options.amount }),
219
+ ...(options.periodEndIso !== undefined && { periodEndIso: options.periodEndIso }),
220
+ ...(options.i18nKey !== undefined && { i18nKey: options.i18nKey }),
178
221
  });
179
222
  if (result.state === "soft-hit" && result.crossed) {
180
223
  await options.notify({
@@ -10,5 +10,6 @@ export declare const rollingIncrementedSchema: z.ZodObject<{
10
10
  export declare const rollingReleasedSchema: z.ZodObject<{
11
11
  capName: z.ZodString;
12
12
  amount: z.ZodNumber;
13
+ incrementedAt: z.ZodOptional<z.ZodString>;
13
14
  }, z.core.$strip>;
14
15
  export declare const incrementRollingCapHandler: WriteHandlerDef;
@@ -34,6 +34,8 @@ export const rollingIncrementedSchema = incrementRollingSchema;
34
34
  /** Payload of the event that gives a reserved rolling amount back. */
35
35
  export const rollingReleasedSchema = incrementRollingSchema.extend({
36
36
  amount: z.number().int().positive(),
37
+ /** When the released increment was booked; absent on events written before it was recorded. */
38
+ incrementedAt: z.string().optional(),
37
39
  });
38
40
  // Rolling-Increment-Handler — append-only. Race-frei: zwei parallele
39
41
  // Increments für (tenant, cap) hängen sich am selben aggregate-stream
@@ -2,7 +2,7 @@ export { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js"
2
2
  export { type BookCapUsageOptions, bookCapUsage, type MarkCapSoftWarnedOptions, markCapSoftWarned, type ReadRollingCapUsageOptions, readRollingCapUsage, } from "./book-cap-usage.js";
3
3
  export { type CalendarCapUsageReservation, type CalendarCapUsageReservationSpec, reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
4
4
  export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_QN, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
5
- export { CAP_TOLERANCES, CapExceededError, type CapToleranceProfile, type CapToleranceProfileName, currentCalendarMonthStartIso, type EnforceCapResult, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, type SoftHitNotifier, type StockCapResult, } from "./enforce-cap.js";
5
+ export { CAP_TOLERANCES, CapExceededError, type CapToleranceProfile, type CapToleranceProfileName, calendarMonthPeriodEndIso, currentCalendarMonthStartIso, type EnforceCapResult, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, type SoftHitNotifier, type StockCapResult, } from "./enforce-cap.js";
6
6
  export { capCounterEntity } from "./entity.js";
7
7
  export { capCounterFeature } from "./feature.js";
8
8
  export { readPayloadField } from "./payload-field.js";
@@ -3,7 +3,7 @@ export { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js"
3
3
  export { bookCapUsage, markCapSoftWarned, readRollingCapUsage, } from "./book-cap-usage.js";
4
4
  export { reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
5
5
  export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_QN, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
6
- export { CAP_TOLERANCES, CapExceededError, currentCalendarMonthStartIso, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, } from "./enforce-cap.js";
6
+ export { CAP_TOLERANCES, CapExceededError, calendarMonthPeriodEndIso, currentCalendarMonthStartIso, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, } from "./enforce-cap.js";
7
7
  export { capCounterEntity } from "./entity.js";
8
8
  export { capCounterFeature } from "./feature.js";
9
9
  export { readPayloadField } from "./payload-field.js";
@@ -10,6 +10,8 @@ import { type CapToleranceProfileName, type SoftHitNotifier } from "./enforce-ca
10
10
  export type CalendarCapDef = {
11
11
  readonly capName: string;
12
12
  readonly periodStartIso: string;
13
+ readonly periodEndIso?: string;
14
+ readonly i18nKey?: string;
13
15
  readonly limit: number;
14
16
  readonly profile: CapToleranceProfileName;
15
17
  /** Increment-amount post-success. Default 1. */
@@ -116,7 +116,9 @@ export function createTenantCapsListQuery(caps, listCaps) {
116
116
  });
117
117
  }
118
118
  const db = ctx.systemDb.acknowledgeCrossTenant("cap-overview:tenant-caps:list — SystemAdmin platform-wide tenant overview");
119
- const tenants = await decryptTenantNames(await selectMany(db, tenantTable, {}), "cap-overview:tenant-caps:list");
119
+ const tenants = await decryptTenantNames(
120
+ // Destroyed tenants decrypt to the erased sentinel, which a picker or list would show verbatim.
121
+ await selectMany(db, tenantTable, { destroyedAt: null }), "cap-overview:tenant-caps:list");
120
122
  const assignments = await selectMany(db, tierAssignmentTable, {});
121
123
  const subscriptions = await selectMany(db, subscriptionsProjectionTable, {});
122
124
  const assignmentByTenant = new Map(assignments.map((row) => [row.tenantId, row]));
@@ -15,7 +15,9 @@ export const tenantOptionsQuery = defineQueryHandler({
15
15
  });
16
16
  }
17
17
  const db = ctx.systemDb.acknowledgeCrossTenant("cap-overview:tenant-options — SystemAdmin dashboard tenant-filter options");
18
- const tenants = await decryptTenantNames(await selectMany(db, tenantTable, {}), "cap-overview:tenant-options");
18
+ const tenants = await decryptTenantNames(
19
+ // Destroyed tenants decrypt to the erased sentinel, which the picker would show verbatim.
20
+ await selectMany(db, tenantTable, { destroyedAt: null }), "cap-overview:tenant-options");
19
21
  const sorted = [...tenants].sort((a, b) => a.name.localeCompare(b.name));
20
22
  return { rows: sorted.map((tenant) => ({ value: tenant.id, label: tenant.name })) };
21
23
  },
@@ -1,7 +1,8 @@
1
1
  import { chatMessageText, createChatWebhookChannel, truncateChars, } from "../delivery/index.js";
2
+ const DISCORD_WEBHOOK_PREFIX = "channel-discord:webhooks.";
2
3
  export const DISCORD_SECRET_KEYS = {
3
- webhookPrefix: "channel-discord:webhooks.",
4
- webhookKeyFor: (connection) => `channel-discord:webhooks.${connection}`,
4
+ webhookPrefix: DISCORD_WEBHOOK_PREFIX,
5
+ webhookKeyFor: (connection) => `${DISCORD_WEBHOOK_PREFIX}${connection}`,
5
6
  };
6
7
  export const DISCORD_DEFAULT_ALLOWED_HOSTS = ["discord.com", "discordapp.com"];
7
8
  export const DISCORD_WEBHOOK_PATH_PREFIX = "/api/webhooks/";
@@ -1,7 +1,8 @@
1
1
  import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
2
+ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
2
3
  import * as z from "zod";
3
4
  import { DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE, DELIVERY_UNSUBSCRIBE_PATH, EMAIL_CHANNEL_NAME, fileAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, renderedPdfAttachmentRefSchema, } from "../delivery/index.js";
4
- import { decryptStoredPii } from "../shared/index.js";
5
+ import { decryptStoredPii, extensionForResourceContentType } from "../shared/index.js";
5
6
  import { guardEmailMessage } from "./pii-guard.js";
6
7
  // Envelope (From / Reply-To / threading headers) rides on the channel data —
7
8
  // the notification's email template echoes it out of the notify() call, since
@@ -22,7 +23,8 @@ const UNSUBSCRIBE_HEADER_NAMES = new Set([
22
23
  "list-unsubscribe",
23
24
  "list-unsubscribe-post",
24
25
  ]);
25
- // Only the framework's own unsubscribe route can honor a one-click POST.
26
+ // Only the framework's own unsubscribe route can honor a one-click POST; an app's
27
+ // own unsubscribe page still gets the plain List-Unsubscribe link (RFC 2369).
26
28
  function listUnsubscribeHeaders(data) {
27
29
  const rawUrl = data?.["unsubscribeUrl"];
28
30
  if (typeof rawUrl !== "string")
@@ -36,8 +38,9 @@ function listUnsubscribeHeaders(data) {
36
38
  }
37
39
  if (url.protocol !== "https:" && url.protocol !== "http:")
38
40
  return undefined;
39
- if (url.pathname !== DELIVERY_UNSUBSCRIBE_PATH)
40
- return undefined;
41
+ if (url.pathname !== DELIVERY_UNSUBSCRIBE_PATH) {
42
+ return { "List-Unsubscribe": `<${url.toString()}>` };
43
+ }
41
44
  return {
42
45
  "List-Unsubscribe": `<${url.toString()}>`,
43
46
  "List-Unsubscribe-Post": DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE,
@@ -81,15 +84,13 @@ export function fileAttachmentsFrom(data, messageLocale) {
81
84
  ? { ...ref, locale: messageLocale }
82
85
  : ref);
83
86
  }
84
- const INLINE_EXTENSION_BY_CONTENT_TYPE = {
85
- "image/png": "png",
86
- "image/jpeg": "jpg",
87
- "image/gif": "gif",
88
- "image/webp": "webp",
89
- };
87
+ function inlineFileName(cid, contentType) {
88
+ const extension = extensionForResourceContentType(contentType);
89
+ return extension === undefined ? cid : `${cid}.${extension}`;
90
+ }
90
91
  async function resolveAttachments(sources, ctx) {
91
92
  if (sources.length === 0)
92
- return [];
93
+ return { kind: "resolved", attachments: [] };
93
94
  if (ctx.readAttachment === undefined) {
94
95
  throw new Error("email channel: the message has attachments but the channel context cannot read files");
95
96
  }
@@ -113,16 +114,12 @@ async function resolveAttachments(sources, ctx) {
113
114
  // A missing inline image only degrades the look; a missing file is content the recipient expects.
114
115
  if (ref.kind === "inline")
115
116
  continue;
116
- throw new Error(`email channel: attachment file ${ref.fileRefId} was not found`);
117
+ return { kind: "missing-file", fileRefId: ref.fileRefId };
117
118
  }
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;
119
+ const fileName = ref.kind === "file" ? ref.fileName : inlineFileName(ref.cid, found.contentType);
123
120
  resolved.push({ ...ref, fileName, content: found.content, contentType: found.contentType });
124
121
  }
125
- return resolved;
122
+ return { kind: "resolved", attachments: resolved };
126
123
  }
127
124
  export function createEmailChannel(options) {
128
125
  const { transport, resolveEmail } = options;
@@ -138,13 +135,20 @@ export function createEmailChannel(options) {
138
135
  throw new Error(`email channel: notification "${notificationType}" has a mail template but the channel context carries no renderMail`);
139
136
  }
140
137
  const unsubscribeUrl = message.data?.["unsubscribeUrl"];
141
- const { subject, html, text, attachments } = await ctx.renderMail({
138
+ const { subject, html, text, attachments, fromName, replyTo } = await ctx.renderMail({
142
139
  slug: notificationType,
143
140
  locale: message.locale ?? "en",
144
141
  variables: message.data ?? {},
145
142
  ...(typeof unsubscribeUrl === "string" && { unsubscribeUrl }),
146
143
  });
147
- return { html, subject, text, ...(attachments !== undefined && { attachments }) };
144
+ return {
145
+ html,
146
+ subject,
147
+ text,
148
+ ...(fromName !== undefined && { fromName }),
149
+ ...(replyTo !== undefined && { replyTo }),
150
+ ...(attachments !== undefined && { attachments }),
151
+ };
148
152
  }
149
153
  return {
150
154
  name: EMAIL_CHANNEL_NAME,
@@ -161,12 +165,25 @@ export function createEmailChannel(options) {
161
165
  return renderMessage(message, ctx);
162
166
  },
163
167
  async send(address, message, ctx, rendered) {
164
- const { html, subject, text, attachments } = rendered ?? (await renderMessage(message, ctx));
165
- const envelope = emailEnvelopeFrom(message.data);
166
- const resolvedAttachments = await resolveAttachments([
168
+ const { html, subject, text, attachments, fromName, replyTo } = rendered ?? (await renderMessage(message, ctx));
169
+ // Template-level sender defaults; notify data wins.
170
+ const envelope = {
171
+ ...(fromName && { fromName }),
172
+ ...(replyTo && { replyTo }),
173
+ ...emailEnvelopeFrom(message.data),
174
+ };
175
+ const resolution = await resolveAttachments([
167
176
  ...(attachments ?? []).map((ref) => ({ ref, origin: "template" })),
168
177
  ...fileAttachmentsFrom(message.data, message.locale).map((ref) => ({ ref, origin: "message" })),
169
178
  ], ctx);
179
+ if (resolution.kind === "missing-file") {
180
+ // A missing file is permanent, a retry cannot bring it back.
181
+ createFallbackLogger("delivery").warn("mail attachment file not found, send failed", {
182
+ fileRefId: resolution.fileRefId,
183
+ });
184
+ return { status: "failed", error: "send_failed", address };
185
+ }
186
+ const resolvedAttachments = resolution.attachments;
170
187
  await transport.send(guardEmailMessage({
171
188
  to: address,
172
189
  subject,
@@ -21,11 +21,16 @@ function isControlCharacter(char) {
21
21
  const code = char.charCodeAt(0);
22
22
  return code <= 0x1f || code === 0x7f;
23
23
  }
24
+ // Mail clients often show only the display name: an embedded address ("<", ">", "@") would
25
+ // pass for a foreign sender on the platform's own SPF/DKIM-signed domain.
26
+ function isUnsafeDisplayNameCharacter(char) {
27
+ return isControlCharacter(char) || char === "<" || char === ">" || char === "@";
28
+ }
24
29
  // Control characters in a display name would let a caller inject headers.
25
30
  function sanitizedDisplayName(name) {
26
31
  const cleaned = name
27
32
  ?.split("")
28
- .map((char) => (isControlCharacter(char) ? " " : char))
33
+ .map((char) => (isUnsafeDisplayNameCharacter(char) ? " " : char))
29
34
  .join("")
30
35
  .trim();
31
36
  return cleaned === undefined || cleaned === "" ? undefined : cleaned;
@@ -1,4 +1,4 @@
1
1
  import * as z from "zod";
2
2
  export declare const markAllReadWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"markAllRead", z.ZodObject<{}, z.core.$strip>, {
3
3
  marked: number;
4
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
4
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -3,4 +3,4 @@ export declare const markReadWrite: import("@cosmicdrift/kumiko-types/define-han
3
3
  id: z.ZodNumber;
4
4
  }, z.core.$strip>, {
5
5
  id: number | undefined;
6
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
6
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;