@cosmicdrift/kumiko-samples 0.367.0 → 0.369.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 (406) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +16 -2
  3. package/packages/bundled-features/src/audit/handlers/details.query.ts +1 -1
  4. package/packages/bundled-features/src/audit/handlers/list.query.ts +1 -1
  5. package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +5 -5
  6. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +1 -1
  7. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +10 -3
  8. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +10 -3
  9. package/packages/bundled-features/src/auth-email-password/handlers/invite-info.query.ts +1 -1
  10. package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +6 -2
  11. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
  12. package/packages/bundled-features/src/auth-email-password/seeding.ts +2 -2
  13. package/packages/bundled-features/src/auth-foundation/feature.ts +43 -34
  14. package/packages/bundled-features/src/auth-foundation/index.ts +1 -0
  15. package/packages/bundled-features/src/auth-mfa/db/queries.ts +5 -2
  16. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -1
  17. package/packages/bundled-features/src/auth-mfa/handlers/enable-start-preauth.write.ts +1 -1
  18. package/packages/bundled-features/src/auth-mfa/handlers/reencrypt.job.ts +2 -2
  19. package/packages/bundled-features/src/auth-mfa/handlers/status.query.ts +1 -1
  20. package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +1 -1
  21. package/packages/bundled-features/src/auth-mfa/mfa-code-verifier.ts +1 -1
  22. package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +1 -1
  23. package/packages/bundled-features/src/auth-mfa-user-data/index.ts +19 -10
  24. package/packages/bundled-features/src/billing-foundation/billing-info-query.ts +1 -1
  25. package/packages/bundled-features/src/billing-foundation/feature.ts +138 -135
  26. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +1 -1
  27. package/packages/bundled-features/src/billing-foundation/projection.ts +3 -2
  28. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +52 -1
  29. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +203 -15
  30. package/packages/bundled-features/src/cap-counter/changes.json +13 -0
  31. package/packages/bundled-features/src/cap-counter/feature.ts +84 -73
  32. package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +3 -4
  33. package/packages/bundled-features/src/cap-counter/index.ts +9 -1
  34. package/packages/bundled-features/src/cap-counter/tables.ts +10 -3
  35. package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +1 -2
  36. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +6 -2
  37. package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +1 -1
  38. package/packages/bundled-features/src/channel-email/changes.json +7 -0
  39. package/packages/bundled-features/src/channel-email/email-channel.ts +39 -8
  40. package/packages/bundled-features/src/channel-email/feature.ts +1 -1
  41. package/packages/bundled-features/src/channel-email/index.ts +5 -1
  42. package/packages/bundled-features/src/channel-in-app/handlers/inbox.query.ts +1 -1
  43. package/packages/bundled-features/src/channel-in-app/handlers/mark-all-read.write.ts +1 -1
  44. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +1 -1
  45. package/packages/bundled-features/src/channel-in-app/handlers/unread-count.query.ts +1 -1
  46. package/packages/bundled-features/src/channel-in-app/tables.ts +1 -1
  47. package/packages/bundled-features/src/compliance-profiles/handlers/for-tenant.query.ts +1 -1
  48. package/packages/bundled-features/src/compliance-profiles/handlers/needs-profile.query.ts +1 -1
  49. package/packages/bundled-features/src/compliance-profiles/handlers/set-profile.write.ts +1 -2
  50. package/packages/bundled-features/src/compliance-profiles/resolve-for-tenant.ts +1 -1
  51. package/packages/bundled-features/src/compliance-profiles/seeding.ts +8 -4
  52. package/packages/bundled-features/src/compliance-profiles-ops/index.ts +26 -17
  53. package/packages/bundled-features/src/config/db/queries/resolver.ts +1 -1
  54. package/packages/bundled-features/src/config/handlers/reencrypt.job.ts +2 -2
  55. package/packages/bundled-features/src/config/resolver.ts +1 -1
  56. package/packages/bundled-features/src/config/web/i18n.ts +1 -2
  57. package/packages/bundled-features/src/config/write-helpers.ts +1 -1
  58. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  59. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +9 -7
  60. package/packages/bundled-features/src/custom-fields/db/queries/field-access.ts +1 -1
  61. package/packages/bundled-features/src/custom-fields/db/queries/projection.ts +1 -1
  62. package/packages/bundled-features/src/custom-fields/db/queries/quota.ts +1 -1
  63. package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +6 -2
  64. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +1 -1
  65. package/packages/bundled-features/src/custom-fields/feature.ts +11 -14
  66. package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +2 -2
  67. package/packages/bundled-features/src/custom-fields/handlers/delete-system-field.write.ts +2 -2
  68. package/packages/bundled-features/src/custom-fields/handlers/delete-tenant-field.write.ts +2 -2
  69. package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +2 -2
  70. package/packages/bundled-features/src/custom-fields/wire-for-entity.ts +4 -4
  71. package/packages/bundled-features/src/data-retention/resolve-for-tenant.ts +1 -1
  72. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +16 -10
  73. package/packages/bundled-features/src/delivery/address-opt-out.ts +5 -2
  74. package/packages/bundled-features/src/delivery/attempt-log-retention.ts +1 -1
  75. package/packages/bundled-features/src/delivery/changes.json +6 -0
  76. package/packages/bundled-features/src/delivery/channel-context.ts +1 -1
  77. package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +1 -1
  78. package/packages/bundled-features/src/delivery/db/queries/preferences.ts +1 -1
  79. package/packages/bundled-features/src/delivery/delivery-service.ts +16 -6
  80. package/packages/bundled-features/src/delivery/feature.ts +2 -2
  81. package/packages/bundled-features/src/delivery/handlers/log.query.ts +3 -2
  82. package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +1 -1
  83. package/packages/bundled-features/src/delivery/index.ts +8 -1
  84. package/packages/bundled-features/src/delivery/unsubscribe.ts +66 -37
  85. package/packages/bundled-features/src/delivery/upsert-preference.ts +5 -2
  86. package/packages/bundled-features/src/derivatives-sharp/feature.ts +21 -12
  87. package/packages/bundled-features/src/derivatives-sharp/index.ts +1 -1
  88. package/packages/bundled-features/src/document-ingest-foundation/feature.ts +112 -103
  89. package/packages/bundled-features/src/document-ingest-foundation/forget-extract-with-file-ref.ts +3 -3
  90. package/packages/bundled-features/src/document-ingest-foundation/index.ts +4 -1
  91. package/packages/bundled-features/src/feature-toggles/global-feature-state-table.ts +1 -1
  92. package/packages/bundled-features/src/feature-toggles/handlers/set.write.ts +3 -5
  93. package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +1 -1
  94. package/packages/bundled-features/src/file-derivatives/feature.ts +134 -129
  95. package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +1 -1
  96. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  97. package/packages/bundled-features/src/file-foundation/feature.ts +38 -29
  98. package/packages/bundled-features/src/file-foundation/index.ts +1 -0
  99. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +1 -1
  100. package/packages/bundled-features/src/file-provider-inmemory/feature.ts +32 -21
  101. package/packages/bundled-features/src/file-provider-inmemory/index.ts +6 -1
  102. package/packages/bundled-features/src/file-provider-s3/changes.json +10 -1
  103. package/packages/bundled-features/src/file-provider-s3/env-feature.ts +62 -0
  104. package/packages/bundled-features/src/file-provider-s3/feature.ts +107 -73
  105. package/packages/bundled-features/src/file-provider-s3/index.ts +6 -1
  106. package/packages/bundled-features/src/file-provider-s3/lib/index.ts +3 -0
  107. package/packages/bundled-features/src/{files-provider-s3 → file-provider-s3/lib}/s3-provider.ts +19 -19
  108. package/packages/bundled-features/src/file-provider-s3-env/index.ts +7 -2
  109. package/packages/bundled-features/src/files-provider-s3/index.ts +11 -3
  110. package/packages/bundled-features/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +1 -1
  111. package/packages/bundled-features/src/files-tenant-data/hooks.ts +1 -2
  112. package/packages/bundled-features/src/folders/feature.ts +8 -3
  113. package/packages/bundled-features/src/folders-user-data/index.ts +27 -18
  114. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +1 -1
  115. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +1 -1
  116. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +3 -2
  117. package/packages/bundled-features/src/form-draft/feature.ts +6 -1
  118. package/packages/bundled-features/src/form-draft/handlers/cleanup.job.ts +2 -2
  119. package/packages/bundled-features/src/form-draft/index.ts +1 -1
  120. package/packages/bundled-features/src/form-draft/lookup.ts +1 -1
  121. package/packages/bundled-features/src/form-draft-user-data/index.ts +19 -10
  122. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +165 -154
  123. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -2
  124. package/packages/bundled-features/src/inbound-mail-foundation/index.ts +1 -1
  125. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +5 -2
  126. package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +3 -2
  127. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +1 -1
  128. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +22 -13
  129. package/packages/bundled-features/src/inbound-provider-imap/index.ts +6 -1
  130. package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +20 -12
  131. package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +1 -0
  132. package/packages/bundled-features/src/jobs/db/queries/retention.ts +1 -1
  133. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +1 -1
  134. package/packages/bundled-features/src/jobs/handlers/detail.query.ts +1 -1
  135. package/packages/bundled-features/src/jobs/handlers/list.query.ts +7 -2
  136. package/packages/bundled-features/src/jobs/handlers/retry.write.ts +1 -1
  137. package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +1 -1
  138. package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +1 -1
  139. package/packages/bundled-features/src/jobs/job-run-logger.ts +11 -10
  140. package/packages/bundled-features/src/jobs/tenant-job-failure-table.ts +1 -1
  141. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +1 -1
  142. package/packages/bundled-features/src/ledger/feature.ts +6 -3
  143. package/packages/bundled-features/src/mail-foundation/feature.ts +66 -46
  144. package/packages/bundled-features/src/mail-foundation/i18n.ts +8 -0
  145. package/packages/bundled-features/src/mail-foundation/index.ts +1 -0
  146. package/packages/bundled-features/src/mail-transport-inmemory/feature.ts +31 -17
  147. package/packages/bundled-features/src/mail-transport-inmemory/index.ts +6 -1
  148. package/packages/bundled-features/src/mail-transport-smtp/changes.json +7 -0
  149. package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +67 -0
  150. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +67 -82
  151. package/packages/bundled-features/src/mail-transport-smtp/i18n.ts +28 -0
  152. package/packages/bundled-features/src/mail-transport-smtp/index.ts +1 -0
  153. package/packages/bundled-features/src/managed-pages/handlers/by-slug.query.ts +1 -1
  154. package/packages/bundled-features/src/managed-pages/handlers/by-tenant-published.query.ts +1 -2
  155. package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +1 -2
  156. package/packages/bundled-features/src/managed-pages/seeding.ts +8 -4
  157. package/packages/bundled-features/src/metrics/compute.ts +1 -1
  158. package/packages/bundled-features/src/metrics/filters.ts +1 -1
  159. package/packages/bundled-features/src/metrics/validate.ts +1 -1
  160. package/packages/bundled-features/src/notes-history/feature.ts +6 -3
  161. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
  162. package/packages/bundled-features/src/notes-history-user-data/hooks.ts +1 -1
  163. package/packages/bundled-features/src/notes-history-user-data/index.ts +27 -18
  164. package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
  165. package/packages/bundled-features/src/personal-access-tokens/feature.ts +17 -2
  166. package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +1 -1
  167. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -2
  168. package/packages/bundled-features/src/personal-access-tokens/handlers/revoke.write.ts +1 -1
  169. package/packages/bundled-features/src/personal-access-tokens/resolver.ts +1 -1
  170. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -2
  171. package/packages/bundled-features/src/personal-access-tokens/roles.ts +8 -6
  172. package/packages/bundled-features/src/personal-access-tokens/screens.ts +2 -0
  173. package/packages/bundled-features/src/readiness/feature.ts +25 -16
  174. package/packages/bundled-features/src/readiness/index.ts +1 -1
  175. package/packages/bundled-features/src/secrets/changes.json +6 -0
  176. package/packages/bundled-features/src/secrets/db/queries/read.ts +1 -1
  177. package/packages/bundled-features/src/secrets/feature.ts +26 -6
  178. package/packages/bundled-features/src/secrets/handlers/delete.write.ts +5 -2
  179. package/packages/bundled-features/src/secrets/handlers/rotate.job.ts +2 -2
  180. package/packages/bundled-features/src/secrets/handlers/set.write.ts +5 -2
  181. package/packages/bundled-features/src/secrets/secrets-context.ts +4 -3
  182. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +1 -1
  183. package/packages/bundled-features/src/sessions/handlers/detail.query.ts +1 -1
  184. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -2
  185. package/packages/bundled-features/src/sessions/handlers/mine.query.ts +1 -1
  186. package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
  187. package/packages/bundled-features/src/sessions/handlers/revoke-all-others.write.ts +1 -1
  188. package/packages/bundled-features/src/sessions/handlers/revoke.write.ts +1 -1
  189. package/packages/bundled-features/src/sessions/session-callbacks.ts +14 -7
  190. package/packages/bundled-features/src/sessions/testing.ts +1 -1
  191. package/packages/bundled-features/src/tags/feature.ts +6 -3
  192. package/packages/bundled-features/src/template-resolver/api.ts +1 -1
  193. package/packages/bundled-features/src/template-resolver/constants.ts +0 -1
  194. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +1 -1
  195. package/packages/bundled-features/src/template-resolver/handlers/by-slug.query.ts +1 -1
  196. package/packages/bundled-features/src/template-resolver/handlers/by-tenant.query.ts +1 -2
  197. package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +1 -1
  198. package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +1 -2
  199. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +1 -1
  200. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +1 -1
  201. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +1 -1
  202. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +1 -1
  203. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +1 -1
  204. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +1 -1
  205. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +1 -1
  206. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +1 -1
  207. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +1 -1
  208. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +1 -1
  209. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +4 -2
  210. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +1 -1
  211. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +1 -1
  212. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +1 -1
  213. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +1 -1
  214. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +1 -1
  215. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +1 -1
  216. package/packages/bundled-features/src/template-resolver/seeding.ts +3 -3
  217. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +1 -1
  218. package/packages/bundled-features/src/template-resolver/template-preview.ts +1 -1
  219. package/packages/bundled-features/src/template-resolver/template-resources.ts +1 -1
  220. package/packages/bundled-features/src/template-resolver/testing.ts +1 -1
  221. package/packages/bundled-features/src/template-resolver-user-data/index.ts +23 -14
  222. package/packages/bundled-features/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
  223. package/packages/bundled-features/src/tenant/handlers/add-member.write.ts +1 -2
  224. package/packages/bundled-features/src/tenant/handlers/cancel-invitation.write.ts +1 -1
  225. package/packages/bundled-features/src/tenant/handlers/invitations.query.ts +1 -1
  226. package/packages/bundled-features/src/tenant/handlers/me.query.ts +1 -1
  227. package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +1 -1
  228. package/packages/bundled-features/src/tenant/handlers/members.query.ts +1 -1
  229. package/packages/bundled-features/src/tenant/handlers/memberships.query.ts +1 -1
  230. package/packages/bundled-features/src/tenant/handlers/remove-member.write.ts +2 -1
  231. package/packages/bundled-features/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
  232. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +1 -1
  233. package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +1 -1
  234. package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +1 -2
  235. package/packages/bundled-features/src/tenant/invited-membership.ts +2 -2
  236. package/packages/bundled-features/src/tenant/last-tenant-admin.ts +1 -2
  237. package/packages/bundled-features/src/tenant/seeding.ts +12 -12
  238. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +1 -2
  239. package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -2
  240. package/packages/bundled-features/src/tenant-lifecycle/lib/revoke-tenant-sessions.ts +1 -1
  241. package/packages/bundled-features/src/tenant-lifecycle/lifecycle-gate.ts +1 -1
  242. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +3 -3
  243. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +10 -5
  244. package/packages/bundled-features/src/tenant-lifecycle/testing.ts +3 -3
  245. package/packages/bundled-features/src/tenant-settings/feature.ts +34 -29
  246. package/packages/bundled-features/src/tier-engine/feature.ts +426 -428
  247. package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +2 -3
  248. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +5 -2
  249. package/packages/bundled-features/src/user/changes.json +6 -0
  250. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +1 -1
  251. package/packages/bundled-features/src/user/handlers/create.write.ts +1 -2
  252. package/packages/bundled-features/src/user/handlers/find-for-auth.query.ts +1 -1
  253. package/packages/bundled-features/src/user/handlers/list.query.ts +1 -1
  254. package/packages/bundled-features/src/user/handlers/update-roles.ts +2 -1
  255. package/packages/bundled-features/src/user/index.ts +5 -1
  256. package/packages/bundled-features/src/user/invited-global-roles.ts +2 -2
  257. package/packages/bundled-features/src/user/principal-status.ts +13 -3
  258. package/packages/bundled-features/src/user/resolve-display-names.ts +1 -1
  259. package/packages/bundled-features/src/user/seeding.ts +5 -5
  260. package/packages/bundled-features/src/user-data-rights/audit-download.ts +2 -2
  261. package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
  262. package/packages/bundled-features/src/user-data-rights/db/queries/export-jobs.ts +1 -1
  263. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +1 -1
  264. package/packages/bundled-features/src/user-data-rights/feature.ts +7 -4
  265. package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +1 -1
  266. package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
  267. package/packages/bundled-features/src/user-data-rights/handlers/export-status.query.ts +1 -1
  268. package/packages/bundled-features/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -1
  269. package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
  270. package/packages/bundled-features/src/user-data-rights/handlers/request-export.write.ts +1 -2
  271. package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +3 -3
  272. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +40 -3
  273. package/packages/bundled-features/src/user-data-rights/lib/deny-if-target-outside-admin-tenant.ts +1 -1
  274. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +7 -2
  275. package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +7 -3
  276. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +11 -5
  277. package/packages/bundled-features/src/user-data-rights/run-user-export.ts +10 -5
  278. package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +1 -2
  279. package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +1 -1
  280. package/packages/bundled-features/src/waitlist/lib.ts +5 -2
  281. package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +1 -1
  282. package/packages/bundled-features/src/workflow-runner/feature.ts +58 -47
  283. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
  284. package/packages/bundled-features/src/workflow-runner/index.ts +1 -1
  285. package/packages/bundled-features/src/workflow-runner/pending-projection.ts +1 -1
  286. package/samples/apps/admin-console/src/run-config.ts +2 -2
  287. package/samples/apps/cap-billing-demo/package.json +3 -0
  288. package/samples/apps/cap-billing-demo/src/feature.ts +2 -2
  289. package/samples/apps/cap-billing-demo/src/run-config.ts +8 -8
  290. package/samples/apps/marketing-demo/src/app/seed.ts +1 -1
  291. package/samples/apps/marketing-demo/src/db/queries/seed-counts.ts +1 -1
  292. package/samples/apps/showcase/package.json +3 -0
  293. package/samples/apps/showcase/src/app/seed-items.ts +1 -1
  294. package/samples/apps/styleguide/src/app/seed.ts +2 -2
  295. package/samples/apps/styleguide/src/features/ai-settings/seed.ts +2 -2
  296. package/samples/apps/ui-walkthrough/scripts/emit-e2e-data.ts +1 -1
  297. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +1 -1
  298. package/samples/apps/ui-walkthrough/src/app/seed-rental.ts +2 -2
  299. package/samples/apps/ui-walkthrough/src/app/seed-vehicles.ts +1 -1
  300. package/samples/apps/ui-walkthrough/src/app/seed.ts +1 -1
  301. package/samples/apps/ui-walkthrough/src/db/queries/row-counts.ts +1 -1
  302. package/samples/apps/ui-walkthrough/src/db/queries/task-counts.ts +1 -1
  303. package/samples/apps/use-all-bundled/feature-manifest.json +16 -16
  304. package/samples/apps/use-all-bundled/package.json +3 -0
  305. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +2 -2
  306. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +1 -1
  307. package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +2 -2
  308. package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +1 -1
  309. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +1 -1
  310. package/samples/apps/use-all-bundled/src/app/seed.ts +11 -5
  311. package/samples/apps/use-all-bundled/src/run-config.ts +55 -56
  312. package/samples/apps/user-data-rights-demo/README.md +2 -2
  313. package/samples/apps/user-data-rights-demo/package.json +3 -0
  314. package/samples/apps/user-data-rights-demo/src/run-config.ts +7 -7
  315. package/samples/apps/workspaces/package.json +3 -0
  316. package/samples/recipes/access-control/package.json +3 -0
  317. package/samples/recipes/anonymous-access/package.json +3 -0
  318. package/samples/recipes/anonymous-access-multitenant/package.json +3 -0
  319. package/samples/recipes/apex-landing/package.json +1 -0
  320. package/samples/recipes/apex-surface-auth/package.json +3 -0
  321. package/samples/recipes/apex-surface-auth/src/feature.ts +2 -2
  322. package/samples/recipes/api-worker-split/package.json +3 -0
  323. package/samples/recipes/api-worker-split/src/feature.ts +1 -2
  324. package/samples/recipes/auth-claims/package.json +3 -0
  325. package/samples/recipes/auth-foundation-providers/src/feature.ts +1 -1
  326. package/samples/recipes/auth-mfa/package.json +3 -0
  327. package/samples/recipes/basic-entity/package.json +3 -0
  328. package/samples/recipes/chat-channels/package.json +3 -0
  329. package/samples/recipes/compliance-profiles-demo/package.json +3 -0
  330. package/samples/recipes/cross-feature-events/package.json +3 -1
  331. package/samples/recipes/cross-feature-events/src/feature.ts +1 -1
  332. package/samples/recipes/crypto-shredding-hr/package.json +3 -0
  333. package/samples/recipes/currencies-global/package.json +3 -0
  334. package/samples/recipes/currencies-per-tenant/package.json +3 -0
  335. package/samples/recipes/custom-fields-basic/package.json +3 -0
  336. package/samples/recipes/custom-handlers/package.json +3 -0
  337. package/samples/recipes/dashboard-stats/package.json +3 -0
  338. package/samples/recipes/delivery-notifications/package.json +3 -0
  339. package/samples/recipes/e2e-generator/package.json +3 -0
  340. package/samples/recipes/embedded/package.json +3 -0
  341. package/samples/recipes/embedded-entity-form/package.json +1 -0
  342. package/samples/recipes/encrypted-tenant-config/README.md +1 -1
  343. package/samples/recipes/encrypted-tenant-config/package.json +3 -0
  344. package/samples/recipes/error-contract/package.json +3 -0
  345. package/samples/recipes/event-sourcing/package.json +3 -1
  346. package/samples/recipes/event-sourcing/src/feature.ts +16 -20
  347. package/samples/recipes/event-sourcing/tsconfig.json +2 -6
  348. package/samples/recipes/feature-toggles/package.json +3 -0
  349. package/samples/recipes/field-access/package.json +3 -0
  350. package/samples/recipes/files-post-processing/package.json +1 -0
  351. package/samples/recipes/golden-path/README.md +30 -0
  352. package/samples/recipes/golden-path/package.json +11 -0
  353. package/samples/recipes/golden-path/src/feature.ts +70 -0
  354. package/samples/recipes/idempotency/package.json +3 -0
  355. package/samples/recipes/inbound-mail-triage/package.json +3 -0
  356. package/samples/recipes/lane-routing/package.json +3 -0
  357. package/samples/recipes/ledger-banking/package.json +3 -0
  358. package/samples/recipes/ledger-invoicing/package.json +3 -0
  359. package/samples/recipes/legal-pages/package.json +3 -0
  360. package/samples/recipes/lifecycle-hooks/package.json +3 -0
  361. package/samples/recipes/managed-config/package.json +3 -0
  362. package/samples/recipes/managed-pages/package.json +3 -0
  363. package/samples/recipes/notes-history-basic/package.json +3 -0
  364. package/samples/recipes/ownership/package.json +3 -0
  365. package/samples/recipes/pdf-audit-export/package.json +1 -0
  366. package/samples/recipes/pipeline-basics/package.json +3 -0
  367. package/samples/recipes/public-share-token/package.json +3 -0
  368. package/samples/recipes/public-share-token/src/feature.ts +1 -1
  369. package/samples/recipes/rate-limiting/package.json +3 -0
  370. package/samples/recipes/realtime-sse/package.json +3 -0
  371. package/samples/recipes/record-detail-layout/package.json +1 -0
  372. package/samples/recipes/reference-data/package.json +3 -0
  373. package/samples/recipes/relations/package.json +3 -0
  374. package/samples/recipes/screens-nav/package.json +3 -0
  375. package/samples/recipes/search/package.json +3 -0
  376. package/samples/recipes/secrets-demo/package.json +3 -0
  377. package/samples/recipes/security-baseline/src/feature.ts +2 -2
  378. package/samples/recipes/session-revocation/README.md +1 -1
  379. package/samples/recipes/session-revocation/package.json +3 -0
  380. package/samples/recipes/session-revocation/src/feature.ts +1 -1
  381. package/samples/recipes/state-machine/package.json +3 -1
  382. package/samples/recipes/state-machine/src/entities/invoice.ts +2 -2
  383. package/samples/recipes/state-machine/src/events.ts +19 -0
  384. package/samples/recipes/state-machine/src/feature.ts +25 -30
  385. package/samples/recipes/state-machine/src/handlers/_helpers.ts +13 -14
  386. package/samples/recipes/state-machine/src/handlers/create.ts +1 -1
  387. package/samples/recipes/state-machine/src/handlers/transitions.ts +129 -122
  388. package/samples/recipes/state-machine/src/reducer.ts +1 -1
  389. package/samples/recipes/state-machine/tsconfig.json +2 -6
  390. package/samples/recipes/tags-basic/package.json +3 -0
  391. package/samples/recipes/tenant-isolation/package.json +3 -0
  392. package/samples/recipes/tenant-settings/package.json +3 -0
  393. package/samples/recipes/tier-admin/package.json +3 -0
  394. package/samples/recipes/timezones/package.json +3 -0
  395. package/samples/recipes/tsconfig.base.json +1 -1
  396. package/samples/recipes/user-data-rights/package.json +3 -0
  397. package/samples/recipes/user-data-rights/src/feature.ts +6 -2
  398. package/samples/recipes/user-profile/package.json +3 -0
  399. package/samples/recipes/user-profile/src/feature.ts +2 -2
  400. package/samples/recipes/webhook-step/package.json +3 -0
  401. package/samples/recipes/wizard-form/package.json +1 -0
  402. package/samples/recipes/workflow-engine/package.json +3 -0
  403. package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +5 -2
  404. package/samples/recipes/writeform-section/package.json +1 -0
  405. package/packages/bundled-features/src/file-provider-s3-env/feature.ts +0 -58
  406. /package/packages/bundled-features/src/{files-provider-s3 → file-provider-s3/lib}/env-helper.ts +0 -0
