@cosmicdrift/kumiko-bundled-features 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (669) hide show
  1. package/package.json +41 -7
  2. package/src/__tests__/access-invalidation-consumer.integration.test.ts +279 -0
  3. package/src/__tests__/access-invalidation-stream.integration.test.ts +217 -0
  4. package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +200 -0
  5. package/src/admin-shell/__tests__/admin-shell.boot.test.ts +147 -0
  6. package/src/admin-shell/__tests__/overview-allowlist.test.ts +43 -0
  7. package/src/admin-shell/__tests__/overview-screens.boot.test.ts +35 -0
  8. package/src/admin-shell/constants.ts +11 -0
  9. package/src/admin-shell/feature.ts +135 -0
  10. package/src/admin-shell/i18n.ts +36 -0
  11. package/src/admin-shell/index.ts +17 -0
  12. package/src/admin-shell/overview-allowlist.ts +37 -0
  13. package/src/admin-shell/web/client-plugin.tsx +26 -0
  14. package/src/admin-shell/web/i18n.ts +16 -0
  15. package/src/admin-shell/web/index.ts +2 -0
  16. package/src/admin-shell/web/overview-layout.tsx +69 -0
  17. package/src/admin-shell/web/overview-query.ts +15 -0
  18. package/src/admin-shell/web/platform-overview-screen.tsx +90 -0
  19. package/src/admin-shell/web/tenant-overview-screen.tsx +90 -0
  20. package/src/agent-tools/__tests__/tool-catalog.test.ts +179 -0
  21. package/src/agent-tools/__tests__/tool-dispatch.integration.test.ts +100 -0
  22. package/src/agent-tools/__tests__/tool-dispatch.test.ts +126 -0
  23. package/src/agent-tools/index.ts +9 -0
  24. package/src/agent-tools/tool-catalog.ts +130 -0
  25. package/src/agent-tools/tool-dispatch.ts +82 -0
  26. package/src/agent-tools/types.ts +38 -0
  27. package/src/audit/__tests__/audit-screens.boot.test.ts +51 -0
  28. package/src/audit/__tests__/audit-security.integration.test.ts +201 -0
  29. package/src/audit/__tests__/audit.integration.test.ts +50 -25
  30. package/src/audit/constants.ts +9 -2
  31. package/src/audit/feature.ts +33 -1
  32. package/src/audit/handlers/details.query.ts +47 -0
  33. package/src/audit/handlers/list.query.ts +3 -2
  34. package/src/audit/i18n.ts +31 -0
  35. package/src/audit/web/audit-log-detail-screen.tsx +131 -0
  36. package/src/audit/web/audit-log-screen.tsx +250 -0
  37. package/src/audit/web/client-plugin.tsx +22 -0
  38. package/src/audit/web/i18n.ts +12 -0
  39. package/src/audit/web/index.ts +5 -0
  40. package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.test.ts +4 -4
  41. package/src/auth-email-password/__tests__/account-lockout.integration.test.ts +4 -4
  42. package/src/auth-email-password/__tests__/account-unlock.integration.test.ts +283 -0
  43. package/src/auth-email-password/__tests__/auth-claims.integration.test.ts +10 -11
  44. package/src/auth-email-password/__tests__/auth-user-row.test.ts +36 -0
  45. package/src/auth-email-password/__tests__/auth.integration.test.ts +33 -6
  46. package/src/auth-email-password/__tests__/email-verification.integration.test.ts +9 -6
  47. package/src/auth-email-password/__tests__/feature-options.test.ts +33 -0
  48. package/src/auth-email-password/__tests__/identity-v3-login.integration.test.ts +3 -3
  49. package/src/auth-email-password/__tests__/invite-flow-kms.integration.test.ts +283 -0
  50. package/src/auth-email-password/__tests__/invite-flow.integration.test.ts +35 -1
  51. package/src/auth-email-password/__tests__/login-mfa-setup-required.integration.test.ts +129 -0
  52. package/src/auth-email-password/__tests__/multi-roles.integration.test.ts +1 -1
  53. package/src/auth-email-password/__tests__/password-policy.test.ts +15 -0
  54. package/src/auth-email-password/__tests__/password-reset.integration.test.ts +19 -6
  55. package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.test.ts +3 -3
  56. package/src/auth-email-password/__tests__/seed-admin.integration.test.ts +1 -1
  57. package/src/auth-email-password/__tests__/self-registration-toggle.integration.test.ts +264 -0
  58. package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +20 -4
  59. package/src/auth-email-password/__tests__/session-strict-mode.integration.test.ts +7 -14
  60. package/src/auth-email-password/__tests__/signup-flow.integration.test.ts +91 -0
  61. package/src/auth-email-password/constants.ts +25 -0
  62. package/src/auth-email-password/email-templates.ts +32 -0
  63. package/src/auth-email-password/errors.ts +9 -0
  64. package/src/auth-email-password/feature.ts +59 -11
  65. package/src/auth-email-password/handlers/change-password.write.ts +14 -13
  66. package/src/auth-email-password/handlers/confirm-account-unlock.write.ts +59 -0
  67. package/src/auth-email-password/handlers/confirm-token-flow.ts +34 -105
  68. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +12 -5
  69. package/src/auth-email-password/handlers/invite-accept.write.ts +9 -2
  70. package/src/auth-email-password/handlers/invite-create.write.ts +2 -2
  71. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +12 -4
  72. package/src/auth-email-password/handlers/login.write.ts +86 -16
  73. package/src/auth-email-password/handlers/logout.write.ts +4 -2
  74. package/src/auth-email-password/handlers/request-account-unlock.write.ts +38 -0
  75. package/src/auth-email-password/handlers/reset-password.write.ts +3 -2
  76. package/src/auth-email-password/handlers/self-registration-status.query.ts +23 -0
  77. package/src/auth-email-password/handlers/signup-confirm.write.ts +19 -11
  78. package/src/auth-email-password/handlers/signup-request.write.ts +22 -0
  79. package/src/auth-email-password/handlers/token-request-handler.ts +4 -2
  80. package/src/auth-email-password/i18n.ts +52 -0
  81. package/src/auth-email-password/index.ts +13 -2
  82. package/src/auth-email-password/lockout-store.ts +10 -5
  83. package/src/auth-email-password/password-policy.ts +2108 -0
  84. package/src/auth-email-password/seeding.ts +55 -27
  85. package/src/auth-email-password/self-registration-toggle.ts +25 -0
  86. package/src/auth-email-password/signed-token.ts +1 -0
  87. package/src/auth-email-password/unlock-token.ts +28 -0
  88. package/src/auth-email-password/web/__tests__/auth-client.test.ts +542 -0
  89. package/src/auth-email-password/web/__tests__/auth-form-logic.test.ts +44 -0
  90. package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +194 -6
  91. package/src/auth-email-password/web/__tests__/client-plugin.test.tsx +34 -0
  92. package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +64 -0
  93. package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +5 -1
  94. package/src/auth-email-password/web/__tests__/invite-accept-screen.test.tsx +129 -2
  95. package/src/auth-email-password/web/__tests__/login-screen.test.tsx +149 -5
  96. package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +137 -0
  97. package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +5 -1
  98. package/src/auth-email-password/web/__tests__/signup-complete-screen.test.tsx +134 -0
  99. package/src/auth-email-password/web/__tests__/signup-screen.test.tsx +93 -0
  100. package/src/auth-email-password/web/__tests__/test-utils.tsx +12 -2
  101. package/src/auth-email-password/web/__tests__/use-url-token.test.tsx +29 -0
  102. package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +5 -1
  103. package/src/auth-email-password/web/auth-client.ts +96 -18
  104. package/src/auth-email-password/web/auth-form-logic.ts +30 -0
  105. package/src/auth-email-password/web/auth-form-primitives.tsx +22 -20
  106. package/src/auth-email-password/web/auth-gate.tsx +140 -19
  107. package/src/auth-email-password/web/client-plugin.ts +24 -2
  108. package/src/auth-email-password/web/confirm-account-unlock-screen.tsx +95 -0
  109. package/src/auth-email-password/web/forgot-password-screen.tsx +6 -6
  110. package/src/auth-email-password/web/index.ts +16 -4
  111. package/src/auth-email-password/web/invite-accept-screen.tsx +5 -5
  112. package/src/auth-email-password/web/login-screen.tsx +78 -22
  113. package/src/auth-email-password/web/request-account-unlock-screen.tsx +112 -0
  114. package/src/auth-email-password/web/reset-password-screen.tsx +15 -18
  115. package/src/auth-email-password/web/session.tsx +11 -6
  116. package/src/auth-email-password/web/signup-complete-screen.tsx +13 -15
  117. package/src/auth-email-password/web/signup-screen.tsx +8 -10
  118. package/src/auth-email-password/web/use-shell-user.ts +5 -2
  119. package/src/auth-email-password/web/user-menu.tsx +3 -1
  120. package/src/auth-email-password/web/verify-email-screen.tsx +10 -14
  121. package/src/auth-foundation/__tests__/feature.test.ts +46 -0
  122. package/src/auth-foundation/__tests__/session-store.test.ts +77 -0
  123. package/src/auth-foundation/__tests__/tenant-providers.test.ts +160 -0
  124. package/src/auth-foundation/__tests__/token-verifier.test.ts +150 -0
  125. package/src/auth-foundation/boot-checks.ts +150 -0
  126. package/src/auth-foundation/feature.ts +157 -0
  127. package/src/auth-foundation/index.ts +37 -0
  128. package/src/auth-foundation/resolve-anonymous-access.ts +62 -0
  129. package/src/auth-foundation/types.ts +170 -0
  130. package/src/auth-mfa/__tests__/base32.test.ts +28 -0
  131. package/src/auth-mfa/__tests__/disable-and-regenerate.integration.test.ts +265 -0
  132. package/src/auth-mfa/__tests__/enable-confirm-preauth.integration.test.ts +321 -0
  133. package/src/auth-mfa/__tests__/enable-flow.integration.test.ts +322 -0
  134. package/src/auth-mfa/__tests__/enable-start-preauth.integration.test.ts +237 -0
  135. package/src/auth-mfa/__tests__/errors-i18n-parity.test.ts +39 -0
  136. package/src/auth-mfa/__tests__/mfa-preauth-setup-token.test.ts +113 -0
  137. package/src/auth-mfa/__tests__/no-redis.integration.test.ts +209 -0
  138. package/src/auth-mfa/__tests__/pii-subject-encryption.integration.test.ts +139 -0
  139. package/src/auth-mfa/__tests__/reencrypt-job.integration.test.ts +247 -0
  140. package/src/auth-mfa/__tests__/session-auto-revoke.integration.test.ts +169 -0
  141. package/src/auth-mfa/__tests__/totp.test.ts +131 -0
  142. package/src/auth-mfa/__tests__/verify.integration.test.ts +319 -0
  143. package/src/auth-mfa/base32.ts +51 -0
  144. package/src/auth-mfa/config.ts +26 -0
  145. package/src/auth-mfa/constants.ts +45 -0
  146. package/src/auth-mfa/db/queries.ts +45 -0
  147. package/src/auth-mfa/errors.ts +66 -0
  148. package/src/auth-mfa/feature.ts +170 -0
  149. package/src/auth-mfa/handlers/disable.write.ts +67 -0
  150. package/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +200 -0
  151. package/src/auth-mfa/handlers/enable-confirm.write.ts +106 -0
  152. package/src/auth-mfa/handlers/enable-start-preauth.write.ts +87 -0
  153. package/src/auth-mfa/handlers/enable-start.write.ts +66 -0
  154. package/src/auth-mfa/handlers/reencrypt.job.ts +234 -0
  155. package/src/auth-mfa/handlers/regenerate-recovery.write.ts +77 -0
  156. package/src/auth-mfa/handlers/status.query.ts +23 -0
  157. package/src/auth-mfa/handlers/verify.write.ts +179 -0
  158. package/src/auth-mfa/i18n.ts +12 -0
  159. package/src/auth-mfa/index.ts +19 -0
  160. package/src/auth-mfa/mfa-challenge-token.ts +90 -0
  161. package/src/auth-mfa/mfa-preauth-setup-token.ts +86 -0
  162. package/src/auth-mfa/mfa-setup-token.ts +96 -0
  163. package/src/auth-mfa/mfa-status-checker.ts +84 -0
  164. package/src/auth-mfa/mfa-verify-attempts.ts +90 -0
  165. package/src/auth-mfa/otpauth-uri.ts +21 -0
  166. package/src/auth-mfa/recovery-codes.ts +54 -0
  167. package/src/auth-mfa/schema/user-mfa.ts +60 -0
  168. package/src/auth-mfa/testing.ts +5 -0
  169. package/src/auth-mfa/totp.ts +67 -0
  170. package/src/auth-mfa/verify-factor.ts +58 -0
  171. package/src/auth-mfa/web/__tests__/mfa-client.test.ts +265 -0
  172. package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +182 -0
  173. package/src/auth-mfa/web/__tests__/mfa-setup-preauth-screen.test.tsx +379 -0
  174. package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +104 -0
  175. package/src/auth-mfa/web/client-plugin.ts +37 -0
  176. package/src/auth-mfa/web/i18n.ts +136 -0
  177. package/src/auth-mfa/web/index.ts +27 -0
  178. package/src/auth-mfa/web/mfa-client.ts +184 -0
  179. package/src/auth-mfa/web/mfa-disable-dialog.tsx +69 -0
  180. package/src/auth-mfa/web/mfa-enable-screen.tsx +228 -0
  181. package/src/auth-mfa/web/mfa-recovery-codes-reveal.tsx +53 -0
  182. package/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +68 -0
  183. package/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +267 -0
  184. package/src/auth-mfa/web/mfa-verify-screen.tsx +117 -0
  185. package/src/auth-mfa/web/qrcode-browser.d.ts +12 -0
  186. package/src/auth-mfa-user-data/__tests__/hooks.integration.test.ts +134 -0
  187. package/src/auth-mfa-user-data/hooks.ts +53 -0
  188. package/src/auth-mfa-user-data/index.ts +21 -0
  189. package/src/billing-foundation/__tests__/billing-foundation.integration.test.ts +206 -2
  190. package/src/billing-foundation/__tests__/subscription-provider-contract.ts +93 -0
  191. package/src/billing-foundation/entities.ts +28 -4
  192. package/src/billing-foundation/events.ts +6 -2
  193. package/src/billing-foundation/feature.ts +9 -1
  194. package/src/billing-foundation/get-subscription-for-tenant.ts +16 -5
  195. package/src/billing-foundation/handlers/create-portal-session.write.ts +17 -1
  196. package/src/billing-foundation/handlers/list-subscriptions.query.ts +15 -1
  197. package/src/billing-foundation/handlers/process-event.write.ts +39 -3
  198. package/src/billing-foundation/tenant-destroy-hook.ts +46 -0
  199. package/src/cap-counter/__tests__/cap-list.boot.test.ts +27 -0
  200. package/src/cap-counter/__tests__/cap-list.security.integration.test.ts +53 -0
  201. package/src/cap-counter/__tests__/entity-ddl.test.ts +15 -0
  202. package/src/cap-counter/__tests__/with-cap-enforcement.integration.test.ts +8 -5
  203. package/src/cap-counter/constants.ts +2 -0
  204. package/src/cap-counter/enforce-cap.ts +15 -18
  205. package/src/cap-counter/entity.ts +3 -3
  206. package/src/cap-counter/feature.ts +28 -1
  207. package/src/cap-counter/i18n.ts +12 -0
  208. package/src/cap-counter/with-cap-enforcement.ts +2 -2
  209. package/src/channel-email/__tests__/pii-guard.test.ts +56 -0
  210. package/src/channel-email/email-channel.ts +2 -1
  211. package/src/channel-email/index.ts +1 -0
  212. package/src/channel-email/pii-guard.ts +39 -0
  213. package/src/compliance-profiles/__tests__/profile-picker.boot.test.ts +28 -0
  214. package/src/compliance-profiles/__tests__/profile-picker.security.integration.test.ts +91 -0
  215. package/src/compliance-profiles/constants.ts +15 -0
  216. package/src/compliance-profiles/feature.ts +22 -1
  217. package/src/compliance-profiles/handlers/set-profile.write.ts +2 -2
  218. package/src/compliance-profiles/i18n.ts +6 -0
  219. package/src/compliance-profiles/index.ts +6 -0
  220. package/src/compliance-profiles/web/client-plugin.tsx +22 -0
  221. package/src/compliance-profiles/web/compliance-profile-screen.tsx +152 -0
  222. package/src/compliance-profiles/web/i18n.ts +25 -0
  223. package/src/compliance-profiles/web/index.ts +6 -0
  224. package/src/config/__tests__/app-overrides.test.ts +11 -0
  225. package/src/config/__tests__/backing-secrets.integration.test.ts +22 -0
  226. package/src/config/__tests__/cascade.integration.test.ts +35 -0
  227. package/src/config/__tests__/config.integration.test.ts +71 -10
  228. package/src/config/__tests__/deserialize-value.test.ts +5 -8
  229. package/src/config/__tests__/inherited-redaction.integration.test.ts +4 -3
  230. package/src/config/__tests__/pii-encrypted.integration.test.ts +179 -0
  231. package/src/config/__tests__/reencrypt-job-concurrent-write-race.integration.test.ts +234 -0
  232. package/src/config/__tests__/reencrypt-job-kek-rotation.integration.test.ts +243 -0
  233. package/src/config/__tests__/write-helpers.test.ts +23 -0
  234. package/src/config/db/queries/resolver.ts +3 -3
  235. package/src/config/feature.ts +15 -8
  236. package/src/config/handlers/cascade.query.ts +2 -1
  237. package/src/config/handlers/reencrypt.job.ts +209 -0
  238. package/src/config/handlers/set.write.ts +55 -3
  239. package/src/config/handlers/values.query.ts +2 -1
  240. package/src/config/i18n.ts +9 -0
  241. package/src/config/index.ts +5 -4
  242. package/src/config/resolver.ts +75 -61
  243. package/src/config/table.ts +1 -1
  244. package/src/config/write-helpers.ts +22 -0
  245. package/src/crypto-shredding/__tests__/forget-subject.integration.test.ts +148 -0
  246. package/src/crypto-shredding/constants.ts +5 -0
  247. package/src/crypto-shredding/feature.ts +18 -0
  248. package/src/crypto-shredding/handlers/forget-subject.write.ts +91 -0
  249. package/src/crypto-shredding/index.ts +11 -0
  250. package/src/custom-fields/__tests__/audit-integration.integration.test.ts +1 -1
  251. package/src/custom-fields/__tests__/custom-fields.integration.test.ts +92 -53
  252. package/src/custom-fields/__tests__/field-access.integration.test.ts +25 -0
  253. package/src/custom-fields/__tests__/parse-serialized-field.test.ts +11 -3
  254. package/src/custom-fields/__tests__/retention.integration.test.ts +40 -0
  255. package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +26 -79
  256. package/src/custom-fields/constants.ts +1 -0
  257. package/src/custom-fields/db/queries/user-data-rights.ts +0 -20
  258. package/src/custom-fields/entity.ts +1 -1
  259. package/src/custom-fields/events.ts +4 -3
  260. package/src/custom-fields/feature.ts +8 -0
  261. package/src/custom-fields/handlers/delete-system-field.write.ts +2 -1
  262. package/src/custom-fields/handlers/delete-tenant-field.write.ts +2 -1
  263. package/src/custom-fields/handlers/set-custom-field.write.ts +3 -44
  264. package/src/custom-fields/handlers/update-tenant-field.write.ts +0 -17
  265. package/src/custom-fields/lib/field-access.ts +8 -1
  266. package/src/custom-fields/lib/parse-serialized-field.ts +12 -4
  267. package/src/custom-fields/lib/value-schema.ts +1 -1
  268. package/src/custom-fields/run-retention.ts +9 -1
  269. package/src/custom-fields/schemas.ts +9 -9
  270. package/src/custom-fields/wire-for-entity.ts +110 -78
  271. package/src/custom-fields/wire-user-data-rights.ts +5 -54
  272. package/src/data-retention/__tests__/parse-override.test.ts +14 -13
  273. package/src/data-retention/__tests__/retention-cleanup.integration.test.ts +145 -8
  274. package/src/data-retention/feature.ts +3 -3
  275. package/src/data-retention/run-retention-cleanup.ts +102 -19
  276. package/src/delivery/__tests__/delivery-pii-kms.integration.test.ts +171 -0
  277. package/src/delivery/__tests__/delivery-screens.boot.test.ts +27 -0
  278. package/src/delivery/__tests__/delivery-security.integration.test.ts +106 -0
  279. package/src/delivery/constants.ts +13 -51
  280. package/src/delivery/feature.ts +29 -5
  281. package/src/delivery/handlers/log.query.ts +28 -7
  282. package/src/delivery/i18n.ts +6 -0
  283. package/src/delivery/index.ts +6 -1
  284. package/src/delivery/public-names.ts +36 -0
  285. package/src/delivery/tables.ts +3 -3
  286. package/src/delivery/web/client-plugin.tsx +20 -0
  287. package/src/delivery/web/delivery-log-screen.tsx +103 -0
  288. package/src/delivery/web/i18n.ts +25 -0
  289. package/src/delivery/web/index.ts +3 -0
  290. package/src/document-ingest-foundation/__tests__/feature.integration.test.ts +147 -0
  291. package/src/document-ingest-foundation/__tests__/feature.test.ts +105 -0
  292. package/src/document-ingest-foundation/__tests__/pages.test.ts +37 -0
  293. package/src/document-ingest-foundation/entity.ts +43 -0
  294. package/src/document-ingest-foundation/events.ts +25 -0
  295. package/src/document-ingest-foundation/feature.ts +126 -0
  296. package/src/document-ingest-foundation/index.ts +12 -0
  297. package/src/document-ingest-foundation/pages.ts +34 -0
  298. package/src/feature-toggles/__tests__/compose-tier-resolver.test.ts +304 -0
  299. package/src/feature-toggles/__tests__/feature-toggles.integration.test.ts +7 -6
  300. package/src/feature-toggles/__tests__/toggle-admin.boot.test.ts +31 -0
  301. package/src/feature-toggles/__tests__/toggle-admin.security.integration.test.ts +69 -0
  302. package/src/feature-toggles/compose-tier-resolver.ts +134 -0
  303. package/src/feature-toggles/constants.ts +14 -0
  304. package/src/feature-toggles/feature.ts +23 -3
  305. package/src/feature-toggles/global-feature-state-table.ts +20 -1
  306. package/src/feature-toggles/handlers/list.query.ts +1 -1
  307. package/src/feature-toggles/handlers/registered.query.ts +1 -1
  308. package/src/feature-toggles/i18n.ts +6 -0
  309. package/src/feature-toggles/index.ts +8 -0
  310. package/src/feature-toggles/toggle-runtime.ts +14 -0
  311. package/src/feature-toggles/web/client-plugin.tsx +22 -0
  312. package/src/feature-toggles/web/i18n.ts +29 -0
  313. package/src/feature-toggles/web/index.ts +6 -0
  314. package/src/feature-toggles/web/toggle-admin-screen.tsx +136 -0
  315. package/src/file-foundation/__tests__/feature.test.ts +5 -5
  316. package/src/file-foundation/__tests__/file-foundation.integration.test.ts +4 -3
  317. package/src/file-foundation/feature.ts +10 -11
  318. package/src/files-provider-s3/__tests__/s3-provider.integration.test.ts +27 -2
  319. package/src/files-provider-s3/s3-provider.ts +12 -1
  320. package/src/folders/__tests__/folders.integration.test.ts +3 -0
  321. package/src/folders/web/__tests__/folder-manager.test.tsx +43 -2
  322. package/src/folders/web/__tests__/tree.test.ts +8 -0
  323. package/src/folders/web/folder-manager.tsx +2 -3
  324. package/src/folders/web/i18n.ts +0 -2
  325. package/src/folders/web/tree.ts +13 -2
  326. package/src/folders-user-data/__tests__/hooks.integration.test.ts +1 -0
  327. package/src/folders-user-data/index.ts +4 -4
  328. package/src/inbound-mail-foundation/__tests__/account-state.test.ts +30 -0
  329. package/src/inbound-mail-foundation/__tests__/connect-routes.integration.test.ts +307 -0
  330. package/src/inbound-mail-foundation/__tests__/feature.test.ts +169 -0
  331. package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +644 -0
  332. package/src/inbound-mail-foundation/__tests__/inbound-mail-provider-contract.ts +111 -0
  333. package/src/inbound-mail-foundation/__tests__/retention.integration.test.ts +237 -0
  334. package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +433 -0
  335. package/src/inbound-mail-foundation/aggregate-id.ts +40 -0
  336. package/src/inbound-mail-foundation/connect-routes.ts +230 -0
  337. package/src/inbound-mail-foundation/constants.ts +80 -0
  338. package/src/inbound-mail-foundation/db/queries/inbound-projections.ts +38 -0
  339. package/src/inbound-mail-foundation/entities.ts +172 -0
  340. package/src/inbound-mail-foundation/events.ts +147 -0
  341. package/src/inbound-mail-foundation/feature.ts +243 -0
  342. package/src/inbound-mail-foundation/handlers/account-state.ts +23 -0
  343. package/src/inbound-mail-foundation/handlers/connect-account.write.ts +112 -0
  344. package/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +69 -0
  345. package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +333 -0
  346. package/src/inbound-mail-foundation/handlers/list-accounts.query.ts +44 -0
  347. package/src/inbound-mail-foundation/handlers/list-messages.query.ts +58 -0
  348. package/src/inbound-mail-foundation/handlers/scope-visibility.ts +14 -0
  349. package/src/inbound-mail-foundation/handlers/update-account.write.ts +78 -0
  350. package/src/inbound-mail-foundation/index.ts +97 -0
  351. package/src/inbound-mail-foundation/oauth-state.ts +103 -0
  352. package/src/inbound-mail-foundation/projection.ts +152 -0
  353. package/src/inbound-mail-foundation/provider-factory.ts +53 -0
  354. package/src/inbound-mail-foundation/retention-sweep.ts +168 -0
  355. package/src/inbound-mail-foundation/tenant-destroy-hook.ts +93 -0
  356. package/src/inbound-mail-foundation/types.ts +250 -0
  357. package/src/inbound-mail-foundation/watch-supervisor.ts +482 -0
  358. package/src/inbound-provider-imap/__tests__/feature.test.ts +115 -0
  359. package/src/inbound-provider-imap/__tests__/imap-foundation.integration.test.ts +302 -0
  360. package/src/inbound-provider-imap/__tests__/imap-live.integration.test.ts +206 -0
  361. package/src/inbound-provider-imap/__tests__/plugin-mocked.test.ts +358 -0
  362. package/src/inbound-provider-imap/credential-document.ts +51 -0
  363. package/src/inbound-provider-imap/feature.ts +283 -0
  364. package/src/inbound-provider-imap/imap-client.ts +158 -0
  365. package/src/inbound-provider-imap/index.ts +17 -0
  366. package/src/inbound-provider-inmemory/__tests__/feature.test.ts +52 -0
  367. package/src/inbound-provider-inmemory/feature.ts +154 -0
  368. package/src/inbound-provider-inmemory/index.ts +12 -0
  369. package/src/jobs/__tests__/entity-ddl.test.ts +15 -0
  370. package/src/jobs/__tests__/job-instant.test.ts +15 -0
  371. package/src/jobs/__tests__/job-system-user.integration.test.ts +4 -8
  372. package/src/jobs/__tests__/jobs-events.integration.test.ts +37 -1
  373. package/src/jobs/__tests__/jobs-pii-kms.integration.test.ts +120 -0
  374. package/src/jobs/__tests__/jobs-screens.boot.test.ts +36 -0
  375. package/src/jobs/__tests__/jobs-security.integration.test.ts +108 -0
  376. package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +169 -0
  377. package/src/jobs/constants.ts +8 -1
  378. package/src/jobs/feature.ts +52 -8
  379. package/src/jobs/handlers/detail.query.ts +6 -0
  380. package/src/jobs/handlers/list.query.ts +8 -1
  381. package/src/jobs/handlers/projection-rebuild.job.ts +14 -4
  382. package/src/jobs/handlers/reindex-entity.job.ts +49 -0
  383. package/src/jobs/i18n.ts +37 -0
  384. package/src/jobs/job-instant.ts +9 -0
  385. package/src/jobs/job-run-table.ts +4 -4
  386. package/src/jobs/web/client-plugin.tsx +22 -0
  387. package/src/jobs/web/i18n.ts +12 -0
  388. package/src/jobs/web/index.ts +4 -0
  389. package/src/jobs/web/job-run-detail-screen.tsx +180 -0
  390. package/src/jobs/web/job-runs-screen.tsx +146 -0
  391. package/src/ledger/__tests__/ledger.integration.test.ts +8 -3
  392. package/src/ledger/handlers/confirm-schedule-period.write.ts +17 -4
  393. package/src/ledger/recurring.ts +17 -8
  394. package/src/ledger/web/index.ts +3 -2
  395. package/src/legal-pages/__tests__/legal-pages.integration.test.ts +55 -0
  396. package/src/legal-pages/feature.ts +24 -31
  397. package/src/legal-pages/index.ts +1 -0
  398. package/src/mail-foundation/__tests__/feature.test.ts +5 -6
  399. package/src/mail-foundation/__tests__/mail-foundation.integration.test.ts +4 -3
  400. package/src/mail-foundation/__tests__/mail-transport-contract.ts +58 -0
  401. package/src/mail-foundation/feature.ts +26 -24
  402. package/src/mail-transport-inmemory/__tests__/feature.test.ts +23 -0
  403. package/src/mail-transport-smtp/__tests__/feature.test.ts +65 -0
  404. package/src/managed-pages/__tests__/branding-coerce.test.ts +2 -6
  405. package/src/managed-pages/__tests__/i18n-surface.boot.test.ts +30 -0
  406. package/src/managed-pages/__tests__/managed-pages.integration.test.ts +137 -0
  407. package/src/managed-pages/feature.ts +45 -47
  408. package/src/managed-pages/handlers/branding.query.ts +5 -1
  409. package/src/managed-pages/handlers/by-tenant-published.query.ts +52 -0
  410. package/src/managed-pages/i18n.ts +66 -0
  411. package/src/managed-pages/screens/page-screens.ts +5 -1
  412. package/src/managed-pages/web/client-plugin.tsx +22 -0
  413. package/src/managed-pages/web/i18n.ts +24 -0
  414. package/src/managed-pages/web/index.ts +3 -0
  415. package/src/page-render/__tests__/layout.test.ts +63 -0
  416. package/src/page-render/__tests__/markdown.test.ts +29 -0
  417. package/src/page-render/branding.ts +4 -3
  418. package/src/page-render/index.ts +1 -1
  419. package/src/page-render/layout.ts +18 -3
  420. package/src/page-render/markdown.ts +1 -9
  421. package/src/page-render/web.ts +13 -0
  422. package/src/personal-access-tokens/__tests__/feature.test.ts +42 -0
  423. package/src/personal-access-tokens/__tests__/pat.integration.test.ts +239 -0
  424. package/src/personal-access-tokens/__tests__/scopes.test.ts +42 -0
  425. package/src/personal-access-tokens/constants.ts +33 -0
  426. package/src/personal-access-tokens/feature.ts +105 -0
  427. package/src/personal-access-tokens/handlers/available-scopes.query.ts +21 -0
  428. package/src/personal-access-tokens/handlers/create.write.ts +47 -0
  429. package/src/personal-access-tokens/handlers/list.query.ts +50 -0
  430. package/src/personal-access-tokens/handlers/revoke.write.ts +37 -0
  431. package/src/personal-access-tokens/hash.ts +17 -0
  432. package/src/personal-access-tokens/i18n.ts +5 -0
  433. package/src/personal-access-tokens/index.ts +31 -0
  434. package/src/personal-access-tokens/resolver.ts +58 -0
  435. package/src/personal-access-tokens/roles.ts +29 -0
  436. package/src/personal-access-tokens/schema/api-token.ts +63 -0
  437. package/src/personal-access-tokens/scopes.ts +43 -0
  438. package/src/personal-access-tokens/web/client-plugin.tsx +29 -0
  439. package/src/personal-access-tokens/web/i18n.ts +69 -0
  440. package/src/personal-access-tokens/web/index.ts +7 -0
  441. package/src/personal-access-tokens/web/pat-tokens-screen.tsx +265 -0
  442. package/src/readiness/__tests__/readiness.integration.test.ts +4 -3
  443. package/src/renderer-simple/__tests__/resolve-variables.test.ts +37 -0
  444. package/src/renderer-simple/resolve-variables.ts +3 -1
  445. package/src/secrets/__tests__/rotate.integration.test.ts +115 -18
  446. package/src/secrets/handlers/rotate.job.ts +77 -98
  447. package/src/secrets/secrets-context.ts +7 -38
  448. package/src/secrets/table.ts +7 -8
  449. package/src/seo/__tests__/llms-txt.test.ts +38 -0
  450. package/src/seo/__tests__/robots-txt.test.ts +18 -0
  451. package/src/seo/__tests__/schema-builders.test.ts +57 -0
  452. package/src/seo/__tests__/seo.integration.test.ts +350 -0
  453. package/src/seo/__tests__/sitemap.test.ts +38 -0
  454. package/src/seo/constants.ts +60 -0
  455. package/src/seo/feature.ts +316 -0
  456. package/src/seo/handlers/seo-config.query.ts +35 -0
  457. package/src/seo/index.ts +19 -0
  458. package/src/seo/llms-txt.ts +33 -0
  459. package/src/seo/robots-txt.ts +13 -0
  460. package/src/seo/schema-builders.ts +57 -0
  461. package/src/seo/sitemap.ts +33 -0
  462. package/src/sessions/__tests__/auto-revoke-binding.test.ts +71 -0
  463. package/src/sessions/__tests__/cleanup.integration.test.ts +2 -1
  464. package/src/sessions/__tests__/password-auto-revoke.integration.test.ts +11 -7
  465. package/src/sessions/__tests__/rebuild-survival.integration.test.ts +18 -7
  466. package/src/sessions/__tests__/session-store-registration.test.ts +21 -0
  467. package/src/sessions/__tests__/sessions-screens.boot.test.ts +55 -0
  468. package/src/sessions/__tests__/sessions.integration.test.ts +272 -20
  469. package/src/sessions/__tests__/test-helpers.ts +16 -3
  470. package/src/sessions/constants.ts +5 -0
  471. package/src/sessions/db/queries/cleanup.ts +2 -2
  472. package/src/sessions/feature.ts +175 -26
  473. package/src/sessions/handlers/detail.query.ts +36 -0
  474. package/src/sessions/handlers/list.query.ts +14 -9
  475. package/src/sessions/handlers/mine.query.ts +13 -8
  476. package/src/sessions/handlers/revoke-all-for-user.write.ts +51 -12
  477. package/src/sessions/handlers/revoke-all-others.write.ts +19 -1
  478. package/src/sessions/handlers/revoke.write.ts +13 -0
  479. package/src/sessions/i18n.ts +23 -0
  480. package/src/sessions/index.ts +9 -2
  481. package/src/sessions/schema/user-session.ts +8 -2
  482. package/src/sessions/session-callbacks.ts +99 -14
  483. package/src/sessions/session-checker-fail-open.test.ts +77 -0
  484. package/src/sessions/session-revoked-event.ts +29 -0
  485. package/src/sessions/testing.ts +14 -1
  486. package/src/shared/__tests__/chunked-entity-migration.test.ts +145 -0
  487. package/src/shared/chunked-entity-migration.ts +89 -0
  488. package/src/shared/decrypt-stored-pii.ts +26 -0
  489. package/src/shared/encrypt-for-direct-write.ts +23 -0
  490. package/src/shared/entities-of.ts +17 -0
  491. package/src/shared/grace-period.test.ts +26 -0
  492. package/src/shared/grace-period.ts +20 -0
  493. package/src/{auth-email-password/__tests__ → shared}/identity-v3-hash.test.ts +2 -2
  494. package/src/shared/index.ts +16 -0
  495. package/src/shared/map-with-concurrency.test.ts +104 -0
  496. package/src/shared/map-with-concurrency.ts +38 -0
  497. package/src/shared/password-hashing.test.ts +35 -0
  498. package/src/{auth-email-password → shared}/password-hashing.ts +11 -0
  499. package/src/shared/system-query.ts +5 -0
  500. package/src/{auth-email-password → shared}/token-burn-store.ts +1 -1
  501. package/src/subscription-mollie/__tests__/feature.test.ts +97 -0
  502. package/src/subscription-mollie/__tests__/mollie-foundation.integration.test.ts +12 -1
  503. package/src/subscription-mollie/__tests__/verify-webhook.test.ts +21 -0
  504. package/src/subscription-mollie/verify-webhook.ts +1 -0
  505. package/src/subscription-stripe/__tests__/feature.test.ts +127 -1
  506. package/src/subscription-stripe/__tests__/runtime.test.ts +14 -24
  507. package/src/subscription-stripe/__tests__/stripe-foundation.integration.test.ts +15 -5
  508. package/src/subscription-stripe/__tests__/verify-webhook.test.ts +20 -0
  509. package/src/subscription-stripe/feature.ts +2 -0
  510. package/src/subscription-stripe/verify-webhook.ts +3 -2
  511. package/src/tags/__tests__/schemas.test.ts +34 -0
  512. package/src/tags/__tests__/tags.integration.test.ts +12 -1
  513. package/src/tags/entity.ts +6 -1
  514. package/src/tags/feature.ts +2 -0
  515. package/src/tags/i18n.ts +5 -0
  516. package/src/tags/schemas.ts +9 -2
  517. package/src/tags/web/__tests__/tag-manager.test.tsx +333 -0
  518. package/src/tags/web/__tests__/tag-section.test.tsx +47 -1
  519. package/src/tags/web/tag-manager.tsx +1 -1
  520. package/src/tags/web/tag-section.tsx +6 -3
  521. package/src/template-resolver/testing.ts +2 -1
  522. package/src/tenant/__tests__/members-query-kms.integration.test.ts +98 -0
  523. package/src/tenant/__tests__/members-screens.boot.test.ts +51 -0
  524. package/src/tenant/__tests__/multi-tenant.integration.test.ts +1 -1
  525. package/src/tenant/__tests__/tenant-security.integration.test.ts +310 -0
  526. package/src/tenant/__tests__/tenant.integration.test.ts +8 -4
  527. package/src/tenant/constants.ts +5 -0
  528. package/src/tenant/feature.ts +19 -0
  529. package/src/tenant/handlers/active-tenant-ids.query.ts +8 -2
  530. package/src/tenant/handlers/cancel-invitation.write.ts +2 -2
  531. package/src/tenant/handlers/invitations.query.ts +21 -4
  532. package/src/tenant/handlers/members.query.ts +41 -6
  533. package/src/tenant/i18n.ts +28 -0
  534. package/src/tenant/index.ts +7 -2
  535. package/src/tenant/invitation-table.ts +3 -2
  536. package/src/tenant/schema/tenant.ts +27 -2
  537. package/src/tenant/screens.ts +3 -2
  538. package/src/tenant/seeding.ts +136 -3
  539. package/src/tenant/web/client-plugin.tsx +20 -0
  540. package/src/tenant/web/i18n.ts +43 -0
  541. package/src/tenant/web/index.ts +4 -0
  542. package/src/tenant/web/members-screen.tsx +270 -0
  543. package/src/tenant-lifecycle/__tests__/stages.test.ts +22 -0
  544. package/src/tenant-lifecycle/__tests__/tenant-lifecycle.integration.test.ts +471 -0
  545. package/src/tenant-lifecycle/boot-checks.ts +45 -0
  546. package/src/tenant-lifecycle/constants.ts +56 -0
  547. package/src/tenant-lifecycle/events.ts +46 -0
  548. package/src/tenant-lifecycle/feature.ts +112 -0
  549. package/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +79 -0
  550. package/src/tenant-lifecycle/handlers/request-destruction.write.ts +83 -0
  551. package/src/tenant-lifecycle/index.ts +7 -0
  552. package/src/tenant-lifecycle/lib/revoke-tenant-sessions.ts +16 -0
  553. package/src/tenant-lifecycle/lifecycle-gate.ts +65 -0
  554. package/src/tenant-lifecycle/run-tenant-destroy.ts +368 -0
  555. package/src/tenant-lifecycle/stages.ts +206 -0
  556. package/src/tenant-settings/__tests__/tenant-settings.integration.test.ts +115 -0
  557. package/src/tenant-settings/config.ts +44 -0
  558. package/src/tenant-settings/constants.ts +16 -0
  559. package/src/tenant-settings/feature.ts +49 -0
  560. package/src/tenant-settings/index.ts +8 -0
  561. package/src/tenant-settings/tenant-defaults.ts +67 -0
  562. package/src/text-content/web/__tests__/editor-read-only.test.tsx +33 -6
  563. package/src/text-content/web/__tests__/group-blocks.test.ts +28 -3
  564. package/src/text-content/web/client-plugin.tsx +126 -104
  565. package/src/tier-engine/__tests__/auto-default-tier.integration.test.ts +1 -1
  566. package/src/tier-engine/__tests__/resolver.integration.test.ts +1 -1
  567. package/src/tier-engine/__tests__/tier-engine.integration.test.ts +7 -4
  568. package/src/tier-engine/__tests__/trial-config-validation.test.ts +30 -0
  569. package/src/tier-engine/feature.ts +13 -6
  570. package/src/tier-engine/i18n.ts +8 -0
  571. package/src/tier-engine/web/tier-admin-screen.tsx +26 -19
  572. package/src/user/__tests__/stream-tenant-backfill.integration.test.ts +186 -0
  573. package/src/user/__tests__/user.integration.test.ts +26 -0
  574. package/src/user/constants.ts +1 -0
  575. package/src/user/db/queries/stream-tenant-backfill.ts +112 -0
  576. package/src/user/feature.ts +3 -0
  577. package/src/user/handlers/find-for-auth.query.ts +8 -6
  578. package/src/user/i18n.ts +20 -0
  579. package/src/user/index.ts +4 -0
  580. package/src/user/schema/user.ts +6 -2
  581. package/src/user/screens.ts +3 -15
  582. package/src/user/seeding.ts +8 -0
  583. package/src/user-data-rights/COMPLIANCE.md +21 -1
  584. package/src/user-data-rights/__tests__/anonymous-deletion-kms.integration.test.ts +145 -0
  585. package/src/user-data-rights/__tests__/anonymous-deletion.integration.test.ts +3 -0
  586. package/src/user-data-rights/__tests__/audit-log.integration.test.ts +67 -0
  587. package/src/user-data-rights/__tests__/boot-checks.test.ts +170 -0
  588. package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +4 -1
  589. package/src/user-data-rights/__tests__/download-by-token-resolver-trust.integration.test.ts +169 -0
  590. package/src/user-data-rights/__tests__/download.integration.test.ts +101 -33
  591. package/src/user-data-rights/__tests__/email-templates.test.ts +15 -0
  592. package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +154 -0
  593. package/src/user-data-rights/__tests__/export-job-idempotency.integration.test.ts +2 -0
  594. package/src/user-data-rights/__tests__/export-job-list-cross-tenant.integration.test.ts +2 -0
  595. package/src/user-data-rights/__tests__/file-binary-forget-cleanup.integration.test.ts +4 -1
  596. package/src/user-data-rights/__tests__/file-binary-forget-failure.integration.test.ts +4 -1
  597. package/src/user-data-rights/__tests__/file-retention.integration.test.ts +4 -1
  598. package/src/user-data-rights/__tests__/file-storage-unification.integration.test.ts +4 -1
  599. package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +41 -62
  600. package/src/user-data-rights/__tests__/forget-hook-registry.integration.test.ts +110 -0
  601. package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +14 -0
  602. package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +19 -2
  603. package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +25 -14
  604. package/src/user-data-rights/__tests__/request-cancel-deletion.integration.test.ts +2 -0
  605. package/src/user-data-rights/__tests__/request-deletion-callback.integration.test.ts +4 -1
  606. package/src/user-data-rights/__tests__/request-export.integration.test.ts +2 -0
  607. package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +172 -19
  608. package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +2 -0
  609. package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +257 -26
  610. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +339 -5
  611. package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +77 -1
  612. package/src/user-data-rights/__tests__/tenant-model-erasure.integration.test.ts +2 -0
  613. package/src/user-data-rights/__tests__/user-data-rights.integration.test.ts +9 -1
  614. package/src/user-data-rights/boot-checks.ts +70 -0
  615. package/src/user-data-rights/db/queries/__tests__/forget-cleanup.integration.test.ts +69 -0
  616. package/src/user-data-rights/db/queries/forget-cleanup.ts +9 -2
  617. package/src/user-data-rights/email-templates.ts +6 -6
  618. package/src/user-data-rights/feature.ts +162 -43
  619. package/src/user-data-rights/handlers/cancel-deletion.write.ts +8 -11
  620. package/src/user-data-rights/handlers/deletion-grace-period.ts +4 -1
  621. package/src/user-data-rights/handlers/download-by-job.query.ts +26 -18
  622. package/src/user-data-rights/handlers/download-by-token.query.ts +23 -15
  623. package/src/user-data-rights/handlers/lift-restriction.write.ts +44 -19
  624. package/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -0
  625. package/src/user-data-rights/handlers/my-audit-log.query.ts +1 -0
  626. package/src/user-data-rights/handlers/request-deletion-by-email.write.ts +3 -1
  627. package/src/user-data-rights/handlers/restrict-account.write.ts +58 -7
  628. package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +2 -0
  629. package/src/user-data-rights/i18n.ts +35 -32
  630. package/src/user-data-rights/lib/is-admin-actor.ts +19 -0
  631. package/src/user-data-rights/lib/storage-provider-resolver.ts +6 -5
  632. package/src/user-data-rights/lib/update-user-lifecycle.ts +8 -24
  633. package/src/user-data-rights/run-export-jobs.ts +55 -19
  634. package/src/user-data-rights/run-forget-cleanup.ts +112 -7
  635. package/src/user-data-rights/run-user-export.ts +70 -4
  636. package/src/user-data-rights/schema/download-token.ts +5 -3
  637. package/src/user-data-rights/schema/export-job.ts +2 -0
  638. package/src/user-data-rights/screens.ts +2 -1
  639. package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +1 -0
  640. package/src/user-data-rights/web/__tests__/public-deletion-gate.test.tsx +7 -1
  641. package/src/user-data-rights/web/client-plugin.tsx +6 -1
  642. package/src/user-data-rights/web/i18n.ts +30 -10
  643. package/src/user-data-rights/web/privacy-center-screen.tsx +5 -3
  644. package/src/user-data-rights-defaults/__tests__/bundled-entities.userdata-hooks.integration.test.ts +455 -0
  645. package/src/user-data-rights-defaults/__tests__/feature-mounted.test.ts +24 -0
  646. package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +33 -9
  647. package/src/user-data-rights-defaults/feature.ts +78 -1
  648. package/src/user-data-rights-defaults/hooks/api-token.userdata-hook.ts +37 -0
  649. package/src/user-data-rights-defaults/hooks/config-value.userdata-hook.ts +51 -0
  650. package/src/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.ts +37 -0
  651. package/src/user-data-rights-defaults/hooks/feature-mounted.ts +10 -0
  652. package/src/user-data-rights-defaults/hooks/in-app-message.userdata-hook.ts +38 -0
  653. package/src/user-data-rights-defaults/hooks/job-run.userdata-hook.ts +35 -0
  654. package/src/user-data-rights-defaults/hooks/notification-preference.userdata-hook.ts +54 -0
  655. package/src/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.ts +112 -0
  656. package/src/user-data-rights-defaults/hooks/user-session.userdata-hook.ts +42 -0
  657. package/src/user-data-rights-defaults/index.ts +26 -0
  658. package/src/user-profile/__tests__/change-email.integration.test.ts +7 -1
  659. package/src/user-profile/__tests__/profile-screen.test.tsx +5 -0
  660. package/src/user-profile/feature.ts +4 -1
  661. package/src/user-profile/handlers/change-email.write.ts +15 -10
  662. package/src/user-profile/web/profile-screen.tsx +15 -3
  663. package/src/auth-email-password/stream-tenant.ts +0 -31
  664. package/src/feature-toggles/db/queries/toggle-state.ts +0 -25
  665. package/src/legal-pages/security-headers.ts +0 -1
  666. package/src/sessions/schema/index.ts +0 -5
  667. package/src/tenant/schema/index.ts +0 -5
  668. package/src/user/schema/index.ts +0 -5
  669. /package/src/{auth-email-password → shared}/identity-v3-hash.ts +0 -0
