@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,319 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
3
+ import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
4
+ import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
5
+ import {
6
+ createTestUser,
7
+ setupTestStack,
8
+ type TestStack,
9
+ unsafeCreateEntityTable,
10
+ unsafePushTables,
11
+ } from "@cosmicdrift/kumiko-framework/stack";
12
+ import {
13
+ createTestEnvelopeCipher,
14
+ expectErrorIncludes,
15
+ seedRow,
16
+ } from "@cosmicdrift/kumiko-framework/testing";
17
+ import { createConfigFeature } from "../../config";
18
+ import { createConfigResolver } from "../../config/resolver";
19
+ import { configValuesTable } from "../../config/table";
20
+ import { createTenantFeature } from "../../tenant";
21
+ import { tenantMembershipsTable } from "../../tenant/membership-table";
22
+ import { tenantEntity } from "../../tenant/schema/tenant";
23
+ import { USER_STATUS } from "../../user";
24
+ import { createUserFeature } from "../../user/feature";
25
+ import { userEntity, userTable } from "../../user/schema/user";
26
+ import { base32Decode } from "../base32";
27
+ import { AuthMfaHandlers } from "../constants";
28
+ import { createAuthMfaFeature } from "../feature";
29
+ import { signMfaChallengeToken } from "../mfa-challenge-token";
30
+ import { userMfaEntity } from "../schema/user-mfa";
31
+ import { currentTotpCode } from "../totp";
32
+
33
+ let stack: TestStack;
34
+
35
+ const SETUP_TOKEN_SECRET = "test-mfa-setup-secret-at-least-32-bytes-long!!";
36
+ const CHALLENGE_TOKEN_SECRET = "test-mfa-challenge-secret-at-least-32-bytes!!";
37
+
38
+ // Handler access is `{ roles: ["all"] }`, matching how the framework route
39
+ // dispatches with a guest identity — a literal here is enough since the
40
+ // handler derives everything from the challenge-token payload, not from
41
+ // event.user.
42
+ const GUEST: SessionUser = {
43
+ id: "00000000-0000-0000-0000-000000000000",
44
+ tenantId: "00000000-0000-4000-8000-000000000001" as TenantId,
45
+ roles: ["all"],
46
+ };
47
+
48
+ beforeAll(async () => {
49
+ const encryption = createTestEnvelopeCipher();
50
+ configureEntityFieldEncryption(encryption);
51
+ const resolver = createConfigResolver({ cipher: encryption });
52
+ stack = await setupTestStack({
53
+ features: [
54
+ createConfigFeature(),
55
+ createUserFeature(),
56
+ createTenantFeature(),
57
+ createAuthMfaFeature({
58
+ setupTokenSecret: SETUP_TOKEN_SECRET,
59
+ issuer: "Kumiko Test",
60
+ challengeTokenSecret: CHALLENGE_TOKEN_SECRET,
61
+ }),
62
+ ],
63
+ extraContext: { configResolver: resolver, configEncryption: encryption },
64
+ });
65
+ await unsafeCreateEntityTable(stack.db, userEntity);
66
+ await unsafeCreateEntityTable(stack.db, tenantEntity);
67
+ await unsafeCreateEntityTable(stack.db, userMfaEntity);
68
+ await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
69
+ });
70
+
71
+ afterAll(async () => {
72
+ await stack.cleanup();
73
+ });
74
+
75
+ async function enableMfaFor(idSeed: number): Promise<{
76
+ user: ReturnType<typeof createTestUser>;
77
+ secret: Buffer;
78
+ recoveryCodes: string[];
79
+ }> {
80
+ const user = createTestUser({ id: idSeed, roles: ["User"] });
81
+ const start = await stack.http.writeOk<{
82
+ setupToken: string;
83
+ otpauthUri: string;
84
+ recoveryCodes: string[];
85
+ }>(AuthMfaHandlers.enableStart, { accountLabel: `user-${idSeed}@example.com` }, user);
86
+ const secretParam = new URLSearchParams(start.otpauthUri.split("?")[1]).get("secret") ?? "";
87
+ const secret = base32Decode(secretParam);
88
+ await stack.http.writeOk(
89
+ AuthMfaHandlers.enableConfirm,
90
+ { setupToken: start.setupToken, code: currentTotpCode(secret) },
91
+ user,
92
+ );
93
+ // verify.write.ts re-checks tenant membership AND the user's own row
94
+ // (status gate) the way login.write.ts does — auth-mfa never writes a
95
+ // read_users row itself (enableStart/enableConfirm only touch user-mfa),
96
+ // so both need seeding here, not just per-test where a scenario cares.
97
+ await seedRow(stack.db, tenantMembershipsTable, {
98
+ userId: user.id,
99
+ tenantId: user.tenantId,
100
+ roles: JSON.stringify(["User"]),
101
+ });
102
+ await seedRow(stack.db, userTable, {
103
+ id: user.id,
104
+ tenantId: user.tenantId,
105
+ email: `user-${user.id}@example.com`,
106
+ passwordHash: "h",
107
+ displayName: `Test User ${idSeed}`,
108
+ locale: "de",
109
+ emailVerified: true,
110
+ roles: "[]",
111
+ status: USER_STATUS.Active,
112
+ });
113
+ return { user, secret, recoveryCodes: start.recoveryCodes };
114
+ }
115
+
116
+ function challengeFor(userId: string, tenantId: TenantId): string {
117
+ return signMfaChallengeToken({ userId, tenantId }, 10, CHALLENGE_TOKEN_SECRET).token;
118
+ }
119
+
120
+ describe("mfa verify — completes a two-step login", () => {
121
+ test("a valid TOTP code succeeds and returns the full session", async () => {
122
+ const { user, secret } = await enableMfaFor(1);
123
+ const challengeToken = challengeFor(user.id, user.tenantId);
124
+
125
+ const res = await stack.http.writeOk<{ session: SessionUser }>(
126
+ AuthMfaHandlers.verify,
127
+ { challengeToken, code: currentTotpCode(secret) },
128
+ GUEST,
129
+ );
130
+ expect(res.session.id).toBe(user.id);
131
+ expect(res.session.tenantId).toBe(user.tenantId);
132
+ });
133
+
134
+ test("a wrong TOTP code is rejected", async () => {
135
+ const { user } = await enableMfaFor(2);
136
+ const challengeToken = challengeFor(user.id, user.tenantId);
137
+ const err = await stack.http.writeErr(
138
+ AuthMfaHandlers.verify,
139
+ { challengeToken, code: "000000" },
140
+ GUEST,
141
+ );
142
+ expectErrorIncludes(err, "invalid_totp_code");
143
+ });
144
+
145
+ test("a valid recovery code succeeds, and the SAME code is rejected on replay", async () => {
146
+ const { user, recoveryCodes } = await enableMfaFor(3);
147
+ const code = recoveryCodes[0];
148
+ if (code === undefined) throw new Error("no recovery code minted");
149
+
150
+ const challengeA = challengeFor(user.id, user.tenantId);
151
+ const res = await stack.http.writeOk<{ session: SessionUser }>(
152
+ AuthMfaHandlers.verify,
153
+ { challengeToken: challengeA, code },
154
+ GUEST,
155
+ );
156
+ expect(res.session.id).toBe(user.id);
157
+
158
+ // Advisor-flagged trap: a recovery code must be single-use. Re-verify
159
+ // with a FRESH challenge token (a real attacker would just re-login)
160
+ // but the SAME recovery code — it must now be rejected because
161
+ // verify.write.ts persists remainingHashes on the row.
162
+ const challengeB = challengeFor(user.id, user.tenantId);
163
+ const err = await stack.http.writeErr(
164
+ AuthMfaHandlers.verify,
165
+ { challengeToken: challengeB, code },
166
+ GUEST,
167
+ );
168
+ expectErrorIncludes(err, "invalid_totp_code");
169
+ });
170
+
171
+ test("an exact-duplicate verify (same challenge token, same code) is rejected", async () => {
172
+ const { user, secret } = await enableMfaFor(4);
173
+ const challengeToken = challengeFor(user.id, user.tenantId);
174
+
175
+ await stack.http.writeOk<{ session: SessionUser }>(
176
+ AuthMfaHandlers.verify,
177
+ { challengeToken, code: currentTotpCode(secret) },
178
+ GUEST,
179
+ );
180
+
181
+ // Same challenge token, same (still time-window-valid) code — rejected
182
+ // by the TOTP-counter burn inside verifyMfaFactor before the challenge-
183
+ // token burn is even reached (that burn now happens after success).
184
+ const err = await stack.http.writeErr(
185
+ AuthMfaHandlers.verify,
186
+ { challengeToken, code: currentTotpCode(secret) },
187
+ GUEST,
188
+ );
189
+ expectErrorIncludes(err, "invalid_totp_code");
190
+ });
191
+
192
+ test("a TOTP code cannot be replayed across a FRESH challenge token (RFC 6238 §5.2)", async () => {
193
+ const { user, secret } = await enableMfaFor(105);
194
+ const code = currentTotpCode(secret);
195
+
196
+ const challengeA = challengeFor(user.id, user.tenantId);
197
+ await stack.http.writeOk<{ session: SessionUser }>(
198
+ AuthMfaHandlers.verify,
199
+ { challengeToken: challengeA, code },
200
+ GUEST,
201
+ );
202
+
203
+ // A DIFFERENT challenge token (as a real re-login would mint) but the
204
+ // SAME still-time-window-valid code — this is the actual attack this
205
+ // finding is about: a phished/shoulder-surfed code used for a parallel
206
+ // login, not a naive replay of the whole prior request.
207
+ const challengeB = challengeFor(user.id, user.tenantId);
208
+ const err = await stack.http.writeErr(
209
+ AuthMfaHandlers.verify,
210
+ { challengeToken: challengeB, code },
211
+ GUEST,
212
+ );
213
+ expectErrorIncludes(err, "invalid_totp_code");
214
+ });
215
+
216
+ test("a malformed challenge token is rejected without leaking account state", async () => {
217
+ const err = await stack.http.writeErr(
218
+ AuthMfaHandlers.verify,
219
+ { challengeToken: "not-a-real-token", code: "123456" },
220
+ GUEST,
221
+ );
222
+ expectErrorIncludes(err, "invalid_challenge_token");
223
+ });
224
+
225
+ test("a challenge token for a user without MFA enabled is rejected", async () => {
226
+ const challengeToken = challengeFor("verify-never-enabled-1", GUEST.tenantId);
227
+ const err = await stack.http.writeErr(
228
+ AuthMfaHandlers.verify,
229
+ { challengeToken, code: "123456" },
230
+ GUEST,
231
+ );
232
+ expectErrorIncludes(err, "invalid_challenge_token");
233
+ });
234
+ });
235
+
236
+ describe("mfa verify — re-checks account state the way login.write.ts does", () => {
237
+ test("membership revoked between login and verify → challenge rejected, no fallback to global roles", async () => {
238
+ const { user, secret } = await enableMfaFor(5);
239
+ const challengeToken = challengeFor(user.id, user.tenantId);
240
+
241
+ // Simulate the membership being pulled after the challenge token was
242
+ // issued but before verify runs — the exact gap login.write.ts's
243
+ // noMembership() guards against.
244
+ await asRawClient(stack.db).unsafe(
245
+ `DELETE FROM "${tenantMembershipsTable.tableName}" WHERE user_id = $1 AND tenant_id = $2`,
246
+ [user.id, user.tenantId],
247
+ );
248
+
249
+ const err = await stack.http.writeErr(
250
+ AuthMfaHandlers.verify,
251
+ { challengeToken, code: currentTotpCode(secret) },
252
+ GUEST,
253
+ );
254
+ expectErrorIncludes(err, "invalid_challenge_token");
255
+ });
256
+
257
+ test("account restricted between login and verify → challenge rejected", async () => {
258
+ const { user, secret } = await enableMfaFor(6);
259
+ const challengeToken = challengeFor(user.id, user.tenantId);
260
+
261
+ // enableMfaFor already seeded the user row Active — flip it to simulate
262
+ // a restriction landing between login and verify.
263
+ await asRawClient(stack.db).unsafe(
264
+ `UPDATE "${userTable.tableName}" SET status = $1 WHERE id = $2`,
265
+ [USER_STATUS.Restricted, user.id],
266
+ );
267
+
268
+ const err = await stack.http.writeErr(
269
+ AuthMfaHandlers.verify,
270
+ { challengeToken, code: currentTotpCode(secret) },
271
+ GUEST,
272
+ );
273
+ expectErrorIncludes(err, "invalid_challenge_token");
274
+ });
275
+
276
+ test("user row deleted between login and verify → challenge rejected (the !userRow gate)", async () => {
277
+ const { user, secret } = await enableMfaFor(7);
278
+ const challengeToken = challengeFor(user.id, user.tenantId);
279
+
280
+ // Simulate the read_users row disappearing after the challenge token
281
+ // was issued but before verify runs — hits verify.write.ts's
282
+ // `if (!userRow) return invalidChallengeToken()` gate directly, distinct
283
+ // from a revoked membership or a restricted-but-present user row.
284
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}" WHERE id = $1`, [
285
+ user.id,
286
+ ]);
287
+
288
+ const err = await stack.http.writeErr(
289
+ AuthMfaHandlers.verify,
290
+ { challengeToken, code: currentTotpCode(secret) },
291
+ GUEST,
292
+ );
293
+ expectErrorIncludes(err, "invalid_challenge_token");
294
+ });
295
+
296
+ test("user row homed in a DIFFERENT tenant than the challenge → verify succeeds (#1235)", async () => {
297
+ const { user, secret } = await enableMfaFor(8);
298
+
299
+ // Re-home the read_users row into another tenant — the #1235 layout:
300
+ // the user entity is tenant-agnostic (systemStream), so its row's
301
+ // tenant_id need not match the tenant being logged into. The old
302
+ // tenant-scoped UserQueries.detail lookup returned null here and every
303
+ // verify died with invalid_challenge_token; the unscoped findForAuth
304
+ // must resolve the row regardless.
305
+ await asRawClient(stack.db).unsafe(
306
+ `UPDATE "${userTable.tableName}" SET tenant_id = $1 WHERE id = $2`,
307
+ ["00000000-0000-4000-8000-0000000000fe", user.id],
308
+ );
309
+
310
+ const challengeToken = challengeFor(user.id, user.tenantId);
311
+ const res = await stack.http.writeOk<{ session: SessionUser }>(
312
+ AuthMfaHandlers.verify,
313
+ { challengeToken, code: currentTotpCode(secret) },
314
+ GUEST,
315
+ );
316
+ expect(res.session.id).toBe(user.id);
317
+ expect(res.session.tenantId).toBe(user.tenantId);
318
+ });
319
+ });
@@ -0,0 +1,51 @@
1
+ // RFC 4648 Base32 (no padding) — the encoding TOTP secrets and otpauth://
2
+ // URIs use. No external dependency: the alphabet lookup is ~20 lines and every
3
+ // authenticator app (Google/Microsoft/Authy) expects exactly this variant.
4
+ const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
5
+
6
+ export function base32Encode(bytes: Buffer): string {
7
+ let bits = 0;
8
+ let value = 0;
9
+ let output = "";
10
+ for (const byte of bytes) {
11
+ value = (value << 8) | byte;
12
+ bits += 8;
13
+ while (bits >= 5) {
14
+ output += ALPHABET[(value >>> (bits - 5)) & 0x1f];
15
+ bits -= 5;
16
+ }
17
+ }
18
+ if (bits > 0) {
19
+ output += ALPHABET[(value << (5 - bits)) & 0x1f];
20
+ }
21
+ return output;
22
+ }
23
+
24
+ // Accepts lowercase and stray whitespace (authenticator apps and users
25
+ // copy-pasting secrets are inconsistent about casing) — normalizes case and
26
+ // strips whitespace before decoding, but throws on a genuinely invalid
27
+ // character so a corrupted stored secret fails loud at read time instead of
28
+ // silently producing wrong codes.
29
+ export function base32Decode(encoded: string): Buffer {
30
+ const clean = encoded.trim().toUpperCase().replace(/\s+/g, "").replace(/=+$/, "");
31
+ let bits = 0;
32
+ let value = 0;
33
+ const bytes: number[] = [];
34
+ for (const char of clean) {
35
+ const index = ALPHABET.indexOf(char);
36
+ if (index === -1) throw new Error(`base32Decode: invalid character "${char}"`);
37
+ value = (value << 5) | index;
38
+ bits += 5;
39
+ if (bits >= 8) {
40
+ bytes.push((value >>> (bits - 8)) & 0xff);
41
+ bits -= 8;
42
+ }
43
+ }
44
+ // Leftover bits must be padding zeros (a canonical-length encoding never
45
+ // leaves a full byte's worth, i.e. >=5 residual bits) — anything else means
46
+ // truncated/non-canonical input decoded to the wrong byte count.
47
+ if (bits >= 5 || (value & ((1 << bits) - 1)) !== 0) {
48
+ throw new Error("base32Decode: invalid length or non-zero padding bits");
49
+ }
50
+ return Buffer.from(bytes);
51
+ }
@@ -0,0 +1,26 @@
1
+ // Tenant-scoped enforcement policy: does a user need MFA to log in even if
2
+ // they haven't opted in themselves? Default "optional" means the feature
3
+ // is fully opt-in (existing tenants/users unaffected — no migration step).
4
+ //
5
+ // ponytail: "admins"/"all" HARD-BLOCK login for an unenrolled user (see
6
+ // mfa-status-checker.ts's setupRequired branch) — there is no in-band way
7
+ // for a blocked user to complete enrollment yet (that flow ships in PR3's
8
+ // UI). Flipping this away from "optional" before PR3 locks out every
9
+ // unenrolled matching user with no recovery path. Fine for a tenant that
10
+ // enrolls its admins out-of-band first; a footgun otherwise.
11
+ import { type ConfigKeyHandle, createTenantConfig } from "@cosmicdrift/kumiko-framework/engine";
12
+
13
+ export const MFA_REQUIRED_POLICIES = ["optional", "admins", "all"] as const;
14
+ export type MfaRequiredPolicy = (typeof MFA_REQUIRED_POLICIES)[number];
15
+
16
+ export const mfaRequiredConfigHandle: ConfigKeyHandle<"select"> = {
17
+ name: "auth-mfa:config:required",
18
+ type: "select",
19
+ };
20
+
21
+ export function mfaRequiredConfigKey() {
22
+ return createTenantConfig("select", {
23
+ default: "optional" satisfies MfaRequiredPolicy,
24
+ options: [...MFA_REQUIRED_POLICIES],
25
+ });
26
+ }
@@ -0,0 +1,45 @@
1
+ // @runtime client
2
+ // Pure constants — client-marked so auth-mfa/web/ may import handler QNs
3
+ // and the screen id without pulling the feature's server runtime barrel.
4
+ // Qualified write-handler names — feature is registered as "auth-mfa", each
5
+ // short `name` below gets auto-prefixed to "auth-mfa:write:<name>" by
6
+ // r.writeHandler. Exported so cross-feature wiring (login.write.ts,
7
+ // run-prod-app.ts) can reference them without hardcoding the string.
8
+ export const AUTH_MFA_FEATURE = "auth-mfa" as const;
9
+
10
+ export const AuthMfaHandlers = {
11
+ enableStart: "auth-mfa:write:enable-start",
12
+ enableStartPreauth: "auth-mfa:write:enable-start-preauth",
13
+ enableConfirm: "auth-mfa:write:enable-confirm",
14
+ enableConfirmPreauth: "auth-mfa:write:enable-confirm-preauth",
15
+ disable: "auth-mfa:write:disable",
16
+ regenerateRecovery: "auth-mfa:write:regenerate-recovery",
17
+ verify: "auth-mfa:write:verify",
18
+ } as const;
19
+
20
+ export const AuthMfaQueries = {
21
+ status: "auth-mfa:query:user-mfa:status",
22
+ } as const;
23
+
24
+ // Write-failure codes minted server-side into UnprocessableError (errors.ts).
25
+ // The client reads the i18n key straight off the write result
26
+ // (res.error.i18nKey) instead of re-deriving it from these codes.
27
+ export const AuthMfaErrorCodes = {
28
+ mfaAlreadyEnabled: "mfa_already_enabled",
29
+ mfaNotEnabled: "mfa_not_enabled",
30
+ invalidSetupToken: "invalid_setup_token",
31
+ invalidTotpCode: "invalid_totp_code",
32
+ invalidRecoveryCode: "invalid_recovery_code",
33
+ invalidChallengeToken: "invalid_challenge_token",
34
+ tooManyAttempts: "too_many_attempts",
35
+ } as const;
36
+
37
+ export const MFA_SETUP_TOKEN_TTL_MINUTES = 10;
38
+ export const MFA_CHALLENGE_TOKEN_TTL_MINUTES = 10;
39
+ export const MFA_PREAUTH_SETUP_TOKEN_TTL_MINUTES = 10;
40
+ export const MFA_VERIFY_MAX_ATTEMPTS = 5;
41
+ export const MFA_VERIFY_LOCKOUT_MINUTES = 5;
42
+
43
+ // Dormant custom-screen id — see personal-access-tokens/feature.ts for the
44
+ // same convention. App places it via r.nav in its logged-in settings area.
45
+ export const MFA_ENABLE_SCREEN_ID = "auth-mfa-enable";
@@ -0,0 +1,45 @@
1
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { createEventStoreExecutor, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
+ import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
4
+ import { parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
5
+ import { userMfaEntity, userMfaTable } from "../schema/user-mfa";
6
+
7
+ export type UserMfaRow = {
8
+ readonly id: string;
9
+ readonly version: number;
10
+ readonly userId: string;
11
+ readonly totpSecret: string;
12
+ readonly recoveryCodes: { readonly hashes: readonly string[] };
13
+ readonly enabledAt: string;
14
+ readonly lastUsedAt: string | null;
15
+ };
16
+
17
+ const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
18
+ entityName: "user-mfa",
19
+ });
20
+
21
+ // `totpSecret`/`recoveryCodes` are `encrypted: true` — a raw fetchOne would
22
+ // return ciphertext, not plaintext. fetchOne only locates the row's id (no
23
+ // decryption needed for that); executor.detail() does the real, decrypting
24
+ // read the same way every other entity handler does. `recoveryCodes` is
25
+ // stored as a JSON string (schema/user-mfa.ts — both encryption layers need
26
+ // a string value) — parsed back into `{ hashes }` here, once, for every
27
+ // caller instead of each handler re-parsing it. A parse failure here means
28
+ // stored data is corrupt (post-decrypt, not user input) — fail loud.
29
+ export async function findUserMfaRow(db: TenantDb, user: SessionUser): Promise<UserMfaRow | null> {
30
+ const idLookup = await fetchOne<{ id: string }>(db, userMfaTable, {
31
+ userId: user.id,
32
+ tenantId: user.tenantId,
33
+ });
34
+ if (!idLookup) return null;
35
+ const detail = await executor.detail({ id: idLookup.id }, user, db);
36
+ if (!detail) return null;
37
+ // @cast-boundary engine-payload — executor.detail returns a decrypted,
38
+ // untyped Record; the userMfa entity's own field shape narrows it.
39
+ const row = detail as Omit<UserMfaRow, "recoveryCodes"> & { recoveryCodes: string };
40
+ const recoveryCodes = parseJsonOrThrow<{ hashes: readonly string[] }>(
41
+ row.recoveryCodes,
42
+ "user-mfa.recoveryCodes",
43
+ );
44
+ return { ...row, recoveryCodes };
45
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ UnprocessableError,
3
+ type WriteFailure,
4
+ writeFailure,
5
+ } from "@cosmicdrift/kumiko-framework/errors";
6
+ import { AuthMfaErrorCodes } from "./constants";
7
+
8
+ export { AuthMfaErrorCodes as AuthMfaErrors } from "./constants";
9
+
10
+ export function mfaAlreadyEnabled(): WriteFailure {
11
+ return writeFailure(
12
+ new UnprocessableError(AuthMfaErrorCodes.mfaAlreadyEnabled, {
13
+ i18nKey: "auth.mfa.errors.mfaAlreadyEnabled",
14
+ }),
15
+ );
16
+ }
17
+
18
+ export function mfaNotEnabled(): WriteFailure {
19
+ return writeFailure(
20
+ new UnprocessableError(AuthMfaErrorCodes.mfaNotEnabled, {
21
+ i18nKey: "auth.mfa.errors.mfaNotEnabled",
22
+ }),
23
+ );
24
+ }
25
+
26
+ export function invalidSetupToken(): WriteFailure {
27
+ return writeFailure(
28
+ new UnprocessableError(AuthMfaErrorCodes.invalidSetupToken, {
29
+ i18nKey: "auth.mfa.errors.invalidSetupToken",
30
+ }),
31
+ );
32
+ }
33
+
34
+ export function invalidTotpCode(): WriteFailure {
35
+ return writeFailure(
36
+ new UnprocessableError(AuthMfaErrorCodes.invalidTotpCode, {
37
+ i18nKey: "auth.mfa.errors.invalidTotpCode",
38
+ }),
39
+ );
40
+ }
41
+
42
+ export function invalidRecoveryCode(): WriteFailure {
43
+ return writeFailure(
44
+ new UnprocessableError(AuthMfaErrorCodes.invalidRecoveryCode, {
45
+ i18nKey: "auth.mfa.errors.invalidRecoveryCode",
46
+ }),
47
+ );
48
+ }
49
+
50
+ export function invalidChallengeToken(): WriteFailure {
51
+ return writeFailure(
52
+ new UnprocessableError(AuthMfaErrorCodes.invalidChallengeToken, {
53
+ i18nKey: "auth.mfa.errors.invalidChallengeToken",
54
+ }),
55
+ );
56
+ }
57
+
58
+ // retryAfterSeconds drives the login UI countdown — must stay > 0.
59
+ export function tooManyAttempts(retryAfterSeconds: number): WriteFailure {
60
+ return writeFailure(
61
+ new UnprocessableError(AuthMfaErrorCodes.tooManyAttempts, {
62
+ i18nKey: "auth.mfa.errors.tooManyAttempts",
63
+ details: { retryAfterSeconds },
64
+ }),
65
+ );
66
+ }