@cosmicdrift/kumiko-samples 0.388.0 → 0.390.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 (385) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +25 -13
  3. package/packages/bundled-features/src/agent-tools/changes.json +6 -0
  4. package/packages/bundled-features/src/agent-tools/index.ts +1 -1
  5. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +42 -26
  6. package/packages/bundled-features/src/audit/feature.ts +2 -1
  7. package/packages/bundled-features/src/auth-email-password/auth-user-row.ts +1 -1
  8. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +13 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +18 -0
  10. package/packages/bundled-features/src/auth-email-password/feature.ts +2 -2
  11. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +7 -7
  12. package/packages/bundled-features/src/auth-email-password/handlers/confirm-account-unlock.write.ts +3 -3
  13. package/packages/bundled-features/src/auth-email-password/handlers/confirm-token-flow.ts +5 -5
  14. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +9 -7
  15. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +11 -7
  16. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -4
  17. package/packages/bundled-features/src/auth-email-password/handlers/invite-info.query.ts +2 -2
  18. package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +9 -7
  19. package/packages/bundled-features/src/auth-email-password/handlers/login.write.ts +23 -16
  20. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
  21. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
  22. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
  23. package/packages/bundled-features/src/auth-email-password/handlers/reset-password.write.ts +3 -2
  24. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +12 -12
  25. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +16 -8
  26. package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +4 -2
  27. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +10 -2
  28. package/packages/bundled-features/src/auth-email-password/handlers/verify-email.write.ts +3 -2
  29. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
  30. package/packages/bundled-features/src/auth-email-password/seeding.ts +14 -9
  31. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
  32. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
  33. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
  34. package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
  35. package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
  36. package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
  37. package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
  38. package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
  39. package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
  40. package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
  41. package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
  42. package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
  43. package/packages/bundled-features/src/auth-foundation/feature.ts +13 -4
  44. package/packages/bundled-features/src/auth-mfa/changes.json +6 -0
  45. package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
  46. package/packages/bundled-features/src/auth-mfa/handlers/disable.write.ts +5 -5
  47. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +24 -14
  48. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm.write.ts +9 -3
  49. package/packages/bundled-features/src/auth-mfa/handlers/enable-start-preauth.write.ts +6 -1
  50. package/packages/bundled-features/src/auth-mfa/handlers/enable-start.write.ts +3 -1
  51. package/packages/bundled-features/src/auth-mfa/handlers/reencrypt.job.ts +14 -9
  52. package/packages/bundled-features/src/auth-mfa/handlers/regenerate-recovery.write.ts +5 -4
  53. package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +25 -14
  54. package/packages/bundled-features/src/auth-mfa/mfa-code-verifier.ts +10 -4
  55. package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +17 -8
  56. package/packages/bundled-features/src/auth-mfa/verify-factor.ts +1 -1
  57. package/packages/bundled-features/src/billing-foundation/billing-info-query.ts +2 -2
  58. package/packages/bundled-features/src/billing-foundation/changes.json +6 -0
  59. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +1 -1
  60. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +5 -5
  61. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +10 -7
  62. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +15 -12
  63. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-open-cancels.ts +22 -0
  64. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-provider-lock.ts +4 -0
  65. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +4 -4
  66. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +17 -58
  67. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +10 -8
  68. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +18 -15
  69. package/packages/bundled-features/src/billing-foundation/events.ts +1 -1
  70. package/packages/bundled-features/src/billing-foundation/feature.ts +8 -3
  71. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +1 -1
  72. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +5 -5
  73. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +3 -3
  74. package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +2 -2
  75. package/packages/bundled-features/src/billing-foundation/projection.ts +1 -1
  76. package/packages/bundled-features/src/billing-foundation/types.ts +1 -1
  77. package/packages/bundled-features/src/cap-counter/aggregate-id.ts +1 -1
  78. package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +5 -4
  79. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +21 -8
  80. package/packages/bundled-features/src/cap-counter/changes.json +12 -0
  81. package/packages/bundled-features/src/cap-counter/constants.ts +1 -1
  82. package/packages/bundled-features/src/cap-counter/feature.ts +3 -2
  83. package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +1 -1
  84. package/packages/bundled-features/src/cap-counter/reservation-chain.ts +1 -1
  85. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +4 -4
  86. package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
  87. package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +1 -1
  88. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +1 -1
  89. package/packages/bundled-features/src/compliance-profiles/seeding.ts +12 -7
  90. package/packages/bundled-features/src/config/changes.json +6 -0
  91. package/packages/bundled-features/src/config/feature.ts +6 -6
  92. package/packages/bundled-features/src/config/handlers/cascade.query.ts +1 -1
  93. package/packages/bundled-features/src/config/handlers/readiness.query.ts +1 -1
  94. package/packages/bundled-features/src/config/handlers/reencrypt.job.ts +23 -13
  95. package/packages/bundled-features/src/config/handlers/reset.write.ts +3 -3
  96. package/packages/bundled-features/src/config/handlers/selected-extensions.query.ts +1 -1
  97. package/packages/bundled-features/src/config/handlers/set.write.ts +22 -5
  98. package/packages/bundled-features/src/config/handlers/values.query.ts +1 -1
  99. package/packages/bundled-features/src/crypto-shredding/changes.json +12 -0
  100. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +9 -9
  101. package/packages/bundled-features/src/crypto-shredding/resolve-file-bytes-deleter.ts +3 -3
  102. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  103. package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +1 -1
  104. package/packages/bundled-features/src/custom-fields/handlers/delete-system-field.write.ts +1 -1
  105. package/packages/bundled-features/src/custom-fields/handlers/delete-tenant-field.write.ts +1 -1
  106. package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +1 -1
  107. package/packages/bundled-features/src/custom-fields/index.ts +4 -0
  108. package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
  109. package/packages/bundled-features/src/data-retention/feature.ts +1 -1
  110. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +12 -2
  111. package/packages/bundled-features/src/delivery/attempt-log.ts +1 -1
  112. package/packages/bundled-features/src/delivery/changes.json +12 -0
  113. package/packages/bundled-features/src/delivery/delivery-service.ts +6 -5
  114. package/packages/bundled-features/src/delivery/feature.ts +3 -2
  115. package/packages/bundled-features/src/delivery/index.ts +5 -0
  116. package/packages/bundled-features/src/delivery/jobs.ts +11 -11
  117. package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
  118. package/packages/bundled-features/src/delivery/testing.ts +1 -1
  119. package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
  120. package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
  121. package/packages/bundled-features/src/document-ingest-foundation/events.ts +16 -1
  122. package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
  123. package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
  124. package/packages/bundled-features/src/feature-toggles/handlers/registered.query.ts +1 -1
  125. package/packages/bundled-features/src/feature-toggles/handlers/set.write.ts +1 -1
  126. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +1 -1
  127. package/packages/bundled-features/src/file-derivatives/feature.ts +2 -2
  128. package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +5 -1
  129. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +7 -5
  130. package/packages/bundled-features/src/file-foundation/feature.ts +1 -1
  131. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -5
  132. package/packages/bundled-features/src/file-provider-s3/feature.ts +4 -1
  133. package/packages/bundled-features/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +8 -8
  134. package/packages/bundled-features/src/form-draft/feature.ts +1 -1
  135. package/packages/bundled-features/src/form-draft/handlers/cleanup.job.ts +22 -12
  136. package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +3 -3
  137. package/packages/bundled-features/src/form-draft/handlers/get.query.ts +1 -1
  138. package/packages/bundled-features/src/foundation-shared/config-helpers.ts +1 -1
  139. package/packages/bundled-features/src/host-routing/changes.json +6 -0
  140. package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
  141. package/packages/bundled-features/src/inbound-mail-foundation/events.ts +1 -1
  142. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +2 -1
  143. package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-state.ts +2 -2
  144. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  145. package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +1 -1
  146. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +10 -10
  147. package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +1 -1
  148. package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +1 -1
  149. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +1 -1
  150. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +4 -1
  151. package/packages/bundled-features/src/jobs/changes.json +6 -0
  152. package/packages/bundled-features/src/jobs/events.ts +1 -1
  153. package/packages/bundled-features/src/jobs/feature.ts +1 -0
  154. package/packages/bundled-features/src/jobs/handlers/consumer.write.ts +7 -4
  155. package/packages/bundled-features/src/jobs/handlers/projection-rebuild.job.ts +1 -1
  156. package/packages/bundled-features/src/jobs/handlers/reindex-entity.job.ts +11 -5
  157. package/packages/bundled-features/src/jobs/handlers/retention-cleanup.job.ts +1 -1
  158. package/packages/bundled-features/src/jobs/handlers/retry.write.ts +1 -1
  159. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +1 -1
  160. package/packages/bundled-features/src/jobs/handlers/trigger.write.ts +2 -2
  161. package/packages/bundled-features/src/jobs/job-run-logger.ts +139 -90
  162. package/packages/bundled-features/src/ledger/web/index.ts +1 -1
  163. package/packages/bundled-features/src/legal-pages/README.md +1 -1
  164. package/packages/bundled-features/src/legal-pages/lib/boot-check.ts +12 -8
  165. package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
  166. package/packages/bundled-features/src/mail-foundation/feature.ts +2 -2
  167. package/packages/bundled-features/src/mail-transport-smtp/changes.json +12 -0
  168. package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
  169. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +13 -12
  170. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  171. package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
  172. package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
  173. package/packages/bundled-features/src/managed-pages/index.ts +1 -0
  174. package/packages/bundled-features/src/managed-pages/seeding.ts +13 -8
  175. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +2 -2
  176. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -1
  177. package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +2 -2
  178. package/packages/bundled-features/src/personal-access-tokens/handlers/revoke.write.ts +1 -1
  179. package/packages/bundled-features/src/rate-limiting/handlers/status.query.ts +2 -2
  180. package/packages/bundled-features/src/readiness/handlers/status.query.ts +4 -1
  181. package/packages/bundled-features/src/renderer-foundation/api.ts +3 -3
  182. package/packages/bundled-features/src/secrets/announce-secret-change.ts +4 -4
  183. package/packages/bundled-features/src/secrets/changes.json +6 -0
  184. package/packages/bundled-features/src/secrets/feature.ts +9 -9
  185. package/packages/bundled-features/src/secrets/handlers/delete.write.ts +4 -3
  186. package/packages/bundled-features/src/secrets/handlers/rotate.job.ts +13 -8
  187. package/packages/bundled-features/src/secrets/handlers/set.write.ts +4 -3
  188. package/packages/bundled-features/src/secrets/secrets-context.ts +16 -6
  189. package/packages/bundled-features/src/secrets/write-gate.ts +10 -3
  190. package/packages/bundled-features/src/seo/feature.ts +1 -1
  191. package/packages/bundled-features/src/sessions/changes.json +19 -0
  192. package/packages/bundled-features/src/sessions/feature.ts +2 -1
  193. package/packages/bundled-features/src/sessions/handlers/cleanup.job.ts +2 -2
  194. package/packages/bundled-features/src/sessions/handlers/detail.query.ts +5 -2
  195. package/packages/bundled-features/src/sessions/handlers/list.query.ts +5 -2
  196. package/packages/bundled-features/src/sessions/handlers/mine.query.ts +5 -2
  197. package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +3 -3
  198. package/packages/bundled-features/src/sessions/handlers/revoke-all-others.write.ts +1 -1
  199. package/packages/bundled-features/src/sessions/handlers/revoke.write.ts +1 -1
  200. package/packages/bundled-features/src/sessions/index.ts +1 -1
  201. package/packages/bundled-features/src/sessions/screens.ts +8 -4
  202. package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
  203. package/packages/bundled-features/src/sessions/session-revoked-event.ts +1 -1
  204. package/packages/bundled-features/src/sessions/web/session-device-cell.tsx +10 -12
  205. package/packages/bundled-features/src/sessions/web/user-agent.ts +12 -0
  206. package/packages/bundled-features/src/shared/index.ts +15 -0
  207. package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +36 -4
  208. package/packages/bundled-features/src/shared/require-for-tenant.ts +7 -5
  209. package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
  210. package/packages/bundled-features/src/shared/stream-tenant-backfill.ts +201 -0
  211. package/packages/bundled-features/src/shared/stream-tenant-derived-state.ts +107 -0
  212. package/packages/bundled-features/src/step-dispatcher/changes.json +6 -0
  213. package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
  214. package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -1
  215. package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
  216. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +1 -1
  217. package/packages/bundled-features/src/subscription-dodo/runtime.ts +7 -7
  218. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +5 -3
  219. package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -7
  220. package/packages/bundled-features/src/tags/feature.ts +1 -0
  221. package/packages/bundled-features/src/template-resolver/api.ts +3 -3
  222. package/packages/bundled-features/src/template-resolver/changes.json +54 -0
  223. package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
  224. package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
  225. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +5 -4
  226. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +2 -2
  227. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +1 -1
  228. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
  229. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +32 -7
  230. package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
  231. package/packages/bundled-features/src/template-resolver/handlers/send-test-shared.ts +1 -1
  232. package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
  233. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +2 -2
  234. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +6 -2
  235. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +4 -2
  236. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +1 -1
  237. package/packages/bundled-features/src/template-resolver/seeding.ts +22 -5
  238. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +68 -19
  239. package/packages/bundled-features/src/template-resolver/template-resources.ts +4 -4
  240. package/packages/bundled-features/src/template-resolver/testing.ts +2 -2
  241. package/packages/bundled-features/src/template-resolver/web/document-insert-fields.tsx +222 -0
  242. package/packages/bundled-features/src/template-resolver/web/document-insert-model.ts +164 -0
  243. package/packages/bundled-features/src/template-resolver/web/document-insert-panel.tsx +115 -0
  244. package/packages/bundled-features/src/template-resolver/web/document-tiptap-bubble.tsx +177 -0
  245. package/packages/bundled-features/src/template-resolver/web/document-tiptap-content.tsx +38 -0
  246. package/packages/bundled-features/src/template-resolver/web/document-tiptap-context.ts +119 -0
  247. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +51 -1098
  248. package/packages/bundled-features/src/template-resolver/web/document-tiptap-helpers.ts +161 -0
  249. package/packages/bundled-features/src/template-resolver/web/document-tiptap-layout.tsx +83 -0
  250. package/packages/bundled-features/src/template-resolver/web/document-tiptap-toolbar-groups.tsx +153 -0
  251. package/packages/bundled-features/src/template-resolver/web/document-tiptap-toolbar.tsx +182 -0
  252. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +18 -0
  253. package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
  254. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +19 -6
  255. package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
  256. package/packages/bundled-features/src/template-resolver/web/use-document-tiptap-model.ts +138 -0
  257. package/packages/bundled-features/src/tenant/changes.json +19 -0
  258. package/packages/bundled-features/src/tenant/feature.ts +1 -0
  259. package/packages/bundled-features/src/tenant/handlers/cancel-invitation.write.ts +1 -1
  260. package/packages/bundled-features/src/tenant/handlers/memberships.query.ts +1 -1
  261. package/packages/bundled-features/src/tenant/handlers/remove-member.write.ts +2 -2
  262. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
  263. package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +1 -1
  264. package/packages/bundled-features/src/tenant/index.ts +5 -0
  265. package/packages/bundled-features/src/tenant/invited-membership.ts +6 -1
  266. package/packages/bundled-features/src/tenant/schema/tenant.ts +4 -1
  267. package/packages/bundled-features/src/tenant/seeding.ts +65 -62
  268. package/packages/bundled-features/src/tenant/stream-tenant-backfill.ts +115 -0
  269. package/packages/bundled-features/src/tenant/testing.ts +77 -0
  270. package/packages/bundled-features/src/tenant-handover/events.ts +1 -1
  271. package/packages/bundled-features/src/tenant-handover/handlers/claim.write.ts +1 -1
  272. package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
  273. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +5 -5
  274. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +22 -10
  275. package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +22 -11
  276. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +23 -12
  277. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +42 -4
  278. package/packages/bundled-features/src/tenant-lifecycle/testing.ts +9 -8
  279. package/packages/bundled-features/src/tier-engine/entity.ts +1 -1
  280. package/packages/bundled-features/src/tier-engine/feature.ts +8 -3
  281. package/packages/bundled-features/src/tier-engine/handlers/active-tier.query.ts +3 -3
  282. package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +7 -1
  283. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +3 -3
  284. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +71 -97
  285. package/packages/bundled-features/src/user/feature.ts +1 -0
  286. package/packages/bundled-features/src/user/handlers/find-for-auth.query.ts +2 -2
  287. package/packages/bundled-features/src/user/handlers/update-roles.ts +2 -2
  288. package/packages/bundled-features/src/user/index.ts +2 -0
  289. package/packages/bundled-features/src/user/invited-global-roles.ts +6 -1
  290. package/packages/bundled-features/src/user/principal-status.ts +1 -1
  291. package/packages/bundled-features/src/user/seeding.ts +24 -6
  292. package/packages/bundled-features/src/user/testing.ts +22 -0
  293. package/packages/bundled-features/src/user-data-rights/COMPLIANCE.md +1 -1
  294. package/packages/bundled-features/src/user-data-rights/README.md +1 -1
  295. package/packages/bundled-features/src/user-data-rights/audit-download.ts +12 -2
  296. package/packages/bundled-features/src/user-data-rights/changes.json +36 -0
  297. package/packages/bundled-features/src/user-data-rights/feature.ts +30 -24
  298. package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +1 -1
  299. package/packages/bundled-features/src/user-data-rights/handlers/deletion-grace-period.ts +1 -1
  300. package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +2 -2
  301. package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +2 -2
  302. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
  303. package/packages/bundled-features/src/user-data-rights/handlers/restrict-account.write.ts +4 -4
  304. package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +12 -6
  305. package/packages/bundled-features/src/user-data-rights/index.ts +8 -1
  306. package/packages/bundled-features/src/user-data-rights/lib/mail-transport-resolver.ts +1 -1
  307. package/packages/bundled-features/src/user-data-rights/lib/storage-provider-resolver.ts +1 -1
  308. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +6 -1
  309. package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +8 -2
  310. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +14 -3
  311. package/packages/bundled-features/src/user-data-rights/run-user-export.ts +11 -1
  312. package/packages/bundled-features/src/user-data-rights/web/constants.ts +16 -0
  313. package/packages/bundled-features/src/user-data-rights/web/index.ts +6 -0
  314. package/packages/bundled-features/src/user-profile/handlers/change-email.write.ts +8 -6
  315. package/packages/bundled-features/src/waitlist/changes.json +18 -0
  316. package/packages/bundled-features/src/waitlist/feature.ts +1 -0
  317. package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +1 -1
  318. package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +2 -2
  319. package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +1 -1
  320. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +34 -10
  321. package/packages/bundled-features/src/waitlist/import.ts +23 -3
  322. package/packages/bundled-features/src/waitlist/legacy-migrate.ts +8 -2
  323. package/packages/bundled-features/src/waitlist/link-on-signup.ts +7 -1
  324. package/packages/bundled-features/src/waitlist/options.ts +2 -1
  325. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  326. package/packages/bundled-features/src/workflow-runner/db/queries/due-runs.ts +1 -1
  327. package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +4 -3
  328. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
  329. package/packages/bundled-features/src/workflow-runner/feature.ts +2 -2
  330. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +8 -8
  331. package/packages/bundled-features/src/workflow-runner/runner.ts +6 -4
  332. package/samples/apps/cap-billing-demo/src/feature.ts +19 -5
  333. package/samples/apps/config-demo/src/app/server.ts +1 -1
  334. package/samples/apps/styleguide/src/app/client.tsx +2 -0
  335. package/samples/apps/styleguide/src/app/seed.ts +6 -4
  336. package/samples/apps/styleguide/src/app/server.ts +6 -3
  337. package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
  338. package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
  339. package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
  340. package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
  341. package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
  342. package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
  343. package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
  344. package/samples/apps/use-all-bundled/bin/main.ts +2 -0
  345. package/samples/apps/use-all-bundled/feature-manifest.json +17 -1
  346. package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
  347. package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
  348. package/samples/apps/use-all-bundled/package.json +2 -1
  349. package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
  350. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
  351. package/samples/apps/use-all-bundled/src/app/seed.ts +12 -2
  352. package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
  353. package/samples/apps/user-data-rights-demo/README.md +1 -1
  354. package/samples/apps/user-data-rights-demo/src/feature.ts +3 -2
  355. package/samples/recipes/api-worker-split/README.md +2 -1
  356. package/samples/recipes/api-worker-split/src/feature.ts +2 -1
  357. package/samples/recipes/cross-feature-events/README.md +5 -5
  358. package/samples/recipes/cross-feature-events/package.json +1 -1
  359. package/samples/recipes/cross-feature-events/src/feature.ts +4 -4
  360. package/samples/recipes/event-sourcing/README.md +5 -5
  361. package/samples/recipes/event-sourcing/package.json +1 -1
  362. package/samples/recipes/event-sourcing/src/feature.ts +15 -15
  363. package/samples/recipes/files-post-processing/README.md +1 -1
  364. package/samples/recipes/files-post-processing/package.json +1 -1
  365. package/samples/recipes/files-post-processing/src/feature.ts +6 -4
  366. package/samples/recipes/golden-path/src/feature.ts +2 -1
  367. package/samples/recipes/lane-routing/package.json +1 -1
  368. package/samples/recipes/legal-pages/README.md +2 -2
  369. package/samples/recipes/lifecycle-hooks/README.md +4 -0
  370. package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
  371. package/samples/recipes/list-table-options/README.md +50 -0
  372. package/samples/recipes/list-table-options/package.json +11 -0
  373. package/samples/recipes/list-table-options/src/feature.ts +221 -0
  374. package/samples/recipes/managed-config/README.md +3 -3
  375. package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
  376. package/samples/recipes/search/README.md +1 -1
  377. package/samples/recipes/search/src/feature.ts +3 -3
  378. package/samples/recipes/secrets-demo/README.md +1 -1
  379. package/samples/recipes/secrets-demo/src/feature.ts +5 -2
  380. package/samples/recipes/state-machine/src/events.ts +4 -3
  381. package/samples/recipes/state-machine/src/reducer.ts +1 -1
  382. package/samples/recipes/tenant-isolation/README.md +1 -0
  383. package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
  384. package/samples/recipes/workflow-engine/src/resume-loop.ts +4 -4
  385. package/samples/recipes/workflow-engine/src/workflow-runner.ts +2 -2
