@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
@@ -1,6 +1,6 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
- import { z } from "zod";
3
+ import * as z from "zod";
4
4
  import { parseRetentionOverrideOrNull } from "../_internal/parse-override";
5
5
  import { type EffectiveRetentionPolicy, resolveRetentionPolicy } from "../resolver";
6
6
  import { tenantRetentionOverrideTable } from "../schema/tenant-retention-override";
@@ -13,7 +13,7 @@
13
13
  // fallback auf Preset/Entity-Default), aber WAS gesetzt ist muss
14
14
  // gueltig sein.
15
15
 
16
- import { z } from "zod";
16
+ import * as z from "zod";
17
17
 
18
18
  const KEEP_FOR_PATTERN = /^\d+[hdwmy]$/;
19
19
 
@@ -6,7 +6,11 @@
6
6
  // Strategy auf Rows an deren reference-Timestamp aelter als der keepFor-Cutoff
7
7
  // ist:
8
8
  //
9
- // - hardDelete → executor.forget pro Row (Event → rebuild-safe hard purge)
9
+ // - hardDelete → executor.forget pro Row (Event → rebuild-safe hard purge).
10
+ // Entities with file/image/files/images fields: bytes first
11
+ // (incl. derivatives), then the fileRef row(s), then the row;
12
+ // shared fileRefs (other owner, other row of the same table)
13
+ // stay untouched.
10
14
  // - softDelete → executor.delete pro Row (Event → rebuild-safe soft-delete),
11
15
  // nur auf noch-nicht-geloeschte (isDeleted:false-Filter)
12
16
  // - anonymize → executor.update pro Row mit den Werten der per-Feld
@@ -39,14 +43,22 @@ import {
39
43
  createEventStoreExecutor,
40
44
  createTenantDb,
41
45
  type DbRunner,
46
+ type TenantDb,
42
47
  type WhereObject,
43
48
  } from "@cosmicdrift/kumiko-framework/db";
49
+ import { deleteStoredFileAndDerivatives } from "@cosmicdrift/kumiko-framework/derivatives";
44
50
  import {
45
51
  createSystemUser,
46
52
  type EntityId,
47
53
  type Registry,
54
+ type SessionUser,
48
55
  type TenantId,
49
56
  } from "@cosmicdrift/kumiko-framework/engine";
57
+ import {
58
+ type FileContext,
59
+ fileRefEntity,
60
+ fileRefsTable,
61
+ } from "@cosmicdrift/kumiko-framework/files";
50
62
  import { computeCutoff, type Instant } from "./keep-for";
51
63
  import type { RetentionPresetKey } from "./presets";
52
64
  import { resolveRetentionPolicyForTenant } from "./resolve-for-tenant";
@@ -76,6 +88,10 @@ export interface RunRetentionCleanupArgs {
76
88
  /** Now-Injection — Tests pinnen den Wert ohne Date-Mock (Pattern keep-for.ts). */
77
89
  readonly now: Instant;
78
90
  readonly batchLimit?: number;
91
+ /** Storage access for hardDelete entities with file/image/files/images fields.
92
+ * Missing while fileRefs are due → the row stays (skipped "missing_file_storage"),
93
+ * fail-closed instead of orphaning bytes. */
94
+ readonly files?: FileContext;
79
95
  }
80
96
 
