@cosmicdrift/kumiko-samples 0.362.0 → 0.364.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 (321) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +23 -6
  3. package/packages/bundled-features/src/admin-shell/changes.json +6 -0
  4. package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
  5. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
  6. package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
  7. package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
  8. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
  9. package/packages/bundled-features/src/audit/changes.json +6 -0
  10. package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
  11. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
  12. package/packages/bundled-features/src/auth-email-password/changes.json +51 -0
  13. package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
  14. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  15. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  16. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
  17. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  18. package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
  19. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  20. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  21. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  22. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  23. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
  24. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +5 -13
  25. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  26. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  27. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  28. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  29. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
  30. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  31. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  32. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  33. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  34. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  35. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  36. package/packages/bundled-features/src/billing-foundation/changes.json +43 -0
  37. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  38. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +90 -44
  39. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
  40. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  41. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +118 -0
  42. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  43. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
  44. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +39 -33
  45. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  46. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  47. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
  48. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
  49. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +36 -20
  50. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
  51. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
  52. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  53. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
  54. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  55. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  56. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +12 -6
  57. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  58. package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
  59. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +20 -11
  60. package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
  61. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
  62. package/packages/bundled-features/src/cap-counter/changes.json +6 -0
  63. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
  64. package/packages/bundled-features/src/cap-counter/index.ts +1 -0
  65. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
  66. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  67. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  68. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  69. package/packages/bundled-features/src/channel-email/email-channel.ts +135 -29
  70. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  71. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  72. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  73. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  74. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  75. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  76. package/packages/bundled-features/src/config/changes.json +6 -0
  77. package/packages/bundled-features/src/config/handlers/set.write.ts +3 -7
  78. package/packages/bundled-features/src/config/resolver.ts +15 -12
  79. package/packages/bundled-features/src/config/write-helpers.ts +23 -0
  80. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  81. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
  82. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  83. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  84. package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
  85. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  86. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  87. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  88. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  89. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  90. package/packages/bundled-features/src/delivery/attachment-ref.ts +105 -0
  91. package/packages/bundled-features/src/delivery/changes.json +66 -0
  92. package/packages/bundled-features/src/delivery/channel-context.ts +152 -4
  93. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  94. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  95. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  96. package/packages/bundled-features/src/delivery/index.ts +19 -2
  97. package/packages/bundled-features/src/delivery/jobs.ts +97 -17
  98. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  99. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  100. package/packages/bundled-features/src/delivery/types.ts +34 -18
  101. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  102. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  103. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  104. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  105. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  106. package/packages/bundled-features/src/file-foundation/changes.json +6 -0
  107. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  108. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +89 -0
  109. package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
  110. package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
  111. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  112. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  113. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  114. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  115. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  116. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
  117. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  118. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  119. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  120. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  121. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  122. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
  123. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  124. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  125. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  126. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  127. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +42 -25
  128. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  129. package/packages/bundled-features/src/jobs/changes.json +19 -0
  130. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
  131. package/packages/bundled-features/src/jobs/feature.ts +7 -4
  132. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  133. package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
  134. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
  135. package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
  136. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
  137. package/packages/bundled-features/src/ledger/changes.json +7 -0
  138. package/packages/bundled-features/src/ledger/entity.ts +3 -1
  139. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  140. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  141. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  142. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  143. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  144. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  145. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  146. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  147. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  148. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  149. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  150. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  151. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  152. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  153. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  154. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  155. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  156. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  157. package/packages/bundled-features/src/renderer-foundation/types.ts +134 -35
  158. package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
  159. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  160. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
  161. package/packages/bundled-features/src/secrets/changes.json +6 -0
  162. package/packages/bundled-features/src/secrets/feature.ts +4 -3
  163. package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
  164. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  165. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  166. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  167. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  168. package/packages/bundled-features/src/shared/changes.json +6 -0
  169. package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
  170. package/packages/bundled-features/src/shared/index.ts +6 -0
  171. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  172. package/packages/bundled-features/src/shared/mail-template-body.ts +42 -0
  173. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  174. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  175. package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
  176. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
  177. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
  178. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  179. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  180. package/packages/bundled-features/src/template-resolver/README.md +66 -8
  181. package/packages/bundled-features/src/template-resolver/api.ts +85 -9
  182. package/packages/bundled-features/src/template-resolver/changes.json +186 -0
  183. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +278 -0
  184. package/packages/bundled-features/src/template-resolver/constants.ts +49 -11
  185. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  186. package/packages/bundled-features/src/template-resolver/document-schema.ts +345 -0
  187. package/packages/bundled-features/src/template-resolver/feature.ts +59 -6
  188. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  189. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  190. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +160 -0
  191. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +33 -0
  192. package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
  193. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +138 -0
  194. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -6
  195. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  196. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  197. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +151 -0
  198. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +19 -18
  199. package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
  200. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +323 -0
  201. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  202. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  203. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  204. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  205. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  206. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  207. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +135 -0
  208. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  209. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  210. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +196 -0
  211. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +233 -0
  212. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +137 -0
  213. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +91 -17
  214. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  215. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +16 -77
  216. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +31 -0
  217. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  218. package/packages/bundled-features/src/template-resolver/index.ts +111 -0
  219. package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
  220. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  221. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  222. package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
  223. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  224. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  225. package/packages/bundled-features/src/template-resolver/qualified-names.ts +34 -0
  226. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  227. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  228. package/packages/bundled-features/src/template-resolver/seeding.ts +171 -20
  229. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  230. package/packages/bundled-features/src/template-resolver/table.ts +27 -1
  231. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  232. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  233. package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
  234. package/packages/bundled-features/src/template-resolver/template-meta.ts +192 -0
  235. package/packages/bundled-features/src/template-resolver/template-preview.ts +676 -0
  236. package/packages/bundled-features/src/template-resolver/template-resources.ts +114 -0
  237. package/packages/bundled-features/src/template-resolver/template-usage.ts +68 -0
  238. package/packages/bundled-features/src/template-resolver/template-validation.ts +230 -0
  239. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  240. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +20 -13
  241. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +215 -0
  242. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +37 -0
  243. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1065 -0
  244. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +72 -0
  245. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  246. package/packages/bundled-features/src/template-resolver/web/i18n.ts +121 -0
  247. package/packages/bundled-features/src/template-resolver/web/index.ts +26 -0
  248. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  249. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +176 -0
  250. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  251. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  252. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  253. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  254. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  255. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  256. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  257. package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
  258. package/packages/bundled-features/src/user/changes.json +6 -0
  259. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  260. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  261. package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
  262. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  263. package/packages/bundled-features/src/user/seeding.ts +19 -5
  264. package/packages/bundled-features/src/user-data-rights/changes.json +13 -0
  265. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  266. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  267. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  268. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  269. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  270. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  271. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  272. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  273. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  274. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  275. package/packages/bundled-features/src/user-profile/changes.json +1 -1
  276. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  277. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  278. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  279. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  280. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  281. package/packages/bundled-features/src/waitlist/options.ts +1 -1
  282. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  283. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +3 -3
  284. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
  285. package/samples/apps/ui-walkthrough/package.json +11 -0
  286. package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
  287. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  288. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  289. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +239 -0
  290. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  291. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  292. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +570 -0
  293. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +72 -0
  294. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  295. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  296. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  297. package/samples/apps/ui-walkthrough/src/run-config.ts +41 -1
  298. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  299. package/samples/apps/use-all-bundled/feature-manifest.json +118 -21
  300. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  301. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  302. package/samples/apps/use-all-bundled/package.json +2 -0
  303. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  304. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  305. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  306. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  307. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  308. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  309. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  310. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  311. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  312. package/samples/recipes/delivery-notifications/README.md +26 -25
  313. package/samples/recipes/delivery-notifications/package.json +2 -1
  314. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  315. package/samples/recipes/seed-migration/README.md +1 -1
  316. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  317. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  318. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  319. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  320. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  321. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -29,21 +29,21 @@ export async function selectStaleDraftsBatch(
29
29
  olderThanDays: number,
30
30
  batchSize: number,
31
31
  ): Promise<readonly StaleDraftRow[]> {
32
- const rows = (await unsafeReadRetrying(
33
- db,
34
- `SELECT "id", "tenant_id", "owner_id", "draft_key", "draft", "inserted_at" FROM "read_form_drafts"
35
- WHERE COALESCE("modified_at", "inserted_at") < now() - ($1::int * interval '1 day')
36
- ORDER BY COALESCE("modified_at", "inserted_at") ASC
37
- LIMIT $2`,
38
- [olderThanDays, batchSize],
39
- )) as readonly {
32
+ const rows = await unsafeReadRetrying<{
40
33
  id: string;
41
34
  tenant_id: string;
42
35
  owner_id: string;
43
36
  draft_key: string;
44
37
  draft: FormDraftBlob;
45
38
  inserted_at: Date;
46
- }[];
39
+ }>(
40
+ db,
41
+ `SELECT "id", "tenant_id", "owner_id", "draft_key", "draft", "inserted_at" FROM "read_form_drafts"
42
+ WHERE COALESCE("modified_at", "inserted_at") < now() - ($1::int * interval '1 day')
43
+ ORDER BY COALESCE("modified_at", "inserted_at") ASC
44
+ LIMIT $2`,
45
+ [olderThanDays, batchSize],
46
+ );
47
47
  return rows.map((row) => ({
48
48
  id: row.id,
49
49
  tenantId: row.tenant_id as TenantId, // @cast-boundary db-row
@@ -61,7 +61,7 @@ export async function isDraftStillStale(
61
61
  id: string,
62
62
  olderThanDays: number,
63
63
  ): Promise<boolean> {
64
- const rows = (await unsafeReadRetrying(
64
+ const rows = await unsafeReadRetrying<unknown>(
65
65
  db,
66
66
  `SELECT 1 FROM "read_form_drafts"
67
67
  WHERE "id" = $1
@@ -69,6 +69,6 @@ export async function isDraftStillStale(
69
69
  AND COALESCE("modified_at", "inserted_at") < now() - ($3::int * interval '1 day')
70
70
  LIMIT 1`,
71
71
  [id, tenantId, olderThanDays],
72
- )) as readonly unknown[];
72
+ );
73
73
  return rows.length > 0;
74
74
  }
@@ -11,11 +11,11 @@ export async function countDraftsByOwner(
11
11
  tenantId: TenantId,
12
12
  ownerId: string,
13
13
  ): Promise<number> {
14
- const rows = (await unsafeReadRetrying(
14
+ const rows = await unsafeReadRetrying<{ count: number }>(
15
15
  db,
16
16
  `SELECT count(*)::int AS "count" FROM "read_form_drafts"
17
17
  WHERE "tenant_id" = $1 AND "owner_id" = $2`,
18
18
  [tenantId, ownerId],
19
- )) as readonly { count: number }[];
19
+ );
20
20
  return rows[0]?.count ?? 0;
21
21
  }
@@ -62,13 +62,13 @@ export async function filterOwnedFileRefs(
62
62
  isCreateMode: boolean,
63
63
  ): Promise<readonly OwnedFileRef[]> {
64
64
  if (candidateKeys.length === 0) return [];
65
- const rows = (await unsafeReadRetrying(
65
+ const rows = await unsafeReadRetrying<{ id: string; storage_key: string }>(
66
66
  db,
67
67
  `SELECT "id", "storage_key" FROM "file_refs"
68
68
  WHERE "tenant_id" = $1 AND "inserted_by_id" = $2
69
69
  AND "storage_key" = ANY($3::text[]) AND "is_deleted" = false
70
70
  AND ($5::boolean OR "inserted_at" > $4::timestamptz)`,
71
71
  [tenantId, ownerId, candidateKeys, draftInsertedAt.toString(), isCreateMode],
72
- )) as readonly { id: string; storage_key: string }[];
72
+ );
73
73
  return rows.map((row) => ({ id: row.id, storageKey: row.storage_key }));
74
74
  }
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
1
2
  import type { FormDraftBlob } from "./schemas.js";
2
3
 
3
4
  // The draft blob's `values` is free-form by design (issue #1889: form-draft
@@ -8,8 +9,8 @@ import type { FormDraftBlob } from "./schemas.js";
8
9
  // step writes into the draft, whether under a single "file" field or nested
9
10
  // in a "files"/"images" array.
10
11
  function isFileRefPointer(value: unknown): value is { readonly storageKey: string } {
11
- if (typeof value !== "object" || value === null) return false;
12
- const storageKey = (value as { storageKey?: unknown }).storageKey;
12
+ if (!isPlainObject(value)) return false;
13
+ const storageKey = value["storageKey"];
13
14
  return typeof storageKey === "string" && storageKey.length > 0;
14
15
  }
15
16
 
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "Watch supervisor skips and logs accounts whose address does not decrypt",
6
+ "detail": "Previously a single such account made the whole account snapshot throw."
7
+ },
2
8
  {
3
9
  "version": "0.340.0",
4
10
  "type": "improvement",
@@ -95,7 +95,7 @@ export const connectAccountHandler: WriteHandlerDef = {
95
95
  authMethod: payload.authMethod,
96
96
  ownerUserId,
97
97
  displayName: payload.displayName,
98
- address: encryptedFields["address"] as string,
98
+ address: encryptedFields.address,
99
99
  // active ab Connect — der Supervisor greift den Account beim
100
100
  // nächsten Tick auf und degradiert ihn bei Credential-Fehlern.
101
101
  status: InboundMailAccountStatuses.active,
@@ -199,11 +199,11 @@ export const ingestMessageHandler: WriteHandlerDef = {
199
199
  ownerUserId: payload.ownerUserId,
200
200
  messageIdHeader: effectiveMessageIdHeader,
201
201
  threadKey,
202
- from: encryptedMessageFields["from"] as string,
203
- to: encryptedMessageFields["to"] as string,
204
- cc: encryptedMessageFields["cc"] as string,
205
- subject: encryptedMessageFields["subject"] as string,
206
- snippet: encryptedMessageFields["snippet"] as string,
202
+ from: encryptedMessageFields.from,
203
+ to: encryptedMessageFields.to,
204
+ cc: encryptedMessageFields.cc,
205
+ subject: encryptedMessageFields.subject,
206
+ snippet: encryptedMessageFields.snippet,
207
207
  receivedAtIso: payload.receivedAtIso,
208
208
  bodyRef: payload.bodyRef,
209
209
  scope: payload.scope,
@@ -296,7 +296,7 @@ export const ingestMessageHandler: WriteHandlerDef = {
296
296
 
297
297
  const threadEventPayload: MailThreadEventPayload = {
298
298
  threadKey,
299
- subject: encryptedThreadFields["subject"] as string,
299
+ subject: encryptedThreadFields.subject,
300
300
  lastMessageAtIso: newLastAt,
301
301
  messageCount,
302
302
  };
@@ -34,7 +34,7 @@ export const listAccountsQuery: QueryHandlerDef = {
34
34
  if (!piiKms) return { rows };
35
35
  const decryptedRows = await Promise.all(
36
36
  rows.map((row) =>
37
- decryptPiiFieldValues(row as Record<string, unknown>, MAIL_ACCOUNT_PII_FIELDS, piiKms, {
37
+ decryptPiiFieldValues(row, MAIL_ACCOUNT_PII_FIELDS, piiKms, {
38
38
  requestId: `inbound-mail-foundation:list-accounts:${ctx.user.tenantId}`,
39
39
  }),
40
40
  ),
@@ -49,7 +49,7 @@ export const listMessagesQuery: QueryHandlerDef = {
49
49
  if (!piiKms) return { rows };
50
50
  const decryptedRows = await Promise.all(
51
51
  rows.map((row) =>
52
- decryptPiiFieldValues(row as Record<string, unknown>, INBOUND_MESSAGE_PII_FIELDS, piiKms, {
52
+ decryptPiiFieldValues(row, INBOUND_MESSAGE_PII_FIELDS, piiKms, {
53
53
  requestId: `inbound-mail-foundation:list-messages:${ctx.user.tenantId}`,
54
54
  }),
55
55
  ),
@@ -4,10 +4,10 @@
4
4
  // für read_mail_accounts und read_inbound_messages (Scope-Vererbung).
5
5
 
6
6
  export function isVisibleToCaller(
7
- row: unknown,
7
+ row: Readonly<Record<string, unknown>>,
8
8
  user: { readonly id: string; readonly roles: readonly string[] },
9
9
  ): boolean {
10
- const owner = (row as Record<string, unknown>)["ownerUserId"];
10
+ const owner = row["ownerUserId"];
11
11
  if (owner == null || owner === "") return true;
12
12
  if (owner === user.id) return true;
13
13
  return user.roles.includes("TenantAdmin") || user.roles.includes("SystemAdmin");
@@ -140,14 +140,17 @@ export function createOAuthAccessTokenManager(
140
140
  const secretKey = inboundCredentialSecretKey(account.id);
141
141
  return transaction(deps.db, async (tx) => {
142
142
  await acquireNamespacedAdvisoryLock(tx, OAUTH_REFRESH_LOCK_NAMESPACE, lockKey(account));
143
- const stored = await deps.secrets.get(account.tenantId, secretKey);
143
+ // Secret read/write join the lock transaction: a second pool connection per refresh
144
+ // would exhaust a small pool when many accounts refresh at once.
145
+ const secrets = deps.secrets.withRunner(tx);
146
+ const stored = await secrets.get(account.tenantId, secretKey);
144
147
  if (!stored) {
145
148
  throw new InboundAuthError("no refresh token stored for account");
146
149
  }
147
150
  const currentRefreshToken = stored.reveal();
148
151
  const tokens = await refreshWithTimeout(plugin, account, currentRefreshToken);
149
152
  if (tokens.refreshToken && tokens.refreshToken !== currentRefreshToken) {
150
- await deps.secrets.set(
153
+ await secrets.set(
151
154
  account.tenantId,
152
155
  secretKey,
153
156
  tokens.refreshToken,
@@ -180,12 +183,14 @@ export function createOAuthAccessTokenManager(
180
183
  async storeRefreshToken(account, refreshToken) {
181
184
  await transaction(deps.db, async (tx) => {
182
185
  await acquireNamespacedAdvisoryLock(tx, OAUTH_REFRESH_LOCK_NAMESPACE, lockKey(account));
183
- await deps.secrets.set(
184
- account.tenantId,
185
- inboundCredentialSecretKey(account.id),
186
- refreshToken,
187
- inboundRefreshTokenSecretOptions(account.id),
188
- );
186
+ await deps.secrets
187
+ .withRunner(tx)
188
+ .set(
189
+ account.tenantId,
190
+ inboundCredentialSecretKey(account.id),
191
+ refreshToken,
192
+ inboundRefreshTokenSecretOptions(account.id),
193
+ );
189
194
  });
190
195
  cache.delete(account.id);
191
196
  },
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
14
14
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
15
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
15
16
 
16
17
  const STATE_PURPOSE = "inbound-mail-oauth-connect";
17
18
 
@@ -91,13 +92,12 @@ export function verifyOAuthState(
91
92
  }
92
93
 
93
94
  function isOAuthStatePayload(v: unknown): v is OAuthStatePayload {
94
- if (typeof v !== "object" || v === null) return false;
95
- const o = v as Record<string, unknown>;
95
+ if (!isPlainObject(v)) return false;
96
96
  return (
97
- typeof o["tenantId"] === "string" &&
98
- (o["ownerUserId"] === null || typeof o["ownerUserId"] === "string") &&
99
- typeof o["providerKey"] === "string" &&
100
- typeof o["mailbox"] === "string" &&
101
- typeof o["nonce"] === "string"
97
+ typeof v["tenantId"] === "string" &&
98
+ (v["ownerUserId"] === null || typeof v["ownerUserId"] === "string") &&
99
+ typeof v["providerKey"] === "string" &&
100
+ typeof v["mailbox"] === "string" &&
101
+ typeof v["nonce"] === "string"
102
102
  );
103
103
  }
@@ -157,12 +157,8 @@ async function purgeExpiredSeenAnchors(
157
157
  // direkt tenant-scoped, kein Account-Join. Trifft auch Anker, deren Account
158
158
  // schon disconnected/geloescht ist.
159
159
  const where = { tenantId, seenAt: { lt: cutoff } };
160
- const expired = await selectMany<{ accountId: string }>(
161
- db,
162
- seenMessageTable as EntityTableMeta,
163
- where,
164
- );
160
+ const expired = await selectMany<{ accountId: string }>(db, seenMessageTable, where);
165
161
  if (expired.length === 0) return 0;
166
- await deleteMany(db, seenMessageTable as EntityTableMeta, where);
162
+ await deleteMany(db, seenMessageTable, where);
167
163
  return expired.length;
168
164
  }
@@ -62,27 +62,19 @@ async function archiveAndDeleteRows(
62
62
  export async function mailAccountTenantDestroyHook(ctx: TenantDataHookCtx): Promise<void> {
63
63
  const accountIds = await archiveAndDeleteRows(
64
64
  ctx,
65
- mailAccountsProjectionTable as EntityTableMeta,
65
+ mailAccountsProjectionTable,
66
66
  MAIL_ACCOUNT_AGGREGATE_TYPE,
67
67
  );
68
68
  for (const accountId of accountIds) {
69
- await ctx.db.deleteMany(syncCursorTable as EntityTableMeta, { accountId });
70
- await ctx.db.deleteMany(seenMessageTable as EntityTableMeta, { accountId });
69
+ await ctx.db.deleteMany(syncCursorTable, { accountId });
70
+ await ctx.db.deleteMany(seenMessageTable, { accountId });
71
71
  }
72
72
  }
73
73
 
74
74
  export async function inboundMessageTenantDestroyHook(ctx: TenantDataHookCtx): Promise<void> {
75
- await archiveAndDeleteRows(
76
- ctx,
77
- inboundMessagesProjectionTable as EntityTableMeta,
78
- INBOUND_MESSAGE_AGGREGATE_TYPE,
79
- );
75
+ await archiveAndDeleteRows(ctx, inboundMessagesProjectionTable, INBOUND_MESSAGE_AGGREGATE_TYPE);
80
76
  }
81
77
 
82
78
  export async function mailThreadTenantDestroyHook(ctx: TenantDataHookCtx): Promise<void> {
83
- await archiveAndDeleteRows(
84
- ctx,
85
- mailThreadsProjectionTable as EntityTableMeta,
86
- MAIL_THREAD_AGGREGATE_TYPE,
87
- );
79
+ await archiveAndDeleteRows(ctx, mailThreadsProjectionTable, MAIL_THREAD_AGGREGATE_TYPE);
88
80
  }
@@ -257,17 +257,15 @@ export class InboundTransientError extends Error {
257
257
  }
258
258
  }
259
259
 
260
- type KindCarrier = { readonly kind?: unknown };
261
-
262
260
  export function isInboundAuthError(e: unknown): e is InboundAuthError {
263
- return e instanceof Error && (e as KindCarrier).kind === "inbound-auth-error";
261
+ return e instanceof Error && "kind" in e && e.kind === "inbound-auth-error";
264
262
  }
265
263
  export function isInboundRateLimitError(e: unknown): e is InboundRateLimitError {
266
- return e instanceof Error && (e as KindCarrier).kind === "inbound-rate-limit-error";
264
+ return e instanceof Error && "kind" in e && e.kind === "inbound-rate-limit-error";
267
265
  }
268
266
  export function isInboundCursorInvalidError(e: unknown): e is InboundCursorInvalidError {
269
- return e instanceof Error && (e as KindCarrier).kind === "inbound-cursor-invalid-error";
267
+ return e instanceof Error && "kind" in e && e.kind === "inbound-cursor-invalid-error";
270
268
  }
271
269
  export function isInboundTransientError(e: unknown): e is InboundTransientError {
272
- return e instanceof Error && (e as KindCarrier).kind === "inbound-transient-error";
270
+ return e instanceof Error && "kind" in e && e.kind === "inbound-transient-error";
273
271
  }
@@ -49,9 +49,10 @@ import {
49
49
  configuredPiiSubjectKms,
50
50
  decryptPiiFieldValues,
51
51
  } from "@cosmicdrift/kumiko-framework/crypto";
52
- import type { DbConnection, EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
52
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
53
53
  import type { DistributedLock } from "@cosmicdrift/kumiko-framework/pipeline";
54
54
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
55
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
55
56
  import { InboundMailAccountStatuses, InboundMailFoundationHandlers } from "./constants.js";
56
57
  import { MAIL_ACCOUNT_PII_FIELDS, syncCursorTable } from "./entities.js";
57
58
  import {
@@ -158,6 +159,17 @@ export type InboundMailSupervisor = {
158
159
  readonly stop: () => Promise<void>;
159
160
  };
160
161
 
162
+ type ActiveMailAccountRow = {
163
+ readonly id: string;
164
+ readonly tenantId: string;
165
+ readonly provider: string;
166
+ readonly authMethod: string;
167
+ readonly ownerUserId?: string | null;
168
+ readonly address: string;
169
+ readonly displayName?: string;
170
+ readonly watchState?: string;
171
+ };
172
+
161
173
  export function createInboundMailSupervisor(
162
174
  deps: InboundMailSupervisorDeps,
163
175
  ): InboundMailSupervisor {
@@ -206,30 +218,34 @@ export function createInboundMailSupervisor(
206
218
  // (Provider arbeiten nie mit Ciphertext).
207
219
  // ---------------------------------------------------------------
208
220
  async function listActiveAccounts(): Promise<readonly MailAccountRecord[]> {
209
- const rows = await selectMany(deps.db, mailAccountsProjectionTable, {
221
+ const rows = await selectMany<ActiveMailAccountRow>(deps.db, mailAccountsProjectionTable, {
210
222
  status: InboundMailAccountStatuses.active,
211
223
  });
212
224
  const piiKms = configuredPiiSubjectKms();
213
225
  const records: MailAccountRecord[] = [];
214
- for (const raw of rows) {
215
- const row = piiKms
216
- ? await decryptPiiFieldValues(
217
- raw as Record<string, unknown>,
218
- MAIL_ACCOUNT_PII_FIELDS,
219
- piiKms,
220
- { requestId: "inbound-mail-foundation:supervisor:list-accounts" },
221
- )
222
- : (raw as Record<string, unknown>);
226
+ for (const row of rows) {
227
+ const decrypted = piiKms
228
+ ? await decryptPiiFieldValues(row, MAIL_ACCOUNT_PII_FIELDS, piiKms, {
229
+ requestId: "inbound-mail-foundation:supervisor:list-accounts",
230
+ })
231
+ : row;
232
+ const decryptedAddress = decrypted["address"];
233
+ // Falling back to row.address would hand the ciphertext to a provider.
234
+ // One undecryptable account must not stop polling of every other tenant's accounts.
235
+ if (typeof decryptedAddress !== "string") {
236
+ log(`inbound-mail: account ${row.id} skipped — address did not decrypt to text`);
237
+ continue;
238
+ }
223
239
  records.push({
224
- id: row["id"] as string,
225
- tenantId: row["tenantId"] as string,
226
- provider: row["provider"] as string,
227
- authMethod: row["authMethod"] as string,
228
- ownerUserId: (row["ownerUserId"] as string | null | undefined) ?? null,
229
- address: row["address"] as string,
230
- displayName: (row["displayName"] as string | undefined) ?? "",
240
+ id: row.id,
241
+ tenantId: row.tenantId,
242
+ provider: row.provider,
243
+ authMethod: row.authMethod,
244
+ ownerUserId: row.ownerUserId ?? null,
245
+ address: decryptedAddress,
246
+ displayName: row.displayName ?? "",
231
247
  status: InboundMailAccountStatuses.active,
232
- watchState: (row["watchState"] as string | undefined) ?? "idle",
248
+ watchState: row.watchState ?? "idle",
233
249
  });
234
250
  }
235
251
  return records;
@@ -239,13 +255,14 @@ export function createInboundMailSupervisor(
239
255
  // Cursor-Persistenz (unmanaged direct-write store).
240
256
  // ---------------------------------------------------------------
241
257
  async function loadCursor(accountId: string): Promise<SyncCursorPayload | null> {
242
- const row = await fetchOne<{ cursor: string }>(deps.db, syncCursorTable as EntityTableMeta, {
258
+ const row = await fetchOne<{ cursor: string }>(deps.db, syncCursorTable, {
243
259
  accountId,
244
260
  scope: CURSOR_SCOPE,
245
261
  });
246
262
  if (!row) return null;
247
263
  try {
248
- return JSON.parse(row.cursor) as SyncCursorPayload; // @cast-boundary eigene JSON-Persistenz
264
+ const parsed: unknown = JSON.parse(row.cursor);
265
+ return isPlainObject(parsed) ? parsed : null;
249
266
  } catch {
250
267
  return null; // korrupter Cursor = wie kein Cursor → Backfill, Dedup fängt Dubletten
251
268
  }
@@ -258,21 +275,21 @@ export function createInboundMailSupervisor(
258
275
  const accountId = account.id;
259
276
  const now = Temporal.Now.instant().toString();
260
277
  const serialized = JSON.stringify(cursor);
261
- const existing = await fetchOne<{ id: string }>(deps.db, syncCursorTable as EntityTableMeta, {
278
+ const existing = await fetchOne<{ id: string }>(deps.db, syncCursorTable, {
262
279
  accountId,
263
280
  scope: CURSOR_SCOPE,
264
281
  });
265
282
  if (existing) {
266
283
  await updateMany(
267
284
  deps.db,
268
- syncCursorTable as EntityTableMeta,
285
+ syncCursorTable,
269
286
  { cursor: serialized, updatedAt: now },
270
287
  { accountId, scope: CURSOR_SCOPE },
271
288
  );
272
289
  // skip: update-Pfad fertig — nicht in den insert-Zweig durchfallen.
273
290
  return;
274
291
  }
275
- await insertOne(deps.db, syncCursorTable as EntityTableMeta, {
292
+ await insertOne(deps.db, syncCursorTable, {
276
293
  id: crypto.randomUUID(),
277
294
  // Direct-write store: no executor stamps the NOT NULL tenant_id for us.
278
295
  tenantId: account.tenantId,
@@ -288,7 +305,7 @@ export function createInboundMailSupervisor(
288
305
  // → nächster fetch läuft als Backfill.
289
306
  await updateMany(
290
307
  deps.db,
291
- syncCursorTable as EntityTableMeta,
308
+ syncCursorTable,
292
309
  { cursor: "", updatedAt: Temporal.Now.instant().toString() },
293
310
  { accountId, scope: CURSOR_SCOPE },
294
311
  );
@@ -18,6 +18,7 @@ import {
18
18
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
19
19
  import { legacyDateToInstant } from "@cosmicdrift/kumiko-framework/time";
20
20
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
21
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
21
22
  import { ImapFlow } from "imapflow";
22
23
  import { type AddressObject, type ParsedMail, simpleParser } from "mailparser";
23
24
  import type { ImapCredentialDocument } from "./credential-document.js";
@@ -97,11 +98,11 @@ export function mapImapError(err: unknown, host: string): Error {
97
98
  // imapflow-Fehler tragen die Server-Antwort in responseText, die
98
99
  // message ist nur "Command failed" — beides in den Match einbeziehen;
99
100
  // authenticationFailed ist das verlässliche Flag.
100
- const carrier = err as { authenticationFailed?: unknown; responseText?: unknown };
101
- const responseText = typeof carrier?.responseText === "string" ? carrier.responseText : "";
101
+ const carrier = isPlainObject(err) ? err : undefined;
102
+ const responseText = typeof carrier?.["responseText"] === "string" ? carrier["responseText"] : "";
102
103
  const msg = `${err instanceof Error ? err.message : String(err)} ${responseText}`.trim();
103
104
  if (
104
- carrier?.authenticationFailed === true ||
105
+ carrier?.["authenticationFailed"] === true ||
105
106
  /auth(entication)? failed|invalid credentials|invalid login|LOGIN failed|AUTHENTICATIONFAILED/i.test(
106
107
  msg,
107
108
  )
@@ -1,4 +1,23 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "jobs list query caps the page size at 200",
6
+ "detail": "limit above 200 is rejected with a validation error."
7
+ },
8
+ {
9
+ "version": "0.364.0",
10
+ "type": "fix",
11
+ "title": "Stale-run sweep uses the job-run retention window for queued runs",
12
+ "detail": "The sweep now only fails queued runs that are older than the configured retention days instead of every queued run past the stale cutoff, so freshly queued runs of a long backlog are no longer failed."
13
+ },
14
+ {
15
+ "version": "0.364.0",
16
+ "type": "fix",
17
+ "title": "Tenant-visible job runs no longer collide across tenants, jobs and lanes",
18
+ "detail": "store_tenant_job_runs was unique on bull_job_id alone, but BullMQ ids count per queue, so a job on the api lane and one on the worker lane (or two tenants after a Redis reset) shared an id and overwrote each other's row. The unique index now covers (tenant_id, job_name, bull_job_id), every write filters by all three, and an enqueue replaces a finished row left over under the same key. onJobDropped now receives { tenantId } as a third argument.",
19
+ "migration": "Run `kumiko-schema generate` in your app to add the migration that creates store_tenant_job_runs_tenant_job_bull_job_id_unique and drops store_tenant_job_runs_bull_job_id_unique (generated as a commented DROP INDEX; uncomment it). A custom onJobDropped callback passed to the job runner now gets a third argument { tenantId }."
20
+ },
2
21
  {
3
22
  "version": "0.360.0",
4
23
  "type": "fix",
@@ -15,6 +15,7 @@
15
15
  import { deleteManyBatched, selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
16
16
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
17
17
  import { mapWithConcurrency } from "../../../shared/index.js";
18
+ import { DEFAULT_JOB_RUN_RETENTION_DAYS } from "../../handlers/retention-cleanup.job.js";
18
19
  import { encryptFailureError } from "../../job-run-logger.js";
19
20
  import { jobRunsTable } from "../../job-run-table.js";
20
21
  import { tenantJobRunsTable } from "../../tenant-job-run-table.js";
@@ -31,18 +32,24 @@ export type StaleRunSweepResult = {
31
32
  readonly tenantQueuedDropped: number;
32
33
  };
33
34
 
35
+ // A queued tenant row can legitimately wait much longer than a run takes (backlog, delay/backoff,
36
+ // sequential requeue), so it is only treated as lost once it is older than the retention window,
37
+ // not after the running-timeout.
34
38
  export async function markStaleJobRunsFailed(
35
39
  db: DbConnection,
36
40
  timeoutHours: number,
41
+ queuedRetentionDays: number = DEFAULT_JOB_RUN_RETENTION_DAYS,
37
42
  ): Promise<StaleRunSweepResult> {
38
43
  const cutoff = Temporal.Now.instant().subtract({ hours: timeoutHours });
44
+ const queuedCutoff = Temporal.Now.instant().subtract({ hours: queuedRetentionDays * 24 });
39
45
  const now = Temporal.Now.instant();
40
46
 
41
47
  // Fetch the matched rows first: `error` carries the same per-triggering-
42
48
  // user subject annotation as job-run-logger.ts's onJobFailed path
43
49
  // (personal: { of: "triggeredById" } on job-run-table.ts), so it is encrypted
44
- // under that subject's DEK. Runs are grouped by subject: one encrypt and one
45
- // updateMany per subject instead of per run.
50
+ // under the triggering user's DEK, or the system tenant key when
51
+ // triggeredById is null (whenAbsent: "tenant", fw#3654). Runs are grouped by
52
+ // that subject: one encrypt and one updateMany per subject instead of per run.
46
53
  const stale = await selectMany<{ id: string; triggeredById: string | null }>(db, jobRunsTable, {
47
54
  status: "running",
48
55
  startedAt: { lt: cutoff },
@@ -86,7 +93,8 @@ export async function markStaleJobRunsFailed(
86
93
 
87
94
  // Tenant-visible run state follows the same clock: a running row whose
88
95
  // worker died is a failure, a queued row that never started (job lost from
89
- // Redis, enqueue hook without a run) would otherwise show "waiting" forever.
96
+ // Redis, enqueue hook without a run) would otherwise show "waiting" forever;
97
+ // it is dropped once it has outlived the retention window.
90
98
  const tenantRunsFailed = await updateMany(
91
99
  db,
92
100
  tenantJobRunsTable,
@@ -96,7 +104,7 @@ export async function markStaleJobRunsFailed(
96
104
  const tenantQueuedStale = await deleteManyBatched(
97
105
  db,
98
106
  tenantJobRunsTable,
99
- { status: "queued", updatedAt: { lt: cutoff } },
107
+ { status: "queued", updatedAt: { lt: queuedCutoff } },
100
108
  { limit: STALE_DELETE_BATCH_SIZE },
101
109
  );
102
110
 
@@ -36,7 +36,8 @@ import { tenantJobRunsTableMeta } from "./tenant-job-run-table.js";
36
36
 
37
37
  export type JobsFeatureOptions = {
38
38
  // How long a job run (and its logs) stays in store_job_runs/
39
- // store_job_run_logs before the daily retention-cleanup job deletes it.
39
+ // store_job_run_logs before the daily retention-cleanup job deletes it. Also the age after
40
+ // which a tenant-visible "queued" row counts as lost and is dropped by the stale-run sweep.
40
41
  readonly retentionDays?: number;
41
42
  // How long a run can sit at status "running" before the hourly
42
43
  // stale-run-sweep job marks it "failed" (#2246 — a process kill mid-run
@@ -51,7 +52,7 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
51
52
  const staleRunTimeoutHours = options.staleRunTimeoutHours ?? DEFAULT_JOB_RUN_STALE_TIMEOUT_HOURS;
52
53
  return defineFeature("jobs", (r) => {
53
54
  r.describe(
54
- "Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079). A job that declares `tenantVisibleRun` also keeps its run state per tenant and subject in `store_tenant_job_runs` (queued, running, plus the latest completed or failed run, no payload, error text or logs), which the tenant reads through `jobs:query:tenant-runs` (fw#3616); the stale-run sweep fails its stuck `running` rows and drops its stuck `queued` rows.",
55
+ "Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079). A job that declares `tenantVisibleRun` also keeps its run state per tenant and subject in `store_tenant_job_runs` (queued, running, plus the latest completed or failed run, no payload, error text or logs), which the tenant reads through `jobs:query:tenant-runs` (fw#3616); the stale-run sweep fails its stuck `running` rows and drops `queued` rows older than `retentionDays`.",
55
56
  );
56
57
  r.uiHints({
57
58
  displayLabel: "Jobs · Audit & Operator UI",
@@ -61,7 +62,9 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
61
62
  r.systemScope();
62
63
  r.storeTable(jobRunsTableMeta, {
63
64
  reason: "direct_write.job_runs",
64
- // payload/error carry personal:{of:"triggeredById"} (job-run-table.ts).
65
+ // payload/error carry personal:{of:"triggeredById"} (job-run-table.ts): encrypted under the
66
+ // triggering user's DEK, or the system tenant key when triggeredById is null
67
+ // (whenAbsent: "tenant", fw#3654).
65
68
  // job-run-logger.ts manually encrypts both before every insert/update
66
69
  // (encryptStartedPayload/encryptFailureError) since this store bypasses
67
70
  // the executor's automatic PII pipeline (#2243).
@@ -115,7 +118,7 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
115
118
  r.job(
116
119
  "stale-run-sweep",
117
120
  { trigger: { cron: "0 * * * *" }, concurrency: "skip" },
118
- createStaleRunSweepJob(staleRunTimeoutHours),
121
+ createStaleRunSweepJob(staleRunTimeoutHours, retentionDays),
119
122
  );
120
123
 
121
124
  const handlers = {
@@ -12,7 +12,7 @@ export type ManualJobCatalogEntry = {
12
12
  function payloadSchemaJson(job: JobDefinition): Record<string, unknown> | null {
13
13
  if (job.schema === undefined) return null;
14
14
  try {
15
- return z.toJSONSchema(job.schema) as Record<string, unknown>;
15
+ return z.toJSONSchema(job.schema);
16
16
  } catch {
17
17
  // Non-JSON-Schema-able Zod types (transforms/effects) — UI still lists the job.
18
18
  return null;
@@ -8,6 +8,8 @@ import { jobRunsTable } from "../job-run-table.js";
8
8
 
9
9
  const KMS_POOL_CONCURRENCY = 4;
10
10
  const DEFAULT_PAGE_SIZE = 50;
11
+ // The offset cursor re-reads everything up to the page end, so the page size bounds that read.
12
+ const MAX_LIST_LIMIT = 200;
11
13
 
12
14
  const jobRunStatusSchema = z.enum(["queued", "running", "completed", "failed"]);
13
15
 
@@ -68,7 +70,7 @@ export const listQuery = defineQueryHandler({
68
70
  .optional(),
69
71
  sort: z.enum(["jobName", "status", "startedAt", "duration"]).optional(),
70
72
  sortDirection: z.enum(["asc", "desc"]).optional(),
71
- limit: z.number().int().min(1).optional(),
73
+ limit: z.number().int().min(1).max(MAX_LIST_LIMIT).optional(),
72
74
  cursor: z.string().optional(),
73
75
  totalCount: z.boolean().optional(),
74
76
  }),