@@ -12,7 +12,7 @@ import * as z from "zod";
12
12
  // _SHORT/_QN pair mirrors cap-counter's rolling-incremented event:
13
13
  // _SHORT — r.defineEvent(short, schema) in feature.ts, framework prefixes
14
14
  // it to the QN below.
15
- // _QN — ctx.unsafeAppendEvent({ type }) / raw append()'s `type` field.
15
+ // _QN — ctx.events.unsafeAppend({ type }) / raw append()'s `type` field.
16
16
  export const SESSION_REVOKED_EVENT_SHORT = "session-revoked" as const;
17
17
  export const SESSION_REVOKED_EVENT_QN = "sessions:event:session-revoked" as const;
18
18
 
@@ -1,19 +1,17 @@
1
- // Column renderer for the parsed `device` of a session row, registered via `sessionsClient()`.
1
+ // Column renderer for session devices, registered via `sessionsClient()`. Takes the server-made
2
+ // `deviceLabel` string, or the parsed `device` object for screens that bind that column.
2
3
 
3
4
  import { type ColumnRendererProps, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
5
  import type { ReactNode } from "react";
5
- import { parsedDeviceSchema } from "./user-agent.ts";
6
+ import { formatDeviceLabel, parsedDeviceSchema } from "./user-agent.ts";
7
+
8
+ function deviceLabelOf(value: unknown): string | null {
9
+ if (typeof value === "string") return value === "" ? null : value;
10
+ const parsed = parsedDeviceSchema.safeParse(value);
11
+ return parsed.success ? formatDeviceLabel(parsed.data) : null;
12
+ }
6
13
 
7
14
  export function SessionDeviceCell({ value }: ColumnRendererProps): ReactNode {
8
15
  const t = useTranslation();
9
- const parsed = parsedDeviceSchema.safeParse(value);
10
- const device = parsed.success ? parsed.data : null;
11
- if (device === null) return t("sessions.device.unknown");
12
- const browser =
13
- device.browser === null
14
- ? null
15
- : device.browserMajor === null
16
- ? device.browser
17
- : `${device.browser} ${device.browserMajor}`;
18
- return [browser, device.os].filter((part) => part !== null).join(" · ");
16
+ return deviceLabelOf(value) ?? t("sessions.device.unknown");
19
17
  }
@@ -56,6 +56,18 @@ const OPERATING_SYSTEM_PATTERNS: readonly {
56
56
  { os: "Linux", pattern: /\bLinux\b/ },
57
57
  ];
58
58
 
59
+ export function formatDeviceLabel(device: ParsedDevice): string | null {
60
+ if (device === null) return null;
61
+ const browser =
62
+ device.browser === null
63
+ ? null
64
+ : device.browserMajor === null
65
+ ? device.browser
66
+ : `${device.browser} ${device.browserMajor}`;
67
+ const parts = [browser, device.os].filter((part) => part !== null);
68
+ return parts.length === 0 ? null : parts.join(" · ");
69
+ }
70
+
59
71
  export function parseUserAgent(userAgent: string | null): ParsedDevice {
60
72
  if (userAgent === null || userAgent === "") return null;
61
73
  let browser: UserAgentBrowser | null = null;
@@ -92,5 +92,20 @@ export {
92
92
  type SignupHandoverProvider,
93
93
  } from "./signup-handover.ts";
94
94
  export { createSingleUseTokenStore, hashSingleUseToken } from "./single-use-token-store.ts";
95
+ export {
96
+ backfillAggregateStreamTenants,
97
+ type StreamBackfillCandidate,
98
+ type StreamBackfillFailure,
99
+ type StreamBackfillOptions,
100
+ type StreamBackfillResult,
101
+ type StreamTenantTarget,
102
+ } from "./stream-tenant-backfill.ts";
103
+ export {
104
+ createSearchBucketCleanup,
105
+ type DerivedStateMode,
106
+ rebuildAndReindex,
107
+ type StreamBackfillStepFailure,
108
+ stepFailures,
109
+ } from "./stream-tenant-derived-state.ts";
95
110
  export type { SystemQueryFn } from "./system-query.ts";
96
111
  export { type BurnResult, burnToken, unburnToken } from "./token-burn-store.ts";
@@ -112,6 +112,29 @@ function fieldsResolvingToSubject(
112
112
  });
113
113
  }
