@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,66 @@
1
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { z } from "zod";
3
+ import { base32Encode } from "../base32";
4
+ import { MFA_SETUP_TOKEN_TTL_MINUTES } from "../constants";
5
+ import { findUserMfaRow } from "../db/queries";
6
+ import { mfaAlreadyEnabled } from "../errors";
7
+ import { signMfaSetupToken } from "../mfa-setup-token";
8
+ import { buildOtpauthUri } from "../otpauth-uri";
9
+ import { generateRecoveryCodes, hashRecoveryCodes } from "../recovery-codes";
10
+ import { generateTotpSecret } from "../totp";
11
+
12
+ export type EnableStartOptions = {
13
+ readonly setupTokenSecret: string;
14
+ readonly issuer: string;
15
+ };
16
+
17
+ // Stateless setup: no `userMfa` row is created here. The generated secret +
18
+ // recovery-code hashes are signed into a short-lived `setupToken` (see
19
+ // mfa-setup-token.ts) that `enable-confirm` verifies. An abandoned setup
20
+ // (user closes the tab without entering a code) leaves zero trace — no
21
+ // cleanup job needed for orphaned "pending MFA" rows.
22
+ export function createEnableStartHandler(opts: EnableStartOptions) {
23
+ return defineWriteHandler({
24
+ name: "enable-start",
25
+ schema: z.object({
26
+ // Client-supplied label for the otpauth:// URI / authenticator-app
27
+ // entry (typically the user's own email) — avoids an extra DB lookup
28
+ // for something the already-authenticated client already knows about
29
+ // itself.
30
+ accountLabel: z.string().min(1).max(200),
31
+ }),
32
+ access: { openToAll: true },
33
+ handler: async (event, ctx) => {
34
+ const existing = await findUserMfaRow(ctx.db, event.user);
35
+ if (existing) return mfaAlreadyEnabled();
36
+
37
+ const secret = generateTotpSecret();
38
+ const recoveryCodes = generateRecoveryCodes();
39
+ const recoveryCodeHashes = await hashRecoveryCodes(recoveryCodes);
40
+
41
+ const { token: setupToken } = signMfaSetupToken(
42
+ {
43
+ userId: event.user.id,
44
+ totpSecretBase32: base32Encode(secret),
45
+ recoveryCodeHashes,
46
+ },
47
+ MFA_SETUP_TOKEN_TTL_MINUTES,
48
+ opts.setupTokenSecret,
49
+ );
50
+
51
+ return {
52
+ isSuccess: true,
53
+ data: {
54
+ setupToken,
55
+ otpauthUri: buildOtpauthUri({
56
+ issuer: opts.issuer,
57
+ accountLabel: event.payload.accountLabel,
58
+ secret,
59
+ }),
60
+ // Plaintext — this is the one and only time these are shown.
61
+ recoveryCodes,
62
+ },
63
+ };
64
+ },
65
+ });
66
+ }
@@ -0,0 +1,234 @@
1
+ // KEK-rotation job for userMfa.totpSecret + recoveryCodes (entity-field
2
+ // encryption, same MasterKeyProvider as secrets/config — see
3
+ // schema/user-mfa.ts). Modeled directly on config/handlers/reencrypt.job.ts:
4
+ // entity-field encryption stores a StoredEnvelope JSON string per value (no
5
+ // separate kekVersion column, unlike secrets' DEK-wrap model), so rotation
6
+ // means decrypt under the OLD key, then let the executor's own write-path
7
+ // re-encrypt under provider.currentVersion() — the same auto-encrypt every
8
+ // enable-confirm write already goes through (see enable-confirm.write.ts: it
9
+ // passes PLAINTEXT to executor.create, never a manually-built envelope).
10
+ //
11
+ // Both fields also carry `userOwned` — the executor's own encryptForStorage
12
+ // wraps them PII(envelope(plaintext)) (see event-store-executor-context.ts).
13
+ // This job reads raw rows via selectMany (bypassing the executor's
14
+ // decryptForRead), so it must peel that PII layer itself before the envelope
15
+ // cipher ever sees the value, mirroring decryptForRead's PII-then-envelope
16
+ // order — an envelope decrypt on a still-PII-wrapped string throws
17
+ // "[envelope-cipher] stored value is not valid JSON" instead of rotating anything.
18
+ //
19
+ // Idempotent: re-running skips rows already on the current kekVersion.
20
+ // Every write is a normal executor.update — after a full run even a
21
+ // from-scratch projection rebuild only ever sees the current-KEK envelope
22
+ // (the very risk this job's plan entry called out: a rebuild resurrecting
23
+ // an old-KEK wrap would happen if rotation only touched the read-side
24
+ // projection instead of appending a real `.updated` event).
25
+
26
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
27
+ import {
28
+ collectPiiSubjectFields,
29
+ configuredPiiSubjectKms,
30
+ decryptPiiFieldValues,
31
+ } from "@cosmicdrift/kumiko-framework/crypto";
32
+ import {
33
+ configuredEntityFieldEncryption,
34
+ createEventStoreExecutor,
35
+ createTenantDb,
36
+ type DbConnection,
37
+ type TenantDb,
38
+ } from "@cosmicdrift/kumiko-framework/db";
39
+ import type { JobHandlerFn, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
40
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
41
+ import { type EnvelopeCipher, isStoredEnvelope } from "@cosmicdrift/kumiko-framework/secrets";
42
+ import { type ChunkedMigrationStopReason, runChunkedMigration } from "../../shared";
43
+ import { userMfaEntity, userMfaTable } from "../schema/user-mfa";
44
+
45
+ const DEFAULT_BATCH_SIZE = 100;
46
+ const DEFAULT_MAX_FAILURES = 10;
47
+ const SYSTEM_ROLES = ["system"] as const;
48
+
49
+ const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
50
+ entityName: "user-mfa",
51
+ });
52
+
53
+ const piiFieldNames = collectPiiSubjectFields(userMfaEntity);
54
+
55
+ export type MfaReencryptJobPayload = {
56
+ readonly batchSize?: number;
57
+ readonly maxDurationMs?: number;
58
+ readonly maxFailures?: number;
59
+ };
60
+
61
+ export type MfaReencryptJobResult = {
62
+ readonly migrated: number;
63
+ readonly failed: number;
64
+ readonly alreadyCurrent: number;
65
+ readonly batchesProcessed: number;
66
+ readonly stoppedReason: ChunkedMigrationStopReason;
67
+ };
68
+
69
+ type EnvelopeClassification = "rotate" | "current" | "unrecognized";
70
+
71
+ // After PII peel (when configured), values must be current-cipher envelopes.
72
+ // Non-JSON / non-envelope is not a supported legacy decrypt path — fail the
73
+ // row instead of treating it as "needs rotate" (#1541, mirrors config #1513).
74
+ function classifyEnvelope(value: string, targetVersion: number): EnvelopeClassification {
75
+ let parsed: unknown;
76
+ try {
77
+ parsed = JSON.parse(value);
78
+ } catch {
79
+ return "unrecognized";
80
+ }
81
+ if (!isStoredEnvelope(parsed)) return "unrecognized";
82
+ return parsed.kekVersion === targetVersion ? "current" : "rotate";
83
+ }
84
+
85
+ export const mfaReencryptJob: JobHandlerFn = async (rawPayload, ctx): Promise<void> => {
86
+ const payload = rawPayload as MfaReencryptJobPayload; // @cast-boundary engine-payload
87
+ const maybeCipher = configuredEntityFieldEncryption();
88
+ if (!maybeCipher) {
89
+ throw new InternalError({
90
+ message:
91
+ "[auth-mfa:reencrypt] entity-field encryption is not configured — provide a master key " +
92
+ "(KUMIKO_SECRETS_MASTER_KEY_V<n>); run{Prod,Dev}App wire this automatically, custom boots " +
93
+ "call configureEntityFieldEncryption(cipher).",
94
+ });
95
+ }
96
+ // hoisted function declarations below capture this — pin the narrowed
97
+ // type explicitly so TS keeps it inside the closures.
98
+ const cipher: EnvelopeCipher = maybeCipher;
99
+ if (!ctx.masterKeyProvider) {
100
+ throw new InternalError({
101
+ message:
102
+ "[auth-mfa:reencrypt] ctx.masterKeyProvider missing — wire it via extraContext.masterKeyProvider at boot.",
103
+ });
104
+ }
105
+ const provider = ctx.masterKeyProvider;
106
+ if (!ctx.db) {
107
+ throw new InternalError({
108
+ message: "[auth-mfa:reencrypt] ctx.db missing — job context requires a database connection.",
109
+ });
110
+ }
111
+ const db = ctx.db as DbConnection; // @cast-boundary db-operator
112
+ // Undefined = per-subject crypto-shredding isn't configured for this app —
113
+ // both fields are then plain envelope strings, no PII layer to peel.
114
+ const piiKms = configuredPiiSubjectKms();
115
+
116
+ const batchSize = payload.batchSize ?? DEFAULT_BATCH_SIZE;
117
+ const maxFailures = payload.maxFailures ?? DEFAULT_MAX_FAILURES;
118
+ const deadline = payload.maxDurationMs
119
+ ? Date.now() + payload.maxDurationMs
120
+ : Number.POSITIVE_INFINITY;
121
+
122
+ const tdbCache = new Map<TenantId, TenantDb>();
123
+ function tdbFor(tenantId: TenantId): TenantDb {
124
+ let existing = tdbCache.get(tenantId);
125
+ if (!existing) {
126
+ existing = createTenantDb(db, tenantId, "system");
127
+ tdbCache.set(tenantId, existing);
128
+ }
129
+ return existing;
130
+ }
131
+
132
+ type UserMfaRow = {
133
+ id: string;
134
+ tenantId: string;
135
+ version: number;
136
+ totpSecret: string;
137
+ recoveryCodes: string;
138
+ };
139
+
140
+ let alreadyCurrent = 0;
141
+ const targetVersion = provider.currentVersion();
142
+
143
+ // ponytail: one full candidate scan — MFA rows are user-scale, not
144
+ // tenant-scale like config; cursor pagination when that ever matters.
145
+ // Served in slices so runChunkedMigration's deadline/signal/failure
146
+ // checks run between chunks, not only once.
147
+ let pending: UserMfaRow[] | undefined;
148
+ async function nextBatch(): Promise<readonly UserMfaRow[]> {
149
+ if (pending === undefined) {
150
+ pending = [...(await selectMany<UserMfaRow>(db, userMfaTable, {}))];
151
+ }
152
+ return pending.splice(0, batchSize);
153
+ }
154
+
155
+ async function migrateRow(row: UserMfaRow): Promise<"migrated" | "skipped" | "failed"> {
156
+ const tenantId = row.tenantId as TenantId; // @cast-boundary db-row
157
+
158
+ // Peel the PII layer first (if active) so classifyEnvelope/cipher.decrypt
159
+ // below only ever see envelope strings, same order as decryptForRead.
160
+ let envelopeValues: { totpSecret: string; recoveryCodes: string } = row;
161
+ if (piiKms) {
162
+ const unwrapped = await decryptPiiFieldValues(row, piiFieldNames, piiKms, {
163
+ requestId: "auth-mfa-reencrypt-job",
164
+ tenantId,
165
+ });
166
+ envelopeValues = unwrapped as { totpSecret: string; recoveryCodes: string }; // @cast-boundary engine-payload
167
+ }
168
+
169
+ const totpClass = classifyEnvelope(envelopeValues.totpSecret, targetVersion);
170
+ const recoveryClass = classifyEnvelope(envelopeValues.recoveryCodes, targetVersion);
171
+ if (totpClass === "unrecognized" || recoveryClass === "unrecognized") {
172
+ ctx.log?.warn?.(
173
+ `[auth-mfa:reencrypt] row ${row.id} has a field that is not a current-cipher envelope, not re-encryptable`,
174
+ );
175
+ return "failed";
176
+ }
177
+ if (totpClass === "current" && recoveryClass === "current") {
178
+ alreadyCurrent++;
179
+ return "skipped";
180
+ }
181
+
182
+ // decrypt failure (unknown kekVersion, tamper) throws → counted as
183
+ // failed via onRowError; the row stays untouched.
184
+ const changes: Record<string, string> = {};
185
+ if (totpClass === "rotate") {
186
+ changes["totpSecret"] = await cipher.decrypt(envelopeValues.totpSecret, { tenantId });
187
+ }
188
+ if (recoveryClass === "rotate") {
189
+ changes["recoveryCodes"] = await cipher.decrypt(envelopeValues.recoveryCodes, { tenantId });
190
+ }
191
+
192
+ const actor: SessionUser = { id: "system", tenantId, roles: SYSTEM_ROLES };
193
+ // PLAINTEXT here, not a manually-built envelope — the executor's own
194
+ // write-path (encryptForStorage) re-encrypts under the CURRENT
195
+ // injected cipher/KEK (and re-wraps the PII layer under the same,
196
+ // unrotated subject DEK), exactly like every other write to this field.
197
+ const result = await executor.update(
198
+ { id: row.id, version: row.version, changes },
199
+ actor,
200
+ tdbFor(tenantId),
201
+ );
202
+
203
+ // version_conflict == a concurrent enable/disable/regenerate beat us;
204
+ // the row now holds a fresh write, already fine.
205
+ if (!result.isSuccess) {
206
+ if (result.error.code === "version_conflict") return "skipped";
207
+ ctx.log?.warn?.(`[auth-mfa:reencrypt] executor rejected row ${row.id}`, {
208
+ code: result.error.code,
209
+ });
210
+ return "failed";
211
+ }
212
+ return "migrated";
213
+ }
214
+
215
+ const outcome = await runChunkedMigration<UserMfaRow>({
216
+ nextBatch,
217
+ migrateRow,
218
+ maxFailures,
219
+ deadlineAt: deadline,
220
+ signal: ctx.signal,
221
+ onRowError: (row, err) => {
222
+ ctx.log?.warn?.(`[auth-mfa:reencrypt] failed to re-encrypt row ${row.id}`, { err });
223
+ },
224
+ });
225
+
226
+ const result: MfaReencryptJobResult = {
227
+ migrated: outcome.migrated,
228
+ failed: outcome.failed,
229
+ alreadyCurrent,
230
+ batchesProcessed: outcome.batchesProcessed,
231
+ stoppedReason: outcome.stoppedReason,
232
+ };
233
+ ctx.log?.info?.(`[auth-mfa:reencrypt] complete: ${JSON.stringify(result)}`);
234
+ };
@@ -0,0 +1,77 @@
1
+ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
+ import { z } from "zod";
5
+ import { findUserMfaRow } from "../db/queries";
6
+ import { invalidTotpCode, mfaNotEnabled } from "../errors";
7
+ import { generateRecoveryCodes, hashRecoveryCodes } from "../recovery-codes";
8
+ import { encodeRecoveryCodes, userMfaEntity, userMfaTable } from "../schema/user-mfa";
9
+ import { verifyMfaFactor } from "../verify-factor";
10
+
11
+ export type RegenerateRecoveryOptions = {
12
+ readonly revokeAllOtherSessions?: (
13
+ userId: string,
14
+ currentSid: string | undefined,
15
+ ) => Promise<number>;
16
+ };
17
+
18
+ const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
19
+ entityName: "user-mfa",
20
+ });
21
+
22
+ // Invalidates ALL existing recovery codes (even unused ones) and issues a
23
+ // fresh set of 8 — the standard response to "I think my recovery codes
24
+ // leaked" without having to disable+re-enable TOTP itself.
25
+ export function createRegenerateRecoveryHandler(opts: RegenerateRecoveryOptions) {
26
+ return defineWriteHandler({
27
+ name: "regenerate-recovery",
28
+ schema: z.object({ code: z.string().min(6).max(9) }),
29
+ access: { openToAll: true },
30
+ handler: async (event, ctx) => {
31
+ const row = await findUserMfaRow(ctx.db, event.user);
32
+ if (!row) return mfaNotEnabled();
33
+
34
+ // Proof of possession before wiping the old codes — a TOTP code or
35
+ // (deliberately) one of the very codes about to be replaced both
36
+ // count, since either way the presented code is consumed/irrelevant
37
+ // afterward.
38
+ //
39
+ // Fail closed, matching verify.write.ts/enable-confirm-preauth.write.ts
40
+ // (#1467): without ctx.redis, verifyMfaFactor's TOTP-replay burn is
41
+ // skipped, letting an observed code be replayed within the ±1-step
42
+ // window to regenerate (and thus invalidate) recovery codes — a
43
+ // session-authed route, so pre-unwiring JWTs stay a valid attack path.
44
+ if (!ctx.redis) {
45
+ return writeFailure(
46
+ new InternalError({
47
+ message: "regenerate-recovery (mfa) requires ctx.redis for TOTP replay protection",
48
+ }),
49
+ );
50
+ }
51
+ const replay = { redis: ctx.redis, userId: event.user.id };
52
+ const verify = await verifyMfaFactor(row, event.payload.code, replay);
53
+ if (!verify.ok) return invalidTotpCode();
54
+
55
+ const newCodes = generateRecoveryCodes();
56
+ const newHashes = await hashRecoveryCodes(newCodes);
57
+
58
+ const result = await executor.update(
59
+ {
60
+ id: row.id,
61
+ version: row.version,
62
+ changes: { recoveryCodes: encodeRecoveryCodes(newHashes) },
63
+ },
64
+ event.user,
65
+ ctx.db,
66
+ );
67
+ if (!result.isSuccess) return result;
68
+
69
+ if (opts.revokeAllOtherSessions) {
70
+ await opts.revokeAllOtherSessions(event.user.id, event.user.sid);
71
+ }
72
+
73
+ // Plaintext — shown once, same as the initial enable-flow codes.
74
+ return { isSuccess: true, data: { recoveryCodes: newCodes } };
75
+ },
76
+ });
77
+ }
@@ -0,0 +1,23 @@
1
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { z } from "zod";
4
+ import { userMfaTable } from "../schema/user-mfa";
5
+
6
+ // "Is MFA enabled for me" — the one thing a settings screen needs before it
7
+ // can decide whether to show the enrollment flow or the disable/regenerate
8
+ // actions. No client-side signal (session/JWT) carries this today, so
9
+ // without this query every app builds its own row-existence check via a
10
+ // side channel. Plain fetchOne — no need for the entity's decrypting
11
+ // executor.detail() (see db/queries.ts's findUserMfaRow), existence is all
12
+ // the caller wants.
13
+ export const mfaStatusQuery = defineQueryHandler({
14
+ name: "user-mfa:status",
15
+ schema: z.object({}),
16
+ access: { openToAll: true },
17
+ handler: async (query, ctx) => {
18
+ const row = await fetchOne<{ id: string }>(ctx.db, userMfaTable, {
19
+ userId: query.user.id,
20
+ });
21
+ return { enabled: row !== undefined };
22
+ },
23
+ });
@@ -0,0 +1,179 @@
1
+ import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ import {
3
+ buildSessionRoles,
4
+ createSystemUser,
5
+ defineWriteHandler,
6
+ type SessionUser,
7
+ } from "@cosmicdrift/kumiko-framework/engine";
8
+ import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
9
+ import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
10
+ import { z } from "zod";
11
+ import { burnToken } from "../../shared";
12
+ import { USER_STATUS, UserQueries } from "../../user";
13
+ import { MFA_VERIFY_LOCKOUT_MINUTES, MFA_VERIFY_MAX_ATTEMPTS } from "../constants";
14
+ import { findUserMfaRow } from "../db/queries";
15
+ import { invalidChallengeToken, invalidTotpCode, tooManyAttempts } from "../errors";
16
+ import { verifyMfaChallengeToken } from "../mfa-challenge-token";
17
+ import {
18
+ clearMfaVerifyAttempts,
19
+ getMfaVerifyLockoutState,
20
+ recordFailedMfaVerifyAttempt,
21
+ } from "../mfa-verify-attempts";
22
+ import { encodeRecoveryCodes, userMfaEntity, userMfaTable } from "../schema/user-mfa";
23
+ import { verifyMfaFactor } from "../verify-factor";
24
+
25
+ export type MfaVerifyOptions = {
26
+ // Must match the secret the login handler signs mfa-challenge tokens
27
+ // with — distinct from setupTokenSecret (different token purpose).
28
+ readonly challengeTokenSecret: string;
29
+ readonly maxAttempts?: number;
30
+ readonly lockoutMinutes?: number;
31
+ };
32
+
33
+ const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
34
+ entityName: "user-mfa",
35
+ });
36
+
37
+ // Completes the two-step login. Runs pre-session (dispatched by the
38
+ // framework's /auth/mfa/verify route with a guest identity, same as
39
+ // login.write.ts) — everything it needs (which user, which tenant) comes
40
+ // from the challenge token, not from an authenticated caller.
41
+ export function createMfaVerifyHandler(opts: MfaVerifyOptions) {
42
+ const maxAttempts = opts.maxAttempts ?? MFA_VERIFY_MAX_ATTEMPTS;
43
+ const lockoutMinutes = opts.lockoutMinutes ?? MFA_VERIFY_LOCKOUT_MINUTES;
44
+
45
+ return defineWriteHandler({
46
+ name: "verify",
47
+ schema: z.object({ challengeToken: z.string().min(1), code: z.string().min(6).max(9) }),
48
+ access: { roles: ["all"] },
49
+ handler: async (event, ctx) => {
50
+ const verified = verifyMfaChallengeToken(
51
+ event.payload.challengeToken,
52
+ opts.challengeTokenSecret,
53
+ );
54
+ if (!verified.ok) return invalidChallengeToken();
55
+ const { userId, tenantId } = verified.payload;
56
+
57
+ // Fail closed, matching enable-confirm-preauth.write.ts (#1467):
58
+ // this route sits behind a challenge token from a successful login,
59
+ // but its own secret is still just a short TOTP/recovery code —
60
+ // silently skipping the brute-force cap and single-use burn when
61
+ // ctx.redis is absent would let a stolen challenge token be guessed
62
+ // against unboundedly, and let a recovery code be replayed via a
63
+ // reissued challenge (mfa-verify-attempts.ts's counter is deliberately
64
+ // NOT reset by reissuance, precisely to survive this).
65
+ if (!ctx.redis) {
66
+ return writeFailure(
67
+ new InternalError({
68
+ message: "verify (mfa) requires ctx.redis for the brute-force cap and single-use burn",
69
+ }),
70
+ );
71
+ }
72
+
73
+ // Per-account brute-force cap — survives challenge-token reissuance
74
+ // on purpose (see mfa-verify-attempts.ts header). Checked BEFORE the
75
+ // TOTP/recovery verify so a locked account can't be guessed against.
76
+ const state = await getMfaVerifyLockoutState(ctx.redis, userId);
77
+ if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
78
+ const now = Date.now();
79
+ if (state.lockedUntil > now) {
80
+ const retryAfterSeconds = Math.max(1, Math.ceil((state.lockedUntil - now) / 1000));
81
+ return tooManyAttempts(retryAfterSeconds);
82
+ }
83
+ }
84
+
85
+ // "system" mode: this handler runs with a guest identity whose own
86
+ // tenantId is meaningless here — the challenge token is the source
87
+ // of truth for which tenant's row to read.
88
+ const scopedDb = createTenantDb(ctx.db.raw, tenantId, "system");
89
+ const scopedUser: SessionUser = { id: userId, tenantId, roles: ["User"] };
90
+ const row = await findUserMfaRow(scopedDb, scopedUser);
91
+ // MFA got disabled between login and verify (race, or a stale
92
+ // challenge token from before a disable) — same generic error as a
93
+ // bad code, no detail leak about account state.
94
+ if (!row) return invalidChallengeToken();
95
+
96
+ const replay = { redis: ctx.redis, userId };
97
+ const verify = await verifyMfaFactor(row, event.payload.code, replay);
98
+ if (!verify.ok) {
99
+ await recordFailedMfaVerifyAttempt(ctx.redis, userId, maxAttempts, lockoutMinutes);
100
+ return invalidTotpCode();
101
+ }
102
+
103
+ // Burn the challenge token only after a successful code check — a
104
+ // still-failing attempt (wrong code) must not consume it, so a
105
+ // fat-fingered retry on the same token still works. The TOTP-counter
106
+ // burn inside verifyMfaFactor already rejects an exact-duplicate
107
+ // replay (same token + same code) with invalid_totp_code before this
108
+ // is reached, so this burn only needs to guard a DIFFERENT code
109
+ // (recovery, or a still-valid-but-not-yet-burned TOTP step) reused
110
+ // against the same challenge token.
111
+ const burnResult = await burnToken(ctx.redis, "mfa-challenge", userId, verified.expiresAtMs);
112
+ if (burnResult === "already-used") return invalidChallengeToken();
113
+
114
+ // Recovery-code use consumes the code — persist the reduced hash-list
115
+ // immediately so it can't be replayed. Without this a recovery code
116
+ // would work forever instead of being single-use.
117
+ if (verify.method === "recovery") {
118
+ const updateResult = await executor.update(
119
+ {
120
+ id: row.id,
121
+ version: row.version,
122
+ changes: { recoveryCodes: encodeRecoveryCodes(verify.remainingHashes) },
123
+ },
124
+ scopedUser,
125
+ scopedDb,
126
+ );
127
+ if (!updateResult.isSuccess) return updateResult;
128
+ }
129
+
130
+ await clearMfaVerifyAttempts(ctx.redis, userId);
131
+
132
+ // Re-derive the full session the way login.write.ts would have, for
133
+ // the SAME tenant the challenge token already committed to (no
134
+ // "pick a tenant" step here — that already happened at login time).
135
+ const systemUser = createSystemUser(tenantId, ["SystemAdmin"]);
136
+ const userRow = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
137
+ id: userId,
138
+ })) as { roles?: string | null; status?: string } | null; // @cast-boundary engine-payload
139
+
140
+ // Re-check status + membership the way login.write.ts does after its
141
+ // password check — the challenge token only proves "password was
142
+ // correct 10 minutes ago", not that the account is still loginable
143
+ // now. Deliberately NOT re-checking strictEmailVerification here:
144
+ // MfaVerifyOptions has no such flag, and email verification can't
145
+ // regress between login and verify within the 10-minute window.
146
+ if (!userRow) return invalidChallengeToken();
147
+ if (
148
+ userRow?.status === USER_STATUS.Restricted ||
149
+ userRow?.status === USER_STATUS.DeletionRequested ||
150
+ userRow?.status === USER_STATUS.Deleted
151
+ ) {
152
+ return invalidChallengeToken();
153
+ }
154
+
155
+ const globalRoles = parseRoles(userRow?.roles ?? null);
156
+
157
+ const memberships = (await ctx.queryAs(systemUser, "tenant:query:memberships", {
158
+ userId,
159
+ })) as ReadonlyArray<{ tenantId: string; roles: readonly string[] }>; // @cast-boundary engine-payload
160
+ const membership = memberships.find((m) => m.tenantId === tenantId);
161
+ // Membership revoked between login and verify (race, or a stale
162
+ // challenge token from before removal) — login.write.ts would have
163
+ // refused with noMembership() at the same juncture; mirror it here
164
+ // instead of silently falling back to globalRoles only.
165
+ if (!membership) return invalidChallengeToken();
166
+ // buildSessionRoles calls stripForbiddenMembershipRoles to strip reserved
167
+ // roles from the membership portion (globalRoles keeps SystemAdmin) —
168
+ // read-time backstop against a rebuild-resurrected role.
169
+ const mergedRoles = buildSessionRoles(globalRoles, membership.roles);
170
+
171
+ const baseSession: SessionUser = { id: userId, tenantId, roles: mergedRoles };
172
+ const claims = await ctx.resolveAuthClaims(baseSession);
173
+ const session: SessionUser =
174
+ Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
175
+
176
+ return { isSuccess: true, data: { kind: "mfa-verify-success", session } };
177
+ },
178
+ });
179
+ }
@@ -0,0 +1,12 @@
1
+ // Server-side, boot-validated i18n keys — screen titles/descriptions the
2
+ // engine requires every registered r.screen to declare. Distinct from
3
+ // web/i18n.ts (the client's free-form UI-string bundle) — see
4
+ // personal-access-tokens/i18n.ts for the same split.
5
+ type LocalizedString = { readonly de: string; readonly en: string };
6
+
7
+ export const AUTH_MFA_FEATURE_I18N: Readonly<Record<string, LocalizedString>> = {
8
+ "screen:auth-mfa-enable.title": {
9
+ de: "Zwei-Faktor-Authentifizierung",
10
+ en: "Two-factor authentication",
11
+ },
12
+ };
@@ -0,0 +1,19 @@
1
+ export { base32Decode } from "./base32";
2
+ export { type MfaRequiredPolicy, mfaRequiredConfigHandle } from "./config";
3
+ export {
4
+ AUTH_MFA_FEATURE,
5
+ AuthMfaHandlers,
6
+ AuthMfaQueries,
7
+ MFA_ENABLE_SCREEN_ID,
8
+ } from "./constants";
9
+ export type {
10
+ AuthMfaFeatureOptions,
11
+ BindMfaRevokeAllOtherSessions,
12
+ } from "./feature";
13
+ export {
14
+ bindMfaRevokeAllOtherSessionsFromFeature,
15
+ createAuthMfaFeature,
16
+ mfaStatusCheckerFromFeature,
17
+ } from "./feature";
18
+ export type { MfaStatusChecker, MfaStatusCheckResult } from "./mfa-status-checker";
19
+ export { userMfaEntity, userMfaTable } from "./schema/user-mfa";