@cosmicdrift/kumiko-bundled-features 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 (359) hide show
  1. package/dist/audit/handlers/details.query.js +1 -1
  2. package/dist/audit/handlers/list.query.js +1 -1
  3. package/dist/audit/run-escape-hatch-retention.js +1 -1
  4. package/dist/auth-email-password/bootstrap.js +1 -1
  5. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +6 -3
  6. package/dist/auth-email-password/handlers/invite-accept.write.js +6 -3
  7. package/dist/auth-email-password/handlers/invite-info.query.js +1 -1
  8. package/dist/auth-email-password/handlers/invite-signup-complete.write.js +6 -3
  9. package/dist/auth-email-password/handlers/signup-confirm.write.js +1 -1
  10. package/dist/auth-email-password/seeding.js +2 -2
  11. package/dist/auth-foundation/feature.d.ts +4 -0
  12. package/dist/auth-foundation/feature.js +37 -33
  13. package/dist/auth-foundation/index.d.ts +1 -1
  14. package/dist/auth-foundation/index.js +1 -1
  15. package/dist/auth-mfa/db/queries.js +1 -2
  16. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
  17. package/dist/auth-mfa/handlers/enable-start-preauth.write.js +1 -1
  18. package/dist/auth-mfa/handlers/reencrypt.job.js +2 -3
  19. package/dist/auth-mfa/handlers/status.query.js +1 -1
  20. package/dist/auth-mfa/handlers/verify.write.js +1 -1
  21. package/dist/auth-mfa/mfa-code-verifier.js +1 -1
  22. package/dist/auth-mfa/mfa-status-checker.js +1 -1
  23. package/dist/auth-mfa-user-data/index.d.ts +4 -0
  24. package/dist/auth-mfa-user-data/index.js +13 -9
  25. package/dist/billing-foundation/billing-info-query.js +1 -1
  26. package/dist/billing-foundation/feature.d.ts +1 -0
  27. package/dist/billing-foundation/feature.js +5 -6
  28. package/dist/billing-foundation/handlers/create-portal-session.write.js +1 -1
  29. package/dist/billing-foundation/projection.js +1 -2
  30. package/dist/cap-counter/calendar-cap-usage-reservation.d.ts +7 -0
  31. package/dist/cap-counter/calendar-cap-usage-reservation.js +28 -2
  32. package/dist/cap-counter/cap-reservation.d.ts +16 -0
  33. package/dist/cap-counter/cap-reservation.js +144 -14
  34. package/dist/cap-counter/feature.d.ts +4 -0
  35. package/dist/cap-counter/feature.js +74 -68
  36. package/dist/cap-counter/handlers/increment-rolling.write.js +3 -4
  37. package/dist/cap-counter/index.d.ts +3 -2
  38. package/dist/cap-counter/index.js +3 -2
  39. package/dist/cap-counter/tables.js +10 -3
  40. package/dist/cap-overview/handlers/caps-usage.query.js +1 -2
  41. package/dist/cap-overview/handlers/tenant-caps-list.query.js +1 -2
  42. package/dist/cap-overview/handlers/tenant-options.query.js +1 -1
  43. package/dist/channel-email/email-channel.d.ts +13 -5
  44. package/dist/channel-email/email-channel.js +17 -3
  45. package/dist/channel-email/feature.js +1 -1
  46. package/dist/channel-email/index.d.ts +1 -1
  47. package/dist/channel-email/index.js +1 -1
  48. package/dist/channel-in-app/handlers/inbox.query.js +1 -1
  49. package/dist/channel-in-app/handlers/mark-all-read.write.js +1 -1
  50. package/dist/channel-in-app/handlers/mark-read.write.js +1 -1
  51. package/dist/channel-in-app/handlers/unread-count.query.js +1 -1
  52. package/dist/channel-in-app/tables.js +1 -2
  53. package/dist/compliance-profiles/handlers/for-tenant.query.js +1 -1
  54. package/dist/compliance-profiles/handlers/needs-profile.query.js +1 -1
  55. package/dist/compliance-profiles/handlers/set-profile.write.js +1 -2
  56. package/dist/compliance-profiles/resolve-for-tenant.js +1 -1
  57. package/dist/compliance-profiles/seeding.js +3 -5
  58. package/dist/compliance-profiles-ops/index.d.ts +8 -0
  59. package/dist/compliance-profiles-ops/index.js +19 -15
  60. package/dist/config/db/queries/resolver.js +1 -1
  61. package/dist/config/handlers/reencrypt.job.js +2 -2
  62. package/dist/config/resolver.js +1 -1
  63. package/dist/config/web/i18n.js +1 -2
  64. package/dist/config/write-helpers.js +1 -1
  65. package/dist/crypto-shredding/handlers/forget-subject.write.js +6 -3
  66. package/dist/custom-fields/db/queries/field-access.js +1 -1
  67. package/dist/custom-fields/db/queries/projection.js +1 -1
  68. package/dist/custom-fields/db/queries/quota.js +1 -1
  69. package/dist/custom-fields/db/queries/retention.js +1 -2
  70. package/dist/custom-fields/db/queries/user-data-rights.js +1 -1
  71. package/dist/custom-fields/feature.d.ts +20 -2
  72. package/dist/custom-fields/feature.js +9 -13
  73. package/dist/custom-fields/handlers/clear-custom-field.write.js +2 -2
  74. package/dist/custom-fields/handlers/delete-system-field.write.js +2 -2
  75. package/dist/custom-fields/handlers/delete-tenant-field.write.js +2 -2
  76. package/dist/custom-fields/handlers/set-custom-field.write.js +2 -2
  77. package/dist/custom-fields/wire-for-entity.js +4 -4
  78. package/dist/data-retention/resolve-for-tenant.js +1 -1
  79. package/dist/data-retention/run-retention-cleanup.js +17 -13
  80. package/dist/delivery/address-opt-out.js +1 -2
  81. package/dist/delivery/attempt-log-retention.js +1 -1
  82. package/dist/delivery/channel-context.js +1 -1
  83. package/dist/delivery/db/queries/address-opt-outs.js +1 -1
  84. package/dist/delivery/db/queries/preferences.js +1 -1
  85. package/dist/delivery/delivery-service.js +16 -7
  86. package/dist/delivery/feature.js +2 -2
  87. package/dist/delivery/handlers/log.query.js +2 -3
  88. package/dist/delivery/handlers/preferences.query.js +1 -1
  89. package/dist/delivery/index.d.ts +3 -2
  90. package/dist/delivery/index.js +3 -2
  91. package/dist/delivery/unsubscribe.d.ts +13 -0
  92. package/dist/delivery/unsubscribe.js +32 -27
  93. package/dist/delivery/upsert-preference.js +1 -2
  94. package/dist/derivatives-sharp/feature.d.ts +4 -0
  95. package/dist/derivatives-sharp/feature.js +15 -11
  96. package/dist/derivatives-sharp/index.d.ts +1 -1
  97. package/dist/derivatives-sharp/index.js +1 -1
  98. package/dist/document-ingest-foundation/feature.d.ts +7 -0
  99. package/dist/document-ingest-foundation/feature.js +94 -90
  100. package/dist/document-ingest-foundation/forget-extract-with-file-ref.js +3 -3
  101. package/dist/document-ingest-foundation/index.d.ts +1 -1
  102. package/dist/document-ingest-foundation/index.js +1 -1
  103. package/dist/feature-toggles/global-feature-state-table.js +1 -2
  104. package/dist/feature-toggles/handlers/set.write.js +3 -5
  105. package/dist/feature-toggles/toggle-runtime.js +1 -1
  106. package/dist/file-derivatives/feature.d.ts +1 -0
  107. package/dist/file-derivatives/feature.js +2 -1
  108. package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
  109. package/dist/file-derivatives/handlers/public-variant.query.js +1 -1
  110. package/dist/file-foundation/feature.d.ts +6 -0
  111. package/dist/file-foundation/feature.js +29 -25
  112. package/dist/file-foundation/index.d.ts +1 -1
  113. package/dist/file-foundation/index.js +1 -1
  114. package/dist/file-foundation/read-file-ref-content.js +1 -1
  115. package/dist/file-provider-inmemory/feature.d.ts +4 -0
  116. package/dist/file-provider-inmemory/feature.js +24 -20
  117. package/dist/file-provider-inmemory/index.d.ts +1 -1
  118. package/dist/file-provider-inmemory/index.js +1 -1
  119. package/dist/file-provider-s3/env-feature.d.ts +3 -0
  120. package/dist/file-provider-s3/env-feature.js +46 -0
  121. package/dist/file-provider-s3/feature.d.ts +26 -0
  122. package/dist/file-provider-s3/feature.js +84 -72
  123. package/dist/file-provider-s3/index.d.ts +1 -1
  124. package/dist/file-provider-s3/index.js +1 -1
  125. package/dist/file-provider-s3/lib/index.d.ts +3 -0
  126. package/dist/file-provider-s3/lib/index.js +2 -0
  127. package/dist/{files-provider-s3 → file-provider-s3/lib}/s3-provider.js +19 -19
  128. package/dist/file-provider-s3-env/index.d.ts +8 -1
  129. package/dist/file-provider-s3-env/index.js +7 -2
  130. package/dist/files-provider-s3/index.d.ts +5 -3
  131. package/dist/files-provider-s3/index.js +5 -2
  132. package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +1 -1
  133. package/dist/files-tenant-data/hooks.js +1 -2
  134. package/dist/folders/feature.d.ts +6 -2
  135. package/dist/folders/feature.js +4 -2
  136. package/dist/folders-user-data/index.d.ts +4 -0
  137. package/dist/folders-user-data/index.js +21 -17
  138. package/dist/form-draft/db/queries/cleanup.js +1 -1
  139. package/dist/form-draft/db/queries/draft-count.js +1 -1
  140. package/dist/form-draft/db/queries/owned-file-refs.js +1 -2
  141. package/dist/form-draft/feature.d.ts +4 -0
  142. package/dist/form-draft/feature.js +5 -1
  143. package/dist/form-draft/handlers/cleanup.job.js +2 -2
  144. package/dist/form-draft/index.d.ts +1 -1
  145. package/dist/form-draft/index.js +1 -1
  146. package/dist/form-draft/lookup.js +1 -1
  147. package/dist/form-draft-user-data/index.d.ts +4 -0
  148. package/dist/form-draft-user-data/index.js +13 -9
  149. package/dist/inbound-mail-foundation/feature.d.ts +4 -0
  150. package/dist/inbound-mail-foundation/feature.js +151 -147
  151. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +1 -2
  152. package/dist/inbound-mail-foundation/index.d.ts +1 -1
  153. package/dist/inbound-mail-foundation/index.js +1 -1
  154. package/dist/inbound-mail-foundation/oauth-access-token.js +1 -2
  155. package/dist/inbound-mail-foundation/projection.js +1 -2
  156. package/dist/inbound-mail-foundation/watch-supervisor.js +1 -1
  157. package/dist/inbound-provider-imap/feature.d.ts +4 -0
  158. package/dist/inbound-provider-imap/feature.js +16 -12
  159. package/dist/inbound-provider-imap/index.d.ts +1 -1
  160. package/dist/inbound-provider-imap/index.js +1 -1
  161. package/dist/inbound-provider-inmemory/feature.d.ts +4 -0
  162. package/dist/inbound-provider-inmemory/feature.js +14 -10
  163. package/dist/inbound-provider-inmemory/index.d.ts +1 -1
  164. package/dist/inbound-provider-inmemory/index.js +1 -1
  165. package/dist/jobs/db/queries/retention.js +1 -1
  166. package/dist/jobs/db/queries/stale-run-sweep.js +1 -1
  167. package/dist/jobs/handlers/detail.query.js +1 -1
  168. package/dist/jobs/handlers/list.query.js +1 -2
  169. package/dist/jobs/handlers/retry.write.js +1 -1
  170. package/dist/jobs/handlers/tenant-failures.query.js +1 -1
  171. package/dist/jobs/handlers/tenant-runs.query.js +1 -1
  172. package/dist/jobs/job-run-logger.js +1 -2
  173. package/dist/jobs/tenant-job-failure-table.js +1 -2
  174. package/dist/jobs/tenant-job-run-table.js +1 -2
  175. package/dist/ledger/feature.d.ts +6 -2
  176. package/dist/ledger/feature.js +4 -2
  177. package/dist/mail-foundation/feature.d.ts +11 -0
  178. package/dist/mail-foundation/feature.js +52 -42
  179. package/dist/mail-foundation/i18n.d.ts +14 -0
  180. package/dist/mail-foundation/i18n.js +8 -0
  181. package/dist/mail-foundation/index.d.ts +1 -1
  182. package/dist/mail-foundation/index.js +1 -1
  183. package/dist/mail-transport-inmemory/feature.d.ts +4 -0
  184. package/dist/mail-transport-inmemory/feature.js +26 -16
  185. package/dist/mail-transport-inmemory/index.d.ts +1 -1
  186. package/dist/mail-transport-inmemory/index.js +1 -1
  187. package/dist/mail-transport-smtp/config-keys.d.ts +7 -0
  188. package/dist/mail-transport-smtp/config-keys.js +65 -0
  189. package/dist/mail-transport-smtp/feature.d.ts +17 -1
  190. package/dist/mail-transport-smtp/feature.js +58 -80
  191. package/dist/mail-transport-smtp/i18n.d.ts +41 -0
  192. package/dist/mail-transport-smtp/i18n.js +28 -0
  193. package/dist/mail-transport-smtp/index.d.ts +1 -1
  194. package/dist/mail-transport-smtp/index.js +1 -1
  195. package/dist/managed-pages/handlers/by-slug.query.js +1 -1
  196. package/dist/managed-pages/handlers/by-tenant-published.query.js +1 -2
  197. package/dist/managed-pages/handlers/set.write.js +1 -2
  198. package/dist/managed-pages/seeding.js +3 -5
  199. package/dist/metrics/compute.d.ts +1 -1
  200. package/dist/metrics/filters.d.ts +1 -1
  201. package/dist/metrics/filters.js +1 -1
  202. package/dist/metrics/validate.js +1 -1
  203. package/dist/notes-history/feature.d.ts +7 -2
  204. package/dist/notes-history/feature.js +4 -2
  205. package/dist/notes-history/handlers/add-note.write.js +1 -1
  206. package/dist/notes-history-user-data/hooks.js +1 -1
  207. package/dist/notes-history-user-data/index.d.ts +4 -0
  208. package/dist/notes-history-user-data/index.js +21 -17
  209. package/dist/personal-access-tokens/feature.d.ts +4 -1
  210. package/dist/personal-access-tokens/feature.js +10 -2
  211. package/dist/personal-access-tokens/handlers/create.write.js +1 -1
  212. package/dist/personal-access-tokens/handlers/list.query.js +2 -2
  213. package/dist/personal-access-tokens/handlers/revoke.write.js +1 -1
  214. package/dist/personal-access-tokens/resolver.js +1 -1
  215. package/dist/personal-access-tokens/revoke-for-user.js +1 -1
  216. package/dist/personal-access-tokens/roles.js +3 -5
  217. package/dist/personal-access-tokens/screens.js +2 -0
  218. package/dist/readiness/feature.d.ts +8 -0
  219. package/dist/readiness/feature.js +18 -14
  220. package/dist/readiness/index.d.ts +1 -1
  221. package/dist/readiness/index.js +1 -1
  222. package/dist/secrets/db/queries/read.js +1 -1
  223. package/dist/secrets/feature.d.ts +4 -0
  224. package/dist/secrets/feature.js +19 -6
  225. package/dist/secrets/handlers/delete.write.js +3 -2
  226. package/dist/secrets/handlers/rotate.job.js +2 -3
  227. package/dist/secrets/handlers/set.write.js +3 -2
  228. package/dist/secrets/secrets-context.js +3 -4
  229. package/dist/sessions/db/queries/cleanup.js +1 -1
  230. package/dist/sessions/handlers/detail.query.js +1 -1
  231. package/dist/sessions/handlers/list.query.js +2 -2
  232. package/dist/sessions/handlers/mine.query.js +1 -1
  233. package/dist/sessions/handlers/revoke-all-for-user.write.js +1 -1
  234. package/dist/sessions/handlers/revoke-all-others.write.js +1 -1
  235. package/dist/sessions/handlers/revoke.write.js +1 -1
  236. package/dist/sessions/session-callbacks.js +4 -6
  237. package/dist/sessions/testing.d.ts +1 -1
  238. package/dist/tags/feature.d.ts +6 -2
  239. package/dist/tags/feature.js +4 -2
  240. package/dist/template-resolver/api.js +1 -1
  241. package/dist/template-resolver/constants.d.ts +0 -1
  242. package/dist/template-resolver/constants.js +0 -1
  243. package/dist/template-resolver/handlers/bundle-import.js +1 -1
  244. package/dist/template-resolver/handlers/by-slug.query.js +1 -1
  245. package/dist/template-resolver/handlers/by-tenant.query.js +1 -2
  246. package/dist/template-resolver/handlers/collection-item.query.js +1 -1
  247. package/dist/template-resolver/handlers/collection-list.query.js +1 -2
  248. package/dist/template-resolver/handlers/collection-set.write.js +1 -1
  249. package/dist/template-resolver/handlers/copy-locale.write.js +1 -1
  250. package/dist/template-resolver/handlers/diff-to-system.query.js +1 -1
  251. package/dist/template-resolver/handlers/export-bundle.query.js +1 -1
  252. package/dist/template-resolver/handlers/find-by-id.query.js +1 -1
  253. package/dist/template-resolver/handlers/layout-source.query.js +1 -1
  254. package/dist/template-resolver/handlers/list.query.js +1 -1
  255. package/dist/template-resolver/handlers/overview.query.js +1 -1
  256. package/dist/template-resolver/handlers/reset-to-system.write.js +1 -1
  257. package/dist/template-resolver/handlers/resource-remove.write.js +1 -1
  258. package/dist/template-resolver/handlers/send-test.write.js +4 -2
  259. package/dist/template-resolver/handlers/set.write.js +1 -1
  260. package/dist/template-resolver/handlers/template-write.js +1 -1
  261. package/dist/template-resolver/handlers/tenant-overrides.query.js +1 -1
  262. package/dist/template-resolver/handlers/toggle-status.write.js +1 -1
  263. package/dist/template-resolver/handlers/usage.query.js +1 -1
  264. package/dist/template-resolver/handlers/validate.query.js +1 -1
  265. package/dist/template-resolver/seeding.js +3 -4
  266. package/dist/template-resolver/store-template-resource.js +1 -1
  267. package/dist/template-resolver/template-preview.js +1 -1
  268. package/dist/template-resolver/template-resources.js +1 -1
  269. package/dist/template-resolver/testing.js +1 -1
  270. package/dist/template-resolver-user-data/index.d.ts +4 -0
  271. package/dist/template-resolver-user-data/index.js +17 -13
  272. package/dist/tenant/handlers/active-tenant-ids.query.js +1 -1
  273. package/dist/tenant/handlers/add-member.write.js +1 -2
  274. package/dist/tenant/handlers/cancel-invitation.write.js +1 -1
  275. package/dist/tenant/handlers/invitations.query.js +1 -1
  276. package/dist/tenant/handlers/me.query.js +1 -1
  277. package/dist/tenant/handlers/member-directory.query.js +1 -1
  278. package/dist/tenant/handlers/members.query.js +1 -1
  279. package/dist/tenant/handlers/memberships.query.js +1 -1
  280. package/dist/tenant/handlers/remove-member.write.js +1 -2
  281. package/dist/tenant/handlers/resolve-user-ids.query.js +1 -1
  282. package/dist/tenant/handlers/team-list.query.js +1 -1
  283. package/dist/tenant/handlers/tenant-directory.query.js +1 -1
  284. package/dist/tenant/handlers/update-member-roles.write.js +1 -2
  285. package/dist/tenant/invited-membership.js +2 -3
  286. package/dist/tenant/last-tenant-admin.js +1 -2
  287. package/dist/tenant/seeding.js +12 -6
  288. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +1 -2
  289. package/dist/tenant-lifecycle/handlers/request-destruction.write.js +1 -2
  290. package/dist/tenant-lifecycle/lib/revoke-tenant-sessions.js +1 -1
  291. package/dist/tenant-lifecycle/lifecycle-gate.js +1 -1
  292. package/dist/tenant-lifecycle/run-tenant-destroy.js +3 -5
  293. package/dist/tenant-lifecycle/stages.js +10 -6
  294. package/dist/tenant-lifecycle/testing.d.ts +1 -1
  295. package/dist/tenant-lifecycle/testing.js +2 -2
  296. package/dist/tenant-settings/feature.d.ts +1 -0
  297. package/dist/tenant-settings/feature.js +2 -1
  298. package/dist/tier-engine/feature.d.ts +1 -8
  299. package/dist/tier-engine/feature.js +71 -78
  300. package/dist/tier-engine/handlers/get-tenant-tier.query.js +2 -3
  301. package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -2
  302. package/dist/user/db/queries/stream-tenant-backfill.js +1 -1
  303. package/dist/user/handlers/create.write.js +1 -2
  304. package/dist/user/handlers/find-for-auth.query.js +1 -1
  305. package/dist/user/handlers/list.query.js +1 -1
  306. package/dist/user/handlers/update-roles.js +1 -2
  307. package/dist/user/index.d.ts +1 -1
  308. package/dist/user/index.js +1 -1
  309. package/dist/user/invited-global-roles.js +2 -3
  310. package/dist/user/principal-status.d.ts +4 -0
  311. package/dist/user/principal-status.js +9 -3
  312. package/dist/user/resolve-display-names.js +1 -1
  313. package/dist/user/seeding.js +5 -6
  314. package/dist/user-data-rights/audit-download.js +2 -2
  315. package/dist/user-data-rights/db/queries/export-jobs.js +1 -1
  316. package/dist/user-data-rights/db/queries/forget-cleanup.js +1 -1
  317. package/dist/user-data-rights/feature.js +7 -4
  318. package/dist/user-data-rights/handlers/download-by-job.query.js +1 -1
  319. package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
  320. package/dist/user-data-rights/handlers/export-status.query.js +1 -1
  321. package/dist/user-data-rights/handlers/list-download-attempts.query.js +1 -1
  322. package/dist/user-data-rights/handlers/my-audit-log.query.js +1 -1
  323. package/dist/user-data-rights/handlers/request-export.write.js +1 -2
  324. package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +3 -3
  325. package/dist/user-data-rights/lib/default-mailers.d.ts +3 -0
  326. package/dist/user-data-rights/lib/default-mailers.js +25 -3
  327. package/dist/user-data-rights/lib/deny-if-target-outside-admin-tenant.d.ts +1 -1
  328. package/dist/user-data-rights/lib/deny-if-target-outside-admin-tenant.js +1 -1
  329. package/dist/user-data-rights/lib/update-user-lifecycle.d.ts +1 -0
  330. package/dist/user-data-rights/lib/update-user-lifecycle.js +3 -3
  331. package/dist/user-data-rights/run-export-jobs.js +2 -3
  332. package/dist/user-data-rights/run-forget-cleanup.js +9 -5
  333. package/dist/user-data-rights/run-user-export.js +9 -5
  334. package/dist/waitlist/handlers/invite.write.js +1 -2
  335. package/dist/waitlist/handlers/reject.write.js +1 -1
  336. package/dist/waitlist/lib.js +1 -2
  337. package/dist/workflow-runner/event-subscriber.js +1 -1
  338. package/dist/workflow-runner/feature.d.ts +4 -0
  339. package/dist/workflow-runner/feature.js +44 -40
  340. package/dist/workflow-runner/handlers/resume-run.write.js +1 -1
  341. package/dist/workflow-runner/index.d.ts +1 -1
  342. package/dist/workflow-runner/index.js +1 -1
  343. package/dist/workflow-runner/pending-projection.js +1 -1
  344. package/package.json +28 -11
  345. package/src/cap-counter/changes.json +13 -0
  346. package/src/channel-email/changes.json +7 -0
  347. package/src/crypto-shredding/changes.json +6 -0
  348. package/src/delivery/changes.json +6 -0
  349. package/src/file-provider-s3/changes.json +10 -1
  350. package/src/mail-transport-smtp/changes.json +7 -0
  351. package/src/personal-access-tokens/changes.json +6 -0
  352. package/src/secrets/changes.json +6 -0
  353. package/src/user/changes.json +6 -0
  354. package/src/user-data-rights/changes.json +6 -0
  355. package/dist/file-provider-s3-env/feature.d.ts +0 -3
  356. package/dist/file-provider-s3-env/feature.js +0 -46
  357. /package/dist/{files-provider-s3 → file-provider-s3/lib}/env-helper.d.ts +0 -0
  358. /package/dist/{files-provider-s3 → file-provider-s3/lib}/env-helper.js +0 -0
  359. /package/dist/{files-provider-s3 → file-provider-s3/lib}/s3-provider.d.ts +0 -0
