@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,433 @@
1
+ // Watch-supervisor integration — drives createInboundMailSupervisor against
2
+ // a real setupTestStack + inbound-provider-inmemory (fetch/watch/error
3
+ // injection). Proves poll ingest, IDLE watch push, auth_error quarantine,
4
+ // and cursor-invalid resync — not existence checks.
5
+
6
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
7
+ import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
8
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
9
+ import {
10
+ configurePiiSubjectKms,
11
+ InMemoryKmsAdapter,
12
+ resetPiiSubjectKmsForTests,
13
+ } from "@cosmicdrift/kumiko-framework/crypto";
14
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
15
+ import { createSystemUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
16
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
17
+ import {
18
+ createTestUser,
19
+ setupTestStack,
20
+ type TestStack,
21
+ testTenantId,
22
+ unsafeCreateEntityTable,
23
+ } from "@cosmicdrift/kumiko-framework/stack";
24
+ import { waitFor } from "@cosmicdrift/kumiko-framework/testing";
25
+ import {
26
+ createComplianceProfilesFeature,
27
+ tenantComplianceProfileEntity,
28
+ } from "../../compliance-profiles";
29
+ import { createConfigFeature } from "../../config";
30
+ import { inboundProviderInMemoryFeature } from "../../inbound-provider-inmemory";
31
+ import {
32
+ emitWatchError,
33
+ failNextFetchWith,
34
+ isWatching,
35
+ resetInboundInMemory,
36
+ seedInboundMessage,
37
+ } from "../../inbound-provider-inmemory/feature";
38
+ import { createTenantFeature } from "../../tenant/feature";
39
+ import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
40
+ import {
41
+ createInboundMailSupervisor,
42
+ InboundAuthError,
43
+ InboundCursorInvalidError,
44
+ InboundMailAccountStatuses,
45
+ InboundMailFoundationHandlers,
46
+ InboundRateLimitError,
47
+ inboundMailFoundationFeature,
48
+ inboundMessagesProjectionTable,
49
+ mailAccountsProjectionTable,
50
+ type RawInboundMessage,
51
+ seenMessageEntity,
52
+ syncCursorEntity,
53
+ } from "../index";
54
+
55
+ let stack: TestStack;
56
+ let db: DbConnection;
57
+
58
+ beforeAll(async () => {
59
+ stack = await setupTestStack({
60
+ features: [
61
+ createConfigFeature(),
62
+ createTenantFeature(),
63
+ createComplianceProfilesFeature(),
64
+ createTenantLifecycleFeature(),
65
+ inboundMailFoundationFeature,
66
+ inboundProviderInMemoryFeature,
67
+ ],
68
+ });
69
+ db = stack.db;
70
+ await createEventsTable(db);
71
+ await unsafeCreateEntityTable(db, tenantComplianceProfileEntity);
72
+ await unsafeCreateEntityTable(db, syncCursorEntity);
73
+ await unsafeCreateEntityTable(db, seenMessageEntity);
74
+ configurePiiSubjectKms(new InMemoryKmsAdapter());
75
+ });
76
+
77
+ afterAll(async () => {
78
+ await stack.cleanup();
79
+ resetPiiSubjectKmsForTests();
80
+ });
81
+
82
+ beforeEach(() => {
83
+ resetInboundInMemory();
84
+ });
85
+
86
+ afterEach(() => {
87
+ resetInboundInMemory();
88
+ });
89
+
90
+ function adminFor(tenantNumber: number) {
91
+ return createTestUser({
92
+ id: tenantNumber,
93
+ tenantId: testTenantId(tenantNumber),
94
+ roles: ["TenantAdmin", "SystemAdmin"],
95
+ });
96
+ }
97
+
98
+ function rawMsg(
99
+ overrides: Partial<RawInboundMessage> & { providerMessageId: string },
100
+ ): RawInboundMessage {
101
+ return {
102
+ messageIdHeader: `${overrides.providerMessageId}@example.com`,
103
+ providerThreadId: null,
104
+ references: [],
105
+ from: "sender@example.com",
106
+ to: ["inbox@tenant.example"],
107
+ cc: [],
108
+ subject: `Subject ${overrides.providerMessageId}`,
109
+ snippet: "snippet",
110
+ receivedAtIso: "2026-07-15T10:00:00Z",
111
+ rawMime: null,
112
+ scope: "inbox",
113
+ ...overrides,
114
+ };
115
+ }
116
+
117
+ async function connectSharedAccount(admin: ReturnType<typeof adminFor>): Promise<string> {
118
+ const result = (await stack.http.writeOk(
119
+ InboundMailFoundationHandlers.connectAccount,
120
+ {
121
+ provider: "inmemory",
122
+ authMethod: "password",
123
+ displayName: "Team-Inbox",
124
+ address: "inbox@tenant.example",
125
+ scope: "shared",
126
+ },
127
+ admin,
128
+ )) as { accountId: string };
129
+ return result.accountId;
130
+ }
131
+
132
+ function createSupervisor() {
133
+ return createInboundMailSupervisor({
134
+ providerCtx: { registry: stack.registry },
135
+ db,
136
+ dispatchWrite: ({ handlerQn, payload, tenantId }) =>
137
+ stack.dispatcher.write(
138
+ handlerQn,
139
+ payload,
140
+ createSystemUser(tenantId as TenantId, [ROLES.SystemAdmin]),
141
+ ),
142
+ // Keep the periodic tick far away so tests only exercise explicit
143
+ // pollOnce / watch push — not a racing background timer.
144
+ pollIntervalMs: 60_000,
145
+ watchBackoffInitialMs: 50,
146
+ watchBackoffMaxMs: 200,
147
+ });
148
+ }
149
+
150
+ describe("watch-supervisor — poll reconciliation", () => {
151
+ test("pollOnce fetches seeded messages and persists them via ingest-message", async () => {
152
+ const admin = adminFor(4101);
153
+ const accountId = await connectSharedAccount(admin);
154
+ await seedInboundMessage(accountId, rawMsg({ providerMessageId: "poll-uid-1" }));
155
+ await seedInboundMessage(accountId, rawMsg({ providerMessageId: "poll-uid-2" }));
156
+
157
+ const supervisor = createSupervisor();
158
+ await supervisor.pollOnce();
159
+
160
+ const rows = await selectMany(db, inboundMessagesProjectionTable, { accountId });
161
+ expect(rows).toHaveLength(2);
162
+ const ids = rows.map((r) => r["messageIdHeader"]).sort();
163
+ expect(ids).toEqual(["poll-uid-1@example.com", "poll-uid-2@example.com"]);
164
+ });
165
+
166
+ test("second pollOnce is idempotent — Dedup keeps a single row per providerMessageId", async () => {
167
+ const admin = adminFor(4102);
168
+ const accountId = await connectSharedAccount(admin);
169
+ await seedInboundMessage(accountId, rawMsg({ providerMessageId: "poll-dedup-1" }));
170
+
171
+ const supervisor = createSupervisor();
172
+ await supervisor.pollOnce();
173
+ await supervisor.pollOnce();
174
+
175
+ const rows = await selectMany(db, inboundMessagesProjectionTable, { accountId });
176
+ expect(rows).toHaveLength(1);
177
+ });
178
+ });
179
+
180
+ describe("watch-supervisor — IDLE watch path", () => {
181
+ test("start() arms watch; seedInboundMessage pushes and ingest lands a row", async () => {
182
+ const admin = adminFor(4103);
183
+ const accountId = await connectSharedAccount(admin);
184
+ const supervisor = createSupervisor();
185
+ try {
186
+ await supervisor.start();
187
+
188
+ await waitFor(() => {
189
+ expect(isWatching(accountId)).toBe(true);
190
+ });
191
+
192
+ await seedInboundMessage(
193
+ accountId,
194
+ rawMsg({ providerMessageId: "watch-uid-1", subject: "via watch" }),
195
+ );
196
+
197
+ await waitFor(async () => {
198
+ const rows = await selectMany(db, inboundMessagesProjectionTable, { accountId });
199
+ expect(rows).toHaveLength(1);
200
+ });
201
+
202
+ const rows = await selectMany(db, inboundMessagesProjectionTable, { accountId });
203
+ expect(rows[0]?.["messageIdHeader"]).toBe("watch-uid-1@example.com");
204
+ } finally {
205
+ await supervisor.stop();
206
+ }
207
+ expect(isWatching(accountId)).toBe(false);
208
+ });
209
+ });
210
+
211
+ describe("watch-supervisor — error semantics", () => {
212
+ test("InboundAuthError on fetch marks the account auth_error", async () => {
213
+ const admin = adminFor(4104);
214
+ const accountId = await connectSharedAccount(admin);
215
+ failNextFetchWith(accountId, new InboundAuthError("credentials rejected"));
216
+
217
+ const supervisor = createSupervisor();
218
+ await supervisor.pollOnce();
219
+
220
+ const accounts = await selectMany(db, mailAccountsProjectionTable, { id: accountId });
221
+ expect(accounts).toHaveLength(1);
222
+ expect(accounts[0]?.["status"]).toBe(InboundMailAccountStatuses.authError);
223
+ });
224
+
225
+ test("InboundAuthError on live watch stops the watcher and marks auth_error", async () => {
226
+ const admin = adminFor(4106);
227
+ const accountId = await connectSharedAccount(admin);
228
+ const supervisor = createSupervisor();
229
+ try {
230
+ await supervisor.start();
231
+ await waitFor(() => {
232
+ expect(isWatching(accountId)).toBe(true);
233
+ });
234
+
235
+ emitWatchError(accountId, new InboundAuthError("token revoked"));
236
+
237
+ await waitFor(async () => {
238
+ expect(isWatching(accountId)).toBe(false);
239
+ const accounts = await selectMany(db, mailAccountsProjectionTable, { id: accountId });
240
+ expect(accounts[0]?.["status"]).toBe(InboundMailAccountStatuses.authError);
241
+ });
242
+ } finally {
243
+ await supervisor.stop();
244
+ }
245
+ });
246
+
247
+ test("InboundCursorInvalidError resets the cursor; a later poll still ingests", async () => {
248
+ const admin = adminFor(4105);
249
+ const accountId = await connectSharedAccount(admin);
250
+ await seedInboundMessage(accountId, rawMsg({ providerMessageId: "cursor-ok-1" }));
251
+
252
+ const supervisor = createSupervisor();
253
+ await supervisor.pollOnce();
254
+ expect(await selectMany(db, inboundMessagesProjectionTable, { accountId })).toHaveLength(1);
255
+
256
+ failNextFetchWith(accountId, new InboundCursorInvalidError("uidValidity changed"));
257
+ await supervisor.pollOnce();
258
+
259
+ // Account stays active (cursor invalid ≠ auth failure).
260
+ const accounts = await selectMany(db, mailAccountsProjectionTable, { id: accountId });
261
+ expect(accounts[0]?.["status"]).toBe(InboundMailAccountStatuses.active);
262
+
263
+ await seedInboundMessage(accountId, rawMsg({ providerMessageId: "cursor-ok-2" }));
264
+ await supervisor.pollOnce();
265
+
266
+ const rows = await selectMany(db, inboundMessagesProjectionTable, { accountId });
267
+ expect(rows.map((r) => r["messageIdHeader"]).sort()).toEqual([
268
+ "cursor-ok-1@example.com",
269
+ "cursor-ok-2@example.com",
270
+ ]);
271
+ });
272
+
273
+ test("InboundRateLimitError on fetch keeps the account active for the next tick", async () => {
274
+ const admin = adminFor(4107);
275
+ const accountId = await connectSharedAccount(admin);
276
+ failNextFetchWith(accountId, new InboundRateLimitError("slow down", 30_000));
277
+
278
+ const supervisor = createSupervisor();
279
+ await supervisor.pollOnce();
280
+
281
+ const accounts = await selectMany(db, mailAccountsProjectionTable, { id: accountId });
282
+ expect(accounts[0]?.["status"]).toBe(InboundMailAccountStatuses.active);
283
+ });
284
+ });
285
+
286
+ describe("watch-supervisor — watch restart + ingest resilience", () => {
287
+ test("transient watch error schedules backoff restart and re-arms watch", async () => {
288
+ const admin = adminFor(4108);
289
+ const accountId = await connectSharedAccount(admin);
290
+ const supervisor = createSupervisor();
291
+ try {
292
+ await supervisor.start();
293
+ await waitFor(() => {
294
+ expect(isWatching(accountId)).toBe(true);
295
+ });
296
+
297
+ emitWatchError(accountId, new Error("socket dropped"));
298
+ // No assertion here: isWatching(accountId) flips to false as a side
299
+ // effect of emitWatchError itself (inbound-provider-inmemory's
300
+ // feature.ts sets state.watch = null before handlers.onError runs),
301
+ // and the restart re-arms fast enough that the persisted `backoff:`
302
+ // projection state isn't reliably observable between the two either
303
+ // — asserting either one here pins test-helper bookkeeping, not
304
+ // production behavior. The real assertions are below: the watch
305
+ // recovers, and the account stays active with a valid watchState.
306
+
307
+ await waitFor(() => {
308
+ expect(isWatching(accountId)).toBe(true);
309
+ });
310
+
311
+ const accounts = await selectMany(db, mailAccountsProjectionTable, { id: accountId });
312
+ expect(accounts[0]?.["status"]).toBe(InboundMailAccountStatuses.active);
313
+ expect(String(accounts[0]?.["watchState"])).toMatch(/^(watching|backoff:)/);
314
+ } finally {
315
+ await supervisor.stop();
316
+ }
317
+ });
318
+
319
+ test("watch-ingest failure is logged; poll reconciliation still ingests", async () => {
320
+ const admin = adminFor(4109);
321
+ const accountId = await connectSharedAccount(admin);
322
+ let ingestCalls = 0;
323
+ const supervisor = createInboundMailSupervisor({
324
+ providerCtx: { registry: stack.registry },
325
+ db,
326
+ dispatchWrite: ({ handlerQn, payload, tenantId }) => {
327
+ if (handlerQn === InboundMailFoundationHandlers.ingestMessage) {
328
+ ingestCalls += 1;
329
+ if (ingestCalls === 1) {
330
+ return Promise.resolve({ isSuccess: false, error: { code: "simulated" } });
331
+ }
332
+ }
333
+ return stack.dispatcher.write(
334
+ handlerQn,
335
+ payload,
336
+ createSystemUser(tenantId as TenantId, [ROLES.SystemAdmin]),
337
+ );
338
+ },
339
+ pollIntervalMs: 60_000,
340
+ watchBackoffInitialMs: 50,
341
+ watchBackoffMaxMs: 200,
342
+ });
343
+ try {
344
+ await supervisor.start();
345
+ await waitFor(() => {
346
+ expect(isWatching(accountId)).toBe(true);
347
+ });
348
+
349
+ await seedInboundMessage(
350
+ accountId,
351
+ rawMsg({ providerMessageId: "watch-ingest-fail-1", subject: "via watch" }),
352
+ );
353
+
354
+ await supervisor.pollOnce();
355
+
356
+ const rows = await selectMany(db, inboundMessagesProjectionTable, { accountId });
357
+ expect(rows.some((r) => r["messageIdHeader"] === "watch-ingest-fail-1@example.com")).toBe(
358
+ true,
359
+ );
360
+ } finally {
361
+ await supervisor.stop();
362
+ }
363
+ });
364
+
365
+ test("storeBody hook persists raw MIME references on poll ingest", async () => {
366
+ const admin = adminFor(4110);
367
+ const accountId = await connectSharedAccount(admin);
368
+ const stored: string[] = [];
369
+ const supervisor = createInboundMailSupervisor({
370
+ providerCtx: { registry: stack.registry },
371
+ db,
372
+ dispatchWrite: ({ handlerQn, payload, tenantId }) =>
373
+ stack.dispatcher.write(
374
+ handlerQn,
375
+ payload,
376
+ createSystemUser(tenantId as TenantId, [ROLES.SystemAdmin]),
377
+ ),
378
+ storeBody: async (_account, msg) => {
379
+ stored.push(msg.providerMessageId);
380
+ return `body-ref:${msg.providerMessageId}`;
381
+ },
382
+ pollIntervalMs: 60_000,
383
+ });
384
+ await seedInboundMessage(
385
+ accountId,
386
+ rawMsg({
387
+ providerMessageId: "body-store-1",
388
+ rawMime: new TextEncoder().encode("raw-mime-bytes"),
389
+ }),
390
+ );
391
+ await supervisor.pollOnce();
392
+ expect(stored).toEqual(["body-store-1"]);
393
+ });
394
+
395
+ test("disconnecting an account tears down its watcher on the next pollOnce", async () => {
396
+ const admin = adminFor(4111);
397
+ const accountId = await connectSharedAccount(admin);
398
+ const supervisor = createSupervisor();
399
+ try {
400
+ await supervisor.start();
401
+ await waitFor(() => {
402
+ expect(isWatching(accountId)).toBe(true);
403
+ });
404
+
405
+ await stack.http.writeOk(
406
+ InboundMailFoundationHandlers.disconnectAccount,
407
+ { accountId, reason: "test" },
408
+ admin,
409
+ );
410
+ await supervisor.pollOnce();
411
+
412
+ expect(isWatching(accountId)).toBe(false);
413
+ } finally {
414
+ await supervisor.stop();
415
+ }
416
+ });
417
+
418
+ test("start() is idempotent — second start keeps the watcher armed", async () => {
419
+ const admin = adminFor(4112);
420
+ const accountId = await connectSharedAccount(admin);
421
+ const supervisor = createSupervisor();
422
+ try {
423
+ await supervisor.start();
424
+ await waitFor(() => {
425
+ expect(isWatching(accountId)).toBe(true);
426
+ });
427
+ await supervisor.start();
428
+ expect(isWatching(accountId)).toBe(true);
429
+ } finally {
430
+ await supervisor.stop();
431
+ }
432
+ });
433
+ });
@@ -0,0 +1,40 @@
1
+ import { v5 as uuidv5 } from "uuid";
2
+
3
+ // Fixed UUID-namespaces für deterministic aggregate-id-Ableitung.
4
+ // Generiert einmalig (2026-07-12), in Stein gemeißelt: ein Wechsel würde
5
+ // jeden existing aggregate-Stream re-keyen → kaputte Message-Historie,
6
+ // kaputte Idempotency. Drift-Pins in __tests__/feature.test.ts.
7
+
8
+ /** Namespace für inbound-message-Streams. */
9
+ const INBOUND_MESSAGE_NAMESPACE = "7f2a9c41-3d6b-4e0f-9c58-2b1e7a4d8f63";
10
+
11
+ /** Namespace für mail-thread-Streams. */
12
+ const MAIL_THREAD_NAMESPACE = "b48d1e72-5c9a-4f36-8e0d-6a3f9b2c7e15";
13
+
14
+ /**
15
+ * Deterministic aggregate-id für eine inbound-message. Key ist
16
+ * (accountId, providerMessageId): derselbe Provider-Fetch der zweimal
17
+ * dieselbe Message liefert (IDLE-Doppel-Notify, Cursor-Overlap,
18
+ * Watch-Restart-Replay) landet auf demselben Stream — der Idempotency-
19
+ * Check im ingest-handler sieht den bereits appendeten Event.
20
+ *
21
+ * accountId (nicht tenantId) im Key: zwei Accounts desselben Tenants
22
+ * können dieselbe Mail empfangen (CC an beide Postfächer) — das sind
23
+ * fachlich ZWEI inbound-messages.
24
+ */
25
+ // @wrapper-known uuid-domain
26
+ export function inboundMessageAggregateId(accountId: string, providerMessageId: string): string {
27
+ return uuidv5(`${accountId}:${providerMessageId}`, INBOUND_MESSAGE_NAMESPACE);
28
+ }
29
+
30
+ /**
31
+ * Deterministic aggregate-id für einen mail-thread. Key ist
32
+ * (tenantId, threadKey) — Threads sind tenant-weit, nicht account-weit:
33
+ * eine Konversation die über zwei verbundene Postfächer läuft ist EIN
34
+ * Thread. threadKey kommt normalisiert vom Provider (References/
35
+ * In-Reply-To-Chain bzw. Provider-Thread-ID).
36
+ */
37
+ // @wrapper-known uuid-domain
38
+ export function mailThreadAggregateId(tenantId: string, threadKey: string): string {
39
+ return uuidv5(`${tenantId}:${threadKey}`, MAIL_THREAD_NAMESPACE);
40
+ }
@@ -0,0 +1,230 @@
1
+ // createInboundMailConnectRoutes — Hono-Route-Factory die der App-Owner
2
+ // via `extraRoutes` in seinem bin/server.ts mountet (Muster
3
+ // billing-foundation/webhook-handler.ts).
4
+ //
5
+ // GET /inbound-mail/connect (auth-gated — im authed Bereich mounten)
6
+ // GET /inbound-mail/oauth/callback (anonymous — MUSS außerhalb /api/*
7
+ // liegen, /api ist auth-gated;
8
+ // verifiziert user-data-rights §5)
9
+ //
10
+ // Beispiel bin/server.ts:
11
+ //
12
+ // const routes = createInboundMailConnectRoutes({
13
+ // providerCtx: { registry: deps.registry, secrets, config: undefined },
14
+ // dispatchWrite: ({ handlerQn, payload, tenantId }) =>
15
+ // deps.dispatchSystemWrite({ handlerQn, payload, tenantId: tenantId as TenantId }),
16
+ // secrets,
17
+ // stateSecret: env.INBOUND_MAIL_STATE_SECRET,
18
+ // callbackUrl: `${env.PUBLIC_BASE_URL}/inbound-mail/oauth/callback`,
19
+ // });
20
+ // app.get("/api/inbound-mail/connect", routes.connect); // auth-gated
21
+ // app.get("/inbound-mail/oauth/callback", routes.callback); // anonymous
22
+ //
23
+ // Provider registrieren KEINE eigenen Routen — ihr oauth-Block wird über
24
+ // resolveInboundProviderForKey aufgelöst. Der Callback vertraut NUR dem
25
+ // HMAC-verifizierten state (CSRF-/Fremd-Claiming-Sperre, oauth-state.ts).
26
+
27
+ import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
28
+ import type { Context } from "hono";
29
+ import {
30
+ INBOUND_MAIL_FOUNDATION_FEATURE,
31
+ InboundMailAuthMethods,
32
+ InboundMailFoundationHandlers,
33
+ inboundCredentialSecretKey,
34
+ } from "./constants";
35
+ import { signOAuthState, verifyOAuthState } from "./oauth-state";
36
+ import { resolveInboundProviderForKey } from "./provider-factory";
37
+ import type { InboundMailContext } from "./types";
38
+
39
+ const DEFAULT_STATE_TTL_MINUTES = 15;
40
+
41
+ export type InboundMailConnectRoutesDeps = {
42
+ /** Slim-Context für Provider-Lookups + oauth-Calls (registry Pflicht). */
43
+ readonly providerCtx: InboundMailContext;
44
+ /** Schreibt durch den Standard-Dispatcher mit auto-konstruiertem
45
+ * SystemUser — muss connect-account als SystemAdmin durchlassen. */
46
+ readonly dispatchWrite: (args: {
47
+ readonly handlerQn: string;
48
+ readonly payload: unknown;
49
+ readonly tenantId: string;
50
+ }) => Promise<{
51
+ readonly isSuccess: boolean;
52
+ readonly data?: unknown;
53
+ readonly error?: unknown;
54
+ }>;
55
+ /** Tenant-Secret-Write für den Refresh-Token (Slot = accountId). */
56
+ readonly secrets: SecretsContext;
57
+ /** HMAC-Secret für den state-Parameter (Deploy-Config aus env —
58
+ * `scope:"system"`-Secrets existieren in secrets-v1 nicht). */
59
+ readonly stateSecret: string;
60
+ /** Absolute Callback-URL — exakt so beim OAuth-Provider registriert. */
61
+ readonly callbackUrl: string;
62
+ /** Wohin der Browser nach erfolgreichem Connect redirected wird.
63
+ * Fehlt sie, antwortet der Callback mit JSON (API-/Test-Modus). */
64
+ readonly successRedirectUrl?: string;
65
+ readonly stateTtlMinutes?: number;
66
+ };
67
+
68
+ function errorJson(c: Context, status: 400 | 401 | 404 | 502, code: string, message: string) {
69
+ return c.json({ error: { code, message } }, status);
70
+ }
71
+
72
+ /** Session-User aus dem Hono-Context — vom Auth-Middleware gesetzt. */
73
+ function sessionUserOf(c: Context): { id: string; tenantId: string } | undefined {
74
+ const user = c.get("user") as { id?: unknown; tenantId?: unknown } | undefined;
75
+ if (!user || typeof user.id !== "string" || typeof user.tenantId !== "string") return undefined;
76
+ return { id: user.id, tenantId: user.tenantId };
77
+ }
78
+
79
+ export function createInboundMailConnectRoutes(deps: InboundMailConnectRoutesDeps) {
80
+ const ttl = deps.stateTtlMinutes ?? DEFAULT_STATE_TTL_MINUTES;
81
+
82
+ const connect = async (c: Context): Promise<Response> => {
83
+ const user = sessionUserOf(c);
84
+ if (!user) {
85
+ return errorJson(c, 401, "unauthenticated", "connect requires a signed-in user");
86
+ }
87
+ const providerKey = c.req.query("provider") ?? "";
88
+ const scope = c.req.query("scope") ?? "";
89
+ const mailbox = c.req.query("mailbox") ?? "";
90
+ if (!providerKey || (scope !== "user" && scope !== "shared") || !mailbox) {
91
+ return errorJson(
92
+ c,
93
+ 400,
94
+ "invalid_connect_request",
95
+ `${INBOUND_MAIL_FOUNDATION_FEATURE}: expected ?provider=<key>&scope=user|shared&mailbox=<address>`,
96
+ );
97
+ }
98
+
99
+ let plugin: ReturnType<typeof resolveInboundProviderForKey>;
100
+ try {
101
+ plugin = resolveInboundProviderForKey(deps.providerCtx, providerKey);
102
+ } catch (e) {
103
+ return errorJson(
104
+ c,
105
+ 404,
106
+ "provider_not_registered",
107
+ e instanceof Error ? e.message : String(e),
108
+ );
109
+ }
110
+ if (!plugin.oauth) {
111
+ return errorJson(
112
+ c,
113
+ 400,
114
+ "provider_has_no_oauth_flow",
115
+ `provider "${providerKey}" connects via credentials form (connect-account write-handler), not OAuth`,
116
+ );
117
+ }
118
+
119
+ const state = signOAuthState(
120
+ {
121
+ tenantId: user.tenantId,
122
+ ownerUserId: scope === "user" ? user.id : null,
123
+ providerKey,
124
+ mailbox,
125
+ },
126
+ ttl,
127
+ deps.stateSecret,
128
+ );
129
+ const authorizeUrl = await plugin.oauth.buildAuthorizeUrl(deps.providerCtx, {
130
+ state,
131
+ redirectUri: deps.callbackUrl,
132
+ });
133
+ return c.redirect(authorizeUrl, 302);
134
+ };
135
+
136
+ const callback = async (c: Context): Promise<Response> => {
137
+ const code = c.req.query("code") ?? "";
138
+ const rawState = c.req.query("state") ?? "";
139
+ if (!code || !rawState) {
140
+ return errorJson(c, 400, "invalid_callback", "missing code or state");
141
+ }
142
+ const verified = verifyOAuthState(rawState, deps.stateSecret);
143
+ if (!verified.ok) {
144
+ return errorJson(c, 400, "invalid_state", `state rejected: ${verified.reason}`);
145
+ }
146
+ const state = verified.payload;
147
+
148
+ let plugin: ReturnType<typeof resolveInboundProviderForKey>;
149
+ try {
150
+ plugin = resolveInboundProviderForKey(deps.providerCtx, state.providerKey);
151
+ } catch (e) {
152
+ return errorJson(
153
+ c,
154
+ 404,
155
+ "provider_not_registered",
156
+ e instanceof Error ? e.message : String(e),
157
+ );
158
+ }
159
+ if (!plugin.oauth) {
160
+ return errorJson(c, 400, "provider_has_no_oauth_flow", state.providerKey);
161
+ }
162
+
163
+ let tokens: Awaited<ReturnType<NonNullable<typeof plugin.oauth>["exchangeCode"]>>;
164
+ try {
165
+ tokens = await plugin.oauth.exchangeCode(deps.providerCtx, {
166
+ code,
167
+ redirectUri: deps.callbackUrl,
168
+ });
169
+ } catch (e) {
170
+ return errorJson(c, 502, "token_exchange_failed", e instanceof Error ? e.message : String(e));
171
+ }
172
+ if (!tokens.refreshToken) {
173
+ // Ohne Refresh-Token kein Server-Sync — Consent muss offline_access
174
+ // (bzw. access_type=offline) anfordern; der Provider-oauth-Block
175
+ // besitzt die Scopes.
176
+ return errorJson(
177
+ c,
178
+ 502,
179
+ "no_refresh_token",
180
+ `provider "${state.providerKey}" returned no refresh token — check the offline-access scope in the provider's oauth.scopes`,
181
+ );
182
+ }
183
+
184
+ // Account anlegen (programmatic SystemUser; echter Owner kommt aus
185
+ // dem HMAC-verifizierten state → ownerUserIdOverride).
186
+ const dispatched = await deps.dispatchWrite({
187
+ handlerQn: InboundMailFoundationHandlers.connectAccount,
188
+ tenantId: state.tenantId,
189
+ payload: {
190
+ provider: state.providerKey,
191
+ authMethod: InboundMailAuthMethods.oauth,
192
+ displayName: state.mailbox,
193
+ address: state.mailbox,
194
+ scope: state.ownerUserId ? "user" : "shared",
195
+ ownerUserIdOverride: state.ownerUserId,
196
+ },
197
+ });
198
+ if (!dispatched.isSuccess) {
199
+ return errorJson(c, 502, "account_create_failed", JSON.stringify(dispatched.error ?? {}));
200
+ }
201
+ const accountId = (dispatched.data as { accountId?: unknown } | undefined)?.accountId;
202
+ if (typeof accountId !== "string") {
203
+ return errorJson(c, 502, "account_create_failed", "connect-account returned no accountId");
204
+ }
205
+
206
+ // Refresh-Token in den per-Account-Secret-Slot (Request-Pfad =
207
+ // voller SecretsContext). Access-Tokens (~1h) werden NIE persistiert
208
+ // — refresh-before-poll im Sync-Pfad.
209
+ await deps.secrets.set(
210
+ state.tenantId,
211
+ inboundCredentialSecretKey(accountId),
212
+ tokens.refreshToken,
213
+ {
214
+ redact: (plaintext) => `${plaintext.slice(0, 4)}…`,
215
+ hint: `OAuth refresh token for inbound mail account ${accountId}`,
216
+ },
217
+ );
218
+
219
+ if (deps.successRedirectUrl) {
220
+ const target = new URL(deps.successRedirectUrl);
221
+ target.searchParams.set("accountId", accountId);
222
+ return c.redirect(target.toString(), 302);
223
+ }
224
+ return c.json({ connected: true, accountId }, 200);
225
+ };
226
+
227
+ return { connect, callback };
228
+ }
229
+
230
+ export type InboundMailConnectRoutes = ReturnType<typeof createInboundMailConnectRoutes>;