@cosmicdrift/kumiko-bundled-features 1.0.0 → 2.0.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 (669) hide show
  1. package/package.json +41 -7
  2. package/src/__tests__/access-invalidation-consumer.integration.test.ts +279 -0
  3. package/src/__tests__/access-invalidation-stream.integration.test.ts +217 -0
  4. package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +200 -0
  5. package/src/admin-shell/__tests__/admin-shell.boot.test.ts +147 -0
  6. package/src/admin-shell/__tests__/overview-allowlist.test.ts +43 -0
  7. package/src/admin-shell/__tests__/overview-screens.boot.test.ts +35 -0
  8. package/src/admin-shell/constants.ts +11 -0
  9. package/src/admin-shell/feature.ts +135 -0
  10. package/src/admin-shell/i18n.ts +36 -0
  11. package/src/admin-shell/index.ts +17 -0
  12. package/src/admin-shell/overview-allowlist.ts +37 -0
  13. package/src/admin-shell/web/client-plugin.tsx +26 -0
  14. package/src/admin-shell/web/i18n.ts +16 -0
  15. package/src/admin-shell/web/index.ts +2 -0
  16. package/src/admin-shell/web/overview-layout.tsx +69 -0
  17. package/src/admin-shell/web/overview-query.ts +15 -0
  18. package/src/admin-shell/web/platform-overview-screen.tsx +90 -0
  19. package/src/admin-shell/web/tenant-overview-screen.tsx +90 -0
  20. package/src/agent-tools/__tests__/tool-catalog.test.ts +179 -0
  21. package/src/agent-tools/__tests__/tool-dispatch.integration.test.ts +100 -0
  22. package/src/agent-tools/__tests__/tool-dispatch.test.ts +126 -0
  23. package/src/agent-tools/index.ts +9 -0
  24. package/src/agent-tools/tool-catalog.ts +130 -0
  25. package/src/agent-tools/tool-dispatch.ts +82 -0
  26. package/src/agent-tools/types.ts +38 -0
  27. package/src/audit/__tests__/audit-screens.boot.test.ts +51 -0
  28. package/src/audit/__tests__/audit-security.integration.test.ts +201 -0
  29. package/src/audit/__tests__/audit.integration.test.ts +50 -25
  30. package/src/audit/constants.ts +9 -2
  31. package/src/audit/feature.ts +33 -1
  32. package/src/audit/handlers/details.query.ts +47 -0
  33. package/src/audit/handlers/list.query.ts +3 -2
  34. package/src/audit/i18n.ts +31 -0
  35. package/src/audit/web/audit-log-detail-screen.tsx +131 -0
  36. package/src/audit/web/audit-log-screen.tsx +250 -0
  37. package/src/audit/web/client-plugin.tsx +22 -0
  38. package/src/audit/web/i18n.ts +12 -0
  39. package/src/audit/web/index.ts +5 -0
  40. package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.test.ts +4 -4
  41. package/src/auth-email-password/__tests__/account-lockout.integration.test.ts +4 -4
  42. package/src/auth-email-password/__tests__/account-unlock.integration.test.ts +283 -0
  43. package/src/auth-email-password/__tests__/auth-claims.integration.test.ts +10 -11
  44. package/src/auth-email-password/__tests__/auth-user-row.test.ts +36 -0
  45. package/src/auth-email-password/__tests__/auth.integration.test.ts +33 -6
  46. package/src/auth-email-password/__tests__/email-verification.integration.test.ts +9 -6
  47. package/src/auth-email-password/__tests__/feature-options.test.ts +33 -0
  48. package/src/auth-email-password/__tests__/identity-v3-login.integration.test.ts +3 -3
  49. package/src/auth-email-password/__tests__/invite-flow-kms.integration.test.ts +283 -0
  50. package/src/auth-email-password/__tests__/invite-flow.integration.test.ts +35 -1
  51. package/src/auth-email-password/__tests__/login-mfa-setup-required.integration.test.ts +129 -0
  52. package/src/auth-email-password/__tests__/multi-roles.integration.test.ts +1 -1
  53. package/src/auth-email-password/__tests__/password-policy.test.ts +15 -0
  54. package/src/auth-email-password/__tests__/password-reset.integration.test.ts +19 -6
  55. package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.test.ts +3 -3
  56. package/src/auth-email-password/__tests__/seed-admin.integration.test.ts +1 -1
  57. package/src/auth-email-password/__tests__/self-registration-toggle.integration.test.ts +264 -0
  58. package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +20 -4
  59. package/src/auth-email-password/__tests__/session-strict-mode.integration.test.ts +7 -14
  60. package/src/auth-email-password/__tests__/signup-flow.integration.test.ts +91 -0
  61. package/src/auth-email-password/constants.ts +25 -0
  62. package/src/auth-email-password/email-templates.ts +32 -0
  63. package/src/auth-email-password/errors.ts +9 -0
  64. package/src/auth-email-password/feature.ts +59 -11
  65. package/src/auth-email-password/handlers/change-password.write.ts +14 -13
  66. package/src/auth-email-password/handlers/confirm-account-unlock.write.ts +59 -0
  67. package/src/auth-email-password/handlers/confirm-token-flow.ts +34 -105
  68. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +12 -5
  69. package/src/auth-email-password/handlers/invite-accept.write.ts +9 -2
  70. package/src/auth-email-password/handlers/invite-create.write.ts +2 -2
  71. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +12 -4
  72. package/src/auth-email-password/handlers/login.write.ts +86 -16
  73. package/src/auth-email-password/handlers/logout.write.ts +4 -2
  74. package/src/auth-email-password/handlers/request-account-unlock.write.ts +38 -0
  75. package/src/auth-email-password/handlers/reset-password.write.ts +3 -2
  76. package/src/auth-email-password/handlers/self-registration-status.query.ts +23 -0
  77. package/src/auth-email-password/handlers/signup-confirm.write.ts +19 -11
  78. package/src/auth-email-password/handlers/signup-request.write.ts +22 -0
  79. package/src/auth-email-password/handlers/token-request-handler.ts +4 -2
  80. package/src/auth-email-password/i18n.ts +52 -0
  81. package/src/auth-email-password/index.ts +13 -2
  82. package/src/auth-email-password/lockout-store.ts +10 -5
  83. package/src/auth-email-password/password-policy.ts +2108 -0
  84. package/src/auth-email-password/seeding.ts +55 -27
  85. package/src/auth-email-password/self-registration-toggle.ts +25 -0
  86. package/src/auth-email-password/signed-token.ts +1 -0
  87. package/src/auth-email-password/unlock-token.ts +28 -0
  88. package/src/auth-email-password/web/__tests__/auth-client.test.ts +542 -0
  89. package/src/auth-email-password/web/__tests__/auth-form-logic.test.ts +44 -0
  90. package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +194 -6
  91. package/src/auth-email-password/web/__tests__/client-plugin.test.tsx +34 -0
  92. package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +64 -0
  93. package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +5 -1
  94. package/src/auth-email-password/web/__tests__/invite-accept-screen.test.tsx +129 -2
  95. package/src/auth-email-password/web/__tests__/login-screen.test.tsx +149 -5
  96. package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +137 -0
  97. package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +5 -1
  98. package/src/auth-email-password/web/__tests__/signup-complete-screen.test.tsx +134 -0
  99. package/src/auth-email-password/web/__tests__/signup-screen.test.tsx +93 -0
  100. package/src/auth-email-password/web/__tests__/test-utils.tsx +12 -2
  101. package/src/auth-email-password/web/__tests__/use-url-token.test.tsx +29 -0
  102. package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +5 -1
  103. package/src/auth-email-password/web/auth-client.ts +96 -18
  104. package/src/auth-email-password/web/auth-form-logic.ts +30 -0
  105. package/src/auth-email-password/web/auth-form-primitives.tsx +22 -20
  106. package/src/auth-email-password/web/auth-gate.tsx +140 -19
  107. package/src/auth-email-password/web/client-plugin.ts +24 -2
  108. package/src/auth-email-password/web/confirm-account-unlock-screen.tsx +95 -0
  109. package/src/auth-email-password/web/forgot-password-screen.tsx +6 -6
  110. package/src/auth-email-password/web/index.ts +16 -4
  111. package/src/auth-email-password/web/invite-accept-screen.tsx +5 -5
  112. package/src/auth-email-password/web/login-screen.tsx +78 -22
  113. package/src/auth-email-password/web/request-account-unlock-screen.tsx +112 -0
  114. package/src/auth-email-password/web/reset-password-screen.tsx +15 -18
  115. package/src/auth-email-password/web/session.tsx +11 -6
  116. package/src/auth-email-password/web/signup-complete-screen.tsx +13 -15
  117. package/src/auth-email-password/web/signup-screen.tsx +8 -10
  118. package/src/auth-email-password/web/use-shell-user.ts +5 -2
  119. package/src/auth-email-password/web/user-menu.tsx +3 -1
  120. package/src/auth-email-password/web/verify-email-screen.tsx +10 -14
  121. package/src/auth-foundation/__tests__/feature.test.ts +46 -0
  122. package/src/auth-foundation/__tests__/session-store.test.ts +77 -0
  123. package/src/auth-foundation/__tests__/tenant-providers.test.ts +160 -0
  124. package/src/auth-foundation/__tests__/token-verifier.test.ts +150 -0
  125. package/src/auth-foundation/boot-checks.ts +150 -0
  126. package/src/auth-foundation/feature.ts +157 -0
  127. package/src/auth-foundation/index.ts +37 -0
  128. package/src/auth-foundation/resolve-anonymous-access.ts +62 -0
  129. package/src/auth-foundation/types.ts +170 -0
  130. package/src/auth-mfa/__tests__/base32.test.ts +28 -0
  131. package/src/auth-mfa/__tests__/disable-and-regenerate.integration.test.ts +265 -0
  132. package/src/auth-mfa/__tests__/enable-confirm-preauth.integration.test.ts +321 -0
  133. package/src/auth-mfa/__tests__/enable-flow.integration.test.ts +322 -0
  134. package/src/auth-mfa/__tests__/enable-start-preauth.integration.test.ts +237 -0
  135. package/src/auth-mfa/__tests__/errors-i18n-parity.test.ts +39 -0
  136. package/src/auth-mfa/__tests__/mfa-preauth-setup-token.test.ts +113 -0
  137. package/src/auth-mfa/__tests__/no-redis.integration.test.ts +209 -0
  138. package/src/auth-mfa/__tests__/pii-subject-encryption.integration.test.ts +139 -0
  139. package/src/auth-mfa/__tests__/reencrypt-job.integration.test.ts +247 -0
  140. package/src/auth-mfa/__tests__/session-auto-revoke.integration.test.ts +169 -0
  141. package/src/auth-mfa/__tests__/totp.test.ts +131 -0
  142. package/src/auth-mfa/__tests__/verify.integration.test.ts +319 -0
  143. package/src/auth-mfa/base32.ts +51 -0
  144. package/src/auth-mfa/config.ts +26 -0
  145. package/src/auth-mfa/constants.ts +45 -0
  146. package/src/auth-mfa/db/queries.ts +45 -0
  147. package/src/auth-mfa/errors.ts +66 -0
  148. package/src/auth-mfa/feature.ts +170 -0
  149. package/src/auth-mfa/handlers/disable.write.ts +67 -0
  150. package/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +200 -0
  151. package/src/auth-mfa/handlers/enable-confirm.write.ts +106 -0
  152. package/src/auth-mfa/handlers/enable-start-preauth.write.ts +87 -0
  153. package/src/auth-mfa/handlers/enable-start.write.ts +66 -0
  154. package/src/auth-mfa/handlers/reencrypt.job.ts +234 -0
  155. package/src/auth-mfa/handlers/regenerate-recovery.write.ts +77 -0
  156. package/src/auth-mfa/handlers/status.query.ts +23 -0
  157. package/src/auth-mfa/handlers/verify.write.ts +179 -0
  158. package/src/auth-mfa/i18n.ts +12 -0
  159. package/src/auth-mfa/index.ts +19 -0
  160. package/src/auth-mfa/mfa-challenge-token.ts +90 -0
  161. package/src/auth-mfa/mfa-preauth-setup-token.ts +86 -0
  162. package/src/auth-mfa/mfa-setup-token.ts +96 -0
  163. package/src/auth-mfa/mfa-status-checker.ts +84 -0
  164. package/src/auth-mfa/mfa-verify-attempts.ts +90 -0
  165. package/src/auth-mfa/otpauth-uri.ts +21 -0
  166. package/src/auth-mfa/recovery-codes.ts +54 -0
  167. package/src/auth-mfa/schema/user-mfa.ts +60 -0
  168. package/src/auth-mfa/testing.ts +5 -0
  169. package/src/auth-mfa/totp.ts +67 -0
  170. package/src/auth-mfa/verify-factor.ts +58 -0
  171. package/src/auth-mfa/web/__tests__/mfa-client.test.ts +265 -0
  172. package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +182 -0
  173. package/src/auth-mfa/web/__tests__/mfa-setup-preauth-screen.test.tsx +379 -0
  174. package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +104 -0
  175. package/src/auth-mfa/web/client-plugin.ts +37 -0
  176. package/src/auth-mfa/web/i18n.ts +136 -0
  177. package/src/auth-mfa/web/index.ts +27 -0
  178. package/src/auth-mfa/web/mfa-client.ts +184 -0
  179. package/src/auth-mfa/web/mfa-disable-dialog.tsx +69 -0
  180. package/src/auth-mfa/web/mfa-enable-screen.tsx +228 -0
  181. package/src/auth-mfa/web/mfa-recovery-codes-reveal.tsx +53 -0
  182. package/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +68 -0
  183. package/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +267 -0
  184. package/src/auth-mfa/web/mfa-verify-screen.tsx +117 -0
  185. package/src/auth-mfa/web/qrcode-browser.d.ts +12 -0
  186. package/src/auth-mfa-user-data/__tests__/hooks.integration.test.ts +134 -0
  187. package/src/auth-mfa-user-data/hooks.ts +53 -0
  188. package/src/auth-mfa-user-data/index.ts +21 -0
  189. package/src/billing-foundation/__tests__/billing-foundation.integration.test.ts +206 -2
  190. package/src/billing-foundation/__tests__/subscription-provider-contract.ts +93 -0
  191. package/src/billing-foundation/entities.ts +28 -4
  192. package/src/billing-foundation/events.ts +6 -2
  193. package/src/billing-foundation/feature.ts +9 -1
  194. package/src/billing-foundation/get-subscription-for-tenant.ts +16 -5
  195. package/src/billing-foundation/handlers/create-portal-session.write.ts +17 -1
  196. package/src/billing-foundation/handlers/list-subscriptions.query.ts +15 -1
  197. package/src/billing-foundation/handlers/process-event.write.ts +39 -3
  198. package/src/billing-foundation/tenant-destroy-hook.ts +46 -0
  199. package/src/cap-counter/__tests__/cap-list.boot.test.ts +27 -0
  200. package/src/cap-counter/__tests__/cap-list.security.integration.test.ts +53 -0
  201. package/src/cap-counter/__tests__/entity-ddl.test.ts +15 -0
  202. package/src/cap-counter/__tests__/with-cap-enforcement.integration.test.ts +8 -5
  203. package/src/cap-counter/constants.ts +2 -0
  204. package/src/cap-counter/enforce-cap.ts +15 -18
  205. package/src/cap-counter/entity.ts +3 -3
  206. package/src/cap-counter/feature.ts +28 -1
  207. package/src/cap-counter/i18n.ts +12 -0
  208. package/src/cap-counter/with-cap-enforcement.ts +2 -2
  209. package/src/channel-email/__tests__/pii-guard.test.ts +56 -0
  210. package/src/channel-email/email-channel.ts +2 -1
  211. package/src/channel-email/index.ts +1 -0
  212. package/src/channel-email/pii-guard.ts +39 -0
  213. package/src/compliance-profiles/__tests__/profile-picker.boot.test.ts +28 -0
  214. package/src/compliance-profiles/__tests__/profile-picker.security.integration.test.ts +91 -0
  215. package/src/compliance-profiles/constants.ts +15 -0
  216. package/src/compliance-profiles/feature.ts +22 -1
  217. package/src/compliance-profiles/handlers/set-profile.write.ts +2 -2
  218. package/src/compliance-profiles/i18n.ts +6 -0
  219. package/src/compliance-profiles/index.ts +6 -0
  220. package/src/compliance-profiles/web/client-plugin.tsx +22 -0
  221. package/src/compliance-profiles/web/compliance-profile-screen.tsx +152 -0
  222. package/src/compliance-profiles/web/i18n.ts +25 -0
  223. package/src/compliance-profiles/web/index.ts +6 -0
  224. package/src/config/__tests__/app-overrides.test.ts +11 -0
  225. package/src/config/__tests__/backing-secrets.integration.test.ts +22 -0
  226. package/src/config/__tests__/cascade.integration.test.ts +35 -0
  227. package/src/config/__tests__/config.integration.test.ts +71 -10
  228. package/src/config/__tests__/deserialize-value.test.ts +5 -8
  229. package/src/config/__tests__/inherited-redaction.integration.test.ts +4 -3
  230. package/src/config/__tests__/pii-encrypted.integration.test.ts +179 -0
  231. package/src/config/__tests__/reencrypt-job-concurrent-write-race.integration.test.ts +234 -0
  232. package/src/config/__tests__/reencrypt-job-kek-rotation.integration.test.ts +243 -0
  233. package/src/config/__tests__/write-helpers.test.ts +23 -0
  234. package/src/config/db/queries/resolver.ts +3 -3
  235. package/src/config/feature.ts +15 -8
  236. package/src/config/handlers/cascade.query.ts +2 -1
  237. package/src/config/handlers/reencrypt.job.ts +209 -0
  238. package/src/config/handlers/set.write.ts +55 -3
  239. package/src/config/handlers/values.query.ts +2 -1
  240. package/src/config/i18n.ts +9 -0
  241. package/src/config/index.ts +5 -4
  242. package/src/config/resolver.ts +75 -61
  243. package/src/config/table.ts +1 -1
  244. package/src/config/write-helpers.ts +22 -0
  245. package/src/crypto-shredding/__tests__/forget-subject.integration.test.ts +148 -0
  246. package/src/crypto-shredding/constants.ts +5 -0
  247. package/src/crypto-shredding/feature.ts +18 -0
  248. package/src/crypto-shredding/handlers/forget-subject.write.ts +91 -0
  249. package/src/crypto-shredding/index.ts +11 -0
  250. package/src/custom-fields/__tests__/audit-integration.integration.test.ts +1 -1
  251. package/src/custom-fields/__tests__/custom-fields.integration.test.ts +92 -53
  252. package/src/custom-fields/__tests__/field-access.integration.test.ts +25 -0
  253. package/src/custom-fields/__tests__/parse-serialized-field.test.ts +11 -3
  254. package/src/custom-fields/__tests__/retention.integration.test.ts +40 -0
  255. package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +26 -79
  256. package/src/custom-fields/constants.ts +1 -0
  257. package/src/custom-fields/db/queries/user-data-rights.ts +0 -20
  258. package/src/custom-fields/entity.ts +1 -1
  259. package/src/custom-fields/events.ts +4 -3
  260. package/src/custom-fields/feature.ts +8 -0
  261. package/src/custom-fields/handlers/delete-system-field.write.ts +2 -1
  262. package/src/custom-fields/handlers/delete-tenant-field.write.ts +2 -1
  263. package/src/custom-fields/handlers/set-custom-field.write.ts +3 -44
  264. package/src/custom-fields/handlers/update-tenant-field.write.ts +0 -17
  265. package/src/custom-fields/lib/field-access.ts +8 -1
  266. package/src/custom-fields/lib/parse-serialized-field.ts +12 -4
  267. package/src/custom-fields/lib/value-schema.ts +1 -1
  268. package/src/custom-fields/run-retention.ts +9 -1
  269. package/src/custom-fields/schemas.ts +9 -9
  270. package/src/custom-fields/wire-for-entity.ts +110 -78
  271. package/src/custom-fields/wire-user-data-rights.ts +5 -54
  272. package/src/data-retention/__tests__/parse-override.test.ts +14 -13
  273. package/src/data-retention/__tests__/retention-cleanup.integration.test.ts +145 -8
  274. package/src/data-retention/feature.ts +3 -3
  275. package/src/data-retention/run-retention-cleanup.ts +102 -19
  276. package/src/delivery/__tests__/delivery-pii-kms.integration.test.ts +171 -0
  277. package/src/delivery/__tests__/delivery-screens.boot.test.ts +27 -0
  278. package/src/delivery/__tests__/delivery-security.integration.test.ts +106 -0
  279. package/src/delivery/constants.ts +13 -51
  280. package/src/delivery/feature.ts +29 -5
  281. package/src/delivery/handlers/log.query.ts +28 -7
  282. package/src/delivery/i18n.ts +6 -0
  283. package/src/delivery/index.ts +6 -1
  284. package/src/delivery/public-names.ts +36 -0
  285. package/src/delivery/tables.ts +3 -3
  286. package/src/delivery/web/client-plugin.tsx +20 -0
  287. package/src/delivery/web/delivery-log-screen.tsx +103 -0
  288. package/src/delivery/web/i18n.ts +25 -0
  289. package/src/delivery/web/index.ts +3 -0
  290. package/src/document-ingest-foundation/__tests__/feature.integration.test.ts +147 -0
  291. package/src/document-ingest-foundation/__tests__/feature.test.ts +105 -0
  292. package/src/document-ingest-foundation/__tests__/pages.test.ts +37 -0
  293. package/src/document-ingest-foundation/entity.ts +43 -0
  294. package/src/document-ingest-foundation/events.ts +25 -0
  295. package/src/document-ingest-foundation/feature.ts +126 -0
  296. package/src/document-ingest-foundation/index.ts +12 -0
  297. package/src/document-ingest-foundation/pages.ts +34 -0
  298. package/src/feature-toggles/__tests__/compose-tier-resolver.test.ts +304 -0
  299. package/src/feature-toggles/__tests__/feature-toggles.integration.test.ts +7 -6
  300. package/src/feature-toggles/__tests__/toggle-admin.boot.test.ts +31 -0
  301. package/src/feature-toggles/__tests__/toggle-admin.security.integration.test.ts +69 -0
  302. package/src/feature-toggles/compose-tier-resolver.ts +134 -0
  303. package/src/feature-toggles/constants.ts +14 -0
  304. package/src/feature-toggles/feature.ts +23 -3
  305. package/src/feature-toggles/global-feature-state-table.ts +20 -1
  306. package/src/feature-toggles/handlers/list.query.ts +1 -1
  307. package/src/feature-toggles/handlers/registered.query.ts +1 -1
  308. package/src/feature-toggles/i18n.ts +6 -0
  309. package/src/feature-toggles/index.ts +8 -0
  310. package/src/feature-toggles/toggle-runtime.ts +14 -0
  311. package/src/feature-toggles/web/client-plugin.tsx +22 -0
  312. package/src/feature-toggles/web/i18n.ts +29 -0
  313. package/src/feature-toggles/web/index.ts +6 -0
  314. package/src/feature-toggles/web/toggle-admin-screen.tsx +136 -0
  315. package/src/file-foundation/__tests__/feature.test.ts +5 -5
  316. package/src/file-foundation/__tests__/file-foundation.integration.test.ts +4 -3
  317. package/src/file-foundation/feature.ts +10 -11
  318. package/src/files-provider-s3/__tests__/s3-provider.integration.test.ts +27 -2
  319. package/src/files-provider-s3/s3-provider.ts +12 -1
  320. package/src/folders/__tests__/folders.integration.test.ts +3 -0
  321. package/src/folders/web/__tests__/folder-manager.test.tsx +43 -2
  322. package/src/folders/web/__tests__/tree.test.ts +8 -0
  323. package/src/folders/web/folder-manager.tsx +2 -3
  324. package/src/folders/web/i18n.ts +0 -2
  325. package/src/folders/web/tree.ts +13 -2
  326. package/src/folders-user-data/__tests__/hooks.integration.test.ts +1 -0
  327. package/src/folders-user-data/index.ts +4 -4
  328. package/src/inbound-mail-foundation/__tests__/account-state.test.ts +30 -0
  329. package/src/inbound-mail-foundation/__tests__/connect-routes.integration.test.ts +307 -0
  330. package/src/inbound-mail-foundation/__tests__/feature.test.ts +169 -0
  331. package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +644 -0
  332. package/src/inbound-mail-foundation/__tests__/inbound-mail-provider-contract.ts +111 -0
  333. package/src/inbound-mail-foundation/__tests__/retention.integration.test.ts +237 -0
  334. package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +433 -0
  335. package/src/inbound-mail-foundation/aggregate-id.ts +40 -0
  336. package/src/inbound-mail-foundation/connect-routes.ts +230 -0
  337. package/src/inbound-mail-foundation/constants.ts +80 -0
  338. package/src/inbound-mail-foundation/db/queries/inbound-projections.ts +38 -0
  339. package/src/inbound-mail-foundation/entities.ts +172 -0
  340. package/src/inbound-mail-foundation/events.ts +147 -0
  341. package/src/inbound-mail-foundation/feature.ts +243 -0
  342. package/src/inbound-mail-foundation/handlers/account-state.ts +23 -0
  343. package/src/inbound-mail-foundation/handlers/connect-account.write.ts +112 -0
  344. package/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +69 -0
  345. package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +333 -0
  346. package/src/inbound-mail-foundation/handlers/list-accounts.query.ts +44 -0
  347. package/src/inbound-mail-foundation/handlers/list-messages.query.ts +58 -0
  348. package/src/inbound-mail-foundation/handlers/scope-visibility.ts +14 -0
  349. package/src/inbound-mail-foundation/handlers/update-account.write.ts +78 -0
  350. package/src/inbound-mail-foundation/index.ts +97 -0
  351. package/src/inbound-mail-foundation/oauth-state.ts +103 -0
  352. package/src/inbound-mail-foundation/projection.ts +152 -0
  353. package/src/inbound-mail-foundation/provider-factory.ts +53 -0
  354. package/src/inbound-mail-foundation/retention-sweep.ts +168 -0
  355. package/src/inbound-mail-foundation/tenant-destroy-hook.ts +93 -0
  356. package/src/inbound-mail-foundation/types.ts +250 -0
  357. package/src/inbound-mail-foundation/watch-supervisor.ts +482 -0
  358. package/src/inbound-provider-imap/__tests__/feature.test.ts +115 -0
  359. package/src/inbound-provider-imap/__tests__/imap-foundation.integration.test.ts +302 -0
  360. package/src/inbound-provider-imap/__tests__/imap-live.integration.test.ts +206 -0
  361. package/src/inbound-provider-imap/__tests__/plugin-mocked.test.ts +358 -0
  362. package/src/inbound-provider-imap/credential-document.ts +51 -0
  363. package/src/inbound-provider-imap/feature.ts +283 -0
  364. package/src/inbound-provider-imap/imap-client.ts +158 -0
  365. package/src/inbound-provider-imap/index.ts +17 -0
  366. package/src/inbound-provider-inmemory/__tests__/feature.test.ts +52 -0
  367. package/src/inbound-provider-inmemory/feature.ts +154 -0
  368. package/src/inbound-provider-inmemory/index.ts +12 -0
  369. package/src/jobs/__tests__/entity-ddl.test.ts +15 -0
  370. package/src/jobs/__tests__/job-instant.test.ts +15 -0
  371. package/src/jobs/__tests__/job-system-user.integration.test.ts +4 -8
  372. package/src/jobs/__tests__/jobs-events.integration.test.ts +37 -1
  373. package/src/jobs/__tests__/jobs-pii-kms.integration.test.ts +120 -0
  374. package/src/jobs/__tests__/jobs-screens.boot.test.ts +36 -0
  375. package/src/jobs/__tests__/jobs-security.integration.test.ts +108 -0
  376. package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +169 -0
  377. package/src/jobs/constants.ts +8 -1
  378. package/src/jobs/feature.ts +52 -8
  379. package/src/jobs/handlers/detail.query.ts +6 -0
  380. package/src/jobs/handlers/list.query.ts +8 -1
  381. package/src/jobs/handlers/projection-rebuild.job.ts +14 -4
  382. package/src/jobs/handlers/reindex-entity.job.ts +49 -0
  383. package/src/jobs/i18n.ts +37 -0
  384. package/src/jobs/job-instant.ts +9 -0
  385. package/src/jobs/job-run-table.ts +4 -4
  386. package/src/jobs/web/client-plugin.tsx +22 -0
  387. package/src/jobs/web/i18n.ts +12 -0
  388. package/src/jobs/web/index.ts +4 -0
  389. package/src/jobs/web/job-run-detail-screen.tsx +180 -0
  390. package/src/jobs/web/job-runs-screen.tsx +146 -0
  391. package/src/ledger/__tests__/ledger.integration.test.ts +8 -3
  392. package/src/ledger/handlers/confirm-schedule-period.write.ts +17 -4
  393. package/src/ledger/recurring.ts +17 -8
  394. package/src/ledger/web/index.ts +3 -2
  395. package/src/legal-pages/__tests__/legal-pages.integration.test.ts +55 -0
  396. package/src/legal-pages/feature.ts +24 -31
  397. package/src/legal-pages/index.ts +1 -0
  398. package/src/mail-foundation/__tests__/feature.test.ts +5 -6
  399. package/src/mail-foundation/__tests__/mail-foundation.integration.test.ts +4 -3
  400. package/src/mail-foundation/__tests__/mail-transport-contract.ts +58 -0
  401. package/src/mail-foundation/feature.ts +26 -24
  402. package/src/mail-transport-inmemory/__tests__/feature.test.ts +23 -0
  403. package/src/mail-transport-smtp/__tests__/feature.test.ts +65 -0
  404. package/src/managed-pages/__tests__/branding-coerce.test.ts +2 -6
  405. package/src/managed-pages/__tests__/i18n-surface.boot.test.ts +30 -0
  406. package/src/managed-pages/__tests__/managed-pages.integration.test.ts +137 -0
  407. package/src/managed-pages/feature.ts +45 -47
  408. package/src/managed-pages/handlers/branding.query.ts +5 -1
  409. package/src/managed-pages/handlers/by-tenant-published.query.ts +52 -0
  410. package/src/managed-pages/i18n.ts +66 -0
  411. package/src/managed-pages/screens/page-screens.ts +5 -1
  412. package/src/managed-pages/web/client-plugin.tsx +22 -0
  413. package/src/managed-pages/web/i18n.ts +24 -0
  414. package/src/managed-pages/web/index.ts +3 -0
  415. package/src/page-render/__tests__/layout.test.ts +63 -0
  416. package/src/page-render/__tests__/markdown.test.ts +29 -0
  417. package/src/page-render/branding.ts +4 -3
  418. package/src/page-render/index.ts +1 -1
  419. package/src/page-render/layout.ts +18 -3
  420. package/src/page-render/markdown.ts +1 -9
  421. package/src/page-render/web.ts +13 -0
  422. package/src/personal-access-tokens/__tests__/feature.test.ts +42 -0
  423. package/src/personal-access-tokens/__tests__/pat.integration.test.ts +239 -0
  424. package/src/personal-access-tokens/__tests__/scopes.test.ts +42 -0
  425. package/src/personal-access-tokens/constants.ts +33 -0
  426. package/src/personal-access-tokens/feature.ts +105 -0
  427. package/src/personal-access-tokens/handlers/available-scopes.query.ts +21 -0
  428. package/src/personal-access-tokens/handlers/create.write.ts +47 -0
  429. package/src/personal-access-tokens/handlers/list.query.ts +50 -0
  430. package/src/personal-access-tokens/handlers/revoke.write.ts +37 -0
  431. package/src/personal-access-tokens/hash.ts +17 -0
  432. package/src/personal-access-tokens/i18n.ts +5 -0
  433. package/src/personal-access-tokens/index.ts +31 -0
  434. package/src/personal-access-tokens/resolver.ts +58 -0
  435. package/src/personal-access-tokens/roles.ts +29 -0
  436. package/src/personal-access-tokens/schema/api-token.ts +63 -0
  437. package/src/personal-access-tokens/scopes.ts +43 -0
  438. package/src/personal-access-tokens/web/client-plugin.tsx +29 -0
  439. package/src/personal-access-tokens/web/i18n.ts +69 -0
  440. package/src/personal-access-tokens/web/index.ts +7 -0
  441. package/src/personal-access-tokens/web/pat-tokens-screen.tsx +265 -0
  442. package/src/readiness/__tests__/readiness.integration.test.ts +4 -3
  443. package/src/renderer-simple/__tests__/resolve-variables.test.ts +37 -0
  444. package/src/renderer-simple/resolve-variables.ts +3 -1
  445. package/src/secrets/__tests__/rotate.integration.test.ts +115 -18
  446. package/src/secrets/handlers/rotate.job.ts +77 -98
  447. package/src/secrets/secrets-context.ts +7 -38
  448. package/src/secrets/table.ts +7 -8
  449. package/src/seo/__tests__/llms-txt.test.ts +38 -0
  450. package/src/seo/__tests__/robots-txt.test.ts +18 -0
  451. package/src/seo/__tests__/schema-builders.test.ts +57 -0
  452. package/src/seo/__tests__/seo.integration.test.ts +350 -0
  453. package/src/seo/__tests__/sitemap.test.ts +38 -0
  454. package/src/seo/constants.ts +60 -0
  455. package/src/seo/feature.ts +316 -0
  456. package/src/seo/handlers/seo-config.query.ts +35 -0
  457. package/src/seo/index.ts +19 -0
  458. package/src/seo/llms-txt.ts +33 -0
  459. package/src/seo/robots-txt.ts +13 -0
  460. package/src/seo/schema-builders.ts +57 -0
  461. package/src/seo/sitemap.ts +33 -0
  462. package/src/sessions/__tests__/auto-revoke-binding.test.ts +71 -0
  463. package/src/sessions/__tests__/cleanup.integration.test.ts +2 -1
  464. package/src/sessions/__tests__/password-auto-revoke.integration.test.ts +11 -7
  465. package/src/sessions/__tests__/rebuild-survival.integration.test.ts +18 -7
  466. package/src/sessions/__tests__/session-store-registration.test.ts +21 -0
  467. package/src/sessions/__tests__/sessions-screens.boot.test.ts +55 -0
  468. package/src/sessions/__tests__/sessions.integration.test.ts +272 -20
  469. package/src/sessions/__tests__/test-helpers.ts +16 -3
  470. package/src/sessions/constants.ts +5 -0
  471. package/src/sessions/db/queries/cleanup.ts +2 -2
  472. package/src/sessions/feature.ts +175 -26
  473. package/src/sessions/handlers/detail.query.ts +36 -0
  474. package/src/sessions/handlers/list.query.ts +14 -9
  475. package/src/sessions/handlers/mine.query.ts +13 -8
  476. package/src/sessions/handlers/revoke-all-for-user.write.ts +51 -12
  477. package/src/sessions/handlers/revoke-all-others.write.ts +19 -1
  478. package/src/sessions/handlers/revoke.write.ts +13 -0
  479. package/src/sessions/i18n.ts +23 -0
  480. package/src/sessions/index.ts +9 -2
  481. package/src/sessions/schema/user-session.ts +8 -2
  482. package/src/sessions/session-callbacks.ts +99 -14
  483. package/src/sessions/session-checker-fail-open.test.ts +77 -0
  484. package/src/sessions/session-revoked-event.ts +29 -0
  485. package/src/sessions/testing.ts +14 -1
  486. package/src/shared/__tests__/chunked-entity-migration.test.ts +145 -0
  487. package/src/shared/chunked-entity-migration.ts +89 -0
  488. package/src/shared/decrypt-stored-pii.ts +26 -0
  489. package/src/shared/encrypt-for-direct-write.ts +23 -0
  490. package/src/shared/entities-of.ts +17 -0
  491. package/src/shared/grace-period.test.ts +26 -0
  492. package/src/shared/grace-period.ts +20 -0
  493. package/src/{auth-email-password/__tests__ → shared}/identity-v3-hash.test.ts +2 -2
  494. package/src/shared/index.ts +16 -0
  495. package/src/shared/map-with-concurrency.test.ts +104 -0
  496. package/src/shared/map-with-concurrency.ts +38 -0
  497. package/src/shared/password-hashing.test.ts +35 -0
  498. package/src/{auth-email-password → shared}/password-hashing.ts +11 -0
  499. package/src/shared/system-query.ts +5 -0
  500. package/src/{auth-email-password → shared}/token-burn-store.ts +1 -1
  501. package/src/subscription-mollie/__tests__/feature.test.ts +97 -0
  502. package/src/subscription-mollie/__tests__/mollie-foundation.integration.test.ts +12 -1
  503. package/src/subscription-mollie/__tests__/verify-webhook.test.ts +21 -0
  504. package/src/subscription-mollie/verify-webhook.ts +1 -0
  505. package/src/subscription-stripe/__tests__/feature.test.ts +127 -1
  506. package/src/subscription-stripe/__tests__/runtime.test.ts +14 -24
  507. package/src/subscription-stripe/__tests__/stripe-foundation.integration.test.ts +15 -5
  508. package/src/subscription-stripe/__tests__/verify-webhook.test.ts +20 -0
  509. package/src/subscription-stripe/feature.ts +2 -0
  510. package/src/subscription-stripe/verify-webhook.ts +3 -2
  511. package/src/tags/__tests__/schemas.test.ts +34 -0
  512. package/src/tags/__tests__/tags.integration.test.ts +12 -1
  513. package/src/tags/entity.ts +6 -1
  514. package/src/tags/feature.ts +2 -0
  515. package/src/tags/i18n.ts +5 -0
  516. package/src/tags/schemas.ts +9 -2
  517. package/src/tags/web/__tests__/tag-manager.test.tsx +333 -0
  518. package/src/tags/web/__tests__/tag-section.test.tsx +47 -1
  519. package/src/tags/web/tag-manager.tsx +1 -1
  520. package/src/tags/web/tag-section.tsx +6 -3
  521. package/src/template-resolver/testing.ts +2 -1
  522. package/src/tenant/__tests__/members-query-kms.integration.test.ts +98 -0
  523. package/src/tenant/__tests__/members-screens.boot.test.ts +51 -0
  524. package/src/tenant/__tests__/multi-tenant.integration.test.ts +1 -1
  525. package/src/tenant/__tests__/tenant-security.integration.test.ts +310 -0
  526. package/src/tenant/__tests__/tenant.integration.test.ts +8 -4
  527. package/src/tenant/constants.ts +5 -0
  528. package/src/tenant/feature.ts +19 -0
  529. package/src/tenant/handlers/active-tenant-ids.query.ts +8 -2
  530. package/src/tenant/handlers/cancel-invitation.write.ts +2 -2
  531. package/src/tenant/handlers/invitations.query.ts +21 -4
  532. package/src/tenant/handlers/members.query.ts +41 -6
  533. package/src/tenant/i18n.ts +28 -0
  534. package/src/tenant/index.ts +7 -2
  535. package/src/tenant/invitation-table.ts +3 -2
  536. package/src/tenant/schema/tenant.ts +27 -2
  537. package/src/tenant/screens.ts +3 -2
  538. package/src/tenant/seeding.ts +136 -3
  539. package/src/tenant/web/client-plugin.tsx +20 -0
  540. package/src/tenant/web/i18n.ts +43 -0
  541. package/src/tenant/web/index.ts +4 -0
  542. package/src/tenant/web/members-screen.tsx +270 -0
  543. package/src/tenant-lifecycle/__tests__/stages.test.ts +22 -0
  544. package/src/tenant-lifecycle/__tests__/tenant-lifecycle.integration.test.ts +471 -0
  545. package/src/tenant-lifecycle/boot-checks.ts +45 -0
  546. package/src/tenant-lifecycle/constants.ts +56 -0
  547. package/src/tenant-lifecycle/events.ts +46 -0
  548. package/src/tenant-lifecycle/feature.ts +112 -0
  549. package/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +79 -0
  550. package/src/tenant-lifecycle/handlers/request-destruction.write.ts +83 -0
  551. package/src/tenant-lifecycle/index.ts +7 -0
  552. package/src/tenant-lifecycle/lib/revoke-tenant-sessions.ts +16 -0
  553. package/src/tenant-lifecycle/lifecycle-gate.ts +65 -0
  554. package/src/tenant-lifecycle/run-tenant-destroy.ts +368 -0
  555. package/src/tenant-lifecycle/stages.ts +206 -0
  556. package/src/tenant-settings/__tests__/tenant-settings.integration.test.ts +115 -0
  557. package/src/tenant-settings/config.ts +44 -0
  558. package/src/tenant-settings/constants.ts +16 -0
  559. package/src/tenant-settings/feature.ts +49 -0
  560. package/src/tenant-settings/index.ts +8 -0
  561. package/src/tenant-settings/tenant-defaults.ts +67 -0
  562. package/src/text-content/web/__tests__/editor-read-only.test.tsx +33 -6
  563. package/src/text-content/web/__tests__/group-blocks.test.ts +28 -3
  564. package/src/text-content/web/client-plugin.tsx +126 -104
  565. package/src/tier-engine/__tests__/auto-default-tier.integration.test.ts +1 -1
  566. package/src/tier-engine/__tests__/resolver.integration.test.ts +1 -1
  567. package/src/tier-engine/__tests__/tier-engine.integration.test.ts +7 -4
  568. package/src/tier-engine/__tests__/trial-config-validation.test.ts +30 -0
  569. package/src/tier-engine/feature.ts +13 -6
  570. package/src/tier-engine/i18n.ts +8 -0
  571. package/src/tier-engine/web/tier-admin-screen.tsx +26 -19
  572. package/src/user/__tests__/stream-tenant-backfill.integration.test.ts +186 -0
  573. package/src/user/__tests__/user.integration.test.ts +26 -0
  574. package/src/user/constants.ts +1 -0
  575. package/src/user/db/queries/stream-tenant-backfill.ts +112 -0
  576. package/src/user/feature.ts +3 -0
  577. package/src/user/handlers/find-for-auth.query.ts +8 -6
  578. package/src/user/i18n.ts +20 -0
  579. package/src/user/index.ts +4 -0
  580. package/src/user/schema/user.ts +6 -2
  581. package/src/user/screens.ts +3 -15
  582. package/src/user/seeding.ts +8 -0
  583. package/src/user-data-rights/COMPLIANCE.md +21 -1
  584. package/src/user-data-rights/__tests__/anonymous-deletion-kms.integration.test.ts +145 -0
  585. package/src/user-data-rights/__tests__/anonymous-deletion.integration.test.ts +3 -0
  586. package/src/user-data-rights/__tests__/audit-log.integration.test.ts +67 -0
  587. package/src/user-data-rights/__tests__/boot-checks.test.ts +170 -0
  588. package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +4 -1
  589. package/src/user-data-rights/__tests__/download-by-token-resolver-trust.integration.test.ts +169 -0
  590. package/src/user-data-rights/__tests__/download.integration.test.ts +101 -33
  591. package/src/user-data-rights/__tests__/email-templates.test.ts +15 -0
  592. package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +154 -0
  593. package/src/user-data-rights/__tests__/export-job-idempotency.integration.test.ts +2 -0
  594. package/src/user-data-rights/__tests__/export-job-list-cross-tenant.integration.test.ts +2 -0
  595. package/src/user-data-rights/__tests__/file-binary-forget-cleanup.integration.test.ts +4 -1
  596. package/src/user-data-rights/__tests__/file-binary-forget-failure.integration.test.ts +4 -1
  597. package/src/user-data-rights/__tests__/file-retention.integration.test.ts +4 -1
  598. package/src/user-data-rights/__tests__/file-storage-unification.integration.test.ts +4 -1
  599. package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +41 -62
  600. package/src/user-data-rights/__tests__/forget-hook-registry.integration.test.ts +110 -0
  601. package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +14 -0
  602. package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +19 -2
  603. package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +25 -14
  604. package/src/user-data-rights/__tests__/request-cancel-deletion.integration.test.ts +2 -0
  605. package/src/user-data-rights/__tests__/request-deletion-callback.integration.test.ts +4 -1
  606. package/src/user-data-rights/__tests__/request-export.integration.test.ts +2 -0
  607. package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +172 -19
  608. package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +2 -0
  609. package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +257 -26
  610. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +339 -5
  611. package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +77 -1
  612. package/src/user-data-rights/__tests__/tenant-model-erasure.integration.test.ts +2 -0
  613. package/src/user-data-rights/__tests__/user-data-rights.integration.test.ts +9 -1
  614. package/src/user-data-rights/boot-checks.ts +70 -0
  615. package/src/user-data-rights/db/queries/__tests__/forget-cleanup.integration.test.ts +69 -0
  616. package/src/user-data-rights/db/queries/forget-cleanup.ts +9 -2
  617. package/src/user-data-rights/email-templates.ts +6 -6
  618. package/src/user-data-rights/feature.ts +162 -43
  619. package/src/user-data-rights/handlers/cancel-deletion.write.ts +8 -11
  620. package/src/user-data-rights/handlers/deletion-grace-period.ts +4 -1
  621. package/src/user-data-rights/handlers/download-by-job.query.ts +26 -18
  622. package/src/user-data-rights/handlers/download-by-token.query.ts +23 -15
  623. package/src/user-data-rights/handlers/lift-restriction.write.ts +44 -19
  624. package/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -0
  625. package/src/user-data-rights/handlers/my-audit-log.query.ts +1 -0
  626. package/src/user-data-rights/handlers/request-deletion-by-email.write.ts +3 -1
  627. package/src/user-data-rights/handlers/restrict-account.write.ts +58 -7
  628. package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +2 -0
  629. package/src/user-data-rights/i18n.ts +35 -32
  630. package/src/user-data-rights/lib/is-admin-actor.ts +19 -0
  631. package/src/user-data-rights/lib/storage-provider-resolver.ts +6 -5
  632. package/src/user-data-rights/lib/update-user-lifecycle.ts +8 -24
  633. package/src/user-data-rights/run-export-jobs.ts +55 -19
  634. package/src/user-data-rights/run-forget-cleanup.ts +112 -7
  635. package/src/user-data-rights/run-user-export.ts +70 -4
  636. package/src/user-data-rights/schema/download-token.ts +5 -3
  637. package/src/user-data-rights/schema/export-job.ts +2 -0
  638. package/src/user-data-rights/screens.ts +2 -1
  639. package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +1 -0
  640. package/src/user-data-rights/web/__tests__/public-deletion-gate.test.tsx +7 -1
  641. package/src/user-data-rights/web/client-plugin.tsx +6 -1
  642. package/src/user-data-rights/web/i18n.ts +30 -10
  643. package/src/user-data-rights/web/privacy-center-screen.tsx +5 -3
  644. package/src/user-data-rights-defaults/__tests__/bundled-entities.userdata-hooks.integration.test.ts +455 -0
  645. package/src/user-data-rights-defaults/__tests__/feature-mounted.test.ts +24 -0
  646. package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +33 -9
  647. package/src/user-data-rights-defaults/feature.ts +78 -1
  648. package/src/user-data-rights-defaults/hooks/api-token.userdata-hook.ts +37 -0
  649. package/src/user-data-rights-defaults/hooks/config-value.userdata-hook.ts +51 -0
  650. package/src/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.ts +37 -0
  651. package/src/user-data-rights-defaults/hooks/feature-mounted.ts +10 -0
  652. package/src/user-data-rights-defaults/hooks/in-app-message.userdata-hook.ts +38 -0
  653. package/src/user-data-rights-defaults/hooks/job-run.userdata-hook.ts +35 -0
  654. package/src/user-data-rights-defaults/hooks/notification-preference.userdata-hook.ts +54 -0
  655. package/src/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.ts +112 -0
  656. package/src/user-data-rights-defaults/hooks/user-session.userdata-hook.ts +42 -0
  657. package/src/user-data-rights-defaults/index.ts +26 -0
  658. package/src/user-profile/__tests__/change-email.integration.test.ts +7 -1
  659. package/src/user-profile/__tests__/profile-screen.test.tsx +5 -0
  660. package/src/user-profile/feature.ts +4 -1
  661. package/src/user-profile/handlers/change-email.write.ts +15 -10
  662. package/src/user-profile/web/profile-screen.tsx +15 -3
  663. package/src/auth-email-password/stream-tenant.ts +0 -31
  664. package/src/feature-toggles/db/queries/toggle-state.ts +0 -25
  665. package/src/legal-pages/security-headers.ts +0 -1
  666. package/src/sessions/schema/index.ts +0 -5
  667. package/src/tenant/schema/index.ts +0 -5
  668. package/src/user/schema/index.ts +0 -5
  669. /package/src/{auth-email-password → shared}/identity-v3-hash.ts +0 -0