@@ -5,7 +5,7 @@ import {
5
5
  type TenantDb,
6
6
  } from "@cosmicdrift/kumiko-framework/db";
7
7
  import type { SessionUser, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
8
- import { reraiseAsKumikoError } from "@cosmicdrift/kumiko-framework/errors";
8
+ import { reraiseAsKumikoError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
9
9
  import {
10
10
  appendEventInTenantDb,
11
11
  getStreamVersionInTenantDb,
@@ -45,6 +45,9 @@ type CapReservationRow = {
45
45
  readonly expiresAt: Temporal.Instant;
46
46
  readonly createdAt: Temporal.Instant;
47
47
  readonly settledAt: Temporal.Instant | null;
48
+ // Units consume already turned into booked ones; the counter holds them on top of amount.
49
+ readonly consumedAmount: number;
50
+ readonly releasedAt: Temporal.Instant | null;
48
51
  };
49
52
 
50
53
  const NOTHING_TO_RELEASE: WriteResult = { isSuccess: true, data: {} };
@@ -69,6 +72,8 @@ async function insertReservationRow(
69
72
  expiresAt: now.add({ minutes: CAP_RESERVATION_TTL_MINUTES }),
70
73
  createdAt: now,
71
74
  settledAt: null,
75
+ consumedAmount: 0,
76
+ releasedAt: null,
72
77
  });
73
78
  }
74
79
 
@@ -197,6 +202,7 @@ async function confirmCapReservation(
197
202
  id,
198
203
  tenantId: confirm.tenantId,
199
204
  settledAt: null,
205
+ releasedAt: null,
200
206
  });
