@cosmicdrift/kumiko-bundled-features 0.307.0 → 0.309.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 (253) hide show
  1. package/package.json +9 -9
  2. package/src/__tests__/access-invalidation-consumer.integration.test.ts +107 -0
  3. package/src/__tests__/access-invalidation-stream.integration.test.ts +1 -1
  4. package/src/__tests__/credential-scoped-sse-revoke.integration.test.ts +296 -0
  5. package/src/agent-tools/__tests__/agent-doc-lint.test.ts +1 -1
  6. package/src/agent-tools/__tests__/agent-manifest.test.ts +1 -1
  7. package/src/agent-tools/__tests__/agent-optout.test.ts +1 -1
  8. package/src/agent-tools/__tests__/agent-tools.integration.test.ts +1 -1
  9. package/src/agent-tools/__tests__/feature.test.ts +1 -1
  10. package/src/agent-tools/__tests__/tool-catalog.test.ts +1 -1
  11. package/src/agent-tools/agent-manifest.ts +1 -1
  12. package/src/audit/__tests__/escape-hatch-audit.integration.test.ts +1 -1
  13. package/src/audit/escape-hatch-audit-sink.ts +1 -1
  14. package/src/audit/handlers/details.query.ts +1 -1
  15. package/src/audit/handlers/list.query.ts +1 -1
  16. package/src/auth-email-password/__tests__/query-as-member.integration.test.ts +1 -1
  17. package/src/auth-email-password/__tests__/seed-admin-guarded.integration.test.ts +31 -0
  18. package/src/auth-email-password/changes.json +6 -0
  19. package/src/auth-email-password/feature.ts +1 -1
  20. package/src/auth-email-password/handlers/change-password.write.ts +1 -1
  21. package/src/auth-email-password/handlers/confirm-account-unlock.write.ts +1 -1
  22. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +1 -1
  23. package/src/auth-email-password/handlers/invite-accept.write.ts +1 -1
  24. package/src/auth-email-password/handlers/invite-create.write.ts +1 -1
  25. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +1 -1
  26. package/src/auth-email-password/handlers/login.write.ts +1 -1
  27. package/src/auth-email-password/handlers/logout.write.ts +1 -1
  28. package/src/auth-email-password/handlers/reset-password.write.ts +1 -1
  29. package/src/auth-email-password/handlers/self-registration-status.query.ts +1 -1
  30. package/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
  31. package/src/auth-email-password/handlers/signup-request.write.ts +1 -1
  32. package/src/auth-email-password/handlers/token-request-handler.ts +1 -1
  33. package/src/auth-email-password/handlers/verify-email.write.ts +1 -1
  34. package/src/auth-email-password/password-policy.ts +1 -1
  35. package/src/auth-email-password/seeding.ts +16 -3
  36. package/src/auth-mfa/__tests__/session-scoped-sse-revoke.integration.test.ts +1 -1
  37. package/src/auth-mfa/handlers/disable.write.ts +1 -1
  38. package/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -1
  39. package/src/auth-mfa/handlers/enable-confirm.write.ts +1 -1
  40. package/src/auth-mfa/handlers/enable-start-preauth.write.ts +1 -1
  41. package/src/auth-mfa/handlers/enable-start.write.ts +1 -1
  42. package/src/auth-mfa/handlers/regenerate-recovery.write.ts +1 -1
  43. package/src/auth-mfa/handlers/status.query.ts +1 -1
  44. package/src/auth-mfa/handlers/verify.write.ts +1 -1
  45. package/src/billing-foundation/billing-info-query.ts +1 -1
  46. package/src/billing-foundation/events.ts +1 -1
  47. package/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -1
  48. package/src/billing-foundation/handlers/create-portal-session.write.ts +1 -1
  49. package/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  50. package/src/billing-foundation/handlers/process-event.write.ts +1 -1
  51. package/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  52. package/src/cap-counter/__tests__/cap-counter.integration.test.ts +1 -1
  53. package/src/cap-counter/__tests__/stock-cap-guard.integration.test.ts +1 -1
  54. package/src/cap-counter/__tests__/with-cap-enforcement.integration.test.ts +1 -1
  55. package/src/cap-counter/book-cap-usage.ts +1 -1
  56. package/src/cap-counter/handlers/get-counter.query.ts +1 -1
  57. package/src/cap-counter/handlers/increment-rolling.write.ts +1 -1
  58. package/src/cap-counter/handlers/increment.write.ts +1 -1
  59. package/src/cap-counter/handlers/mark-soft-warned.write.ts +1 -1
  60. package/src/cap-overview/handlers/caps-usage.query.ts +1 -1
  61. package/src/cap-overview/handlers/tenant-caps-list.query.ts +1 -1
  62. package/src/cap-overview/handlers/tenant-options.query.ts +1 -1
  63. package/src/channel-in-app/handlers/inbox.query.ts +1 -1
  64. package/src/channel-in-app/handlers/mark-all-read.write.ts +1 -1
  65. package/src/channel-in-app/handlers/mark-read.write.ts +1 -1
  66. package/src/channel-in-app/handlers/unread-count.query.ts +1 -1
  67. package/src/compliance-profiles/handlers/for-tenant.query.ts +1 -1
  68. package/src/compliance-profiles/handlers/list-profiles.query.ts +1 -1
  69. package/src/compliance-profiles/handlers/needs-profile.query.ts +1 -1
  70. package/src/compliance-profiles/handlers/set-profile.write.ts +1 -1
  71. package/src/compliance-profiles/handlers/sub-processors.query.ts +1 -1
  72. package/src/compliance-profiles-ops/handlers/tenants-missing-profile.query.ts +1 -1
  73. package/src/config/__tests__/backing-secrets.integration.test.ts +1 -1
  74. package/src/config/__tests__/config.integration.test.ts +1 -1
  75. package/src/config/__tests__/tz-resolution.integration.test.ts +1 -1
  76. package/src/config/handlers/cascade.query.ts +1 -1
  77. package/src/config/handlers/readiness.query.ts +1 -1
  78. package/src/config/handlers/reset.write.ts +1 -1
  79. package/src/config/handlers/schema.query.ts +1 -1
  80. package/src/config/handlers/set.write.ts +1 -1
  81. package/src/config/handlers/values.query.ts +1 -1
  82. package/src/crypto-shredding/handlers/forget-subject.write.ts +1 -1
  83. package/src/custom-fields/__tests__/audit-integration.integration.test.ts +1 -1
  84. package/src/custom-fields/__tests__/cross-tenant-field-delete.integration.test.ts +1 -1
  85. package/src/custom-fields/__tests__/cross-tenant-set-write.integration.test.ts +1 -1
  86. package/src/custom-fields/__tests__/custom-fields.integration.test.ts +1 -1
  87. package/src/custom-fields/__tests__/field-access.integration.test.ts +1 -1
  88. package/src/custom-fields/__tests__/quota.integration.test.ts +1 -1
  89. package/src/custom-fields/__tests__/retention.integration.test.ts +1 -1
  90. package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +1 -1
  91. package/src/custom-fields/events.ts +1 -1
  92. package/src/custom-fields/feature.ts +1 -1
  93. package/src/custom-fields/handlers/clear-custom-field.write.ts +1 -1
  94. package/src/custom-fields/handlers/set-custom-field.write.ts +1 -1
  95. package/src/custom-fields/lib/value-schema.ts +1 -1
  96. package/src/custom-fields/schemas.ts +1 -1
  97. package/src/data-retention/__tests__/retention-cleanup-files.integration.test.ts +454 -0
  98. package/src/data-retention/changes.json +6 -0
  99. package/src/data-retention/feature.ts +1 -0
  100. package/src/data-retention/handlers/policy-for.query.ts +1 -1
  101. package/src/data-retention/override-schema.ts +1 -1
  102. package/src/data-retention/run-retention-cleanup.ts +312 -5
  103. package/src/delivery/__tests__/delivery-pii-kms.integration.test.ts +1 -1
  104. package/src/delivery/__tests__/delivery.integration.test.ts +1 -1
  105. package/src/delivery/events.ts +1 -1
  106. package/src/delivery/feature.ts +1 -1
  107. package/src/delivery/handlers/log.query.ts +1 -1
  108. package/src/delivery/handlers/preferences.query.ts +1 -1
  109. package/src/delivery/handlers/set-preference.write.ts +1 -1
  110. package/src/delivery/jobs.ts +1 -1
  111. package/src/delivery/unsubscribe.ts +1 -1
  112. package/src/derivatives-sharp/__tests__/derivatives-sharp.integration.test.ts +1 -1
  113. package/src/document-ingest-foundation/events.ts +1 -1
  114. package/src/document-ingest-foundation/feature.ts +1 -1
  115. package/src/feature-toggles/__tests__/feature-toggles.integration.test.ts +1 -1
  116. package/src/feature-toggles/events.ts +1 -1
  117. package/src/feature-toggles/handlers/list.query.ts +1 -1
  118. package/src/feature-toggles/handlers/registered.query.ts +1 -1
  119. package/src/feature-toggles/handlers/set.write.ts +1 -1
  120. package/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  121. package/src/file-foundation/__tests__/file-foundation.integration.test.ts +1 -1
  122. package/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +1 -1
  123. package/src/folders/handlers/delete-folder.write.ts +1 -1
  124. package/src/folders/schemas.ts +1 -1
  125. package/src/form-draft/schemas.ts +1 -1
  126. package/src/inbound-mail-foundation/events.ts +1 -1
  127. package/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  128. package/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +1 -1
  129. package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -1
  130. package/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  131. package/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  132. package/src/inbound-mail-foundation/handlers/update-account.write.ts +1 -1
  133. package/src/inbound-provider-imap/credential-document.ts +1 -1
  134. package/src/jobs/__tests__/jobs-catalog.integration.test.ts +1 -1
  135. package/src/jobs/__tests__/tenant-job-failures.integration.test.ts +1 -1
  136. package/src/jobs/events.ts +1 -1
  137. package/src/jobs/handlers/catalog.query.ts +1 -1
  138. package/src/jobs/handlers/detail.query.ts +1 -1
  139. package/src/jobs/handlers/list.query.ts +1 -1
  140. package/src/jobs/handlers/projection-rebuild.job.ts +1 -1
  141. package/src/jobs/handlers/reindex-entity.job.ts +1 -1
  142. package/src/jobs/handlers/retry.write.ts +1 -1
  143. package/src/jobs/handlers/tenant-failures.query.ts +1 -1
  144. package/src/jobs/handlers/trigger.write.ts +1 -1
  145. package/src/ledger/handlers/reports.query.ts +1 -1
  146. package/src/ledger/schemas.ts +1 -1
  147. package/src/mail-foundation/__tests__/mail-foundation.integration.test.ts +1 -1
  148. package/src/managed-pages/handlers/branding.query.ts +1 -1
  149. package/src/managed-pages/handlers/by-slug.query.ts +1 -1
  150. package/src/managed-pages/handlers/by-tenant-published.query.ts +1 -1
  151. package/src/managed-pages/handlers/set.write.ts +1 -1
  152. package/src/notes-history/changes.json +7 -0
  153. package/src/notes-history/schemas.ts +1 -1
  154. package/src/notes-history/web/notes-section.tsx +37 -36
  155. package/src/personal-access-tokens/feature.ts +10 -0
  156. package/src/personal-access-tokens/handlers/available-scopes.query.ts +1 -1
  157. package/src/personal-access-tokens/handlers/create.write.ts +1 -1
  158. package/src/personal-access-tokens/handlers/list.query.ts +1 -1
  159. package/src/personal-access-tokens/handlers/revoke.write.ts +18 -2
  160. package/src/personal-access-tokens/index.ts +7 -0
  161. package/src/personal-access-tokens/pat-revoked-event.ts +23 -0
  162. package/src/personal-access-tokens/revoke-for-user.ts +31 -0
  163. package/src/rate-limiting/__tests__/rate-limiting.integration.test.ts +1 -1
  164. package/src/rate-limiting/handlers/status.query.ts +1 -1
  165. package/src/readiness/handlers/status.query.ts +1 -1
  166. package/src/secrets/feature.ts +1 -1
  167. package/src/secrets/handlers/delete.write.ts +1 -1
  168. package/src/secrets/handlers/list.query.ts +1 -1
  169. package/src/secrets/handlers/set.write.ts +1 -1
  170. package/src/secrets/secrets-context.ts +1 -1
  171. package/src/seo/handlers/seo-config.query.ts +1 -1
  172. package/src/sessions/handlers/detail.query.ts +1 -1
  173. package/src/sessions/handlers/list.query.ts +1 -1
  174. package/src/sessions/handlers/mine.query.ts +1 -1
  175. package/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
  176. package/src/sessions/handlers/revoke-all-others.write.ts +1 -1
  177. package/src/sessions/handlers/revoke.write.ts +1 -1
  178. package/src/sessions/session-callbacks.ts +25 -1
  179. package/src/sessions/session-revoked-event.ts +6 -1
  180. package/src/shared/__tests__/row-bound-grant.integration.test.ts +1 -1
  181. package/src/step-dispatcher/mail-runner.ts +1 -1
  182. package/src/step-dispatcher/webhook-runner.ts +1 -1
  183. package/src/subscription-mollie/feature.ts +1 -1
  184. package/src/subscription-stripe/feature.ts +1 -1
  185. package/src/tags/schemas.ts +1 -1
  186. package/src/template-resolver/handlers/by-slug.query.ts +1 -1
  187. package/src/template-resolver/handlers/by-tenant.query.ts +1 -1
  188. package/src/template-resolver/handlers/collection-item.query.ts +1 -1
  189. package/src/template-resolver/handlers/collection-list.query.ts +1 -1
  190. package/src/template-resolver/handlers/collection-set.write.ts +1 -1
  191. package/src/template-resolver/handlers/find-by-id.query.ts +1 -1
  192. package/src/template-resolver/handlers/list.query.ts +1 -1
  193. package/src/template-resolver/handlers/set.write.ts +1 -1
  194. package/src/template-resolver/handlers/shared.ts +1 -1
  195. package/src/template-resolver/handlers/toggle-status.write.ts +1 -1
  196. package/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
  197. package/src/tenant/__tests__/tenant-timezone-boot.integration.test.ts +1 -1
  198. package/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
  199. package/src/tenant/handlers/add-member.write.ts +1 -1
  200. package/src/tenant/handlers/cancel-invitation.write.ts +1 -1
  201. package/src/tenant/handlers/create.write.ts +1 -1
  202. package/src/tenant/handlers/invitations.query.ts +1 -1
  203. package/src/tenant/handlers/list.query.ts +1 -1
  204. package/src/tenant/handlers/me.query.ts +1 -1
  205. package/src/tenant/handlers/member-directory.query.ts +1 -1
  206. package/src/tenant/handlers/members.query.ts +1 -1
  207. package/src/tenant/handlers/memberships.query.ts +1 -1
  208. package/src/tenant/handlers/remove-member.write.ts +1 -1
  209. package/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
  210. package/src/tenant/handlers/team-list.query.ts +1 -1
  211. package/src/tenant/handlers/tenant-directory.query.ts +1 -1
  212. package/src/tenant/handlers/toggle-enabled.write.ts +1 -1
  213. package/src/tenant/handlers/update-member-roles.write.ts +1 -1
  214. package/src/tenant/handlers/update.write.ts +1 -1
  215. package/src/tenant-handover/events.ts +1 -1
  216. package/src/tenant-handover/handlers/claim.write.ts +1 -1
  217. package/src/tenant-lifecycle/__tests__/tenant-lifecycle.integration.test.ts +8 -24
  218. package/src/tenant-lifecycle/events.ts +1 -1
  219. package/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +1 -1
  220. package/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -1
  221. package/src/tenant-settings/tenant-defaults.ts +1 -1
  222. package/src/tier-engine/__tests__/tier-resolver.integration.test.ts +1 -1
  223. package/src/tier-engine/feature.ts +1 -1
  224. package/src/tier-engine/handlers/active-tier.query.ts +1 -1
  225. package/src/tier-engine/handlers/get-tenant-tier.query.ts +1 -1
  226. package/src/tier-engine/handlers/set-tenant-tier.write.ts +1 -1
  227. package/src/user/__tests__/user-global-tenancy.integration.test.ts +1 -1
  228. package/src/user/handlers/create.write.ts +1 -1
  229. package/src/user/handlers/find-for-auth.query.ts +1 -1
  230. package/src/user/handlers/me.query.ts +1 -1
  231. package/src/user/handlers/update.write.ts +1 -1
  232. package/src/user/roles-input-schema.ts +1 -1
  233. package/src/user/seeding.ts +29 -20
  234. package/src/user-data-rights/__tests__/boot-checks.test.ts +1 -1
  235. package/src/user-data-rights/changes.json +6 -0
  236. package/src/user-data-rights/feature.ts +1 -0
  237. package/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
  238. package/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +1 -1
  239. package/src/user-data-rights/handlers/download-by-job.query.ts +1 -1
  240. package/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
  241. package/src/user-data-rights/handlers/export-status.query.ts +1 -1
  242. package/src/user-data-rights/handlers/lift-restriction.write.ts +1 -1
  243. package/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -1
  244. package/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
  245. package/src/user-data-rights/handlers/request-deletion-by-email.write.ts +1 -1
  246. package/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
  247. package/src/user-data-rights/handlers/request-export.write.ts +1 -1
  248. package/src/user-data-rights/handlers/restrict-account.write.ts +1 -1
  249. package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +1 -1
  250. package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +8 -61
  251. package/src/user-profile/handlers/change-email.write.ts +1 -1
  252. package/src/workflow-runner/__tests__/pending-projection.integration.test.ts +1 -1
  253. package/src/workflow-runner/handlers/resume-run.write.ts +1 -1
