@cosmicdrift/kumiko-bundled-features 0.386.0 → 0.388.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 (643) hide show
  1. package/LICENSE +21 -57
  2. package/dist/agent-tools/tool-catalog.js +1 -1
  3. package/dist/agent-tools/tool-dispatch.d.ts +7 -7
  4. package/dist/agent-tools/tool-dispatch.js +9 -9
  5. package/dist/audit/handlers/audit-events.js +1 -1
  6. package/dist/audit/screens.js +1 -1
  7. package/dist/auth-email-password/feature.js +1 -1
  8. package/dist/auth-email-password/handlers/change-password.write.js +3 -3
  9. package/dist/auth-email-password/handlers/confirm-token-flow.js +5 -5
  10. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +1 -1
  11. package/dist/auth-email-password/handlers/invite-accept.write.js +18 -18
  12. package/dist/auth-email-password/handlers/invite-create.write.d.ts +2 -2
  13. package/dist/auth-email-password/handlers/invite-create.write.js +10 -10
  14. package/dist/auth-email-password/handlers/invite-info.query.js +1 -1
  15. package/dist/auth-email-password/handlers/invite-signup-complete.write.js +24 -26
  16. package/dist/auth-email-password/handlers/login.write.d.ts +1 -1
  17. package/dist/auth-email-password/handlers/login.write.js +1 -1
  18. package/dist/auth-email-password/handlers/self-registration-status.query.js +1 -1
  19. package/dist/auth-email-password/handlers/signup-confirm.write.js +34 -37
  20. package/dist/auth-email-password/handlers/signup-request.write.js +6 -7
  21. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.d.ts +1 -1
  22. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +1 -4
  23. package/dist/auth-email-password/index.js +5 -5
  24. package/dist/auth-email-password/lockout-store.js +1 -1
  25. package/dist/auth-email-password/seeding.d.ts +42 -45
  26. package/dist/auth-email-password/seeding.js +51 -53
  27. package/dist/auth-email-password/signup-token-store.js +8 -8
  28. package/dist/auth-email-password/testing.js +3 -3
  29. package/dist/auth-email-password/web/auth-client.d.ts +2 -2
  30. package/dist/auth-email-password/web/auth-client.js +36 -37
  31. package/dist/auth-email-password/web/auth-form-primitives.js +10 -10
  32. package/dist/auth-email-password/web/auth-gate.js +15 -16
  33. package/dist/auth-email-password/web/client-plugin.d.ts +7 -7
  34. package/dist/auth-email-password/web/client-plugin.js +4 -5
  35. package/dist/auth-email-password/web/confirm-account-unlock-screen.js +1 -1
  36. package/dist/auth-email-password/web/constants.js +3 -3
  37. package/dist/auth-email-password/web/default-topbar-actions.d.ts +8 -8
  38. package/dist/auth-email-password/web/default-topbar-actions.js +6 -6
  39. package/dist/auth-email-password/web/forgot-password-screen.d.ts +2 -2
  40. package/dist/auth-email-password/web/forgot-password-screen.js +9 -9
  41. package/dist/auth-email-password/web/index.js +5 -5
  42. package/dist/auth-email-password/web/invite-accept-screen.d.ts +1 -1
  43. package/dist/auth-email-password/web/invite-accept-screen.js +16 -16
  44. package/dist/auth-email-password/web/login-screen.d.ts +1 -1
  45. package/dist/auth-email-password/web/login-screen.js +2 -3
  46. package/dist/auth-email-password/web/reset-password-screen.d.ts +4 -4
  47. package/dist/auth-email-password/web/reset-password-screen.js +12 -13
  48. package/dist/auth-email-password/web/session.d.ts +10 -9
  49. package/dist/auth-email-password/web/session.js +28 -29
  50. package/dist/auth-email-password/web/signup-complete-screen.js +3 -3
  51. package/dist/auth-email-password/web/signup-screen.d.ts +1 -1
  52. package/dist/auth-email-password/web/signup-screen.js +13 -13
  53. package/dist/auth-email-password/web/tenant-switcher.d.ts +4 -4
  54. package/dist/auth-email-password/web/tenant-switcher.js +21 -22
  55. package/dist/auth-email-password/web/use-shell-user.js +12 -13
  56. package/dist/auth-email-password/web/user-menu.d.ts +4 -4
  57. package/dist/auth-email-password/web/user-menu.js +12 -13
  58. package/dist/auth-email-password/web/verify-email-screen.d.ts +2 -2
  59. package/dist/auth-email-password/web/verify-email-screen.js +9 -10
  60. package/dist/auth-foundation/boot-checks.js +4 -4
  61. package/dist/auth-foundation/resolve-anonymous-access.d.ts +1 -1
  62. package/dist/auth-foundation/resolve-anonymous-access.js +3 -3
  63. package/dist/auth-foundation/types.js +2 -2
  64. package/dist/auth-mfa/feature-exports.d.ts +2 -2
  65. package/dist/auth-mfa/feature.js +8 -0
  66. package/dist/auth-mfa/handlers/disable.write.js +3 -3
  67. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +4 -4
  68. package/dist/auth-mfa/handlers/enable-confirm.write.js +2 -2
  69. package/dist/auth-mfa/handlers/enable-start-preauth.write.js +1 -1
  70. package/dist/auth-mfa/handlers/reencrypt.job.js +1 -1
  71. package/dist/auth-mfa/handlers/regenerate-recovery.write.js +5 -5
  72. package/dist/auth-mfa/handlers/verify.write.js +1 -1
  73. package/dist/auth-mfa/token-secrets.js +1 -1
  74. package/dist/auth-mfa/web/client-plugin.d.ts +1 -1
  75. package/dist/auth-mfa/web/client-plugin.js +1 -1
  76. package/dist/auth-mfa/web/i18n.js +8 -4
  77. package/dist/auth-mfa/web/index.js +4 -4
  78. package/dist/auth-mfa/web/mfa-client.js +17 -16
  79. package/dist/auth-mfa/web/mfa-code-or-recovery-field.d.ts +11 -0
  80. package/dist/auth-mfa/web/mfa-code-or-recovery-field.js +12 -0
  81. package/dist/auth-mfa/web/mfa-disable-dialog.js +14 -3
  82. package/dist/auth-mfa/web/mfa-regenerate-recovery-dialog.js +14 -3
  83. package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +10 -5
  84. package/dist/auth-mfa/web/mfa-verify-screen.js +32 -11
  85. package/dist/billing-foundation/aggregate-id.d.ts +5 -5
  86. package/dist/billing-foundation/aggregate-id.js +10 -11
  87. package/dist/billing-foundation/checkout-core.d.ts +9 -14
  88. package/dist/billing-foundation/checkout-core.js +24 -39
  89. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +8 -2
  90. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +1 -0
  91. package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -0
  92. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +9 -3
  93. package/dist/billing-foundation/consumer-protection/mail-notifications.js +3 -1
  94. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +4 -2
  95. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +6 -5
  96. package/dist/billing-foundation/consumer-protection/termination-record.d.ts +5 -1
  97. package/dist/billing-foundation/consumer-protection/termination-record.js +27 -17
  98. package/dist/billing-foundation/consumer-protection/termination-request.js +12 -7
  99. package/dist/billing-foundation/consumer-protection/termination-revoke.js +12 -7
  100. package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
  101. package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -0
  102. package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +5 -2
  103. package/dist/billing-foundation/consumer-protection/withdrawal-period.js +8 -9
  104. package/dist/billing-foundation/entities.js +27 -27
  105. package/dist/billing-foundation/events.d.ts +1 -0
  106. package/dist/billing-foundation/events.js +19 -17
  107. package/dist/billing-foundation/feature.js +45 -44
  108. package/dist/billing-foundation/get-subscription-for-tenant.d.ts +3 -3
  109. package/dist/billing-foundation/get-subscription-for-tenant.js +3 -5
  110. package/dist/billing-foundation/handlers/billing-plans.query.js +35 -5
  111. package/dist/billing-foundation/handlers/create-checkout-session.write.js +19 -17
  112. package/dist/billing-foundation/handlers/create-portal-session.write.js +11 -12
  113. package/dist/billing-foundation/handlers/list-subscriptions.query.js +4 -4
  114. package/dist/billing-foundation/handlers/plan-switch-preview.query.d.ts +13 -0
  115. package/dist/billing-foundation/handlers/plan-switch-preview.query.js +48 -0
  116. package/dist/billing-foundation/handlers/process-event.write.d.ts +6 -0
  117. package/dist/billing-foundation/handlers/process-event.write.js +28 -22
  118. package/dist/billing-foundation/handlers/start-plan-checkout.write.js +32 -5
  119. package/dist/billing-foundation/handlers/switch-plan.write.d.ts +2 -2
  120. package/dist/billing-foundation/handlers/switch-plan.write.js +56 -64
  121. package/dist/billing-foundation/handlers/sync-subscription.write.js +12 -1
  122. package/dist/billing-foundation/i18n.js +22 -0
  123. package/dist/billing-foundation/index.d.ts +1 -1
  124. package/dist/billing-foundation/plan-catalog.d.ts +21 -5
  125. package/dist/billing-foundation/plan-catalog.js +25 -16
  126. package/dist/billing-foundation/plan-switch.d.ts +12 -0
  127. package/dist/billing-foundation/plan-switch.js +60 -0
  128. package/dist/billing-foundation/projection.d.ts +15 -11
  129. package/dist/billing-foundation/projection.js +38 -37
  130. package/dist/billing-foundation/subscription-tier-sync.js +3 -3
  131. package/dist/billing-foundation/tenant-destroy-hook.d.ts +2 -2
  132. package/dist/billing-foundation/tenant-destroy-hook.js +7 -7
  133. package/dist/billing-foundation/types.d.ts +97 -42
  134. package/dist/billing-foundation/types.js +29 -20
  135. package/dist/billing-foundation/validate-options.js +17 -0
  136. package/dist/billing-foundation/web/billing-plans-panel.js +82 -36
  137. package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +17 -3
  138. package/dist/billing-foundation/web/checkout-consent-dialog.js +14 -10
  139. package/dist/billing-foundation/web/constants.d.ts +4 -0
  140. package/dist/billing-foundation/web/constants.js +4 -0
  141. package/dist/billing-foundation/web/plan-price.d.ts +15 -0
  142. package/dist/billing-foundation/web/plan-price.js +32 -0
  143. package/dist/billing-foundation/web/plan-switch-confirm-dialog.d.ts +15 -0
  144. package/dist/billing-foundation/web/plan-switch-confirm-dialog.js +66 -0
  145. package/dist/cap-counter/aggregate-id.js +8 -9
  146. package/dist/cap-counter/cap-reservation.js +5 -1
  147. package/dist/cap-counter/constants.js +11 -12
  148. package/dist/cap-counter/enforce-cap.d.ts +59 -62
  149. package/dist/cap-counter/enforce-cap.js +49 -49
  150. package/dist/cap-counter/entity.js +29 -29
  151. package/dist/cap-counter/feature.js +34 -35
  152. package/dist/cap-counter/handlers/increment-rolling.write.d.ts +2 -2
  153. package/dist/cap-counter/handlers/increment-rolling.write.js +31 -31
  154. package/dist/cap-counter/handlers/mark-soft-warned.write.js +4 -5
  155. package/dist/cap-counter/stock-cap-guard.js +8 -2
  156. package/dist/cap-counter/with-cap-enforcement.d.ts +3 -5
  157. package/dist/channel-email/feature.js +1 -1
  158. package/dist/channel-email/smtp-transport.d.ts +18 -17
  159. package/dist/channel-email/smtp-transport.js +24 -21
  160. package/dist/channel-in-app/handlers/unread-count.query.js +3 -4
  161. package/dist/compliance-profiles/feature.js +4 -4
  162. package/dist/compliance-profiles/handlers/for-tenant.query.js +3 -6
  163. package/dist/compliance-profiles/handlers/list-profiles.query.js +5 -5
  164. package/dist/compliance-profiles/handlers/needs-profile.query.js +4 -4
  165. package/dist/compliance-profiles/handlers/set-profile.write.js +13 -14
  166. package/dist/compliance-profiles/handlers/sub-processors.query.js +9 -11
  167. package/dist/compliance-profiles/resolve-for-tenant.js +7 -7
  168. package/dist/compliance-profiles/schema/profile-selection.js +18 -23
  169. package/dist/compliance-profiles/seeding.js +14 -15
  170. package/dist/compliance-profiles-ops/handlers/tenants-missing-profile.query.js +3 -3
  171. package/dist/compliance-profiles-ops/index.js +2 -2
  172. package/dist/config/handlers/cascade.query.d.ts +5 -0
  173. package/dist/config/handlers/cascade.query.js +4 -3
  174. package/dist/config/handlers/readiness.query.d.ts +4 -4
  175. package/dist/config/handlers/readiness.query.js +1 -1
  176. package/dist/config/handlers/reencrypt.job.js +4 -5
  177. package/dist/config/handlers/set.write.js +4 -4
  178. package/dist/config/handlers/values.query.d.ts +5 -0
  179. package/dist/config/handlers/values.query.js +4 -3
  180. package/dist/config/resolver.js +4 -4
  181. package/dist/config/table.js +10 -10
  182. package/dist/config/write-helpers.d.ts +1 -0
  183. package/dist/config/write-helpers.js +19 -10
  184. package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +2 -2
  185. package/dist/crypto-shredding/handlers/forget-subject.write.js +25 -21
  186. package/dist/custom-fields/aggregate-id.d.ts +12 -13
  187. package/dist/custom-fields/aggregate-id.js +16 -17
  188. package/dist/custom-fields/entity.js +49 -47
  189. package/dist/custom-fields/events.js +7 -8
  190. package/dist/custom-fields/feature.d.ts +14 -14
  191. package/dist/custom-fields/feature.js +53 -54
  192. package/dist/custom-fields/handlers/clear-custom-field.write.d.ts +3 -2
  193. package/dist/custom-fields/handlers/clear-custom-field.write.js +9 -8
  194. package/dist/custom-fields/handlers/define-tenant-field.write.d.ts +1 -1
  195. package/dist/custom-fields/handlers/define-tenant-field.write.js +2 -2
  196. package/dist/custom-fields/handlers/delete-system-field.write.js +6 -7
  197. package/dist/custom-fields/handlers/delete-tenant-field.write.d.ts +1 -1
  198. package/dist/custom-fields/handlers/delete-tenant-field.write.js +3 -3
  199. package/dist/custom-fields/handlers/set-custom-field.write.d.ts +3 -3
  200. package/dist/custom-fields/handlers/set-custom-field.write.js +25 -28
  201. package/dist/custom-fields/handlers/update-tenant-field.write.d.ts +1 -1
  202. package/dist/custom-fields/handlers/update-tenant-field.write.js +1 -1
  203. package/dist/custom-fields/lib/field-access.js +2 -2
  204. package/dist/custom-fields/lib/parse-serialized-field.js +1 -1
  205. package/dist/custom-fields/lib/quota.js +1 -1
  206. package/dist/custom-fields/lib/value-schema.js +10 -12
  207. package/dist/custom-fields/run-retention.js +3 -3
  208. package/dist/custom-fields/schemas.js +14 -14
  209. package/dist/custom-fields/web/client-plugin.js +2 -5
  210. package/dist/custom-fields/web/constants.js +2 -2
  211. package/dist/custom-fields/web/custom-fields-form-section.d.ts +4 -4
  212. package/dist/custom-fields/web/custom-fields-form-section.js +22 -22
  213. package/dist/custom-fields/wire-for-entity.js +65 -67
  214. package/dist/custom-fields/wire-user-data-rights.js +3 -3
  215. package/dist/data-retention/_internal/parse-override.js +2 -3
  216. package/dist/data-retention/feature.js +11 -18
  217. package/dist/data-retention/handlers/policy-for.query.js +6 -6
  218. package/dist/data-retention/keep-for.d.ts +7 -7
  219. package/dist/data-retention/keep-for.js +15 -15
  220. package/dist/data-retention/override-schema.d.ts +3 -4
  221. package/dist/data-retention/override-schema.js +13 -15
  222. package/dist/data-retention/presets.d.ts +4 -5
  223. package/dist/data-retention/presets.js +18 -23
  224. package/dist/data-retention/resolve-tenant-preset.js +17 -16
  225. package/dist/data-retention/resolver.d.ts +12 -12
  226. package/dist/data-retention/resolver.js +24 -23
  227. package/dist/data-retention/run-retention-cleanup.d.ts +8 -3
  228. package/dist/data-retention/run-retention-cleanup.js +61 -51
  229. package/dist/data-retention/schema/tenant-retention-override.js +13 -11
  230. package/dist/delivery/channel-context.js +6 -3
  231. package/dist/delivery/db/queries/preferences.js +2 -2
  232. package/dist/delivery/delivery-service.js +22 -7
  233. package/dist/delivery/feature.js +2 -2
  234. package/dist/delivery/handlers/log.query.js +1 -1
  235. package/dist/delivery/handlers/preferences.query.d.ts +1 -1
  236. package/dist/delivery/handlers/preferences.query.js +20 -1
  237. package/dist/delivery/handlers/unsubscribe-page-chrome.query.js +1 -1
  238. package/dist/delivery/tables.js +9 -8
  239. package/dist/delivery/unsubscribe-link.d.ts +3 -1
  240. package/dist/delivery/unsubscribe-page-strings.d.ts +1 -0
  241. package/dist/delivery/unsubscribe-page-strings.js +1 -0
  242. package/dist/delivery/unsubscribe-page.d.ts +2 -0
  243. package/dist/delivery/unsubscribe-page.js +5 -0
  244. package/dist/delivery/unsubscribe.js +29 -11
  245. package/dist/document-ingest-foundation/entity.js +4 -4
  246. package/dist/document-ingest-foundation/events.js +2 -2
  247. package/dist/document-ingest-foundation/feature.js +2 -2
  248. package/dist/document-ingest-foundation/pages.js +1 -1
  249. package/dist/document-ingest-foundation/tenant-destroy-hook.js +4 -4
  250. package/dist/feature-toggles/compose-tier-resolver.js +1 -1
  251. package/dist/feature-toggles/feature.js +1 -1
  252. package/dist/feature-toggles/global-feature-state-table.js +1 -1
  253. package/dist/feature-toggles/handlers/registered.query.js +5 -5
  254. package/dist/feature-toggles/toggle-sync-signal.js +1 -1
  255. package/dist/feature-toggles/web/constants.js +1 -1
  256. package/dist/file-derivatives/feature.d.ts +1 -1
  257. package/dist/file-derivatives/feature.js +1 -1
  258. package/dist/file-derivatives/handlers/public-variant.query.js +1 -1
  259. package/dist/file-derivatives/presets.js +1 -1
  260. package/dist/file-foundation/feature.js +6 -6
  261. package/dist/file-foundation/index.d.ts +1 -1
  262. package/dist/file-foundation/index.js +1 -1
  263. package/dist/file-foundation/read-file-ref-content.d.ts +3 -0
  264. package/dist/file-foundation/read-file-ref-content.js +6 -1
  265. package/dist/file-provider-inmemory/feature.d.ts +1 -1
  266. package/dist/file-provider-inmemory/feature.js +16 -16
  267. package/dist/file-provider-s3/env-named-feature.d.ts +0 -2
  268. package/dist/file-provider-s3/feature.js +16 -16
  269. package/dist/file-provider-s3/lib/s3-provider.js +6 -6
  270. package/dist/files/feature.js +6 -6
  271. package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +4 -4
  272. package/dist/files-tenant-data/hooks.js +24 -26
  273. package/dist/files-tenant-data/index.js +1 -1
  274. package/dist/folders/feature.d.ts +1 -1
  275. package/dist/folders/web/folder-manager.js +2 -2
  276. package/dist/folders/web/tree.js +5 -5
  277. package/dist/form-draft/db/queries/cleanup.js +3 -3
  278. package/dist/form-draft/db/queries/owned-file-refs.js +1 -1
  279. package/dist/form-draft/decrypt-draft.js +5 -1
  280. package/dist/form-draft/entity.js +3 -3
  281. package/dist/form-draft/feature.js +4 -5
  282. package/dist/form-draft/handlers/discard.write.js +1 -1
  283. package/dist/form-draft/release-file-refs.js +6 -6
  284. package/dist/form-draft/schemas.js +3 -3
  285. package/dist/form-draft/web/constants.js +0 -2
  286. package/dist/host-routing/dispatch.js +11 -20
  287. package/dist/inbound-mail-foundation/aggregate-id.d.ts +13 -13
  288. package/dist/inbound-mail-foundation/aggregate-id.js +19 -19
  289. package/dist/inbound-mail-foundation/connect-routes.js +4 -3
  290. package/dist/inbound-mail-foundation/constants.d.ts +22 -24
  291. package/dist/inbound-mail-foundation/constants.js +30 -33
  292. package/dist/inbound-mail-foundation/entities.d.ts +12 -12
  293. package/dist/inbound-mail-foundation/entities.js +47 -48
  294. package/dist/inbound-mail-foundation/events.d.ts +4 -3
  295. package/dist/inbound-mail-foundation/events.js +50 -47
  296. package/dist/inbound-mail-foundation/feature.js +39 -39
  297. package/dist/inbound-mail-foundation/handlers/account-state.js +8 -8
  298. package/dist/inbound-mail-foundation/handlers/connect-account.write.js +25 -26
  299. package/dist/inbound-mail-foundation/handlers/disconnect-account.write.js +7 -7
  300. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +50 -51
  301. package/dist/inbound-mail-foundation/handlers/list-accounts.query.js +8 -8
  302. package/dist/inbound-mail-foundation/handlers/list-messages.query.js +10 -10
  303. package/dist/inbound-mail-foundation/handlers/scope-visibility.js +3 -4
  304. package/dist/inbound-mail-foundation/handlers/update-account.write.d.ts +2 -2
  305. package/dist/inbound-mail-foundation/handlers/update-account.write.js +22 -14
  306. package/dist/inbound-mail-foundation/i18n.js +0 -1
  307. package/dist/inbound-mail-foundation/oauth-state.d.ts +5 -5
  308. package/dist/inbound-mail-foundation/oauth-state.js +11 -10
  309. package/dist/inbound-mail-foundation/projection.d.ts +4 -4
  310. package/dist/inbound-mail-foundation/projection.js +31 -29
  311. package/dist/inbound-mail-foundation/provider-factory.d.ts +4 -4
  312. package/dist/inbound-mail-foundation/provider-factory.js +9 -9
  313. package/dist/inbound-mail-foundation/retention-sweep.d.ts +5 -3
  314. package/dist/inbound-mail-foundation/retention-sweep.js +38 -36
  315. package/dist/inbound-mail-foundation/screens.js +0 -3
  316. package/dist/inbound-mail-foundation/tenant-destroy-hook.js +14 -16
  317. package/dist/inbound-mail-foundation/types.d.ts +49 -49
  318. package/dist/inbound-mail-foundation/types.js +21 -21
  319. package/dist/inbound-mail-foundation/watch-supervisor.d.ts +10 -10
  320. package/dist/inbound-mail-foundation/watch-supervisor.js +85 -67
  321. package/dist/inbound-provider-imap/credential-document.js +13 -13
  322. package/dist/inbound-provider-imap/feature.d.ts +2 -2
  323. package/dist/inbound-provider-imap/feature.js +42 -40
  324. package/dist/inbound-provider-imap/imap-client.d.ts +12 -11
  325. package/dist/inbound-provider-imap/imap-client.js +20 -19
  326. package/dist/inbound-provider-inmemory/feature.d.ts +9 -9
  327. package/dist/inbound-provider-inmemory/feature.js +19 -19
  328. package/dist/jobs/db/queries/retention.js +2 -2
  329. package/dist/jobs/db/queries/stale-run-sweep.js +3 -3
  330. package/dist/jobs/events.js +1 -1
  331. package/dist/jobs/feature.d.ts +1 -0
  332. package/dist/jobs/feature.js +13 -10
  333. package/dist/jobs/handlers/detail.query.js +4 -4
  334. package/dist/jobs/handlers/list.query.js +1 -1
  335. package/dist/jobs/handlers/projection-rebuild.job.js +1 -1
  336. package/dist/jobs/handlers/reindex-entity.job.js +1 -1
  337. package/dist/jobs/handlers/retry.write.js +1 -1
  338. package/dist/jobs/handlers/tenant-failures.query.d.ts +1 -1
  339. package/dist/jobs/handlers/tenant-failures.query.js +45 -41
  340. package/dist/jobs/handlers/tenant-runs.query.d.ts +1 -1
  341. package/dist/jobs/handlers/tenant-runs.query.js +54 -50
  342. package/dist/jobs/job-instant.js +1 -1
  343. package/dist/jobs/job-run-logger.js +15 -15
  344. package/dist/jobs/job-run-table.js +3 -3
  345. package/dist/jobs/screens.js +1 -1
  346. package/dist/jobs/tenant-job-failure-table.js +1 -1
  347. package/dist/jobs/tenant-job-run-table.js +1 -1
  348. package/dist/ledger/entity.js +5 -5
  349. package/dist/ledger/feature.js +3 -3
  350. package/dist/ledger/handlers/confirm-schedule-period.write.js +3 -3
  351. package/dist/ledger/recurring.js +12 -12
  352. package/dist/ledger/schemas.js +1 -1
  353. package/dist/ledger/web/constants.js +2 -2
  354. package/dist/ledger/web/index.js +7 -7
  355. package/dist/legal-pages/feature.d.ts +4 -4
  356. package/dist/legal-pages/feature.js +25 -26
  357. package/dist/legal-pages/markdown.js +3 -3
  358. package/dist/legal-pages/web/client-plugin.js +23 -24
  359. package/dist/legal-pages/web/index.js +0 -1
  360. package/dist/mail-foundation/feature.d.ts +5 -5
  361. package/dist/mail-foundation/feature.js +27 -25
  362. package/dist/mail-transport-inmemory/feature.d.ts +4 -4
  363. package/dist/mail-transport-inmemory/feature.js +27 -28
  364. package/dist/mail-transport-smtp/feature.js +20 -20
  365. package/dist/managed-pages/branding.js +6 -6
  366. package/dist/managed-pages/feature.d.ts +26 -26
  367. package/dist/managed-pages/feature.js +34 -34
  368. package/dist/managed-pages/handlers/by-slug.query.js +5 -6
  369. package/dist/managed-pages/handlers/by-tenant-published.query.js +2 -2
  370. package/dist/managed-pages/handlers/set.write.js +5 -7
  371. package/dist/managed-pages/screens/page-screens.js +11 -11
  372. package/dist/managed-pages/seeding.js +8 -8
  373. package/dist/managed-pages/table.js +13 -12
  374. package/dist/managed-pages/web/client-plugin.d.ts +1 -1
  375. package/dist/managed-pages/web/client-plugin.js +5 -5
  376. package/dist/managed-pages/web/i18n.js +1 -1
  377. package/dist/notes-history/entity.js +4 -4
  378. package/dist/notes-history/feature.d.ts +2 -2
  379. package/dist/notes-history-user-data/hooks.js +1 -1
  380. package/dist/page-render/layout.js +9 -9
  381. package/dist/page-render/markdown.js +11 -11
  382. package/dist/page-render/security-headers.js +7 -6
  383. package/dist/personal-access-tokens/feature.d.ts +1 -1
  384. package/dist/personal-access-tokens/feature.js +9 -5
  385. package/dist/personal-access-tokens/roles.js +2 -2
  386. package/dist/readiness/handlers/status.query.js +3 -3
  387. package/dist/renderer-foundation/api.js +12 -13
  388. package/dist/renderer-foundation/constants.js +2 -2
  389. package/dist/renderer-foundation/feature.js +7 -7
  390. package/dist/renderer-pdf/render-table-pdf.d.ts +1 -1
  391. package/dist/renderer-pdf/render-table-pdf.js +22 -10
  392. package/dist/secrets/feature.js +9 -9
  393. package/dist/secrets/web/constants.js +2 -2
  394. package/dist/seo/feature.js +3 -3
  395. package/dist/sessions/constants.d.ts +1 -1
  396. package/dist/sessions/constants.js +1 -1
  397. package/dist/sessions/feature.d.ts +2 -2
  398. package/dist/sessions/feature.js +12 -8
  399. package/dist/sessions/handlers/revoke-all-for-user.write.js +2 -2
  400. package/dist/sessions/handlers/revoke-all-others.write.js +2 -2
  401. package/dist/sessions/handlers/revoke.write.js +1 -1
  402. package/dist/sessions/schema/user-session.js +4 -4
  403. package/dist/sessions/screens.js +2 -2
  404. package/dist/sessions/session-callbacks.js +2 -2
  405. package/dist/sessions/session-revoked-event.js +2 -2
  406. package/dist/shared/assert-no-where-rule-in-ownership-write.js +1 -1
  407. package/dist/shared/decrypt-stored-pii.d.ts +1 -0
  408. package/dist/shared/decrypt-stored-pii.js +7 -2
  409. package/dist/shared/encrypt-for-direct-write.js +1 -1
  410. package/dist/shared/index.d.ts +1 -1
  411. package/dist/shared/index.js +1 -1
  412. package/dist/shared/mail-template-body.d.ts +6 -2
  413. package/dist/shared/mail-template-body.js +7 -4
  414. package/dist/shared/map-with-concurrency.js +5 -4
  415. package/dist/shared/parent-visibility.js +2 -2
  416. package/dist/shared/password-hashing.js +1 -1
  417. package/dist/shared/signup-handover.js +1 -2
  418. package/dist/shared/single-use-token-store.js +1 -1
  419. package/dist/step-dispatcher/feature.js +1 -0
  420. package/dist/subscription-dodo/client.d.ts +17 -0
  421. package/dist/subscription-dodo/client.js +31 -0
  422. package/dist/subscription-dodo/constants.d.ts +12 -0
  423. package/dist/subscription-dodo/constants.js +19 -0
  424. package/dist/subscription-dodo/feature.d.ts +37 -0
  425. package/dist/subscription-dodo/feature.js +144 -0
  426. package/dist/subscription-dodo/index.d.ts +2 -0
  427. package/dist/subscription-dodo/index.js +4 -0
  428. package/dist/subscription-dodo/plugin-methods.d.ts +23 -0
  429. package/dist/subscription-dodo/plugin-methods.js +260 -0
  430. package/dist/subscription-dodo/runtime.d.ts +27 -0
  431. package/dist/subscription-dodo/runtime.js +89 -0
  432. package/dist/subscription-dodo/verify-webhook.d.ts +37 -0
  433. package/dist/subscription-dodo/verify-webhook.js +189 -0
  434. package/dist/subscription-mollie/constants.js +15 -17
  435. package/dist/subscription-mollie/feature.d.ts +17 -18
  436. package/dist/subscription-mollie/feature.js +24 -24
  437. package/dist/subscription-mollie/index.js +3 -3
  438. package/dist/subscription-mollie/plugin-methods.d.ts +2 -2
  439. package/dist/subscription-mollie/plugin-methods.js +15 -14
  440. package/dist/subscription-mollie/verify-webhook.d.ts +10 -10
  441. package/dist/subscription-mollie/verify-webhook.js +27 -27
  442. package/dist/subscription-stripe/constants.js +4 -6
  443. package/dist/subscription-stripe/feature.d.ts +14 -14
  444. package/dist/subscription-stripe/feature.js +36 -34
  445. package/dist/subscription-stripe/index.js +6 -6
  446. package/dist/subscription-stripe/plugin-methods.js +18 -18
  447. package/dist/subscription-stripe/runtime.d.ts +1 -1
  448. package/dist/subscription-stripe/runtime.js +8 -7
  449. package/dist/subscription-stripe/verify-webhook.d.ts +11 -11
  450. package/dist/subscription-stripe/verify-webhook.js +59 -61
  451. package/dist/tags/feature.d.ts +3 -3
  452. package/dist/tags/screens.js +1 -1
  453. package/dist/template-resolver/api.d.ts +3 -2
  454. package/dist/template-resolver/api.js +34 -12
  455. package/dist/template-resolver/compile-document-body.js +1 -0
  456. package/dist/template-resolver/feature.js +2 -2
  457. package/dist/template-resolver/handlers/bundle-import.js +3 -3
  458. package/dist/template-resolver/handlers/copy-locale.write.d.ts +5 -1
  459. package/dist/template-resolver/handlers/copy-locale.write.js +22 -7
  460. package/dist/template-resolver/handlers/diff-to-system.query.js +3 -2
  461. package/dist/template-resolver/handlers/document-base.js +3 -2
  462. package/dist/template-resolver/handlers/find-by-id.query.js +8 -7
  463. package/dist/template-resolver/handlers/list.query.js +6 -7
  464. package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
  465. package/dist/template-resolver/handlers/overview.query.js +6 -3
  466. package/dist/template-resolver/handlers/shared.js +2 -2
  467. package/dist/template-resolver/handlers/template-write.js +3 -3
  468. package/dist/template-resolver/handlers/tenant-override-counts.query.js +1 -3
  469. package/dist/template-resolver/handlers/tenant-override-selection.d.ts +2 -1
  470. package/dist/template-resolver/handlers/tenant-overrides.query.js +28 -5
  471. package/dist/template-resolver/handlers/upsert-system.write.js +3 -4
  472. package/dist/template-resolver/handlers/upsert-tenant.write.js +4 -5
  473. package/dist/template-resolver/handlers/usage.query.d.ts +2 -1
  474. package/dist/template-resolver/handlers/usage.query.js +26 -16
  475. package/dist/template-resolver/handlers/validate.query.js +7 -2
  476. package/dist/template-resolver/seeding.js +30 -14
  477. package/dist/template-resolver/store-template-resource.d.ts +1 -0
  478. package/dist/template-resolver/store-template-resource.js +19 -4
  479. package/dist/template-resolver/table.js +10 -11
  480. package/dist/template-resolver/template-delivery-policy.d.ts +0 -5
  481. package/dist/template-resolver/template-delivery-policy.js +0 -6
  482. package/dist/template-resolver/template-meta.js +7 -4
  483. package/dist/template-resolver/template-preview.js +4 -3
  484. package/dist/template-resolver/web/client-plugin.js +2 -2
  485. package/dist/template-resolver/web/constants.d.ts +2 -2
  486. package/dist/template-resolver/web/constants.js +3 -3
  487. package/dist/template-resolver/web/index.d.ts +1 -0
  488. package/dist/template-resolver/web/index.js +1 -0
  489. package/dist/template-resolver/web/mail-layout-constants.d.ts +5 -0
  490. package/dist/template-resolver/web/mail-layout-constants.js +7 -0
  491. package/dist/tenant/feature.js +7 -7
  492. package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
  493. package/dist/tenant/handlers/member-directory.query.js +11 -4
  494. package/dist/tenant/handlers/members.query.js +1 -1
  495. package/dist/tenant/handlers/team-list.query.js +1 -1
  496. package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
  497. package/dist/tenant/handlers/tenant-directory.query.js +9 -5
  498. package/dist/tenant/invitation-table.js +37 -40
  499. package/dist/tenant/membership-table.js +3 -3
  500. package/dist/tenant/schema/tenant.js +7 -9
  501. package/dist/tenant/screens.js +1 -1
  502. package/dist/tenant/seeding.d.ts +6 -5
  503. package/dist/tenant/seeding.js +12 -12
  504. package/dist/tenant/testing.js +5 -6
  505. package/dist/tenant/web/constants.js +2 -2
  506. package/dist/tenant-handover/events.js +5 -7
  507. package/dist/tenant-handover/feature.js +1 -1
  508. package/dist/tenant-handover/grant.js +1 -1
  509. package/dist/tenant-handover/handlers/claim.write.js +1 -1
  510. package/dist/tenant-handover/move-entity-graph.js +8 -9
  511. package/dist/tenant-handover/signup-handover-provider.js +1 -2
  512. package/dist/tenant-handover/transfer-graph.js +4 -5
  513. package/dist/tenant-lifecycle/boot-checks.js +1 -1
  514. package/dist/tenant-lifecycle/constants.d.ts +1 -0
  515. package/dist/tenant-lifecycle/constants.js +4 -0
  516. package/dist/tenant-lifecycle/feature.js +1 -1
  517. package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
  518. package/dist/tenant-settings/constants.js +1 -1
  519. package/dist/tenant-settings/tenant-defaults.d.ts +1 -1
  520. package/dist/tenant-settings/tenant-defaults.js +9 -2
  521. package/dist/tier-engine/aggregate-id.d.ts +11 -11
  522. package/dist/tier-engine/aggregate-id.js +15 -16
  523. package/dist/tier-engine/compose-app.d.ts +14 -14
  524. package/dist/tier-engine/compose-app.js +1 -1
  525. package/dist/tier-engine/entity.js +18 -19
  526. package/dist/tier-engine/feature.d.ts +19 -17
  527. package/dist/tier-engine/feature.js +68 -69
  528. package/dist/tier-engine/handlers/active-tier.query.js +6 -7
  529. package/dist/tier-engine/handlers/get-tenant-tier.query.js +4 -4
  530. package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +7 -8
  531. package/dist/tier-engine/handlers/set-tenant-tier.write.js +7 -4
  532. package/dist/tier-engine/tier-resolver.js +2 -2
  533. package/dist/tier-engine/trial.js +8 -8
  534. package/dist/tier-engine/web/constants.js +2 -2
  535. package/dist/tier-engine/web/index.js +4 -4
  536. package/dist/user/db/queries/stream-tenant-backfill.js +8 -8
  537. package/dist/user/handlers/create.write.js +4 -4
  538. package/dist/user/schema/user.d.ts +11 -11
  539. package/dist/user/schema/user.js +45 -44
  540. package/dist/user/seeding.d.ts +15 -15
  541. package/dist/user/seeding.js +28 -28
  542. package/dist/user/testing.js +3 -3
  543. package/dist/user-data-rights/audit-download.d.ts +8 -8
  544. package/dist/user-data-rights/audit-download.js +19 -20
  545. package/dist/user-data-rights/boot-checks.js +1 -1
  546. package/dist/user-data-rights/deletion-token.js +1 -1
  547. package/dist/user-data-rights/feature.d.ts +49 -50
  548. package/dist/user-data-rights/feature.js +54 -53
  549. package/dist/user-data-rights/handlers/cancel-deletion.write.js +9 -9
  550. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +8 -8
  551. package/dist/user-data-rights/handlers/download-by-job.query.js +29 -30
  552. package/dist/user-data-rights/handlers/download-by-token.query.js +40 -42
  553. package/dist/user-data-rights/handlers/export-status.query.js +8 -9
  554. package/dist/user-data-rights/handlers/lift-restriction.write.js +7 -8
  555. package/dist/user-data-rights/handlers/list-download-attempts.query.js +2 -2
  556. package/dist/user-data-rights/handlers/my-audit-log.query.js +4 -4
  557. package/dist/user-data-rights/handlers/request-deletion-by-email.write.d.ts +5 -5
  558. package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +16 -15
  559. package/dist/user-data-rights/handlers/request-deletion.write.d.ts +2 -2
  560. package/dist/user-data-rights/handlers/request-deletion.write.js +7 -9
  561. package/dist/user-data-rights/handlers/request-export.write.js +42 -41
  562. package/dist/user-data-rights/handlers/restrict-account.write.js +22 -19
  563. package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +8 -8
  564. package/dist/user-data-rights/index.js +8 -10
  565. package/dist/user-data-rights/lib/default-mailers.js +3 -3
  566. package/dist/user-data-rights/lib/update-user-lifecycle.js +14 -17
  567. package/dist/user-data-rights/run-export-jobs.d.ts +38 -42
  568. package/dist/user-data-rights/run-export-jobs.js +130 -237
  569. package/dist/user-data-rights/run-forget-cleanup.d.ts +16 -20
  570. package/dist/user-data-rights/run-forget-cleanup.js +65 -83
  571. package/dist/user-data-rights/run-user-export.d.ts +34 -33
  572. package/dist/user-data-rights/run-user-export.js +55 -57
  573. package/dist/user-data-rights/schema/download-attempt.js +11 -11
  574. package/dist/user-data-rights/schema/download-token.js +37 -41
  575. package/dist/user-data-rights/schema/export-job.d.ts +4 -4
  576. package/dist/user-data-rights/schema/export-job.js +65 -67
  577. package/dist/user-data-rights/token-helpers.d.ts +8 -9
  578. package/dist/user-data-rights/token-helpers.js +21 -25
  579. package/dist/user-data-rights/web/client-plugin.d.ts +5 -5
  580. package/dist/user-data-rights/web/client-plugin.js +1 -1
  581. package/dist/user-data-rights/web/confirm-deletion-screen.js +6 -5
  582. package/dist/user-data-rights/web/constants.js +1 -1
  583. package/dist/user-data-rights/web/index.js +5 -5
  584. package/dist/user-data-rights/web/privacy-center-screen.js +14 -13
  585. package/dist/user-data-rights/web/public-deletion-gate.d.ts +4 -4
  586. package/dist/user-data-rights/web/request-deletion-screen.js +6 -6
  587. package/dist/user-data-rights/zip-path.d.ts +15 -15
  588. package/dist/user-data-rights/zip-path.js +41 -40
  589. package/dist/user-data-rights-defaults/feature.js +15 -15
  590. package/dist/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.js +3 -5
  591. package/dist/user-data-rights-defaults/hooks/file-ref.userdata-hook.js +45 -51
  592. package/dist/user-data-rights-defaults/hooks/job-run.userdata-hook.js +2 -2
  593. package/dist/user-data-rights-defaults/hooks/notification-preference.userdata-hook.js +1 -1
  594. package/dist/user-data-rights-defaults/hooks/user-session.userdata-hook.js +3 -3
  595. package/dist/user-data-rights-defaults/hooks/user.userdata-hook.js +32 -31
  596. package/dist/user-profile/handlers/change-email.write.js +13 -13
  597. package/dist/user-profile/index.js +3 -3
  598. package/dist/user-profile/web/client-plugin.js +1 -1
  599. package/dist/user-profile/web/constants.js +1 -1
  600. package/dist/user-profile/web/profile-screen.js +1 -1
  601. package/dist/waitlist/handlers/export-purge.write.js +2 -1
  602. package/dist/waitlist/handlers/submit.write.js +5 -5
  603. package/dist/waitlist/import.d.ts +1 -0
  604. package/dist/waitlist/import.js +4 -0
  605. package/dist/waitlist/legacy-migrate.js +17 -6
  606. package/dist/waitlist/link-on-signup.js +18 -6
  607. package/dist/waitlist/options.d.ts +3 -0
  608. package/dist/workflow-runner/aggregate-id.d.ts +1 -1
  609. package/dist/workflow-runner/aggregate-id.js +1 -1
  610. package/dist/workflow-runner/event-subscriber.js +4 -5
  611. package/dist/workflow-runner/event-trigger.js +4 -17
  612. package/dist/workflow-runner/feature.js +2 -2
  613. package/dist/workflow-runner/handlers/resume-run.write.js +4 -4
  614. package/dist/workflow-runner/pending-projection.js +6 -7
  615. package/dist/workflow-runner/runner.js +6 -6
  616. package/dist/workflow-runner/tables.js +6 -6
  617. package/dist/workflow-runner/workflow-registry.js +1 -1
  618. package/package.json +20 -10
  619. package/src/agent-tools/changes.json +6 -0
  620. package/src/audit/changes.json +12 -0
  621. package/src/auth-mfa/changes.json +6 -0
  622. package/src/billing-foundation/changes.json +55 -0
  623. package/src/cap-counter/changes.json +18 -0
  624. package/src/channel-email/changes.json +6 -0
  625. package/src/config/changes.json +12 -0
  626. package/src/crypto-shredding/changes.json +6 -0
  627. package/src/delivery/changes.json +30 -0
  628. package/src/form-draft/changes.json +6 -0
  629. package/src/host-routing/changes.json +6 -0
  630. package/src/inbound-mail-foundation/changes.json +18 -0
  631. package/src/jobs/changes.json +13 -0
  632. package/src/mail-foundation/changes.json +6 -0
  633. package/src/renderer-pdf/changes.json +6 -0
  634. package/src/step-dispatcher/changes.json +6 -0
  635. package/src/subscription-dodo/changes.json +14 -0
  636. package/src/subscription-stripe/changes.json +12 -0
  637. package/src/template-resolver/changes.json +48 -0
  638. package/src/tenant/changes.json +6 -0
  639. package/src/tenant-lifecycle/changes.json +6 -0
  640. package/src/tier-engine/changes.json +6 -0
  641. package/src/waitlist/changes.json +18 -0
  642. package/dist/billing-foundation/consumer-protection/stored-user-email.d.ts +0 -1
  643. package/dist/billing-foundation/consumer-protection/stored-user-email.js +0 -8