81
97
  export interface RetentionCleanupSkip {
@@ -83,7 +99,9 @@ export interface RetentionCleanupSkip {
83
99
  readonly reason:
84
100
  | "missing_reference_column"
85
101
  | "missing_softdelete_columns"
86
- | "missing_anonymize_fields";
102
+ | "missing_anonymize_fields"
103
+ | "missing_file_storage"
104
+ | "file_delete_failed";
87
105
  }
88
106
 
89
107
  export interface RunRetentionCleanupResult {
@@ -113,6 +131,272 @@ async function purgeMatchingRows(
113
131
  return count;
114
132
  }
115
133
 
134
+ // --- hardDelete + file-fields (kumiko-framework#3089) ---
135
+
136
+ type FileFieldPlan = {
137
+ /** type file|image — stores the fileRef-id directly in the entity column. */
138
+ readonly singleFields: readonly string[];
139
+ /** type files|images — no entity column, resolved via file_refs. */
140
+ readonly multiFields: readonly string[];
141
+ };
142
+
143
+ const NO_FILE_FIELDS: FileFieldPlan = { singleFields: [], multiFields: [] };
144
+
145
+ // file/image = single-column fields (fileRef-id lives directly in the entity
146
+ // row); files/images = plural, no column, resolved via file_refs. Same set
147
+ // as boot-validator/entity-handler.ts FILE_FIELD_TYPES and engine's
148
+ // isFileField — inlined here rather than importing across the package
149
+ // barrel since this module already needs the single/multi split, not just
150
+ // membership.
151
+ function planFileFields(fields: Readonly<Record<string, unknown>>): FileFieldPlan {
152
+ const singleFields: string[] = [];
153
+ const multiFields: string[] = [];
154
+ for (const [name, field] of Object.entries(fields)) {
155
+ const type = (field as { readonly type?: unknown }).type; // @cast-boundary schema-walk
156
+ if (type === "file" || type === "image") singleFields.push(name);
157
+ else if (type === "files" || type === "images") multiFields.push(name);
158
+ }
159
+ return singleFields.length > 0 || multiFields.length > 0
160
+ ? { singleFields, multiFields }
161
+ : NO_FILE_FIELDS;
162
+ }
163
+
164
+ // Every fileRef-id a row's file/image/files/images-fields reference: single
165
+ // fields hold the id in their own column, plural fields have no column and
166
+ // resolve via file_refs (tenantId, entityType, entityId, fieldName).
167
+ async function collectFileRefIds(
168
+ db: DbRunner,
169
+ tenantId: TenantId,
170
+ entityName: string,
171
+ row: Record<string, unknown>,
172
+ plan: FileFieldPlan,
173
+ ): Promise<readonly string[]> {
174
+ const ids = new Set<string>();
175
+ for (const field of plan.singleFields) {
176
+ const value = row[field];
177
+ if (typeof value === "string" && value.length > 0) ids.add(value);
178
+ }
179
+ if (plan.multiFields.length > 0) {
180
+ const refRows = await selectMany<{ id: string }>(db, fileRefsTable, {
181
+ tenantId,
182
+ entityType: entityName,
183
+ entityId: String(row["id"]),
184
+ fieldName: { in: plan.multiFields },
185
+ });
186
+ for (const r of refRows) ids.add(r.id);
187
+ }
188
+ return Array.from(ids);
189
+ }
190
+
191
+ // A fileRef is bound to a DIFFERENT entity/record than the row currently
192
+ // being purged — its binding (entityType/entityId), not the row's own
193
+ // single-field column, decides ownership. A single-field value can point at
194
+ // a fileRef uploaded/attached elsewhere; that fileRef's bytes belong to
195
+ // whatever it's actually bound to, not to this row.
196
+ function isFileRefBoundElsewhere(
197
+ fileRef: Record<string, unknown>,
198
+ entityName: string,
199
+ rowId: string,
200
+ ): boolean {
201
+ const entityType = fileRef["entityType"];
202
+ const entityId = fileRef["entityId"];
203
+ if (typeof entityType === "string" && entityType !== entityName) return true;
204
+ if (typeof entityId === "string" && entityId !== rowId) return true;
205
+ return false;
206
+ }
207
+
208
+ // A single-field fileRef-id can be shared by more than one row of the SAME
209
+ // table (e.g. a stock-photo re-picked across records). Purging its bytes
210
+ // would corrupt the row that isn't being deleted. Bounded by batchLimit rows
211
+ // per run and singleFields.length queries per fileRef — the same per-row
212
+ // event-cost tradeoff as the rest of this cron (see header).
213
+ async function isFileRefSharedByOtherRow(
214
+ db: DbRunner,
215
+ table: Parameters<typeof selectMany>[1],
216
+ tableHasTenantId: boolean,
217
+ tenantId: TenantId,
218
+ rowId: string,
219
+ singleFields: readonly string[],
220
+ fileRefId: string,
221
+ ): Promise<boolean> {
222
+ for (const field of singleFields) {
223
+ const where: WhereObject = { [field]: fileRefId, id: { ne: rowId } };
224
+ if (tableHasTenantId) where["tenantId"] = tenantId;
225
+ const others = await selectMany(db, table, where, { limit: 1 });
226
+ if (others.length > 0) return true;
227
+ }
228
+ return false;
229
+ }
230
+
231
+ // hardDelete for ONE row of an entity that has file/image/files/images
232
+ // fields: delete the un-shared fileRefs' bytes (+ derivatives) first, then
233
+ // their fileRef rows, then the entity row itself — bytes before rows so a
234
+ // crash mid-way leaves an orphaned fileRef pointing at already-gone bytes
235
+ // (safe: a later run's provider.delete is idempotent) rather than a gone
236
+ // fileRef whose bytes leak forever.
237
+ async function purgeHardDeleteRowWithFiles(args: {
238
+ readonly db: DbRunner;
239
+ readonly table: Parameters<typeof selectMany>[1];
240
+ readonly tableHasTenantId: boolean;
241
+ readonly tenantId: TenantId;
242
+ readonly entityName: string;
243
+ readonly row: Record<string, unknown>;
244
+ readonly plan: FileFieldPlan;
245
+ readonly files: FileContext | undefined;
246
+ readonly entityExecutor: ReturnType<typeof createEventStoreExecutor>;
247
+ readonly fileRefExecutor: ReturnType<typeof createEventStoreExecutor>;
248
+ readonly systemUser: SessionUser;
249
+ readonly tdb: TenantDb;
250
+ readonly onSkip: (reason: "missing_file_storage" | "file_delete_failed") => void;
251
+ }): Promise<boolean> {
252
+ const rowId = String(args.row["id"]);
253
+ const fileRefIds = await collectFileRefIds(
254
+ args.db,
255
+ args.tenantId,
256
+ args.entityName,
257
+ args.row,
258
+ args.plan,
259
+ );
260
+ if (fileRefIds.length === 0) {
261
+ return (
262
+ await args.entityExecutor.forget(
263
+ { id: args.row["id"] as EntityId },
264
+ args.systemUser,
265
+ args.tdb,
266
+ )
267
+ ).isSuccess;
268
+ }
269
+
270
+ const fileRefRows = await selectMany<Record<string, unknown>>(args.db, fileRefsTable, {
271
+ id: { in: fileRefIds },
272
+ tenantId: args.tenantId,
273
+ });
274
+
275
+ const toDelete: Record<string, unknown>[] = [];
276
+ for (const fileRef of fileRefRows) {
277
+ if (isFileRefBoundElsewhere(fileRef, args.entityName, rowId)) continue;
278
+ const shared = await isFileRefSharedByOtherRow(
279
+ args.db,
280
+ args.table,
281
+ args.tableHasTenantId,
282
+ args.tenantId,
283
+ rowId,
284
+ args.plan.singleFields,
285
+ String(fileRef["id"]),
286
+ );
287
+ if (shared) continue;
288
+ toDelete.push(fileRef);
289
+ }
290
+
291
+ if (toDelete.length === 0) {
292
+ return (
293
+ await args.entityExecutor.forget(
294
+ { id: args.row["id"] as EntityId },
295
+ args.systemUser,
296
+ args.tdb,
297
+ )
298
+ ).isSuccess;
299
+ }
300
+
301
+ if (!args.files) {
302
+ args.onSkip("missing_file_storage");
303
+ return false;
304
+ }
305
+ const files = args.files;
306
+ const store = { list: files.list, delete: (key: string) => files.ref(key).delete() };
307
+
308
+ let anyFailed = false;
309
+ for (const fileRef of toDelete) {
310
+ const key = fileRef["storageKey"];
311
+ if (typeof key !== "string" || key.length === 0) continue;
312
+ const failedKeys = await deleteStoredFileAndDerivatives(
313
+ key,
314
+ store,
315
+ "data-retention:hardDelete",
316
+ );
317
+ if (failedKeys.length > 0) {
318
+ anyFailed = true;
319
+ // biome-ignore lint/suspicious/noConsole: operator-visibility for storage-cleanup failures
320
+ console.warn(
321
+ `[data-retention:hardDelete] tenant=${args.tenantId} entity=${args.entityName} row=${rowId} storage delete failed keys=${failedKeys.join(",")}`,
322
+ );
323
+ }
324
+ }
325
+ if (anyFailed) {
326
+ args.onSkip("file_delete_failed");
327
+ return false;
328
+ }
329
+
330
+ for (const fileRef of toDelete) {
331
+ const res = await args.fileRefExecutor.forget(
332
+ { id: fileRef["id"] as EntityId },
333
+ args.systemUser,
334
+ args.tdb,
335
+ );
336
+ if (!res.isSuccess) {
337
+ args.onSkip("file_delete_failed");
338
+ return false;
339
+ }
340
+ }
341
+
342
+ return (
343
+ await args.entityExecutor.forget({ id: args.row["id"] as EntityId }, args.systemUser, args.tdb)
344
+ ).isSuccess;
345
+ }
346
+
347
+ // hardDelete page for an entity WITH file/image/files/images fields — loads
348
+ // full rows (not just id) so file-field values are available, and reports
349
+ // each skip reason at most once per entity per run (repeat skips would just
350
+ // be noise; the per-row failure is already warned above).
351
+ async function purgeHardDeleteRowsWithFiles(args: {
352
+ readonly db: DbRunner;
353
+ readonly table: Parameters<typeof selectMany>[1];
354
+ readonly tableHasTenantId: boolean;
355
+ readonly where: WhereObject;
356
+ readonly batchLimit: number;
357
+ readonly tenantId: TenantId;
358
+ readonly entityName: string;
359
+ readonly plan: FileFieldPlan;
360
+ readonly files: FileContext | undefined;
361
+ readonly entityExecutor: ReturnType<typeof createEventStoreExecutor>;
362
+ readonly fileRefExecutor: ReturnType<typeof createEventStoreExecutor>;
363
+ readonly systemUser: SessionUser;
364
+ readonly tdb: TenantDb;
365
+ readonly skipped: RetentionCleanupSkip[];
366
+ }): Promise<number> {
367
+ const rows = await selectMany<Record<string, unknown>>(args.db, args.table, args.where, {
368
+ limit: args.batchLimit,
369
+ });
370
+ const reasonsSeen = new Set<RetentionCleanupSkip["reason"]>();
371
+ const onSkip = (reason: "missing_file_storage" | "file_delete_failed") => {
372
+ // skip: reason already reported for this entity in this run
373
+ if (reasonsSeen.has(reason)) return;
374
+ reasonsSeen.add(reason);
375
+ args.skipped.push({ entityName: args.entityName, reason });
376
+ };
377
+
378
+ let count = 0;
379
+ for (const row of rows) {
380
+ const ok = await purgeHardDeleteRowWithFiles({
381
+ db: args.db,
382
+ table: args.table,
383
+ tableHasTenantId: args.tableHasTenantId,
384
+ tenantId: args.tenantId,
385
+ entityName: args.entityName,
386
+ row,
387
+ plan: args.plan,
388
+ files: args.files,
389
+ entityExecutor: args.entityExecutor,
390
+ fileRefExecutor: args.fileRefExecutor,
391
+ systemUser: args.systemUser,
392
+ tdb: args.tdb,
393
+ onSkip,
394
+ });
395
+ if (ok) count++;
396
+ }
397
+ return count;
398
+ }
399
+
116
400
  // PiiAnnotations.anonymize — structural view, avoids reaching into the
117
401
  // engine's field-type internals from here.
118
402
  type AnonymizeCapableField = { readonly anonymize?: () => unknown | Promise<unknown> };
@@ -244,9 +528,32 @@ export async function runRetentionCleanup(
244
528
  switch (policy.strategy) {
245
529
  case "hardDelete": {
246
530
  const executor = createEventStoreExecutor(proj.table, entity, { entityName });
247
- hardDeleted += await purgeMatchingRows(db, proj.table, where, batchLimit, (id) =>
248
- executor.forget({ id }, systemUser, tdb),
249
- );
531
+ const filePlan = planFileFields(entity.fields);
532
+ if (filePlan.singleFields.length === 0 && filePlan.multiFields.length === 0) {
533
+ hardDeleted += await purgeMatchingRows(db, proj.table, where, batchLimit, (id) =>
534
+ executor.forget({ id }, systemUser, tdb),
535
+ );
536
+ break;
537
+ }
538
+ const fileRefExecutor = createEventStoreExecutor(fileRefsTable, fileRefEntity, {
539
+ entityName: "fileRef",
540
+ });
541
+ hardDeleted += await purgeHardDeleteRowsWithFiles({
542
+ db,
543
+ table: proj.table,
544
+ tableHasTenantId: table["tenantId"] !== undefined,
545
+ where,
546
+ batchLimit,
547
+ tenantId,
548
+ entityName,
549
+ plan: filePlan,
550
+ files: args.files,
551
+ entityExecutor: executor,
552
+ fileRefExecutor,
553
+ systemUser,
554
+ tdb,
555
+ skipped,
556
+ });
250
557
  break;
251
558
  }
252
559
  case "softDelete": {
@@ -28,7 +28,7 @@ import {
28
28
  unsafePushTables,
29
29
  } from "@cosmicdrift/kumiko-framework/stack";
30
30
  import { resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
31
- import { z } from "zod";
31
+ import * as z from "zod";
32
32
  import { createChannelEmailFeature } from "../../channel-email/feature";
33
33
  import { createInMemoryTransport } from "../../channel-email/types";
34
34
  import { createConfigFeature } from "../../config/feature";
@@ -26,7 +26,7 @@ import {
26
26
  testTenantId,
27
27
  unsafePushTables,
28
28
  } from "@cosmicdrift/kumiko-framework/stack";
29
- import { z } from "zod";
29
+ import * as z from "zod";
30
30
  import { createChannelEmailFeature } from "../../channel-email/feature";
31
31
  import { createInMemoryTransport, type EmailMessage } from "../../channel-email/types";
32
32
  import { InAppHandlers, InAppQueries } from "../../channel-in-app/constants";
@@ -3,7 +3,7 @@
3
3
  // delivery-service.ts (validates payloads before the low-level append()
4
4
  // — out-of-dispatcher writes otherwise skip schema enforcement).
5
5
 
6
- import { z } from "zod";
6
+ import * as z from "zod";
7
7
  import { DeliveryStatus } from "./constants";
8
8
 
9
9
  export const deliveryAttemptSchema = z.object({
@@ -5,7 +5,7 @@ import {
5
5
  type FeatureDefinition,
6
6
  i18nKey,
7
7
  } from "@cosmicdrift/kumiko-framework/engine";
8
- import type { z } from "zod";
8
+ import type * as z from "zod";
9
9
  import {
10
10
  DELIVERY_ATTEMPT_EVENT,
11
11
  DELIVERY_CHANNEL_EXTENSION,
@@ -8,7 +8,7 @@ import {
8
8
  } from "@cosmicdrift/kumiko-framework/engine";
9
9
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
10
10
  import { Temporal } from "temporal-polyfill";
11
- import { z } from "zod";
11
+ import * as z from "zod";
12
12
  import { decryptStoredPii } from "../../shared";
13
13
  import type { DeliveryStatusValue } from "../constants";
14
14
  import { deliveryAttemptsTable } from "../tables";
@@ -1,7 +1,7 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
4
- import { z } from "zod";
4
+ import * as z from "zod";
5
5
  import { notificationPreferencesTable } from "../tables";
6
6
 
7
7
  export const preferencesQuery = defineQueryHandler({
@@ -1,6 +1,6 @@
1
1
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
- import { z } from "zod";
3
+ import * as z from "zod";
4
4
  import { upsertPreference } from "../upsert-preference";
5
5
 
6
6
  export const setPreferenceWrite = defineWriteHandler({
@@ -14,7 +14,7 @@ import type {
14
14
  } from "@cosmicdrift/kumiko-framework/engine";
15
15
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
16
16
  import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
17
- import { z } from "zod";
17
+ import * as z from "zod";
18
18
  import { appendAttemptEvent } from "./attempt-log";
19
19
  import { buildChannelContext } from "./channel-context";
20
20
  import { DeliveryJobs, deliveryPriorityRank } from "./constants";
@@ -2,7 +2,7 @@ import { createTenantDb, type DbConnection } from "@cosmicdrift/kumiko-framework
2
2
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { Hono } from "hono";
4
4
  import * as jose from "jose";
5
- import { z } from "zod";
5
+ import * as z from "zod";
6
6
  import { upsertPreference } from "./upsert-preference";
7
7
 
8
8
  // Shape des verified-JWT-payloads. tenantId kommt als string aus jose und
@@ -18,7 +18,7 @@ import {
18
18
  patchFileInstanceofForBunTest,
19
19
  } from "@cosmicdrift/kumiko-framework/testing";
20
20
  import sharp from "sharp";
21
- import { z } from "zod";
21
+ import * as z from "zod";
22
22
  import { createConfigFeature } from "../../config";
23
23
  import { fileDerivativesFeature } from "../../file-derivatives";
24
24
  import { fileFoundationFeature } from "../../file-foundation";
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
 
3
3
  // documentIngest — own aggregate-type for the ingest-request stream, one
4
4
  // stream per fileRef (aggregateId = fileRefId). Not backed by an r.entity;
@@ -18,7 +18,7 @@ import {
18
18
  defineFeature,
19
19
  EXT_TENANT_DATA,
20
20
  } from "@cosmicdrift/kumiko-framework/engine";
21
- import { z } from "zod";
21
+ import * as z from "zod";
22
22
  import { documentExtractEntity } from "./entity";
23
23
  import {
24
24
  DOCUMENT_INGEST_AGGREGATE_TYPE,
@@ -29,7 +29,7 @@ import {
29
29
  import { createLateBoundHolder, seedRow, waitFor } from "@cosmicdrift/kumiko-framework/testing";
30
30
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
31
31
  import { Temporal } from "temporal-polyfill";
32
- import { z } from "zod";
32
+ import * as z from "zod";
33
33
  import { createFeatureTogglesFeature } from "../feature";
34
34
  import { globalFeatureStateTable } from "../global-feature-state-table";
35
35
  import { GlobalFeatureToggleRuntime } from "../toggle-runtime";
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
 
3
3
  // Toggle-change event payload.
4
4
  //
@@ -1,5 +1,5 @@
1
1
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
- import { z } from "zod";
2
+ import * as z from "zod";
3
3
  import { FEATURE_TOGGLE_CROSS_TENANT_REASON } from "../constants";
4
4
  import { globalFeatureStateTable } from "../global-feature-state-table";
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
2
- import { z } from "zod";
2
+ import * as z from "zod";
3
3
  import { FEATURE_TOGGLE_CROSS_TENANT_REASON } from "../constants";
4
4
  import { globalFeatureStateTable } from "../global-feature-state-table";
5
5
 
@@ -6,7 +6,7 @@ import {
6
6
  writeFailure,
7
7
  } from "@cosmicdrift/kumiko-framework/errors";
8
8
  import { Temporal } from "temporal-polyfill";
9
- import { z } from "zod";
9
+ import * as z from "zod";
10
10
  import {
11
11
  FEATURE_TOGGLE_AGGREGATE_TYPE,
12
12
  FEATURE_TOGGLE_CROSS_TENANT_REASON,
@@ -28,7 +28,7 @@ import {
28
28
  type TenantId,
29
29
  } from "@cosmicdrift/kumiko-framework/engine";
30
30
  import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
31
- import { z } from "zod";
31
+ import * as z from "zod";
32
32
 
33
33
  type FileRefRow = {
34
34
  readonly entityType: string | null;
@@ -28,7 +28,7 @@ import {
28
28
  createTestEnvelopeCipher,
29
29
  type MutableMasterKeyProvider,
30
30
  } from "@cosmicdrift/kumiko-framework/testing";
31
- import { z } from "zod";
31
+ import * as z from "zod";
32
32
  import { createConfigFeature } from "../../config";
33
33
  import { ConfigHandlers } from "../../config/constants";
34
34
  import { createConfigAccessorFactory } from "../../config/feature";
@@ -28,7 +28,7 @@ import {
28
28
  type FileStorageProvider,
29
29
  fileRefsTable,
30
30
  } from "@cosmicdrift/kumiko-framework/files";
31
- import { z } from "zod";
31
+ import * as z from "zod";
32
32
  import { runChunkedMigration } from "../../shared";
33
33
  import { tenantTable } from "../../tenant";
34
34
 
@@ -1,6 +1,6 @@
1
1
  import type { AccessRule, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
- import { z } from "zod";
3
+ import * as z from "zod";
4
4
  import { DEFAULT_FOLDER_ACCESS } from "../constants";
5
5
  import { folderAssignmentExecutor, folderExecutor } from "../executor";
6
6
 
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
 
3
3
  // set-folder + clear-folder share the (entity) reference shape; set-folder adds
4
4
  // the target folderId. Folder catalog CRUD (create/update/delete) uses the
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  import { FORM_DRAFT_KEY_MAX_LENGTH, FORM_DRAFT_VALUES_MAX_BYTES } from "./constants";
3
3
 
4
4
  const draftKeySchema = z.string().trim().min(1).max(FORM_DRAFT_KEY_MAX_LENGTH);
@@ -19,7 +19,7 @@
19
19
  // eraseSubjectKeys macht Event-Log UND Projection unlesbar
20
20
  // (crypto-shredding, Muster billing-foundation #800).
21
21
 
22
- import { z } from "zod";
22
+ import * as z from "zod";
23
23
  import { INBOUND_MAIL_FOUNDATION_FEATURE, InboundMailAccountStatuses } from "./constants";
24
24
 
25
25
  // Aggregate-types für den event-store.
@@ -19,7 +19,7 @@ import {
19
19
  } from "@cosmicdrift/kumiko-framework/crypto";
20
20
  import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
21
21
  import { failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
22
- import { z } from "zod";
22
+ import * as z from "zod";
23
23
  import { InboundMailAccountStatuses, InboundMailAuthMethods } from "../constants";
24
24
  import { MAIL_ACCOUNT_PII_FIELDS, mailAccountEntity } from "../entities";
25
25
  import {
@@ -9,7 +9,7 @@
9
9
 
10
10
  import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
11
11
  import { failNotFound } from "@cosmicdrift/kumiko-framework/errors";
12
- import { z } from "zod";
12
+ import * as z from "zod";
13
13
  import { InboundMailAccountStatuses } from "../constants";
14
14
  import {
15
15
  MAIL_ACCOUNT_AGGREGATE_TYPE,
@@ -29,7 +29,7 @@ import { acquireNamespacedAdvisoryLock } from "@cosmicdrift/kumiko-framework/db"
29
29
  import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
30
30
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
31
31
  import { Temporal } from "temporal-polyfill";
32
- import { z } from "zod";
32
+ import * as z from "zod";
33
33
  import { inboundMessageAggregateId, mailThreadAggregateId } from "../aggregate-id";
34
34
  import {
35
35
  INBOUND_MESSAGE_PII_FIELDS,
@@ -12,7 +12,7 @@ import {
12
12
  decryptPiiFieldValues,
13
13
  } from "@cosmicdrift/kumiko-framework/crypto";
14
14
  import type { QueryHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
15
- import { z } from "zod";
15
+ import * as z from "zod";
16
16
  import { MAIL_ACCOUNT_PII_FIELDS } from "../entities";
17
17
  import { mailAccountsProjectionTable } from "../projection";
18
18
  import { isVisibleToCaller } from "./scope-visibility";