201
207
  }
202
208
 
@@ -208,6 +214,8 @@ async function giveBackCalendar(
208
214
  if (row.periodStartIso === null) {
209
215
  throw new Error(`cap-counter: calendar reservation ${row.id} has no period to release against`);
210
216
  }
217
+ // Fully consumed by consumeCalendarCapReservation: nothing left to give back.
218
+ if (row.amount === 0) return NOTHING_TO_RELEASE;
211
219
  return applyCapDeltaOn(
212
220
  txDb,
213
221
  user,
@@ -247,29 +255,45 @@ async function giveBackReservation(
247
255
  });
248
256
  if (!row) return NOTHING_TO_RELEASE;
249
257
  const claimed = await claimReservationRow(txDb, user, row);
250
- if (!claimed) return NOTHING_TO_RELEASE;
258
+ if (!claimed || claimed.releasedAt !== null) return NOTHING_TO_RELEASE;
251
259
  const given =
252
- row.kind === "rolling"
253
- ? await giveBackRolling(txDb, user, row)
254
- : await giveBackCalendar(txDb, user, row);
260
+ claimed.kind === "rolling"
261
+ ? await giveBackRolling(txDb, user, claimed)
262
+ : await giveBackCalendar(txDb, user, claimed);
255
263
  if (!given.isSuccess) return given;
256
- await txDb.deleteMany(capReservationsTable, ownRow);
264
+ // Only consumed units need remembering: a later settle or consume must not count them twice.
265
+ if (claimed.kind === "rolling" || claimed.consumedAmount === 0) {
266
+ await txDb.deleteMany(capReservationsTable, ownRow);
267
+ return given;
268
+ }
269
+ const now = Temporal.Now.instant();
270
+ await txDb.updateMany(
271
+ capReservationsTable,
272
+ {
273
+ amount: 0,
274
+ releasedAt: now,
275
+ expiresAt: now.add({ minutes: CAP_SETTLED_TOMBSTONE_TTL_MINUTES }),
276
+ },
277
+ ownRow,
278
+ );
257
279
  return given;
258
280
  }
