@cosmicdrift/kumiko-samples 0.366.0 → 0.368.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 (411) 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 +6 -0
  39. package/packages/bundled-features/src/channel-email/email-channel.ts +12 -2
  40. package/packages/bundled-features/src/channel-email/feature.ts +1 -1
  41. package/packages/bundled-features/src/channel-in-app/handlers/inbox.query.ts +1 -1
  42. package/packages/bundled-features/src/channel-in-app/handlers/mark-all-read.write.ts +1 -1
  43. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +1 -1
  44. package/packages/bundled-features/src/channel-in-app/handlers/unread-count.query.ts +1 -1
  45. package/packages/bundled-features/src/channel-in-app/tables.ts +1 -1
  46. package/packages/bundled-features/src/compliance-profiles/handlers/for-tenant.query.ts +1 -1
  47. package/packages/bundled-features/src/compliance-profiles/handlers/needs-profile.query.ts +1 -1
  48. package/packages/bundled-features/src/compliance-profiles/handlers/set-profile.write.ts +1 -2
  49. package/packages/bundled-features/src/compliance-profiles/resolve-for-tenant.ts +1 -1
  50. package/packages/bundled-features/src/compliance-profiles/seeding.ts +8 -4
  51. package/packages/bundled-features/src/compliance-profiles-ops/index.ts +26 -17
  52. package/packages/bundled-features/src/config/db/queries/resolver.ts +1 -1
  53. package/packages/bundled-features/src/config/handlers/reencrypt.job.ts +2 -2
  54. package/packages/bundled-features/src/config/resolver.ts +1 -1
  55. package/packages/bundled-features/src/config/write-helpers.ts +1 -1
  56. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  57. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +9 -7
  58. package/packages/bundled-features/src/custom-fields/db/queries/field-access.ts +1 -1
  59. package/packages/bundled-features/src/custom-fields/db/queries/projection.ts +1 -1
  60. package/packages/bundled-features/src/custom-fields/db/queries/quota.ts +1 -1
  61. package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +6 -2
  62. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +1 -1
  63. package/packages/bundled-features/src/custom-fields/feature.ts +11 -14
  64. package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +2 -2
  65. package/packages/bundled-features/src/custom-fields/handlers/delete-system-field.write.ts +2 -2
  66. package/packages/bundled-features/src/custom-fields/handlers/delete-tenant-field.write.ts +2 -2
  67. package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +2 -2
  68. package/packages/bundled-features/src/custom-fields/wire-for-entity.ts +4 -4
  69. package/packages/bundled-features/src/data-retention/resolve-for-tenant.ts +1 -1
  70. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +16 -10
  71. package/packages/bundled-features/src/delivery/address-opt-out.ts +5 -2
  72. package/packages/bundled-features/src/delivery/attempt-log-retention.ts +1 -1
  73. package/packages/bundled-features/src/delivery/changes.json +12 -0
  74. package/packages/bundled-features/src/delivery/channel-context.ts +2 -1
  75. package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +1 -1
  76. package/packages/bundled-features/src/delivery/db/queries/preferences.ts +1 -1
  77. package/packages/bundled-features/src/delivery/delivery-service.ts +70 -8
  78. package/packages/bundled-features/src/delivery/feature.ts +2 -2
  79. package/packages/bundled-features/src/delivery/handlers/log.query.ts +3 -2
  80. package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +1 -1
  81. package/packages/bundled-features/src/delivery/handlers/unsubscribe-address.write.ts +10 -5
  82. package/packages/bundled-features/src/delivery/index.ts +6 -0
  83. package/packages/bundled-features/src/delivery/testing.ts +2 -0
  84. package/packages/bundled-features/src/delivery/types.ts +4 -0
  85. package/packages/bundled-features/src/delivery/unsubscribe-link.ts +73 -0
  86. package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +17 -0
  87. package/packages/bundled-features/src/delivery/unsubscribe.ts +94 -31
  88. package/packages/bundled-features/src/delivery/upsert-preference.ts +5 -2
  89. package/packages/bundled-features/src/derivatives-sharp/feature.ts +21 -12
  90. package/packages/bundled-features/src/derivatives-sharp/index.ts +1 -1
  91. package/packages/bundled-features/src/document-ingest-foundation/feature.ts +112 -103
  92. package/packages/bundled-features/src/document-ingest-foundation/forget-extract-with-file-ref.ts +3 -3
  93. package/packages/bundled-features/src/document-ingest-foundation/index.ts +4 -1
  94. package/packages/bundled-features/src/feature-toggles/global-feature-state-table.ts +1 -1
  95. package/packages/bundled-features/src/feature-toggles/handlers/set.write.ts +3 -5
  96. package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +1 -1
  97. package/packages/bundled-features/src/file-derivatives/feature.ts +134 -129
  98. package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +1 -1
  99. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  100. package/packages/bundled-features/src/file-foundation/feature.ts +38 -29
  101. package/packages/bundled-features/src/file-foundation/index.ts +1 -0
  102. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +1 -1
  103. package/packages/bundled-features/src/file-provider-inmemory/feature.ts +32 -21
  104. package/packages/bundled-features/src/file-provider-inmemory/index.ts +6 -1
  105. package/packages/bundled-features/src/file-provider-s3/changes.json +10 -1
  106. package/packages/bundled-features/src/file-provider-s3/env-feature.ts +62 -0
  107. package/packages/bundled-features/src/file-provider-s3/feature.ts +107 -73
  108. package/packages/bundled-features/src/file-provider-s3/index.ts +6 -1
  109. package/packages/bundled-features/src/file-provider-s3/lib/index.ts +3 -0
  110. package/packages/bundled-features/src/{files-provider-s3 → file-provider-s3/lib}/s3-provider.ts +19 -19
  111. package/packages/bundled-features/src/file-provider-s3-env/index.ts +7 -2
  112. package/packages/bundled-features/src/files-provider-s3/index.ts +11 -3
  113. package/packages/bundled-features/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +1 -1
  114. package/packages/bundled-features/src/files-tenant-data/hooks.ts +1 -2
  115. package/packages/bundled-features/src/folders/feature.ts +8 -3
  116. package/packages/bundled-features/src/folders-user-data/index.ts +27 -18
  117. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +1 -1
  118. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +1 -1
  119. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +3 -2
  120. package/packages/bundled-features/src/form-draft/feature.ts +6 -1
  121. package/packages/bundled-features/src/form-draft/handlers/cleanup.job.ts +2 -2
  122. package/packages/bundled-features/src/form-draft/index.ts +1 -1
  123. package/packages/bundled-features/src/form-draft/lookup.ts +1 -1
  124. package/packages/bundled-features/src/form-draft-user-data/index.ts +19 -10
  125. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +165 -154
  126. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -2
  127. package/packages/bundled-features/src/inbound-mail-foundation/index.ts +1 -1
  128. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +5 -2
  129. package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +3 -2
  130. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +1 -1
  131. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +22 -13
  132. package/packages/bundled-features/src/inbound-provider-imap/index.ts +6 -1
  133. package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +20 -12
  134. package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +1 -0
  135. package/packages/bundled-features/src/jobs/db/queries/retention.ts +1 -1
  136. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +1 -1
  137. package/packages/bundled-features/src/jobs/handlers/detail.query.ts +1 -1
  138. package/packages/bundled-features/src/jobs/handlers/list.query.ts +7 -2
  139. package/packages/bundled-features/src/jobs/handlers/retry.write.ts +1 -1
  140. package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +1 -1
  141. package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +1 -1
  142. package/packages/bundled-features/src/jobs/job-run-logger.ts +11 -10
  143. package/packages/bundled-features/src/jobs/tenant-job-failure-table.ts +1 -1
  144. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +1 -1
  145. package/packages/bundled-features/src/ledger/feature.ts +6 -3
  146. package/packages/bundled-features/src/mail-foundation/feature.ts +107 -55
  147. package/packages/bundled-features/src/mail-foundation/index.ts +4 -0
  148. package/packages/bundled-features/src/mail-transport-inmemory/feature.ts +25 -17
  149. package/packages/bundled-features/src/mail-transport-inmemory/index.ts +6 -1
  150. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +91 -79
  151. package/packages/bundled-features/src/mail-transport-smtp/index.ts +1 -0
  152. package/packages/bundled-features/src/managed-pages/handlers/by-slug.query.ts +1 -1
  153. package/packages/bundled-features/src/managed-pages/handlers/by-tenant-published.query.ts +1 -2
  154. package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +1 -2
  155. package/packages/bundled-features/src/managed-pages/seeding.ts +8 -4
  156. package/packages/bundled-features/src/metrics/compute.ts +1 -1
  157. package/packages/bundled-features/src/metrics/filters.ts +1 -1
  158. package/packages/bundled-features/src/metrics/validate.ts +1 -1
  159. package/packages/bundled-features/src/notes-history/feature.ts +6 -3
  160. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
  161. package/packages/bundled-features/src/notes-history-user-data/hooks.ts +1 -1
  162. package/packages/bundled-features/src/notes-history-user-data/index.ts +27 -18
  163. package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
  164. package/packages/bundled-features/src/personal-access-tokens/feature.ts +17 -2
  165. package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +1 -1
  166. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -2
  167. package/packages/bundled-features/src/personal-access-tokens/handlers/revoke.write.ts +1 -1
  168. package/packages/bundled-features/src/personal-access-tokens/resolver.ts +1 -1
  169. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -2
  170. package/packages/bundled-features/src/personal-access-tokens/roles.ts +8 -6
  171. package/packages/bundled-features/src/personal-access-tokens/screens.ts +2 -0
  172. package/packages/bundled-features/src/readiness/feature.ts +25 -16
  173. package/packages/bundled-features/src/readiness/index.ts +1 -1
  174. package/packages/bundled-features/src/secrets/db/queries/read.ts +1 -1
  175. package/packages/bundled-features/src/secrets/feature.ts +8 -3
  176. package/packages/bundled-features/src/secrets/handlers/rotate.job.ts +2 -2
  177. package/packages/bundled-features/src/secrets/secrets-context.ts +4 -3
  178. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +1 -1
  179. package/packages/bundled-features/src/sessions/handlers/detail.query.ts +1 -1
  180. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -2
  181. package/packages/bundled-features/src/sessions/handlers/mine.query.ts +1 -1
  182. package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
  183. package/packages/bundled-features/src/sessions/handlers/revoke-all-others.write.ts +1 -1
  184. package/packages/bundled-features/src/sessions/handlers/revoke.write.ts +1 -1
  185. package/packages/bundled-features/src/sessions/session-callbacks.ts +14 -7
  186. package/packages/bundled-features/src/sessions/testing.ts +1 -1
  187. package/packages/bundled-features/src/tags/feature.ts +6 -3
  188. package/packages/bundled-features/src/template-resolver/api.ts +1 -1
  189. package/packages/bundled-features/src/template-resolver/changes.json +25 -0
  190. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +61 -9
  191. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +19 -0
  192. package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -0
  193. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +10 -2
  194. package/packages/bundled-features/src/template-resolver/handlers/by-slug.query.ts +1 -1
  195. package/packages/bundled-features/src/template-resolver/handlers/by-tenant.query.ts +1 -2
  196. package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +1 -1
  197. package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +1 -2
  198. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +1 -1
  199. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +9 -3
  200. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +1 -1
  201. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +1 -1
  202. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -2
  203. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +1 -1
  204. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +7 -2
  205. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +8 -3
  206. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +1 -1
  207. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +1 -1
  208. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +1 -1
  209. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +22 -2
  210. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +1 -1
  211. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +1 -1
  212. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +1 -1
  213. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +1 -1
  214. package/packages/bundled-features/src/template-resolver/index.ts +4 -0
  215. package/packages/bundled-features/src/template-resolver/qualified-names.ts +1 -0
  216. package/packages/bundled-features/src/template-resolver/seeding.ts +57 -10
  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 +3 -1
  219. package/packages/bundled-features/src/template-resolver/template-resources.ts +1 -1
  220. package/packages/bundled-features/src/template-resolver/template-system-only.ts +21 -0
  221. package/packages/bundled-features/src/template-resolver/template-validation.ts +48 -24
  222. package/packages/bundled-features/src/template-resolver/testing.ts +1 -1
  223. package/packages/bundled-features/src/template-resolver/web/i18n.ts +2 -0
  224. package/packages/bundled-features/src/template-resolver-user-data/index.ts +23 -14
  225. package/packages/bundled-features/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
  226. package/packages/bundled-features/src/tenant/handlers/add-member.write.ts +1 -2
  227. package/packages/bundled-features/src/tenant/handlers/cancel-invitation.write.ts +1 -1
  228. package/packages/bundled-features/src/tenant/handlers/invitations.query.ts +1 -1
  229. package/packages/bundled-features/src/tenant/handlers/me.query.ts +1 -1
  230. package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +1 -1
  231. package/packages/bundled-features/src/tenant/handlers/members.query.ts +1 -1
  232. package/packages/bundled-features/src/tenant/handlers/memberships.query.ts +1 -1
  233. package/packages/bundled-features/src/tenant/handlers/remove-member.write.ts +2 -1
  234. package/packages/bundled-features/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
  235. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +1 -1
  236. package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +1 -1
  237. package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +1 -2
  238. package/packages/bundled-features/src/tenant/invited-membership.ts +2 -2
  239. package/packages/bundled-features/src/tenant/last-tenant-admin.ts +1 -2
  240. package/packages/bundled-features/src/tenant/seeding.ts +12 -12
  241. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +1 -2
  242. package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -2
  243. package/packages/bundled-features/src/tenant-lifecycle/lib/revoke-tenant-sessions.ts +1 -1
  244. package/packages/bundled-features/src/tenant-lifecycle/lifecycle-gate.ts +1 -1
  245. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +3 -3
  246. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +10 -5
  247. package/packages/bundled-features/src/tenant-lifecycle/testing.ts +3 -3
  248. package/packages/bundled-features/src/tenant-settings/feature.ts +34 -29
  249. package/packages/bundled-features/src/tier-engine/feature.ts +426 -428
  250. package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +2 -3
  251. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +5 -2
  252. package/packages/bundled-features/src/user/changes.json +6 -0
  253. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +1 -1
  254. package/packages/bundled-features/src/user/handlers/create.write.ts +1 -2
  255. package/packages/bundled-features/src/user/handlers/find-for-auth.query.ts +1 -1
  256. package/packages/bundled-features/src/user/handlers/list.query.ts +1 -1
  257. package/packages/bundled-features/src/user/handlers/update-roles.ts +2 -1
  258. package/packages/bundled-features/src/user/index.ts +5 -1
  259. package/packages/bundled-features/src/user/invited-global-roles.ts +2 -2
  260. package/packages/bundled-features/src/user/principal-status.ts +13 -3
  261. package/packages/bundled-features/src/user/resolve-display-names.ts +1 -1
  262. package/packages/bundled-features/src/user/seeding.ts +5 -5
  263. package/packages/bundled-features/src/user-data-rights/audit-download.ts +2 -2
  264. package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
  265. package/packages/bundled-features/src/user-data-rights/db/queries/export-jobs.ts +1 -1
  266. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +1 -1
  267. package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +1 -1
  268. package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
  269. package/packages/bundled-features/src/user-data-rights/handlers/export-status.query.ts +1 -1
  270. package/packages/bundled-features/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -1
  271. package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
  272. package/packages/bundled-features/src/user-data-rights/handlers/request-export.write.ts +1 -2
  273. package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +3 -3
  274. package/packages/bundled-features/src/user-data-rights/lib/deny-if-target-outside-admin-tenant.ts +1 -1
  275. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +7 -2
  276. package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +7 -3
  277. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +11 -5
  278. package/packages/bundled-features/src/user-data-rights/run-user-export.ts +10 -5
  279. package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +1 -2
  280. package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +1 -1
  281. package/packages/bundled-features/src/waitlist/lib.ts +5 -2
  282. package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +1 -1
  283. package/packages/bundled-features/src/workflow-runner/feature.ts +58 -47
  284. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
  285. package/packages/bundled-features/src/workflow-runner/index.ts +1 -1
  286. package/packages/bundled-features/src/workflow-runner/pending-projection.ts +1 -1
  287. package/samples/apps/admin-console/src/run-config.ts +2 -2
  288. package/samples/apps/cap-billing-demo/package.json +3 -0
  289. package/samples/apps/cap-billing-demo/src/feature.ts +2 -2
  290. package/samples/apps/cap-billing-demo/src/run-config.ts +8 -8
  291. package/samples/apps/marketing-demo/src/app/seed.ts +1 -1
  292. package/samples/apps/marketing-demo/src/db/queries/seed-counts.ts +1 -1
  293. package/samples/apps/showcase/package.json +3 -0
  294. package/samples/apps/showcase/src/app/seed-items.ts +1 -1
  295. package/samples/apps/styleguide/src/app/seed.ts +2 -2
  296. package/samples/apps/styleguide/src/features/ai-settings/seed.ts +2 -2
  297. package/samples/apps/ui-walkthrough/scripts/emit-e2e-data.ts +1 -1
  298. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +1 -1
  299. package/samples/apps/ui-walkthrough/src/app/seed-rental.ts +2 -2
  300. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +2 -0
  301. package/samples/apps/ui-walkthrough/src/app/seed-vehicles.ts +1 -1
  302. package/samples/apps/ui-walkthrough/src/app/seed.ts +1 -1
  303. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +12 -0
  304. package/samples/apps/ui-walkthrough/src/db/queries/row-counts.ts +1 -1
  305. package/samples/apps/ui-walkthrough/src/db/queries/task-counts.ts +1 -1
  306. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +7 -0
  307. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +2 -0
  308. package/samples/apps/use-all-bundled/feature-manifest.json +4 -4
  309. package/samples/apps/use-all-bundled/package.json +4 -0
  310. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +2 -2
  311. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +1 -1
  312. package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +2 -2
  313. package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +1 -1
  314. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +1 -1
  315. package/samples/apps/use-all-bundled/src/app/seed.ts +11 -5
  316. package/samples/apps/use-all-bundled/src/run-config.ts +55 -56
  317. package/samples/apps/user-data-rights-demo/README.md +2 -2
  318. package/samples/apps/user-data-rights-demo/package.json +3 -0
  319. package/samples/apps/user-data-rights-demo/src/run-config.ts +7 -7
  320. package/samples/apps/workspaces/package.json +3 -0
  321. package/samples/recipes/access-control/package.json +3 -0
  322. package/samples/recipes/anonymous-access/package.json +3 -0
  323. package/samples/recipes/anonymous-access-multitenant/package.json +3 -0
  324. package/samples/recipes/apex-landing/package.json +1 -0
  325. package/samples/recipes/apex-surface-auth/package.json +3 -0
  326. package/samples/recipes/apex-surface-auth/src/feature.ts +2 -2
  327. package/samples/recipes/api-worker-split/package.json +3 -0
  328. package/samples/recipes/api-worker-split/src/feature.ts +1 -2
  329. package/samples/recipes/auth-claims/package.json +3 -0
  330. package/samples/recipes/auth-foundation-providers/src/feature.ts +1 -1
  331. package/samples/recipes/auth-mfa/package.json +3 -0
  332. package/samples/recipes/basic-entity/package.json +3 -0
  333. package/samples/recipes/chat-channels/package.json +3 -0
  334. package/samples/recipes/compliance-profiles-demo/package.json +3 -0
  335. package/samples/recipes/cross-feature-events/package.json +3 -1
  336. package/samples/recipes/cross-feature-events/src/feature.ts +1 -1
  337. package/samples/recipes/crypto-shredding-hr/package.json +3 -0
  338. package/samples/recipes/currencies-global/package.json +3 -0
  339. package/samples/recipes/currencies-per-tenant/package.json +3 -0
  340. package/samples/recipes/custom-fields-basic/package.json +3 -0
  341. package/samples/recipes/custom-handlers/package.json +3 -0
  342. package/samples/recipes/dashboard-stats/package.json +3 -0
  343. package/samples/recipes/delivery-notifications/package.json +3 -0
  344. package/samples/recipes/e2e-generator/package.json +3 -0
  345. package/samples/recipes/embedded/package.json +3 -0
  346. package/samples/recipes/embedded-entity-form/package.json +1 -0
  347. package/samples/recipes/encrypted-tenant-config/README.md +1 -1
  348. package/samples/recipes/encrypted-tenant-config/package.json +3 -0
  349. package/samples/recipes/error-contract/package.json +3 -0
  350. package/samples/recipes/event-sourcing/package.json +3 -1
  351. package/samples/recipes/event-sourcing/src/feature.ts +16 -20
  352. package/samples/recipes/event-sourcing/tsconfig.json +2 -6
  353. package/samples/recipes/feature-toggles/package.json +3 -0
  354. package/samples/recipes/field-access/package.json +3 -0
  355. package/samples/recipes/files-post-processing/package.json +1 -0
  356. package/samples/recipes/golden-path/README.md +30 -0
  357. package/samples/recipes/golden-path/package.json +11 -0
  358. package/samples/recipes/golden-path/src/feature.ts +70 -0
  359. package/samples/recipes/idempotency/package.json +3 -0
  360. package/samples/recipes/inbound-mail-triage/package.json +3 -0
  361. package/samples/recipes/lane-routing/package.json +3 -0
  362. package/samples/recipes/ledger-banking/package.json +3 -0
  363. package/samples/recipes/ledger-invoicing/package.json +3 -0
  364. package/samples/recipes/legal-pages/package.json +3 -0
  365. package/samples/recipes/lifecycle-hooks/package.json +3 -0
  366. package/samples/recipes/managed-config/package.json +3 -0
  367. package/samples/recipes/managed-pages/package.json +3 -0
  368. package/samples/recipes/notes-history-basic/package.json +3 -0
  369. package/samples/recipes/ownership/package.json +3 -0
  370. package/samples/recipes/pdf-audit-export/package.json +1 -0
  371. package/samples/recipes/pipeline-basics/package.json +3 -0
  372. package/samples/recipes/public-share-token/package.json +3 -0
  373. package/samples/recipes/public-share-token/src/feature.ts +1 -1
  374. package/samples/recipes/rate-limiting/package.json +3 -0
  375. package/samples/recipes/realtime-sse/package.json +3 -0
  376. package/samples/recipes/record-detail-layout/package.json +1 -0
  377. package/samples/recipes/reference-data/package.json +3 -0
  378. package/samples/recipes/relations/package.json +3 -0
  379. package/samples/recipes/screens-nav/package.json +3 -0
  380. package/samples/recipes/search/package.json +3 -0
  381. package/samples/recipes/secrets-demo/package.json +3 -0
  382. package/samples/recipes/security-baseline/src/feature.ts +2 -2
  383. package/samples/recipes/session-revocation/README.md +1 -1
  384. package/samples/recipes/session-revocation/package.json +3 -0
  385. package/samples/recipes/session-revocation/src/feature.ts +1 -1
  386. package/samples/recipes/state-machine/package.json +3 -1
  387. package/samples/recipes/state-machine/src/entities/invoice.ts +2 -2
  388. package/samples/recipes/state-machine/src/events.ts +19 -0
  389. package/samples/recipes/state-machine/src/feature.ts +25 -30
  390. package/samples/recipes/state-machine/src/handlers/_helpers.ts +13 -14
  391. package/samples/recipes/state-machine/src/handlers/create.ts +1 -1
  392. package/samples/recipes/state-machine/src/handlers/transitions.ts +129 -122
  393. package/samples/recipes/state-machine/src/reducer.ts +1 -1
  394. package/samples/recipes/state-machine/tsconfig.json +2 -6
  395. package/samples/recipes/tags-basic/package.json +3 -0
  396. package/samples/recipes/tenant-isolation/package.json +3 -0
  397. package/samples/recipes/tenant-settings/package.json +3 -0
  398. package/samples/recipes/tier-admin/package.json +3 -0
  399. package/samples/recipes/timezones/package.json +3 -0
  400. package/samples/recipes/tsconfig.base.json +1 -1
  401. package/samples/recipes/user-data-rights/package.json +3 -0
  402. package/samples/recipes/user-data-rights/src/feature.ts +6 -2
  403. package/samples/recipes/user-profile/package.json +3 -0
  404. package/samples/recipes/user-profile/src/feature.ts +2 -2
  405. package/samples/recipes/webhook-step/package.json +3 -0
  406. package/samples/recipes/wizard-form/package.json +1 -0
  407. package/samples/recipes/workflow-engine/package.json +3 -0
  408. package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +5 -2
  409. package/samples/recipes/writeform-section/package.json +1 -0
  410. package/packages/bundled-features/src/file-provider-s3-env/feature.ts +0 -58
  411. /package/packages/bundled-features/src/{files-provider-s3 → file-provider-s3/lib}/env-helper.ts +0 -0