@@ -1,4 +1,5 @@
1
1
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
2
3
  import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
3
4
  import { createTenantDb, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
4
5
  import { createRegistry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
@@ -19,7 +20,7 @@ import { createUserFeature } from "../../user/feature";
19
20
  import { createSessionsFeature } from "../feature";
20
21
  import { userSessionEntity, userSessionTable } from "../schema/user-session";
21
22
 
22
- // read_user_sessions is a hot-path direct-write store: sessionCreator inserts
23
+ // store_user_sessions is a hot-path direct-write store: sessionCreator inserts
23
24
  // rows and the revoke handlers update them WITHOUT emitting lifecycle events.
24
25
  // If the table is registered as an r.entity, the framework makes it a
25
26
  // rebuildable implicit projection whose replay finds zero matching events and
@@ -27,7 +28,7 @@ import { userSessionEntity, userSessionTable } from "../schema/user-session";
27
28
  // session on the next projection rebuild (deploy / `schema apply`). #498/#494.
28
29
  //
29
30
  // Pre-fix both tests are RED: the implicit projection "sessions:projection:
30
- // user-session-entity" exists and rebuilding it empties read_user_sessions.
31
+ // user-session-entity" exists and rebuilding it empties store_user_sessions.
31
32
  // Post-fix (r.unmanagedTable) the table is no longer a rebuild target.
32
33
 
33
34
  const IMPLICIT_PROJECTION = "sessions:projection:user-session-entity";
@@ -48,7 +49,7 @@ afterAll(async () => {
48
49
 
49
50
  beforeEach(async () => {
50
51
  await asRawClient(testDb.db).unsafe(
51
- "TRUNCATE read_user_sessions, kumiko_events, kumiko_projections RESTART IDENTITY CASCADE",
52
+ "TRUNCATE store_user_sessions, kumiko_events, kumiko_projections RESTART IDENTITY CASCADE",
52
53
  );
53
54
  });
54
55
 
@@ -56,7 +57,9 @@ beforeEach(async () => {
56
57
  // written directly on the hot path, then revoked — no events anywhere.
57
58
  const SID = "00000000-0000-0000-0000-000000000001";
58
59
 
59
- async function insertRevokedSession(db: DbConnection): Promise<void> {
60
+ async function insertRevokedSession(
61
+ db: DbConnection | ReturnType<typeof createTenantDb>,
62
+ ): Promise<void> {
60
63
  const now = Temporal.Now.instant();
61
64
  await seedRow(db, userSessionTable, {
62
65
  id: SID,
@@ -70,16 +73,24 @@ async function insertRevokedSession(db: DbConnection): Promise<void> {
70
73
  await updateRows(db, userSessionTable, { revokedAt: now }, { id: SID, revokedAt: null });
71
74
  }
72
75
 
73
- describe("sessions / read_user_sessions survives projection rebuild", () => {
76
+ describe("sessions / store_user_sessions survives projection rebuild", () => {
74
77
  test("is NOT registered as a rebuildable implicit projection", () => {
75
- const registry = createRegistry([createSessionsFeature(), createUserFeature()]);
78
+ const registry = createRegistry([
79
+ authFoundationFeature,
80
+ createSessionsFeature(),
81
+ createUserFeature(),
82
+ ]);
76
83
  expect(registry.getAllProjections().has(IMPLICIT_PROJECTION)).toBe(false);
77
84
  });
78
85
 
79
86
  test("direct-written rows (incl. revoked state) survive a rebuild", async () => {
80
87
  await insertRevokedSession(createTenantDb(testDb.db, TENANT));
81
88
 
82
- const registry = createRegistry([createSessionsFeature(), createUserFeature()]);
89
+ const registry = createRegistry([
90
+ authFoundationFeature,
91
+ createSessionsFeature(),
92
+ createUserFeature(),
93
+ ]);
83
94
  // Pre-fix: the implicit projection exists → rebuild swaps an empty shadow
84
95
  // → rows wiped. Post-fix: absent → no rebuild → rows untouched. Either way
85
96
  // a regression (re-adding r.entity) makes this fail.
@@ -0,0 +1,21 @@
1
+ // sessions self-registers as auth-foundation's sessionStore provider (#1371)
2
+ // instead of only being wired by hand into buildServer({ auth: { ... } }).
3
+
4
+ import { describe, expect, test } from "bun:test";
5
+ import { EXT_SESSION_STORE } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
6
+ import { createSessionsFeature } from "../feature";
7
+
8
+ describe("createSessionsFeature — sessionStore registration (#1371)", () => {
9
+ test('registers via r.useExtension(EXT_SESSION_STORE, "default", ...)', () => {
10
+ const feature = createSessionsFeature();
11
+ const usage = feature.extensionUsages.find((u) => u.extensionName === EXT_SESSION_STORE);
12
+ expect(usage).toBeDefined();
13
+ expect(usage?.entityName).toBe("default");
14
+ expect(typeof (usage?.options as { build?: unknown } | undefined)?.build).toBe("function");
15
+ });
16
+
17
+ test("requires auth-foundation (owner of EXT_SESSION_STORE)", () => {
18
+ const feature = createSessionsFeature();
19
+ expect(feature.requires).toContain("auth-foundation");
20
+ });
21
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { rolesOf } from "@cosmicdrift/kumiko-framework/testing";
4
+ import { authFoundationFeature } from "../../auth-foundation";
5
+ import { createConfigFeature } from "../../config/feature";
6
+ import { createPersonalAccessTokensFeature } from "../../personal-access-tokens";
7
+ import { createTenantFeature } from "../../tenant";
8
+ import { createUserFeature } from "../../user/feature";
9
+ import { SESSION_DETAIL_SCREEN_ID, SESSION_LIST_SCREEN_ID } from "../constants";
10
+ import { createSessionsFeature } from "../feature";
11
+
12
+ describe("sessions screens + query access alignment (kumiko-framework#255)", () => {
13
+ const features = [
14
+ createConfigFeature(),
15
+ createUserFeature(),
16
+ createTenantFeature(),
17
+ authFoundationFeature,
18
+ createPersonalAccessTokensFeature({ scopes: {} }),
19
+ createSessionsFeature(),
20
+ ];
21
+
22
+ test("boot-validates with session-list/session-detail screens registered", () => {
23
+ expect(() => validateBoot(features)).not.toThrow();
24
+ });
25
+
26
+ test("session-list is projectionList, session-detail is projectionDetail, both admin-gated", () => {
27
+ const sessions = createSessionsFeature();
28
+ const list = sessions.screens[SESSION_LIST_SCREEN_ID];
29
+ expect(list?.type).toBe("projectionList");
30
+ const detail = sessions.screens[SESSION_DETAIL_SCREEN_ID];
31
+ expect(detail?.type).toBe("projectionDetail");
32
+ for (const screen of [list, detail]) {
33
+ if (screen && "access" in screen && screen.access && "roles" in screen.access) {
34
+ expect(screen.access.roles).toEqual(["TenantAdmin", "Admin", "SystemAdmin"]);
35
+ }
36
+ }
37
+ });
38
+
39
+ test("session-list row-action navigates to session-detail with entityId 'id'", () => {
40
+ const sessions = createSessionsFeature();
41
+ const list = sessions.screens[SESSION_LIST_SCREEN_ID];
42
+ if (list?.type !== "projectionList") throw new Error("expected projectionList");
43
+ const openAction = list.rowActions?.find((a) => a.id === "open");
44
+ if (openAction?.kind !== "navigate") throw new Error("expected a navigate rowAction");
45
+ expect(openAction.screen).toBe(SESSION_DETAIL_SCREEN_ID);
46
+ expect(openAction.entityId).toBe("id");
47
+ });
48
+
49
+ test("sessions queries share the admin-or-higher access rule", () => {
50
+ const sessions = createSessionsFeature();
51
+ const roles = ["TenantAdmin", "Admin", "SystemAdmin"];
52
+ expect(rolesOf(sessions.queryHandlers["user-session:list"]?.access)).toEqual(roles);
53
+ expect(rolesOf(sessions.queryHandlers["user-session:detail"]?.access)).toEqual(roles);
54
+ });
55
+ });
@@ -1,17 +1,28 @@
1
1
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { randomBytes } from "node:crypto";
3
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
3
4
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
4
- import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
5
+ import {
6
+ configureBlindIndexKey,
7
+ configurePiiSubjectKms,
8
+ InMemoryKmsAdapter,
9
+ isPiiCiphertext,
10
+ resetBlindIndexKeyForTests,
11
+ resetPiiSubjectKmsForTests,
12
+ } from "@cosmicdrift/kumiko-framework/crypto";
5
13
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
14
+ import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
6
15
  import {
7
16
  setupTestStack,
8
17
  type TestStack,
18
+ TestUsers,
9
19
  testTenantId,
10
20
  unsafeCreateEntityTable,
11
21
  unsafePushTables,
12
22
  } from "@cosmicdrift/kumiko-framework/stack";
13
23
  import {
14
24
  createLateBoundHolder,
25
+ createTestEnvelopeCipher,
15
26
  deleteRows,
16
27
  resetTestTables,
17
28
  updateRows,
@@ -31,6 +42,7 @@ import { SessionHandlers, SessionQueries } from "../constants";
31
42
  import { createSessionsFeature } from "../feature";
32
43
  import { userSessionEntity, userSessionTable } from "../schema/user-session";
33
44
  import { createSessionCallbacks, type SessionCallbacks } from "../session-callbacks";
45
+ import { SESSION_REVOKED_EVENT_QN, type SessionRevokedPayload } from "../session-revoked-event";
34
46
  import { sessionCallbacksFromLateBound } from "../testing";
35
47
  import { makeSessionHelpers } from "./test-helpers";
36
48
 
@@ -47,8 +59,8 @@ const encryptionKey = randomBytes(32).toString("base64");
47
59
  const TENANT: TenantId = testTenantId(1);
48
60
 
49
61
  beforeAll(async () => {
50
- const encryption = createEncryptionProvider(encryptionKey);
51
- const resolver = createConfigResolver({ encryption });
62
+ const encryption = createTestEnvelopeCipher(encryptionKey);
63
+ const resolver = createConfigResolver({ cipher: encryption });
52
64
  const bound = sessionCallbacksFromLateBound(callbacks);
53
65
 
54
66
  stack = await setupTestStack({
@@ -57,6 +69,7 @@ beforeAll(async () => {
57
69
  createUserFeature(),
58
70
  createTenantFeature(),
59
71
  createAuthEmailPasswordFeature(),
72
+ authFoundationFeature,
60
73
  createSessionsFeature(),
61
74
  ],
62
75
  extraContext: { configResolver: resolver, configEncryption: encryption },
@@ -67,12 +80,13 @@ beforeAll(async () => {
67
80
  },
68
81
  });
69
82
  callbacks.set(createSessionCallbacks({ db: stack.db }));
70
- h = makeSessionHelpers(stack, TENANT);
83
+ h = makeSessionHelpers(stack, TENANT, bound.asAuthConfig().sessionCreator);
71
84
 
72
85
  await unsafeCreateEntityTable(stack.db, userEntity);
73
86
  await unsafeCreateEntityTable(stack.db, tenantEntity);
74
87
  await unsafeCreateEntityTable(stack.db, userSessionEntity);
75
88
  await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
89
+ await createEventsTable(stack.db);
76
90
  });
77
91
 
78
92
  afterAll(async () => {
@@ -80,7 +94,12 @@ afterAll(async () => {
80
94
  });
81
95
 
82
96
  beforeEach(async () => {
83
- await resetTestTables(stack.db, [userTable, tenantMembershipsTable, userSessionTable]);
97
+ await resetTestTables(stack.db, [
98
+ userTable,
99
+ tenantMembershipsTable,
100
+ userSessionTable,
101
+ eventsTable,
102
+ ]);
84
103
  });
85
104
 
86
105
  describe("sessions feature — login → check → revoke → rejected", () => {
@@ -89,9 +108,10 @@ describe("sessions feature — login → check → revoke → rejected", () => {
89
108
  const { sid } = await h.login("persist@example.com", "pw-long-enough");
90
109
 
91
110
  const rows = await selectMany(stack.db, userSessionTable);
92
- expect(rows).toHaveLength(1);
93
- expect(rows[0]?.["id"]).toBe(sid);
94
- expect(rows[0]?.["revokedAt"]).toBeNull();
111
+ const userRows = rows.filter((r) => r["userId"] !== TestUsers.systemAdmin.id);
112
+ expect(userRows).toHaveLength(1);
113
+ expect(userRows[0]?.["id"]).toBe(sid);
114
+ expect(userRows[0]?.["revokedAt"]).toBeNull();
95
115
  });
96
116
 
97
117
  test("authenticated request passes while session is live, 401s once revoked", async () => {
@@ -130,8 +150,9 @@ describe("sessions feature — login → check → revoke → rejected", () => {
130
150
  expect(logoutRes.status).toBe(200);
131
151
 
132
152
  const rows = await selectMany(stack.db, userSessionTable);
133
- expect(rows[0]?.["id"]).toBe(sid);
134
- expect(rows[0]?.["revokedAt"]).not.toBeNull();
153
+ const row = rows.find((r) => r["id"] === sid);
154
+ expect(row?.["id"]).toBe(sid);
155
+ expect(row?.["revokedAt"]).not.toBeNull();
135
156
 
136
157
  const next = await h.authedPost("/api/query", token, {
137
158
  type: "user:query:user:me",
@@ -178,6 +199,69 @@ describe("sessions feature — login → check → revoke → rejected", () => {
178
199
  expect(preservedRevokedAt?.epochMilliseconds).toBe(originalRevokedAt?.epochMilliseconds);
179
200
  });
180
201
 
202
+ // #1559 — store_user_sessions stays a direct-write store (no CRUD
203
+ // lifecycle events, see feature.ts), but a successful revoke must still
204
+ // append a lightweight session-revoked event so a cross-instance
205
+ // consumer (#1560) can react. Asserted directly against kumiko_events —
206
+ // the response body carries no trace of it.
207
+ test("revoke appends a session-revoked event carrying the userId + sid", async () => {
208
+ const { userId } = await h.seedUser("event1@example.com", "pw-long-enough");
209
+ const { token, sid } = await h.login("event1@example.com", "pw-long-enough");
210
+
211
+ const res = await h.authedPost("/api/write", token, {
212
+ type: SessionHandlers.revoke,
213
+ payload: { id: sid },
214
+ });
215
+ expect(res.status).toBe(200);
216
+
217
+ const events = await selectMany(stack.db, eventsTable, { type: SESSION_REVOKED_EVENT_QN });
218
+ expect(events).toHaveLength(1);
219
+ const payload = events[0]?.["payload"] as SessionRevokedPayload;
220
+ expect(payload.userId).toBe(userId);
221
+ expect(payload.sessionIds).toEqual([sid]);
222
+ });
223
+
224
+ // Negative case: an already-revoked sid must NOT emit a second event —
225
+ // a spurious invalidation is the failure mode #1559 guards against.
226
+ test("revoking an already-revoked sid emits no additional event", async () => {
227
+ await h.seedUser("event2@example.com", "pw-long-enough");
228
+ const first = await h.login("event2@example.com", "pw-long-enough");
229
+
230
+ await h.authedPost("/api/write", first.token, {
231
+ type: SessionHandlers.revoke,
232
+ payload: { id: first.sid },
233
+ });
234
+
235
+ const second = await h.login("event2@example.com", "pw-long-enough");
236
+ const retry = await h.authedPost("/api/write", second.token, {
237
+ type: SessionHandlers.revoke,
238
+ payload: { id: first.sid },
239
+ });
240
+ expect(retry.status).toBe(422);
241
+
242
+ const events = await selectMany(stack.db, eventsTable, { type: SESSION_REVOKED_EVENT_QN });
243
+ expect(events).toHaveLength(1); // only the first, successful revoke
244
+ });
245
+
246
+ test("revoke-all-others appends one event listing every revoked sid", async () => {
247
+ const { userId } = await h.seedUser("event3@example.com", "pw-long-enough");
248
+ const a = await h.login("event3@example.com", "pw-long-enough");
249
+ const b = await h.login("event3@example.com", "pw-long-enough");
250
+ const c = await h.login("event3@example.com", "pw-long-enough");
251
+
252
+ const res = await h.authedPost("/api/write", b.token, {
253
+ type: SessionHandlers.revokeAllOthers,
254
+ payload: {},
255
+ });
256
+ expect(res.status).toBe(200);
257
+
258
+ const events = await selectMany(stack.db, eventsTable, { type: SESSION_REVOKED_EVENT_QN });
259
+ expect(events).toHaveLength(1);
260
+ const payload = events[0]?.["payload"] as SessionRevokedPayload;
261
+ expect(payload.userId).toBe(userId);
262
+ expect(new Set(payload.sessionIds)).toEqual(new Set([a.sid, c.sid]));
263
+ });
264
+
181
265
  test("session:mine only returns live sessions, marks the current one", async () => {
182
266
  await h.seedUser("mine@example.com", "pw-long-enough");
183
267
  // Three logins = three sessions (think: browser, mobile app, tablet)
@@ -286,12 +370,13 @@ describe("sessions feature — login → check → revoke → rejected", () => {
286
370
  expect(body.error?.details?.reason).toBe("ownership_denied");
287
371
  });
288
372
 
289
- test("revoke-all-others on a sidless JWT refuses loudly (session_required)", async () => {
373
+ test("revoke-all-others on a sidless JWT is rejected by the middleware (no_sid)", async () => {
290
374
  const { userId } = await h.seedUser("sidless@example.com", "pw-long-enough");
291
375
 
292
- // Hand-sign a JWT without jti — simulates a stateless-JWT deployment
293
- // or a rolling-deploy gap. "sign out everywhere else" is ill-defined
294
- // without knowing which session is "current", so refuse.
376
+ // Hand-sign a JWT without jti. Once a sessionChecker is wired there is
377
+ // no relaxed mode (see session-strict-mode.integration.test.ts) — the
378
+ // middleware rejects it with no_sid before the handler's own
379
+ // session_required guard is ever reached.
295
380
  const tokenNoSid = await stack.jwt.sign({ id: userId, tenantId: TENANT, roles: ["User"] });
296
381
 
297
382
  const res = await stack.http.raw(
@@ -300,9 +385,9 @@ describe("sessions feature — login → check → revoke → rejected", () => {
300
385
  { type: SessionHandlers.revokeAllOthers, payload: {} },
301
386
  { Authorization: `Bearer ${tokenNoSid}` },
302
387
  );
303
- expect(res.status).toBe(422);
388
+ expect(res.status).toBe(401);
304
389
  const body = (await res.json()) as { error?: { details?: { reason?: string } } };
305
- expect(body.error?.details?.reason).toBe("session_required");
390
+ expect(body.error?.details?.reason).toBe("no_sid");
306
391
  });
307
392
 
308
393
  test("N concurrent revoke+logout requests on the same sid converge safely", async () => {
@@ -383,6 +468,41 @@ describe("sessions feature — login → check → revoke → rejected", () => {
383
468
  expect(body.error?.details?.reason).toBe("missing");
384
469
  });
385
470
 
471
+ // Cross-user check: a valid JWT for Alice paired with Bob's live sid must
472
+ // look identical to a forged/missing sid — otherwise the checker would be
473
+ // an existence oracle on other users' sessions.
474
+ test("sid belonging to another user → 401 reason=missing (no existence oracle)", async () => {
475
+ const alice = await h.seedUser("alice-xuser@example.com", "pw-long-enough");
476
+ const bob = await h.seedUser("bob-xuser@example.com", "pw-long-enough");
477
+ const bobLogin = await h.login("bob-xuser@example.com", "pw-long-enough");
478
+
479
+ // Direct invariant: Bob's sid + Alice's userId → missing (not live/revoked).
480
+ expect(await callbacks.get().sessionChecker(bobLogin.sid, alice.userId)).toBe("missing");
481
+ expect(await callbacks.get().sessionChecker(bobLogin.sid, bob.userId)).toBe("live");
482
+
483
+ // HTTP path: mint Alice's identity onto Bob's sid (stolen-sid scenario).
484
+ const forged = await stack.jwt.sign({
485
+ id: alice.userId,
486
+ tenantId: TENANT,
487
+ roles: ["User"],
488
+ sid: bobLogin.sid,
489
+ });
490
+ const res = await h.authedPost("/api/query", forged, {
491
+ type: "user:query:user:me",
492
+ payload: {},
493
+ });
494
+ expect(res.status).toBe(401);
495
+ const body = (await res.json()) as { error?: { details?: { reason?: string } } };
496
+ expect(body.error?.details?.reason).toBe("missing");
497
+
498
+ // Control: Bob's own JWT still authenticates — the sid itself is fine.
499
+ const bobOk = await h.authedPost("/api/query", bobLogin.token, {
500
+ type: "user:query:user:me",
501
+ payload: {},
502
+ });
503
+ expect(bobOk.status).toBe(200);
504
+ });
505
+
386
506
  test("expired session row → 401 with reason=expired", async () => {
387
507
  await h.seedUser("stale@example.com", "pw-long-enough");
388
508
  const { token, sid } = await h.login("stale@example.com", "pw-long-enough");
@@ -448,10 +568,13 @@ describe("sessions feature — login → check → revoke → rejected", () => {
448
568
  revokedAt: string | null;
449
569
  }>;
450
570
  };
451
- // Three rows total: Alice's pre-promotion session, Alice's post-promotion
452
- // session, Bob's session. Two distinct users.
453
- expect(body.data).toHaveLength(3);
454
- const userIds = new Set(body.data.map((r) => r.userId));
571
+ // Three rows total for the two test users: Alice's pre-promotion session,
572
+ // Alice's post-promotion session, Bob's session. seedUser's bootstrap
573
+ // actor (systemAdmin) also holds live sessions in this tenant — exclude
574
+ // them, they aren't part of what this test pins.
575
+ const nonSystemRows = body.data.filter((r) => r.userId !== TestUsers.systemAdmin.id);
576
+ expect(nonSystemRows).toHaveLength(3);
577
+ const userIds = new Set(nonSystemRows.map((r) => r.userId));
455
578
  expect(userIds.size).toBe(2);
456
579
 
457
580
  // Order: most-recently-created first. aliceAsAdmin's session was the
@@ -459,6 +582,94 @@ describe("sessions feature — login → check → revoke → rejected", () => {
459
582
  // silent orderBy removal.
460
583
  expect(body.data[0]?.id).toBe(aliceAsAdmin.sid);
461
584
  });
585
+
586
+ // Single-row inspector backing the session-detail screen (kumiko-framework#255).
587
+ // Same access-gate as session:list (admin-or-higher); verifies field-shape,
588
+ // decryption of ip/userAgent, and the "unknown id" not-found path.
589
+ test("session:detail returns one decrypted row for admins, null for an unknown id", async () => {
590
+ const { userId: aliceId } = await h.seedUser("alice3@example.com", "pw-long-enough");
591
+
592
+ await updateRows(
593
+ stack.db,
594
+ tenantMembershipsTable,
595
+ { roles: JSON.stringify(["Admin"]) },
596
+ { userId: aliceId, tenantId: TENANT },
597
+ );
598
+ const aliceAsAdmin = await h.login("alice3@example.com", "pw-long-enough");
599
+
600
+ const res = await h.authedPost("/api/query", aliceAsAdmin.token, {
601
+ type: SessionQueries.detail,
602
+ payload: { id: aliceAsAdmin.sid },
603
+ });
604
+ expect(res.status).toBe(200);
605
+ const body = (await res.json()) as {
606
+ data: { id: string; userId: string; revokedAt: string | null } | null;
607
+ };
608
+ expect(body.data?.id).toBe(aliceAsAdmin.sid);
609
+ expect(body.data?.userId).toBe(aliceId);
610
+ expect(body.data?.revokedAt).toBeNull();
611
+
612
+ const missing = await h.authedPost("/api/query", aliceAsAdmin.token, {
613
+ type: SessionQueries.detail,
614
+ payload: { id: "00000000-0000-4000-8000-0000deadbeef" },
615
+ });
616
+ expect(missing.status).toBe(200);
617
+ const missingBody = (await missing.json()) as { data: unknown };
618
+ expect(missingBody.data).toBeNull();
619
+
620
+ // Plain User gets the same 403 as session:list — same access-gate.
621
+ await h.seedUser("bob3@example.com", "pw-long-enough");
622
+ const bob = await h.login("bob3@example.com", "pw-long-enough");
623
+ const asUser = await h.authedPost("/api/query", bob.token, {
624
+ type: SessionQueries.detail,
625
+ payload: { id: aliceAsAdmin.sid },
626
+ });
627
+ expect(asUser.status).toBe(403);
628
+ });
629
+
630
+ // IDOR guard: session:detail/session:list are scoped by ctx.db (TenantDb)
631
+ // alone, no explicit tenant predicate in the handler — an admin from a
632
+ // different tenant must not be able to read another tenant's session rows.
633
+ test("session:detail and session:list never leak sessions across tenants", async () => {
634
+ const TENANT_2 = testTenantId(2);
635
+ const h2 = makeSessionHelpers(stack, TENANT_2, (user, meta) =>
636
+ callbacks.get().sessionCreator(user, meta),
637
+ );
638
+
639
+ const { userId: carolId } = await h.seedUser("carol4@example.com", "pw-long-enough");
640
+ await updateRows(
641
+ stack.db,
642
+ tenantMembershipsTable,
643
+ { roles: JSON.stringify(["Admin"]) },
644
+ { userId: carolId, tenantId: TENANT },
645
+ );
646
+ const carolAsAdmin = await h.login("carol4@example.com", "pw-long-enough");
647
+
648
+ const { userId: daveId } = await h2.seedUser("dave4@example.com", "pw-long-enough");
649
+ await updateRows(
650
+ stack.db,
651
+ tenantMembershipsTable,
652
+ { roles: JSON.stringify(["Admin"]) },
653
+ { userId: daveId, tenantId: TENANT_2 },
654
+ );
655
+ const daveAsAdmin = await h2.login("dave4@example.com", "pw-long-enough");
656
+
657
+ const detailRes = await h2.authedPost("/api/query", daveAsAdmin.token, {
658
+ type: SessionQueries.detail,
659
+ payload: { id: carolAsAdmin.sid },
660
+ });
661
+ expect(detailRes.status).toBe(200);
662
+ const detailBody = (await detailRes.json()) as { data: unknown };
663
+ expect(detailBody.data).toBeNull();
664
+
665
+ const listRes = await h2.authedPost("/api/query", daveAsAdmin.token, {
666
+ type: SessionQueries.list,
667
+ payload: {},
668
+ });
669
+ expect(listRes.status).toBe(200);
670
+ const listBody = (await listRes.json()) as { data: Array<{ id: string }> };
671
+ expect(listBody.data.map((r) => r.id)).not.toContain(carolAsAdmin.sid);
672
+ });
462
673
  });
463
674
 
464
675
  // Defense-in-depth: the sessionChecker refuses a live sid once the user it
@@ -538,3 +749,44 @@ describe("sessions feature — locked accounts blocked on a live session", () =>
538
749
  expect(res.status).toBe(200);
539
750
  });
540
751
  });
752
+
753
+ describe("sessions with active KMS (#820): ip/userAgent are userOwned PII", () => {
754
+ test("sessionCreator stores ciphertext at rest, mine returns plaintext", async () => {
755
+ configurePiiSubjectKms(new InMemoryKmsAdapter());
756
+ configureBlindIndexKey(Buffer.alloc(32, 7).toString("base64"));
757
+ try {
758
+ const { userId } = await h.seedUser("kms-session@example.com", "pw-long-enough");
759
+ const cbs = createSessionCallbacks({ db: stack.db });
760
+ const sid = await cbs.sessionCreator(
761
+ { id: userId, tenantId: TENANT, roles: ["User"] },
762
+ { ip: "203.0.113.7", userAgent: "TestBrowser/1.0" },
763
+ );
764
+
765
+ const stored = await selectMany<{ ip: string | null; userAgent: string | null }>(
766
+ stack.db,
767
+ userSessionTable,
768
+ { id: sid },
769
+ );
770
+ expect(isPiiCiphertext(stored[0]?.ip)).toBe(true);
771
+ expect(isPiiCiphertext(stored[0]?.userAgent)).toBe(true);
772
+
773
+ // Login (bidx-Lookup auf verschluesselter user.email) + mine ueber HTTP:
774
+ // die manuell erzeugte Session kommt mit Klartext-ip/userAgent zurueck.
775
+ const login = await h.login("kms-session@example.com", "pw-long-enough");
776
+ const res = await h.authedPost("/api/query", login.token, {
777
+ type: SessionQueries.mine,
778
+ payload: {},
779
+ });
780
+ expect(res.status).toBe(200);
781
+ const body = (await res.json()) as {
782
+ data: Array<{ id: string; ip: string | null; userAgent: string | null }>;
783
+ };
784
+ const manual = body.data.find((s) => s.id === sid);
785
+ expect(manual?.ip).toBe("203.0.113.7");
786
+ expect(manual?.userAgent).toBe("TestBrowser/1.0");
787
+ } finally {
788
+ resetPiiSubjectKmsForTests();
789
+ resetBlindIndexKeyForTests();
790
+ }
791
+ });
792
+ });
@@ -9,12 +9,14 @@
9
9
  // const res = await h.authedPost("/api/query", token, { type, payload });
10
10
 
11
11
  import { expect } from "bun:test";
12
+ import type { SessionCreator } from "@cosmicdrift/kumiko-framework/api";
12
13
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
13
14
  import { type TestStack, TestUsers } from "@cosmicdrift/kumiko-framework/stack";
14
15
  import * as jose from "jose";
15
- import { hashPassword } from "../../auth-email-password/password-hashing";
16
+ import { hashPassword } from "../../shared";
16
17
  import { seedTenantMembership } from "../../tenant/seeding";
17
18
  import { UserHandlers } from "../../user";
19
+ import { withMintedSession } from "../testing";
18
20
 
19
21
  export type LoginResult = {
20
22
  readonly token: string;
@@ -25,7 +27,15 @@ export type LoginResult = {
25
27
  // `ReturnType<typeof makeSessionHelpers>` — no separate type export to
26
28
  // keep in sync with the implementation. Params are typed inline on each
27
29
  // method so the inference is sharp.
28
- export function makeSessionHelpers(stack: TestStack, tenantId: TenantId) {
30
+ // `sessionCreator` is only needed when the suite wires a real sessionChecker
31
+ // (auth-middleware then rejects the sidless systemAdmin bootstrap actor
32
+ // used by seedUser). Suites without a sessionChecker (e.g. PAT tests) can
33
+ // omit it.
34
+ export function makeSessionHelpers(
35
+ stack: TestStack,
36
+ tenantId: TenantId,
37
+ sessionCreator?: SessionCreator,
38
+ ) {
29
39
  return {
30
40
  async seedUser(
31
41
  email: string,
@@ -33,10 +43,13 @@ export function makeSessionHelpers(stack: TestStack, tenantId: TenantId) {
33
43
  opts?: { roles?: readonly string[] },
34
44
  ): Promise<{ userId: string }> {
35
45
  const hash = await hashPassword(password);
46
+ const actor = sessionCreator
47
+ ? await withMintedSession(sessionCreator, TestUsers.systemAdmin)
48
+ : TestUsers.systemAdmin;
36
49
  const created = await stack.http.writeOk<{ id: string }>(
37
50
  UserHandlers.create,
38
51
  { email, passwordHash: hash, displayName: email.split("@")[0] ?? "u" },
39
- TestUsers.systemAdmin,
52
+ actor,
40
53
  );
41
54
  await seedTenantMembership(stack.db, {
42
55
  userId: created.id,
@@ -17,6 +17,8 @@ export const SessionQueries = {
17
17
  // Admin-scoped: all sessions in the caller's tenant (live + revoked).
18
18
  // Tenant isolation comes from ctx.db; access-gate is admin-or-higher.
19
19
  list: "sessions:query:user-session:list",
20
+ // Admin-scoped single-session inspector, backs the session-detail screen.
21
+ detail: "sessions:query:user-session:detail",
20
22
  } as const;
21
23
 
22
24
  export const SessionErrors = {
@@ -45,3 +47,6 @@ export const DEFAULT_SESSION_CACHE_TTL_MS = 60_000;
45
47
  // longer so refresh can rotate the token without requiring a new password).
46
48
  // MVP ships a single window; per-app overrides can come later.
47
49
  export const DEFAULT_SESSION_EXPIRY_MS = 30 * 24 * 60 * 60 * 1000;
50
+
51
+ export const SESSION_LIST_SCREEN_ID = "session-list" as const;
52
+ export const SESSION_DETAIL_SCREEN_ID = "session-detail" as const;
@@ -7,9 +7,9 @@ export async function deleteStaleSessionsBatch(
7
7
  batchSize: number,
8
8
  ): Promise<number> {
9
9
  const rows = (await asRawClient(db).unsafe(
10
- `DELETE FROM "read_user_sessions"
10
+ `DELETE FROM "store_user_sessions"
11
11
  WHERE "id" IN (
12
- SELECT "id" FROM "read_user_sessions"
12
+ SELECT "id" FROM "store_user_sessions"
13
13
  WHERE "expires_at" < now() - ($1::int * interval '1 day')
14
14
  OR "revoked_at" < now() - ($1::int * interval '1 day')
15
15
  LIMIT $2