@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
@@ -1,4 +1,8 @@
1
- import type { FileStorageProvider, SignedUrlOptions } from "@cosmicdrift/kumiko-framework/files";
1
+ import {
2
+ assertSafeStorageKey,
3
+ type FileStorageProvider,
4
+ type SignedUrlOptions,
5
+ } from "@cosmicdrift/kumiko-framework/files";
2
6
 
3
7
  // =============================================================================
4
8
  // Operator-Pflicht-Setup (Multipart-Upload-Cleanup)
@@ -72,10 +76,12 @@ export function createS3Provider(config: S3ProviderConfig): FileStorageProvider
72
76
 
73
77
  return {
74
78
  async write(key, data, mimeType): Promise<void> {
79
+ assertSafeStorageKey(key);
75
80
  await client.write(key, data, mimeType !== undefined ? { type: mimeType } : undefined);
76
81
  },
77
82
 
78
83
  async writeStream(key, source, options): Promise<void> {
84
+ assertSafeStorageKey(key);
79
85
  // Echtes multipart-streaming via Bun's S3-Writer — partSize steuert die
80
86
  // Part-Boundary intern (AWS/R2 verlangen non-final Parts >= 5 MiB,
81
87
  // sonst EntityTooSmall beim CompleteMultipartUpload). Manuelles flush()
@@ -96,10 +102,12 @@ export function createS3Provider(config: S3ProviderConfig): FileStorageProvider
96
102
  },
97
103
 
98
104
  async read(key): Promise<Uint8Array> {
105
+ assertSafeStorageKey(key);
99
106
  return new Uint8Array(await client.file(key).arrayBuffer());
100
107
  },
101
108
 
102
109
  readStream(key): AsyncIterable<Uint8Array> {
110
+ assertSafeStorageKey(key);
103
111
  // Lazy: erst beim ersten chunk-pull wird der GET-Request abgesetzt.
104
112
  // Existiert der Key nicht, faellt der Error genau dort (nicht beim
105
113
  // readStream-Aufruf) — gleiches Lazy-Verhalten wie inmemory + local.
@@ -113,10 +121,12 @@ export function createS3Provider(config: S3ProviderConfig): FileStorageProvider
113
121
  },
114
122
 
115
123
  async delete(key): Promise<void> {
124
+ assertSafeStorageKey(key);
116
125
  await client.delete(key);
117
126
  },
118
127
 
119
128
  async exists(key): Promise<boolean> {
129
+ assertSafeStorageKey(key);
120
130
  return client.exists(key);
121
131
  },
122
132
 
@@ -125,6 +135,7 @@ export function createS3Provider(config: S3ProviderConfig): FileStorageProvider
125
135
  expiresInSeconds: number,
126
136
  options?: SignedUrlOptions,
127
137
  ): Promise<string> {
138
+ assertSafeStorageKey(key);
128
139
  // contentDisposition wird von Bun als response-content-disposition
129
140
  // Query-Param signiert (Response-Override fuer den GET-Download) —
130
141
  // der Browser sieht den Original-Dateinamen statt des UUID-Keys.
@@ -331,6 +331,7 @@ describe("folders-user-data — tenantScopedDelete hooks", () => {
331
331
  await seedOneFolderWithAssignment();
332
332
  const ctx = {
333
333
  db: stack.db,
334
+ registry: stack.registry,
334
335
  tenantId: admin.tenantId,
335
336
  userId: admin.id,
336
337
  tenantModel: "multi-user" as const,
@@ -345,6 +346,7 @@ describe("folders-user-data — tenantScopedDelete hooks", () => {
345
346
  await seedOneFolderWithAssignment();
346
347
  const ctx = {
347
348
  db: stack.db,
349
+ registry: stack.registry,
348
350
  tenantId: admin.tenantId,
349
351
  userId: admin.id,
350
352
  tenantModel: "single-user" as const,
@@ -359,6 +361,7 @@ describe("folders-user-data — tenantScopedDelete hooks", () => {
359
361
  await seedOneFolderWithAssignment();
360
362
  const ctx = {
361
363
  db: stack.db,
364
+ registry: stack.registry,
362
365
  tenantId: admin.tenantId,
363
366
  userId: admin.id,
364
367
  tenantModel: "single-user" as const,
@@ -149,11 +149,28 @@ describe("FolderManager filing mode", () => {
149
149
  );
150
150
  });
151
151
 
152
- test("the in-tree new-folder row opens a draft; submitting (Enter) creates a root folder", async () => {
152
+ test("deleting a folder in filing mode refetches AND tells the host to reassign", async () => {
153
+ folderRows = [{ id: "f1", name: "A", parentId: null, version: 1 }];
154
+ const onReassigned = mock(() => {});
155
+ render(
156
+ <Wrapper>
157
+ <FolderManager filing={filingWith(onReassigned)} />
158
+ </Wrapper>,
159
+ );
160
+ fireEvent.click(screen.getByTestId("folder-delete-f1"));
161
+ fireEvent.click(await screen.findByTestId("folder-manager-delete-dialog-confirm"));
162
+ await waitFor(() =>
163
+ expect(dispatchSpy).toHaveBeenCalledWith(FoldersHandlers.deleteFolder, { id: "f1" }),
164
+ );
165
+ await waitFor(() => expect(onReassigned).toHaveBeenCalled());
166
+ });
167
+
168
+ test("the in-tree new-folder row opens a draft; submitting (Enter) creates a root folder and reassigns", async () => {
153
169
  folderRows = [];
170
+ const onReassigned = mock(() => {});
154
171
  render(
155
172
  <Wrapper>
156
- <FolderManager />
173
+ <FolderManager filing={filingWith(onReassigned)} />
157
174
  </Wrapper>,
158
175
  );
159
176
  fireEvent.click(screen.getByTestId("folder-manager-new-root"));
@@ -164,6 +181,30 @@ describe("FolderManager filing mode", () => {
164
181
  await waitFor(() =>
165
182
  expect(dispatchSpy).toHaveBeenCalledWith(FoldersHandlers.createFolder, { name: "Inbox" }),
166
183
  );
184
+ await waitFor(() => expect(onReassigned).toHaveBeenCalled());
185
+ });
186
+
187
+ test("renaming a folder dispatches update-folder and reassigns", async () => {
188
+ folderRows = [{ id: "f1", name: "A", parentId: null, version: 1 }];
189
+ const onReassigned = mock(() => {});
190
+ render(
191
+ <Wrapper>
192
+ <FolderManager filing={filingWith(onReassigned)} />
193
+ </Wrapper>,
194
+ );
195
+ fireEvent.click(screen.getByTestId("folder-rename-f1"));
196
+ fireEvent.change(document.getElementById("folder-manager-draft") as HTMLInputElement, {
197
+ target: { value: "Renamed" },
198
+ });
199
+ fireEvent.submit(screen.getByTestId("folder-manager-draft"));
200
+ await waitFor(() =>
201
+ expect(dispatchSpy).toHaveBeenCalledWith(FoldersHandlers.updateFolder, {
202
+ id: "f1",
203
+ version: 1,
204
+ changes: { name: "Renamed" },
205
+ }),
206
+ );
207
+ await waitFor(() => expect(onReassigned).toHaveBeenCalled());
167
208
  });
168
209
 
169
210
  test("a mixed-type tree files each leaf under its own entityType (per-leaf override + fallback)", async () => {
@@ -55,4 +55,12 @@ describe("folderPath", () => {
55
55
  test("unknown id → empty string", () => {
56
56
  expect(folderPath(rows, "nope")).toBe("");
57
57
  });
58
+
59
+ // 658/4: a parentId cycle must terminate at the row-count cap, not loop
60
+ // forever or run one iteration past it.
61
+ test("a parentId cycle terminates instead of looping forever", () => {
62
+ const cyclic = [f("x", "X", "y"), f("y", "Y", "x")];
63
+ expect(() => folderPath(cyclic, "x")).not.toThrow();
64
+ expect(folderPath(cyclic, "x").split(" / ")).toHaveLength(cyclic.length);
65
+ });
58
66
  });
@@ -47,9 +47,7 @@ export type FolderLeaf = {
47
47
  readonly label: string;
48
48
  readonly trailing?: ReactNode;
49
49
  readonly onOpen?: () => void;
50
- // Overrides filing.entityType for this leaf — lets one filing tree hold leaves
51
- // of mixed entity types (e.g. credits + Bausparverträge), each filed/cleared
52
- // under its own type. Omit for single-type trees.
50
+ // Overrides filing.entityType (mixed-entity-type trees); omit for single-type trees.
53
51
  readonly entityType?: string;
54
52
  };
55
53
 
@@ -183,6 +181,7 @@ export function FolderManager({
183
181
  if (await write()) {
184
182
  setPending(null);
185
183
  await catalog.refetch();
184
+ await filing?.onReassigned();
186
185
  }
187
186
  } finally {
188
187
  setBusy(false);
@@ -18,7 +18,6 @@ export const defaultTranslations: TranslationsByLocale = {
18
18
  "folders.section.working": "Speichert…",
19
19
 
20
20
  "folders.manager.loading": "Lädt…",
21
- "folders.manager.empty": "Noch keine Ordner.",
22
21
  "folders.manager.newRoot": "Neuer Ordner",
23
22
  "folders.manager.add": "Anlegen",
24
23
  "folders.manager.addChild": "Unterordner",
@@ -44,7 +43,6 @@ export const defaultTranslations: TranslationsByLocale = {
44
43
  "folders.section.working": "Saving…",
45
44
 
46
45
  "folders.manager.loading": "Loading…",
47
- "folders.manager.empty": "No folders yet.",
48
46
  "folders.manager.newRoot": "New folder",
49
47
  "folders.manager.add": "Create",
50
48
  "folders.manager.addChild": "Subfolder",
@@ -31,6 +31,15 @@ export function buildFolderTree(rows: readonly FolderRow[]): readonly FolderNode
31
31
  if (siblings) siblings.push(row);
32
32
  else byParent.set(key, [row]);
33
33
  }
34
+ // visited guards against a parent cycle recursing forever (658/4) —
35
+ // currently impossible (no reparent feature yet), but cheap insurance for
36
+ // when one lands; a cyclic node's subtree just stops re-descending.
37
+ // No cycle guard needed (658/4, verified not just asserted): build() only
38
+ // descends via byParent[parentId] starting from roots (parentId === null
39
+ // or dangling) — a row in a cycle has a non-null, existing parentId by
40
+ // definition, so it can never be a root, and each row contributes exactly
41
+ // one parentId edge, so nothing outside a cycle points into it either. A
42
+ // pure a<->b cycle is structurally unreachable from build(null).
34
43
  const build = (parentId: string | null, depth: number): readonly FolderNode[] =>
35
44
  (byParent.get(parentId) ?? [])
36
45
  .slice()
@@ -41,12 +50,14 @@ export function buildFolderTree(rows: readonly FolderRow[]): readonly FolderNode
41
50
 
42
51
  // "Immobilie Berlin / Person Müller" for a folder id, walking parentId up.
43
52
  // Empty string if the id is unknown. The row-count cap stops a (currently
44
- // impossible — no reparent yet) parent cycle from spinning forever.
53
+ // impossible — no reparent yet) parent cycle from spinning forever — `< `,
54
+ // not `<=` (658/4): a cycle of exactly rows.length nodes must stop AT the
55
+ // cap, not run one iteration past it.
45
56
  export function folderPath(rows: readonly FolderRow[], id: string, separator = " / "): string {
46
57
  const byId = new Map(rows.map((r) => [r.id, r]));
47
58
  const names: string[] = [];
48
59
  let current = byId.get(id);
49
- for (let i = 0; current !== undefined && i <= rows.length; i += 1) {
60
+ for (let i = 0; current !== undefined && i < rows.length; i += 1) {
50
61
  names.unshift(current.name);
51
62
  current = current.parentId !== null ? byId.get(current.parentId) : undefined;
52
63
  }
@@ -57,6 +57,7 @@ describe("folderAssignmentExportHook", () => {
57
57
 
58
58
  const snippet = await folderAssignmentExportHook({
59
59
  db: stack.db,
60
+ registry: stack.registry,
60
61
  tenantId: admin.tenantId,
61
62
  userId: admin.id,
62
63
  });
@@ -16,10 +16,10 @@ export const foldersUserDataFeature = defineFeature("folders-user-data", (r) =>
16
16
  r.describe(
17
17
  "GDPR (Art. 20 export / Art. 17 erasure) coverage for the `folders` feature's `folder` + `folder-assignment` entities. Mounts the EXT_USER_DATA export + delete hooks so a tenant's folder tree and its entity-to-folder assignments are included in the user-data export bundle and erased on a tenant-scoped forget (single-user tenants only; multi-user + anonymize are no-ops since folder rows carry no per-user PII). Kept separate from `folders` so folder consumers without the user-data-rights pipeline don't pull a hard dependency — requires `user-data-rights`, optionalRequires `folders`.",
18
18
  );
19
- // user-data-rights ist die harte Abhängigkeit (EXT_USER_DATA-Host). `folders` ist
20
- // OPTIONAL: ist es toggleable(default=false) gemountet (z.B. per-Tenant via Tier),
21
- // würde ein hartes r.requires eine „effectively disabled"-Boot-Warnung werfen,
22
- // obwohl die folder-Entities existieren und die Hooks korrekt greifen.
19
+ // user-data-rights is the hard dependency (EXT_USER_DATA host). `folders` is
20
+ // OPTIONAL: if it's mounted toggleable(default=false) (e.g. per-tenant via
21
+ // tier), a hard r.requires would throw an "effectively disabled" boot
22
+ // warning even though the folder entities exist and the hooks work fine.
23
23
  r.requires("user-data-rights");
24
24
  r.optionalRequires("folders");
25
25
  r.useExtension(EXT_USER_DATA, "folder", {
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
3
+ import type { MailAccountEventPayload } from "../events";
4
+ import { loadCurrentMailAccountPayload } from "../handlers/account-state";
5
+
6
+ function ctxWithEvents(
7
+ payloads: readonly MailAccountEventPayload[],
8
+ ): Pick<HandlerContext, "loadAggregate"> {
9
+ return {
10
+ loadAggregate: async () =>
11
+ payloads.map((payload) => ({
12
+ // @cast-boundary test fixture — only `payload` is read by the function under test
13
+ payload,
14
+ })) as unknown as Awaited<ReturnType<HandlerContext["loadAggregate"]>>,
15
+ };
16
+ }
17
+
18
+ describe("loadCurrentMailAccountPayload", () => {
19
+ test("returns undefined when the aggregate has no events", async () => {
20
+ const ctx = ctxWithEvents([]);
21
+ expect(await loadCurrentMailAccountPayload(ctx, "acc-1")).toBeUndefined();
22
+ });
23
+
24
+ test("returns the payload of the LAST event — full-snapshot semantics", async () => {
25
+ const first = { status: "active" } as unknown as MailAccountEventPayload;
26
+ const last = { status: "degraded" } as unknown as MailAccountEventPayload;
27
+ const ctx = ctxWithEvents([first, last]);
28
+ expect(await loadCurrentMailAccountPayload(ctx, "acc-1")).toBe(last);
29
+ });
30
+ });
@@ -0,0 +1,307 @@
1
+ // Connect-routes integration — mounts createInboundMailConnectRoutes on a
2
+ // Hono app wired to a real setupTestStack dispatcher + secrets context.
3
+ // Exercises OAuth connect redirect, HMAC state, callback account create,
4
+ // and error paths (401/400/404/502) — not mock-only existence checks.
5
+
6
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7
+ import { randomBytes } from "node:crypto";
8
+ import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
9
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
10
+ import {
11
+ configurePiiSubjectKms,
12
+ InMemoryKmsAdapter,
13
+ resetPiiSubjectKmsForTests,
14
+ } from "@cosmicdrift/kumiko-framework/crypto";
15
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
16
+ import {
17
+ createSystemUser,
18
+ defineFeature,
19
+ type TenantId,
20
+ } from "@cosmicdrift/kumiko-framework/engine";
21
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
22
+ import { createEnvMasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
23
+ import {
24
+ createTestUser,
25
+ setupTestStack,
26
+ type TestStack,
27
+ testTenantId,
28
+ unsafeCreateEntityTable,
29
+ unsafePushTables,
30
+ } from "@cosmicdrift/kumiko-framework/stack";
31
+ import {
32
+ createMutableMasterKeyProvider,
33
+ type MutableMasterKeyProvider,
34
+ } from "@cosmicdrift/kumiko-framework/testing";
35
+ import { Hono } from "hono";
36
+ import {
37
+ createComplianceProfilesFeature,
38
+ tenantComplianceProfileEntity,
39
+ } from "../../compliance-profiles";
40
+ import { createConfigFeature } from "../../config";
41
+ import { inboundProviderInMemoryFeature } from "../../inbound-provider-inmemory";
42
+ import { createSecretsContext, createSecretsFeature, tenantSecretsTable } from "../../secrets";
43
+ import { createTenantFeature } from "../../tenant/feature";
44
+ import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
45
+ import {
46
+ createInboundMailConnectRoutes,
47
+ INBOUND_MAIL_PROVIDER_EXTENSION,
48
+ type InboundMailProviderPlugin,
49
+ inboundCredentialSecretKey,
50
+ inboundMailFoundationFeature,
51
+ mailAccountsProjectionTable,
52
+ type OAuthTokenSet,
53
+ seenMessageEntity,
54
+ syncCursorEntity,
55
+ } from "../index";
56
+
57
+ const OAUTH_PROVIDER_KEY = "oauth-test";
58
+ const STATE_SECRET = "inbound-mail-connect-test-state-secret-32b";
59
+ const CALLBACK_URL = "http://localhost/inbound-mail/oauth/callback";
60
+
61
+ const oauthTestPlugin: InboundMailProviderPlugin = {
62
+ verify: async () => {},
63
+ fetch: async () => ({ messages: [], nextCursor: { offset: 0 }, hasMore: false }),
64
+ oauth: {
65
+ scopes: { receive: ["mail.read"] },
66
+ buildAuthorizeUrl: async (_ctx, p) =>
67
+ `https://oauth.test/authorize?state=${encodeURIComponent(p.state)}&redirect_uri=${encodeURIComponent(p.redirectUri)}`,
68
+ exchangeCode: async (_ctx, p): Promise<OAuthTokenSet> => {
69
+ if (p.code === "fail-exchange") {
70
+ throw new Error("token endpoint unavailable");
71
+ }
72
+ if (p.code === "no-refresh") {
73
+ return {
74
+ accessToken: "access-only",
75
+ expiresAt: "2099-01-01T00:00:00Z",
76
+ scopesGranted: ["mail.read"],
77
+ };
78
+ }
79
+ return {
80
+ accessToken: "access-token",
81
+ refreshToken: "refresh-token-abc",
82
+ expiresAt: "2099-01-01T00:00:00Z",
83
+ scopesGranted: ["mail.read"],
84
+ };
85
+ },
86
+ refreshAccessToken: async () => {
87
+ throw new Error("not used in connect-routes tests");
88
+ },
89
+ },
90
+ };
91
+
92
+ const oauthTestProviderFeature = defineFeature("inbound-provider-oauth-test", (r) => {
93
+ r.requires("inbound-mail-foundation");
94
+ r.useExtension(INBOUND_MAIL_PROVIDER_EXTENSION, OAUTH_PROVIDER_KEY, oauthTestPlugin);
95
+ });
96
+
97
+ let stack: TestStack;
98
+ let db: DbConnection;
99
+ let secrets: ReturnType<typeof createSecretsContext>;
100
+ let providerRef: MutableMasterKeyProvider;
101
+ let routes: ReturnType<typeof createInboundMailConnectRoutes>;
102
+
103
+ beforeAll(async () => {
104
+ const initialKp = createEnvMasterKeyProvider({
105
+ env: {
106
+ KUMIKO_SECRETS_MASTER_KEY_V1: randomBytes(32).toString("base64"),
107
+ KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: "1",
108
+ },
109
+ });
110
+ providerRef = createMutableMasterKeyProvider(initialKp);
111
+
112
+ stack = await setupTestStack({
113
+ features: [
114
+ createConfigFeature(),
115
+ createTenantFeature(),
116
+ createSecretsFeature(),
117
+ createComplianceProfilesFeature(),
118
+ createTenantLifecycleFeature(),
119
+ inboundMailFoundationFeature,
120
+ inboundProviderInMemoryFeature,
121
+ oauthTestProviderFeature,
122
+ ],
123
+ masterKeyProvider: providerRef,
124
+ extraContext: ({ db: stackDb }) => ({
125
+ secrets: createSecretsContext({ db: stackDb, masterKeyProvider: providerRef }),
126
+ }),
127
+ });
128
+ db = stack.db;
129
+ secrets = createSecretsContext({ db, masterKeyProvider: providerRef });
130
+
131
+ await createEventsTable(db);
132
+ await unsafeCreateEntityTable(db, tenantComplianceProfileEntity);
133
+ await unsafeCreateEntityTable(db, syncCursorEntity);
134
+ await unsafeCreateEntityTable(db, seenMessageEntity);
135
+ await unsafePushTables(db, { tenant_secrets: tenantSecretsTable });
136
+ configurePiiSubjectKms(new InMemoryKmsAdapter());
137
+
138
+ routes = createInboundMailConnectRoutes({
139
+ providerCtx: { registry: stack.registry, secrets },
140
+ dispatchWrite: ({ handlerQn, payload, tenantId }) =>
141
+ stack.dispatcher.write(
142
+ handlerQn,
143
+ payload,
144
+ createSystemUser(tenantId as TenantId, [ROLES.SystemAdmin]),
145
+ ),
146
+ secrets,
147
+ stateSecret: STATE_SECRET,
148
+ callbackUrl: CALLBACK_URL,
149
+ });
150
+ });
151
+
152
+ afterAll(async () => {
153
+ await stack.cleanup();
154
+ resetPiiSubjectKmsForTests();
155
+ });
156
+
157
+ function adminFor(tenantNumber: number) {
158
+ return createTestUser({
159
+ id: tenantNumber,
160
+ tenantId: testTenantId(tenantNumber),
161
+ roles: ["TenantAdmin", "SystemAdmin"],
162
+ });
163
+ }
164
+
165
+ function buildApp(opts: { user?: { id: string; tenantId: string } | null } = {}) {
166
+ const app = new Hono<{ Variables: { user?: { id: string; tenantId: string } } }>();
167
+ if (opts.user !== null) {
168
+ const user = opts.user ?? adminFor(4201);
169
+ app.use("*", async (c, next) => {
170
+ c.set("user", { id: user.id, tenantId: user.tenantId });
171
+ await next();
172
+ });
173
+ }
174
+ app.get("/api/inbound-mail/connect", routes.connect);
175
+ app.get("/inbound-mail/oauth/callback", routes.callback);
176
+ return app;
177
+ }
178
+
179
+ describe("connect-routes — connect", () => {
180
+ test("401 without session user", async () => {
181
+ const app = buildApp({ user: null });
182
+ const res = await app.request(
183
+ `/api/inbound-mail/connect?provider=${OAUTH_PROVIDER_KEY}&scope=shared&mailbox=inbox@acme.test`,
184
+ );
185
+ expect(res.status).toBe(401);
186
+ });
187
+
188
+ test("400 when query params are incomplete", async () => {
189
+ const app = buildApp();
190
+ const res = await app.request(`/api/inbound-mail/connect?provider=${OAUTH_PROVIDER_KEY}`);
191
+ expect(res.status).toBe(400);
192
+ const body = (await res.json()) as { error: { code: string } };
193
+ expect(body.error.code).toBe("invalid_connect_request");
194
+ });
195
+
196
+ test("404 for unknown provider key", async () => {
197
+ const app = buildApp();
198
+ const res = await app.request(
199
+ "/api/inbound-mail/connect?provider=does-not-exist&scope=shared&mailbox=inbox@acme.test",
200
+ );
201
+ expect(res.status).toBe(404);
202
+ });
203
+
204
+ test("400 when provider has no oauth flow (inmemory)", async () => {
205
+ const app = buildApp();
206
+ const res = await app.request(
207
+ "/api/inbound-mail/connect?provider=inmemory&scope=shared&mailbox=inbox@acme.test",
208
+ );
209
+ expect(res.status).toBe(400);
210
+ const body = (await res.json()) as { error: { code: string } };
211
+ expect(body.error.code).toBe("provider_has_no_oauth_flow");
212
+ });
213
+
214
+ test("302 redirect to provider authorize URL with signed state", async () => {
215
+ const app = buildApp({ user: adminFor(4202) });
216
+ const res = await app.request(
217
+ `/api/inbound-mail/connect?provider=${OAUTH_PROVIDER_KEY}&scope=shared&mailbox=team@acme.test`,
218
+ { redirect: "manual" },
219
+ );
220
+ expect(res.status).toBe(302);
221
+ const location = res.headers.get("location");
222
+ expect(location).toBeTruthy();
223
+ const url = new URL(location!);
224
+ expect(url.origin + url.pathname).toBe("https://oauth.test/authorize");
225
+ expect(url.searchParams.get("redirect_uri")).toBe(CALLBACK_URL);
226
+ expect(url.searchParams.get("state")).toBeTruthy();
227
+ });
228
+ });
229
+
230
+ describe("connect-routes — oauth callback", () => {
231
+ test("400 when code/state missing", async () => {
232
+ const app = buildApp({ user: null });
233
+ const res = await app.request("/inbound-mail/oauth/callback");
234
+ expect(res.status).toBe(400);
235
+ });
236
+
237
+ test("400 on tampered state", async () => {
238
+ const app = buildApp({ user: null });
239
+ const res = await app.request("/inbound-mail/oauth/callback?code=ok&state=not.a.valid.state");
240
+ expect(res.status).toBe(400);
241
+ const body = (await res.json()) as { error: { code: string } };
242
+ expect(body.error.code).toBe("invalid_state");
243
+ });
244
+
245
+ test("happy path: creates mail account + stores refresh token secret", async () => {
246
+ const user = adminFor(4203);
247
+ const connectApp = buildApp({ user });
248
+ const connectRes = await connectApp.request(
249
+ `/api/inbound-mail/connect?provider=${OAUTH_PROVIDER_KEY}&scope=user&mailbox=owner@acme.test`,
250
+ { redirect: "manual" },
251
+ );
252
+ const state = new URL(connectRes.headers.get("location")!).searchParams.get("state");
253
+ expect(state).toBeTruthy();
254
+
255
+ const callbackApp = buildApp({ user: null });
256
+ const callbackRes = await callbackApp.request(
257
+ `/inbound-mail/oauth/callback?code=ok&state=${encodeURIComponent(state!)}`,
258
+ );
259
+ expect(callbackRes.status).toBe(200);
260
+ const body = (await callbackRes.json()) as { connected: boolean; accountId: string };
261
+ expect(body.connected).toBe(true);
262
+ expect(body.accountId).toBeTruthy();
263
+
264
+ const accounts = await selectMany(db, mailAccountsProjectionTable, { id: body.accountId });
265
+ expect(accounts).toHaveLength(1);
266
+ expect(accounts[0]?.["provider"]).toBe(OAUTH_PROVIDER_KEY);
267
+ expect(accounts[0]?.["ownerUserId"]).toBe(user.id);
268
+
269
+ const stored = await secrets.get(user.tenantId, inboundCredentialSecretKey(body.accountId));
270
+ expect(stored).toBeDefined();
271
+ expect(stored!.reveal()).toBe("refresh-token-abc");
272
+ });
273
+
274
+ test("502 when token exchange fails", async () => {
275
+ const user = adminFor(4204);
276
+ const connectApp = buildApp({ user });
277
+ const connectRes = await connectApp.request(
278
+ `/api/inbound-mail/connect?provider=${OAUTH_PROVIDER_KEY}&scope=shared&mailbox=fail@acme.test`,
279
+ { redirect: "manual" },
280
+ );
281
+ const state = new URL(connectRes.headers.get("location")!).searchParams.get("state");
282
+
283
+ const res = await buildApp({ user: null }).request(
284
+ `/inbound-mail/oauth/callback?code=fail-exchange&state=${encodeURIComponent(state!)}`,
285
+ );
286
+ expect(res.status).toBe(502);
287
+ const body = (await res.json()) as { error: { code: string } };
288
+ expect(body.error.code).toBe("token_exchange_failed");
289
+ });
290
+
291
+ test("502 when provider omits refresh token", async () => {
292
+ const user = adminFor(4205);
293
+ const connectApp = buildApp({ user });
294
+ const connectRes = await connectApp.request(
295
+ `/api/inbound-mail/connect?provider=${OAUTH_PROVIDER_KEY}&scope=shared&mailbox=norefresh@acme.test`,
296
+ { redirect: "manual" },
297
+ );
298
+ const state = new URL(connectRes.headers.get("location")!).searchParams.get("state");
299
+
300
+ const res = await buildApp({ user: null }).request(
301
+ `/inbound-mail/oauth/callback?code=no-refresh&state=${encodeURIComponent(state!)}`,
302
+ );
303
+ expect(res.status).toBe(502);
304
+ const body = (await res.json()) as { error: { code: string } };
305
+ expect(body.error.code).toBe("no_refresh_token");
306
+ });
307
+ });