@@ -6,6 +6,7 @@ import {
6
6
  signatureRoute,
7
7
  } from "@cosmicdrift/kumiko-framework/api";
8
8
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
9
+ import { mailT, resolveMailLocale } from "@cosmicdrift/kumiko-framework/i18n";
9
10
  import { escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
10
11
  import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
11
12
  import * as jose from "jose";
@@ -17,6 +18,14 @@ import {
17
18
  DeliveryErrors,
18
19
  DeliveryHandlers,
19
20
  } from "./constants.js";
21
+ import "./unsubscribe-page-strings.js";
22
+
23
+ // The mail's locale, so the pages behind the link speak the mail's language. Optional: links
24
+ // signed before the claim existed keep verifying and fall back to English.
25
+ const localeClaimSchema = z
26
+ .string()
27
+ .regex(/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{1,8})*$/)
28
+ .optional();
20
29
 
21
30
  // Shape des verified-JWT-payloads. tenantId kommt als string aus jose und
22
31
  // wird NACH erfolgreichem parse() zur Branded TenantId — kein blind-cast.
@@ -32,6 +41,7 @@ const unsubscribeJwtPayloadSchema = z.object({
32
41
  tenantId: z.string().min(1),
33
42
  notificationType: z.string().min(1),
34
43
  channel: z.string().min(1),
44
+ locale: localeClaimSchema,
35
45
  });