114
114
 
115
+ // A user owning many records must not load them all at once; the id cursor keeps each page
116
+ // bounded while rows purged in place stay out of the way.
117
+ export const PURGE_CANDIDATE_PAGE_SIZE = 200;
118
+
119
+ async function* candidateRowsPaged(
120
+ db: DbRunner,
121
+ table: FileEntity["table"],
122
+ where: WhereObject,
123
+ ): AsyncGenerator<Record<string, unknown>> {
124
+ let cursor: unknown;
125
+ for (;;) {
126
+ const pageWhere: WhereObject = cursor === undefined ? where : { ...where, id: { gt: cursor } };
127
+ const page = await selectMany<Record<string, unknown>>(db, table, pageWhere, {
128
+ orderBy: { col: "id", direction: "asc" },
129
+ limit: PURGE_CANDIDATE_PAGE_SIZE,
130
+ });
131
+ yield* page;
132
+ // skip: a short page is the last one
133
+ if (page.length < PURGE_CANDIDATE_PAGE_SIZE) return;
134
+ cursor = page[page.length - 1]?.["id"];
135
+ }
136
+ }
137
+
115
138
  const fileRefExecutor = createEventStoreExecutor(fileRefsTable, fileRefEntity, {
116
139
  entityName: "fileRef",
117
140
  });
