@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,4 @@
1
- import { extractTableName } from "@cosmicdrift/kumiko-framework/db";
1
+ import { type DbRunner, extractTableName } from "@cosmicdrift/kumiko-framework/db";
2
2
  import {
3
3
  createJsonbField,
4
4
  type FeatureRegistrar,
@@ -6,7 +6,8 @@ import {
6
6
  type JsonbFieldDef,
7
7
  type TenantId,
8
8
  } from "@cosmicdrift/kumiko-framework/engine";
9
- import { CUSTOM_FIELDS_EXTENSION } from "./constants";
9
+ import type { StoredEvent } from "@cosmicdrift/kumiko-framework/event-store";
10
+ import { CUSTOM_FIELDS_EXTENSION, FIELD_DEFINITION_AGGREGATE_TYPE } from "./constants";
10
11
  import {
11
12
  clearCustomFieldKey,
12
13
  removeCustomFieldKeyForTenant,
@@ -36,8 +37,9 @@ export function customFieldsField(): JsonbFieldDef {
36
37
 
37
38
  // Vollständige integration der custom-fields-Bundle für eine spezifische
38
39
  // host-entity. Eine einzige Aufruf-Stelle pro consumer registriert ALLE
39
- // wiring-Aspekte: extension-tracking, MSP für value-projection, postQuery-
40
- // hook für API-flatten, search-payload-extension für indexable customFields.
40
+ // wiring-Aspekte: extension-tracking, MSP für value-projection, rebuild-
41
+ // replay via extendEntityProjection, postQuery-hook für API-flatten,
42
+ // search-payload-extension für indexable customFields.
41
43
  //
42
44
  // Consumer-side:
43
45
  //
@@ -61,15 +63,20 @@ export function customFieldsField(): JsonbFieldDef {
61
63
  // emittiert haben. Updated entityTable.customFields jsonb über
62
64
  // jsonb_set / jsonb-key-removal.
63
65
  //
64
- // 3. r.entityHook("postQuery", entity, flatten-fn) bei JEDEM Read auf
66
+ // 3. r.extendEntityProjection hängt dieselben apply-Handler in die
67
+ // implicit projection der host-entity, damit rebuildProjection die
68
+ // customField-Events mit-replayt. Ohne das fällt customFields bei
69
+ // jedem Entity-Rebuild (Schema-Migration!) auf `{}` zurück (#759).
70
+ //
71
+ // 4. r.entityHook("postQuery", entity, flatten-fn) — bei JEDEM Read auf
65
72
  // diese entity wird `row.customFields` jsonb auf root-level expanded
66
73
  // damit die API-response wie Stammfelder aussieht.
67
74
  //
68
- // 4. r.searchPayloadExtension(entity, contributor) — searchable
75
+ // 5. r.searchPayloadExtension(entity, contributor) — searchable
69
76
  // customFields-keys werden flach ins Meilisearch-Index-Doc beigetragen
70
77
  // (F3-wiring).
71
78
  //
72
- // 5. fieldDefinition.deleted-Event-Handler im selben MSP — bei delete
79
+ // 6. fieldDefinition.deleted-Event-Handler im selben MSP — bei delete
73
80
  // einer fieldDefinition werden orphan values aus allen entity-rows
74
81
  // entfernt (key-removal pro fieldKey).
75
82
  export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
@@ -87,86 +94,111 @@ export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
87
94
  const clearedEventType = customFieldsFeature.exports.clearedEvent.name;
88
95
  const fieldDefDeletedType = customFieldsFeature.exports.fieldDefinitionDeletedEvent.name;
89
96
 
90
- r.multiStreamProjection({
91
- name: `custom-fields-${entityName}-projection`,
92
- apply: {
93
- [setEventType]: async (event, tx) => {
94
- // skip: MSP feuert für ALLE aggregate-types die customField.set
95
- // emittieren wir wollen nur die unserer wired host-entity.
96
- // Andere consumers haben eigene MSPs für ihre Entities.
97
- if (event.aggregateType !== entityName) return;
98
- const payload = event.payload as CustomFieldSetPayload; // @cast-boundary engine-payload
97
+ // Geteilte apply-Bodies: die MSP liefert sie live (async cursor), die
98
+ // entity-projection-extension replayt sie in rebuildProjection — gleiche
99
+ // SQL-Helper, beide idempotent. 2-Param-Signatur ist die gemeinsame
100
+ // Teilmenge von MultiStreamApplyFn und SingleStreamApplyFn.
101
+ const applyCustomFieldSet = async (event: StoredEvent, tx: DbRunner): Promise<void> => {
102
+ // skip: feuert für ALLE aggregate-types die customField.set emittieren
103
+ // wir wollen nur die unserer wired host-entity. Andere consumers haben
104
+ // eigene Wirings für ihre Entities.
105
+ if (event.aggregateType !== entityName) return;
106
+ const payload = event.payload as CustomFieldSetPayload; // @cast-boundary engine-payload
107
+
108
+ // skip: a value-less customField.set is an anomaly since #972 (every set
109
+ // carries its value; the sensitive self-projection path was removed) —
110
+ // warn and leave the row untouched instead of binding undefined.
111
+ if (payload.value === undefined) {
112
+ // biome-ignore lint/suspicious/noConsole: boot-adjacent correctness canary, no logger available in an apply function
113
+ console.warn(
114
+ `[custom-fields] customField.set for "${payload.fieldKey}" on ${event.aggregateType}/${event.aggregateId} has no value — skipping (value-less sets should not exist since #972).`,
115
+ );
116
+ // skip: warned above — leave the row untouched instead of binding undefined
117
+ return;
118
+ }
119
+
120
+ // jsonb_set: setze key auf value. Wenn key noch nicht existiert →
121
+ // wird angelegt (create_missing=true ist default). value muss als
122
+ // jsonb-literal kommen.
123
+ const tableName = extractTableName(entityTable, "custom-fields/wire-for-entity");
124
+ await setCustomFieldValue(
125
+ tx,
126
+ tableName,
127
+ payload.fieldKey,
128
+ payload.value,
129
+ event.aggregateId,
130
+ event.tenantId,
131
+ );
132
+ };
133
+
134
+ const applyCustomFieldCleared = async (event: StoredEvent, tx: DbRunner): Promise<void> => {
135
+ // skip: feuert für alle aggregate-types — nur unsere host-entity
136
+ // verarbeiten.
137
+ if (event.aggregateType !== entityName) return;
138
+ const payload = event.payload as CustomFieldClearedPayload; // @cast-boundary engine-payload
99
139
 
100
- // skip: sensitive fields self-project in the write handler (see
101
- // set-custom-field) and persist a value-less event so PII never enters
102
- // the log. Such events arrive here with value === undefined — skipping
103
- // is correct both live (the handler already wrote the row) and on replay
104
- // (the value is intentionally gone, the accepted rebuild-loss).
105
- if (payload.value === undefined) return;
140
+ // jsonb minus operator (`-`) entfernt key aus jsonb-object.
141
+ const tableName = extractTableName(entityTable, "custom-fields/wire-for-entity");
142
+ await clearCustomFieldKey(tx, tableName, payload.fieldKey, event.aggregateId, event.tenantId);
143
+ };
106
144
 
107
- // jsonb_set: setze key auf value. Wenn key noch nicht existiert →
108
- // wird angelegt (create_missing=true ist default). value muss als
109
- // jsonb-literal kommen.
110
- const tableName = extractTableName(entityTable, "custom-fields/wire-for-entity");
111
- await setCustomFieldValue(
112
- tx,
113
- tableName,
114
- payload.fieldKey,
115
- payload.value,
116
- event.aggregateId,
117
- event.tenantId,
118
- );
119
- },
120
- [clearedEventType]: async (event, tx) => {
121
- // skip: MSP feuert für alle aggregate-types — nur unsere host-entity
122
- // verarbeiten.
123
- if (event.aggregateType !== entityName) return;
124
- const payload = event.payload as CustomFieldClearedPayload; // @cast-boundary engine-payload
145
+ const applyFieldDefinitionDeleted = async (event: StoredEvent, tx: DbRunner): Promise<void> => {
146
+ // fieldDefinition.deleted fires nur einmal pro fieldDef-delete
147
+ // (NICHT per-entity). Wir entfernen den key aus ALLEN rows der host-
148
+ // entity falls die deleted-fieldDef für diese entity galt.
149
+ const payload = event.payload as {
150
+ entityName: string;
151
+ fieldKey: string;
152
+ tenantId?: TenantId;
153
+ }; // @cast-boundary engine-payload
154
+ // skip: fieldDefinition.deleted feuert für ALLE fieldDefs cross-entity;
155
+ // nur wenn die deleted-fieldDef diese host-entity betraf, cleanen wir
156
+ // ihre Rows.
157
+ if (payload.entityName !== entityName) return;
125
158
 
126
- // jsonb minus operator (`-`) entfernt key aus jsonb-object.
127
- const tableName = extractTableName(entityTable, "custom-fields/wire-for-entity");
128
- await clearCustomFieldKey(
129
- tx,
130
- tableName,
131
- payload.fieldKey,
132
- event.aggregateId,
133
- event.tenantId,
134
- );
135
- },
136
- [fieldDefDeletedType]: async (event, tx) => {
137
- // fieldDefinition.deleted fires nur einmal pro fieldDef-delete
138
- // (NICHT per-entity). Wir entfernen den key aus ALLEN rows der host-
139
- // entity falls die deleted-fieldDef für diese entity galt.
140
- const payload = event.payload as {
141
- entityName: string;
142
- fieldKey: string;
143
- tenantId?: TenantId;
144
- }; // @cast-boundary engine-payload
145
- // skip: fieldDefinition.deleted feuert für ALLE fieldDefs cross-entity;
146
- // nur wenn die deleted-fieldDef diese host-entity betraf, cleanen wir
147
- // ihre Rows.
148
- if (payload.entityName !== entityName) return;
159
+ const tableName = extractTableName(entityTable, "custom-fields/wire-for-entity");
160
+ // Scope cleanup to the deleted definition's owning tenant. System-scope
161
+ // definitions apply to every tenant → cascade across all rows; tenant-
162
+ // scope deletions must only touch that tenant's rows, else deleting one
163
+ // tenant's field strips the same kebab key from every tenant (data loss).
164
+ // Fallback to the event's stream tenantId for events appended before the
165
+ // payload carried tenantId.
166
+ const defTenantId = payload.tenantId ?? event.tenantId;
167
+ if (isSystemTenant(defTenantId)) {
168
+ await removeCustomFieldKeyFromAllTenants(tx, tableName, payload.fieldKey);
169
+ } else {
170
+ await removeCustomFieldKeyForTenant(tx, tableName, payload.fieldKey, defTenantId);
171
+ }
172
+ };
173
+
174
+ r.multiStreamProjection({
175
+ name: `custom-fields-${entityName}-projection`,
176
+ apply: {
177
+ [setEventType]: applyCustomFieldSet,
178
+ [clearedEventType]: applyCustomFieldCleared,
179
+ [fieldDefDeletedType]: applyFieldDefinitionDeleted,
180
+ },
181
+ });
149
182
 
150
- const tableName = extractTableName(entityTable, "custom-fields/wire-for-entity");
151
- // Scope cleanup to the deleted definition's owning tenant. System-scope
152
- // definitions apply to every tenant cascade across all rows; tenant-
153
- // scope deletions must only touch that tenant's rows, else deleting one
154
- // tenant's field strips the same kebab key from every tenant (data loss).
155
- // Fallback to the event's stream tenantId for events appended before the
156
- // payload carried tenantId.
157
- const defTenantId = payload.tenantId ?? event.tenantId;
158
- if (isSystemTenant(defTenantId)) {
159
- await removeCustomFieldKeyFromAllTenants(tx, tableName, payload.fieldKey);
160
- } else {
161
- await removeCustomFieldKeyForTenant(tx, tableName, payload.fieldKey, defTenantId);
162
- }
163
- },
183
+ // Rebuild-wiring (#759): ohne das replayt rebuildProjection nur die
184
+ // entity-lifecycle-Events und resettet jede customFields-jsonb auf ihr
185
+ // Default `{}` die Werte existieren ausschließlich als customField.set/
186
+ // .cleared-Events. set/cleared reiten auf dem HOST-entity-Stream
187
+ // (aggregateType = entityName, von source schon abgedeckt);
188
+ // fieldDefinition.deleted lebt auf dem "field-definition"-Stream und
189
+ // kommt deshalb als extra source dazu.
190
+ r.extendEntityProjection(entityName, {
191
+ sources: [FIELD_DEFINITION_AGGREGATE_TYPE],
192
+ apply: {
193
+ [setEventType]: applyCustomFieldSet,
194
+ [clearedEventType]: applyCustomFieldCleared,
195
+ [fieldDefDeletedType]: applyFieldDefinitionDeleted,
164
196
  },
165
197
  });
166
198
 
167
199
  // postQuery-hook: flatten row.customFields jsonb auf root-level der
168
200
  // API-response. Spec-Promise Z.4 "indistinguishable von Stammfeldern".
169
- r.entityHook("postQuery", entityName, async ({ rows }) => ({
201
+ r.hook("postQuery", { allOf: entityName }, async ({ rows }) => ({
170
202
  rows: rows.map((row) => {
171
203
  const customFields = row["customFields"];
172
204
  if (customFields && typeof customFields === "object" && !Array.isArray(customFields)) {
@@ -1,18 +1,9 @@
1
1
  // T1.5c — user-data-rights wiring for custom-fields.
2
2
 
3
3
  import { extractTableName } from "@cosmicdrift/kumiko-framework/db";
4
- import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
5
- import {
6
- EXT_USER_DATA,
7
- EXT_USER_DATA_ORDER,
8
- type FeatureRegistrar,
9
- } from "@cosmicdrift/kumiko-framework/engine";
10
- import {
11
- selectCustomFieldsHostRows,
12
- selectFieldDefinitionsForEntity,
13
- stripSensitiveCustomFieldKeys,
14
- } from "./db/queries/user-data-rights";
15
- import { isFieldDefinitionRow, parseSerializedField } from "./lib/parse-serialized-field";
4
+ import type { UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
5
+ import { EXT_USER_DATA, type FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
6
+ import { selectCustomFieldsHostRows } from "./db/queries/user-data-rights";
16
7
 
17
8
  export interface WireCustomFieldsUserDataRightsOptions {
18
9
  readonly entityName: string;
@@ -36,14 +27,8 @@ function asCustomFieldsHostRow(value: unknown): CustomFieldsHostRow | null {
36
27
  return { id: value.id, customFields: Object.fromEntries(Object.entries(cf)) };
37
28
  }
38
29
 
39
- // The anonymize strip filters rows by `WHERE userIdColumn = userId`. A host
40
- // anonymize hook on the SAME entity that nulls that column (e.g. inserted_by_id
41
- // = NULL) would, if it ran first, leave the strip matching 0 rows → sensitive
42
- // jsonb PII silently retained (DSGVO Art. 17 violation). A negative order makes
43
- // runForgetCleanup run this strip before any default-order (0) owner-nulling
44
- // hook, independent of feature registration order.
45
- const ORDER_REDACT_BEFORE_OWNER_MUTATION = EXT_USER_DATA_ORDER.REDACT_BEFORE_OWNER;
46
-
30
+ // Export-only wiring: custom fields hold supplemental business data, not PII
31
+ // (#972) there is nothing to redact on user-forget, only Art. 20 export.
47
32
  export function wireCustomFieldsUserDataRightsFor<TReg extends FeatureRegistrar<string>>(
48
33
  r: TReg,
49
34
  opts: WireCustomFieldsUserDataRightsOptions,
@@ -71,42 +56,8 @@ export function wireCustomFieldsUserDataRightsFor<TReg extends FeatureRegistrar<
71
56
  return { entity: `${opts.entityName}.customFields`, rows: snippetRows };
72
57
  };
73
58
 
74
- const deleteHook: UserDataDeleteHook = async (ctx, strategy) => {
75
- // skip: delete strategy removes rows wholesale — custom-field redaction N/A.
76
- if (strategy === "delete") return;
77
- const sensitiveKeys = await loadSensitiveFieldKeys(ctx.db, ctx.tenantId, opts.entityName);
78
- // skip: no sensitive custom fields configured for this entity.
79
- if (sensitiveKeys.length === 0) return;
80
-
81
- await stripSensitiveCustomFieldKeys(
82
- ctx.db,
83
- tableName,
84
- opts.userIdColumn,
85
- sensitiveKeys,
86
- ctx.userId,
87
- ctx.tenantId,
88
- );
89
- };
90
-
91
59
  // biome-ignore lint/correctness/useHookAtTopLevel: r.useExtension is a registrar API, not a React hook.
92
60
  r.useExtension(EXT_USER_DATA, opts.entityName, {
93
61
  export: exportHook,
94
- delete: deleteHook,
95
- order: ORDER_REDACT_BEFORE_OWNER_MUTATION,
96
62
  });
97
63
  }
98
-
99
- async function loadSensitiveFieldKeys(
100
- db: Parameters<UserDataExportHook>[0]["db"],
101
- tenantId: string,
102
- entityName: string,
103
- ): Promise<string[]> {
104
- const rows = await selectFieldDefinitionsForEntity(db, entityName, tenantId);
105
- const keys: string[] = [];
106
- for (const raw of rows) {
107
- if (!isFieldDefinitionRow(raw)) continue;
108
- const parsed = parseSerializedField(raw.serialized_field);
109
- if (parsed?.sensitive === true) keys.push(raw.field_key);
110
- }
111
- return keys;
112
- }
@@ -1,13 +1,16 @@
1
- import { describe, expect, spyOn, test } from "bun:test";
1
+ import { afterEach, describe, expect, spyOn, test } from "bun:test";
2
2
  import { parseRetentionOverrideOrNull } from "../_internal/parse-override";
3
3
 
4
- // parseRetentionOverrideOrNull is the read boundary for a tenant's stored
5
- // data-retention override (DSGVO-relevant policy in a config column). It must
6
- // never let a corrupt or schema-violating value reach the retention decision:
7
- // invalid JSON and schema drift both collapse to null (the resolver then falls
8
- // back to preset/entity defaults) AND surface one operator warning. The schema
9
- // itself is covered by override-schema.test.ts — this pins the parser's
10
- // defensive wrapping: empty guard, no-throw on corruption, drop-not-leak on drift.
4
+ // A mid-test assertion throw skips the trailing `warn.mockRestore()` in that
5
+ // test, leaving the spy live for every test after it (551/1) this backstop
6
+ // restores unconditionally regardless of how the test exited.
7
+ afterEach(() => {
8
+ const warn = console.warn as unknown as { mockRestore?: () => void };
9
+ warn.mockRestore?.();
10
+ });
11
+
12
+ // DSGVO invariant: corrupt or schema-drifted overrides collapse to null +
13
+ // warn, never leak through (schema itself is covered by override-schema.test.ts).
11
14
 
12
15
  const parse = (raw: string | null) => parseRetentionOverrideOrNull(raw, "tenant-1", "test");
13
16
 
@@ -31,26 +34,24 @@ describe("parseRetentionOverrideOrNull", () => {
31
34
  });
32
35
 
33
36
  test("corrupt JSON returns null without throwing", () => {
34
- const warn = spyOn(console, "warn").mockImplementation(() => {});
37
+ spyOn(console, "warn").mockImplementation(() => {});
35
38
  expect(() => parse("{not json")).not.toThrow();
36
39
  expect(parse("{not json")).toBeNull();
37
- warn.mockRestore();
38
40
  });
39
41
 
40
42
  test("JSON that parses but violates the schema is dropped to null — never leaked through", () => {
41
- const warn = spyOn(console, "warn").mockImplementation(() => {});
43
+ spyOn(console, "warn").mockImplementation(() => {});
42
44
  expect(parse('{"strategy":"delete"}')).toBeNull(); // enum drift
43
45
  expect(parse('{"keepFor":"30days"}')).toBeNull(); // keepFor format drift
44
46
  expect(parse('{"keepFor":42}')).toBeNull(); // wrong type
45
47
  expect(parse('{"unknownKey":1}')).toBeNull(); // strict() rejects extra keys
46
- warn.mockRestore();
47
48
  });
48
49
 
49
50
  test("each dropped value surfaces exactly one operator warning", () => {
50
51
  const warn = spyOn(console, "warn").mockImplementation(() => {});
51
52
  parse("{not json");
53
+ expect(warn).toHaveBeenCalledTimes(1);
52
54
  parse('{"strategy":"delete"}');
53
55
  expect(warn).toHaveBeenCalledTimes(2);
54
- warn.mockRestore();
55
56
  });
56
57
  });
@@ -55,10 +55,9 @@ const staleEntity = createEntity({
55
55
  });
56
56
 
57
57
  // blockDelete = Aufbewahrungspflicht (HGB/DSGVO-Presets: invoice/booking/
58
- // contract). Der Cleanup-Job muss diese Rows IGNORIEREN — nur user-forget
59
- // darf sie anonymisieren. Ein Regress im switch-Default oder im blockDelete-
60
- // Case, der sie trotzdem löscht, wäre schwerwiegender als ein verpasstes
61
- // hardDelete (Aufbewahrungspflicht-Verletzung statt nur "zu spät aufgeräumt").
58
+ // contract). Während keepFor muss der Cleanup diese Rows IGNORIEREN (löschen
59
+ // wäre eine Aufbewahrungspflicht-Verletzung); NACH Ablauf laufen die
60
+ // anonymize-Feld-Funktionen Row bleibt, Personen-Bezug raus (fields.ts).
62
61
  const retainedEntity = createEntity({
63
62
  table: "read_c7_retained",
64
63
  fields: {
@@ -67,12 +66,33 @@ const retainedEntity = createEntity({
67
66
  retention: { keepFor: "30d", strategy: "blockDelete" },
68
67
  });
69
68
 
69
+ // anonymize-Strategie: label wird nach Ablauf ueberschrieben, note traegt
70
+ // keine anonymize-Funktion und muss unangetastet bleiben (Geschaeftsdaten).
71
+ const anonEntity = createEntity({
72
+ table: "read_c7_anon",
73
+ fields: {
74
+ label: createTextField({ required: true, anonymize: () => "[ANONYMIZED]" }),
75
+ note: createTextField({ allowPlaintext: "is-business-data" }),
76
+ },
77
+ retention: { keepFor: "30d", strategy: "anonymize" },
78
+ });
79
+
80
+ // anonymize-Strategie OHNE anonymize-Felder — der Runner kann nichts tun und
81
+ // muss das als skip melden statt still nichts zu tun.
82
+ const bareEntity = createEntity({
83
+ table: "read_c7_bare",
84
+ fields: { label: createTextField({ required: true, allowPlaintext: "is-business-data" }) },
85
+ retention: { keepFor: "30d", strategy: "anonymize" },
86
+ });
87
+
70
88
  const c7Feature = defineFeature("c7-retention-fixtures", (r) => {
71
89
  r.entity("c7-widget", widgetEntity);
72
90
  r.entity("c7-gadget", gadgetEntity);
73
91
  r.entity("c7-plain", plainEntity);
74
92
  r.entity("c7-stale", staleEntity);
75
93
  r.entity("c7-retained", retainedEntity);
94
+ r.entity("c7-anon", anonEntity);
95
+ r.entity("c7-bare", bareEntity);
76
96
  });
77
97
 
78
98
  const noopLogger: JobContext["log"] = {
@@ -102,6 +122,8 @@ beforeAll(async () => {
102
122
  plainEntity,
103
123
  staleEntity,
104
124
  retainedEntity,
125
+ anonEntity,
126
+ bareEntity,
105
127
  ]) {
106
128
  await unsafeCreateEntityTable(stack.db, e);
107
129
  }
@@ -144,6 +166,8 @@ beforeEach(async () => {
144
166
  "read_c7_plain",
145
167
  "read_c7_stale",
146
168
  "read_c7_retained",
169
+ "read_c7_anon",
170
+ "read_c7_bare",
147
171
  ]) {
148
172
  await asRawClient(stack.db).unsafe(`DELETE FROM ${t}`);
149
173
  }
@@ -244,8 +268,9 @@ describe("runRetentionCleanup :: real postgres", () => {
244
268
  expect(await labels("read_c7_widget", T1)).toEqual(["fresh-via-handler"]);
245
269
  });
246
270
 
247
- test("blockDelete: past-cutoff rows are never touched by the cleanup job", async () => {
248
- await seed("read_c7_retained", T1, "should-survive", pastIso);
271
+ test("blockDelete: within keepFor untouched, after expiry anonymized row NEVER deleted", async () => {
272
+ await seed("read_c7_retained", T1, "in-hold", withinIso);
273
+ await seed("read_c7_retained", T1, "hold-expired", pastIso);
249
274
 
250
275
  const result = await runRetentionCleanup({
251
276
  db: stack.db,
@@ -255,9 +280,121 @@ describe("runRetentionCleanup :: real postgres", () => {
255
280
  now,
256
281
  });
257
282
 
258
- expect(await labels("read_c7_retained", T1)).toEqual(["should-survive"]);
283
+ // Beide Rows physisch da — blockDelete darf NIE loeschen.
284
+ expect((await labels("read_c7_retained", T1)).sort()).toEqual(["[ANONYMIZED]", "in-hold"]);
259
285
  expect(result.hardDeleted).toBe(0);
260
286
  expect(result.softDeleted).toBe(0);
261
- expect(result.anonymizeDeferred).not.toContain("c7-retained");
287
+ expect(result.anonymized).toBe(1);
288
+ });
289
+
290
+ test("anonymize: expired row gets field functions applied, other fields + fresh rows untouched", async () => {
291
+ await asRawClient(stack.db).unsafe(
292
+ `INSERT INTO read_c7_anon (tenant_id, label, note, inserted_at) VALUES ($1, 'old-pii', 'business-note', $2::timestamptz)`,
293
+ [T1, pastIso],
294
+ );
295
+ await asRawClient(stack.db).unsafe(
296
+ `INSERT INTO read_c7_anon (tenant_id, label, note, inserted_at) VALUES ($1, 'fresh-pii', 'fresh-note', $2::timestamptz)`,
297
+ [T1, withinIso],
298
+ );
299
+
300
+ const result = await runRetentionCleanup({
301
+ db: stack.db,
302
+ registry: stack.registry,
303
+ tenantId: T1,
304
+ tenantPreset: null,
305
+ now,
306
+ });
307
+
308
+ expect(result.anonymized).toBe(1);
309
+ const rows = (await asRawClient(stack.db).unsafe(
310
+ `SELECT label, note FROM read_c7_anon WHERE tenant_id = $1 ORDER BY note`,
311
+ [T1],
312
+ )) as { label: string; note: string }[];
313
+ expect(rows).toEqual([
314
+ { label: "[ANONYMIZED]", note: "business-note" }, // PII raus, Geschaeftsdaten bleiben
315
+ { label: "fresh-pii", note: "fresh-note" }, // innerhalb keepFor: unangetastet
316
+ ]);
317
+ });
318
+
319
+ test("anonymize is idempotent: second run appends ZERO events (no daily event spam)", async () => {
320
+ await asRawClient(stack.db).unsafe(
321
+ `INSERT INTO read_c7_anon (tenant_id, label, note, inserted_at) VALUES ($1, 'old-pii', 'n', $2::timestamptz)`,
322
+ [T1, pastIso],
323
+ );
324
+
325
+ const first = await runRetentionCleanup({
326
+ db: stack.db,
327
+ registry: stack.registry,
328
+ tenantId: T1,
329
+ tenantPreset: null,
330
+ now,
331
+ });
332
+ expect(first.anonymized).toBe(1);
333
+
334
+ const eventsAfterFirst = (await asRawClient(stack.db).unsafe(
335
+ `SELECT count(*)::int AS n FROM kumiko_events`,
336
+ )) as { n: number }[];
337
+
338
+ const second = await runRetentionCleanup({
339
+ db: stack.db,
340
+ registry: stack.registry,
341
+ tenantId: T1,
342
+ tenantPreset: null,
343
+ now,
344
+ });
345
+ expect(second.anonymized).toBe(0);
346
+
347
+ const eventsAfterSecond = (await asRawClient(stack.db).unsafe(
348
+ `SELECT count(*)::int AS n FROM kumiko_events`,
349
+ )) as { n: number }[];
350
+ expect(eventsAfterSecond[0]?.n).toBe(eventsAfterFirst[0]?.n);
351
+ });
352
+
353
+ test("anonymize converges under batchLimit=1: cursor pages past already-anonymized rows", async () => {
354
+ for (const label of ["a-pii", "b-pii", "c-pii"]) {
355
+ await asRawClient(stack.db).unsafe(
356
+ `INSERT INTO read_c7_anon (tenant_id, label, note, inserted_at) VALUES ($1, $2, $3, $4::timestamptz)`,
357
+ [T1, label, `note-${label}`, pastIso],
358
+ );
359
+ }
360
+
361
+ const run = () =>
362
+ runRetentionCleanup({
363
+ db: stack.db,
364
+ registry: stack.registry,
365
+ tenantId: T1,
366
+ tenantPreset: null,
367
+ now,
368
+ batchLimit: 1,
369
+ });
370
+
371
+ expect((await run()).anonymized).toBe(1);
372
+ expect((await run()).anonymized).toBe(1); // Cursor muss ueber die erledigte Row hinweg
373
+ expect((await run()).anonymized).toBe(1);
374
+ expect((await run()).anonymized).toBe(0); // konvergiert
375
+
376
+ const rows = (await asRawClient(stack.db).unsafe(
377
+ `SELECT label FROM read_c7_anon WHERE tenant_id = $1`,
378
+ [T1],
379
+ )) as { label: string }[];
380
+ expect(rows.map((r) => r.label)).toEqual(["[ANONYMIZED]", "[ANONYMIZED]", "[ANONYMIZED]"]);
381
+ });
382
+
383
+ test("anonymize strategy without anonymize fields → skipped with reason, rows untouched", async () => {
384
+ await seed("read_c7_bare", T1, "survives", pastIso);
385
+
386
+ const result = await runRetentionCleanup({
387
+ db: stack.db,
388
+ registry: stack.registry,
389
+ tenantId: T1,
390
+ tenantPreset: null,
391
+ now,
392
+ });
393
+
394
+ expect(await labels("read_c7_bare", T1)).toEqual(["survives"]);
395
+ expect(result.skipped).toContainEqual({
396
+ entityName: "c7-bare",
397
+ reason: "missing_anonymize_fields",
398
+ });
262
399
  });
263
400
  });
@@ -104,10 +104,10 @@ export function createDataRetentionFeature(): FeatureDefinition {
104
104
  tenantPreset,
105
105
  now: T.Now.instant(),
106
106
  });
107
- if (result.anonymizeDeferred.length > 0 || result.skipped.length > 0) {
108
- // biome-ignore lint/suspicious/noConsole: operator-visibility for deferred/skipped entities
107
+ if (result.skipped.length > 0) {
108
+ // biome-ignore lint/suspicious/noConsole: operator-visibility for skipped entities
109
109
  console.warn(
110
- `[data-retention:retention-cleanup] tenant=${tenantId} anonymizeDeferred=${result.anonymizeDeferred.join(",") || "-"} skipped=${result.skipped.map((s) => `${s.entityName}:${s.reason}`).join(",") || "-"}`,
110
+ `[data-retention:retention-cleanup] tenant=${tenantId} skipped=${result.skipped.map((s) => `${s.entityName}:${s.reason}`).join(",")}`,
111
111
  );
112
112
  }
113
113
  },