36
46
 
37
47
  // Address-token payload. `sub` and `addressHash` are the same value (the
@@ -45,6 +55,7 @@ const addressUnsubscribeJwtPayloadSchema = z.object({
45
55
  notificationType: z.string().min(1),
46
56
  channel: z.string().min(1),
47
57
  addressHash: z.string().min(1),
58
+ locale: localeClaimSchema,
48
59
  });
49
60
 
50
61
  export type UnsubscribeTokenPayload = {
@@ -52,6 +63,7 @@ export type UnsubscribeTokenPayload = {
52
63
  readonly tenantId: TenantId;
53
64
  readonly notificationType: string;
54
65
  readonly channel: string;
66
+ readonly locale?: string | undefined;
55
67
  };
56
68
 
57
69
  export type AddressUnsubscribeTokenPayload = {
@@ -59,6 +71,7 @@ export type AddressUnsubscribeTokenPayload = {
59
71
  readonly address: string;
60
72
  readonly notificationType: string;
61
73
  readonly channel: string;
74
+ readonly locale?: string | undefined;
62
75
  };
63
76
 
64
77
  /**
@@ -87,6 +100,11 @@ function assertUnsubscribeSecret(secret: string, caller: string): void {
87
100
  }
88
101
  }
89
102
 
103
+ // An unusable locale is left out instead of failing the send; the pages then fall back to English.
104
+ function localeClaim(locale: string | undefined): { locale?: string } {
105
+ return locale !== undefined && localeClaimSchema.safeParse(locale).success ? { locale } : {};
106
+ }
107
+
90
108
  const UNSUBSCRIBE_TOKEN_INVALID_BODY = {
91
109
  error: { code: "unsubscribe_token_invalid", message: "Invalid or expired token" },
92
110
  } as const;
@@ -101,6 +119,7 @@ export async function signUnsubscribeToken(
101
119
  tenantId: payload.tenantId,
102
120
  notificationType: payload.notificationType,
103
121
  channel: payload.channel,
122
+ ...localeClaim(payload.locale),
104
123
  })
105
124
  .setProtectedHeader({ alg: "HS256" })
106
125
  .setSubject(String(payload.userId))
@@ -136,6 +155,7 @@ export async function signAddressUnsubscribeToken(
136
155
  channel: payload.channel,
137
156
  addressHash,
138
157
  kind: "address",
158
+ ...localeClaim(payload.locale),
139
159
  })
140
160
  .setProtectedHeader({ alg: "HS256" })
141
161
  .setSubject(addressHash)
@@ -154,6 +174,7 @@ type VerifiedUnsubscribe =
154
174
  readonly addressHash: string;
155
175
  readonly notificationType: string;
156
176
  readonly channel: string;
177
+ readonly locale: string | undefined;
157
178
  }
158
179
  | {
159
180
  readonly kind: "user";
@@ -161,6 +182,7 @@ type VerifiedUnsubscribe =
161
182
  readonly userId: string;
162
183
  readonly notificationType: string;
163
184
  readonly channel: string;
185
+ readonly locale: string | undefined;
164
186
  };
165
187
 
166
188
  // RFC 8058 fixed value — both the List-Unsubscribe-Post header and the body a
@@ -183,6 +205,7 @@ async function signResubscribeToken(
183
205
  tenantId: verified.tenantId,
184
206
  notificationType: verified.notificationType,
185
207
  channel: verified.channel,
208
+ ...localeClaim(verified.locale),
186
209
  };
187
210
  const jwt =
188
211
  verified.kind === "address"
@@ -254,6 +277,7 @@ async function verifyUnsubscribeToken(
254
277
  addressHash: parsed.addressHash,
255
278
  notificationType: parsed.notificationType,
256
279
  channel: parsed.channel,
280
+ locale: parsed.locale,
257
281
  };
258
282
  }
259
283
 
@@ -274,6 +298,7 @@ async function verifyUnsubscribeToken(
274
298
  userId: parsed.sub,
275
299
  notificationType: parsed.notificationType,
276
300
  channel: parsed.channel,
301
+ locale: parsed.locale,
277
302
  };
278
303
  } catch (err) {
279
304
  if (err instanceof ExtraRouteRejection) throw err;
@@ -358,40 +383,68 @@ async function dispatchResubscribeWrite(
358
383
  return isResubscribeLimitReached(dispatched.error) ? "limit_reached" : "failed";
359
384
  }
360
385
 
361
- function confirmationPage(token: string): string {
386
+ function pageLocale(locale: string | undefined): string {
387
+ return resolveMailLocale(locale ?? "en");
388
+ }
389
+
390
+ function htmlPage(locale: string, title: string, bodyHtml: string): string {
362
391
  return `<!doctype html>
363
- <html>
392
+ <html lang="${escapeHtmlAttr(locale)}">
364
393
  <head>
365
394
  <meta charset="utf-8">
366
395
  <meta name="robots" content="noindex">
367
- <title>Unsubscribe</title>
396
+ <meta name="viewport" content="width=device-width, initial-scale=1">
397
+ <title>${escapeHtmlAttr(title)}</title>
368
398
  </head>
369
399
  <body>
370
- <p>Unsubscribe from these emails?</p>
371
- <form method="post" action="${DELIVERY_UNSUBSCRIBE_PATH}">
372
- <input type="hidden" name="token" value="${escapeHtmlAttr(token)}">
373
- <button type="submit">Unsubscribe</button>
374
- </form>
400
+ ${bodyHtml}
375
401
  </body>
376
402
  </html>`;
377
403
  }
378
404
 
379
- function unsubscribedPage(resubscribeToken: string): string {
380
- return `<!doctype html>
381
- <html>
382
- <head>
383
- <meta charset="utf-8">
384
- <meta name="robots" content="noindex">
385
- <title>Unsubscribed</title>
386
- </head>
387
- <body>
388
- <p>You have been unsubscribed.</p>
405
+ function confirmationPage(token: string, locale: string | undefined): string {
406
+ const lang = pageLocale(locale);
407
+ return htmlPage(
408
+ lang,
409
+ mailT(lang, "mail.unsubscribe.confirmTitle"),
410
+ `<p>${escapeHtmlAttr(mailT(lang, "mail.unsubscribe.confirmQuestion"))}</p>
411
+ <form method="post" action="${DELIVERY_UNSUBSCRIBE_PATH}">
412
+ <input type="hidden" name="token" value="${escapeHtmlAttr(token)}">
413
+ <button type="submit">${escapeHtmlAttr(mailT(lang, "mail.unsubscribe.confirmButton"))}</button>
414
+ </form>`,
415
+ );
416
+ }
417
+
418
+ function unsubscribedPage(resubscribeToken: string, locale: string | undefined): string {
419
+ const lang = pageLocale(locale);
420
+ return htmlPage(
421
+ lang,
422
+ mailT(lang, "mail.unsubscribe.doneTitle"),
423
+ `<p>${escapeHtmlAttr(mailT(lang, "mail.unsubscribe.doneMessage"))}</p>
389
424
  <form method="post" action="${DELIVERY_RESUBSCRIBE_PATH}">
390
425
  <input type="hidden" name="token" value="${escapeHtmlAttr(resubscribeToken)}">
391
- <button type="submit">Undo</button>
392
- </form>
393
- </body>
394
- </html>`;
426
+ <button type="submit">${escapeHtmlAttr(mailT(lang, "mail.unsubscribe.undoButton"))}</button>
427
+ </form>`,
428
+ );
429
+ }
430
+
431
+ function resubscribedPage(locale: string | undefined): string {
432
+ const lang = pageLocale(locale);
433
+ return htmlPage(
434
+ lang,
435
+ mailT(lang, "mail.unsubscribe.resubscribedTitle"),
436
+ `<p>${escapeHtmlAttr(mailT(lang, "mail.unsubscribe.resubscribedMessage"))}</p>`,
437
+ );
438
+ }
439
+
440
+ function failedPage(locale: string | undefined): string {
441
+ const lang = pageLocale(locale);
442
+ const message = mailT(lang, "mail.unsubscribe.failed");
443
+ return htmlPage(lang, message, `<p>${escapeHtmlAttr(message)}</p>`);
444
+ }
445
+
446
+ function isFormRequest(contentType: string | undefined): boolean {
447
+ return contentType?.includes("application/x-www-form-urlencoded") === true;
395
448
  }
396
449
 
397
450
  // One-click clients put the token in the query string; only the
@@ -431,7 +484,7 @@ export function createUnsubscribeRoutes(
431
484
  assertUnsubscribeSecret(options.secret, "createUnsubscribeRoutes");
432
485
  const encodedSecret = new TextEncoder().encode(options.secret);
433
486
 
434
- const confirmRoute = signatureRoute<{ token: string }>({
487
+ const confirmRoute = signatureRoute<{ token: string; locale: string | undefined }>({
435
488
  method: "GET",
436
489
  path: DELIVERY_UNSUBSCRIBE_PATH,
437
490
  entry: "signature",
@@ -444,11 +497,11 @@ export function createUnsubscribeRoutes(
444
497
  "missing unsubscribe token",
445
498
  );
446
499
  }
447
- await verifyUnsubscribeToken(token, encodedSecret, "unsubscribe");
448
- return { token };
500
+ const verified = await verifyUnsubscribeToken(token, encodedSecret, "unsubscribe");
501
+ return { token, locale: verified.locale };
449
502
  },
450
- handler: async (c, { token }) =>
451
- c.html(confirmationPage(token), 200, CONFIRMATION_PAGE_HEADERS),
503
+ handler: async (c, { token, locale }) =>
504
+ c.html(confirmationPage(token, locale), 200, CONFIRMATION_PAGE_HEADERS),
452
505
  });
453
506
 
454
507
  const writeRoute = signatureRoute<VerifiedUnsubscribe>({
@@ -463,11 +516,13 @@ export function createUnsubscribeRoutes(
463
516
  // (e.g. events-table missing, DB down) behind a misleading 400.
464
517
  const succeeded = await dispatchUnsubscribeWrite(verified, deps);
465
518
  if (!succeeded) {
466
- return c.html("Unsubscribe failed", 500, { "Cache-Control": "no-store" });
519
+ return c.html(failedPage(verified.locale), 500, { "Cache-Control": "no-store" });
467
520
  }
468
- return c.html(unsubscribedPage(await signResubscribeToken(verified, encodedSecret)), 200, {
469
- "Cache-Control": "no-store",
470
- });
521
+ return c.html(
522
+ unsubscribedPage(await signResubscribeToken(verified, encodedSecret), verified.locale),
523
+ 200,
524
+ { "Cache-Control": "no-store" },
525
+ );
471
526
  },
472
527
  });
473
528
 
@@ -482,6 +537,14 @@ export function createUnsubscribeRoutes(
482
537
  verifyUnsubscribeToken(tokenFromRequestBody(request), encodedSecret, "resubscribe"),
483
538
  handler: async (c, verified, deps) => {
484
539
  const outcome = await dispatchResubscribeWrite(verified, deps);
540
+ // The undo button on the unsubscribed page posts a form; a browser should land on a page.
541
+ if (isFormRequest(c.req.header("content-type"))) {
542
+ return outcome === "success"
543
+ ? c.html(resubscribedPage(verified.locale), 200, { "Cache-Control": "no-store" })
544
+ : c.html(failedPage(verified.locale), outcome === "limit_reached" ? 409 : 500, {
545
+ "Cache-Control": "no-store",
546
+ });
547
+ }
485
548
  if (outcome === "limit_reached") {
486
549
  return c.json({ error: { code: DeliveryErrors.resubscribeLimitReached } }, 409, {
487
550
  "Cache-Control": "no-store",
@@ -13,8 +13,11 @@
13
13
  // fall through to update, or no-op if the winner already set the target
14
14
  // `enabled` state.
15
15
 
16
- import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
17
- import { createEventStoreExecutor, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
16
+ import {
17
+ createEventStoreExecutor,
18
+ fetchOne,
19
+ type TenantDb,
20
+ } from "@cosmicdrift/kumiko-framework/db";
18
21
  import type { SessionUser, TenantId, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
19
22
  import { generateDeterministicId } from "@cosmicdrift/kumiko-framework/utils";
20
23
  import { notificationPreferenceEntity, notificationPreferencesTable } from "./tables.js";
@@ -6,17 +6,26 @@ import { renderImage } from "./render.js";
6
6
 
7
7
  const FEATURE_NAME = "derivatives-sharp";
8
8
 
9
- export const derivativesSharpFeature = defineFeature(FEATURE_NAME, (r) => {
10
- r.describe(
11
- "Registers an `image/*` renderer against the `derivativeRenderer` extension point declared by `file-derivatives`, backed by sharp. Supports resize/fit (cover, inside, contain), format conversion with quality (webp, avif, jpeg), whole-image blur, and blurring individual regions burned in before resize (see BlurRegion). EXIF orientation is applied and then normalized away, and all other EXIF (including GPS) is dropped. Server-only: sharp is a native binding — never import this from client code.",
9
+ export function createDerivativesSharpFeature() {
10
+ return defineFeature(
11
+ FEATURE_NAME,
12
+ (r) => {
13
+ r.describe(
14
+ "Registers an `image/*` renderer against the `derivativeRenderer` extension point declared by `file-derivatives`, backed by sharp. Supports resize/fit (cover, inside, contain), format conversion with quality (webp, avif, jpeg), whole-image blur, and blurring individual regions burned in before resize (see BlurRegion). EXIF orientation is applied and then normalized away, and all other EXIF (including GPS) is dropped. Server-only: sharp is a native binding — never import this from client code.",
15
+ );
16
+ r.uiHints({
17
+ displayLabel: "File Derivatives · Sharp",
18
+ category: "storage",
19
+ recommended: false,
20
+ });
21
+ r.requires({ features: ["file-derivatives"] });
22
+
23
+ const plugin: DerivativeRendererPlugin = { render: renderImage };
24
+ r.useExtension(EXT_DERIVATIVE_RENDERER, "image/*", plugin);
25
+ },
26
+ { dedupeOptions: {} },
12
27
  );
13
- r.uiHints({
14
- displayLabel: "File Derivatives · Sharp",
15
- category: "storage",
16
- recommended: false,
17
- });
18
- r.requires({ features: ["file-derivatives"] });
28
+ }
19
29
 
20
- const plugin: DerivativeRendererPlugin = { render: renderImage };
21
- r.useExtension(EXT_DERIVATIVE_RENDERER, "image/*", plugin);
22
- });
30
+ /** @deprecated Use createDerivativesSharpFeature(). Run `bun kumiko-upgrade --apply`. Removed in the first release after 2026-10-29. */
31
+ export const derivativesSharpFeature = createDerivativesSharpFeature();
@@ -1,2 +1,2 @@
1
- export { derivativesSharpFeature } from "./feature.js";
1
+ export { createDerivativesSharpFeature, derivativesSharpFeature } from "./feature.js";
2
2
  export { imageMetadata } from "./render.js";
@@ -121,108 +121,117 @@ async function requestIngestForFileRef(
121
121
  });
122
122
  }
