@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,170 @@
1
+ import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { mfaRequiredConfigKey } from "./config";
3
+ import { MFA_ENABLE_SCREEN_ID } from "./constants";
4
+ import { createDisableHandler } from "./handlers/disable.write";
5
+ import { createEnableConfirmHandler } from "./handlers/enable-confirm.write";
6
+ import { createEnableConfirmPreauthHandler } from "./handlers/enable-confirm-preauth.write";
7
+ import { createEnableStartHandler } from "./handlers/enable-start.write";
8
+ import { createEnableStartPreauthHandler } from "./handlers/enable-start-preauth.write";
9
+ import { mfaReencryptJob } from "./handlers/reencrypt.job";
10
+ import { createRegenerateRecoveryHandler } from "./handlers/regenerate-recovery.write";
11
+ import { mfaStatusQuery } from "./handlers/status.query";
12
+ import { createMfaVerifyHandler } from "./handlers/verify.write";
13
+ import { AUTH_MFA_FEATURE_I18N } from "./i18n";
14
+ import { createMfaStatusChecker, type MfaStatusChecker } from "./mfa-status-checker";
15
+ import { userMfaEntity } from "./schema/user-mfa";
16
+
17
+ export type AuthMfaFeatureOptions = {
18
+ // HMAC secret for the stateless enable-flow token (carries the generated
19
+ // TOTP secret + recovery-code hashes between enable.start and
20
+ // enable.confirm). Distinct secret from any other token flow in the app —
21
+ // do not reuse passwordReset.hmacSecret.
22
+ readonly setupTokenSecret: string;
23
+ // otpauth:// URI issuer — shown in the user's authenticator app next to
24
+ // the account label ("Kumiko: jane@example.com").
25
+ readonly issuer: string;
26
+ // HMAC secret for the login-flow challenge token (carries {userId,
27
+ // tenantId} between the password-check and /auth/mfa/verify). Distinct
28
+ // secret from setupTokenSecret — a compromised setup-token secret must
29
+ // not also forge login challenges.
30
+ readonly challengeTokenSecret: string;
31
+ };
32
+
33
+ export type BindMfaRevokeAllOtherSessions = (
34
+ revoker: (userId: string, currentSid: string | undefined) => Promise<number>,
35
+ ) => void;
36
+
37
+ // Reads the late-bind setter off a mounted auth-mfa feature's exports —
38
+ // mirrors sessions' own bindAutoRevokeFromFeature. run{Prod,Dev}App call
39
+ // this once the sessions feature (if mounted) has produced a concrete
40
+ // sessionRevokeAllOthers callback.
41
+ export function bindMfaRevokeAllOtherSessionsFromFeature(
42
+ feature: FeatureDefinition,
43
+ ): BindMfaRevokeAllOtherSessions | undefined {
44
+ const exports = feature.exports;
45
+ if (exports && typeof exports === "object" && "bindRevokeAllOtherSessions" in exports) {
46
+ const { bindRevokeAllOtherSessions } = exports as {
47
+ bindRevokeAllOtherSessions: unknown;
48
+ };
49
+ if (typeof bindRevokeAllOtherSessions === "function") {
50
+ // @cast-boundary exports-walk — feature.exports is untyped by design
51
+ return bindRevokeAllOtherSessions as BindMfaRevokeAllOtherSessions;
52
+ }
53
+ }
54
+ return undefined;
55
+ }
56
+
57
+ // Reads the eagerly-built `checkMfaStatus` off a mounted auth-mfa feature's
58
+ // exports — no bind-setter needed (see the comment where it's built).
59
+ export function mfaStatusCheckerFromFeature(
60
+ feature: FeatureDefinition,
61
+ ): MfaStatusChecker | undefined {
62
+ const exports = feature.exports;
63
+ if (exports && typeof exports === "object" && "checkMfaStatus" in exports) {
64
+ const { checkMfaStatus } = exports as { checkMfaStatus: unknown };
65
+ if (typeof checkMfaStatus === "function") {
66
+ // @cast-boundary exports-walk — feature.exports is untyped by design
67
+ return checkMfaStatus as MfaStatusChecker;
68
+ }
69
+ }
70
+ return undefined;
71
+ }
72
+
73
+ export function createAuthMfaFeature(opts: AuthMfaFeatureOptions): FeatureDefinition {
74
+ return defineFeature("auth-mfa", (r) => {
75
+ r.describe(
76
+ "TOTP-based two-factor authentication: enable/disable flow with QR-code setup, 8 single-use recovery codes, and (once wired into a login flow) a second login step after password verification. Secrets are envelope-encrypted at rest via the same MasterKeyProvider as `secrets`/`config`.",
77
+ );
78
+ r.uiHints({
79
+ displayLabel: "2FA / TOTP",
80
+ category: "identity",
81
+ recommended: true,
82
+ });
83
+ r.requires("user");
84
+ r.requires("config");
85
+ // verify.write.ts dispatches "tenant:query:memberships" — without this
86
+ // the app boots clean and the first MFA login dies at runtime on an
87
+ // unknown handler instead of failing at mount time.
88
+ r.requires("tenant");
89
+ r.config("required", mfaRequiredConfigKey());
90
+
91
+ // Dormant custom-screen — the client maps MFA_ENABLE_SCREEN_ID to
92
+ // MfaEnableScreen (see personal-access-tokens/feature.ts for the same
93
+ // convention). App places it via r.nav in its logged-in settings area.
94
+ r.screen({
95
+ id: MFA_ENABLE_SCREEN_ID,
96
+ type: "custom",
97
+ renderer: { react: { __component: "MfaEnableScreen" } },
98
+ access: { openToAll: true },
99
+ });
100
+ r.translations({ keys: AUTH_MFA_FEATURE_I18N });
101
+
102
+ // KEK-rotation for totpSecret (entity-field encryption). Manual
103
+ // trigger — ops runs it once after adding a new master key version,
104
+ // same operator workflow as config's own reencrypt job.
105
+ r.job("reencrypt", { trigger: { manual: true } }, mfaReencryptJob);
106
+
107
+ r.entity("user-mfa", userMfaEntity);
108
+
109
+ // Late-bound by run-prod-app once the sessions feature (if mounted) has
110
+ // produced a concrete `sessionRevokeAllOthers` callback — mirrors
111
+ // sessions' own bindAutoRevokeOnPasswordChange. Shared by every handler
112
+ // that changes MFA state (enable/disable/regenerate), so each one's
113
+ // "log out every other session" behavior tracks the same wiring.
114
+ let revokeAllOtherSessions:
115
+ | ((userId: string, currentSid: string | undefined) => Promise<number>)
116
+ | undefined;
117
+ const sharedRevoker = (userId: string, currentSid: string | undefined): Promise<number> =>
118
+ revokeAllOtherSessions?.(userId, currentSid) ?? Promise.resolve(0);
119
+
120
+ const handlers = {
121
+ enableStart: r.writeHandler(
122
+ createEnableStartHandler({ setupTokenSecret: opts.setupTokenSecret, issuer: opts.issuer }),
123
+ ),
124
+ enableStartPreauth: r.writeHandler(
125
+ createEnableStartPreauthHandler({
126
+ challengeTokenSecret: opts.challengeTokenSecret,
127
+ setupTokenSecret: opts.setupTokenSecret,
128
+ issuer: opts.issuer,
129
+ }),
130
+ ),
131
+ enableConfirm: r.writeHandler(
132
+ createEnableConfirmHandler({
133
+ setupTokenSecret: opts.setupTokenSecret,
134
+ revokeAllOtherSessions: sharedRevoker,
135
+ }),
136
+ ),
137
+ enableConfirmPreauth: r.writeHandler(
138
+ createEnableConfirmPreauthHandler({
139
+ setupTokenSecret: opts.setupTokenSecret,
140
+ revokeAllOtherSessions: sharedRevoker,
141
+ }),
142
+ ),
143
+ disable: r.writeHandler(createDisableHandler({ revokeAllOtherSessions: sharedRevoker })),
144
+ regenerateRecovery: r.writeHandler(
145
+ createRegenerateRecoveryHandler({ revokeAllOtherSessions: sharedRevoker }),
146
+ ),
147
+ verify: r.writeHandler(
148
+ createMfaVerifyHandler({ challengeTokenSecret: opts.challengeTokenSecret }),
149
+ ),
150
+ };
151
+
152
+ const queries = {
153
+ status: r.queryHandler(mfaStatusQuery),
154
+ };
155
+
156
+ // No late-bind needed (unlike sharedRevoker) — this checker only needs
157
+ // the HandlerContext the CALLER already has (login.write.ts runs it
158
+ // from inside its own dispatcher call), not a raw db handle assembled
159
+ // at app-boot time.
160
+ const checkMfaStatus = createMfaStatusChecker({
161
+ challengeTokenSecret: opts.challengeTokenSecret,
162
+ });
163
+
164
+ const bindRevokeAllOtherSessions: BindMfaRevokeAllOtherSessions = (revoker) => {
165
+ revokeAllOtherSessions = revoker;
166
+ };
167
+
168
+ return { handlers, queries, bindRevokeAllOtherSessions, checkMfaStatus };
169
+ });
170
+ }
@@ -0,0 +1,67 @@
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 { userMfaEntity, userMfaTable } from "../schema/user-mfa";
8
+ import { verifyMfaFactor } from "../verify-factor";
9
+
10
+ export type DisableOptions = {
11
+ readonly revokeAllOtherSessions?: (
12
+ userId: string,
13
+ currentSid: string | undefined,
14
+ ) => Promise<number>;
15
+ };
16
+
17
+ const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
18
+ entityName: "user-mfa",
19
+ });
20
+
21
+ // A TOTP code or a recovery code both prove possession of the second
22
+ // factor — either is accepted to turn MFA back off. Password alone is NOT
23
+ // enough: that would make MFA worthless against exactly the "stolen
24
+ // password" scenario it exists to defend.
25
+ export function createDisableHandler(opts: DisableOptions) {
26
+ return defineWriteHandler({
27
+ name: "disable",
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
+ // Fail closed, matching verify.write.ts/enable-confirm-preauth.write.ts
35
+ // (#1467): without ctx.redis, verifyMfaFactor's TOTP-replay burn is
36
+ // skipped, and a code observed once (phishing proxy/shoulder-surfing)
37
+ // could be replayed within the ±1-step window to disable MFA outright.
38
+ // "no ctx.redis means no one gets through MFA login" doesn't cover
39
+ // this route — it's session-authed, and JWTs issued before Redis was
40
+ // unwired stay valid until they expire.
41
+ if (!ctx.redis) {
42
+ return writeFailure(
43
+ new InternalError({
44
+ message: "disable (mfa) requires ctx.redis for TOTP replay protection",
45
+ }),
46
+ );
47
+ }
48
+ const replay = { redis: ctx.redis, userId: event.user.id };
49
+ const verify = await verifyMfaFactor(row, event.payload.code, replay);
50
+ if (!verify.ok) return invalidTotpCode();
51
+
52
+ const result = await executor.delete({ id: row.id }, event.user, ctx.db);
53
+ if (!result.isSuccess) return result;
54
+
55
+ // Disabling MFA is a security-relevant state change — same
56
+ // auto-revoke as enable, in case the person doing this isn't the
57
+ // legitimate account owner (attacker with a stolen-but-not-yet-
58
+ // MFA-locked session, or a recovery code obtained via social
59
+ // engineering).
60
+ if (opts.revokeAllOtherSessions) {
61
+ await opts.revokeAllOtherSessions(event.user.id, event.user.sid);
62
+ }
63
+
64
+ return { isSuccess: true, data: { disabled: true } };
65
+ },
66
+ });
67
+ }
@@ -0,0 +1,200 @@
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 { Temporal } from "temporal-polyfill";
11
+ import { z } from "zod";
12
+ import { burnToken } from "../../shared";
13
+ import { USER_STATUS, UserQueries } from "../../user";
14
+ import { base32Decode } from "../base32";
15
+ import { MFA_VERIFY_LOCKOUT_MINUTES, MFA_VERIFY_MAX_ATTEMPTS } from "../constants";
16
+ import { findUserMfaRow } from "../db/queries";
17
+ import { invalidSetupToken, invalidTotpCode, mfaAlreadyEnabled, tooManyAttempts } from "../errors";
18
+ import { verifyMfaSetupToken } from "../mfa-setup-token";
19
+ import {
20
+ clearMfaVerifyAttempts,
21
+ getMfaVerifyLockoutState,
22
+ recordFailedMfaVerifyAttempt,
23
+ } from "../mfa-verify-attempts";
24
+ import { encodeRecoveryCodes, userMfaEntity, userMfaTable } from "../schema/user-mfa";
25
+ import { verifyTotp } from "../totp";
26
+
27
+ export type EnableConfirmPreauthOptions = {
28
+ // Must match the secret enable-start-preauth.write.ts signs setupTokens
29
+ // with — same secret + format as the session-authed enable-confirm.
30
+ readonly setupTokenSecret: string;
31
+ readonly maxAttempts?: number;
32
+ readonly lockoutMinutes?: number;
33
+ // Wired late by run-prod-app, same shared callback as enable-confirm and
34
+ // enable-start's session-authed siblings.
35
+ readonly revokeAllOtherSessions?: (
36
+ userId: string,
37
+ currentSid: string | undefined,
38
+ ) => Promise<number>;
39
+ };
40
+
41
+ const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
42
+ entityName: "user-mfa",
43
+ });
44
+
45
+ // Pre-auth twin of enable-confirm.write.ts: identity comes from the
46
+ // verified setupToken (minted by enable-start-preauth.write.ts), not from
47
+ // event.user — there is no session yet. Combines enable-confirm's
48
+ // enrollment-completion logic with verify.write.ts's session-derivation
49
+ // (membership lookup, status re-check, buildSessionRoles) since this IS
50
+ // the completion of the blocked login, not just an enrollment step — a
51
+ // successful call here logs the user in.
52
+ export function createEnableConfirmPreauthHandler(opts: EnableConfirmPreauthOptions) {
53
+ const maxAttempts = opts.maxAttempts ?? MFA_VERIFY_MAX_ATTEMPTS;
54
+ const lockoutMinutes = opts.lockoutMinutes ?? MFA_VERIFY_LOCKOUT_MINUTES;
55
+
56
+ return defineWriteHandler({
57
+ name: "enable-confirm-preauth",
58
+ schema: z.object({
59
+ setupToken: z.string().min(1),
60
+ code: z.string().length(6),
61
+ }),
62
+ access: { roles: ["all"] },
63
+ handler: async (event, ctx) => {
64
+ const verify = verifyMfaSetupToken(event.payload.setupToken, opts.setupTokenSecret);
65
+ if (!verify.ok) return invalidSetupToken();
66
+ // A setupToken minted for the session-authed enable-start never
67
+ // carries tenantId (see mfa-setup-token.ts) — reject it here rather
68
+ // than falling back to a guest tenant, that token was never meant to
69
+ // reach this pre-auth endpoint.
70
+ const { userId, tenantId } = verify.payload;
71
+ if (tenantId === undefined) return invalidSetupToken();
72
+
73
+ // Fail closed: unlike verify.write.ts/enable-confirm.write.ts, this
74
+ // endpoint has NO gate before it (access: { roles: ["all"] }, no JWT,
75
+ // no prior challenge-token from a successful login) — its only secret
76
+ // is a 6-digit TOTP code. Silently skipping the brute-force cap and
77
+ // single-use burn when ctx.redis is absent would let the setup token
78
+ // be guessed against unboundedly until its TTL expires. The route's
79
+ // IP rate limiter is not a substitute: it's IP-scoped and resets on
80
+ // every success.
81
+ if (!ctx.redis) {
82
+ return writeFailure(
83
+ new InternalError({
84
+ message:
85
+ "enable-confirm-preauth requires ctx.redis for the brute-force cap and single-use burn",
86
+ }),
87
+ );
88
+ }
89
+
90
+ // Same guessing surface as /auth/mfa/verify (a short TOTP code) and
91
+ // the same per-userId cap — deliberately shared counter, see
92
+ // mfa-verify-attempts.ts.
93
+ const state = await getMfaVerifyLockoutState(ctx.redis, userId);
94
+ if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
95
+ const now = Date.now();
96
+ if (state.lockedUntil > now) {
97
+ const retryAfterSeconds = Math.max(1, Math.ceil((state.lockedUntil - now) / 1000));
98
+ return tooManyAttempts(retryAfterSeconds);
99
+ }
100
+ }
101
+
102
+ const secret = base32Decode(verify.payload.totpSecretBase32);
103
+ if (verifyTotp(secret, event.payload.code) === false) {
104
+ await recordFailedMfaVerifyAttempt(ctx.redis, userId, maxAttempts, lockoutMinutes);
105
+ return invalidTotpCode();
106
+ }
107
+
108
+ // Burn the setup token on the first successful confirm — same
109
+ // single-use guarantee as enable-confirm.write.ts.
110
+ const burnResult = await burnToken(ctx.redis, "mfa-setup", userId, verify.expiresAtMs);
111
+ if (burnResult === "already-used") return invalidSetupToken();
112
+
113
+ // "system" mode: no session exists yet, tenantId comes from the
114
+ // verified token, mirroring enable-start-preauth.write.ts.
115
+ const scopedDb = createTenantDb(ctx.db.raw, tenantId, "system");
116
+ const scopedUser: SessionUser = { id: userId, tenantId, roles: ["User"] };
117
+ const existing = await findUserMfaRow(scopedDb, scopedUser);
118
+ if (existing) return mfaAlreadyEnabled();
119
+
120
+ // Status + membership must be verified BEFORE the entity-write below —
121
+ // an admin can restrict/delete the account or revoke its tenant
122
+ // membership in the window between preauth-enable-start and
123
+ // preauth-confirm (up to MFA_SETUP_TOKEN_TTL_MINUTES). Checking after
124
+ // the write left two irreversible side effects behind an
125
+ // already-signed, non-forgeable setupToken that then just returns
126
+ // invalidSetupToken(): the MFA row persisted and (formerly) all other
127
+ // sessions revoked — with mfaAlreadyEnabled() blocking any
128
+ // re-enrollment until an operator manually disables MFA. NOT fixed by
129
+ // this ordering: burnToken() above still runs before this check, so a
130
+ // mid-flight restricted/deleted user still burns their setup token
131
+ // (framework#1523/5) — moving the burn later would need care around
132
+ // the existing userId-unique-constraint backstop between the
133
+ // `existing` check above and executor.create() below.
134
+ const systemUser = createSystemUser(tenantId, ["SystemAdmin"]);
135
+ const userRow = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
136
+ id: userId,
137
+ })) as { roles?: string | null; status?: string } | null; // @cast-boundary engine-payload
138
+
139
+ if (!userRow) return invalidSetupToken();
140
+ if (
141
+ userRow?.status === USER_STATUS.Restricted ||
142
+ userRow?.status === USER_STATUS.DeletionRequested ||
143
+ userRow?.status === USER_STATUS.Deleted
144
+ ) {
145
+ return invalidSetupToken();
146
+ }
147
+
148
+ const globalRoles = parseRoles(userRow?.roles ?? null);
149
+
150
+ const memberships = (await ctx.queryAs(systemUser, "tenant:query:memberships", {
151
+ userId,
152
+ })) as ReadonlyArray<{ tenantId: string; roles: readonly string[] }>; // @cast-boundary engine-payload
153
+ const membership = memberships.find((m) => m.tenantId === tenantId);
154
+ if (!membership) return invalidSetupToken();
155
+
156
+ const result = await executor.create(
157
+ {
158
+ userId,
159
+ totpSecret: verify.payload.totpSecretBase32,
160
+ recoveryCodes: encodeRecoveryCodes(verify.payload.recoveryCodeHashes),
161
+ enabledAt: Temporal.Now.instant(),
162
+ lastUsedAt: null,
163
+ },
164
+ scopedUser,
165
+ scopedDb,
166
+ );
167
+ if (!result.isSuccess) return result;
168
+
169
+ // ctx.redis is guaranteed by the fail-closed check above.
170
+ await clearMfaVerifyAttempts(ctx.redis, userId);
171
+
172
+ // Re-derive the full session the way verify.write.ts does — the
173
+ // setupToken only proves "password + new TOTP secret", not roles;
174
+ // roles come from the membership lookup above, same as every other
175
+ // login-completing handler.
176
+ // buildSessionRoles calls stripForbiddenMembershipRoles to strip reserved
177
+ // roles from the membership portion (globalRoles keeps SystemAdmin) —
178
+ // read-time backstop against a rebuild-resurrected role, same as
179
+ // verify.write.ts.
180
+ const mergedRoles = buildSessionRoles(globalRoles, membership.roles);
181
+
182
+ const baseSession: SessionUser = { id: userId, tenantId, roles: mergedRoles };
183
+ const claims = await ctx.resolveAuthClaims(baseSession);
184
+ const session: SessionUser =
185
+ Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
186
+
187
+ // Last step before returning success: every gate above (status,
188
+ // membership, entity-write) has passed, so this is the point of no
189
+ // return anyway — revoking here instead of right after the write
190
+ // means a rejected confirm never touches the user's other sessions.
191
+ // No currentSid to exclude — this is the first session for this
192
+ // login, not a follow-up on an already-running one.
193
+ if (opts.revokeAllOtherSessions) {
194
+ await opts.revokeAllOtherSessions(userId, undefined);
195
+ }
196
+
197
+ return { isSuccess: true, data: { kind: "mfa-preauth-confirm-success", session } };
198
+ },
199
+ });
200
+ }
@@ -0,0 +1,106 @@
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 { Temporal } from "temporal-polyfill";
5
+ import { z } from "zod";
6
+ import { burnToken } from "../../shared";
7
+ import { base32Decode } from "../base32";
8
+ import { findUserMfaRow } from "../db/queries";
9
+ import { invalidSetupToken, invalidTotpCode, mfaAlreadyEnabled } from "../errors";
10
+ import { verifyMfaSetupToken } from "../mfa-setup-token";
11
+ import { encodeRecoveryCodes, userMfaEntity, userMfaTable } from "../schema/user-mfa";
12
+ import { verifyTotp } from "../totp";
13
+
14
+ export type EnableConfirmOptions = {
15
+ readonly setupTokenSecret: string;
16
+ // Wired late by run-prod-app once the sessions feature (if mounted) is
17
+ // concrete — see sessions/session-callbacks.ts. Absent when sessions
18
+ // isn't mounted: enabling MFA just doesn't revoke other sessions.
19
+ readonly revokeAllOtherSessions?: (
20
+ userId: string,
21
+ currentSid: string | undefined,
22
+ ) => Promise<number>;
23
+ };
24
+
25
+ const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
26
+ entityName: "user-mfa",
27
+ });
28
+
29
+ export function createEnableConfirmHandler(opts: EnableConfirmOptions) {
30
+ return defineWriteHandler({
31
+ name: "enable-confirm",
32
+ schema: z.object({
33
+ setupToken: z.string().min(1),
34
+ code: z.string().length(6),
35
+ }),
36
+ access: { openToAll: true },
37
+ handler: async (event, ctx) => {
38
+ const verify = verifyMfaSetupToken(event.payload.setupToken, opts.setupTokenSecret);
39
+ if (!verify.ok) return invalidSetupToken();
40
+ // A setup token minted for one user can't be redeemed by another —
41
+ // guards against a leaked/shared token, not just tampering (the HMAC
42
+ // already rules that out).
43
+ if (verify.payload.userId !== event.user.id) return invalidSetupToken();
44
+
45
+ // Fail closed, matching verify.write.ts/enable-confirm-preauth.write.ts
46
+ // (#1467): session-authed (lower risk — the caller already has a
47
+ // valid JWT), but silently skipping the single-use burn here would
48
+ // still let a double-confirm race complete twice instead of the
49
+ // second seeing invalidSetupToken(). Checked before the TOTP verify
50
+ // for the same reason the other two handlers check it early: fail
51
+ // closed before doing any of the guarded work, not partway through.
52
+ if (!ctx.redis) {
53
+ return writeFailure(
54
+ new InternalError({
55
+ message: "enable-confirm requires ctx.redis for the single-use setup-token burn",
56
+ }),
57
+ );
58
+ }
59
+
60
+ const secret = base32Decode(verify.payload.totpSecretBase32);
61
+ if (verifyTotp(secret, event.payload.code) === false) return invalidTotpCode();
62
+
63
+ // Burn the setup token on the first successful confirm — without this
64
+ // a `disable` doesn't retire it (re-enabling with the secret the user
65
+ // thinks they discarded), and two parallel confirms both proceed to
66
+ // executor.create instead of the second seeing mfa_already_enabled().
67
+ // ponytail: burned here, not unburned on a later executor.create
68
+ // failure (unlike login.write.ts's unburnToken pattern) — self-service
69
+ // enable-start always mints a fresh setup token on demand (no mailed
70
+ // link to go stale), so the realistic remaining failure path (a
71
+ // double-confirm race where executor.create itself fails, e.g. a
72
+ // concurrent mfa_already_enabled() write) is meant to leave the burn
73
+ // standing. Add unburnToken here if create-failure retries turn out
74
+ // to matter.
75
+ const burnResult = await burnToken(ctx.redis, "mfa-setup", event.user.id, verify.expiresAtMs);
76
+ if (burnResult === "already-used") return invalidSetupToken();
77
+
78
+ const existing = await findUserMfaRow(ctx.db, event.user);
79
+ if (existing) return mfaAlreadyEnabled();
80
+
81
+ const result = await executor.create(
82
+ {
83
+ userId: event.user.id,
84
+ totpSecret: verify.payload.totpSecretBase32,
85
+ // Stored as a JSON string — see schema/user-mfa.ts (recoveryCodes
86
+ // is an encrypted+userOwned text field, both layers need a string).
87
+ recoveryCodes: encodeRecoveryCodes(verify.payload.recoveryCodeHashes),
88
+ enabledAt: Temporal.Now.instant(),
89
+ lastUsedAt: null,
90
+ },
91
+ event.user,
92
+ ctx.db,
93
+ );
94
+ if (!result.isSuccess) return result;
95
+
96
+ // Confirming enable proves possession of a second factor — every
97
+ // other session (a stolen-cookie attacker included) gets logged out.
98
+ // The session that just did this confirm keeps running.
99
+ if (opts.revokeAllOtherSessions) {
100
+ await opts.revokeAllOtherSessions(event.user.id, event.user.sid);
101
+ }
102
+
103
+ return { isSuccess: true, data: { enabled: true } };
104
+ },
105
+ });
106
+ }
@@ -0,0 +1,87 @@
1
+ import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ import { defineWriteHandler, type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { z } from "zod";
4
+ import { base32Encode } from "../base32";
5
+ import { MFA_SETUP_TOKEN_TTL_MINUTES } from "../constants";
6
+ import { findUserMfaRow } from "../db/queries";
7
+ import { invalidChallengeToken, mfaAlreadyEnabled } from "../errors";
8
+ import { verifyMfaPreauthSetupToken } from "../mfa-preauth-setup-token";
9
+ import { signMfaSetupToken } from "../mfa-setup-token";
10
+ import { buildOtpauthUri } from "../otpauth-uri";
11
+ import { generateRecoveryCodes, hashRecoveryCodes } from "../recovery-codes";
12
+ import { generateTotpSecret } from "../totp";
13
+
14
+ export type EnableStartPreauthOptions = {
15
+ // Verifies the incoming preauthSetupToken — must match login.write.ts's
16
+ // mfaStatusChecker (challengeTokenSecret), NOT setupTokenSecret.
17
+ readonly challengeTokenSecret: string;
18
+ // Signs the outgoing secret-carrying setupToken — same secret + token
19
+ // format as enable-start.write.ts, so the later pre-auth confirm step can
20
+ // verify it with the existing verifyMfaSetupToken.
21
+ readonly setupTokenSecret: string;
22
+ readonly issuer: string;
23
+ };
24
+
25
+ // Pre-auth twin of enable-start.write.ts: identity comes from a verified
26
+ // preauthSetupToken (minted by login.write.ts when enforcement policy
27
+ // blocks an unenrolled user), not from event.user — there is no session at
28
+ // this point. Runs pre-session the same way verify.write.ts does (dispatched
29
+ // by the framework's /auth/mfa/preauth-enable-start route with GUEST_USER).
30
+ export function createEnableStartPreauthHandler(opts: EnableStartPreauthOptions) {
31
+ return defineWriteHandler({
32
+ name: "enable-start-preauth",
33
+ schema: z.object({
34
+ preauthSetupToken: z.string().min(1),
35
+ accountLabel: z.string().min(1).max(200),
36
+ }),
37
+ access: { roles: ["all"] },
38
+ handler: async (event, ctx) => {
39
+ const verified = verifyMfaPreauthSetupToken(
40
+ event.payload.preauthSetupToken,
41
+ opts.challengeTokenSecret,
42
+ );
43
+ // Same generic "sign in again" error as an expired/invalid MFA
44
+ // challenge token — both mean the login-flow token expired, no
45
+ // distinct i18n needed (see #1465's scoping comment).
46
+ if (!verified.ok) return invalidChallengeToken();
47
+ const { userId, tenantId } = verified.payload;
48
+
49
+ // "system" mode: the guest dispatch identity's own tenantId is
50
+ // meaningless here — the preauthSetupToken is the source of truth
51
+ // for which tenant's row to read, mirroring verify.write.ts.
52
+ const scopedDb = createTenantDb(ctx.db.raw, tenantId, "system");
53
+ const scopedUser: SessionUser = { id: userId, tenantId, roles: ["User"] };
54
+ const existing = await findUserMfaRow(scopedDb, scopedUser);
55
+ if (existing) return mfaAlreadyEnabled();
56
+
57
+ const secret = generateTotpSecret();
58
+ const recoveryCodes = generateRecoveryCodes();
59
+ const recoveryCodeHashes = await hashRecoveryCodes(recoveryCodes);
60
+
61
+ const { token: setupToken } = signMfaSetupToken(
62
+ {
63
+ userId,
64
+ totpSecretBase32: base32Encode(secret),
65
+ recoveryCodeHashes,
66
+ tenantId,
67
+ },
68
+ MFA_SETUP_TOKEN_TTL_MINUTES,
69
+ opts.setupTokenSecret,
70
+ );
71
+
72
+ return {
73
+ isSuccess: true,
74
+ data: {
75
+ setupToken,
76
+ otpauthUri: buildOtpauthUri({
77
+ issuer: opts.issuer,
78
+ accountLabel: event.payload.accountLabel,
79
+ secret,
80
+ }),
81
+ // Plaintext — this is the one and only time these are shown.
82
+ recoveryCodes,
83
+ },
84
+ };
85
+ },
86
+ });
87
+ }