@@ -1,5 +1,5 @@
1
1
  import { deleteMany, isUniqueViolation, runInOwnTransaction, } from "@cosmicdrift/kumiko-framework/db";
2
- import { reraiseAsKumikoError } from "@cosmicdrift/kumiko-framework/errors";
2
+ import { reraiseAsKumikoError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import { appendEventInTenantDb, getStreamVersionInTenantDb, } from "@cosmicdrift/kumiko-framework/event-store";
4
4
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
5
5
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
@@ -20,6 +20,8 @@ async function insertReservationRow(txDb, user, id, draft) {
20
20
  expiresAt: now.add({ minutes: CAP_RESERVATION_TTL_MINUTES }),
21
21
  createdAt: now,
22
22
  settledAt: null,
23
+ consumedAmount: 0,
24
+ releasedAt: null,
23
25
  });
24
26
  }
25
27
  function toHandle(ctx, id, outsideTransaction) {
@@ -99,12 +101,16 @@ async function confirmCapReservation(confirm, id) {
99
101
  id,
100
102
  tenantId: confirm.tenantId,
101
103
  settledAt: null,
104
+ releasedAt: null,
102
105
  });
103
106
  }
104
107
  async function giveBackCalendar(txDb, user, row) {
105
108
  if (row.periodStartIso === null) {
106
109
  throw new Error(`cap-counter: calendar reservation ${row.id} has no period to release against`);
107
110
  }
111
+ // Fully consumed by consumeCalendarCapReservation: nothing left to give back.
112
+ if (row.amount === 0)
113
+ return NOTHING_TO_RELEASE;
108
114
  return applyCapDeltaOn(txDb, user, { capName: row.capName, periodStartIso: row.periodStartIso, amount: row.amount }, "subtract");
109
115
  }