123
123
 
124
- export const documentIngestFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
125
- r.describe(
126
- "Shared PDF/Scan/Image → normalized-text ingest primitive. Owns the `documentExtract` entity (fileRefId, storageKey, per-page text + metadata) as an implicit entity-projection, the per-tenant `ocrLanguage`/`maxPagesPerFile` config keys, the `documentIngestProvider` extension-point providers register accepted mimeTypes/size caps under, and a fileRef.created/fileRef.restored trigger that resolves the provider for a file's mimeType and requests ingest via `documentIngest.requested` tagged with the winning provider — a delete→restore round-trip re-requests ingest the same way a fresh upload does. An orphan-extract guard forgets any `documentExtract` whose fileRef is no longer live at processing time, regardless of how a provider's own write races the fileRef's delete/forget — providers should write through `writeDocumentExtractForLiveFileRef`.",
127
- );
128
- r.uiHints({
129
- displayLabel: "Document Ingest Foundation",
130
- category: "storage",
131
- recommended: false,
132
- });
133
- // tenant-lifecycle hosts EXT_TENANT_DATA — documentExtract.pages is
134
- // tenant-subject ciphertext and needs the destroy hook below.
135
- r.requires({ features: ["config", "tenant-lifecycle"] });
136
-
137
- r.entity({ name: "documentExtract", ...documentExtractEntity });
138
- r.useExtension(EXT_TENANT_DATA, "documentExtract", {
139
- destroy: documentExtractTenantDestroyHook,
140
- });
141
-
142
- // No onRegister: resolution happens at boot and per-apply, not on register.
143
- r.extendsRegistrar(EXT_DOCUMENT_INGEST_PROVIDER, {});
144
- r.bootCheck(({ features }) => validateDocumentIngestProviderWiring(features));
145
-
146
- const ocrLanguageConfigKey = r.config(
147
- "ocrLanguage",
148
- createTenantConfig("text", {
149
- default: "deu+eng",
150
- // Tesseract -l argument syntax: one or more 3-letter language codes
151
- // joined by "+" (e.g. "deu+eng"). Unconstrained free text here would
152
- // reach the OCR provider call unvalidated (#1501).
153
- pattern: { regex: "^[a-z]{3}(\\+[a-z]{3})*$" },
154
- write: access.roles("TenantAdmin", "SystemAdmin"),
155
- read: access.roles("TenantAdmin", "SystemAdmin", "User"),
156
- }),
157
- );
158
- const maxPagesPerFileConfigKey = r.config(
159
- "maxPagesPerFile",
160
- createTenantConfig("number", {
161
- default: 50,
162
- // Without a bound a tenant could set 0 (silently kills ingest) or an
163
- // unbounded value (OCR worker runs per-file for as long as the PDF
164
- // has pages — job-queue starvation) (#1501).
165
- bounds: { min: 1, max: 500 },
166
- write: access.roles("TenantAdmin", "SystemAdmin"),
167
- read: access.roles("TenantAdmin", "SystemAdmin", "User"),
168
- }),
169
- );
170
-
171
- // "fileName" can carry a real person's name, but it reaches both events already as the
172
- // fileRef's own self-subject ciphertext (copied from fileRef.created/restored), which
173
- // append() passes through; shredding the fileRef's key shreds it here too.
174
- r.defineEvent(DOCUMENT_INGEST_REQUESTED_EVENT_SHORT, documentIngestRequestedPayloadSchema, {
175
- piiFields: "none",
176
- version: 2,
177
- migrations: [
178
- {
179
- fromVersion: 1,
180
- toVersion: 2,
181
- // The sentinel can never match a real where.provider filter (providers
182
- // may not register under it) — an upcast v1 row reads as unclaimed
183
- // instead of silently misrouting to a currently mounted provider.
184
- transform: { default: { provider: UNCLAIMED_PROVIDER_SENTINEL } },
185
- },
186
- ],
187
- });
188
- r.defineEvent(DOCUMENT_INGEST_SKIPPED_EVENT_SHORT, documentIngestSkippedPayloadSchema, {
189
- piiFields: "none",
190
- });
191
-
192
- r.multiStreamProjection({
193
- name: "request-ingest",
194
- apply: {
195
- [FILE_REF_CREATED]: async (event, _tx, ctx) => {
196
- // entity-event payloads are generic Record<string, unknown> — parse
197
- // at the MSP boundary (same pattern as storage-tracking's readNumber).
198
- const parsed = fileRefIngestFieldsSchema.safeParse(event.payload);
199
- // skip: malformed / incomplete payload — don't poison the consumer
200
- if (!parsed.success) return;
201
- await requestIngestForFileRef(event, parsed.data, ctx);
202
- },
203
- [FILE_REF_RESTORED]: async (event, _tx, ctx) => {
204
- // fileRef.restored carries { previous }, the pre-restore soft-deleted
205
- // row (event-store-executor-write.ts restore()) — same field shape
206
- // as fileRef.created's payload.
207
- const parsed = fileRefIngestFieldsSchema.safeParse(event.payload["previous"]);
208
- // skip: malformed / incomplete previous snapshot — don't poison the consumer
209
- if (!parsed.success) return;
210
- await requestIngestForFileRef(event, parsed.data, ctx);
211
- },
212
- },
213
- });
214
-
215
- // fileRef delete/forget cleanup: documentExtract rows are derived data —
216
- // once the source file is gone (soft-deleted or Art.17-forgotten), the
217
- // extracted text has no reason to survive it either.
218
- r.multiStreamProjection({
219
- name: "forget-extract-with-file-ref",
220
- apply: {
221
- [FILE_REF_DELETED]: forgetExtractOnFileRefDeletedHook,
222
- [FILE_REF_FORGOTTEN]: forgetExtractOnFileRefForgottenHook,
223
- [DOCUMENT_EXTRACT_CREATED]: forgetOrphanedDocumentExtractHook,
124
+ export function createDocumentIngestFoundationFeature() {
125
+ return defineFeature(
126
+ FEATURE_NAME,
127
+ (r) => {
128
+ r.describe(
129
+ "Shared PDF/Scan/Image → normalized-text ingest primitive. Owns the `documentExtract` entity (fileRefId, storageKey, per-page text + metadata) as an implicit entity-projection, the per-tenant `ocrLanguage`/`maxPagesPerFile` config keys, the `documentIngestProvider` extension-point providers register accepted mimeTypes/size caps under, and a fileRef.created/fileRef.restored trigger that resolves the provider for a file's mimeType and requests ingest via `documentIngest.requested` tagged with the winning provider — a delete→restore round-trip re-requests ingest the same way a fresh upload does. An orphan-extract guard forgets any `documentExtract` whose fileRef is no longer live at processing time, regardless of how a provider's own write races the fileRef's delete/forget — providers should write through `writeDocumentExtractForLiveFileRef`.",
130
+ );
131
+ r.uiHints({
132
+ displayLabel: "Document Ingest Foundation",
133
+ category: "storage",
134
+ recommended: false,
135
+ });
136
+ // tenant-lifecycle hosts EXT_TENANT_DATA — documentExtract.pages is
137
+ // tenant-subject ciphertext and needs the destroy hook below.
138
+ r.requires({ features: ["config", "tenant-lifecycle"] });
139
+
140
+ r.entity({ name: "documentExtract", ...documentExtractEntity });
141
+ r.useExtension(EXT_TENANT_DATA, "documentExtract", {
142
+ destroy: documentExtractTenantDestroyHook,
143
+ });
144
+
145
+ // No onRegister: resolution happens at boot and per-apply, not on register.
146
+ r.extendsRegistrar(EXT_DOCUMENT_INGEST_PROVIDER, {});
147
+ r.bootCheck(({ features }) => validateDocumentIngestProviderWiring(features));
148
+
149
+ const ocrLanguageConfigKey = r.config(
150
+ "ocrLanguage",
151
+ createTenantConfig("text", {
152
+ default: "deu+eng",
153
+ // Tesseract -l argument syntax: one or more 3-letter language codes
154
+ // joined by "+" (e.g. "deu+eng"). Unconstrained free text here would
155
+ // reach the OCR provider call unvalidated (#1501).
156
+ pattern: { regex: "^[a-z]{3}(\\+[a-z]{3})*$" },
157
+ write: access.roles("TenantAdmin", "SystemAdmin"),
158
+ read: access.roles("TenantAdmin", "SystemAdmin", "User"),
159
+ }),
160
+ );
161
+ const maxPagesPerFileConfigKey = r.config(
162
+ "maxPagesPerFile",
163
+ createTenantConfig("number", {
164
+ default: 50,
165
+ // Without a bound a tenant could set 0 (silently kills ingest) or an
166
+ // unbounded value (OCR worker runs per-file for as long as the PDF
167
+ // has pages — job-queue starvation) (#1501).
168
+ bounds: { min: 1, max: 500 },
169
+ write: access.roles("TenantAdmin", "SystemAdmin"),
170
+ read: access.roles("TenantAdmin", "SystemAdmin", "User"),
171
+ }),
172
+ );
173
+
174
+ // "fileName" can carry a real person's name, but it reaches both events already as the
175
+ // fileRef's own self-subject ciphertext (copied from fileRef.created/restored), which
176
+ // append() passes through; shredding the fileRef's key shreds it here too.
177
+ r.defineEvent(DOCUMENT_INGEST_REQUESTED_EVENT_SHORT, documentIngestRequestedPayloadSchema, {
178
+ piiFields: "none",
179
+ version: 2,
180
+ migrations: [
181
+ {
182
+ fromVersion: 1,
183
+ toVersion: 2,
184
+ // The sentinel can never match a real where.provider filter (providers
185
+ // may not register under it) — an upcast v1 row reads as unclaimed
186
+ // instead of silently misrouting to a currently mounted provider.
187
+ transform: { default: { provider: UNCLAIMED_PROVIDER_SENTINEL } },
188
+ },
189
+ ],
190
+ });
191
+ r.defineEvent(DOCUMENT_INGEST_SKIPPED_EVENT_SHORT, documentIngestSkippedPayloadSchema, {
192
+ piiFields: "none",
193
+ });
194
+
195
+ r.multiStreamProjection({
196
+ name: "request-ingest",
197
+ apply: {
198
+ [FILE_REF_CREATED]: async (event, _tx, ctx) => {
199
+ // entity-event payloads are generic Record<string, unknown> — parse
200
+ // at the MSP boundary (same pattern as storage-tracking's readNumber).
201
+ const parsed = fileRefIngestFieldsSchema.safeParse(event.payload);
202
+ // skip: malformed / incomplete payload — don't poison the consumer
203
+ if (!parsed.success) return;
204
+ await requestIngestForFileRef(event, parsed.data, ctx);
205
+ },
206
+ [FILE_REF_RESTORED]: async (event, _tx, ctx) => {
207
+ // fileRef.restored carries { previous }, the pre-restore soft-deleted
208
+ // row (event-store-executor-write.ts restore()) — same field shape
209
+ // as fileRef.created's payload.
210
+ const parsed = fileRefIngestFieldsSchema.safeParse(event.payload["previous"]);
211
+ // skip: malformed / incomplete previous snapshot — don't poison the consumer
212
+ if (!parsed.success) return;
213
+ await requestIngestForFileRef(event, parsed.data, ctx);
214
+ },
215
+ },
216
+ });
217
+
218
+ // fileRef delete/forget cleanup: documentExtract rows are derived data —
219
+ // once the source file is gone (soft-deleted or Art.17-forgotten), the
220
+ // extracted text has no reason to survive it either.
221
+ r.multiStreamProjection({
222
+ name: "forget-extract-with-file-ref",
223
+ apply: {
224
+ [FILE_REF_DELETED]: forgetExtractOnFileRefDeletedHook,
225
+ [FILE_REF_FORGOTTEN]: forgetExtractOnFileRefForgottenHook,
226
+ [DOCUMENT_EXTRACT_CREATED]: forgetOrphanedDocumentExtractHook,
227
+ },
228
+ });
229
+
230
+ return { ocrLanguageConfigKey, maxPagesPerFileConfigKey };
224
231
  },
225
- });
232
+ { dedupeOptions: {} },
233
+ );
234
+ }
226
235
 
227
- return { ocrLanguageConfigKey, maxPagesPerFileConfigKey };
228
- });
236
+ /** @deprecated Use createDocumentIngestFoundationFeature(). Run `bun kumiko-upgrade --apply`. Removed in the first release after 2026-10-29. */
237
+ export const documentIngestFoundationFeature = createDocumentIngestFoundationFeature();
@@ -57,7 +57,7 @@ export const forgetOrphanedDocumentExtractHook: MultiStreamApplyFn = async (even
57
57
  const parsed = documentExtractCreatedPayloadSchema.safeParse(event.payload);
58
58
  // skip: malformed payload — don't poison the consumer
59
59
  if (!parsed.success) return;
60
- const tenantDb = createTenantDb(tx, event.tenantId);
60
+ const tenantDb = createTenantDb({ db: tx, tenantId: event.tenantId });
61
61
  // skip: fileRef still live — the extract is legitimate
62
62
  if (await isFileRefLive(tenantDb, parsed.data.fileRefId)) return;
63
63
  const user = createSystemUser(event.tenantId);
@@ -70,7 +70,7 @@ export const forgetOrphanedDocumentExtractHook: MultiStreamApplyFn = async (even
70
70
  };
71
71
 
72
72
  export const forgetExtractOnFileRefDeletedHook: MultiStreamApplyFn = async (event, tx) => {
73
- const tenantDb = createTenantDb(tx, event.tenantId);
73
+ const tenantDb = createTenantDb({ db: tx, tenantId: event.tenantId });
74
74
  // skip: fileRef is live again — request-ingest and this consumer poll via
75
75
  // separate cursors, so a delete→restore round-trip can finish before this
76
76
  // consumer sees fileRef.deleted; forgetting would drop the live file's extract.
@@ -79,6 +79,6 @@ export const forgetExtractOnFileRefDeletedHook: MultiStreamApplyFn = async (even
79
79
  };
80
80
 
81
81
  export const forgetExtractOnFileRefForgottenHook: MultiStreamApplyFn = async (event, tx) => {
82
- const tenantDb = createTenantDb(tx, event.tenantId);
82
+ const tenantDb = createTenantDb({ db: tx, tenantId: event.tenantId });
83
83
  await forgetExtractsForFileRef(event, tenantDb);
84
84
  };
@@ -18,7 +18,10 @@ export {
18
18
  documentIngestRequestedPayloadSchema,
19
19
  documentIngestSkippedPayloadSchema,
20
20
  } from "./events.js";
21
- export { documentIngestFoundationFeature } from "./feature.js";
21
+ export {
22
+ createDocumentIngestFoundationFeature,
23
+ documentIngestFoundationFeature,
24
+ } from "./feature.js";
22
25
  export { readIngestPages, writeIngestPages } from "./pages.js";
23
26
  export {
24
27
  type DocumentIngestProviderOptions,
@@ -1,4 +1,3 @@
1
- import { requireEntityTableMeta } from "@cosmicdrift/kumiko-framework/bun-db";
2
1
  import {
3
2
  boolean,
4
3
  declareGlobalTenancy,
@@ -6,6 +5,7 @@ import {
6
5
  instant,
7
6
  integer,
8
7
  table as pgTable,
8
+ requireEntityTableMeta,
9
9
  type SchemaTable,
10
10
  sql,
11
11
  text,
@@ -1,4 +1,4 @@
1
- import { insertOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
+ import { insertOne, selectMany } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { defineWriteHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import {
4
4
  UnprocessableError,
@@ -158,10 +158,8 @@ export function createSetWriteHandler(getRuntime: (() => GlobalFeatureToggleRunt
158
158
  // and filtering by payload.featureName is trivial at query time.
159
159
  // This mirrors how `config` handles the same constraint for
160
160
  // its config-changed events.
161
- // unsafeAppendEvent — bundled-features ohne lokalen Wrapper. Apps
162
- // mit `bun kumiko codegen` kriegen `.kumiko/define.ts` als strict-
163
- // path; bundled-features bleibt bei der unsafe-Variante. Schema-
164
- // Validation läuft trotzdem via r.defineEvent("toggle-set", ...).
161
+ // unsafeAppendEvent: the strict appendEvent is bound to an app's own event map, which
162
+ // bundled-features cannot see. Schema validation still runs via r.defineEvent("toggle-set", ...).
165
163
  await ctx.unsafeAppendEvent({
166
164
  aggregateId: SYSTEM_TENANT_ID,
167
165
  aggregateType: FEATURE_TOGGLE_AGGREGATE_TYPE,
@@ -1,5 +1,5 @@
1
- import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
1
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
+ import { selectMany } from "@cosmicdrift/kumiko-framework/db";
3
3
  import {
4
4
  computeEffectiveFeatures,
5
5
  type Registry,