@@ -1,46 +1,46 @@
1
- // Retention-Cleanup-Runner (S2.D2b) — pure Function, vom retention-cleanup-Cron
2
- // pro fan-out-Tenant aufgerufen.
1
+ // Retention-Cleanup-Runner — pure function, called by the retention-cleanup cron
2
+ // for each fan-out tenant.
3
3
  //
4
- // Iteriert alle implicit-Entity-Projektionen, loest pro Entity die effektive
5
- // Retention-Policy (3-Schicht-Resolver, siehe resolver.ts) und wendet die
6
- // Strategy auf Rows an deren reference-Timestamp aelter als der keepFor-Cutoff
7
- // ist:
4
+ // Iterates all implicit entity projections, resolves the effective retention
5
+ // policy per entity (3-layer resolver, see resolver.ts), and applies the
6
+ // strategy to rows whose reference timestamp is older than the keepFor cutoff:
8
7
  //
9
- // - hardDelete → executor.forget pro Row (Event → rebuild-safe hard purge).
8
+ // - hardDelete → executor.forget per row (event → rebuild-safe hard purge).
10
9
  // Entities with file/image/files/images fields: bytes first
11
10
  // (incl. derivatives), then the fileRef row(s), then the row;
12
11
  // shared fileRefs (other owner, other row of the same table)