110
116
  async function giveBackRolling(txDb, user, row) {
@@ -125,21 +131,33 @@ async function giveBackReservation(txDb, user, id) {
125
131
  if (!row)
126
132
  return NOTHING_TO_RELEASE;
127
133
  const claimed = await claimReservationRow(txDb, user, row);
128
- if (!claimed)
134
+ if (!claimed || claimed.releasedAt !== null)
129
135
  return NOTHING_TO_RELEASE;
130
- const given = row.kind === "rolling"
131
- ? await giveBackRolling(txDb, user, row)
132
- : await giveBackCalendar(txDb, user, row);
136
+ const given = claimed.kind === "rolling"
137
+ ? await giveBackRolling(txDb, user, claimed)
138
+ : await giveBackCalendar(txDb, user, claimed);
133
139
  if (!given.isSuccess)
134
140
  return given;
135
- await txDb.deleteMany(capReservationsTable, ownRow);
141
+ // Only consumed units need remembering: a later settle or consume must not count them twice.
142
+ if (claimed.kind === "rolling" || claimed.consumedAmount === 0) {
143
+ await txDb.deleteMany(capReservationsTable, ownRow);
144
+ return given;
145
+ }
146
+ const now = Temporal.Now.instant();
147
+ await txDb.updateMany(capReservationsTable, {
148
+ amount: 0,
149
+ releasedAt: now,
150
+ expiresAt: now.add({ minutes: CAP_SETTLED_TOMBSTONE_TTL_MINUTES }),
151
+ }, ownRow);
136
152
  return given;
137
153
  }
138
- // Waits for a confirm still holding the row; false means that confirm, a settle or another release
139
- // won (Postgres re-checks settled_at after the lock wait).
154
+ // Waits for a confirm still holding the row; undefined means that confirm or a settle won (Postgres
155
+ // re-checks settled_at after the lock wait). A released row is claimed too: callers branch on
156
+ // releasedAt. The returned row is read after the lock wait, so it already includes a concurrent
157
+ // consume.
140
158
  async function claimReservationRow(txDb, user, row) {
141
- const claimed = await txDb.updateMany(capReservationsTable, { expiresAt: row.expiresAt }, { id: row.id, tenantId: user.tenantId, settledAt: null });
142
- return claimed.length > 0;
159
+ const [claimed] = await txDb.updateMany(capReservationsTable, { expiresAt: row.expiresAt }, { id: row.id, tenantId: user.tenantId, settledAt: null });
160
+ return claimed;
143
161
  }
144
162
  export async function releaseCapReservation(ctx, id, outsideTransaction = true) {
145
163
  const { user, writeDb } = resolveCapCounterScope(ctx, outsideTransaction);
@@ -147,6 +165,22 @@ export async function releaseCapReservation(ctx, id, outsideTransaction = true)
147
165
  if (!released.isSuccess)
148
166
  throw reraiseAsKumikoError(released.error);
149
167
  }
168
+ export class CapSettleBelowConsumedError extends UnprocessableError {
169
+ constructor(capName, consumed, actualAmount) {
170
+ super("cap_settle_below_consumed", {
171
+ message: `Cap "${capName}": cannot settle ${actualAmount}, ${consumed} units were already consumed.`,
172
+ details: { capName, consumed, actualAmount },
173
+ });
174
+ }
175
+ }
176
+ export class CapReservationSettledError extends UnprocessableError {
177
+ constructor(capName) {
178
+ super("cap_reservation_settled", {
179
+ message: `Cap "${capName}": the reservation is already settled, consume is no longer possible.`,
180
+ details: { capName },
181
+ });
182
+ }
183
+ }
150
184
  function bookCalendarDelta(txDb, user, period, delta) {
151
185
  if (delta === 0)
152
186
  return Promise.resolve({ isSuccess: true, data: {} });
@@ -158,8 +192,13 @@ async function settleInTransaction(txDb, user, id, options) {
158
192
  ...ownRow,
159
193
  settledAt: null,
160
194
  });
161
- if (open && (await claimReservationRow(txDb, user, open))) {
162
- const booked = await bookCalendarDelta(txDb, user, options, options.actualAmount - open.amount);
195
+ const claimed = open && (await claimReservationRow(txDb, user, open));
196
+ if (claimed) {
197
+ if (options.actualAmount < claimed.consumedAmount) {
198
+ throw new CapSettleBelowConsumedError(options.capName, claimed.consumedAmount, options.actualAmount);
199
+ }
200
+ const alreadyBooked = claimed.amount + claimed.consumedAmount;
201
+ const booked = await bookCalendarDelta(txDb, user, options, options.actualAmount - alreadyBooked);
163
202
  if (!booked.isSuccess)
164
203
  return booked;
165
204
  const now = Temporal.Now.instant();
@@ -172,6 +211,11 @@ async function settleInTransaction(txDb, user, id, options) {
172
211
  }
173
212
  // Row swept or released: its amount was given back, so the full actual amount is booked and a
174
213
  // tombstone makes a repeated settle a no-op. A parallel settle inserting first fails this insert.
214
+ // Booking first: a lost counter race returns a result instead of throwing, so the transaction
215
+ // commits and a tombstone written before it would turn the retry into a no-op.
216
+ const booked = await bookCalendarDelta(txDb, user, options, options.actualAmount);
217
+ if (!booked.isSuccess)
218
+ return booked;
175
219
  const now = Temporal.Now.instant();
176
220
  await txDb.insertOne(capReservationsTable, {
177
221
  ...ownRow,
@@ -182,8 +226,10 @@ async function settleInTransaction(txDb, user, id, options) {
182
226
  expiresAt: now.add({ minutes: CAP_SETTLED_TOMBSTONE_TTL_MINUTES }),
183
227
  createdAt: now,
184
228
  settledAt: now,
229
+ consumedAmount: 0,
230
+ releasedAt: null,
185
231
  });
186
- return bookCalendarDelta(txDb, user, options, options.actualAmount);
232
+ return booked;
187
233
  }
188
234
  // The failed tombstone insert aborts the whole attempt, so nothing was booked; the repeat sees the
189
235
  // winner's tombstone.
@@ -208,8 +254,87 @@ export async function settleCalendarCapReservation(ctx, id, options, outsideTran
208
254
  if (!settled.isSuccess)
209
255
  throw reraiseAsKumikoError(settled.error);
210
256
  }
257
+ export class CapReservationOverConsumedError extends UnprocessableError {
258
+ constructor(capName, remaining, requested) {
259
+ super("cap_reservation_over_consumed", {
260
+ message: `Cap "${capName}": cannot consume ${requested} units, only ${remaining} still reserved.`,
261
+ details: { capName, remaining, requested },
262
+ });
263
+ }
264
+ }
265
+ async function consumeClaimedRow(txDb, user, claimed, options) {
266
+ const ownRow = { id: claimed.id, tenantId: user.tenantId };
267
+ if (claimed.releasedAt !== null) {
268
+ // The reserved rest was given back, so each unit is booked on top of what is still counted.
269
+ const booked = await bookCalendarDelta(txDb, user, options, options.units);
270
+ if (!booked.isSuccess)
271
+ return booked;
272
+ await txDb.updateMany(capReservationsTable, { consumedAmount: claimed.consumedAmount + options.units }, ownRow);
273
+ return booked;
274
+ }
275
+ if (options.units > claimed.amount) {
276
+ throw new CapReservationOverConsumedError(options.capName, claimed.amount, options.units);
277
+ }
278
+ await txDb.updateMany(capReservationsTable, {
279
+ amount: claimed.amount - options.units,
280
+ consumedAmount: claimed.consumedAmount + options.units,
281
+ }, ownRow);
282
+ return NOTHING_TO_RELEASE;
283
+ }
284
+ // Row already expired away: the units are booked directly and a released tombstone remembers them
285
+ // so a later settle subtracts them. A parallel consume inserting first fails this insert.
286
+ async function consumeWithoutRow(txDb, user, id, options) {
287
+ // Booking first, for the same reason as settle's missing-row path.
288
+ const booked = await bookCalendarDelta(txDb, user, options, options.units);
289
+ if (!booked.isSuccess)
290
+ return booked;
291
+ const now = Temporal.Now.instant();
292
+ await txDb.insertOne(capReservationsTable, {
293
+ id,
294
+ tenantId: user.tenantId,
295
+ capName: options.capName,
296
+ kind: "calendar",
297
+ periodStartIso: options.periodStartIso,
298
+ amount: 0,
299
+ expiresAt: now.add({ minutes: CAP_SETTLED_TOMBSTONE_TTL_MINUTES }),
300
+ createdAt: now,
301
+ settledAt: null,
302
+ consumedAmount: options.units,
303
+ releasedAt: now,
304
+ });
305
+ return booked;
306
+ }
307
+ async function consumeInTransaction(txDb, user, id, options) {
308
+ const ownRow = { id, tenantId: user.tenantId };
309
+ const open = await txDb.fetchOne(capReservationsTable, {
310
+ ...ownRow,
311
+ settledAt: null,
312
+ });
313
+ if (open && open.capName !== options.capName) {
314
+ throw new Error(`cap-counter: reservation ${id} belongs to cap "${open.capName}", not "${options.capName}"`);
315
+ }
316
+ const claimed = open && (await claimReservationRow(txDb, user, open));
317
+ if (claimed)
318
+ return consumeClaimedRow(txDb, user, claimed, options);
319
+ if (await txDb.fetchOne(capReservationsTable, ownRow)) {
320
+ throw new CapReservationSettledError(options.capName);
321
+ }
322
+ return consumeWithoutRow(txDb, user, id, options);
323
+ }
324
+ // Turns reserved units into booked ones without touching the counter; the row keeps the unconsumed
325
+ // rest so a release gives back exactly that. After a release or sweep the units are booked directly.
326
+ export async function consumeCalendarCapReservation(ctx, id, options, outsideTransaction = true) {
327
+ if (!Number.isInteger(options.units) || options.units < 1) {
328
+ throw new Error(`cap-counter: consume units must be a positive integer, got ${options.units}`);
329
+ }
330
+ const { user, writeDb } = resolveCapCounterScope(ctx, outsideTransaction);
331
+ const consumed = await retryCounterWriteOnVersionConflict(() => retryOnceOnTombstoneRace(() => runInOwnTransaction(writeDb, (txDb) => consumeInTransaction(txDb, user, id, options))));
332
+ if (!consumed.isSuccess)
333
+ throw reraiseAsKumikoError(consumed.error);
334
+ }
211
335
  // A failed release leaves its row behind; giving it back once the TTL passed bounds how long that
212
- // capacity stays counted. Expired settled tombstones are only deleted: their amount stays booked.
336
+ // capacity stays counted. Expired settled and released tombstones are only deleted: what they
337
+ // stand for stays booked.
213
338
  export async function releaseExpiredCapReservations(ctx, capName, outsideTransaction = true) {
214
339
  const { user, writeDb } = resolveCapCounterScope(ctx, outsideTransaction);
215
340
  const expiredWhere = {
@@ -221,9 +346,14 @@ export async function releaseExpiredCapReservations(ctx, capName, outsideTransac
221
346
  ...expiredWhere,
222
347
  settledAt: { ne: null },
223
348
  });
349
+ await writeDb.deleteMany(capReservationsTable, {
350
+ ...expiredWhere,
351
+ releasedAt: { ne: null },
352
+ });
224
353
  const expired = await writeDb.selectMany(capReservationsTable, {
225
354
  ...expiredWhere,
226
355
  settledAt: null,
356
+ releasedAt: null,
227
357
  });
228
358
  for (const { id } of expired) {
229
359
  try {
@@ -1,3 +1,7 @@
1
+ export declare function createCapCounterFeature(): import("@cosmicdrift/kumiko-types/feature").FeatureDefinition & {
2
+ readonly exports: void;
3
+ };
4
+ /** @deprecated Use createCapCounterFeature(). Run `bun kumiko-upgrade --apply`. Removed in the first release after 2026-10-29. */
1
5
  export declare const capCounterFeature: import("@cosmicdrift/kumiko-types/feature").FeatureDefinition & {
2
6
  readonly exports: void;
3
7
  };
@@ -53,71 +53,77 @@ import { incrementRollingCapHandler, rollingIncrementedSchema, rollingReleasedSc
53
53
  import { markSoftWarnedHandler } from "./handlers/mark-soft-warned.write.js";
54
54
  import { CAP_COUNTER_I18N } from "./i18n.js";
55
55
  import { capReservationsTableMeta } from "./tables.js";
56
- export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
57
- r.describe("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.");
58
- r.uiHints({
59
- displayLabel: "Cap Counter · Usage Limits",
60
- category: "operations",
61
- recommended: false,
62
- });
63
- r.entity({ name: "cap-counter", ...capCounterEntity });
64
- r.storeTable(capReservationsTableMeta, {
65
- reason: "coordination.cap_reservation_ledger",
66
- });
67
- // Custom Domain-Event für Rolling-Counter. r.defineEvent registriert
68
- // das Schema beim Registry; ctx.unsafeAppendEvent im Handler nutzt
69
- // dasselbe Schema für Append-Time-Validation. QN nach Prefixing:
70
- // "cap-counter:event:rolling-incremented" (siehe
71
- // ROLLING_INCREMENTED_EVENT_QN).
72
- r.defineEvent(ROLLING_INCREMENTED_EVENT_SHORT, rollingIncrementedSchema, { piiFields: "none" });
73
- r.defineEvent(ROLLING_RELEASED_EVENT_SHORT, rollingReleasedSchema, { piiFields: "none" });
74
- // Custom write-handlers.
75
- // - increment: Calendar-Period (CRUD via projection-row).
76
- // - increment-rolling: Rolling-Window (Custom-Event, no projection).
77
- // - mark-soft-warned: Anti-Notification-Storm-Flag (nur Calendar).
78
- r.writeHandler(incrementCapHandler);
79
- r.writeHandler(incrementRollingCapHandler);
80
- r.writeHandler(markSoftWarnedHandler);
81
- // Custom + standard reads. Sysadmin-cross-tenant via list, per-tenant
82
- // single-row via get-counter. Detail-by-id-handler bewusst weggelassen
83
- // (kein Use-Case; der natürliche Lookup ist über capName + period, nicht
84
- // über aggregate-id).
85
- r.queryHandler(defineEntityListHandler("cap-counter", capCounterEntity, {
86
- access: { roles: access.systemAdmin },
87
- escapeHatch: {
88
- reason: "SystemAdmin quota operator view lists usage counters of every tenant to see who consumes which cap",
89
- },
90
- description: "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.",
91
- }));
92
- r.queryHandler(getCounterQuery);
93
- r.screen({
94
- id: CAP_COUNTER_LIST_SCREEN_ID,
95
- type: "entityList",
96
- entity: "cap-counter",
97
- columns: [
98
- "tenantId",
99
- "capName",
100
- "value",
101
- "periodStart",
102
- { field: "lastSoftWarnedAt", hideOnNarrow: true },
103
- ],
104
- defaultSort: { field: "capName", dir: "asc" },
105
- // Search resolves against the caller's own tenant stream, so on this
106
- // cross-tenant operator list it would only ever hit the SystemAdmin's tenant.
107
- searchable: false,
108
- access: { roles: ["SystemAdmin"] },
109
- });
110
- r.nav({
111
- id: "cap-list",
112
- label: "cap-counter:nav.cap-list",
113
- screen: "cap-counter:screen:cap-list",
114
- icon: "gauge",
115
- order: 60,
116
- });
117
- r.translations({
118
- keys: {
119
- ...CAP_COUNTER_I18N,
120
- "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
121
- },
122
- });
123
- });
56
+ export function createCapCounterFeature() {
57
+ return defineFeature(CAP_COUNTER_FEATURE, (r) => {
58
+ r.describe("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.");
59
+ r.uiHints({
60
+ displayLabel: "Cap Counter · Usage Limits",
61
+ category: "operations",
62
+ recommended: false,
63
+ });
64
+ r.entity({ name: "cap-counter", ...capCounterEntity });
65
+ r.storeTable(capReservationsTableMeta, {
66
+ reason: "coordination.cap_reservation_ledger",
67
+ });
68
+ // Custom domain event for the rolling counter. r.defineEvent registers the
69
+ // schema with the registry; ctx.unsafeAppendEvent in the handler reuses the
70
+ // same schema for append-time validation. QN after prefixing:
71
+ // "cap-counter:event:rolling-incremented" (see
72
+ // ROLLING_INCREMENTED_EVENT_QN).
73
+ r.defineEvent(ROLLING_INCREMENTED_EVENT_SHORT, rollingIncrementedSchema, {
74
+ piiFields: "none",
75
+ });
76
+ r.defineEvent(ROLLING_RELEASED_EVENT_SHORT, rollingReleasedSchema, { piiFields: "none" });
77
+ // Custom write-handlers.
78
+ // - increment: calendar period (CRUD via projection row).
79
+ // - increment-rolling: rolling window (custom event, no projection).
80
+ // - mark-soft-warned: anti-notification-storm flag (calendar only).
81
+ r.writeHandler(incrementCapHandler);
82
+ r.writeHandler(incrementRollingCapHandler);
83
+ r.writeHandler(markSoftWarnedHandler);
84
+ // Custom + standard reads. Sysadmin cross-tenant via list, per-tenant
85
+ // single row via get-counter. Detail-by-id handler left out on purpose
86
+ // (no use case; the natural lookup is capName + period, not the
87
+ // aggregate id).
88
+ r.queryHandler(defineEntityListHandler("cap-counter", capCounterEntity, {
89
+ access: { roles: access.systemAdmin },
90
+ escapeHatch: {
91
+ reason: "SystemAdmin quota operator view lists usage counters of every tenant to see who consumes which cap",
92
+ },
93
+ description: "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.",
94
+ }));
95
+ r.queryHandler(getCounterQuery);
96
+ r.screen({
97
+ id: CAP_COUNTER_LIST_SCREEN_ID,
98
+ type: "entityList",
99
+ entity: "cap-counter",
100
+ columns: [
101
+ "tenantId",
102
+ "capName",
103
+ "value",
104
+ "periodStart",
105
+ { field: "lastSoftWarnedAt", hideOnNarrow: true },
106
+ ],
107
+ defaultSort: { field: "capName", dir: "asc" },
108
+ // Search resolves against the caller's own tenant stream, so on this
109
+ // cross-tenant operator list it would only ever hit the SystemAdmin's tenant.
110
+ searchable: false,
111
+ access: { roles: ["SystemAdmin"] },
112
+ });
113
+ r.nav({
114
+ id: "cap-list",
115
+ label: "cap-counter:nav.cap-list",
116
+ screen: "cap-counter:screen:cap-list",
117
+ icon: "gauge",
118
+ order: 60,
119
+ });
120
+ r.translations({
121
+ keys: {
122
+ ...CAP_COUNTER_I18N,
123
+ "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
124
+ },
125
+ });
126
+ }, { dedupeOptions: {} });
127
+ }
128
+ /** @deprecated Use createCapCounterFeature(). Run `bun kumiko-upgrade --apply`. Removed in the first release after 2026-10-29. */
129
+ export const capCounterFeature = createCapCounterFeature();
@@ -60,10 +60,9 @@ export const incrementRollingCapHandler = {
60
60
  // existing increment.write.ts-Cast — gleiche dispatcher-boundary.
61
61
  const payload = event.payload;
62
62
  const aggregateId = rollingCapAggregateId(event.user.tenantId, payload.capName);
63
- // unsafeAppendEvent — bundled-features-Pfad (apps mit bun kumiko
64
- // codegen kriegen den strict-typed appendEvent-Wrapper). Schema-
65
- // Validation läuft trotzdem, weil r.defineEvent das Schema
66
- // registriert hat.
63
+ // unsafeAppendEvent: the strict appendEvent is bound to an app's own event map, which
64
+ // bundled-features cannot see. Schema validation still runs because r.defineEvent
65
+ // registered the schema.
67
66
  await ctx.unsafeAppendEvent({
68
67
  aggregateId,
69
68
  aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
@@ -1,10 +1,11 @@
1
1
  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
- export { type CalendarCapUsageReservation, type CalendarCapUsageReservationSpec, reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
3
+ export { type CalendarCapReservationHandle, type CalendarCapReservationHandleSpec, type CalendarCapUsageReservation, type CalendarCapUsageReservationSpec, calendarCapReservationHandle, reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
4
+ export { CapReservationOverConsumedError, CapReservationSettledError, CapSettleBelowConsumedError, } from "./cap-reservation.js";
4
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";
5
6
  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
7
  export { capCounterEntity } from "./entity.js";
7
- export { capCounterFeature } from "./feature.js";
8
+ export { capCounterFeature, createCapCounterFeature } from "./feature.js";
8
9
  export { readPayloadField } from "./payload-field.js";
9
10
  export { type CapGuardContext, type CapLimitContext, createStockCapGuard, type StockCapGuard, type StockCapSpec, } from "./stock-cap-guard.js";
10
11
  export { capReservationsTable, capReservationsTableMeta } from "./tables.js";
@@ -1,11 +1,12 @@
1
1
  // Public API of the cap-counter bundled-feature.
2
2
  export { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js";
3
3
  export { bookCapUsage, markCapSoftWarned, readRollingCapUsage, } from "./book-cap-usage.js";
4
- export { reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
4
+ export { calendarCapReservationHandle, reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
5
+ export { CapReservationOverConsumedError, CapReservationSettledError, CapSettleBelowConsumedError, } from "./cap-reservation.js";
5
6
  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
7
  export { CAP_TOLERANCES, CapExceededError, calendarMonthPeriodEndIso, currentCalendarMonthStartIso, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, } from "./enforce-cap.js";
7
8
  export { capCounterEntity } from "./entity.js";
8
- export { capCounterFeature } from "./feature.js";
9
+ export { capCounterFeature, createCapCounterFeature } from "./feature.js";
9
10
  export { readPayloadField } from "./payload-field.js";
10
11
  export { createStockCapGuard, } from "./stock-cap-guard.js";
11
12
  export { capReservationsTable, capReservationsTableMeta } from "./tables.js";
@@ -1,9 +1,12 @@
1
1
  import { defineUnmanagedTable, index, instant, integer, table as pgTable, text, uuid, } from "@cosmicdrift/kumiko-framework/db";
2
2
  const CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX = "store_cap_reservations_tenant_id_cap_name_expires_at_idx";
3
3
  // One row per capacity that was booked before a handler transaction and is not settled yet. The
4
- // handler transaction deletes it on its own commit (confirm); a release or the TTL sweep deletes it
5
- // together with the counter decrement. A settle keeps the row as a tombstone (settled_at set) until
6
- // its shortened TTL passes, so a repeated settle or a late release finds it and does nothing.
4
+ // handler transaction deletes it on its own commit (confirm). While a calendar row is open the
5
+ // counter holds amount + consumed_amount for it. A release or the TTL sweep gives back amount and
6
+ // keeps the row as a released tombstone (released_at set, amount 0, consumed_amount still booked)
7
+ // until its shortened TTL passes, so a later consume or settle accounts for the consumed units.
8
+ // A settle keeps the row as a settled tombstone (settled_at set) the same way, so a repeated settle
9
+ // or a late release finds it and does nothing.
7
10
  // Unmanaged: the rows are a coordination aid, not state with an event history.
8
11
  export const capReservationsTable = pgTable("store_cap_reservations", {
9
12
  id: uuid("id").primaryKey(),
@@ -16,6 +19,8 @@ export const capReservationsTable = pgTable("store_cap_reservations", {
16
19
  expiresAt: instant("expires_at").notNull(),
17
20
  createdAt: instant("created_at").notNull(),
18
21
  settledAt: instant("settled_at"),
22
+ consumedAmount: integer("consumed_amount").notNull().default(0),
23
+ releasedAt: instant("released_at"),
19
24
  }, (t) => [index(CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX).on(t.tenantId, t.capName, t.expiresAt)]);
20
25
  export const capReservationsTableMeta = defineUnmanagedTable({
21
26
  tableName: "store_cap_reservations",
@@ -35,5 +40,7 @@ export const capReservationsTableMeta = defineUnmanagedTable({
35
40
  { name: "expires_at", pgType: "timestamptz", notNull: true },
36
41
  { name: "created_at", pgType: "timestamptz", notNull: true },
37
42
  { name: "settled_at", pgType: "timestamptz", notNull: false },
43
+ { name: "consumed_amount", pgType: "integer", notNull: true, defaultSql: "0" },
44
+ { name: "released_at", pgType: "timestamptz", notNull: false },
38
45
  ],
39
46
  });
@@ -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 { defineQueryHandler, mayOverrideTenant, } from "@cosmicdrift/kumiko-framework/engine";
4
3
  import { AccessDeniedError, FrameworkReasons, InternalError, } from "@cosmicdrift/kumiko-framework/errors";
5
4
  import * as z from "zod";
@@ -1,5 +1,4 @@
1
- import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import { buildEntityTable, decodeCursor, encodeCursor } from "@cosmicdrift/kumiko-framework/db";
1
+ import { buildEntityTable, decodeCursor, encodeCursor, selectMany, } from "@cosmicdrift/kumiko-framework/db";
3
2
  import { definePagedQueryHandler, MAX_LIST_LIMIT, } from "@cosmicdrift/kumiko-framework/engine";
4
3
  import { InternalError, ValidationError } from "@cosmicdrift/kumiko-framework/errors";
5
4
  import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
@@ -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";
@@ -2,15 +2,23 @@ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { type ChannelContext, type DeliveryChannel, type EmailAttachmentRef } from "../delivery/index.js";
3
3
  import type { EmailTransport } from "./types.js";
4
4
  export declare function fileAttachmentsFrom(data: Readonly<Record<string, unknown>> | undefined, messageLocale?: string): readonly EmailAttachmentRef[];
5
- export type EmailChannelOptions = {
6
- /** The app-wide transport, used for every tenant `tenantTransport` returns null for. */
5
+ /** Resolves the sending tenant's own transport, null when it has none. Built per send and
6
+ * never cached, so a provider change applies to the next mail; a throw fails the send. */
7
+ export type TenantTransportResolver = (ctx: ChannelContext) => Promise<EmailTransport | null>;
8
+ type EmailChannelTransports = {
9
+ /** The app-wide transport for every mail that does not go through a tenant transport. */
7
10
  readonly transport: EmailTransport;
8
- /** Resolves the sending tenant's own transport, null when it has none. Built per send and
9
- * never cached, so a provider change applies to the next mail; a throw fails the send. */
10
- readonly tenantTransport?: (ctx: ChannelContext) => Promise<EmailTransport | null>;
11
+ /** Used only for notifications declared with `tenantTransport: true`. */
12
+ readonly tenantTransport?: TenantTransportResolver;
13
+ } | {
14
+ readonly transport?: undefined;
15
+ readonly tenantTransport: TenantTransportResolver;
16
+ };
17
+ export type EmailChannelOptions = EmailChannelTransports & {
11
18
  readonly resolveEmail: (userId: string, ctx: {
12
19
  db: unknown;
13
20
  tenantId: TenantId;
14
21
  }) => Promise<string | null>;
15
22
  };
16
23
  export declare function createEmailChannel(options: EmailChannelOptions): DeliveryChannel;
24
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
2
2
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
3
3
  import * as z from "zod";
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 { currentNotificationType, DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE, DELIVERY_UNSUBSCRIBE_PATH, EMAIL_CHANNEL_NAME, fileAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, renderedPdfAttachmentRefSchema, } from "../delivery/index.js";
5
5
  import { decryptStoredPii, extensionForResourceContentType } from "../shared/index.js";
6
6
  import { guardEmailMessage } from "./pii-guard.js";
7
7
  // Envelope (From / Reply-To / threading headers) rides on the channel data —
@@ -121,6 +121,10 @@ async function resolveAttachments(sources, ctx) {
121
121
  }
122
122
  return { kind: "resolved", attachments: resolved };
123
123
  }
124
+ function isTenantTransportNotification(ctx, notificationType) {
125
+ const current = currentNotificationType(ctx.registry, notificationType);
126
+ return ctx.registry.getAllNotifications().get(current)?.tenantTransport === true;
127
+ }
124
128
  export function createEmailChannel(options) {
125
129
  const { transport, resolveEmail, tenantTransport } = options;
126
130
  // Render runs in the delivery.render job, decoupled from the SMTP send so
@@ -150,9 +154,11 @@ export function createEmailChannel(options) {
150
154
  ...(attachments !== undefined && { attachments }),
151
155
  };
152
156
  }
153
- async function transportFor(ctx) {
157
+ async function transportFor(message, ctx) {
154
158
  if (tenantTransport === undefined)
155
159
  return transport;
160
+ if (!isTenantTransportNotification(ctx, message.notificationType))
161
+ return transport;
156
162
  return (await tenantTransport(ctx)) ?? transport;
157
163
  }
158
164
  return {
@@ -189,7 +195,15 @@ export function createEmailChannel(options) {
189
195
  return { status: "failed", error: "send_failed", address };
190
196
  }
191
197
  const resolvedAttachments = resolution.attachments;
192
- const sendTransport = await transportFor(ctx);
198
+ const sendTransport = await transportFor(message, ctx);
199
+ if (sendTransport === undefined) {
200
+ // Configuration, not a transient error: a retry cannot conjure a transport.
201
+ createFallbackLogger("delivery").warn("no mail transport for this mail, send failed", {
202
+ tenantId: ctx.tenantId,
203
+ notificationType: message.notificationType,
204
+ });
205
+ return { status: "failed", error: "missing_credentials", address };
206
+ }
193
207
  await sendTransport.send(guardEmailMessage({
194
208
  to: address,
195
209
  subject,
@@ -4,7 +4,7 @@ import { createEmailChannel } from "./email-channel.js";
4
4
  export function createChannelEmailFeature(options) {
5
5
  const channel = createEmailChannel(options);
6
6
  return defineFeature("channel-email", (r) => {
7
- r.describe("Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails. Pass `tenantTransport: tenantMailTransport` (from `mail-foundation`) to send each tenant's mail through its own configured provider: a broken provider fails the send, a tenant without one uses the app `transport`. Notifications declared with `unsubscribe: true` get a signed `data.unsubscribeUrl` from delivery, which turns into the `List-Unsubscribe` headers.");
7
+ r.describe("Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails. Pass `tenantTransport: tenantMailTransport` (from `mail-foundation`) to send the notifications declared with `tenantTransport: true` through the sending tenant's own configured provider: a broken provider fails the send, a tenant without one uses the app `transport`; every other notification (password reset, sign-in) always goes through the app `transport`. `transport` may be left out when `tenantTransport` is set; a mail that then has no transport fails with `missing_credentials`. Notifications declared with `unsubscribe: true` get a signed `data.unsubscribeUrl` from delivery, which turns into the `List-Unsubscribe` headers.");
8
8
  r.uiHints({
9
9
  displayLabel: "Email Channel",
10
10
  category: "notifications",
@@ -1,4 +1,4 @@
1
- export { createEmailChannel, type EmailChannelOptions } from "./email-channel.js";
1
+ export { createEmailChannel, type EmailChannelOptions, type TenantTransportResolver, } from "./email-channel.js";
2
2
  export { createChannelEmailFeature } from "./feature.js";
3
3
  export { guardEmailMessage, withPiiCiphertextGuard } from "./pii-guard.js";
4
4
  export { createSmtpTransport, createSmtpTransportFromEnv, type SmtpEnv, type SmtpTransportOptions, } from "./smtp-transport.js";
@@ -1,4 +1,4 @@
1
- export { createEmailChannel } from "./email-channel.js";
1
+ export { createEmailChannel, } from "./email-channel.js";
2
2
  export { createChannelEmailFeature } from "./feature.js";
3
3
  export { guardEmailMessage, withPiiCiphertextGuard } from "./pii-guard.js";
4
4
  export { createSmtpTransport, createSmtpTransportFromEnv, } from "./smtp-transport.js";
@@ -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 * as z from "zod";
4
4
  import { inAppMessagesTable } from "../tables.js";
@@ -1,4 +1,4 @@
1
- import { updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
+ import { updateMany } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import * as z from "zod";
4
4
  import { inAppMessagesTable } from "../tables.js";