@@ -0,0 +1,358 @@
1
+ // Always-on unit coverage for imapInboundMailPlugin.verify/fetch/watch.
2
+ // Greenmail suites skip in CI when the container is down — this file mocks
3
+ // imapflow so the plugin body stays on the coverage badge without Docker.
4
+
5
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
6
+ import { EventEmitter } from "node:events";
7
+ import { createSecret } from "@cosmicdrift/kumiko-framework/secrets";
8
+ import {
9
+ type InboundMailContext,
10
+ isInboundAuthError,
11
+ isInboundCursorInvalidError,
12
+ isInboundTransientError,
13
+ type MailAccountRecord,
14
+ type RawInboundMessage,
15
+ } from "../../inbound-mail-foundation";
16
+
17
+ type FakeMsg = {
18
+ readonly uid: number;
19
+ readonly source: Buffer;
20
+ readonly internalDate: Date;
21
+ };
22
+
23
+ type FakeState = {
24
+ connectError?: Error;
25
+ authFailed?: boolean;
26
+ uidValidity: string;
27
+ uidNext: number;
28
+ messages: FakeMsg[];
29
+ /** When set, search() returns this list instead of all message uids. */
30
+ searchUids?: number[];
31
+ };
32
+
33
+ let state: FakeState;
34
+ let lastIdleClient: FakeImapFlow | undefined;
35
+
36
+ class FakeImapFlow extends EventEmitter {
37
+ mailbox: { uidValidity: bigint; uidNext: number } | false = false;
38
+ private idleReject: ((err: Error) => void) | undefined;
39
+
40
+ constructor(_opts: unknown) {
41
+ super();
42
+ lastIdleClient = this;
43
+ }
44
+
45
+ async connect(): Promise<void> {
46
+ if (state.connectError) {
47
+ const err = state.connectError as Error & { authenticationFailed?: boolean };
48
+ if (state.authFailed) err.authenticationFailed = true;
49
+ throw err;
50
+ }
51
+ }
52
+
53
+ async logout(): Promise<void> {
54
+ this.idleReject?.(new Error("connection closed"));
55
+ this.idleReject = undefined;
56
+ }
57
+
58
+ async getMailboxLock(_box: string): Promise<{ release: () => void }> {
59
+ this.mailbox = {
60
+ uidValidity: BigInt(state.uidValidity),
61
+ uidNext: state.uidNext,
62
+ };
63
+ return { release: () => {} };
64
+ }
65
+
66
+ async mailboxOpen(_box: string): Promise<{ uidValidity: bigint; uidNext: number }> {
67
+ this.mailbox = {
68
+ uidValidity: BigInt(state.uidValidity),
69
+ uidNext: state.uidNext,
70
+ };
71
+ return this.mailbox;
72
+ }
73
+
74
+ async search(_query: unknown, _opts?: unknown): Promise<number[]> {
75
+ return state.searchUids ?? state.messages.map((m) => m.uid);
76
+ }
77
+
78
+ async *fetch(
79
+ range: string,
80
+ _opts?: unknown,
81
+ _uidOpts?: unknown,
82
+ ): AsyncGenerator<{
83
+ uid: number;
84
+ source: Buffer;
85
+ internalDate: Date;
86
+ }> {
87
+ // Real imapflow is called with either "N,M,…" (explicit uid list) or "N:*".
88
+ const wanted = new Set<number>();
89
+ if (range.includes("*")) {
90
+ const start = Number(range.split(":")[0]);
91
+ for (const m of state.messages) {
92
+ if (m.uid >= start) wanted.add(m.uid);
93
+ }
94
+ } else {
95
+ for (const part of range.split(",")) {
96
+ const n = Number(part.trim());
97
+ if (Number.isFinite(n)) wanted.add(n);
98
+ }
99
+ }
100
+ for (const m of state.messages) {
101
+ if (wanted.size > 0 && !wanted.has(m.uid)) continue;
102
+ yield { uid: m.uid, source: m.source, internalDate: m.internalDate };
103
+ }
104
+ }
105
+
106
+ async idle(): Promise<void> {
107
+ await new Promise<void>((_resolve, reject) => {
108
+ this.idleReject = reject;
109
+ });
110
+ }
111
+ }
112
+
113
+ // Restore hygiene: Bun runs a test-run's files in one process, so a
114
+ // top-level mock.module leaks into every file that runs after this one. The
115
+ // real module is captured before mocking so afterAll can put it back — a
116
+ // later file that needs the real imapflow (or a filtered/reordered run via
117
+ // `-t`) would otherwise silently see FakeImapFlow instead.
118
+ const realImapflow = await import("imapflow");
119
+ mock.module("imapflow", () => ({ ImapFlow: FakeImapFlow }));
120
+
121
+ const { imapInboundMailPlugin } = await import("../feature");
122
+
123
+ afterAll(() => {
124
+ mock.module("imapflow", () => realImapflow);
125
+ });
126
+
127
+ function mime(subject: string, text: string): Buffer {
128
+ return Buffer.from(
129
+ [
130
+ "From: Sender <sender@example.com>",
131
+ "To: inbox@example.com",
132
+ `Subject: ${subject}`,
133
+ `Message-ID: <${subject}-@example.com>`,
134
+ "Date: Mon, 01 Jan 2024 12:00:00 +0000",
135
+ "MIME-Version: 1.0",
136
+ "Content-Type: text/plain; charset=utf-8",
137
+ "",
138
+ text,
139
+ ].join("\r\n"),
140
+ );
141
+ }
142
+
143
+ const account: MailAccountRecord = {
144
+ id: "00000000-0000-4000-8000-000000001a01",
145
+ tenantId: "00000000-0000-4000-8000-000000004242",
146
+ provider: "imap",
147
+ authMethod: "password",
148
+ ownerUserId: null,
149
+ address: "inbox@example.com",
150
+ displayName: "Mock",
151
+ status: "active",
152
+ watchState: "idle",
153
+ };
154
+
155
+ async function waitFor(predicate: () => boolean, timeoutMs = 2000): Promise<void> {
156
+ const t0 = Date.now();
157
+ while (!predicate() && Date.now() - t0 < timeoutMs) {
158
+ await Bun.sleep(5);
159
+ }
160
+ }
161
+
162
+ function ctxWithDoc(doc: string | null): InboundMailContext {
163
+ return {
164
+ _userId: "imap-plugin-mocked",
165
+ secrets: {
166
+ get: async () => (doc === null ? null : createSecret(doc)),
167
+ has: async () => doc !== null,
168
+ set: async () => {},
169
+ delete: async () => {},
170
+ } as unknown as import("@cosmicdrift/kumiko-framework/secrets").SecretsContext, // @cast-boundary test-double
171
+ };
172
+ }
173
+
174
+ const goodDoc = JSON.stringify({
175
+ host: "imap.example.com",
176
+ port: 993,
177
+ secure: true,
178
+ user: "u@example.com",
179
+ password: "pw",
180
+ });
181
+
182
+ beforeEach(() => {
183
+ lastIdleClient = undefined;
184
+ state = {
185
+ uidValidity: "17",
186
+ uidNext: 3,
187
+ messages: [
188
+ {
189
+ uid: 1,
190
+ source: mime("one", "hello one"),
191
+ internalDate: new Date("2024-01-01T12:00:00Z"),
192
+ },
193
+ {
194
+ uid: 2,
195
+ source: mime("two", "hello two"),
196
+ internalDate: new Date("2024-01-02T12:00:00Z"),
197
+ },
198
+ ],
199
+ };
200
+ });
201
+
202
+ describe("imapInboundMailPlugin — mocked imapflow", () => {
203
+ test("verify: connect+logout happy path", async () => {
204
+ await expect(
205
+ imapInboundMailPlugin.verify(ctxWithDoc(goodDoc), account),
206
+ ).resolves.toBeUndefined();
207
+ });
208
+
209
+ test("verify: auth failure → InboundAuthError", async () => {
210
+ state.connectError = new Error("Authentication failed");
211
+ state.authFailed = true;
212
+ try {
213
+ await imapInboundMailPlugin.verify(ctxWithDoc(goodDoc), account);
214
+ expect.unreachable("expected verify to throw");
215
+ } catch (e) {
216
+ expect(isInboundAuthError(e)).toBe(true);
217
+ }
218
+ });
219
+
220
+ test("verify: missing credential secret → InboundAuthError", async () => {
221
+ try {
222
+ await imapInboundMailPlugin.verify(ctxWithDoc(null), account);
223
+ expect.unreachable("expected verify to throw");
224
+ } catch (e) {
225
+ expect(isInboundAuthError(e)).toBe(true);
226
+ }
227
+ });
228
+
229
+ test("verify: malformed credential JSON → InboundAuthError", async () => {
230
+ try {
231
+ await imapInboundMailPlugin.verify(ctxWithDoc("not-json"), account);
232
+ expect.unreachable("expected verify to throw");
233
+ } catch (e) {
234
+ expect(isInboundAuthError(e)).toBe(true);
235
+ }
236
+ });
237
+
238
+ test("fetch: backfill (null cursor) returns messages + nextCursor", async () => {
239
+ const result = await imapInboundMailPlugin.fetch(ctxWithDoc(goodDoc), account, null, {
240
+ backfillWindowDays: 7,
241
+ maxMessages: 50,
242
+ });
243
+ expect(result.messages).toHaveLength(2);
244
+ expect(result.messages[0]?.subject).toBe("one");
245
+ expect(result.nextCursor).toEqual({ uidValidity: "17", lastUid: 2 });
246
+ expect(result.hasMore).toBe(false);
247
+ // Fixtures must carry distinct Message-ID headers — colliding fixtures
248
+ // would mask a mapping bug where toRawInboundMessage produces a
249
+ // wrong/constant messageIdHeader per batch (used for the References/
250
+ // threading chain and the self-heal dedup fallback).
251
+ expect(new Set(result.messages.map((m) => m.messageIdHeader)).size).toBe(2);
252
+ });
253
+
254
+ test("fetch: incremental cursor filters uid <= lastUid + hasMore via maxMessages", async () => {
255
+ state.searchUids = [1, 2, 3, 4];
256
+ state.messages = [
257
+ {
258
+ uid: 2,
259
+ source: mime("two", "x"),
260
+ internalDate: new Date("2024-01-02T12:00:00Z"),
261
+ },
262
+ {
263
+ uid: 3,
264
+ source: mime("three", "y"),
265
+ internalDate: new Date("2024-01-03T12:00:00Z"),
266
+ },
267
+ {
268
+ uid: 4,
269
+ source: mime("four", "z"),
270
+ internalDate: new Date("2024-01-04T12:00:00Z"),
271
+ },
272
+ ];
273
+ const result = await imapInboundMailPlugin.fetch(
274
+ ctxWithDoc(goodDoc),
275
+ account,
276
+ { uidValidity: "17", lastUid: 1 },
277
+ { backfillWindowDays: 7, maxMessages: 2 },
278
+ );
279
+ // search returns 2,3,4 (> lastUid=1); maxMessages=2 → hasMore, uid 1
280
+ // filtered out and only the first 2 of the 3 remaining survive the cap.
281
+ expect(result.messages.map((m) => m.subject)).toEqual(["two", "three"]);
282
+ expect(result.hasMore).toBe(true);
283
+ expect(result.nextCursor).toEqual({ uidValidity: "17", lastUid: 3 });
284
+ });
285
+
286
+ test("fetch: UIDVALIDITY change → InboundCursorInvalidError", async () => {
287
+ try {
288
+ await imapInboundMailPlugin.fetch(
289
+ ctxWithDoc(goodDoc),
290
+ account,
291
+ { uidValidity: "16", lastUid: 1 },
292
+ { backfillWindowDays: 7, maxMessages: 50 },
293
+ );
294
+ expect.unreachable("expected fetch to throw");
295
+ } catch (e) {
296
+ expect(isInboundCursorInvalidError(e)).toBe(true);
297
+ }
298
+ });
299
+
300
+ test("fetch: network error → InboundTransientError", async () => {
301
+ state.connectError = new Error("ECONNREFUSED");
302
+ try {
303
+ await imapInboundMailPlugin.fetch(ctxWithDoc(goodDoc), account, null, {
304
+ backfillWindowDays: 7,
305
+ maxMessages: 50,
306
+ });
307
+ expect.unreachable("expected fetch to throw");
308
+ } catch (e) {
309
+ expect(isInboundTransientError(e)).toBe(true);
310
+ }
311
+ });
312
+
313
+ test("watch: exists → onMessages, stop() tears down", async () => {
314
+ expect(imapInboundMailPlugin.watch).toBeDefined();
315
+ const received: RawInboundMessage[][] = [];
316
+ const stop = await imapInboundMailPlugin.watch!(ctxWithDoc(goodDoc), account, {
317
+ onMessages: async (msgs) => {
318
+ received.push([...msgs]);
319
+ },
320
+ onError: () => {},
321
+ });
322
+
323
+ // New mail above uidNext-1 (uidNext=3 → nextUid starts at 3)
324
+ state.messages = [
325
+ {
326
+ uid: 3,
327
+ source: mime("pushed", "idle body"),
328
+ internalDate: new Date("2024-01-05T12:00:00Z"),
329
+ },
330
+ ];
331
+ lastIdleClient?.emit("exists");
332
+
333
+ // drainNew() does MIME-parsing before onMessages fires — a fixed sleep
334
+ // flakes on a loaded CI runner; poll instead.
335
+ await waitFor(() => received.some((batch) => batch.some((m) => m.subject === "pushed")));
336
+ expect(received.some((batch) => batch.some((m) => m.subject === "pushed"))).toBe(true);
337
+
338
+ await stop();
339
+ });
340
+
341
+ test("watch: client error → onError once", async () => {
342
+ let errors = 0;
343
+ const stop = await imapInboundMailPlugin.watch!(ctxWithDoc(goodDoc), account, {
344
+ onMessages: async () => {},
345
+ onError: () => {
346
+ errors += 1;
347
+ },
348
+ });
349
+ lastIdleClient?.emit("error", new Error("socket hang up"));
350
+ await waitFor(() => errors >= 1);
351
+ lastIdleClient?.emit("error", new Error("second"));
352
+ // Confirms onError stays unsubscribed after the first error — polls the
353
+ // same bounded window rather than betting on a fixed sleep outlasting it.
354
+ await waitFor(() => errors >= 2, 100);
355
+ expect(errors).toBe(1);
356
+ await stop().catch(() => {});
357
+ });
358
+ });
@@ -0,0 +1,51 @@
1
+ // Connection-Dokument im per-Account-Secret-Slot
2
+ // (inboundCredentialSecretKey(accountId), Plan §4).
3
+ //
4
+ // **Warum JSON im Secret statt Tenant-Config-Keys:** anders als
5
+ // mail-transport-smtp (EIN SMTP pro Tenant) hat Inbound MEHRERE
6
+ // Accounts pro Tenant mit verschiedenen Hosts — per-Tenant-Config-Keys
7
+ // können das nicht tragen. Host/Port/User sind zusammen mit dem
8
+ // Passwort ein zusammengehöriges Verbindungs-Dokument; der Slot ist
9
+ // ohnehin encrypted-at-rest.
10
+ //
11
+ // authMethod des Accounts bestimmt die Interpretation:
12
+ // password → { host, port, secure, user, password }
13
+ // xoauth2 → { host, port, secure, user, accessToken } — V1-Fallback:
14
+ // Token-Beschaffung/-Refresh gehört den OAuth-Providern
15
+ // (M365/Gmail, Phase 4/5); der imap-Provider konsumiert
16
+ // nur einen gültigen Access-Token aus dem Slot.
17
+
18
+ import { z } from "zod";
19
+
20
+ export const imapCredentialDocumentSchema = z.object({
21
+ host: z.string().min(1).max(255),
22
+ port: z.number().int().min(1).max(65535).default(993),
23
+ secure: z.boolean().default(true),
24
+ user: z.string().min(1).max(320),
25
+ password: z.string().min(1).optional(),
26
+ accessToken: z.string().min(1).optional(),
27
+ });
28
+ export type ImapCredentialDocument = z.infer<typeof imapCredentialDocumentSchema>;
29
+
30
+ export type ImapCredentialRejection = "not_json" | "invalid_schema" | "missing_credentials";
31
+
32
+ export type ParseCredentialResult =
33
+ | { readonly ok: true; readonly doc: ImapCredentialDocument }
34
+ | { readonly ok: false; readonly reason: ImapCredentialRejection; readonly detail?: string };
35
+
36
+ export function parseImapCredentialDocument(raw: string): ParseCredentialResult {
37
+ let json: unknown;
38
+ try {
39
+ json = JSON.parse(raw);
40
+ } catch {
41
+ return { ok: false, reason: "not_json" };
42
+ }
43
+ const parsed = imapCredentialDocumentSchema.safeParse(json);
44
+ if (!parsed.success) {
45
+ return { ok: false, reason: "invalid_schema", detail: parsed.error.message };
46
+ }
47
+ if (!parsed.data.password && !parsed.data.accessToken) {
48
+ return { ok: false, reason: "missing_credentials", detail: "needs password or accessToken" };
49
+ }
50
+ return { ok: true, doc: parsed.data };
51
+ }
@@ -0,0 +1,283 @@
1
+ // kumiko-feature-version: 1
2
+ //
3
+ // inbound-provider-imap — IMAP-Implementierung der inbound-mail-
4
+ // foundation Plugin-API. Deckt den DACH-Long-Tail (IONOS, Telekom,
5
+ // Strato, GMX/Web.de, mailbox.org, Hetzner, all-inkl) via Passwort/
6
+ // App-Passwort ab — komplett OHNE OAuth. XOAUTH2 als Fallback für
7
+ // M365-/Gmail-Konten (Access-Token aus dem Secret-Slot; Token-Refresh
8
+ // gehört den OAuth-Providern, Phase 4/5).
9
+ //
10
+ // **Was diese Feature liefert:**
11
+ // 1. Plugin-Registration via r.useExtension("inboundMailProvider",
12
+ // "imap", { verify, fetch, watch }).
13
+ // 2. verify: connect+logout mit typisiertem Fehler-Mapping.
14
+ // 3. fetch: inkrementeller Sync über UIDVALIDITY:lastUid-Cursor —
15
+ // UIDVALIDITY-Wechsel wirft InboundCursorInvalidError (Foundation
16
+ // resettet + resynct im Backfill-Fenster). Erst-Befüllung via
17
+ // SEARCH since=backfillWindowDays.
18
+ // 4. watch: IMAP IDLE via imapflow — 'exists'-Event → neue UIDs
19
+ // fetchen → onMessages; Verbindungs-Tod → onError (Supervisor
20
+ // restartet mit Backoff, der Provider reconnected NICHT selbst).
21
+ //
22
+ // Credentials: JSON-Dokument im per-Account-Secret-Slot
23
+ // (credential-document.ts) — kein Tenant-Config, weil mehrere Accounts
24
+ // pro Tenant verschiedene Hosts haben.
25
+
26
+ import {
27
+ INBOUND_MAIL_PROVIDER_EXTENSION,
28
+ InboundAuthError,
29
+ type InboundFetchResult,
30
+ type InboundMailContext,
31
+ type InboundMailProviderPlugin,
32
+ inboundCredentialSecretKey,
33
+ type MailAccountRecord,
34
+ type RawInboundMessage,
35
+ } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
36
+ import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
37
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
38
+ import { instantToLegacyDate } from "@cosmicdrift/kumiko-framework/time";
39
+ import type { ImapFlow, MailboxObject } from "imapflow";
40
+ import { Temporal } from "temporal-polyfill";
41
+ import { type ImapCredentialDocument, parseImapCredentialDocument } from "./credential-document";
42
+ import {
43
+ assertUidValidity,
44
+ coerceDate,
45
+ createImapClient,
46
+ IMAP_MAILBOX,
47
+ mapImapError,
48
+ parseImapCursor,
49
+ toRawInboundMessage,
50
+ } from "./imap-client";
51
+
52
+ const FEATURE_NAME = "inbound-provider-imap";
53
+ export const IMAP_PROVIDER_KEY = "imap";
54
+
55
+ // =============================================================================
56
+ // Credential-Read — per-Account-Slot, Worker-tauglich (slim ctx).
57
+ // =============================================================================
58
+
59
+ async function readCredentialDocument(
60
+ ctx: InboundMailContext,
61
+ account: MailAccountRecord,
62
+ ): Promise<ImapCredentialDocument> {
63
+ const secrets = requireSecretsContext(ctx, FEATURE_NAME);
64
+ const secret = await secrets.get(account.tenantId, inboundCredentialSecretKey(account.id));
65
+ if (!secret) {
66
+ throw new InboundAuthError(
67
+ `${FEATURE_NAME}: no credential document in secret slot for account ${account.id} — ` +
68
+ `store the IMAP connection JSON under ${inboundCredentialSecretKey(account.id)} first`,
69
+ );
70
+ }
71
+ const parsed = parseImapCredentialDocument(secret.reveal());
72
+ if (!parsed.ok) {
73
+ throw new InboundAuthError(
74
+ `${FEATURE_NAME}: account ${account.id}: ${parsed.reason}${parsed.detail ? `: ${parsed.detail}` : ""}`,
75
+ );
76
+ }
77
+ return parsed.doc;
78
+ }
79
+
80
+ // =============================================================================
81
+ // fetch — inkrementeller UID-Sync
82
+ // =============================================================================
83
+
84
+ async function fetchMessages(
85
+ ctx: InboundMailContext,
86
+ account: MailAccountRecord,
87
+ cursor: Parameters<InboundMailProviderPlugin["fetch"]>[2],
88
+ opts: { readonly backfillWindowDays: number; readonly maxMessages: number },
89
+ ): Promise<InboundFetchResult> {
90
+ const doc = await readCredentialDocument(ctx, account);
91
+ const client = createImapClient(doc);
92
+ let lock: Awaited<ReturnType<typeof client.getMailboxLock>> | undefined;
93
+ try {
94
+ await client.connect();
95
+ lock = await client.getMailboxLock(IMAP_MAILBOX);
96
+ const mailbox = client.mailbox as MailboxObject;
97
+ const serverUidValidity = String(mailbox.uidValidity);
98
+ const parsedCursor = parseImapCursor(cursor);
99
+ assertUidValidity(parsedCursor, serverUidValidity);
100
+
101
+ // UID-Menge bestimmen: inkrementell ab lastUid+1, sonst Backfill
102
+ // über SEARCH since (Fenster begrenzt die Erst-Befüllung).
103
+ let uids: number[];
104
+ if (parsedCursor) {
105
+ const found = await client.search({ uid: `${parsedCursor.lastUid + 1}:*` }, { uid: true });
106
+ // `N:*`-Gotcha: liegt N über der höchsten UID, liefert IMAP
107
+ // trotzdem die letzte Message — rausfiltern.
108
+ uids = (found || []).filter((uid) => uid > parsedCursor.lastUid);
109
+ } else {
110
+ const since = Temporal.Now.instant().subtract({ hours: opts.backfillWindowDays * 24 });
111
+ // imapflow-API-Boundary: search() verlangt ein JS-Date.
112
+ uids = (await client.search({ since: instantToLegacyDate(since) }, { uid: true })) || [];
113
+ }
114
+ uids.sort((a, b) => a - b);
115
+
116
+ const batch = uids.slice(0, opts.maxMessages);
117
+ const hasMore = uids.length > batch.length;
118
+ const messages: RawInboundMessage[] = [];
119
+ let maxUid = parsedCursor?.lastUid ?? 0;
120
+
121
+ if (batch.length > 0) {
122
+ for await (const msg of client.fetch(
123
+ batch.join(","),
124
+ { uid: true, internalDate: true, source: true },
125
+ { uid: true },
126
+ )) {
127
+ if (!msg.source) continue;
128
+ messages.push(
129
+ await toRawInboundMessage({
130
+ source: msg.source,
131
+ uid: msg.uid,
132
+ uidValidity: serverUidValidity,
133
+ internalDate: coerceDate(msg.internalDate ?? undefined),
134
+ }),
135
+ );
136
+ if (msg.uid > maxUid) maxUid = msg.uid;
137
+ }
138
+ }
139
+
140
+ return {
141
+ messages,
142
+ nextCursor: { uidValidity: serverUidValidity, lastUid: maxUid },
143
+ hasMore,
144
+ };
145
+ } catch (err) {
146
+ throw err instanceof Error && "kind" in err ? err : mapImapError(err, doc.host);
147
+ } finally {
148
+ lock?.release();
149
+ await client.logout().catch(() => {});
150
+ }
151
+ }
152
+
153
+ // =============================================================================
154
+ // watch — IMAP IDLE (imapflow idlet automatisch bei offener Mailbox und
155
+ // emittet 'exists' bei neuen Messages).
156
+ // =============================================================================
157
+
158
+ async function watchMailbox(
159
+ ctx: InboundMailContext,
160
+ account: MailAccountRecord,
161
+ handlers: {
162
+ readonly onMessages: (msgs: readonly RawInboundMessage[]) => Promise<void>;
163
+ readonly onError: (err: unknown) => void;
164
+ },
165
+ ): Promise<() => Promise<void>> {
166
+ const doc = await readCredentialDocument(ctx, account);
167
+ const client: ImapFlow = createImapClient(doc);
168
+ let stopped = false;
169
+
170
+ try {
171
+ await client.connect();
172
+ const mailbox = (await client.mailboxOpen(IMAP_MAILBOX)) as MailboxObject;
173
+ const uidValidity = String(mailbox.uidValidity);
174
+ // uidNext = nächste zu vergebende UID — alles ab hier ist "neu".
175
+ let nextUid = Number(mailbox.uidNext ?? 1);
176
+
177
+ const drainNew = async () => {
178
+ const messages: RawInboundMessage[] = [];
179
+ let maxSeen = nextUid - 1;
180
+ for await (const msg of client.fetch(
181
+ `${nextUid}:*`,
182
+ { uid: true, internalDate: true, source: true },
183
+ { uid: true },
184
+ )) {
185
+ // `N:*`-Gotcha (siehe fetch): alte letzte Message rausfiltern.
186
+ if (msg.uid < nextUid || !msg.source) continue;
187
+ messages.push(
188
+ await toRawInboundMessage({
189
+ source: msg.source,
190
+ uid: msg.uid,
191
+ uidValidity,
192
+ internalDate: coerceDate(msg.internalDate ?? undefined),
193
+ }),
194
+ );
195
+ if (msg.uid > maxSeen) maxSeen = msg.uid;
196
+ }
197
+ nextUid = maxSeen + 1;
198
+ if (messages.length > 0) await handlers.onMessages(messages);
199
+ };
200
+
201
+ client.on("exists", () => {
202
+ // skip: Watcher gestoppt — exists-Nachzügler ignorieren.
203
+ if (stopped) return;
204
+ void drainNew().catch((err) => {
205
+ if (!stopped) handlers.onError(mapImapError(err, doc.host));
206
+ });
207
+ });
208
+ const die = (err: unknown) => {
209
+ // skip: bereits gestoppt/gestorben — onError nur einmal feuern.
210
+ if (stopped) return;
211
+ stopped = true;
212
+ handlers.onError(mapImapError(err, doc.host));
213
+ };
214
+ client.on("error", die);
215
+ client.on("close", () => die(new Error("connection closed")));
216
+
217
+ // IDLE-Loop: 'exists' feuert NUR während client.idle() läuft
218
+ // (empirisch gegen greenmail verifiziert). Jedes zwischengeschobene
219
+ // Kommando (drainNew-fetch) beendet idle() — die Loop re-idlet,
220
+ // bis stop() die Verbindung schließt.
221
+ void (async () => {
222
+ while (!stopped) {
223
+ try {
224
+ await client.idle();
225
+ } catch (err) {
226
+ if (!stopped) die(err);
227
+ // skip: idle-Loop endet — die() hat den Supervisor informiert (bzw. stop() war die Ursache).
228
+ return;
229
+ }
230
+ }
231
+ })();
232
+
233
+ return async () => {
234
+ stopped = true;
235
+ client.removeAllListeners("exists");
236
+ client.removeAllListeners("error");
237
+ client.removeAllListeners("close");
238
+ await client.logout().catch(() => {});
239
+ };
240
+ } catch (err) {
241
+ await client.logout().catch(() => {});
242
+ throw mapImapError(err, doc.host);
243
+ }
244
+ }
245
+
246
+ // =============================================================================
247
+ // Plugin + Feature-definition
248
+ // =============================================================================
249
+
250
+ /** Exportiert für den Live-Test (imap-live.integration.test.ts) —
251
+ * Apps nutzen das Feature, nie das Plugin direkt. */
252
+ export const imapInboundMailPlugin: InboundMailProviderPlugin = {
253
+ verify: async (ctx, account) => {
254
+ const doc = await readCredentialDocument(ctx, account);
255
+ const client = createImapClient(doc);
256
+ try {
257
+ await client.connect();
258
+ } catch (err) {
259
+ throw mapImapError(err, doc.host);
260
+ } finally {
261
+ await client.logout().catch(() => {});
262
+ }
263
+ },
264
+ fetch: fetchMessages,
265
+ watch: watchMailbox,
266
+ // Kein oauth-Block: Passwort-Connect läuft über das Credential-
267
+ // Formular (connect-account + Secret-Write); XOAUTH2-Token-Flows
268
+ // gehören den OAuth-Providern (M365/Gmail).
269
+ };
270
+
271
+ export const inboundProviderImapFeature = defineFeature(FEATURE_NAME, (r) => {
272
+ r.describe(
273
+ 'Registers the `"imap"` provider for `inbound-mail-foundation` using imapflow — password/app-password auth covers classic IMAP hosts without any OAuth (plus an XOAUTH2 fallback consuming an access token from the secret slot). Incremental sync via a UIDVALIDITY:lastUid cursor (UIDVALIDITY change triggers a cursor-invalid full resync), initial backfill bounded by the foundation backfill window, and live push via IMAP IDLE for the watch supervisor. Store the per-account connection JSON (host/port/secure/user/password) in the account credential secret slot before the first sync.',
274
+ );
275
+ r.uiHints({
276
+ displayLabel: "Inbound Mail · IMAP",
277
+ category: "notifications",
278
+ recommended: false,
279
+ });
280
+ r.requires("inbound-mail-foundation");
281
+ r.requires("secrets");
282
+ r.useExtension(INBOUND_MAIL_PROVIDER_EXTENSION, IMAP_PROVIDER_KEY, imapInboundMailPlugin);
283
+ });