13
12
  // stay untouched.
14
- // - softDelete → executor.delete pro Row (Event → rebuild-safe soft-delete),
15
- // nur auf noch-nicht-geloeschte (isDeleted:false-Filter)
16
- // - anonymize → executor.update pro Row mit den Werten der per-Feld
17
- // anonymize-Funktionen; Row bleibt (fields.ts-Contract)
18
- // - blockDelete → waehrend keepFor unangetastet (Aufbewahrungs-Pflicht;
19
- // user-forget loest anonymize aus). NACH Ablauf laufen die
20
- // anonymize-Funktionen — Geschaeftsdaten bleiben, Personen-
21
- // Bezug raus (fields.ts:44-47).
13
+ // - softDelete → executor.delete per row (event → rebuild-safe soft-delete),
14
+ // only on not-yet-deleted rows (isDeleted:false filter)
15
+ // - anonymize → executor.update per row with the values of the per-field
16
+ // anonymize functions; row remains (fields.ts contract)
17
+ // - blockDelete → untouched during keepFor (retention requirement;
18
+ // user-forget triggers anonymize). AFTER expiration, the
19
+ // anonymize functions run — business data remains, the person-
20
+ // identifying link is removed (fields.ts:44-47).
22
21
  //
23
- // Über den Executor statt Batch-deleteMany/updateMany: ein eventloser Batch-Write
24
- // auf die (erased) Projektions-Tabelle wird beim Projection-Rebuild
25
- // gewischt/resurrektiert — das #648-Loch. Kosten: N Events pro Cleanup statt ein
26
- // Batch-Statement (per-Row-Ceiling, Batch-Event-Variante als Follow-up).
22
+ // Via the executor instead of batch-deleteMany/updateMany: an eventless batch write
23
+ // to the (erased) projection table is wiped/resurrected during a projection rebuild.
24
+ // Cost: N events per cleanup instead of one batch statement (per-row ceiling, with
25
+ // a batch-event variant later).
27
26
  //
