@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,265 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
3
+ import {
4
+ createTestUser,
5
+ setupTestStack,
6
+ type TestStack,
7
+ testTenantId,
8
+ unsafeCreateEntityTable,
9
+ unsafePushTables,
10
+ } from "@cosmicdrift/kumiko-framework/stack";
11
+ import {
12
+ createTestEnvelopeCipher,
13
+ expectErrorIncludes,
14
+ } from "@cosmicdrift/kumiko-framework/testing";
15
+ import { createConfigFeature } from "../../config";
16
+ import { createConfigResolver } from "../../config/resolver";
17
+ import { configValuesTable } from "../../config/table";
18
+ import { createTenantFeature } from "../../tenant";
19
+ import { createUserFeature } from "../../user/feature";
20
+ import { userEntity } from "../../user/schema/user";
21
+ import { base32Decode } from "../base32";
22
+ import { AuthMfaHandlers, AuthMfaQueries } from "../constants";
23
+ import { createAuthMfaFeature } from "../feature";
24
+ import { userMfaEntity } from "../schema/user-mfa";
25
+ import { currentTotpCode } from "../totp";
26
+
27
+ let stack: TestStack;
28
+
29
+ const SETUP_TOKEN_SECRET = "test-setup-token-secret-do-not-use-in-prod";
30
+
31
+ beforeAll(async () => {
32
+ const encryption = createTestEnvelopeCipher();
33
+ configureEntityFieldEncryption(encryption);
34
+ const resolver = createConfigResolver({ cipher: encryption });
35
+ stack = await setupTestStack({
36
+ features: [
37
+ createConfigFeature(),
38
+ createUserFeature(),
39
+ createTenantFeature(),
40
+ createAuthMfaFeature({
41
+ setupTokenSecret: SETUP_TOKEN_SECRET,
42
+ issuer: "Kumiko Test",
43
+ challengeTokenSecret: "test-mfa-challenge-secret-at-least-32-bytes!!",
44
+ }),
45
+ ],
46
+ extraContext: { configResolver: resolver, configEncryption: encryption },
47
+ });
48
+ await unsafeCreateEntityTable(stack.db, userEntity);
49
+ await unsafeCreateEntityTable(stack.db, userMfaEntity);
50
+ await unsafePushTables(stack.db, { configValuesTable });
51
+ });
52
+
53
+ afterAll(async () => {
54
+ await stack.cleanup();
55
+ });
56
+
57
+ // Runs enable-start + enable-confirm for a fresh user and returns the
58
+ // user + the live secret so tests can derive current TOTP codes.
59
+ async function enableMfaFor(userId: string): Promise<{
60
+ user: ReturnType<typeof createTestUser>;
61
+ secret: Buffer;
62
+ recoveryCodes: string[];
63
+ }> {
64
+ const user = createTestUser({ id: userId, roles: ["User"] });
65
+ const start = await stack.http.writeOk<{
66
+ setupToken: string;
67
+ otpauthUri: string;
68
+ recoveryCodes: string[];
69
+ }>(AuthMfaHandlers.enableStart, { accountLabel: `${userId}@example.com` }, user);
70
+
71
+ const secretParam = new URLSearchParams(start.otpauthUri.split("?")[1]).get("secret") ?? "";
72
+ const secret = base32Decode(secretParam);
73
+ await stack.http.writeOk(
74
+ AuthMfaHandlers.enableConfirm,
75
+ { setupToken: start.setupToken, code: currentTotpCode(secret) },
76
+ user,
77
+ );
78
+
79
+ return { user, secret, recoveryCodes: start.recoveryCodes };
80
+ }
81
+
82
+ describe("disable", () => {
83
+ test("a valid TOTP code disables MFA, and a fresh enable-start succeeds afterward", async () => {
84
+ const { user, secret } = await enableMfaFor("disable-totp-1");
85
+
86
+ const res = await stack.http.writeOk<{ disabled: boolean }>(
87
+ AuthMfaHandlers.disable,
88
+ { code: currentTotpCode(secret) },
89
+ user,
90
+ );
91
+ expect(res.disabled).toBe(true);
92
+
93
+ const secondStart = await stack.http.writeOk<{ setupToken: string }>(
94
+ AuthMfaHandlers.enableStart,
95
+ { accountLabel: "disable-totp-1@example.com" },
96
+ user,
97
+ );
98
+ expect(secondStart.setupToken).toBeTruthy();
99
+ });
100
+
101
+ test("MFA can be fully re-enabled after disable (re-create on a deleted aggregate)", async () => {
102
+ const { user, secret: firstSecret } = await enableMfaFor("disable-reenable-1");
103
+
104
+ await stack.http.writeOk<{ disabled: boolean }>(
105
+ AuthMfaHandlers.disable,
106
+ { code: currentTotpCode(firstSecret) },
107
+ user,
108
+ );
109
+
110
+ const restart = await stack.http.writeOk<{ setupToken: string; otpauthUri: string }>(
111
+ AuthMfaHandlers.enableStart,
112
+ { accountLabel: "disable-reenable-1@example.com" },
113
+ user,
114
+ );
115
+ const secretParam = new URLSearchParams(restart.otpauthUri.split("?")[1]).get("secret") ?? "";
116
+ const secondSecret = base32Decode(secretParam);
117
+
118
+ const confirm = await stack.http.writeOk<{ enabled: boolean }>(
119
+ AuthMfaHandlers.enableConfirm,
120
+ { setupToken: restart.setupToken, code: currentTotpCode(secondSecret) },
121
+ user,
122
+ );
123
+ expect(confirm.enabled).toBe(true);
124
+
125
+ // The re-enabled row is fully usable — a real code disables it again.
126
+ const res = await stack.http.writeOk<{ disabled: boolean }>(
127
+ AuthMfaHandlers.disable,
128
+ { code: currentTotpCode(secondSecret) },
129
+ user,
130
+ );
131
+ expect(res.disabled).toBe(true);
132
+ });
133
+
134
+ test("a valid recovery code also disables MFA", async () => {
135
+ const { user, recoveryCodes } = await enableMfaFor("disable-recovery-1");
136
+
137
+ const res = await stack.http.writeOk<{ disabled: boolean }>(
138
+ AuthMfaHandlers.disable,
139
+ { code: recoveryCodes[0] },
140
+ user,
141
+ );
142
+ expect(res.disabled).toBe(true);
143
+ });
144
+
145
+ test("a recovery code typed lowercase and without the dash still matches", async () => {
146
+ const { user, recoveryCodes } = await enableMfaFor("disable-recovery-normalize-1");
147
+ const code = recoveryCodes[0];
148
+ if (code === undefined) throw new Error("test setup produced no recovery code");
149
+
150
+ const sloppy = code.toLowerCase().replace("-", "");
151
+ const res = await stack.http.writeOk<{ disabled: boolean }>(
152
+ AuthMfaHandlers.disable,
153
+ { code: sloppy },
154
+ user,
155
+ );
156
+ expect(res.disabled).toBe(true);
157
+ });
158
+
159
+ test("a wrong code is rejected and MFA stays enabled", async () => {
160
+ const { user, secret } = await enableMfaFor("disable-wrong-1");
161
+
162
+ const err = await stack.http.writeErr(AuthMfaHandlers.disable, { code: "000000" }, user);
163
+ expectErrorIncludes(err, "invalid_totp_code");
164
+
165
+ // Still enabled — a real code still works.
166
+ const res = await stack.http.writeOk<{ disabled: boolean }>(
167
+ AuthMfaHandlers.disable,
168
+ { code: currentTotpCode(secret) },
169
+ user,
170
+ );
171
+ expect(res.disabled).toBe(true);
172
+ });
173
+
174
+ test("disable on a user without MFA is rejected", async () => {
175
+ const user = createTestUser({ id: "disable-never-enabled-1", roles: ["User"] });
176
+ const err = await stack.http.writeErr(AuthMfaHandlers.disable, { code: "123456" }, user);
177
+ expectErrorIncludes(err, "mfa_not_enabled");
178
+ });
179
+ });
180
+
181
+ describe("regenerate-recovery", () => {
182
+ test("issues 8 fresh codes and invalidates the old ones", async () => {
183
+ const { user, secret, recoveryCodes: oldCodes } = await enableMfaFor("regen-1");
184
+
185
+ const res = await stack.http.writeOk<{ recoveryCodes: string[] }>(
186
+ AuthMfaHandlers.regenerateRecovery,
187
+ { code: currentTotpCode(secret) },
188
+ user,
189
+ );
190
+ expect(res.recoveryCodes).toHaveLength(8);
191
+ expect(res.recoveryCodes).not.toEqual(oldCodes);
192
+
193
+ // An old code no longer works to disable MFA.
194
+ const oldCode = oldCodes[0];
195
+ if (oldCode === undefined) throw new Error("test setup produced no old recovery code");
196
+ const err = await stack.http.writeErr(AuthMfaHandlers.disable, { code: oldCode }, user);
197
+ expectErrorIncludes(err, "invalid_totp_code");
198
+
199
+ // A NEW code works.
200
+ const newCode = res.recoveryCodes[0];
201
+ if (newCode === undefined) throw new Error("regenerate produced no new recovery code");
202
+ const disableRes = await stack.http.writeOk<{ disabled: boolean }>(
203
+ AuthMfaHandlers.disable,
204
+ { code: newCode },
205
+ user,
206
+ );
207
+ expect(disableRes.disabled).toBe(true);
208
+ });
209
+
210
+ test("regenerate on a user without MFA is rejected", async () => {
211
+ const user = createTestUser({ id: "regen-never-enabled-1", roles: ["User"] });
212
+ const err = await stack.http.writeErr(
213
+ AuthMfaHandlers.regenerateRecovery,
214
+ { code: "123456" },
215
+ user,
216
+ );
217
+ expectErrorIncludes(err, "mfa_not_enabled");
218
+ });
219
+ });
220
+
221
+ // pr-review kumiko-framework #1062/4 — the only prior coverage of
222
+ // user-mfa:status was the false → true transition (implicit in
223
+ // enableMfaFor's own setup); the more interesting true → false after
224
+ // disable, and tenant isolation, had no assertion anywhere.
225
+ describe("status query", () => {
226
+ test("true → false after disable", async () => {
227
+ const { user, secret } = await enableMfaFor("status-disable-1");
228
+
229
+ const before = await stack.http.queryOk<{ enabled: boolean }>(AuthMfaQueries.status, {}, user);
230
+ expect(before.enabled).toBe(true);
231
+
232
+ await stack.http.writeOk(AuthMfaHandlers.disable, { code: currentTotpCode(secret) }, user);
233
+
234
+ const after = await stack.http.queryOk<{ enabled: boolean }>(AuthMfaQueries.status, {}, user);
235
+ expect(after.enabled).toBe(false);
236
+ });
237
+
238
+ test("tenant isolation: same userId enrolled in tenant A stays invisible to the same id in tenant B", async () => {
239
+ // The entity's uniqueness is [userId, tenantId] — a second tenant CAN
240
+ // have a distinct (non-enrolled) row for the identical userId. The
241
+ // query filters only by userId (see status.query.ts's own comment) and
242
+ // relies on ctx.db's tenant scoping to keep them apart; assert that
243
+ // scoping actually holds instead of just trusting it.
244
+ const { user: tenantAUser } = await enableMfaFor("status-tenant-iso-1");
245
+ const tenantBUser = createTestUser({
246
+ id: tenantAUser.id,
247
+ tenantId: testTenantId(2),
248
+ roles: ["User"],
249
+ });
250
+
251
+ const tenantAStatus = await stack.http.queryOk<{ enabled: boolean }>(
252
+ AuthMfaQueries.status,
253
+ {},
254
+ tenantAUser,
255
+ );
256
+ expect(tenantAStatus.enabled).toBe(true);
257
+
258
+ const tenantBStatus = await stack.http.queryOk<{ enabled: boolean }>(
259
+ AuthMfaQueries.status,
260
+ {},
261
+ tenantBUser,
262
+ );
263
+ expect(tenantBStatus.enabled).toBe(false);
264
+ });
265
+ });
@@ -0,0 +1,321 @@
1
+ // #1456: completes both the enrollment #1465 started AND the login that
2
+ // mfa-setup-required blocked. Real HTTP through setupTestStack, exercising
3
+ // the full chain: login → preauthSetupToken → preauth-enable-start →
4
+ // preauth-confirm → the minted JWT actually authenticates a follow-up
5
+ // request. That end-to-end seam is the thing a mocked test would fake.
6
+
7
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
8
+ import { randomBytes } from "node:crypto";
9
+ import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
10
+ import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
11
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
12
+ import {
13
+ createTestUser,
14
+ setupTestStack,
15
+ type TestStack,
16
+ testTenantId,
17
+ unsafeCreateEntityTable,
18
+ unsafePushTables,
19
+ } from "@cosmicdrift/kumiko-framework/stack";
20
+ import { createTestEnvelopeCipher } from "@cosmicdrift/kumiko-framework/testing";
21
+ import {
22
+ AuthErrors as AuthEmailPasswordErrors,
23
+ AuthHandlers as AuthEmailPasswordHandlers,
24
+ createAuthEmailPasswordFeature,
25
+ } from "../../auth-email-password";
26
+ import { ConfigHandlers, createConfigFeature } from "../../config";
27
+ import { createConfigResolver } from "../../config/resolver";
28
+ import { configValuesTable } from "../../config/table";
29
+ import { hashPassword } from "../../shared";
30
+ import { createTenantFeature } from "../../tenant";
31
+ import { tenantMembershipsTable } from "../../tenant/membership-table";
32
+ import { tenantEntity } from "../../tenant/schema/tenant";
33
+ import { seedTenantMembership } from "../../tenant/testing";
34
+ import { UserHandlers } from "../../user";
35
+ import { createUserFeature } from "../../user/feature";
36
+ import { userEntity, userTable } from "../../user/schema/user";
37
+ import { base32Decode } from "../base32";
38
+ import { mfaRequiredConfigHandle } from "../config";
39
+ import { AuthMfaHandlers } from "../constants";
40
+ import {
41
+ bindMfaRevokeAllOtherSessionsFromFeature,
42
+ createAuthMfaFeature,
43
+ mfaStatusCheckerFromFeature,
44
+ } from "../feature";
45
+ import { userMfaEntity, userMfaTable } from "../schema/user-mfa";
46
+ import { currentTotpCode } from "../totp";
47
+
48
+ let stack: TestStack;
49
+ const revokedForUserIds: string[] = [];
50
+
51
+ const CHALLENGE_TOKEN_SECRET = "integration-test-challenge-token-secret-not-real-0001";
52
+ const SETUP_TOKEN_SECRET = "integration-test-setup-token-secret-not-real-0002";
53
+ const TENANT_ID: TenantId = testTenantId(420);
54
+
55
+ beforeAll(async () => {
56
+ const encryption = createTestEnvelopeCipher(randomBytes(32).toString("base64"));
57
+ configureEntityFieldEncryption(encryption);
58
+ const resolver = createConfigResolver({ cipher: encryption });
59
+ const authMfaFeature = createAuthMfaFeature({
60
+ setupTokenSecret: SETUP_TOKEN_SECRET,
61
+ issuer: "Kumiko Test",
62
+ challengeTokenSecret: CHALLENGE_TOKEN_SECRET,
63
+ });
64
+
65
+ stack = await setupTestStack({
66
+ features: [
67
+ createConfigFeature(),
68
+ createUserFeature(),
69
+ createTenantFeature(),
70
+ authMfaFeature,
71
+ createAuthEmailPasswordFeature({
72
+ mfaStatusChecker: mfaStatusCheckerFromFeature(authMfaFeature),
73
+ }),
74
+ ],
75
+ extraContext: { configResolver: resolver, configEncryption: encryption },
76
+ authConfig: {
77
+ membershipQuery: "tenant:query:memberships",
78
+ loginHandler: AuthEmailPasswordHandlers.login,
79
+ loginErrorStatusMap: {
80
+ [AuthEmailPasswordErrors.invalidCredentials]: 401,
81
+ [AuthEmailPasswordErrors.noMembership]: 403,
82
+ },
83
+ mfaPreauthEnableStartHandler: AuthMfaHandlers.enableStartPreauth,
84
+ mfaPreauthConfirmHandler: AuthMfaHandlers.enableConfirmPreauth,
85
+ },
86
+ });
87
+
88
+ await unsafeCreateEntityTable(stack.db, userEntity);
89
+ await unsafeCreateEntityTable(stack.db, tenantEntity);
90
+ await unsafeCreateEntityTable(stack.db, userMfaEntity);
91
+ await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
92
+
93
+ // Wire a spy in place of the real session-revoke callback (run-prod-app
94
+ // only binds this once the sessions feature is mounted — not the case in
95
+ // this test stack) so #1467's regression can observe whether
96
+ // revokeAllOtherSessions actually fires, instead of silently no-op'ing.
97
+ const bind = bindMfaRevokeAllOtherSessionsFromFeature(authMfaFeature);
98
+ // If the binder ever returns undefined, the spy below never wires up and
99
+ // the #1467 regression check further down would pass vacuously (nothing
100
+ // to revoke because nothing observed) instead of catching a real re-break.
101
+ expect(bind).toBeDefined();
102
+ bind?.((userId) => {
103
+ revokedForUserIds.push(userId);
104
+ return Promise.resolve(0);
105
+ });
106
+ });
107
+
108
+ afterAll(async () => {
109
+ await stack.cleanup();
110
+ });
111
+
112
+ beforeEach(async () => {
113
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
114
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
115
+ revokedForUserIds.length = 0;
116
+ });
117
+
118
+ async function loginBlockedByEnforcement(
119
+ email = "unenrolled@example.com",
120
+ ): Promise<{ preauthSetupToken: string; userId: string }> {
121
+ const password = "correct-horse-battery-2026";
122
+ const hash = await hashPassword(password);
123
+ const created = await stack.http.writeOk<{ id: string }>(
124
+ UserHandlers.create,
125
+ { email, passwordHash: hash, displayName: "Unenrolled" },
126
+ createTestUser({ id: 421, tenantId: TENANT_ID, roles: ["SystemAdmin"] }),
127
+ );
128
+ await seedTenantMembership(stack.db, {
129
+ userId: created.id,
130
+ tenantId: TENANT_ID,
131
+ roles: ["User"],
132
+ });
133
+ await stack.http.writeOk(
134
+ ConfigHandlers.set,
135
+ { key: mfaRequiredConfigHandle.name, value: "all" },
136
+ createTestUser({ id: 422, tenantId: TENANT_ID, roles: ["Admin"] }),
137
+ );
138
+
139
+ const loginRes = await stack.http.raw("POST", "/api/auth/login", { email, password });
140
+ const loginBody = await loginRes.json();
141
+ return { preauthSetupToken: loginBody.preauthSetupToken as string, userId: created.id };
142
+ }
143
+
144
+ async function startEnrollment(
145
+ preauthSetupToken: string,
146
+ accountLabel: string,
147
+ ): Promise<{ setupToken: string; secret: Buffer }> {
148
+ const startRes = await stack.http.raw("POST", "/api/auth/mfa/preauth-enable-start", {
149
+ preauthSetupToken,
150
+ accountLabel,
151
+ });
152
+ const startBody = await startRes.json();
153
+ const secretParam = new URLSearchParams(startBody.otpauthUri.split("?")[1]).get("secret") ?? "";
154
+ return { setupToken: startBody.setupToken as string, secret: base32Decode(secretParam) };
155
+ }
156
+
157
+ describe("POST /auth/mfa/preauth-confirm", () => {
158
+ test("completes enrollment + login: full chain, minted JWT authenticates a follow-up request", async () => {
159
+ const email = "chain@example.com";
160
+ const { preauthSetupToken } = await loginBlockedByEnforcement(email);
161
+ const { setupToken, secret } = await startEnrollment(preauthSetupToken, email);
162
+
163
+ const res = await stack.http.raw("POST", "/api/auth/mfa/preauth-confirm", {
164
+ setupToken,
165
+ code: currentTotpCode(secret),
166
+ });
167
+
168
+ expect(res.status).toBe(200);
169
+ const body = await res.json();
170
+ expect(body.isSuccess).toBe(true);
171
+ expect(typeof body.token).toBe("string");
172
+ expect(body.user.tenantId).toBe(TENANT_ID);
173
+
174
+ // The proof that matters: the minted JWT actually authenticates.
175
+ const meRes = await stack.http.raw(
176
+ "POST",
177
+ "/api/query",
178
+ { type: "user:query:user:me", payload: {} },
179
+ { Authorization: `Bearer ${body.token}` },
180
+ );
181
+ expect(meRes.status).toBe(200);
182
+
183
+ // Positive control for the #1467 regression below — proves the spy
184
+ // wiring actually observes a real revoke call, not just silent no-ops.
185
+ expect(revokedForUserIds).toContain(body.user.id);
186
+ });
187
+
188
+ test("wrong TOTP code → invalid_totp_code, no enrollment persisted", async () => {
189
+ const email = "wrongcode@example.com";
190
+ const { preauthSetupToken, userId } = await loginBlockedByEnforcement(email);
191
+ const { setupToken } = await startEnrollment(preauthSetupToken, email);
192
+
193
+ const res = await stack.http.raw("POST", "/api/auth/mfa/preauth-confirm", {
194
+ setupToken,
195
+ code: "000000",
196
+ });
197
+
198
+ expect(res.status).not.toBe(200);
199
+ const body = await res.json();
200
+ expect(body.isSuccess).toBe(false);
201
+ expect(body.error.details.reason).toBe("invalid_totp_code");
202
+ expect(body.token).toBeUndefined();
203
+ // "no enrollment persisted" is the actual claim in the test name — the
204
+ // reason string alone doesn't prove it. Scoped to this test's own user:
205
+ // the table isn't truncated between tests in this file.
206
+ const rows = await selectMany(stack.db, userMfaTable, { userId });
207
+ expect(rows).toHaveLength(0);
208
+ });
209
+
210
+ test("replaying the same setupToken after a successful confirm → invalid_setup_token (burned), not a second enrollment", async () => {
211
+ const email = "race@example.com";
212
+ const { preauthSetupToken, userId } = await loginBlockedByEnforcement(email);
213
+ const { setupToken, secret } = await startEnrollment(preauthSetupToken, email);
214
+ const code = currentTotpCode(secret);
215
+
216
+ const first = await stack.http.raw("POST", "/api/auth/mfa/preauth-confirm", {
217
+ setupToken,
218
+ code,
219
+ });
220
+ expect(first.status).toBe(200);
221
+
222
+ // Same setupToken again — this integration stack wires real Redis, so
223
+ // the single-use burn from the first call rejects the replay before it
224
+ // ever reaches the already-enrolled DB check.
225
+ const second = await stack.http.raw("POST", "/api/auth/mfa/preauth-confirm", {
226
+ setupToken,
227
+ code,
228
+ });
229
+ expect(second.status).not.toBe(200);
230
+ const secondBody = await second.json();
231
+ expect(secondBody.error.details.reason).toBe("invalid_setup_token");
232
+ // "not a second enrollment" — pin the row count, not just the error reason.
233
+ const rows = await selectMany(stack.db, userMfaTable, { userId });
234
+ expect(rows).toHaveLength(1);
235
+ });
236
+
237
+ test("a session-issued setupToken (no tenantId) is rejected, not silently accepted", async () => {
238
+ const email = "sessionissued@example.com";
239
+ const password = "correct-horse-battery-2026";
240
+ const hash = await hashPassword(password);
241
+ const created = await stack.http.writeOk<{ id: string }>(
242
+ UserHandlers.create,
243
+ { email, passwordHash: hash, displayName: "Session Issued" },
244
+ createTestUser({ id: 423, tenantId: TENANT_ID, roles: ["SystemAdmin"] }),
245
+ );
246
+ await seedTenantMembership(stack.db, {
247
+ userId: created.id,
248
+ tenantId: TENANT_ID,
249
+ roles: ["User"],
250
+ });
251
+ const sessionUser = createTestUser({ id: created.id, tenantId: TENANT_ID, roles: ["User"] });
252
+
253
+ // The session-authed enable-start signs a setupToken with the SAME
254
+ // secret + format, but never carries tenantId (see mfa-setup-token.ts).
255
+ // A malicious or confused client resubmitting it to the pre-auth
256
+ // endpoint must not be able to piggyback on tenantId inference.
257
+ const start = await stack.http.writeOk<{ setupToken: string }>(
258
+ AuthMfaHandlers.enableStart,
259
+ { accountLabel: email },
260
+ sessionUser,
261
+ );
262
+
263
+ const res = await stack.http.raw("POST", "/api/auth/mfa/preauth-confirm", {
264
+ setupToken: start.setupToken,
265
+ code: "123456",
266
+ });
267
+
268
+ expect(res.status).not.toBe(200);
269
+ const body = await res.json();
270
+ expect(body.isSuccess).toBe(false);
271
+ expect(body.error.details.reason).toBe("invalid_setup_token");
272
+ });
273
+
274
+ // #1467: findForAuth's status check + membership lookup used to run AFTER
275
+ // the entity-write and the revokeAllOtherSessions call — an admin
276
+ // restricting/deleting the account between preauth-enable-start and
277
+ // preauth-confirm still got invalid_setup_token back correctly (the
278
+ // handler's whole DB transaction rolls back on a non-success return, so
279
+ // the MFA row itself never actually persisted), but revokeAllOtherSessions
280
+ // is an external side effect outside that transaction — it fired
281
+ // regardless, logging the user's other devices out over an enrollment
282
+ // attempt that was rejected. Regression: with status/membership checked
283
+ // BEFORE the write, the revoke call must not fire at all for this case.
284
+ test("user restricted between start and confirm → invalid_setup_token, no session revoked (#1467)", async () => {
285
+ const email = "restricted-midflight@example.com";
286
+ const { preauthSetupToken, userId } = await loginBlockedByEnforcement(email);
287
+ const { setupToken, secret } = await startEnrollment(preauthSetupToken, email);
288
+
289
+ await asRawClient(stack.db).unsafe(
290
+ `UPDATE "${userTable.tableName}" SET "status" = 'restricted' WHERE "id" = $1`,
291
+ [userId],
292
+ );
293
+
294
+ const res = await stack.http.raw("POST", "/api/auth/mfa/preauth-confirm", {
295
+ setupToken,
296
+ code: currentTotpCode(secret),
297
+ });
298
+
299
+ expect(res.status).not.toBe(200);
300
+ const body = await res.json();
301
+ expect(body.error.details.reason).toBe("invalid_setup_token");
302
+ expect(revokedForUserIds).not.toContain(userId);
303
+ // Enrollment write itself rolls back with the handler's failure return —
304
+ // pinned here too so a future change to that transactional behavior
305
+ // doesn't silently reintroduce the original "row persists" half of the bug.
306
+ const rows = await selectMany(stack.db, userMfaTable, { userId });
307
+ expect(rows).toHaveLength(0);
308
+ });
309
+
310
+ test("expired/garbage setupToken → invalid_setup_token, no session minted", async () => {
311
+ const res = await stack.http.raw("POST", "/api/auth/mfa/preauth-confirm", {
312
+ setupToken: "not-a-real-token",
313
+ code: "123456",
314
+ });
315
+
316
+ expect(res.status).not.toBe(200);
317
+ const body = await res.json();
318
+ expect(body.isSuccess).toBe(false);
319
+ expect(body.token).toBeUndefined();
320
+ });
321
+ });