@@ -165,8 +188,7 @@ export async function purgeFilesReachingSubject(args: {
165
188
  for (const fileEntity of fileEntities) {
166
189
  const seenRowIds = new Set<string>();
167
190
  for (const where of candidateWheres(subject, recordTenantId, fileEntity)) {
168
- const rows = await selectMany<Record<string, unknown>>(db, fileEntity.table, where);
169
- for (const row of rows) {
191
+ for await (const row of candidateRowsPaged(db, fileEntity.table, where)) {
170
192
  const rowId = String(row["id"]);
171
193
  const tenantId = parseTenantId(row["tenantId"]);
172
194
  // skip: a row without a tenant cannot own tenant-scoped file refs
@@ -184,7 +206,12 @@ export async function purgeFilesReachingSubject(args: {
184
206
  }
185
207
  const deleteBytes = await storagePromise;
186
208
  const systemUser = createSystemUser(tenantId);
187
- const tdb = createTenantDb({ db, tenantId, mode: "system" });
209
+ const tdb = createTenantDb({
210
+ db,
211
+ tenantId,
212
+ mode: "system",
213
+ reason: "subject erasure purges file refs in every tenant that holds them",
214
+ });
188
215
 
189
216
  const outcome = await purgeRowFileRefs({
190
217
  db,
@@ -331,7 +358,12 @@ async function purgePendingFileRef(
331
358
  const forgotten = await fileRefExecutor.forget(
332
359
  { id: fileRefId },
333
360
  createSystemUser(tenantId),
334
- createTenantDb({ db, tenantId, mode: "system" }),
361
+ createTenantDb({
362
+ db,
363
+ tenantId,
364
+ mode: "system",
365
+ reason: "subject erasure purges file refs in every tenant that holds them",
366
+ }),
335
367
  );
336
368
  return forgotten.isSuccess
337
369
  ? undefined
@@ -5,14 +5,16 @@ import type {
5
5
  } from "@cosmicdrift/kumiko-framework/engine";
6
6
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
7
7
 
8
- // ctx.forTenant is absent for r.systemScope() handlers and without a db; a write handler that
8
+ // ctx.tenants.writeTarget is absent for r.systemScope() handlers and without a db; a write handler that
9
9
  // needs a cross-tenant target must fail loudly instead of falling back to ctx.db.
10
10
  export function requireForTenant(
11
- ctx: Pick<HandlerContext, "forTenant">,
11
+ ctx: Pick<HandlerContext, "tenants">,
12
12
  targetTenantId: TenantId,
13
13
  ): TenantWriteTarget {
14
- if (!ctx.forTenant) {
15
- throw new InternalError({ message: "ctx.forTenant is not available in this handler context" });
14
+ if (!ctx.tenants.writeTarget) {
15
+ throw new InternalError({
16
+ message: "ctx.tenants.writeTarget is not available in this handler context",
17
+ });
16
18
  }
17
- return ctx.forTenant(targetTenantId);
19
+ return ctx.tenants.writeTarget(targetTenantId);
18
20
  }
@@ -1,11 +1,11 @@
1
1
  import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
2
2
 
3
3
  // Per-unit sub-transaction (one user, one purged row), nesting-aware across both db shapes:
4
- // - top-level connection (postgres-js Sql) → `.begin` (BEGIN)
5
- // - TransactionSql (inside the dispatcher's outer tx, where every
4
+ // - top-level connection (Bun.SQL pool) → `.begin` (BEGIN)
5
+ // - TransactionSQL (inside the dispatcher's outer tx, where every
6
6
  // writeHandler already runs) → `.savepoint` (SAVEPOINT)
7
- // A TransactionSql has no `.begin`, so the previous unconditional `.begin`
8
- // threw "is not a function" on every user when invoked through the dispatcher
7
+ // A transaction handle cannot begin another transaction, so the previous unconditional `.begin`
8
+ // threw on every user when invoked through the dispatcher
9
9
  // (the cron path) → zero deletions in production, while direct-connection tests
10
10
  // stayed green. Selecting the available method makes the sub-tx work in both
11
11
  // contexts; on throw the savepoint rolls back just this user (others survive).
@@ -21,7 +21,7 @@ export async function runInSubTransaction<T>(
21
21
  savepoint?: (f: (tx: DbRunner) => Promise<T>) => Promise<T>;
22
22
  };
23
23
  // savepoint-first: empirically (Bun 1.3.14, not re-checked on 1.4.0) the
24
- // surfaces are NOT mutually exclusive — a TransactionSql exposes begin AND
24
+ // surfaces are NOT mutually exclusive — a TransactionSQL exposes begin AND
25
25
  // savepoint, only the top-level connection has begin alone. begin-first
26
26
  // picked the nested BEGIN inside a tx (the incident class above);
27
27
  // savepoint-first hits the savepoint there and falls back to begin top-level.
@@ -0,0 +1,201 @@
1
+ // One-time migration tooling for event streams that live on the wrong tenant.
2
+ //
3
+ // An aggregate type that became tenant-independent (systemStream) or whose
4
+ // stream must follow the tenant in its payload has older streams on whichever
5
+ // tenant happened to write them. Newer writes then version-conflict against
6
+ // the empty target stream, and a raw tenant_id UPDATE trips
7
+ // events_aggregate_version_uq as soon as an aggregate has BOTH a legacy
8
+ // stream and newer events on the target (split stream).
9
+ //
10
+ // This tool merges per aggregate: all events of the aggregate, ordered by
11
+ // global event id, are renumbered 1..n and retenanted to the target - two-phase
12
+ // version writes (negative interim values) dodge the unique index while the
13
+ // per-aggregate transaction is in flight. Snapshots of migrated aggregates
14
+ // are dropped (version numbering changed; the next snapshotting load
15
+ // recreates them); archived-stream markers move with the stream.
16
+ //
17
+ // Idempotent: candidates are only aggregates that still have events outside
18
+ // their target tenant. One failing aggregate does not abort the run: the rest
19
+ // of the estate migrates, failures are reported.
20
+ //
21
+ // After a run, rebuild the affected projections so read-table tenant_id
22
+ // reflects the stream move.
23
+
24
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
25
+ import {
26
+ asRawClient,
27
+ toPostgresArrayLiteral,
28
+ transaction,
29
+ unsafeReadRetrying,
30
+ } from "@cosmicdrift/kumiko-framework/db";
31
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
32
+
33
+ export type StreamTenantTarget =
34
+ | { readonly kind: "system" }
35
+ // The tenantId carried by the aggregate's earliest event payload.
36
+ | { readonly kind: "payload-tenant" };
37
+
38
+ export type StreamBackfillCandidate = {
39
+ readonly aggregateId: string;
40
+ readonly targetTenantId: string;
41
+ readonly sourceTenantIds: readonly string[];
42
+ };
43
+
44
+ export type StreamBackfillFailure = { readonly aggregateId: string; readonly error: string };
45
+
46
+ export type StreamBackfillResult = {
47
+ readonly aggregatesMigrated: number;
48
+ readonly eventsMigrated: number;
49
+ readonly failed: readonly StreamBackfillFailure[];
50
+ };
51
+
52
+ export type StreamBackfillOptions = {
53
+ readonly aggregateType: string;
54
+ readonly target: StreamTenantTarget;
55
+ // Runs per candidate before its events move; a throw skips the aggregate
56
+ // and lands in `failed` (search cleanup must not be lost by a move).
57
+ readonly beforeMove?: (candidate: StreamBackfillCandidate) => Promise<void>;
58
+ // Restricts the candidate scan to events on these tenants, which lets Postgres use
59
+ // events_tenant_type_idx instead of grouping every event of the type. Streams on a tenant
60
+ // outside the list are not found; omit it for the full scan.
61
+ readonly scanTenantIds?: readonly string[];
62
+ // Called once when the scan found candidates, before the first move. "skip" ends the run
63
+ // with nothing moved (this process cannot do the work); a throw propagates to the caller.
64
+ readonly onFirstCandidate?: () => Promise<"proceed" | "skip">;
65
+ };
66
+
67
+ type CandidateRow = {
68
+ aggregate_id: string;
69
+ target_tenant_id: string | null;
70
+ source_tenant_ids: string[];
71
+ };
72
+
73
+ export async function backfillAggregateStreamTenants(
74
+ db: DbConnection,
75
+ options: StreamBackfillOptions,
76
+ ): Promise<StreamBackfillResult> {
77
+ const rows = await readCandidateRows(db, options);
78
+ if (rows.length > 0 && options.onFirstCandidate) {
79
+ if ((await options.onFirstCandidate()) === "skip") {
80
+ return { aggregatesMigrated: 0, eventsMigrated: 0, failed: [] };
81
+ }
82
+ }
83
+
84
+ let aggregatesMigrated = 0;
85
+ let eventsMigrated = 0;
86
+ const failed: StreamBackfillFailure[] = [];
87
+
88
+ for (const row of rows) {
89
+ try {
90
+ if (!row.target_tenant_id) {
91
+ throw new Error(`earliest event of the aggregate carries no payload.tenantId`);
92
+ }
93
+ const candidate: StreamBackfillCandidate = {
94
+ aggregateId: row.aggregate_id,
95
+ targetTenantId: row.target_tenant_id,
96
+ sourceTenantIds: row.source_tenant_ids,
97
+ };
98
+ await options.beforeMove?.(candidate);
99
+ eventsMigrated += await migrateAggregate(db, options.aggregateType, candidate);
100
+ aggregatesMigrated++;
101
+ } catch (e) {
102
+ failed.push({
103
+ aggregateId: row.aggregate_id,
104
+ error: e instanceof Error ? e.message : String(e),
105
+ });
106
+ }
107
+ }
108
+ return { aggregatesMigrated, eventsMigrated, failed };
109
+ }
110
+
111
+ async function readCandidateRows(
112
+ db: DbConnection,
113
+ options: StreamBackfillOptions,
114
+ ): Promise<readonly CandidateRow[]> {
115
+ const targetSql =
116
+ options.target.kind === "system"
117
+ ? `$2::text`
118
+ : `(SELECT e2."payload"->>'tenantId' FROM "kumiko_events" e2
119
+ WHERE e2."aggregate_type" = $1 AND e2."aggregate_id" = e."aggregate_id"
120
+ ORDER BY e2."id" ASC LIMIT 1)`;
121
+ const params: unknown[] = [options.aggregateType];
122
+ if (options.target.kind === "system") params.push(SYSTEM_TENANT_ID);
123
+ let scanSql = "";
124
+ if (options.scanTenantIds) {
125
+ // Events already on the SYSTEM target are not legacy; leaving them out keeps the scan to
126
+ // the legacy events only. A payload target differs per aggregate, so nothing is left out.
127
+ const scanned =
128
+ options.target.kind === "system"
129
+ ? options.scanTenantIds.filter((id) => id !== SYSTEM_TENANT_ID)
130
+ : options.scanTenantIds;
131
+ params.push(toPostgresArrayLiteral(scanned));
132
+ scanSql = ` AND e."tenant_id" = ANY($${params.length}::uuid[])`;
133
+ }
134
+ // An aggregate whose earliest event has no payload tenant yields a NULL
135
+ // target and is reported as failed instead of being silently skipped.
136
+ return unsafeReadRetrying<CandidateRow>(
137
+ db,
138
+ `SELECT e."aggregate_id" AS aggregate_id,
139
+ ${targetSql} AS target_tenant_id,
140
+ array_agg(DISTINCT e."tenant_id"::text) AS source_tenant_ids
141
+ FROM "kumiko_events" e
142
+ WHERE e."aggregate_type" = $1${scanSql}
143
+ GROUP BY e."aggregate_id"
144
+ HAVING bool_or(e."tenant_id"::text IS DISTINCT FROM ${targetSql})`,
145
+ params,
146
+ );
147
+ }
148
+
149
+ async function migrateAggregate(
150
+ db: DbConnection,
151
+ aggregateType: string,
152
+ candidate: StreamBackfillCandidate,
153
+ ): Promise<number> {
154
+ const { aggregateId, targetTenantId } = candidate;
155
+ return transaction(db, async (tx) => {
156
+ const raw = asRawClient(tx);
157
+ // Global id order = commit-adjacent replay order; merges a split stream
158
+ // into one consistent version sequence.
159
+ const events = await raw.unsafe<{ id: bigint | string }>(
160
+ `SELECT "id" FROM "kumiko_events"
161
+ WHERE "aggregate_type" = $1 AND "aggregate_id" = $2::uuid
162
+ ORDER BY "id" ASC
163
+ FOR UPDATE`,
164
+ [aggregateType, aggregateId],
165
+ );
166
+
167
+ // Negative interim versions: real versions are always >= 1, so nothing
168
+ // can collide with events_aggregate_version_uq mid-migration.
169
+ for (const [i, ev] of events.entries()) {
170
+ await raw.unsafe(
171
+ `UPDATE "kumiko_events" SET "tenant_id" = $1::uuid, "version" = $2 WHERE "id" = $3`,
172
+ [targetTenantId, -(i + 1), ev.id],
173
+ );
174
+ }
175
+ for (const [i, ev] of events.entries()) {
176
+ await raw.unsafe(`UPDATE "kumiko_events" SET "version" = $1 WHERE "id" = $2`, [i + 1, ev.id]);
177
+ }
178
+
179
+ // Version numbering changed, so any snapshot of this aggregate is stale.
180
+ await raw.unsafe(`DELETE FROM "kumiko_snapshots" WHERE "aggregate_id" = $1::uuid`, [
181
+ aggregateId,
182
+ ]);
183
+
184
+ // Archived markers key on (tenant_id, aggregate_id): move them with the
185
+ // stream, keeping an existing target marker if both exist.
186
+ await raw.unsafe(
187
+ `INSERT INTO "kumiko_archived_streams" ("tenant_id", "aggregate_id", "aggregate_type", "archived_at", "archived_by", "reason")
188
+ SELECT $1::uuid, "aggregate_id", "aggregate_type", "archived_at", "archived_by", "reason"
189
+ FROM "kumiko_archived_streams"
190
+ WHERE "aggregate_id" = $2::uuid AND "tenant_id" <> $1::uuid
191
+ ON CONFLICT DO NOTHING`,
192
+ [targetTenantId, aggregateId],
193
+ );
194
+ await raw.unsafe(
195
+ `DELETE FROM "kumiko_archived_streams" WHERE "aggregate_id" = $1::uuid AND "tenant_id" <> $2::uuid`,
196
+ [aggregateId, targetTenantId],
197
+ );
198
+
199
+ return events.length;
200
+ });
201
+ }
@@ -0,0 +1,107 @@
1
+ // Derived state of a stream-tenant backfill: search docs in the buckets the streams
2
+ // left, projection rows and the index on SYSTEM_TENANT_ID. The stream move itself lives
3
+ // in stream-tenant-backfill.ts; this part is repeatable and safe to run without moves.
4
+
5
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
6
+ import { type Registry, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { rebuildProjection } from "@cosmicdrift/kumiko-framework/pipeline";
8
+ import { reindexEntity, type SearchAdapter } from "@cosmicdrift/kumiko-framework/search";
9
+ import type { StreamBackfillCandidate, StreamBackfillFailure } from "./stream-tenant-backfill.ts";
10
+
11
+ export type StreamBackfillStepFailure = StreamBackfillFailure & { readonly step: string };
12
+
13
+ // "if-moved" skips rebuild and reindex when no stream moved; "always" repairs
14
+ // derived state after a run that moved streams and then crashed.
15
+ export type DerivedStateMode = "if-moved" | "always";
16
+
17
+ export function createSearchBucketCleanup(
18
+ searchAdapter: SearchAdapter | undefined,
19
+ entityType: string,
20
+ ): {
21
+ readonly beforeMove: (candidate: StreamBackfillCandidate) => Promise<void>;
22
+ readonly cleanedAggregates: () => number;
23
+ } {
24
+ let cleaned = 0;
25
+ return {
26
+ cleanedAggregates: () => cleaned,
27
+ beforeMove: async (candidate) => {
28
+ // skip: no adapter means no search docs to clean
29
+ if (!searchAdapter) return;
30
+ const oldBuckets = candidate.sourceTenantIds.filter((id) => id !== candidate.targetTenantId);
31
+ for (const bucket of oldBuckets) {
32
+ if (searchAdapter.removeBatch) {
33
+ await searchAdapter.removeBatch(bucket, [
34
+ { entityType, entityId: candidate.aggregateId },
35
+ ]);
36
+ } else {
37
+ await searchAdapter.remove(bucket, entityType, candidate.aggregateId);
38
+ }
39
+ }
40
+ if (oldBuckets.length > 0) cleaned++;
41
+ },
42
+ };
43
+ }
44
+
45
+ export type DerivedStateResult = {
46
+ readonly projectionsRebuilt: readonly string[];
47
+ readonly reindexedRows: number;
48
+ readonly failures: readonly StreamBackfillStepFailure[];
49
+ };
50
+
51
+ export async function rebuildAndReindex(deps: {
52
+ readonly db: DbConnection;
53
+ readonly registry: Registry;
54
+ readonly searchAdapter?: SearchAdapter;
55
+ readonly projections: readonly string[];
56
+ readonly searchEntity: string;
57
+ }): Promise<DerivedStateResult> {
58
+ const { db, registry, searchAdapter, projections, searchEntity } = deps;
59
+ const failures: StreamBackfillStepFailure[] = [];
60
+ const projectionsRebuilt: string[] = [];
61
+ for (const projection of projections) {
62
+ try {
63
+ await rebuildProjection(projection, { db, registry });
64
+ projectionsRebuilt.push(projection);
65
+ } catch (e) {
66
+ failures.push({ aggregateId: projection, step: "rebuild", error: messageOf(e) });
67
+ }
68
+ }
69
+
70
+ let reindexedRows = 0;
71
+ if (searchAdapter && isEntitySearchable(registry, searchEntity)) {
72
+ try {
73
+ const reindex = await reindexEntity(
74
+ db,
75
+ registry,
76
+ searchAdapter,
77
+ searchEntity,
78
+ SYSTEM_TENANT_ID,
79
+ );
80
+ reindexedRows = reindex.indexedRows;
81
+ for (const f of reindex.failures) {
82
+ failures.push({ aggregateId: f.entityId, step: "reindex", error: f.reason });
83
+ }
84
+ } catch (e) {
85
+ failures.push({ aggregateId: searchEntity, step: "reindex", error: messageOf(e) });
86
+ }
87
+ }
88
+ return { projectionsRebuilt, reindexedRows, failures };
89
+ }
90
+
91
+ function isEntitySearchable(registry: Registry, entityName: string): boolean {
92
+ return (
93
+ registry.getSearchableFields(entityName).length > 0 ||
94
+ registry.getSearchPayloadExtensions(entityName).length > 0
95
+ );
96
+ }
97
+
98
+ export function stepFailures(
99
+ step: string,
100
+ failed: readonly StreamBackfillFailure[],
101
+ ): StreamBackfillStepFailure[] {
102
+ return failed.map((f) => ({ ...f, step }));
103
+ }
104
+
105
+ function messageOf(e: unknown): string {
106
+ return e instanceof Error ? e.message : String(e);
107
+ }
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.389.0",
4
+ "type": "fix",
5
+ "title": "Mail steps do not retry permanent rejections or throws",
6
+ "detail": "MailDispatchResult failures take an optional smtpCode; 5xx is permanent and recorded after the first attempt. A throwing mail runner is not retried, since the message may already be sent. A mail runner that wants a retry after a transport error should return a failure without smtpCode instead of throwing."
7
+ },
2
8
  {
3
9
  "version": "0.388.0",
4
10
  "type": "fix",
@@ -54,9 +54,17 @@ type DeliveryResult =
54
54
  | { readonly ok: true; readonly status: number }
55
55
  | { readonly ok: false; readonly error: string; readonly retryable: boolean };
56
56
 
57
- // Mail adapter failures carry no status to classify, so every one is retried.
57
+ const SMTP_PERMANENT_REJECTION_MIN = 500;
58
+ const SMTP_PERMANENT_REJECTION_MAX = 599;
59
+
60
+ // Without an SMTP code the adapter gave nothing to classify, so the failure counts as transient.
58
61
  function mailResultAsDelivery(result: MailDispatchResult): DeliveryResult {
59
- return result.ok ? result : { ...result, retryable: true };
62
+ if (result.ok) return result;
63
+ const isPermanent =
64
+ result.smtpCode !== undefined &&
65
+ result.smtpCode >= SMTP_PERMANENT_REJECTION_MIN &&
66
+ result.smtpCode <= SMTP_PERMANENT_REJECTION_MAX;
67
+ return { ok: false, error: result.error, retryable: !isPermanent };
60
68
  }
61
69
 
62
70
  export type DeliverDispatchRequestArgs = {
@@ -205,7 +213,13 @@ async function sendDispatch(
205
213
  aggregateId: target.aggregateId,
206
214
  errorName: err instanceof Error ? err.name : "unknown",
207
215
  });
208
- return { ok: false, error: DISPATCH_THREW_ERROR, retryable: true };
216
+ // A mail adapter has no idempotency key: a throw after the provider accepted the message
217
+ // would send it again, so only the webhook (which carries one) is retried.
218
+ return {
219
+ ok: false,
220
+ error: DISPATCH_THREW_ERROR,
221
+ retryable: dispatchSpec.stepKind === "webhook.send",
222
+ };
209
223
  }
210
224
  }
211
225
 
@@ -94,7 +94,7 @@ export function createStepDispatcherFeature(): FeatureDefinition {
94
94
  tenantId,
95
95
  userId,
96
96
  callerFeature: STEP_DISPATCHER_FEATURE,
97
- ...(ctx.secrets && { secrets: ctx.secrets }),
97
+ ...(ctx.services.secrets && { secrets: ctx.services.secrets }),
98
98
  }),
99
99
  });
100
100
  },
@@ -15,7 +15,8 @@ export type MailSpec = z.infer<typeof mailSpecSchema>;
15
15
 
16
16
  export type MailDispatchResult =
17
17
  | { readonly ok: true; readonly status: number }
18
- | { readonly ok: false; readonly error: string };
18
+ // smtpCode is the adapter's SMTP reply code; a 5xx is a permanent rejection and is not retried.
19
+ | { readonly ok: false; readonly error: string; readonly smtpCode?: number };
19
20
 
20
21
  let mailRunner: (spec: MailSpec) => Promise<MailDispatchResult> = async () => ({
21
22
  ok: false,
@@ -225,7 +225,7 @@ async function fillPriceCache(
225
225
  } else {
226
226
  const message =
227
227
  outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
228
- ctx.log?.warn("subscription-dodo: product lookup failed", { message });
228
+ ctx.obs.log?.warn("subscription-dodo: product lookup failed", { message });
229
229
  }
230
230
  }
231
231
  }
@@ -64,11 +64,11 @@ export function createDodoRuntimes(deps: DodoRuntimeDeps): DodoRuntimes {
64
64
 
65
65
  async function ctxApiKey(ctx: HandlerContext): Promise<string> {
66
66
  let key: string | undefined;
67
- if (ctx.secrets) {
68
- const got = await requireSecretsContext(ctx, SUBSCRIPTION_DODO_FEATURE).get(
69
- SYSTEM_TENANT_ID,
70
- deps.apiKeyHandle.name,
71
- );
67
+ if (ctx.services.secrets) {
68
+ const got = await requireSecretsContext(
69
+ { secrets: ctx.services.secrets, user: ctx.user },
70
+ SUBSCRIPTION_DODO_FEATURE,
71
+ ).get(SYSTEM_TENANT_ID, deps.apiKeyHandle.name);
72
72
  key = parseStoredSecret(got?.reveal());
73
73
  }
74
74
  if (!key && deps.fallback.apiKey && deps.fallback.apiKey.length > 0) {
@@ -102,8 +102,8 @@ export function createDodoRuntimes(deps: DodoRuntimeDeps): DodoRuntimes {
102
102
  if (live !== true) return false;
103
103
  if (deps.fallback.apiKey !== undefined && deps.fallback.apiKey.length > 0) return true;
104
104
  // Existence-only probe on every billing-plans render: must stay un-audited.
105
- return ctx.secrets
106
- ? await ctx.secrets.has(SYSTEM_TENANT_ID, deps.apiKeyHandle.name)
105
+ return ctx.services.secrets
106
+ ? await ctx.services.secrets.has(SYSTEM_TENANT_ID, deps.apiKeyHandle.name)
107
107
  : false;
108
108
  },
109
109
  },
@@ -169,7 +169,9 @@ export function createStripeCheckoutSession(
169
169
  if (!options.providerCustomerId || !isUnknownCustomerStripeError(error)) throw error;
170
170
  // Stripe account switch (test -> live) or a customer deleted in the dashboard leaves a
171
171
  // stored customer id Stripe no longer knows; checkout creates a fresh customer instead.
172
- ctx.log?.warn("subscription-stripe: stored customer unknown to Stripe, retrying without it");
172
+ ctx.obs.log?.warn(
173
+ "subscription-stripe: stored customer unknown to Stripe, retrying without it",
174
+ );
173
175
  session = await stripe.checkout.sessions.create(sessionParams(undefined));
174
176
  }
175
177
 
@@ -383,7 +385,7 @@ async function resolvePricesViaCache(
383
385
  } else {
384
386
  const message =
385
387
  outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
386
- ctx.log?.warn("subscription-stripe: prices.retrieve failed", { message });
388
+ ctx.obs.log?.warn("subscription-stripe: prices.retrieve failed", { message });
387
389
  }
388
390
  }
389
391
  }
@@ -402,7 +404,7 @@ export function createStripeRetrievePrices(runtime: StripeCtxRuntime, cache: Str
402
404
  const rawInterval = price.recurring?.interval;
403
405
  if (rawInterval && mapped.interval === null && !warnedUnknownIntervalPriceIds.has(priceId)) {
404
406
  warnedUnknownIntervalPriceIds.add(priceId);
405
- ctx.log?.warn("subscription-stripe: price has an unknown recurring interval", {
407
+ ctx.obs.log?.warn("subscription-stripe: price has an unknown recurring interval", {
406
408
  priceId,
407
409
  interval: rawInterval,
408
410
  });