28
- // **Schaerfer als soft-delete-cleanup:** dieser Cron hardDeleted LIVE Rows
29
- // (keyed auf reference, Default createdAt), nicht bereits-soft-geloeschte.
30
- // Darum die Spalten-Existenz-Pruefung vor jedem WHERE — eine fehlende/vertippte
31
- // reference-Spalte wuerde sonst ein malformed/all-matching WHERE ergeben und
32
- // pauschal loeschen.
27
+ // **Stricter than soft-delete-cleanup:** this cron hard-deletes LIVE rows
28
+ // (keyed on reference, default createdAt), not already-soft-deleted ones.
29
+ // Therefore the column-existence check comes before every WHERE — a missing/mistyped
30
+ // reference column would otherwise produce a malformed/all-matching WHERE and
31
+ // delete everything.
33
32
  //
34
- // **anonymize-Idempotenz ohne Marker-Spalte:** die anonymize-Funktionen sind
35
- // row-unabhaengig (`() => unknown`) — einmal pro Entity ausgewertet ergeben
36
- // sie die Ziel-Werte. Eine Row die sie schon traegt wird uebersprungen, der
37
- // taegliche Re-Lauf appended also null Events. Der WHERE matcht anonymisierte
38
- // Rows weiter (reference bleibt alt), deshalb id-Cursor-Paging statt einer
39
- // einzelnen Page — sonst verstopfen erledigte Rows das batchLimit-Fenster.
33
+ // **anonymize idempotence without a marker column:** the anonymize functions are
34
+ // row-independent (`() => unknown`) — evaluated once per entity, they produce
35
+ // the target values. A row that already carries them is skipped, so the
36
+ // daily rerun appends zero events. The WHERE continues to match anonymized
37
+ // rows (reference remains old), hence id-cursor paging instead of a single
38
+ // page — otherwise completed rows clog the batchLimit window.
40
39
  import { collectPiiSubjectFields, configuredPiiSubjectKms, isRecordKeyedField, isSelfPiiField, resolveSubjectForField, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
41
40
  import { createEventStoreExecutor, createTenantDb, nullBlindIndexesForSubject, selectMany, WritePurposes, } from "@cosmicdrift/kumiko-framework/db";
42
41
  import { deleteFileRefBytes } from "@cosmicdrift/kumiko-framework/derivatives";
43
42
  import { createSystemUser, entityHasAnonymizableSubjectField, } from "@cosmicdrift/kumiko-framework/engine";
43
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
44
44
  import { fileRefEntity, fileRefsTable, } from "@cosmicdrift/kumiko-framework/files";
45
45
  import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
46
46
  import { collectSingleFileHolders, planFileFields, purgeRowFileRefs, runInSubTransaction, } from "../shared/index.js";
@@ -49,13 +49,13 @@ import { resolveRetentionPolicyForTenant } from "./resolve-for-tenant.js";
49
49
  import { tenantRetentionOverrideTable } from "./schema/tenant-retention-override.js";
50
50
  const DEFAULT_BATCH_LIMIT = 1000;
51
51
  const DEFAULT_REFERENCE_FIELD = "createdAt";
52
- // Der Boot-Validator (boot-validator/pii-retention.ts FRAMEWORK_TIMESTAMP_FIELDS)
53
- // erlaubt diese Aliase als retention.reference, auch wenn sie nicht in
54
- // entity.fields deklariert sind — die physischen Spalten heissen aber anders
55
- // (table-builder.ts: inserted_at/modified_at). Hier zur Cleanup-Zeit auf das
56
- // echte Entity-Feld mappen, sonst trifft die Spalten-Existenz-Pruefung unten
57
- // und der Cron wuerde lautlos nichts tun. deletedAt/lastSeenAt sind echte
58
- // Felder (softDelete bzw. session) und brauchen keine Uebersetzung.
52
+ // The boot validator (boot-validator/pii-retention.ts FRAMEWORK_TIMESTAMP_FIELDS)
53
+ // permits these aliases as retention.reference even when they are not declared in
54
+ // entity.fields, but the physical columns use different names
55
+ // (table-builder.ts: inserted_at/modified_at). Map them to the actual entity field
56
+ // during cleanup; otherwise the column existence check below would cause the cron
57
+ // to do nothing silently. deletedAt/lastSeenAt are real fields (softDelete and
58
+ // session respectively) and need no translation.
59
59
  const FRAMEWORK_REFERENCE_ALIAS = {
60
60
  createdAt: "insertedAt",
61
61
  updatedAt: "modifiedAt",
@@ -72,7 +72,7 @@ async function purgeMatchingRows(db, table, where, batchLimit, op) {
72
72
  }
73
73
  return count;
74
74
  }
75
- // --- hardDelete crypto-shredding (Refs #2057) ---
75
+ // --- hardDelete crypto-shredding ---
76
76
  //
77
77
  // recordOwned/self-pii resolution (subject-resolver.ts) only ever reads
78
78
  // `row.id` for these two kinds — resolveRecordSubject uses opts.entityName +
@@ -137,7 +137,7 @@ async function eraseHardDeletedRowKeys(args) {
137
137
  async function forgetRowAndEraseKeys(args) {
138
138
  return runInSubTransaction(args.db, async (sp) => {
139
139
  const tdb = createTenantDb({ db: sp, tenantId: args.tenantId, mode: "system" });
140
- const result = await args.executor.forget({ id: args.row["id"] }, args.systemUser, tdb);
140
+ const result = await args.executor.forget({ id: args.rowId }, args.systemUser, tdb);
141
141
  if (!result.isSuccess)
142
142
  return { isSuccess: false };
143
143
  await eraseHardDeletedRowKeys({
@@ -152,7 +152,7 @@ async function forgetRowAndEraseKeys(args) {
152
152
  return { isSuccess: true };
153
153
  });
154
154
  }
155
- // --- hardDelete + file-fields (kumiko-framework#3089) ---
155
+ // --- hardDelete + file-fields ---
156
156
  // Throws (unsafe key, missing list permission) must fail this row closed,
157
157
  // not abort the whole tenant run.
158
158
  async function deleteFileFailingClosed(key, files, logContext) {
@@ -169,8 +169,7 @@ async function deleteFileFailingClosed(key, files, logContext) {
169
169
  // fields: purge the un-shared fileRefs (bytes + derivatives, then the fileRef
170
170
  // rows), then the entity row itself.
171
171
  async function purgeHardDeleteRowWithFiles(args) {
172
- const rowId = String(args.row["id"]);
173
- const logContext = `tenant=${args.tenantId} entity=${args.entityName} row=${rowId}`;
172
+ const logContext = `tenant=${args.tenantId} entity=${args.entityName} row=${String(args.rowId)}`;
174
173
  const files = args.files;
175
174
  const outcome = await purgeRowFileRefs({
176
175
  db: args.db,
@@ -198,6 +197,7 @@ async function purgeHardDeleteRowWithFiles(args) {
198
197
  entityName: args.entityName,
199
198
  tenantId: args.tenantId,
200
199
  systemUser: args.systemUser,
200
+ rowId: args.rowId,
201
201
  row: args.row,
202
202
  kms: args.kms,
203
203
  registry: args.registry,
@@ -230,7 +230,14 @@ async function purgeHardDeleteRowsWithFiles(args) {
230
230
  if (rows.length === 0)
231
231
  break;
232
232
  for (const row of rows) {
233
- cursor = row["id"]; // @cast-boundary db-row
233
+ const rawId = row["id"];
234
+ if (typeof rawId !== "string" && typeof rawId !== "number") {
235
+ throw new InternalError({
236
+ message: `data-retention: ${args.entityName} row has no usable id (got ${typeof rawId})`,
237
+ });
238
+ }
239
+ const rowId = rawId;
240
+ cursor = rowId;
234
241
  const ok = await purgeHardDeleteRowWithFiles({
235
242
  db: args.db,
236
243
  table: args.table,
@@ -238,6 +245,7 @@ async function purgeHardDeleteRowsWithFiles(args) {
238
245
  tenantId: args.tenantId,
239
246
  entityName: args.entityName,
240
247
  entity: args.entity,
248
+ rowId,
241
249
  row,
242
250
  files: args.files,
243
251
  entityExecutor: args.entityExecutor,
@@ -311,9 +319,9 @@ export async function runRetentionCleanup(args) {
311
319
  const skipped = [];
312
320
  // Retention writes go through the executor (events) so a projection rebuild
313
321
  // replays the cleanup — a batch deleteMany/updateMany on the (erased)
314
- // projection table is eventless and gets wiped/resurrected on rebuild (the
315
- // #648 hole this closes). The cron acts as the system actor; the WHERE already
316
- // tenant-scopes, so a system-mode TenantDb (no extra filter) is correct.
322
+ // projection table is eventless and gets wiped/resurrected on rebuild.
323
+ // The cron acts as the system actor; the WHERE already tenant-scopes, so a
324
+ // system-mode TenantDb (no extra filter) is correct.
317
325
  // ponytail: per-row events, one batchLimit-sized page per entity per run —
318
326
  // the daily cron converges (hardDelete removes rows, softDelete's isDeleted
319
327
  // filter shrinks the set). A single batched forget/delete event is a follow-up.
@@ -326,8 +334,9 @@ export async function runRetentionCleanup(args) {
326
334
  const overrideRows = await selectMany(db, tenantRetentionOverrideTable, { tenantId });
327
335
  const overrideByEntity = new Map(overrideRows.map((r) => [r.entityName, { config: r.config }]));
328
336
  for (const proj of registry.getAllProjections().values()) {
329
- // Nur implicit-Entity-Projektionen mit Tabelle — wie soft-delete-cleanup.
330
- // Custom-Projektionen + unmanaged-Tables (z.B. sessions) sind kein Target.
337
+ // Only implicit entity projections with a table are targets, as in
338
+ // soft-delete-cleanup. Custom projections and unmanaged tables (such as
339
+ // sessions) are excluded.
331
340
  if (proj.isImplicit !== true || typeof proj.source !== "string" || !proj.table)
332
341
  continue;
333
342
  const entityName = proj.source;
@@ -353,9 +362,9 @@ export async function runRetentionCleanup(args) {
353
362
  }
354
363
  const cutoff = computeCutoff(policy.keepFor, now);
355
364
  const where = { [referenceField]: { lt: cutoff } };
356
- // Tenant-Scope nur wenn die Tabelle eine tenantId-Spalte hat — identisch zu
357
- // soft-delete-cleanup. Ohne diesen Filter wuerde ein Tenant die Rows eines
358
- // anderen treffen.
365
+ // Apply tenant scoping only when the table has a tenantId column, as in
366
+ // soft-delete-cleanup. Without this filter, one tenant could affect another
367
+ // tenant's rows.
359
368
  if (propertyOf(proj.table, "tenantId") !== undefined) {
360
369
  where["tenantId"] = tenantId;
361
370
  }
@@ -371,6 +380,7 @@ export async function runRetentionCleanup(args) {
371
380
  entityName,
372
381
  tenantId,
373
382
  systemUser,
383
+ rowId: id,
374
384
  row: { id },
375
385
  kms,
376
386
  registry,
@@ -1,20 +1,22 @@
1
1
  import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { createEntity, createLongTextField, createTextField, } from "@cosmicdrift/kumiko-framework/engine";
3
- // tenantRetentionOverride — Layer 3 im 3-Schicht-Resolver.
3
+ // tenantRetentionOverride — Layer 3 in the three-layer resolver.
4
4
  //
5
- // Per (tenantId, entityName) eine optionale Override-Config die das
6
- // Preset (Layer 2) für genau diese eine Entity überschreibt. Use-Cases:
7
- // - Anwaltskanzlei in DE: caseFile 6y blockDelete (nicht im Preset)
8
- // - Pilot-Tenant der länger speichert für Test
9
- // - Branchenspezifische verkürzte Fristen
5
+ // One optional override config per (tenantId, entityName) replaces the
6
+ // preset (Layer 2) for that entity only. Use cases:
7
+ // - Law firm in DE: caseFile 6y blockDelete (not in the preset)
8
+ // - Pilot tenant retaining data longer for testing
9
+ // - Industry-specific shorter retention periods
10
10
  //
11
- // reason ist Pflicht — Audit für DPO + Aufsichtsbehörde nachvollziehbar.
11
+ // reason is required so the decision is auditable by the DPO and supervisory
12
+ // authorities.
12
13
  //
13
- // config ist JSON-String mit `{ keepFor, strategy, reference? }`. Zod-
14
- // Schema validiert beim set-override-Call (S2.D2 ggf. erweitert).
14
+ // config is a JSON string with `{ keepFor, strategy, reference? }`. The Zod
15
+ // schema validates it when set-override is called.
15
16
  //
16
- // Tenant-1-zu-N: pro Tenant beliebig viele Entity-Overrides. UNIQUE-
17
- // Index auf (tenantId, entityName) damit pro Entity max ein Override.
17
+ // Tenant one-to-many: each tenant can have any number of entity overrides.
18
+ // A UNIQUE index on (tenantId, entityName) allows at most one override per
19
+ // entity.
18
20
  export const tenantRetentionOverrideEntity = createEntity({
19
21
  table: "read_tenant_retention_overrides",
20
22
  fields: {
@@ -1,7 +1,7 @@
1
1
  import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
4
- import { readFileRefContent } from "../file-foundation/index.js";
4
+ import { FileStorageUnavailableError, readFileRefContent, } from "../file-foundation/index.js";
5
5
  import { collectRendererPlugins, createRendererFoundationApi, RendererError, } from "../renderer-foundation/index.js";
6
6
  import { FALLBACK_LOCALE } from "../template-resolver/index.js";
7
7
  export function createTemplateMailRenderer(db, registry, tenantId, config) {
@@ -44,15 +44,18 @@ function createPdfRenderer(db, registry, tenantId, config, filesForTenant) {
44
44
  return { content: response.pdfBytes, contentType: PDF_CONTENT_TYPE };
45
45
  };
46
46
  }
47
- // A decorative inline image (logo) that cannot be read must not fail the whole mail.
47
+ // A decorative inline image (logo) of a tenant without file storage must not fail the whole mail;
48
+ // transient read errors rethrow so the send job retries.
48
49
  async function readInlineImageOrNull(fileRefId, read) {
49
50
  try {
50
51
  return await read();
51
52
  }
52
53
  catch (err) {
54
+ if (!(err instanceof FileStorageUnavailableError))
55
+ throw err;
53
56
  createFallbackLogger("delivery").warn("inline image could not be read, sending without it", {
54
57
  fileRefId,
55
- errorName: err instanceof Error ? err.name : typeof err,
58
+ errorName: err.name,
56
59
  });
57
60
  return null;
58
61
  }
@@ -1,4 +1,4 @@
1
- import { unsafeReadRetrying } from "@cosmicdrift/kumiko-framework/db";
1
+ import { toPostgresArrayLiteral, unsafeReadRetrying } from "@cosmicdrift/kumiko-framework/db";
2
2
  // @wrapper-known semantic-alias
3
3
  export async function selectNotificationPreferences(db, tenantId, userId, notificationTypeNames, channelName) {
4
4
  return unsafeReadRetrying(db, `SELECT notification_type AS "notificationType", channel, enabled
@@ -9,5 +9,5 @@ export async function selectNotificationPreferences(db, tenantId, userId, notifi
9
9
  (notification_type = ANY($3::text[]) AND channel = $4)
10
10
  OR (notification_type = '*' AND channel = $4)
11
11
  OR (notification_type = ANY($3::text[]) AND channel = '*')
12
- )`, [tenantId, userId, notificationTypeNames, channelName]);
12
+ )`, [tenantId, userId, toPostgresArrayLiteral(notificationTypeNames), channelName]);
13
13
  }
@@ -266,9 +266,27 @@ export function createDeliveryService(options) {
266
266
  });
267
267
  }
268
268
  }
269
+ // The app's tenantOrigin callback typically hits the DB: once per run and tenant, not per recipient.
270
+ const unsubscribeOriginsByRun = new WeakMap();
271
+ function unsubscribeOriginOnce(run, links, channelCtx, notificationType) {
272
+ let originsByTenant = unsubscribeOriginsByRun.get(run);
273
+ if (originsByTenant === undefined) {
274
+ originsByTenant = new Map();
275
+ unsubscribeOriginsByRun.set(run, originsByTenant);
276
+ }
277
+ let origin = originsByTenant.get(channelCtx.tenantId);
278
+ if (origin === undefined) {
279
+ origin = unsubscribeLinkOrigin(links, channelCtx, (reason) => logError.warn(`unsubscribe link falls back to the app origin: ${reason}`, {
280
+ tenantId: channelCtx.tenantId,
281
+ notificationType,
282
+ }));
283
+ originsByTenant.set(channelCtx.tenantId, origin);
284
+ }
285
+ return origin;
286
+ }
269
287
  // A caller-supplied unsubscribeUrl wins; the recipient comes from the server-side notify
270
288
  // context, never from the notification data.
271
- async function withUnsubscribeUrl(message, channelName, recipient, channelCtx) {
289
+ async function withUnsubscribeUrl(run, message, channelName, recipient, channelCtx) {
272
290
  if (!unsubscribeLinks || channelName !== EMAIL_CHANNEL_NAME)
273
291
  return message;
274
292
  if (registry.getAllNotifications().get(message.notificationType)?.unsubscribe !== true) {
@@ -277,10 +295,7 @@ export function createDeliveryService(options) {
277
295
  const supplied = message.data?.["unsubscribeUrl"];
278
296
  if (typeof supplied === "string" && supplied !== "")
279
297
  return message;
280
- const origin = await unsubscribeLinkOrigin(unsubscribeLinks, channelCtx, (reason) => logError.warn(`unsubscribe link falls back to the app origin: ${reason}`, {
281
- tenantId: channelCtx.tenantId,
282
- notificationType: message.notificationType,
283
- }));
298
+ const origin = await unsubscribeOriginOnce(run, unsubscribeLinks, channelCtx, message.notificationType);
284
299
  const unsubscribeUrl = await buildUnsubscribeUrl(unsubscribeLinks, origin, recipient, {
285
300
  tenantId: channelCtx.tenantId,
286
301
  notificationType: message.notificationType,
@@ -476,7 +491,7 @@ export function createDeliveryService(options) {
476
491
  run,
477
492
  channel,
478
493
  address,
479
- message: await withUnsubscribeUrl(message, channel.name, { kind: "user", userId }, channelCtx),
494
+ message: await withUnsubscribeUrl(run, message, channel.name, { kind: "user", userId }, channelCtx),
480
495
  channelCtx,
481
496
  tenantId,
482
497
  recipientId: userId,
@@ -549,7 +564,7 @@ export function createDeliveryService(options) {
549
564
  run,
550
565
  channel,
551
566
  address,
552
- message: await withUnsubscribeUrl(message, channel.name, { kind: "address", address }, channelCtx),
567
+ message: await withUnsubscribeUrl(run, message, channel.name, { kind: "address", address }, channelCtx),
553
568
  channelCtx,
554
569
  tenantId,
555
570
  recipientId,
@@ -55,10 +55,10 @@ export function createDeliveryFeature(options) {
55
55
  // boot-validator accepts the projection below because at least one
56
56
  // apply-key is a registered domain-event (DELIVERY_ATTEMPT_EVENT).
57
57
  // recipientAddress is the real PII (email address); recipientId stays
58
- // plaintext — pseudonymous fk, same line as config.userId (#821).
58
+ // plaintext — pseudonymous fk, same line as config.userId.
59
59
  // recipientId is nullable (system-triggered sends to non-users), so the
60
60
  // address falls back to the tenant key instead of the plaintext it used
61
- // to be — still shreddable, one tenant wide (fw#2776).
61
+ // to be — still shreddable, one tenant wide.
62
62
  r.defineEvent("attempt", deliveryAttemptSchema, {
63
63
  piiFields: {
64
64
  recipientAddress: { personal: { of: "recipientId", whenAbsent: "tenant" } },
@@ -135,7 +135,7 @@ export const logQuery = definePagedQueryHandler({
135
135
  const rows = [...tiedRows, ...beyondRows];
136
136
  const lastRow = rows[rows.length - 1];
137
137
  const nextCursor = rows.length === limit && lastRow ? encodeSortCursor(sortColumn, lastRow) : null;
138
- // recipientAddress is stored encrypted under the recipient's DEK (#799)
138
+ // recipientAddress is stored encrypted under the recipient's DEK
139
139
  // — decrypt for the admin log view; forgotten subjects show [[erased]].
140
140
  // The notificationType/recipientAddress → type/recipient rename happens
141
141
  // here, not in the client: a projectionList screen has no entity to
@@ -1,4 +1,4 @@
1
1
  import * as z from "zod";
2
2
  export declare const preferencesQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"preferences", z.ZodObject<{}, z.core.$strip>, {
3
- rows: readonly any[];
3
+ rows: any[];
4
4
  }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -2,6 +2,7 @@ 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";
5
+ import { currentNotificationType } from "../notification-type-aliases.js";
5
6
  import { notificationPreferencesTable } from "../tables.js";
6
7
  export const preferencesQuery = defineQueryHandler({
7
8
  name: "preferences",
@@ -31,6 +32,24 @@ export const preferencesQuery = defineQueryHandler({
31
32
  tenantId: query.user.tenantId,
32
33
  userId: query.user.id,
33
34
  });
34
- return { rows };
35
+ // Rows stored under a former notification name are reported under the current
36
+ // one, collapsed with the same precedence as delivery: an exact row wins,
37
+ // otherwise any disabled former-name row disables.
38
+ const collapsed = new Map();
39
+ for (const row of rows) {
40
+ const notificationType = currentNotificationType(ctx.registry, row.notificationType);
41
+ const key = `${notificationType}\u0000${row.channel}`;
42
+ const isExact = row.notificationType === notificationType;
43
+ const existing = collapsed.get(key);
44
+ if (existing?.isExact)
45
+ continue;
46
+ const formerNameRowDisablesAlready = existing !== undefined &&
47
+ !isExact &&
48
+ (existing.row.enabled === false || row.enabled !== false);
49
+ if (formerNameRowDisablesAlready)
50
+ continue;
51
+ collapsed.set(key, { row: { ...row, notificationType }, isExact });
52
+ }
53
+ return { rows: [...collapsed.values()].map((entry) => entry.row) };
35
54
  },
36
55
  });
@@ -23,7 +23,7 @@ export const unsubscribePageChromeQuery = defineQueryHandler({
23
23
  // Only reachable via a system dispatch: the signed unsubscribe routes after verify(),
24
24
  // or createUnsubscribePageChromeReader for a tenant the app resolved server-side.
25
25
  agent: { expose: false },
26
- description: "Returns the tenant's mail brand row and footer resolved for one locale, or null without a mail renderer; dispatched by the signed unsubscribe route to dress its pages like the mail, not meant for UI callers.",
26
+ description: "Returns the tenant's mail brand row and footer resolved for one locale, or null without a mail renderer; dispatched by the signed unsubscribe route or by createUnsubscribePageChromeReader for a tenant the app resolved server-side, to dress its pages like the mail; not meant for UI callers.",
27
27
  handler: async (query, ctx) => {
28
28
  const { tenantId } = query.user;
29
29
  if (!ctx.systemDb) {
@@ -30,14 +30,15 @@ export const deliveryAttemptsTable = pgTable("store_delivery_attempts", {
30
30
  priority: text("priority").notNull().default("normal").$type(),
31
31
  createdAt: instant("created_at").default(sql `now()`).notNull(),
32
32
  }, (t) => [index(DELIVERY_ATTEMPTS_TENANT_CREATED_INDEX).on(t.tenantId, t.createdAt)]);
33
- // **Unmanaged table** — bewusst KEIN createEntity. Begründung:
34
- // - id kommt aus dem Aggregate-Stream (kein gen_random_uuid()-DEFAULT)
35
- // - kein version/inserted_by/modified_by/modified_at — keine in-place-
36
- // Edits, keine Audit-Spalten nötig (idempotent-on-replay via PK-Konflikt)
37
- // - created_at statt inserted_at — historischer Naming-Drift, kein Bug
38
- // App trägt Verantwortung für tenant-scoping in Queries + replay-idempotency.
39
- // pgTable bleibt source-of-truth für Query-API; Phase 4 leitet das pgTable
40
- // aus dieser Meta ab.
33
+ // **Unmanaged table** — deliberately has no createEntity because:
34
+ // - id comes from the aggregate stream (no gen_random_uuid()-DEFAULT)
35
+ // - no version/inserted_by/modified_by/modified_at: no in-place edits or
36
+ // audit columns are needed (idempotent-on-replay via PK conflict)
37
+ // - created_at rather than inserted_at reflects historical naming drift, not
38
+ // a bug
39
+ // The app is responsible for tenant scoping in queries and replay idempotency.
40
+ // pgTable remains the source of truth for the query API; the pgTable is derived
41
+ // from this metadata.
41
42
  export const deliveryAttemptsTableMeta = defineUnmanagedTable({
42
43
  tableName: "store_delivery_attempts",
43
44
  indexes: [{ name: DELIVERY_ATTEMPTS_TENANT_CREATED_INDEX, columns: ["tenant_id", "created_at"] }],
@@ -5,7 +5,9 @@ export type UnsubscribeLinkConfig = {
5
5
  readonly secret: string;
6
6
  readonly baseUrl: string;
7
7
  /** The sending tenant's own public origin (subdomain or custom domain), null when it has
8
- * none. The host must serve this app's `/api` routes. Null, a throw or an unusable value
8
+ * none. The host must serve this app's `/api` routes, and the callback must only return a
9
+ * verified, active domain of the tenant (the check that lets the app serve under it), else null:
10
+ * the link carries a bearer token that unsubscribes and yields a resubscribe token. Null, a throw or an unusable value
9
11
  * fall back to `baseUrl`, so a missing tenant origin never fails a send. */
10
12
  readonly tenantOrigin?: (ctx: ChannelContext) => Promise<string | null | undefined>;
11
13
  };
@@ -10,6 +10,7 @@ export declare const UNSUBSCRIBE_PAGE_EN: {
10
10
  readonly "mail.unsubscribe.resubscribedMessage": "You have been subscribed again.";
11
11
  readonly "mail.unsubscribe.failed": "That did not work. Please try again later.";
12
12
  readonly "mail.unsubscribe.failedTitle": "Something went wrong";
13
+ readonly "mail.unsubscribe.resubscribeLimitReached": "This address cannot be subscribed again here.";
13
14
  readonly "mail.unsubscribe.invalidTitle": "Link no longer valid";
14
15
  readonly "mail.unsubscribe.invalidMessage": "This link is invalid or has expired. Please use the unsubscribe link in a more recent email.";
15
16
  };
@@ -13,6 +13,7 @@ export const UNSUBSCRIBE_PAGE_EN = {
13
13
  "mail.unsubscribe.resubscribedMessage": "You have been subscribed again.",
14
14
  "mail.unsubscribe.failed": "That did not work. Please try again later.",
15
15
  "mail.unsubscribe.failedTitle": "Something went wrong",
16
+ "mail.unsubscribe.resubscribeLimitReached": "This address cannot be subscribed again here.",
16
17
  "mail.unsubscribe.invalidTitle": "Link no longer valid",
17
18
  "mail.unsubscribe.invalidMessage": "This link is invalid or has expired. Please use the unsubscribe link in a more recent email.",
18
19
  };
@@ -10,6 +10,8 @@ export type UnsubscribePageState = {
10
10
  readonly kind: "resubscribed";
11
11
  } | {
12
12
  readonly kind: "failed";
13
+ } | {
14
+ readonly kind: "resubscribeLimitReached";
13
15
  } | {
14
16
  readonly kind: "invalid";
15
17
  };
@@ -135,6 +135,11 @@ ${tokenForm(DELIVERY_RESUBSCRIBE_PATH, state.resubscribeToken, mailT(lang, "mail
135
135
  title: mailT(lang, "mail.unsubscribe.failedTitle"),
136
136
  bodyHtml: paragraph("mail.unsubscribe.failed"),
137
137
  };
138
+ case "resubscribeLimitReached":
139
+ return {
140
+ title: mailT(lang, "mail.unsubscribe.failedTitle"),
141
+ bodyHtml: paragraph("mail.unsubscribe.resubscribeLimitReached"),
142
+ };
138
143
  case "invalid":
139
144
  return {
140
145
  title: mailT(lang, "mail.unsubscribe.invalidTitle"),
@@ -13,13 +13,13 @@ const localeClaimSchema = z
13
13
  .string()
14
14
  .regex(/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{1,8})*$/)
15
15
  .optional();
16
- // Shape des verified-JWT-payloads. tenantId kommt als string aus jose und
17
- // wird NACH erfolgreichem parse() zur Branded TenantId — kein blind-cast.
16
+ // Shape of verified JWT payloads. `tenantId` arrives from jose as a string and
17
+ // becomes a branded `TenantId` only after successful `parse()`—never via a blind cast.
18
18
  //
19
19
  // `kind` is explicitly typed as "absent or undefined, nothing else" here
20
20
  // (not bare `.optional()`, which would also accept any other value on an
21
- // unrelated key) so an address-token payload — which always carries
22
- // `kind: "address"` — fails this schema instead of silently parsing with
21
+ // unrelated key) so an address-token payload—which always carries
22
+ // `kind: "address"`—fails this schema instead of silently parsing with
23
23
  // `addressHash` mistaken for a userId.
24
24
  const unsubscribeJwtPayloadSchema = z.object({
25
25
  kind: z.undefined().optional(),
@@ -373,10 +373,16 @@ async function verifyForPage(token, encodedSecret, purpose, headers) {
373
373
  }
374
374
  }
375
375
  async function verifyPost(request, token, encodedSecret, purpose) {
376
- return isFormRequest(request.headers["content-type"])
376
+ return isFormRequest(request.headers["content-type"]) && !isOneClickPost(request)
377
377
  ? verifyForPage(token, encodedSecret, purpose, request.headers)
378
378
  : verifyUnsubscribeToken(token, encodedSecret, purpose);
379
379
  }
380
+ // RFC 8058 mail-provider POSTs are form-encoded too, but nobody looks at the answer: they keep
381
+ // the JSON rejection and get no branded page.
382
+ function isOneClickPost(request) {
383
+ return (isFormRequest(request.headers["content-type"]) &&
384
+ request.rawBody.trim() === DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE);
385
+ }
380
386
  function isFormRequest(contentType) {
381
387
  return contentType?.includes("application/x-www-form-urlencoded") === true;
382
388
  }
@@ -435,22 +441,31 @@ export function createUnsubscribeRoutes(options) {
435
441
  method: "POST",
436
442
  path: DELIVERY_UNSUBSCRIBE_PATH,
437
443
  entry: "signature",
438
- verify: async (request) => verifyPost(request, tokenFromPostRequest(request), encodedSecret, "unsubscribe"),
444
+ verify: async (request) => {
445
+ const verified = await verifyPost(request, tokenFromPostRequest(request), encodedSecret, "unsubscribe");
446
+ return verified.kind === "rejected"
447
+ ? verified
448
+ : { ...verified, oneClick: isOneClickPost(request) };
449
+ },
439
450
  handler: async (c, verified, deps) => {
440
451
  if (verified.kind === "rejected") {
441
452
  return pageResponse(c, deps, { kind: "invalid" }, verified, 400);
442
453
  }
454
+ // No chrome lookup for a one-click client: it never renders the page.
455
+ const pageScope = verified.oneClick
456
+ ? { tenantId: undefined, locale: verified.locale }
457
+ : verified;
443
458
  // Token-verify passed — everything below is a legitimate write. Don't
444
459
  // swallow write-errors as "invalid token", that would mask real bugs
445
460
  // (e.g. events-table missing, DB down) behind a misleading 400.
446
461
  const succeeded = await dispatchUnsubscribeWrite(verified, deps);
447
462
  if (!succeeded) {
448
- return pageResponse(c, deps, { kind: "failed" }, verified, 500);
463
+ return pageResponse(c, deps, { kind: "failed" }, pageScope, 500);
449
464
  }
450
465
  return pageResponse(c, deps, {
451
466
  kind: "unsubscribed",
452
467
  resubscribeToken: await signResubscribeToken(verified, encodedSecret),
453
- }, verified, 200);
468
+ }, pageScope, 200);
454
469
  },
455
470
  });
456
471
  // Undo direction: token only from the JSON/form body, never the query
@@ -468,9 +483,12 @@ export function createUnsubscribeRoutes(options) {
468
483
  const outcome = await dispatchResubscribeWrite(verified, deps);
469
484
  // The undo button on the unsubscribed page posts a form; a browser should land on a page.
470
485
  if (isFormRequest(c.req.header("content-type"))) {
471
- return outcome === "success"
472
- ? pageResponse(c, deps, { kind: "resubscribed" }, verified, 200)
473
- : pageResponse(c, deps, { kind: "failed" }, verified, outcome === "limit_reached" ? 409 : 500);
486
+ if (outcome === "success") {
487
+ return pageResponse(c, deps, { kind: "resubscribed" }, verified, 200);
488
+ }
489
+ return outcome === "limit_reached"
490
+ ? pageResponse(c, deps, { kind: "resubscribeLimitReached" }, verified, 409)
491
+ : pageResponse(c, deps, { kind: "failed" }, verified, 500);
474
492
  }
475
493
  if (outcome === "limit_reached") {
476
494
  return c.json({ error: { code: DeliveryErrors.resubscribeLimitReached } }, 409, NO_STORE_HEADERS);