259
281
 
260
- // Waits for a confirm still holding the row; false means that confirm, a settle or another release
261
- // won (Postgres re-checks settled_at after the lock wait).
282
+ // Waits for a confirm still holding the row; undefined means that confirm or a settle won (Postgres
283
+ // re-checks settled_at after the lock wait). A released row is claimed too: callers branch on
284
+ // releasedAt. The returned row is read after the lock wait, so it already includes a concurrent
285
+ // consume.
262
286
  async function claimReservationRow(
263
287
  txDb: TenantDb,
264
288
  user: SessionUser,
265
289
  row: CapReservationRow,
266
- ): Promise<boolean> {
267
- const claimed = await txDb.updateMany(
290
+ ): Promise<CapReservationRow | undefined> {
291
+ const [claimed] = await txDb.updateMany<CapReservationRow>(
268
292
  capReservationsTable,
269
293
  { expiresAt: row.expiresAt },
270
294
  { id: row.id, tenantId: user.tenantId, settledAt: null },
271
295
  );
272
- return claimed.length > 0;
296
+ return claimed;
273
297
  }
274
298
 
275
299
  export async function releaseCapReservation(
@@ -289,9 +313,28 @@ export async function releaseCapReservation(
289
313
  export type SettleCalendarCapOptions = {
290
314
  readonly capName: string;
291
315
  readonly periodStartIso: string;
316
+ // Total used, including units already consumed through a handle.
292
317
  readonly actualAmount: number;
293
318
  };
294
319
 
320
+ export class CapSettleBelowConsumedError extends UnprocessableError {
321
+ constructor(capName: string, consumed: number, actualAmount: number) {
322
+ super("cap_settle_below_consumed", {
323
+ message: `Cap "${capName}": cannot settle ${actualAmount}, ${consumed} units were already consumed.`,
324
+ details: { capName, consumed, actualAmount },
325
+ });
326
+ }
327
+ }
328
+
329
+ export class CapReservationSettledError extends UnprocessableError {
330
+ constructor(capName: string) {
331
+ super("cap_reservation_settled", {
332
+ message: `Cap "${capName}": the reservation is already settled, consume is no longer possible.`,
333
+ details: { capName },
334
+ });
335
+ }
336
+ }
337
+
295
338
  function bookCalendarDelta(
296
339
  txDb: TenantDb,
297
340
  user: SessionUser,
@@ -318,8 +361,22 @@ async function settleInTransaction(
318
361
  ...ownRow,
319
362
  settledAt: null,
320
363
  });
321
- if (open && (await claimReservationRow(txDb, user, open))) {
322
- const booked = await bookCalendarDelta(txDb, user, options, options.actualAmount - open.amount);
364
+ const claimed = open && (await claimReservationRow(txDb, user, open));
365
+ if (claimed) {
366
+ if (options.actualAmount < claimed.consumedAmount) {
367
+ throw new CapSettleBelowConsumedError(
368
+ options.capName,
369
+ claimed.consumedAmount,
370
+ options.actualAmount,
371
+ );
372
+ }
373
+ const alreadyBooked = claimed.amount + claimed.consumedAmount;
374
+ const booked = await bookCalendarDelta(
375
+ txDb,
376
+ user,
377
+ options,
378
+ options.actualAmount - alreadyBooked,
379
+ );
323
380
  if (!booked.isSuccess) return booked;
324
381
  const now = Temporal.Now.instant();
325
382
  await txDb.updateMany(
@@ -335,6 +392,10 @@ async function settleInTransaction(
335
392
  }
336
393
  // Row swept or released: its amount was given back, so the full actual amount is booked and a
337
394
  // tombstone makes a repeated settle a no-op. A parallel settle inserting first fails this insert.
395
+ // Booking first: a lost counter race returns a result instead of throwing, so the transaction
396
+ // commits and a tombstone written before it would turn the retry into a no-op.
397
+ const booked = await bookCalendarDelta(txDb, user, options, options.actualAmount);
398
+ if (!booked.isSuccess) return booked;
338
399
  const now = Temporal.Now.instant();
339
400
  await txDb.insertOne(capReservationsTable, {
340
401
  ...ownRow,
@@ -345,8 +406,10 @@ async function settleInTransaction(
345
406
  expiresAt: now.add({ minutes: CAP_SETTLED_TOMBSTONE_TTL_MINUTES }),
346
407
  createdAt: now,
347
408
  settledAt: now,
409
+ consumedAmount: 0,
410
+ releasedAt: null,
348
411
  });
349
- return bookCalendarDelta(txDb, user, options, options.actualAmount);
412
+ return booked;
350
413
  }
351
414
 
352
415
  // The failed tombstone insert aborts the whole attempt, so nothing was booked; the repeat sees the
@@ -382,8 +445,128 @@ export async function settleCalendarCapReservation(
382
445
  if (!settled.isSuccess) throw reraiseAsKumikoError(settled.error);
383
446
  }
384
447
 
448
+ export type ConsumeCalendarCapOptions = {
449
+ readonly capName: string;
450
+ readonly periodStartIso: string;
451
+ readonly units: number;
452
+ };
453
+
454
+ export class CapReservationOverConsumedError extends UnprocessableError {
455
+ constructor(capName: string, remaining: number, requested: number) {
456
+ super("cap_reservation_over_consumed", {
457
+ message: `Cap "${capName}": cannot consume ${requested} units, only ${remaining} still reserved.`,
458
+ details: { capName, remaining, requested },
459
+ });
460
+ }
461
+ }
462
+
463
+ async function consumeClaimedRow(
464
+ txDb: TenantDb,
465
+ user: SessionUser,
466
+ claimed: CapReservationRow,
467
+ options: ConsumeCalendarCapOptions,
468
+ ): Promise<WriteResult> {
469
+ const ownRow = { id: claimed.id, tenantId: user.tenantId };
470
+ if (claimed.releasedAt !== null) {
471
+ // The reserved rest was given back, so each unit is booked on top of what is still counted.
472
+ const booked = await bookCalendarDelta(txDb, user, options, options.units);
473
+ if (!booked.isSuccess) return booked;
474
+ await txDb.updateMany(
475
+ capReservationsTable,
476
+ { consumedAmount: claimed.consumedAmount + options.units },
477
+ ownRow,
478
+ );
479
+ return booked;
480
+ }
481
+ if (options.units > claimed.amount) {
482
+ throw new CapReservationOverConsumedError(options.capName, claimed.amount, options.units);
483
+ }
484
+ await txDb.updateMany(
485
+ capReservationsTable,
486
+ {
487
+ amount: claimed.amount - options.units,
488
+ consumedAmount: claimed.consumedAmount + options.units,
489
+ },
490
+ ownRow,
491
+ );
492
+ return NOTHING_TO_RELEASE;
493
+ }
494
+
495
+ // Row already expired away: the units are booked directly and a released tombstone remembers them
496
+ // so a later settle subtracts them. A parallel consume inserting first fails this insert.
497
+ async function consumeWithoutRow(
498
+ txDb: TenantDb,
499
+ user: SessionUser,
500
+ id: string,
501
+ options: ConsumeCalendarCapOptions,
502
+ ): Promise<WriteResult> {
503
+ // Booking first, for the same reason as settle's missing-row path.
504
+ const booked = await bookCalendarDelta(txDb, user, options, options.units);
505
+ if (!booked.isSuccess) return booked;
506
+ const now = Temporal.Now.instant();
507
+ await txDb.insertOne(capReservationsTable, {
508
+ id,
509
+ tenantId: user.tenantId,
510
+ capName: options.capName,
511
+ kind: "calendar",
512
+ periodStartIso: options.periodStartIso,
513
+ amount: 0,
514
+ expiresAt: now.add({ minutes: CAP_SETTLED_TOMBSTONE_TTL_MINUTES }),
515
+ createdAt: now,
516
+ settledAt: null,
517
+ consumedAmount: options.units,
518
+ releasedAt: now,
519
+ });
520
+ return booked;
521
+ }
522
+
523
+ async function consumeInTransaction(
524
+ txDb: TenantDb,
525
+ user: SessionUser,
526
+ id: string,
527
+ options: ConsumeCalendarCapOptions,
528
+ ): Promise<WriteResult> {
529
+ const ownRow = { id, tenantId: user.tenantId };
530
+ const open = await txDb.fetchOne<CapReservationRow>(capReservationsTable, {
531
+ ...ownRow,
532
+ settledAt: null,
533
+ });
534
+ if (open && open.capName !== options.capName) {
535
+ throw new Error(
536
+ `cap-counter: reservation ${id} belongs to cap "${open.capName}", not "${options.capName}"`,
537
+ );
538
+ }
539
+ const claimed = open && (await claimReservationRow(txDb, user, open));
540
+ if (claimed) return consumeClaimedRow(txDb, user, claimed, options);
541
+ if (await txDb.fetchOne<CapReservationRow>(capReservationsTable, ownRow)) {
542
+ throw new CapReservationSettledError(options.capName);
543
+ }
544
+ return consumeWithoutRow(txDb, user, id, options);
545
+ }
546
+
547
+ // Turns reserved units into booked ones without touching the counter; the row keeps the unconsumed
548
+ // rest so a release gives back exactly that. After a release or sweep the units are booked directly.
549
+ export async function consumeCalendarCapReservation(
550
+ ctx: CapCounterContext,
551
+ id: string,
552
+ options: ConsumeCalendarCapOptions,
553
+ outsideTransaction = true,
554
+ ): Promise<void> {
555
+ if (!Number.isInteger(options.units) || options.units < 1) {
556
+ throw new Error(`cap-counter: consume units must be a positive integer, got ${options.units}`);
557
+ }
558
+ const { user, writeDb } = resolveCapCounterScope(ctx, outsideTransaction);
559
+ const consumed = await retryCounterWriteOnVersionConflict(() =>
560
+ retryOnceOnTombstoneRace(() =>
561
+ runInOwnTransaction(writeDb, (txDb) => consumeInTransaction(txDb, user, id, options)),
562
+ ),
563
+ );
564
+ if (!consumed.isSuccess) throw reraiseAsKumikoError(consumed.error);
565
+ }
566
+
385
567
  // A failed release leaves its row behind; giving it back once the TTL passed bounds how long that
386
- // capacity stays counted. Expired settled tombstones are only deleted: their amount stays booked.
568
+ // capacity stays counted. Expired settled and released tombstones are only deleted: what they
569
+ // stand for stays booked.
387
570
  export async function releaseExpiredCapReservations(
388
571
  ctx: CapCounterContext,
389
572
  capName: string,
@@ -399,9 +582,14 @@ export async function releaseExpiredCapReservations(
399
582
  ...expiredWhere,
400
583
  settledAt: { ne: null },
401
584
  });
585
+ await writeDb.deleteMany(capReservationsTable, {
586
+ ...expiredWhere,
587
+ releasedAt: { ne: null },
588
+ });
402
589
  const expired = await writeDb.selectMany<Pick<CapReservationRow, "id">>(capReservationsTable, {
403
590
  ...expiredWhere,
404
591
  settledAt: null,
592
+ releasedAt: null,
405
593
  });
406
594
  for (const { id } of expired) {
407
595
  try {
@@ -1,4 +1,17 @@
1
1
  [
2
+ {
3
+ "version": "0.368.0",
4
+ "type": "improvement",
5
+ "title": "calendarCapReservationHandle consumes a reservation per unit",
6
+ "detail": "`reserveCalendarCapUsage` now returns `reservationId`. `calendarCapReservationHandle(ctx, reservationId, { capName, periodStartIso })` returns a handle with `consume(units)` (turns reserved units into booked ones, counter unchanged) and `release()` (gives back the unconsumed rest). A handler reserves the worst case before the job is dispatched, the job consumes per written unit, so parallel dispatches can no longer overshoot the hard cap and a retry only counts what was written.\n\n`consume` beyond the still-reserved rest throws `CapReservationOverConsumedError` (422, `cap_reservation_over_consumed`). `consume` against a settle tombstone throws `CapReservationSettledError` (422, `cap_reservation_settled`); `release` against it is a no-op. When the reservation outlived its TTL and was swept, `consume` books the units directly, the way settle does for a missing row, without a hard-cap check."
7
+ },
8
+ {
9
+ "version": "0.368.0",
10
+ "type": "fix",
11
+ "title": "Swept or released calendar reservations remember the units consume booked",
12
+ "detail": "The reservation row now stores `consumed_amount`. A release or the TTL sweep gives back only the unconsumed rest. If units were consumed, it keeps the row as a released tombstone (`released_at` set, `amount` 0) until the tombstone TTL passes, instead of deleting it. A later `settle` subtracts the consumed units from the booking, and a `consume` after the sweep books its units directly and is remembered the same way, so the counter ends at exactly the settled total. `settle` below the consumed units throws `CapSettleBelowConsumedError` (422, `cap_settle_below_consumed`) in every state. Expired released tombstones are deleted by the sweep.",
13
+ "migration": "Apps with cap-counter generate a migration for the new columns store_cap_reservations.consumed_amount (integer NOT NULL DEFAULT 0) and store_cap_reservations.released_at (kumiko migrate generate)."
14
+ },
2
15
  {
3
16
  "version": "0.366.0",
4
17
  "type": "fix",
@@ -68,81 +68,92 @@ import { markSoftWarnedHandler } from "./handlers/mark-soft-warned.write.js";
68
68
  import { CAP_COUNTER_I18N } from "./i18n.js";
69
69
  import { capReservationsTableMeta } from "./tables.js";
70
70
 
71
- export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
72
- r.describe(
73
- "Tracks per-tenant usage against configurable limits using two complementary storage models: calendar-period counters (one projection row per tenant/capName/period, reset implicitly by period rollover) and rolling-window counters (append-only event stream, no projection). Use `enforceCap` / `enforceRollingCap` (or the `withCapEnforcement` / `withRollingCapEnforcement` handler wrappers) in your write-handlers to check limits with soft-warn and hard-block tolerances; call `enforceCapAndMaybeNotify` when you also want to trigger a delivery notification on soft-threshold hits.",
74
- );
75
- r.uiHints({
76
- displayLabel: "Cap Counter · Usage Limits",
77
- category: "operations",
78
- recommended: false,
79
- });
80
- r.entity({ name: "cap-counter", ...capCounterEntity });
81
- r.storeTable(capReservationsTableMeta, {
82
- reason: "coordination.cap_reservation_ledger",
83
- });
71
+ export function createCapCounterFeature() {
72
+ return defineFeature(
73
+ CAP_COUNTER_FEATURE,
74
+ (r) => {
75
+ r.describe(
76
+ "Tracks per-tenant usage against configurable limits using two complementary storage models: calendar-period counters (one projection row per tenant/capName/period, reset implicitly by period rollover) and rolling-window counters (append-only event stream, no projection). Use `enforceCap` / `enforceRollingCap` (or the `withCapEnforcement` / `withRollingCapEnforcement` handler wrappers) in your write-handlers to check limits with soft-warn and hard-block tolerances; call `enforceCapAndMaybeNotify` when you also want to trigger a delivery notification on soft-threshold hits.",
77
+ );
78
+ r.uiHints({
79
+ displayLabel: "Cap Counter · Usage Limits",
80
+ category: "operations",
81
+ recommended: false,
82
+ });
83
+ r.entity({ name: "cap-counter", ...capCounterEntity });
84
+ r.storeTable(capReservationsTableMeta, {
85
+ reason: "coordination.cap_reservation_ledger",
86
+ });
84
87
 
85
- // Custom Domain-Event für Rolling-Counter. r.defineEvent registriert
86
- // das Schema beim Registry; ctx.unsafeAppendEvent im Handler nutzt
87
- // dasselbe Schema für Append-Time-Validation. QN nach Prefixing:
88
- // "cap-counter:event:rolling-incremented" (siehe
89
- // ROLLING_INCREMENTED_EVENT_QN).
90
- r.defineEvent(ROLLING_INCREMENTED_EVENT_SHORT, rollingIncrementedSchema, { piiFields: "none" });
91
- r.defineEvent(ROLLING_RELEASED_EVENT_SHORT, rollingReleasedSchema, { piiFields: "none" });
88
+ // Custom domain event for the rolling counter. r.defineEvent registers the
89
+ // schema with the registry; ctx.unsafeAppendEvent in the handler reuses the
90
+ // same schema for append-time validation. QN after prefixing:
91
+ // "cap-counter:event:rolling-incremented" (see
92
+ // ROLLING_INCREMENTED_EVENT_QN).
93
+ r.defineEvent(ROLLING_INCREMENTED_EVENT_SHORT, rollingIncrementedSchema, {
94
+ piiFields: "none",
95
+ });
96
+ r.defineEvent(ROLLING_RELEASED_EVENT_SHORT, rollingReleasedSchema, { piiFields: "none" });
92
97
 
93
- // Custom write-handlers.
94
- // - increment: Calendar-Period (CRUD via projection-row).
95
- // - increment-rolling: Rolling-Window (Custom-Event, no projection).
96
- // - mark-soft-warned: Anti-Notification-Storm-Flag (nur Calendar).
97
- r.writeHandler(incrementCapHandler);
98
- r.writeHandler(incrementRollingCapHandler);
99
- r.writeHandler(markSoftWarnedHandler);
98
+ // Custom write-handlers.
99
+ // - increment: calendar period (CRUD via projection row).
100
+ // - increment-rolling: rolling window (custom event, no projection).
101
+ // - mark-soft-warned: anti-notification-storm flag (calendar only).
102
+ r.writeHandler(incrementCapHandler);
103
+ r.writeHandler(incrementRollingCapHandler);
104
+ r.writeHandler(markSoftWarnedHandler);
100
105
 
101
- // Custom + standard reads. Sysadmin-cross-tenant via list, per-tenant
102
- // single-row via get-counter. Detail-by-id-handler bewusst weggelassen
103
- // (kein Use-Case; der natürliche Lookup ist über capName + period, nicht
104
- // über aggregate-id).
105
- r.queryHandler(
106
- defineEntityListHandler("cap-counter", capCounterEntity, {
107
- access: { roles: access.systemAdmin },
108
- escapeHatch: {
109
- reason:
110
- "SystemAdmin quota operator view lists usage counters of every tenant to see who consumes which cap",
111
- },
112
- description:
113
- "Lists usage-counter rows across all tenants with cap name, current value, period start and soft-warn timestamp; use it as the operator view of who is consuming which quota.",
114
- }),
115
- );
116
- r.queryHandler(getCounterQuery);
106
+ // Custom + standard reads. Sysadmin cross-tenant via list, per-tenant
107
+ // single row via get-counter. Detail-by-id handler left out on purpose
108
+ // (no use case; the natural lookup is capName + period, not the
109
+ // aggregate id).
110
+ r.queryHandler(
111
+ defineEntityListHandler("cap-counter", capCounterEntity, {
112
+ access: { roles: access.systemAdmin },
113
+ escapeHatch: {
114
+ reason:
115
+ "SystemAdmin quota operator view lists usage counters of every tenant to see who consumes which cap",
116
+ },
117
+ description:
118
+ "Lists usage-counter rows across all tenants with cap name, current value, period start and soft-warn timestamp; use it as the operator view of who is consuming which quota.",
119
+ }),
120
+ );
121
+ r.queryHandler(getCounterQuery);
117
122
 
118
- r.screen({
119
- id: CAP_COUNTER_LIST_SCREEN_ID,
120
- type: "entityList",
121
- entity: "cap-counter",
122
- columns: [
123
- "tenantId",
124
- "capName",
125
- "value",
126
- "periodStart",
127
- { field: "lastSoftWarnedAt", hideOnNarrow: true },
128
- ],
129
- defaultSort: { field: "capName", dir: "asc" },
130
- // Search resolves against the caller's own tenant stream, so on this
131
- // cross-tenant operator list it would only ever hit the SystemAdmin's tenant.
132
- searchable: false,
133
- access: { roles: ["SystemAdmin"] },
134
- });
135
- r.nav({
136
- id: "cap-list",
137
- label: "cap-counter:nav.cap-list",
138
- screen: "cap-counter:screen:cap-list",
139
- icon: "gauge",
140
- order: 60,
141
- });
142
- r.translations({
143
- keys: {
144
- ...CAP_COUNTER_I18N,
145
- "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
123
+ r.screen({
124
+ id: CAP_COUNTER_LIST_SCREEN_ID,
125
+ type: "entityList",
126
+ entity: "cap-counter",
127
+ columns: [
128
+ "tenantId",
129
+ "capName",
130
+ "value",
131
+ "periodStart",
132
+ { field: "lastSoftWarnedAt", hideOnNarrow: true },
133
+ ],
134
+ defaultSort: { field: "capName", dir: "asc" },
135
+ // Search resolves against the caller's own tenant stream, so on this
136
+ // cross-tenant operator list it would only ever hit the SystemAdmin's tenant.
137
+ searchable: false,
138
+ access: { roles: ["SystemAdmin"] },
139
+ });
140
+ r.nav({
141
+ id: "cap-list",
142
+ label: "cap-counter:nav.cap-list",
143
+ screen: "cap-counter:screen:cap-list",
144
+ icon: "gauge",
145
+ order: 60,
146
+ });
147
+ r.translations({
148
+ keys: {
149
+ ...CAP_COUNTER_I18N,
150
+ "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
151
+ },
152
+ });
146
153
  },
147
- });
148
- });
154
+ { dedupeOptions: {} },
155
+ );
156
+ }
157
+
158
+ /** @deprecated Use createCapCounterFeature(). Run `bun kumiko-upgrade --apply`. Removed in the first release after 2026-10-29. */
159
+ export const capCounterFeature = createCapCounterFeature();
@@ -68,10 +68,9 @@ export const incrementRollingCapHandler: WriteHandlerDef = {
68
68
  const payload = event.payload as IncrementRollingPayload;
69
69
  const aggregateId = rollingCapAggregateId(event.user.tenantId, payload.capName);
70
70
 
71
- // unsafeAppendEvent — bundled-features-Pfad (apps mit bun kumiko
72
- // codegen kriegen den strict-typed appendEvent-Wrapper). Schema-
73
- // Validation läuft trotzdem, weil r.defineEvent das Schema
74
- // registriert hat.
71
+ // unsafeAppendEvent: the strict appendEvent is bound to an app's own event map, which
72
+ // bundled-features cannot see. Schema validation still runs because r.defineEvent
73
+ // registered the schema.
75
74
  await ctx.unsafeAppendEvent({
76
75
  aggregateId,
77
76
  aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
@@ -10,10 +10,18 @@ export {
10
10
  readRollingCapUsage,
11
11
  } from "./book-cap-usage.js";
12
12
  export {
13
+ type CalendarCapReservationHandle,
14
+ type CalendarCapReservationHandleSpec,
13
15
  type CalendarCapUsageReservation,
14
16
  type CalendarCapUsageReservationSpec,
17
+ calendarCapReservationHandle,
15
18
  reserveCalendarCapUsage,
16
19
  } from "./calendar-cap-usage-reservation.js";
20
+ export {
21
+ CapReservationOverConsumedError,
22
+ CapReservationSettledError,
23
+ CapSettleBelowConsumedError,
24
+ } from "./cap-reservation.js";
17
25
  export {
18
26
  CAP_COUNTER_FEATURE,
19
27
  CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
@@ -41,7 +49,7 @@ export {
41
49
  type StockCapResult,
42
50
  } from "./enforce-cap.js";
43
51
  export { capCounterEntity } from "./entity.js";
44
- export { capCounterFeature } from "./feature.js";
52
+ export { capCounterFeature, createCapCounterFeature } from "./feature.js";
45
53
  export { readPayloadField } from "./payload-field.js";
46
54
  export {
47
55
  type CapGuardContext,
@@ -13,9 +13,12 @@ const CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX =
13
13
  "store_cap_reservations_tenant_id_cap_name_expires_at_idx";
14
14
 
15
15
  // One row per capacity that was booked before a handler transaction and is not settled yet. The
16
- // handler transaction deletes it on its own commit (confirm); a release or the TTL sweep deletes it
17
- // together with the counter decrement. A settle keeps the row as a tombstone (settled_at set) until
18
- // its shortened TTL passes, so a repeated settle or a late release finds it and does nothing.
16
+ // handler transaction deletes it on its own commit (confirm). While a calendar row is open the
17
+ // counter holds amount + consumed_amount for it. A release or the TTL sweep gives back amount and
18
+ // keeps the row as a released tombstone (released_at set, amount 0, consumed_amount still booked)
19
+ // until its shortened TTL passes, so a later consume or settle accounts for the consumed units.
20
+ // A settle keeps the row as a settled tombstone (settled_at set) the same way, so a repeated settle
21
+ // or a late release finds it and does nothing.
19
22
  // Unmanaged: the rows are a coordination aid, not state with an event history.
20
23
  export const capReservationsTable = pgTable(
21
24
  "store_cap_reservations",
@@ -30,6 +33,8 @@ export const capReservationsTable = pgTable(
30
33
  expiresAt: instant("expires_at").notNull(),
31
34
  createdAt: instant("created_at").notNull(),
32
35
  settledAt: instant("settled_at"),
36
+ consumedAmount: integer("consumed_amount").notNull().default(0),
37
+ releasedAt: instant("released_at"),
33
38
  },
34
39
  (t) => [index(CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX).on(t.tenantId, t.capName, t.expiresAt)],
35
40
  );
@@ -52,5 +57,7 @@ export const capReservationsTableMeta: EntityTableMeta = defineUnmanagedTable({
52
57
  { name: "expires_at", pgType: "timestamptz", notNull: true },
53
58
  { name: "created_at", pgType: "timestamptz", notNull: true },
54
59
  { name: "settled_at", pgType: "timestamptz", notNull: false },
60
+ { name: "consumed_amount", pgType: "integer", notNull: true, defaultSql: "0" },
61
+ { name: "released_at", pgType: "timestamptz", notNull: false },
55
62
  ],
56
63
  });
@@ -1,5 +1,4 @@
1
- import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
1
+ import { buildEntityTable, selectMany } from "@cosmicdrift/kumiko-framework/db";
3
2
  import {
4
3
  defineQueryHandler,
5
4
  mayOverrideTenant,
@@ -1,5 +1,9 @@
1
- import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import { buildEntityTable, decodeCursor, encodeCursor } from "@cosmicdrift/kumiko-framework/db";
1
+ import {
2
+ buildEntityTable,
3
+ decodeCursor,
4
+ encodeCursor,
5
+ selectMany,
6
+ } from "@cosmicdrift/kumiko-framework/db";
3
7
  import {
4
8
  type ConfigAccessor,
5
9
  definePagedQueryHandler,
@@ -1,4 +1,4 @@
1
- import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
4
4
  import * as z from "zod";
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.369.0",
4
+ "type": "breaking",
5
+ "title": "The tenant mail transport applies per notification",
6
+ "detail": "r.notification(name, { tenantTransport: true }) marks a notification whose mails go through the tenant's own mail transport. channel-email consults its tenantTransport resolver only for these (also under a renamedFrom name); every other notification, such as password reset or sign-in mails, always takes the app transport, even when the tenant has a provider. With tenantTransport set, transport may be omitted: a mail that then has no transport fails with missing_credentials instead of throwing. The template test mail (template-resolver send-test) now gets the tenant config in its channel context and uses the template's notification, so it leaves through the same transport as the real mail.",
7
+ "migration": "Apps that pass tenantTransport to createChannelEmailFeature add `tenantTransport: true` to every r.notification whose mails should go through the tenant's provider; without the flag those mails now go through the app transport. Apps without an app SMTP server can drop transport and keep tenantTransport."
8
+ },
2
9
  {
3
10
  "version": "0.367.0",
4
11
  "type": "improvement",