@@ -7,7 +7,7 @@ import {
7
7
  type SessionUser,
8
8
  toKebab,
9
9
  } from "@cosmicdrift/kumiko-framework/engine";
10
- import { z } from "zod";
10
+ import * as z from "zod";
11
11
  import { requireConfigResolver, requireSystemDb } from "../feature";
12
12
  import { hasConfigAccess } from "../write-helpers";
13
13
 
@@ -5,7 +5,7 @@ import {
5
5
  SYSTEM_TENANT_ID,
6
6
  } from "@cosmicdrift/kumiko-framework/engine";
7
7
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
8
- import { z } from "zod";
8
+ import * as z from "zod";
9
9
  import { requireSystemDb } from "../feature";
10
10
  import { configValueEntity, configValuesTable } from "../table";
11
11
  import { findConfigRow, prepareConfigWrite } from "../write-helpers";
@@ -1,5 +1,5 @@
1
1
  import { type ConfigKeyDefinition, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
- import { z } from "zod";
2
+ import * as z from "zod";
3
3
  import { hasConfigAccess } from "../write-helpers";
4
4
 
5
5
  export const schemaQuery = defineQueryHandler({
@@ -15,7 +15,7 @@ import {
15
15
  UnprocessableError,
16
16
  writeFailure,
17
17
  } from "@cosmicdrift/kumiko-framework/errors";
18
- import { z } from "zod";
18
+ import * as z from "zod";
19
19
  import { requireConfigEncryption, requireSystemDb } from "../feature";
20
20
  import { configValueEntity, configValuesTable } from "../table";
21
21
  import {
@@ -5,7 +5,7 @@ import {
5
5
  defineQueryHandler,
6
6
  isEncryptedAtRest,
7
7
  } from "@cosmicdrift/kumiko-framework/engine";
8
- import { z } from "zod";
8
+ import * as z from "zod";
9
9
  import { requireConfigResolver, requireSystemDb } from "../feature";
10
10
  import { MASKED, redactInheritedCascade, shouldRedactInherited } from "../read-redaction";
11
11
  import { hasConfigAccess } from "../write-helpers";
@@ -30,7 +30,7 @@ import {
30
30
  import { append } from "@cosmicdrift/kumiko-framework/event-store";
31
31
  import { purgeSearchDocumentsForSubject } from "@cosmicdrift/kumiko-framework/search";
32
32
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
33
- import { z } from "zod";
33
+ import * as z from "zod";
34
34
  import { revokeAllPatTokensForUser } from "../../personal-access-tokens";
35
35
  import { USER_STATUS } from "../../user";
36
36
  import {
@@ -27,7 +27,7 @@ import {
27
27
  TestUsers,
28
28
  unsafeCreateEntityTable,
29
29
  } from "@cosmicdrift/kumiko-framework/stack";
30
- import { z } from "zod";
30
+ import * as z from "zod";
31
31
  import { AuditQueries } from "../../audit/constants";
32
32
  import { createAuditFeature } from "../../audit/feature";
33
33
  import { createConfigFeature } from "../../config";
@@ -28,7 +28,7 @@ import {
28
28
  testUserId,
29
29
  unsafeCreateEntityTable,
30
30
  } from "@cosmicdrift/kumiko-framework/stack";
31
- import { z } from "zod";
31
+ import * as z from "zod";
32
32
  import { fieldDefinitionEntity } from "../entity";
33
33
  import { createCustomFieldsFeature } from "../feature";
34
34
  import { customFieldsField, wireCustomFieldsFor } from "../wire-for-entity";
@@ -24,7 +24,7 @@ import {
24
24
  testUserId,
25
25
  unsafeCreateEntityTable,
26
26
  } from "@cosmicdrift/kumiko-framework/stack";
27
- import { z } from "zod";
27
+ import * as z from "zod";
28
28
  import { fieldDefinitionEntity } from "../entity";
29
29
  import { createCustomFieldsFeature } from "../feature";
30
30
  import { customFieldsField, wireCustomFieldsFor } from "../wire-for-entity";
@@ -31,7 +31,7 @@ import {
31
31
  type TestStack,
32
32
  unsafeCreateEntityTable,
33
33
  } from "@cosmicdrift/kumiko-framework/stack";
34
- import { z } from "zod";
34
+ import * as z from "zod";
35
35
  import { fieldDefinitionEntity } from "../entity";
36
36
  import { createCustomFieldsFeature } from "../feature";
37
37
  import { customFieldsField, wireCustomFieldsFor } from "../wire-for-entity";
@@ -25,7 +25,7 @@ import {
25
25
  type TestStack,
26
26
  unsafeCreateEntityTable,
27
27
  } from "@cosmicdrift/kumiko-framework/stack";
28
- import { z } from "zod";
28
+ import * as z from "zod";
29
29
  import { fieldDefinitionEntity } from "../entity";
30
30
  import { createCustomFieldsFeature } from "../feature";
31
31
  import { customFieldsField, wireCustomFieldsFor } from "../wire-for-entity";
@@ -21,7 +21,7 @@ import {
21
21
  type TestStack,
22
22
  unsafeCreateEntityTable,
23
23
  } from "@cosmicdrift/kumiko-framework/stack";
24
- import { z } from "zod";
24
+ import * as z from "zod";
25
25
  import { fieldDefinitionEntity } from "../entity";
26
26
  import { createCustomFieldsFeature } from "../feature";
27
27
  import { customFieldsField, wireCustomFieldsFor } from "../wire-for-entity";
@@ -26,7 +26,7 @@ import {
26
26
  unsafeCreateEntityTable,
27
27
  } from "@cosmicdrift/kumiko-framework/stack";
28
28
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
29
- import { z } from "zod";
29
+ import * as z from "zod";
30
30
  import { applyRetentionRemovals, selectHostRowsWithCustomFields } from "../db/queries/retention";
31
31
  import { fieldDefinitionEntity } from "../entity";
32
32
  import { createCustomFieldsFeature } from "../feature";
@@ -40,7 +40,7 @@ import {
40
40
  } from "@cosmicdrift/kumiko-framework/stack";
41
41
  import { seedRow } from "@cosmicdrift/kumiko-framework/testing";
42
42
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
43
- import { z } from "zod";
43
+ import * as z from "zod";
44
44
  import { createComplianceProfilesFeature } from "../../compliance-profiles";
45
45
  import { createDataRetentionFeature, tenantRetentionOverrideEntity } from "../../data-retention";
46
46
  import { tenantRetentionOverrideTable } from "../../data-retention/schema/tenant-retention-override";
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
 
3
3
  // Domain-Events für custom-field-VALUES. customField.set + .cleared leben
4
4
  // auf der host-aggregate stream (Plan-Doc v2 ES-Option-B: customField-Events
@@ -52,7 +52,7 @@ import {
52
52
  type FeatureRegistrar,
53
53
  type WriteHandlerDef,
54
54
  } from "@cosmicdrift/kumiko-framework/engine";
55
- import { z } from "zod";
55
+ import * as z from "zod";
56
56
  import {
57
57
  CUSTOM_FIELD_CLEARED_EVENT,
58
58
  CUSTOM_FIELD_SET_EVENT,
@@ -1,6 +1,6 @@
1
1
  import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
3
- import { z } from "zod";
3
+ import * as z from "zod";
4
4
  import { DEFAULT_VALUE_WRITE_ROLES } from "../constants";
5
5
  import { customFieldsFeature } from "../feature";
6
6
  import { checkFieldAccessForWrite } from "../lib/field-access";
@@ -1,6 +1,6 @@
1
1
  import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
3
- import { z } from "zod";
3
+ import * as z from "zod";
4
4
  import { DEFAULT_VALUE_WRITE_ROLES } from "../constants";
5
5
  import { customFieldsFeature } from "../feature";
6
6
  import { fieldWriteAccessDeniedRoles, loadFieldDefinition } from "../lib/field-access";
@@ -3,7 +3,7 @@ import {
3
3
  type FieldDefinition,
4
4
  fieldToZod,
5
5
  } from "@cosmicdrift/kumiko-framework/engine";
6
- import type { z } from "zod";
6
+ import type * as z from "zod";
7
7
  import { SUPPORTED_FIELD_TYPES } from "../constants";
8
8
 
9
9
  // Builds a Zod schema that validates a custom-field VALUE against its
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  import { SUPPORTED_FIELD_TYPES } from "./constants";
3
3
 
4
4
  // Field-Type-Validator — pinnt valid type-Werte für fieldDefinition.
@@ -0,0 +1,454 @@
1
+ // kumiko-framework#3089 — hardDelete deletes a row's file/image/files/images
2
+ // bytes (+ derivatives) and fileRef rows, not just the row itself.
3
+ //
4
+ // Verifies the negative cases where a data-loss/leak bug hides:
5
+ // - fresh rows + other rows' files are NEVER touched
6
+ // - a fileRef SHARED with another row of the same entity survives (bytes +
7
+ // row) even though the row that's being purged references it too
8
+ // - a storage-delete failure fails the row closed (row + fileRef survive,
9
+ // skipped "file_delete_failed") and converges on the next run
10
+ // - no file storage wired at all → row stays, skipped "missing_file_storage"
11
+ // - softDelete/anonymize entities with file fields never touch bytes
12
+ // - the registered cron handler forwards ctx.files, not just direct calls
13
+
14
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
15
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
16
+ import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
17
+ import {
18
+ createEntity,
19
+ createFilesField,
20
+ createImageField,
21
+ createSystemUser,
22
+ createTextField,
23
+ defineFeature,
24
+ type JobContext,
25
+ } from "@cosmicdrift/kumiko-framework/engine";
26
+ import {
27
+ createFileContext,
28
+ createInMemoryFileProvider,
29
+ deriveKey,
30
+ type FileStorageProvider,
31
+ fileRefEntity,
32
+ fileRefsTable,
33
+ } from "@cosmicdrift/kumiko-framework/files";
34
+ import {
35
+ setupTestStack,
36
+ type TestStack,
37
+ unsafeCreateEntityTable,
38
+ } from "@cosmicdrift/kumiko-framework/stack";
39
+ import { bridgeStub } from "@cosmicdrift/kumiko-framework/testing";
40
+ import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
41
+ import { createDataRetentionFeature } from "../feature";
42
+ import { runRetentionCleanup } from "../run-retention-cleanup";
43
+
44
+ // hardDelete entity with BOTH a single (image) and a plural (files) field —
45
+ // covers the column-value path AND the file_refs-lookup path in one entity.
46
+ const docEntity = createEntity({
47
+ table: "read_c8_doc",
48
+ fields: {
49
+ label: createTextField({ required: true, personal: false, reason: "test_fixture" }),
50
+ cover: createImageField(),
51
+ attachments: createFilesField(),
52
+ },
53
+ retention: { keepFor: "30d", strategy: "hardDelete" },
54
+ });
55
+
56
+ // softDelete entity with a file field — the strategy's own path (mark
57
+ // is_deleted) must never touch bytes; only hardDelete does.
58
+ const softDocEntity = createEntity({
59
+ table: "read_c8_soft_doc",
60
+ softDelete: true,
61
+ fields: {
62
+ label: createTextField({ required: true, personal: false, reason: "test_fixture" }),
63
+ cover: createImageField(),
64
+ },
65
+ retention: { keepFor: "30d", strategy: "softDelete" },
66
+ });
67
+
68
+ // anonymize entity with a file field — same guarantee for the anonymize path.
69
+ const anonDocEntity = createEntity({
70
+ table: "read_c8_anon_doc",
71
+ fields: {
72
+ label: {
73
+ ...createTextField({ required: true, personal: false, reason: "test_fixture" }),
74
+ anonymize: () => "[ANONYMIZED]",
75
+ },
76
+ cover: createImageField(),
77
+ },
78
+ retention: { keepFor: "30d", strategy: "anonymize" },
79
+ });
80
+
81
+ const c8Feature = defineFeature("c8-retention-file-fixtures", (r) => {
82
+ r.entity("c8-doc", docEntity);
83
+ r.entity("c8-soft-doc", softDocEntity);
84
+ r.entity("c8-anon-doc", anonDocEntity);
85
+ });
86
+
87
+ const noopLogger: JobContext["log"] = {
88
+ info() {},
89
+ warn() {},
90
+ error() {},
91
+ debug() {},
92
+ child() {
93
+ return noopLogger;
94
+ },
95
+ };
96
+
97
+ const T1 = "33333333-3333-3333-3333-333333333333";
98
+ const T2 = "44444444-4444-4444-4444-444444444444";
99
+
100
+ let stack: TestStack;
101
+ let provider: ReturnType<typeof createInMemoryFileProvider>;
102
+ let now: ReturnType<ReturnType<typeof getTemporal>["Now"]["instant"]>;
103
+ let pastIso: string;
104
+ let withinIso: string;
105
+ let fileRefCrud: ReturnType<typeof createEventStoreExecutor>;
106
+
107
+ beforeAll(async () => {
108
+ provider = createInMemoryFileProvider();
109
+ stack = await setupTestStack({
110
+ features: [createDataRetentionFeature(), c8Feature],
111
+ files: { storageProvider: provider },
112
+ });
113
+ for (const e of [docEntity, softDocEntity, anonDocEntity]) {
114
+ await unsafeCreateEntityTable(stack.db, e);
115
+ }
116
+ fileRefCrud = createEventStoreExecutor(fileRefsTable, fileRefEntity, { entityName: "fileRef" });
117
+ now = getTemporal().Now.instant();
118
+ pastIso = now.subtract({ hours: 60 * 24 }).toString();
119
+ withinIso = now.subtract({ hours: 10 * 24 }).toString();
120
+ });
121
+
122
+ afterAll(async () => {
123
+ await stack.cleanup();
124
+ });
125
+
126
+ function filesCtx(p: FileStorageProvider = provider) {
127
+ return createFileContext(() => Promise.resolve(p));
128
+ }
129
+
130
+ async function seedDoc(
131
+ table: string,
132
+ tenantId: string,
133
+ label: string,
134
+ insertedAtIso: string,
135
+ ): Promise<string> {
136
+ const rows = (await asRawClient(stack.db).unsafe(
137
+ `INSERT INTO ${table} (tenant_id, label, inserted_at) VALUES ($1, $2, $3::timestamptz) RETURNING id`,
138
+ [tenantId, label, insertedAtIso],
139
+ )) as { id: string | number }[];
140
+ return String(rows[0]?.id);
141
+ }
142
+
143
+ async function setCover(table: string, rowId: string, fileRefId: string | null): Promise<void> {
144
+ await asRawClient(stack.db).unsafe(`UPDATE ${table} SET cover = $1 WHERE id = $2`, [
145
+ fileRefId,
146
+ rowId,
147
+ ]);
148
+ }
149
+
150
+ async function seedFileRef(args: {
151
+ readonly tenantId: string;
152
+ readonly entityName: string | null;
153
+ readonly entityId: string | null;
154
+ readonly fieldName: string | null;
155
+ readonly key: string;
156
+ }): Promise<string> {
157
+ await provider.write(args.key, new Uint8Array([1, 2, 3]), "application/octet-stream");
158
+ const tdb = createTenantDb(stack.db, args.tenantId, "system");
159
+ const systemUser = createSystemUser(args.tenantId);
160
+ const id = crypto.randomUUID();
161
+ const res = await fileRefCrud.create(
162
+ {
163
+ id,
164
+ storageKey: args.key,
165
+ fileName: "f.bin",
166
+ mimeType: "application/octet-stream",
167
+ size: 3,
168
+ entityType: args.entityName,
169
+ entityId: args.entityId,
170
+ fieldName: args.fieldName,
171
+ },
172
+ systemUser,
173
+ tdb,
174
+ );
175
+ expect(res.isSuccess).toBe(true);
176
+ return id;
177
+ }
178
+
179
+ async function fileRefExists(id: string): Promise<boolean> {
180
+ const rows = await asRawClient(stack.db).unsafe(`SELECT id FROM file_refs WHERE id = $1`, [id]);
181
+ return (rows as unknown[]).length > 0;
182
+ }
183
+
184
+ async function labels(table: string, tenantId: string): Promise<string[]> {
185
+ const rows = (await asRawClient(stack.db).unsafe(
186
+ `SELECT label FROM ${table} WHERE tenant_id = $1 ORDER BY label`,
187
+ [tenantId],
188
+ )) as { label: string }[];
189
+ return rows.map((r) => r.label);
190
+ }
191
+
192
+ beforeEach(async () => {
193
+ for (const t of ["read_c8_doc", "read_c8_soft_doc", "read_c8_anon_doc"]) {
194
+ await asRawClient(stack.db).unsafe(`DELETE FROM ${t}`);
195
+ }
196
+ await asRawClient(stack.db).unsafe(`DELETE FROM file_refs`);
197
+ provider.clear();
198
+ });
199
+
200
+ describe("runRetentionCleanup :: hardDelete purges file bytes + derivatives + fileRefs", () => {
201
+ test("expired row: bytes + derivatives + fileRefs (single + plural field) gone; fresh + other-tenant untouched", async () => {
202
+ const expiredId = await seedDoc("read_c8_doc", T1, "expired", pastIso);
203
+ const coverKey = "t1/expired/cover/orig.jpg";
204
+ const coverId = await seedFileRef({
205
+ tenantId: T1,
206
+ entityName: "c8-doc",
207
+ entityId: expiredId,
208
+ fieldName: "cover",
209
+ key: coverKey,
210
+ });
211
+ await setCover("read_c8_doc", expiredId, coverId);
212
+ // Derivative-suffix grammar is "<name>-<16 hex chars>" (variantSuffix),
213
+ // not a bare variant name — see isDerivativeKeyOf/DERIVATIVE_SUFFIX_PATTERN.
214
+ const thumbKey = deriveKey(coverKey, "thumb-0123456789abcdef");
215
+ await provider.write(thumbKey, new Uint8Array([9]), "image/jpeg");
216
+
217
+ const attKey = "t1/expired/attachments/a.pdf";
218
+ const attId = await seedFileRef({
219
+ tenantId: T1,
220
+ entityName: "c8-doc",
221
+ entityId: expiredId,
222
+ fieldName: "attachments",
223
+ key: attKey,
224
+ });
225
+
226
+ const freshId = await seedDoc("read_c8_doc", T1, "fresh", withinIso);
227
+ const freshCoverKey = "t1/fresh/cover/orig.jpg";
228
+ const freshCoverId = await seedFileRef({
229
+ tenantId: T1,
230
+ entityName: "c8-doc",
231
+ entityId: freshId,
232
+ fieldName: "cover",
233
+ key: freshCoverKey,
234
+ });
235
+ await setCover("read_c8_doc", freshId, freshCoverId);
236
+
237
+ const otherTenantId = await seedDoc("read_c8_doc", T2, "expired-t2", pastIso);
238
+ const otherTenantKey = "t2/expired/cover/orig.jpg";
239
+ const otherTenantCoverId = await seedFileRef({
240
+ tenantId: T2,
241
+ entityName: "c8-doc",
242
+ entityId: otherTenantId,
243
+ fieldName: "cover",
244
+ key: otherTenantKey,
245
+ });
246
+ await setCover("read_c8_doc", otherTenantId, otherTenantCoverId);
247
+
248
+ const result = await runRetentionCleanup({
249
+ db: stack.db,
250
+ registry: stack.registry,
251
+ tenantId: T1,
252
+ tenantPreset: null,
253
+ now,
254
+ files: filesCtx(),
255
+ });
256
+
257
+ expect(result.hardDeleted).toBe(1);
258
+ expect(await labels("read_c8_doc", T1)).toEqual(["fresh"]);
259
+
260
+ expect(await provider.exists(coverKey)).toBe(false);
261
+ expect(await provider.exists(thumbKey)).toBe(false);
262
+ expect(await provider.exists(attKey)).toBe(false);
263
+ expect(await fileRefExists(coverId)).toBe(false);
264
+ expect(await fileRefExists(attId)).toBe(false);
265
+
266
+ expect(await provider.exists(freshCoverKey)).toBe(true);
267
+ expect(await fileRefExists(freshCoverId)).toBe(true);
268
+
269
+ expect(await provider.exists(otherTenantKey)).toBe(true);
270
+ expect(await fileRefExists(otherTenantCoverId)).toBe(true);
271
+ expect(await labels("read_c8_doc", T2)).toEqual(["expired-t2"]);
272
+ });
273
+
274
+ test("shared fileRef (same entity, second row) survives even though its owning row is purged", async () => {
275
+ const rowA = await seedDoc("read_c8_doc", T1, "expired-shared", pastIso);
276
+ const rowB = await seedDoc("read_c8_doc", T1, "fresh-shared", withinIso);
277
+ const sharedKey = "t1/shared/cover/orig.jpg";
278
+ const sharedId = await seedFileRef({
279
+ tenantId: T1,
280
+ entityName: "c8-doc",
281
+ entityId: rowA,
282
+ fieldName: "cover",
283
+ key: sharedKey,
284
+ });
285
+ await setCover("read_c8_doc", rowA, sharedId);
286
+ await setCover("read_c8_doc", rowB, sharedId);
287
+
288
+ const result = await runRetentionCleanup({
289
+ db: stack.db,
290
+ registry: stack.registry,
291
+ tenantId: T1,
292
+ tenantPreset: null,
293
+ now,
294
+ files: filesCtx(),
295
+ });
296
+
297
+ expect(result.hardDeleted).toBe(1);
298
+ expect(await labels("read_c8_doc", T1)).toEqual(["fresh-shared"]);
299
+ expect(await provider.exists(sharedKey)).toBe(true);
300
+ expect(await fileRefExists(sharedId)).toBe(true);
301
+ });
302
+
303
+ test("storage-delete failure fails the row closed; skipped + convergence on retry", async () => {
304
+ const rowId = await seedDoc("read_c8_doc", T1, "expired-fails", pastIso);
305
+ const key = "t1/fails/cover/orig.jpg";
306
+ const fileRefId = await seedFileRef({
307
+ tenantId: T1,
308
+ entityName: "c8-doc",
309
+ entityId: rowId,
310
+ fieldName: "cover",
311
+ key,
312
+ });
313
+ await setCover("read_c8_doc", rowId, fileRefId);
314
+
315
+ const failingProvider: FileStorageProvider = {
316
+ ...provider,
317
+ delete: async (k: string) => {
318
+ if (k === key) throw new Error("boom");
319
+ return provider.delete(k);
320
+ },
321
+ };
322
+
323
+ const failed = await runRetentionCleanup({
324
+ db: stack.db,
325
+ registry: stack.registry,
326
+ tenantId: T1,
327
+ tenantPreset: null,
328
+ now,
329
+ files: filesCtx(failingProvider),
330
+ });
331
+
332
+ expect(failed.hardDeleted).toBe(0);
333
+ expect(failed.skipped).toContainEqual({ entityName: "c8-doc", reason: "file_delete_failed" });
334
+ expect(await labels("read_c8_doc", T1)).toEqual(["expired-fails"]);
335
+ expect(await fileRefExists(fileRefId)).toBe(true);
336
+ expect(await provider.exists(key)).toBe(true);
337
+
338
+ const retried = await runRetentionCleanup({
339
+ db: stack.db,
340
+ registry: stack.registry,
341
+ tenantId: T1,
342
+ tenantPreset: null,
343
+ now,
344
+ files: filesCtx(),
345
+ });
346
+ expect(retried.hardDeleted).toBe(1);
347
+ expect(await labels("read_c8_doc", T1)).toEqual([]);
348
+ expect(await fileRefExists(fileRefId)).toBe(false);
349
+ expect(await provider.exists(key)).toBe(false);
350
+ });
351
+
352
+ test("no file storage wired → row stays, skipped missing_file_storage", async () => {
353
+ const rowId = await seedDoc("read_c8_doc", T1, "expired-no-storage", pastIso);
354
+ const key = "t1/no-storage/cover/orig.jpg";
355
+ const fileRefId = await seedFileRef({
356
+ tenantId: T1,
357
+ entityName: "c8-doc",
358
+ entityId: rowId,
359
+ fieldName: "cover",
360
+ key,
361
+ });
362
+ await setCover("read_c8_doc", rowId, fileRefId);
363
+
364
+ const result = await runRetentionCleanup({
365
+ db: stack.db,
366
+ registry: stack.registry,
367
+ tenantId: T1,
368
+ tenantPreset: null,
369
+ now,
370
+ });
371
+
372
+ expect(result.hardDeleted).toBe(0);
373
+ expect(result.skipped).toContainEqual({
374
+ entityName: "c8-doc",
375
+ reason: "missing_file_storage",
376
+ });
377
+ expect(await labels("read_c8_doc", T1)).toEqual(["expired-no-storage"]);
378
+ expect(await fileRefExists(fileRefId)).toBe(true);
379
+ });
380
+
381
+ test("softDelete and anonymize entities with a file field never touch bytes", async () => {
382
+ const softRowId = await seedDoc("read_c8_soft_doc", T1, "soft-expired", pastIso);
383
+ const softKey = "t1/soft/cover/orig.jpg";
384
+ const softFileRefId = await seedFileRef({
385
+ tenantId: T1,
386
+ entityName: "c8-soft-doc",
387
+ entityId: softRowId,
388
+ fieldName: "cover",
389
+ key: softKey,
390
+ });
391
+ await setCover("read_c8_soft_doc", softRowId, softFileRefId);
392
+
393
+ const anonRowId = await seedDoc("read_c8_anon_doc", T1, "anon-expired", pastIso);
394
+ const anonKey = "t1/anon/cover/orig.jpg";
395
+ const anonFileRefId = await seedFileRef({
396
+ tenantId: T1,
397
+ entityName: "c8-anon-doc",
398
+ entityId: anonRowId,
399
+ fieldName: "cover",
400
+ key: anonKey,
401
+ });
402
+ await setCover("read_c8_anon_doc", anonRowId, anonFileRefId);
403
+
404
+ const result = await runRetentionCleanup({
405
+ db: stack.db,
406
+ registry: stack.registry,
407
+ tenantId: T1,
408
+ tenantPreset: null,
409
+ now,
410
+ files: filesCtx(),
411
+ });
412
+
413
+ expect(result.softDeleted).toBe(1);
414
+ expect(result.anonymized).toBe(1);
415
+ expect(await provider.exists(softKey)).toBe(true);
416
+ expect(await fileRefExists(softFileRefId)).toBe(true);
417
+ expect(await provider.exists(anonKey)).toBe(true);
418
+ expect(await fileRefExists(anonFileRefId)).toBe(true);
419
+ });
420
+
421
+ test("the registered job handler forwards ctx.files — bytes gone after the cron runs", async () => {
422
+ const rowId = await seedDoc("read_c8_doc", T1, "expired-via-job", pastIso);
423
+ const key = "t1/via-job/cover/orig.jpg";
424
+ const fileRefId = await seedFileRef({
425
+ tenantId: T1,
426
+ entityName: "c8-doc",
427
+ entityId: rowId,
428
+ fieldName: "cover",
429
+ key,
430
+ });
431
+ await setCover("read_c8_doc", rowId, fileRefId);
432
+
433
+ const job = stack.registry.getJob("data-retention:job:retention-cleanup");
434
+ expect(job).toBeDefined();
435
+ if (!job) return;
436
+
437
+ const ctx: JobContext = {
438
+ db: createTenantDb(stack.db, T1, "tenant", undefined, undefined, undefined, {
439
+ unsafeRaw: { reason: "per-tenant fan-out cleanup using raw helpers" },
440
+ }),
441
+ registry: stack.registry,
442
+ systemUser: { id: "system", tenantId: T1, roles: ["all"] },
443
+ log: noopLogger,
444
+ triggeredBy: null,
445
+ files: filesCtx(),
446
+ ...bridgeStub(),
447
+ };
448
+ await job.handler({}, ctx);
449
+
450
+ expect(await labels("read_c8_doc", T1)).toEqual([]);
451
+ expect(await fileRefExists(fileRefId)).toBe(false);
452
+ expect(await provider.exists(key)).toBe(false);
453
+ });
454
+ });
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.309.0",
4
+ "type": "breaking",
5
+ "title": "data-retention hardDelete now purges file bytes + fileRef rows, not just the entity row (fw#3089)",
6
+ "migration": "hardDelete previously deleted only the entity row and left every file/image/files/images field's storage bytes and `file_refs` row behind — a DSGVO Art. 17 gap. As of this release, an expired row's un-shared fileRefs (same tenant, bound to this row, not referenced by another row of the same entity) have their storage bytes deleted (including derivatives/thumbnails), then their `file_refs` row, then the entity row itself. A fileRef still referenced by another row, or bound to a different entity/record, is left untouched. Without a resolvable file-storage provider in the retention cron's job context, an entity with pending file deletions is skipped entirely (`skipped` reason `missing_file_storage`) rather than silently dropping the row with orphaned bytes; a storage-delete failure skips the row too (`file_delete_failed`) and retries on the next run. Before upgrading, a consumer relying on hardDelete NOT touching file storage should: list entities with `retention.strategy: \"hardDelete\"` and at least one file/image/files/images field, count rows already past `keepFor` for each, and confirm the referenced files are safe to delete (not needed elsewhere) before the next cron run."
7
+ },
2
8
  {
3
9
  "version": "0.167.1",
4
10
  "type": "fix",
@@ -109,6 +109,7 @@ export function createDataRetentionFeature(): FeatureDefinition {
109
109
  tenantId,
110
110
  tenantPreset,
111
111
  now: T.Now.instant(),
112
+ files: ctx.files,
112
113
  });
113
114
  if (result.skipped.length > 0) {
114
115
  // biome-ignore lint/suspicious/noConsole: operator-visibility for skipped entities