@cosmicdrift/kumiko-framework 0.386.0 → 0.387.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 (288) hide show
  1. package/LICENSE +21 -57
  2. package/README.md +2 -2
  3. package/agents/widgets.md +39 -39
  4. package/dist/api/api-constants.js +2 -2
  5. package/dist/api/auth-middleware.js +7 -7
  6. package/dist/api/auth-rate-limiters.js +7 -3
  7. package/dist/api/auth-routes-mfa.js +1 -1
  8. package/dist/api/auth-routes-session.js +1 -1
  9. package/dist/api/auth-routes-shared.js +7 -3
  10. package/dist/api/auth-routes-signup.js +1 -1
  11. package/dist/api/client-ip.js +3 -1
  12. package/dist/api/extra-route.js +1 -1
  13. package/dist/api/pii-leak-guard.js +4 -2
  14. package/dist/api/readiness.js +5 -5
  15. package/dist/api/redis-sse-broker.js +8 -6
  16. package/dist/api/routes.js +2 -2
  17. package/dist/api/server.js +34 -33
  18. package/dist/api/sse-broker.js +1 -1
  19. package/dist/api/sse-route.d.ts +13 -13
  20. package/dist/api/sse-route.js +15 -15
  21. package/dist/auth/index.js +4 -4
  22. package/dist/auth/roles.d.ts +4 -4
  23. package/dist/auth/roles.js +24 -24
  24. package/dist/compliance/duration-spec.js +18 -20
  25. package/dist/compliance/index.js +2 -3
  26. package/dist/compliance/override-schema.d.ts +4 -4
  27. package/dist/compliance/override-schema.js +19 -22
  28. package/dist/compliance/profiles.d.ts +26 -29
  29. package/dist/compliance/profiles.js +62 -69
  30. package/dist/compliance/sub-processors.d.ts +28 -30
  31. package/dist/compliance/sub-processors.js +17 -21
  32. package/dist/consumer-cli.js +4 -4
  33. package/dist/crypto/blind-index.js +18 -17
  34. package/dist/crypto/ciphertext-pattern.js +1 -1
  35. package/dist/crypto/event-pii.js +3 -3
  36. package/dist/crypto/kek-source.d.ts +1 -1
  37. package/dist/crypto/kek-source.js +3 -2
  38. package/dist/crypto/kms-adapter.d.ts +1 -1
  39. package/dist/crypto/kms-adapter.js +4 -6
  40. package/dist/crypto/kms-wiring.d.ts +1 -2
  41. package/dist/crypto/kms-wiring.js +2 -2
  42. package/dist/crypto/pg-kms-adapter.js +2 -6
  43. package/dist/crypto/pii-field-encryption.js +4 -4
  44. package/dist/crypto/subject-resolver.d.ts +1 -1
  45. package/dist/crypto/subject-resolver.js +2 -2
  46. package/dist/db/apply-entity-event.d.ts +6 -6
  47. package/dist/db/apply-entity-event.js +55 -55
  48. package/dist/db/blind-index-cleanup.js +7 -7
  49. package/dist/db/collect-table-metas.js +10 -11
  50. package/dist/db/compound-types.d.ts +1 -1
  51. package/dist/db/compound-types.js +12 -12
  52. package/dist/db/cursor.js +3 -4
  53. package/dist/db/dialect.d.ts +2 -2
  54. package/dist/db/dialect.js +2 -2
  55. package/dist/db/eagerload.d.ts +3 -3
  56. package/dist/db/eagerload.js +21 -12
  57. package/dist/db/entity-table-meta.d.ts +2 -2
  58. package/dist/db/entity-table-meta.js +4 -4
  59. package/dist/db/event-store-executor-context.d.ts +1 -1
  60. package/dist/db/event-store-executor-context.js +13 -13
  61. package/dist/db/event-store-executor-read.js +27 -28
  62. package/dist/db/event-store-executor-write.js +65 -70
  63. package/dist/db/located-timestamp.d.ts +11 -11
  64. package/dist/db/located-timestamp.js +29 -30
  65. package/dist/db/migrate-generator.d.ts +1 -1
  66. package/dist/db/migrate-generator.js +26 -19
  67. package/dist/db/migrate-runner.js +25 -25
  68. package/dist/db/money.d.ts +6 -6
  69. package/dist/db/money.js +29 -27
  70. package/dist/db/queries/backfill-pii.js +5 -5
  71. package/dist/db/queries/ddl.js +1 -1
  72. package/dist/db/queries/es-ops.d.ts +1 -1
  73. package/dist/db/queries/es-ops.js +1 -1
  74. package/dist/db/queries/event-consumer.js +3 -3
  75. package/dist/db/queries/event-store.js +4 -2
  76. package/dist/db/queries/projection-rebuild.js +3 -3
  77. package/dist/db/queries/raw-sql.d.ts +2 -2
  78. package/dist/db/queries/raw-sql.js +2 -2
  79. package/dist/db/queries/shadow-swap.js +19 -19
  80. package/dist/db/query.d.ts +1 -1
  81. package/dist/db/query.js +6 -6
  82. package/dist/db/rebuild-marker.js +20 -21
  83. package/dist/db/replay-migration-sql.js +2 -2
  84. package/dist/db/schema-inspection.js +4 -5
  85. package/dist/db/table-builder.d.ts +1 -2
  86. package/dist/db/table-builder.js +82 -82
  87. package/dist/db/tenant-db.d.ts +2 -2
  88. package/dist/db/tenant-db.js +9 -2
  89. package/dist/derivatives/delete-stored-file.js +5 -2
  90. package/dist/derivatives/derivatives-context.js +1 -1
  91. package/dist/engine/boot-validator/access-roles.js +4 -3
  92. package/dist/engine/boot-validator/api-ext.js +14 -14
  93. package/dist/engine/boot-validator/config-deps.js +7 -6
  94. package/dist/engine/boot-validator/detail-screens.js +3 -3
  95. package/dist/engine/boot-validator/entity-handler.js +60 -61
  96. package/dist/engine/boot-validator/gdpr-storage.js +4 -4
  97. package/dist/engine/boot-validator/i18n-keys.js +1 -1
  98. package/dist/engine/boot-validator/index.d.ts +2 -2
  99. package/dist/engine/boot-validator/index.js +20 -20
  100. package/dist/engine/boot-validator/nav.js +5 -5
  101. package/dist/engine/boot-validator/ownership.js +12 -9
  102. package/dist/engine/boot-validator/parent-ref.js +1 -1
  103. package/dist/engine/boot-validator/pii-retention.js +50 -57
  104. package/dist/engine/boot-validator/projection-list-screens.js +7 -7
  105. package/dist/engine/boot-validator/query-refs.js +1 -1
  106. package/dist/engine/boot-validator/screens-actions.js +15 -16
  107. package/dist/engine/boot-validator/screens-forms.js +10 -11
  108. package/dist/engine/boot-validator/screens-lists.js +14 -15
  109. package/dist/engine/boot-validator/screens.js +34 -40
  110. package/dist/engine/boot-validator/security-baseline.js +3 -2
  111. package/dist/engine/boot-validator/transfer-graph.js +2 -2
  112. package/dist/engine/boot-validator/workspaces.js +3 -3
  113. package/dist/engine/bridge-stub.js +3 -4
  114. package/dist/engine/build-app-schema.js +75 -75
  115. package/dist/engine/build-config-feature-schema.js +50 -50
  116. package/dist/engine/build-target.js +20 -23
  117. package/dist/engine/codemod/pipeline-codemod.js +20 -13
  118. package/dist/engine/config-helpers.js +4 -4
  119. package/dist/engine/constants.js +2 -2
  120. package/dist/engine/create-app.js +2 -2
  121. package/dist/engine/define-feature.js +12 -11
  122. package/dist/engine/define-handler.js +12 -11
  123. package/dist/engine/define-step.js +1 -1
  124. package/dist/engine/define-workflow.d.ts +5 -5
  125. package/dist/engine/define-workflow.js +6 -6
  126. package/dist/engine/entity-handlers.js +13 -14
  127. package/dist/engine/event-def.js +2 -2
  128. package/dist/engine/extension-names.d.ts +16 -17
  129. package/dist/engine/extension-names.js +37 -38
  130. package/dist/engine/extensions/escape-hatch-usage.js +1 -1
  131. package/dist/engine/extensions/tenant-data.js +2 -2
  132. package/dist/engine/extensions/user-data.d.ts +23 -24
  133. package/dist/engine/factories.d.ts +14 -13
  134. package/dist/engine/factories.js +38 -37
  135. package/dist/engine/feature-ast/entity-field-types.js +1 -1
  136. package/dist/engine/feature-ast/extractors/handlers.js +1 -1
  137. package/dist/engine/feature-ast/extractors/hooks.js +1 -1
  138. package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
  139. package/dist/engine/feature-ast/extractors/shared.js +2 -3
  140. package/dist/engine/feature-ast/parse.js +13 -13
  141. package/dist/engine/feature-ast/patch.js +1 -1
  142. package/dist/engine/feature-ast/pattern-change-schema.js +1 -1
  143. package/dist/engine/feature-ast/patterns.d.ts +2 -2
  144. package/dist/engine/feature-ast/render.js +2 -2
  145. package/dist/engine/feature-config-events-jobs.js +15 -12
  146. package/dist/engine/feature-entity-handlers.js +9 -5
  147. package/dist/engine/feature-manifest.d.ts +9 -9
  148. package/dist/engine/feature-manifest.js +6 -7
  149. package/dist/engine/feature-ui-extensions.d.ts +3 -3
  150. package/dist/engine/feature-ui-extensions.js +30 -22
  151. package/dist/engine/field-access.js +5 -6
  152. package/dist/engine/index.d.ts +1 -0
  153. package/dist/engine/index.js +4 -4
  154. package/dist/engine/object-form.d.ts +1 -0
  155. package/dist/engine/object-form.js +14 -4
  156. package/dist/engine/ownership.js +3 -3
  157. package/dist/engine/parse-ref-target.js +5 -7
  158. package/dist/engine/pattern-library/index.js +2 -2
  159. package/dist/engine/pattern-library/library.js +2 -2
  160. package/dist/engine/pattern-library/types.js +3 -3
  161. package/dist/engine/pipeline.js +2 -2
  162. package/dist/engine/project-app-schema-for-roles.js +1 -1
  163. package/dist/engine/projection-helpers.js +9 -9
  164. package/dist/engine/qualified-name.js +2 -2
  165. package/dist/engine/registry-facade.js +1 -1
  166. package/dist/engine/registry-ingest.js +12 -12
  167. package/dist/engine/registry-state.js +17 -19
  168. package/dist/engine/registry-validate.js +12 -12
  169. package/dist/engine/run-in.js +2 -2
  170. package/dist/engine/run-pipeline.js +7 -0
  171. package/dist/engine/schema-builder.js +40 -38
  172. package/dist/engine/screen-filter-ops.js +17 -20
  173. package/dist/engine/screen-helpers.js +4 -3
  174. package/dist/engine/state-machine.d.ts +12 -12
  175. package/dist/engine/state-machine.js +7 -7
  176. package/dist/engine/steps/_resolver-utils.js +4 -3
  177. package/dist/engine/steps/for-each.js +4 -4
  178. package/dist/engine/steps/unsafe-projection-delete.js +1 -1
  179. package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
  180. package/dist/engine/steps/wait-for-event.js +4 -4
  181. package/dist/engine/steps/workflow-system-events.js +3 -3
  182. package/dist/engine/tier-resolver-extension.d.ts +29 -27
  183. package/dist/engine/tier-resolver-extension.js +26 -26
  184. package/dist/engine/transfer-adjacency.js +2 -2
  185. package/dist/engine/validate-projection-allowlist.js +7 -27
  186. package/dist/engine/walk-step-instances.d.ts +2 -0
  187. package/dist/engine/walk-step-instances.js +38 -0
  188. package/dist/engine/where-rule-lint.js +1 -1
  189. package/dist/engine/write-handler-qn-extract.js +1 -1
  190. package/dist/entrypoint/index.js +3 -3
  191. package/dist/env/index.d.ts +7 -6
  192. package/dist/env/index.js +10 -9
  193. package/dist/errors/transition-details.js +7 -7
  194. package/dist/errors/write-error-info.d.ts +6 -6
  195. package/dist/errors/write-error-info.js +6 -6
  196. package/dist/es-ops/context.d.ts +9 -9
  197. package/dist/es-ops/context.js +28 -28
  198. package/dist/es-ops/index.js +7 -9
  199. package/dist/es-ops/operations-schema.js +4 -10
  200. package/dist/es-ops/runner.d.ts +11 -11
  201. package/dist/es-ops/runner.js +49 -53
  202. package/dist/es-ops/types.d.ts +55 -55
  203. package/dist/es-ops/types.js +11 -11
  204. package/dist/event-store/admin-api.js +6 -7
  205. package/dist/event-store/errors.js +1 -1
  206. package/dist/event-store/event-store.js +2 -2
  207. package/dist/event-store/rebuild-dead-letter.js +1 -1
  208. package/dist/files/feature.js +9 -10
  209. package/dist/files/file-handle.js +5 -6
  210. package/dist/files/file-ref-entity.js +15 -15
  211. package/dist/files/file-ref-table.js +7 -8
  212. package/dist/files/file-routes.d.ts +7 -0
  213. package/dist/files/file-routes.js +11 -5
  214. package/dist/files/in-memory-provider.js +8 -9
  215. package/dist/files/local-provider.js +18 -25
  216. package/dist/files/provider-resolver.d.ts +7 -7
  217. package/dist/files/storage-tracking.js +2 -2
  218. package/dist/files/zip-stream.d.ts +12 -19
  219. package/dist/files/zip-stream.js +44 -63
  220. package/dist/i18n/required-surface-keys.d.ts +1 -1
  221. package/dist/i18n/required-surface-keys.js +8 -8
  222. package/dist/jobs/job-runner-helpers.js +8 -2
  223. package/dist/jobs/job-runner-schedulers.d.ts +1 -1
  224. package/dist/jobs/job-runner-schedulers.js +3 -4
  225. package/dist/jobs/job-runner.js +13 -15
  226. package/dist/lifecycle/lifecycle.js +3 -5
  227. package/dist/lifecycle/signal-handlers.js +5 -4
  228. package/dist/logging/utils.d.ts +2 -1
  229. package/dist/logging/utils.js +38 -10
  230. package/dist/migrations/index.js +1 -1
  231. package/dist/migrations/kumiko-drift.js +14 -13
  232. package/dist/migrations/pending-rebuilds.d.ts +21 -20
  233. package/dist/migrations/pending-rebuilds.js +27 -27
  234. package/dist/migrations/projection-table-index.d.ts +3 -3
  235. package/dist/migrations/projection-table-index.js +8 -8
  236. package/dist/observability/metric-validator.js +1 -1
  237. package/dist/observability/metrics-handle.js +3 -3
  238. package/dist/pipeline/append-event-core.js +11 -2
  239. package/dist/pipeline/cascade-handler.d.ts +1 -1
  240. package/dist/pipeline/dispatch-batch.js +2 -2
  241. package/dist/pipeline/dispatch-query.js +10 -10
  242. package/dist/pipeline/dispatch-shared.d.ts +1 -0
  243. package/dist/pipeline/dispatch-shared.js +15 -17
  244. package/dist/pipeline/dispatch-stream.js +1 -1
  245. package/dist/pipeline/dispatch-write.js +3 -3
  246. package/dist/pipeline/dispatcher-utils.d.ts +1 -1
  247. package/dist/pipeline/dispatcher-utils.js +1 -1
  248. package/dist/pipeline/event-consumer-state.js +2 -2
  249. package/dist/pipeline/event-dispatcher-delivery.js +1 -1
  250. package/dist/pipeline/event-dispatcher.js +17 -28
  251. package/dist/pipeline/irreversible-operation-gate.js +1 -1
  252. package/dist/pipeline/lifecycle-pipeline.js +9 -9
  253. package/dist/pipeline/member-read-only-transaction.js +1 -3
  254. package/dist/pipeline/msp-rebuild.js +1 -1
  255. package/dist/pipeline/projection-rebuild.js +13 -12
  256. package/dist/pipeline/system-hooks.js +39 -41
  257. package/dist/pipeline/system-identity-switch.js +1 -3
  258. package/dist/pipeline/tenant-timezone-cache.js +1 -1
  259. package/dist/random/generate.d.ts +29 -29
  260. package/dist/random/generate.js +30 -30
  261. package/dist/random/words.js +20 -20
  262. package/dist/rate-limit/middleware.js +9 -5
  263. package/dist/redis/client.js +2 -2
  264. package/dist/redis/pubsub-signal.js +14 -17
  265. package/dist/schema-cli.js +116 -22
  266. package/dist/search/index.js +4 -4
  267. package/dist/search/purge-subject.js +4 -5
  268. package/dist/search/reindex-entity.js +6 -9
  269. package/dist/time/boot-tz-warning.d.ts +2 -2
  270. package/dist/time/boot-tz-warning.js +8 -8
  271. package/dist/time/iana.js +10 -10
  272. package/dist/time/index.js +9 -9
  273. package/dist/time/legacy-date.d.ts +2 -2
  274. package/dist/time/legacy-date.js +6 -8
  275. package/dist/time/polyfill.js +1 -1
  276. package/dist/time/tz-context.d.ts +1 -1
  277. package/dist/time/tz-context.js +1 -1
  278. package/dist/ui-types/app-schema.js +7 -7
  279. package/dist/ui-types/list-row-meta.js +4 -4
  280. package/dist/upgrade-cli.js +10 -9
  281. package/dist/utils/compare.js +1 -1
  282. package/package.json +4 -4
  283. package/src/changes.json +48 -0
  284. package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
  285. package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
  286. package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
  287. package/src/scripts/codemod/pii-personal-migration.ts +17 -8
  288. package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
@@ -3,49 +3,50 @@ import type { TenantId, WriteResult } from "../engine/index.ts";
3
3
  export type EsOperationAppliedBy = "boot" | "cli" | "ci-pipeline";
4
4
  /** Default-Export einer seed-Migration-File. */
5
5
  export type SeedMigration = {
6
- /** Kurze Beschreibung was die Migration tut. Wird in kumiko_es_operations.notes
7
- * und im `ops seed:status`-Output gezeigt. */
6
+ /** A short description of what the migration does. Shown in
7
+ * kumiko_es_operations.notes and in the `ops seed:status` output. */
8
8
  readonly description: string;
9
- /** Optional: skippe diesen Seed wenn die env-var
10
- * `KUMIKO_SKIP_ES_OPS_<sanitized-filename>=1` gesetzt ist (für Recovery /
11
- * Debug-Boots). Default false = always-run-pending. */
9
+ /** Optional: skip this seed when the env var
10
+ * `KUMIKO_SKIP_ES_OPS_<sanitized-filename>=1` is set (for recovery or
11
+ * debug boots). Default false = always-run-pending. */
12
12
  readonly skippable?: boolean;
13
- /** Hauptarbeit. ctx liefert systemWriteAs (Event-Store-konformer Pfad,
14
- * bypassed Access-Checks via system-user) plus Read-Helpers.
13
+ /** Main migration work. ctx provides systemWriteAs (the Event Store-compliant
14
+ * path, bypassing access checks through the system user) plus read helpers.
15
15
  *
16
- * Throws → Marker NICHT geschrieben, App-Boot bricht ab, Retry bei
17
- * nächstem Boot. Pro-Migration eigene Transaction; ein Failure stoppt
18
- * alle nachfolgenden pending-Migrations (Order-Erhalt). */
16
+ * If this throws, the marker is NOT written, app boot aborts, and the
17
+ * migration is retried on the next boot. Each migration has its own
18
+ * transaction; a failure stops all subsequent pending migrations, preserving
19
+ * their order. */
19
20
  readonly run: (ctx: SeedMigrationContext) => Promise<void>;
20
21
  };
21
- /** Read-shape eines User-Eintrags wie an Seed-Helpers exposed.
22
- * Schmaler Subset von AuthUserRow — Seeds brauchen typischerweise nur
23
- * diese Felder zur Identifikation. */
22
+ /** Read shape of a user entry exposed to seed helpers.
23
+ * A narrow subset of AuthUserRow, since seeds typically need only
24
+ * these fields for identification. */
24
25
  export type SeedUserRow = {
25
26
  readonly id: string;
26
27
  readonly email: string;
27
28
  readonly tenantId: string;
28
29
  };
29
- /** Read-shape eines Membership-Eintrags wie an Seed-Helpers exposed.
30
- * Unterscheidet zwei tenantIds: die "logische" aus dem Read-Projektion
31
- * (`tenantId`) und die "physische" aus dem Aggregate-Stream
32
- * (`streamTenantId`). Die beiden weichen voneinander ab wenn das
33
- * Aggregate von einem Executor mit anderer tenantId angelegt wurde
34
- * (z.B. seedTenantMembership-by=systemAdmin) — typischer
35
- * publicstatus-Driver-Use-Case. */
30
+ /** Read shape of a membership entry exposed to seed helpers.
31
+ * Distinguishes two tenantIds: the logical one from the read projection
32
+ * (`tenantId`) and the physical one from the aggregate stream
33
+ * (`streamTenantId`). They differ when the aggregate was created by an
34
+ * executor with a different tenantId (for example,
35
+ * seedTenantMembership-by=systemAdmin), which is a typical
36
+ * publicstatus driver use case. */
36
37
  export type SeedMembershipRow = {
37
38
  readonly userId: string;
38
- /** Payload-tenant aus `read_tenant_memberships.tenant_id`. Geht ins
39
- * write-payload als `tenantId`. */
39
+ /** Payload tenant from `read_tenant_memberships.tenant_id`; it is used
40
+ * as `tenantId` in the write payload. */
40
41
  readonly tenantId: string;
41
- /** Stream-tenant aus `kumiko_events.tenant_id` der v1-Row. MUSS als
42
- * `tenantIdOverride` an `systemWriteAs` durchgereicht werden, sonst
43
- * sucht der Event-Store-Executor den Stream im falschen Tenant und
44
- * liefert `version_conflict`. */
42
+ /** Stream tenant from `kumiko_events.tenant_id` in the v1 row. MUST be
43
+ * passed as `tenantIdOverride` to `systemWriteAs`; otherwise the Event
44
+ * Store executor looks for the stream in the wrong tenant and returns
45
+ * `version_conflict`. */
45
46
  readonly streamTenantId: string;
46
47
  readonly roles: readonly string[];
47
48
  };
48
- /** Read-shape eines Tenant-Eintrags wie an Seed-Helpers exposed. */
49
+ /** Read shape of a tenant entry exposed to seed helpers. */
49
50
  export type SeedTenantRow = {
50
51
  readonly id: string;
51
52
  readonly name: string;
@@ -69,34 +70,32 @@ export type SeedTemplateResourceFilter = {
69
70
  readonly locale?: string;
70
71
  };
71
72
  export type SeedMigrationContext = {
72
- /** Event-Store-konformer Write via existing write-handler. System-User
73
- * als Executor bypassed Access-Check (Standard-Seed-Pattern). Events
74
- * haben inserted_by_id = SYSTEM_TENANT_ID-User → audit-fähig.
73
+ /** Event Store-compliant write through the existing write handler. The system
74
+ * user executes it while bypassing access checks. Events have
75
+ * inserted_by_id = SYSTEM_TENANT_ID-user, making them auditable.
75
76
  *
76
- * Typ-Signatur folgt existing ctx.writeAs (payload als unknown) — Type-
77
- * Safety kommt über handler-spezifische Wrapper im Aufrufer ("ich weiß
78
- * was updateMemberRoles braucht"). Versucht NICHT Generic-Magic.
77
+ * The type signature follows existing ctx.writeAs (with payload as unknown).
78
+ * Type safety comes from handler-specific wrappers in the caller; do not
79
+ * attempt generic magic.
79
80
  *
80
- * **tenantIdOverride (Phase 1.5):** wenn das Ziel-Aggregate in einem
81
- * spezifischen Tenant-Stream lebt (nicht SYSTEM_TENANT_ID, was Default
82
- * ist), MUSS der Caller die Stream-tenantId mitgeben — sonst sucht der
83
- * Event-Store-Executor den Aggregate-Stream gegen `SYSTEM_TENANT_ID`
84
- * und liefert `version_conflict` (siehe Memory
85
- * `feedback_event_store_tenant_consistency.md` + Driver-Use-Case
86
- * publicstatus-admin-roles in `project_es_ops_phase1_retro.md`).
81
+ * **tenantIdOverride:** when the target aggregate lives in a specific
82
+ * tenant stream (rather than SYSTEM_TENANT_ID, the default), the caller
83
+ * MUST provide the stream tenantId. Otherwise the Event Store executor
84
+ * looks for the aggregate stream under `SYSTEM_TENANT_ID` and returns
85
+ * `version_conflict`.
87
86
  *
88
- * Typische Pattern:
89
- * - System-scope-Aggregate (config-values, system text-content) →
90
- * tenantIdOverride weglassen (Default SYSTEM_TENANT_ID).
91
- * - Tenant-scope-Aggregate (memberships, tenant-config, app-data) →
92
- * `tenantIdOverride: m.tenantId` (oder den Stream-Tenant aus
93
- * einem find*-Helper).
87
+ * Typical patterns:
88
+ * - System-scope aggregates (config-values, system text-content): omit
89
+ * tenantIdOverride (default SYSTEM_TENANT_ID).
90
+ * - Tenant-scope aggregates (memberships, tenant-config, app-data): use
91
+ * `tenantIdOverride: m.tenantId` or the stream tenant from a find*
92
+ * helper.
94
93
  *
95
- * **extraRoles:** hasAccess has no system-bypass — a handler with an
96
- * explicit role gate (e.g. `access: { roles: ["SystemAdmin"] }` or
97
- * `["anonymous"]`) otherwise rejects the bare `system` actor with
98
- * `access_denied`. Pass the required role(s) here — see
99
- * `createSystemUser`'s `extraRoles` doc. */
94
+ * **extraRoles:** hasAccess has no system bypass. A handler with an
95
+ * explicit role gate (for example, `access: { roles: ["SystemAdmin"] }` or
96
+ * `["anonymous"]`) otherwise rejects the bare `system` actor with
97
+ * `access_denied`. Pass the required role(s) here; see
98
+ * `createSystemUser`'s `extraRoles` documentation. */
100
99
  readonly systemWriteAs: (handlerQualifiedName: string, payload: unknown, tenantIdOverride?: TenantId, extraRoles?: readonly string[]) => Promise<WriteResult>;
101
100
  readonly findUserByEmail: (email: string) => Promise<SeedUserRow | null>;
102
101
  readonly findMembershipsOfUser: (userId: string) => Promise<readonly SeedMembershipRow[]>;
@@ -104,9 +103,10 @@ export type SeedMigrationContext = {
104
103
  /** Template rows ordered by slug, locale. Empty list when the
105
104
  * template-resolver feature (its table) is not present. */
106
105
  readonly findTemplateResources: (filter?: SeedTemplateResourceFilter) => Promise<readonly SeedTemplateResourceRow[]>;
107
- /** Escape-Hatch — direkter DB-Zugang. Nur für READ-only Lookups die der
108
- * Context nicht standard-mäßig anbietet. WRITES via systemWriteAs!
109
- * Type ist DbRunner (Connection oder aktive Tx) weil der Runner den
110
- * Context pro-Migration im Tx-Scope erzeugt. */
106
+ /** Escape hatch for direct database access. Use only for READ-only lookups
107
+ * that the context does not provide by default; WRITES must use
108
+ * systemWriteAs. The type is DbRunner (a connection or active transaction)
109
+ * because the runner creates the context within each migration's
110
+ * transaction scope. */
111
111
  readonly db: DbRunner;
112
112
  };
@@ -1,14 +1,14 @@
1
- // Public API für seed-migrations. Files in /seeds/<date>-<slug>.ts
2
- // exportieren ein default-Object dieses Typs. Runner ruft `run(ctx)` in
3
- // chronologischer Reihenfolge auf, einmal pro App-Boot (track in
4
- // kumiko_es_operations).
1
+ // Public API for seed migrations. Files in /seeds/<date>-<slug>.ts
2
+ // export a default object of this type. The runner calls `run(ctx)` in
3
+ // chronological order, once per app boot, tracking runs in
4
+ // kumiko_es_operations.
5
5
  //
6
- // API-Stabilität: Phase 1 ist "minimal-viable". Helper am Context
7
- // (findUserByEmail, etc.) wachsen on-demand — wenn eine real-Migration
8
- // einen Lookup braucht den der Context nicht anbietet, fügen wir ihn
9
- // dort hinzu. Das vermeidet "wir-bauen-alle-möglichen-Helper-spekulativ".
6
+ // API stability: the initial API is intentionally minimal. Context helpers
7
+ // (findUserByEmail, etc.) grow on demand: when a real migration needs a
8
+ // lookup the context does not provide, add it there. This avoids speculative
9
+ // helper APIs.
10
10
  //
11
- // ctx.db als Escape-Hatch ist erlaubt für READ-only lookups. WRITES
12
- // IMMER via ctx.systemWriteAs damit Event-Store-Invariants bleiben
13
- // (Source-of-Truth + Projection läuft automatisch).
11
+ // ctx.db is permitted as an escape hatch for READ-only lookups. WRITES must
12
+ // always go through ctx.systemWriteAs so Event Store invariants remain intact
13
+ // (source of truth plus automatic projection).
14
14
  export {};
@@ -1,12 +1,11 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
- // Event-Store Admin-API — Marten-Bypass für Legacy-Daten-Importe.
3
- // Spec + Verhalten: docs/plans/features/event-store-admin-api.md
2
+ // Event-store admin API — bypasses Marten for legacy-data imports.
3
+ // Specification and behavior: docs/plans/features/event-store-admin-api.md
4
4
  //
5
- // Guard-Rail: dieses Modul ist NICHT aus event-store/index.ts re-exportiert.
6
- // Import nur via deep-path `@cosmicdrift/kumiko-framework/event-store/admin-api`. Das
7
- // Guard-Script `scripts/guard-admin-api.ts` blockt Aufrufe aus App-Code —
8
- // Allowlist: samples/*/migration/, scripts/migrations/, die Definition
9
- // selbst, das Guard-Script selbst.
5
+ // Guard rail: this module is NOT re-exported from event-store/index.ts. Import only via the deep
6
+ // path `@cosmicdrift/kumiko-framework/event-store/admin-api`. The guard script
7
+ // `scripts/guard-admin-api.ts` blocks calls from application code. Allowlist: samples/*/migration/,
8
+ // scripts/migrations/, this definition, and the guard script itself.
10
9
  import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
11
10
  import { claimXactId, runAtomicEventWrite } from "../db/queries/event-store.js";
12
11
  import { eventPredecessorExists, findExistingEventVersion, insertRawEventBatch, insertRawFirstEvent, insertRawSubsequentEvent, } from "../db/queries/event-store-admin.js";
@@ -3,7 +3,7 @@
3
3
  // errors so the executor layer can map them to the framework's
4
4
  // WriteResult error contract (version_conflict).
5
5
  //
6
- // These live here and not in kumiko-types (#1629): `instanceof` needs a
6
+ // These live here and not in kumiko-types: `instanceof` needs a
7
7
  // single copy of the class, which forced kumiko-types to be a peerDependency
8
8
  // and made every minor changeset resolve to a major bump.
9
9
  export class VersionConflictError extends Error {
@@ -35,7 +35,7 @@ import { currentSeedCreatedAt } from "./seed-clock.js";
35
35
  // by TCP round-trip instead of pollIntervalMs.
36
36
  export const EVENTS_PUBSUB_CHANNEL = "kumiko_events_new";
37
37
  export async function append(db, event) {
38
- // Event-PII (#799): stored payload AND returned echo carry ciphertext, so
38
+ // Event-PII: stored payload AND returned echo carry ciphertext, so
39
39
  // inline projections and rebuilds materialize identical rows.
40
40
  const payload = await encryptEventPayloadPii(event.type, event.payload, {
41
41
  tenantId: event.tenantId,
@@ -80,7 +80,7 @@ export async function append(db, event) {
80
80
  throw e;
81
81
  }
82
82
  }
83
- // #3043 — attribution is derived from the execution scope, never taken from
83
+ // Attribution is derived from the execution scope, never taken from
84
84
  // the caller: a passed-in value can lie about who wrote the row, a derived
85
85
  // one cannot. Hence overwrite rather than merge. appendRaw/appendRawBatch
86
86
  // bypass this deliberately — they replay historical rows verbatim.
@@ -5,7 +5,7 @@
5
5
  // whose (possibly years-old) payload makes its apply throw rolls the whole
6
6
  // replay back — the projection stays permanently un-rebuildable until the
7
7
  // event is repaired by hand. The upcaster dead-letter only quarantines
8
- // upcast-TRANSFORM failures, not apply failures (#760).
8
+ // upcast-TRANSFORM failures, not apply failures.
9
9
  //
10
10
  // Quarantine mode (errorPolicy.skipApplyErrors on RebuildDeps, or
11
11
  // MspErrorMode.rebuild.skipApplyErrors on the MSP definition) confines each
@@ -4,17 +4,16 @@ import { FILE_REF_ENTITY_NAME, fileRefEntity } from "./file-ref-entity.js";
4
4
  import { DEFAULT_UPLOAD_SWEEP_GRACE_HOURS, MIN_UPLOAD_SWEEP_GRACE_HOURS, sweepOrphanedUploads, } from "./sweep-orphaned-uploads.js";
5
5
  import { fileUploadLedgerTable } from "./upload-ledger.js";
6
6
  export { fileRefEntity } from "./file-ref-entity.js";
7
- // files — `fileRef` als ganz normales ES-Entity. Upload/Delete laufen über
8
- // den Standard-Entity-Executor (file-routes.ts: executor.create/delete →
9
- // `fileRef.created`/`fileRef.deleted` → applyEntityEvent materialisiert
10
- // `file_refs`). Soft-Delete/Anonymize/Restore/Retention kommen damit
11
- // generisch aus dem Entity-Lifecycle + data-retention — keine
12
- // file-spezifische Lösch-Logik.
7
+ // `fileRef` is a regular ES entity. Uploads and deletes go through the standard
8
+ // entity executor (file-routes.ts: executor.create/delete → `fileRef.created` /
9
+ // `fileRef.deleted` → applyEntityEvent materializes `file_refs`), so soft-delete,
10
+ // anonymization, restore and retention come generically from the entity lifecycle
11
+ // and data-retention, with no file-specific deletion logic.
13
12
  //
14
- // `r.entity` macht die Tabelle für PII-Boot-Validation +
15
- // userData/tenantData-Extensions sichtbar und registriert die implizite
16
- // Entity-Projektion (für rebuildProjection). Liegt im Framework neben
17
- // file-routes + fileRefsTable; bundled-features/files re-exportiert nur.
13
+ // `r.entity` exposes the table to PII boot validation and the userData/tenantData
14
+ // extensions, and registers the implicit entity projection used by
15
+ // rebuildProjection. It lives in the framework next to file-routes and
16
+ // fileRefsTable; bundled-features/files only re-exports it.
18
17
  export const FILES_FEATURE = "files";
19
18
  function assertValidSweepGraceHours(hours) {
20
19
  if (!Number.isInteger(hours) || hours < MIN_UPLOAD_SWEEP_GRACE_HOURS) {
@@ -121,12 +121,11 @@ export function deriveKey(key, suffix) {
121
121
  const { stem, ext } = splitKeyExtension(key);
122
122
  return ext === "" ? `${key}.${suffix}` : `${stem}.${suffix}${ext}`;
123
123
  }
124
- // The prefix that covers a key AND every `derive()`d variant of it —
125
- // `foo/bar.jpg` → `foo/bar.`, matching `foo/bar.jpg` and `foo/bar.medium.jpg`
126
- // alike, since derive() only ever inserts a suffix between the stem and the
127
- // extension. Used by the tenant-handover destroy sweep (kumiko-framework#3035)
128
- // to reach a moved fileRef's original plus every already-rendered derivative,
129
- // none of which have their own `file_refs` row.
124
+ // The prefix that covers a key AND every `derive()`d variant of it — `foo/bar.jpg` → `foo/bar.`,
125
+ // matching `foo/bar.jpg` and `foo/bar.medium.jpg` alike, since derive() only ever inserts a suffix
126
+ // between the stem and the extension. Used by the tenant-handover destroy sweep to reach a moved
127
+ // fileRef's original plus every already-rendered derivative, none of which have their own
128
+ // `file_refs` row.
130
129
  export function storageKeyStemPrefix(key) {
131
130
  const { stem, ext } = splitKeyExtension(key);
132
131
  return ext === "" ? `${key}.` : `${stem}.`;
@@ -1,22 +1,22 @@
1
1
  import { createBigIntField, createEntity, createTextField } from "../engine/index.js";
2
- // fileRef — das File-Metadata-Entity. Ganz normales ES-Entity: Upload/Delete
3
- // laufen über den Standard-Executor (file-routes.ts), die Tabelle `file_refs`
4
- // wird via buildEntityTable aus dieser Definition gebaut.
2
+ // fileRef — the file-metadata entity. A standard ES entity: uploads/deletes
3
+ // run through the standard executor (file-routes.ts), and the `file_refs` table
4
+ // is built from this definition via buildEntityTable.
5
5
  //
6
- // softDelete: true — wie das `user`-Entity + die data-retention-Strategien.
7
- // Ein Delete markiert `isDeleted=true` (wiederherstellbar, kein "sofort weg");
8
- // echtes Erasure (Art. 17) läuft über den Forget-Hook + Retention-Cleanup.
6
+ // softDelete: true — as with the `user` entity and the data-retention strategies.
7
+ // A delete sets `isDeleted=true` (recoverable, not "gone immediately"); actual
8
+ // erasure (Art. 17) runs through the Forget hook and retention cleanup.
9
9
  //
10
- // `insertedAt`/`insertedById` sind framework-managed base columns (siehe
11
- // buildBaseColumns in table-builder.ts) und dürfen NICHT als Entity-Fields
12
- // dupliziert werden — fieldColumns gewinnen beim Merge, und die Field-Variante
13
- // ohne `.default(now()).notNull()` macht inserted_at still nullable.
10
+ // `insertedAt`/`insertedById` are framework-managed base columns (see
11
+ // buildBaseColumns in table-builder.ts) and must NOT be duplicated as entity
12
+ // fields — fieldColumns win during the merge, and the field variant without
13
+ // `.default(now()).notNull()` silently makes inserted_at nullable.
14
14
  //
15
- // PII-Annotations:
16
- // - fileName → personal: "self" (original filename often carries a personal
17
- // reference: "Marc-Lebenslauf.pdf", "Krankheitsattest-Mai.pdf"). The other
18
- // fields (storageKey, mimeType, entityType, entityId, fieldName) are
19
- // technical references or system metadata, not subject content.
15
+ // PII annotations:
16
+ // - fileName → personal: "self" (the original filename often carries a
17
+ // personal reference: "Marc-Lebenslauf.pdf", "Krankheitsattest-Mai.pdf").
18
+ // The other fields (storageKey, mimeType, entityType, entityId, fieldName)
19
+ // are technical references or system metadata, not subject content.
20
20
  export { FILE_REF_ENTITY_NAME } from "./types.js";
21
21
  export const fileRefEntity = createEntity({
22
22
  table: "file_refs",
@@ -1,12 +1,11 @@
1
1
  import { buildEntityTable } from "../db/table-builder.js";
2
2
  import { FILE_REF_ENTITY_NAME, fileRefEntity } from "./file-ref-entity.js";
3
- // `file_refs` ist die read-table des `fileRef`-Entity. Aus der Entity-
4
- // Definition gebaut (kein hand-gepflegtes pgTable mehr), damit die implizite
5
- // Entity-Projektion (Rebuild) und der Live-Executor-Write spaltengleich auf
6
- // dieselbe Tabelle schreiben — Single Source, keine Dual-Definition-Drift.
3
+ // `file_refs` is the read table for the `fileRef` entity. It is built from the entity
4
+ // definition rather than a manually maintained `pgTable`, so the implicit entity
5
+ // projection (rebuild) and live executor writes use the same columns and table—one
6
+ // source of truth, with no drift between duplicate definitions.
7
7
  //
8
- // `id` ist UUID und doppelt als Aggregate-Id des fileRef-Event-Streams:
9
- // jeder Upload appended `fileRef.created`, jeder Delete `fileRef.deleted`
10
- // (Standard-Entity-Auto-Verben). UUIDs schließen die Enumeration-Attacke
11
- // auf /files/:id.
8
+ // `id` is a UUID and also serves as the aggregate ID for the `fileRef` event stream:
9
+ // each upload appends `fileRef.created`, and each deletion appends `fileRef.deleted`
10
+ // (standard entity auto-verbs). UUIDs prevent enumeration attacks against /files/:id.
12
11
  export const fileRefsTable = buildEntityTable(FILE_REF_ENTITY_NAME, fileRefEntity);
@@ -1,9 +1,15 @@
1
+ import type { WriteErrorInfo } from "@cosmicdrift/kumiko-types/write-error-info-types";
1
2
  import { Hono } from "hono";
2
3
  import type { DbConnection } from "../db/connection.ts";
3
4
  import { type Registry, type SessionUser, type TenantId } from "../engine/types/index.ts";
4
5
  import type { FileProviderResolver } from "./provider-resolver.ts";
5
6
  import type { RecordReadCheck } from "./record-read-check.ts";
6
7
  import { type RecordWriteCheck } from "./record-write-check.ts";
8
+ declare const ERROR_STATUSES: readonly [400, 401, 403, 404, 409, 422, 429, 500, 503, 504];
9
+ export declare function deleteFailureResponseFor(error: WriteErrorInfo): {
10
+ readonly status: (typeof ERROR_STATUSES)[number];
11
+ readonly code: string;
12
+ };
7
13
  export type FileAccessDecision = "allow" | "deny";
8
14
  export type FileRef = {
9
15
  id: string;
@@ -36,3 +42,4 @@ export type FileRoutesOptions = {
36
42
  export declare function resolveMaxUploadBodyBytes(options: Pick<FileRoutesOptions, "registry" | "maxUploadSize">): number;
37
43
  export declare function readFilesRouteOptions(registry: Registry): Pick<FileRoutesOptions, "accessGuard" | "privilegedRoles" | "unclaimedUploadReaders" | "maxUploadSize">;
38
44
  export declare function createFileRoutes(options: FileRoutesOptions): Hono;
45
+ export {};
@@ -1,3 +1,4 @@
1
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
1
2
  import { Hono } from "hono";
2
3
  import { bodyLimit } from "hono/body-limit";
3
4
  import { getUser } from "../api/auth-middleware.js";
@@ -18,6 +19,12 @@ import { RecordWriteDecisions } from "./record-write-check.js";
18
19
  import { neutralFileNameForMimeType } from "./resolve-content-type.js";
19
20
  import { parseMaxSize, resolveServedContentType } from "./types.js";
20
21
  import { createFileUploadLedger } from "./upload-ledger.js";
22
+ // Exactly the httpStatus values declared by KumikoError subclasses across packages/*.
23
+ const ERROR_STATUSES = [400, 401, 403, 404, 409, 422, 429, 500, 503, 504];
24
+ export function deleteFailureResponseFor(error) {
25
+ const status = isOneOf(ERROR_STATUSES, error.httpStatus) ? error.httpStatus : 500;
26
+ return { status, code: status === 404 ? "not_found" : error.code };
27
+ }
21
28
  const DEFAULT_PRIVILEGED_ROLES = ["Admin", "SystemAdmin"];
22
29
  // 15 minutes — long enough for a download to start, short enough that a
23
30
  // leaked URL (e.g. from a browser history screenshot) isn't a long-lived
@@ -281,7 +288,7 @@ export function createFileRoutes(options) {
281
288
  // GET /files/:id/variant/:name — a named derived version of the file.
282
289
  // Same tenant + guard gate as the download above; the original stays
283
290
  // reachable only through that route. The public, anonymous counterpart is
284
- // a separate cut (#1951) and does not go through createFileRoutes.
291
+ // a separate cut and does not go through createFileRoutes.
285
292
  api.get("/files/:id/variant/:name", async (c) => {
286
293
  const user = getUser(c);
287
294
  const id = c.req.param("id");
@@ -358,9 +365,8 @@ export function createFileRoutes(options) {
358
365
  // pattern from the access-deny path above. Everything else (version
359
366
  // conflict 409, ownership 403, validation 422, internal 500) gets the
360
367
  // real status off the error so callers can distinguish recoverable
361
- // from terminal — pauschales 404 hier hat genau das verschleiert.
362
- const status = result.error.httpStatus;
363
- const code = status === 404 ? "not_found" : result.error.code;
368
+ // from terminal — a blanket 404 here had obscured exactly that.
369
+ const { status, code } = deleteFailureResponseFor(result.error);
364
370
  return c.json({ error: code }, status);
365
371
  }
366
372
  return c.json({ ok: true });
@@ -411,7 +417,7 @@ export function createFileRoutes(options) {
411
417
  return c.json({ url, expiresAt });
412
418
  });
413
419
  // Metadata without the bytes. Unlike the byte-serving routes, this returns
414
- // fileName as JSON — and fileName is `personal: "self"` (fw#2423), so it must
420
+ // fileName as JSON — and fileName is `personal: "self"`, so it must
415
421
  // go through the entity executor's detail() to decrypt instead of
416
422
  // loadFileForTenant's raw, still-encrypted read.
417
423
  async function loadReadableFileMeta(id, user) {
@@ -12,9 +12,8 @@ export function createInMemoryFileProvider() {
12
12
  store.set(key, { data: new Uint8Array(data), mimeType });
13
13
  },
14
14
  async writeStream(key, source, options) {
15
- // In-Memory hat kein "Streaming" im real-physikalischen Sinn —
16
- // wir collecten chunks in einen Uint8Array. Test-Tauglich, kein
17
- // Production-Pfad.
15
+ // In-memory storage does not provide real streaming; it collects chunks
16
+ // into a Uint8Array. Suitable for tests, not a production path.
18
17
  const chunks = [];
19
18
  let total = 0;
20
19
  for await (const chunk of source) {
@@ -36,12 +35,12 @@ export function createInMemoryFileProvider() {
36
35
  return new Uint8Array(entry.data);
37
36
  },
38
37
  readStream(key) {
39
- // In-Memory hat technisch keine Chunks, aber das Surface muss
40
- // identisch zu echten Streamern (Local/S3) sein damit Test-Code
41
- // den Pfad genauso geht. Wir yielden die Bytes als single-chunk.
42
- // Map-lookup ist O(1), also kein Verlust durch eager-resolve.
43
- // Throw passiert beim ersten chunk-pull — gleicher Lazy-Pattern
44
- // wie S3 (request abgesetzt beim ersten Iterator-Step).
38
+ // In-memory storage has no chunks, but the surface must match real
39
+ // streamers (Local/S3) so test code exercises the same path. Yield the
40
+ // bytes as a single chunk.
41
+ // The O(1) map lookup avoids a cost from eager resolution.
42
+ // Errors are thrown on the first chunk pull, matching S3's lazy pattern
43
+ // (the request starts on the first iterator step).
45
44
  const entry = store.get(key);
46
45
  const captured = entry ? new Uint8Array(entry.data) : null;
47
46
  return {
@@ -33,16 +33,12 @@ export function createLocalProvider(basePath) {
33
33
  await writeFile(filePath, data);
34
34
  },
35
35
  async writeStream(key, source, _options) {
36
- // Atomar via tmp-File + rename: ein Reader der den finalen Pfad
37
- // sieht, sieht entweder die alte Version (falls vorhanden) oder
38
- // die vollstaendige neue. Niemals einen halb-fertigen Stream.
39
- // Falls der Stream mid-write bricht, ist das tmp-Cleanup
40
- // best-effort — je nach OS-Race im stream-destroy-Pfad kann
41
- // das `.tmp`-File kurz liegen bleiben. Kein Korrektheits-
42
- // Problem (kein Reader sucht `*.tmp`-Patterns), nur Operations-
43
- // Hygiene; ein periodischer cron-cleanup auf alten `.tmp`-Files
44
- // ist die saubere Loesung wenn das in Production realistisch
45
- // greift.
36
+ // Atomic via a temporary file and rename: readers see either the previous version (if any) or
37
+ // the complete new version, never a partially written stream. If the stream fails during
38
+ // writing, temporary-file cleanup is best-effort; an OS race in stream destruction can leave
39
+ // the `.tmp` file briefly. This affects operational hygiene only, because readers never
40
+ // search `*.tmp` patterns. A periodic cleanup of old `.tmp` files is appropriate if this runs
41
+ // in production.
46
42
  const filePath = resolveContainedPath(key);
47
43
  const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
48
44
  await mkdir(dirname(filePath), { recursive: true });
@@ -51,8 +47,8 @@ export function createLocalProvider(basePath) {
51
47
  await rename(tmpPath, filePath);
52
48
  }
53
49
  catch (e) {
54
- // Best-effort tmp-Cleanup; wenn das auch failt, hat der
55
- // Filesystem ein Problem das nicht in unserem Scope liegt.
50
+ // Best-effort temporary-file cleanup; if that also fails, the filesystem problem is outside
51
+ // this component's scope.
56
52
  await unlink(tmpPath).catch(() => { });
57
53
  throw e;
58
54
  }
@@ -62,24 +58,21 @@ export function createLocalProvider(basePath) {
62
58
  return readFile(filePath);
63
59
  },
64
60
  readStream(key) {
65
- // node:fs createReadStream ist ein AsyncIterable<Buffer>; Buffer
66
- // extends Uint8Array. Wir bauen einen kleinen Adapter weil
67
- // @types/node das asyncIterator als AsyncIterableIterator<any>
68
- // typt — Adapter sichert die Surface auf Uint8Array.
69
- // Default-highWaterMark = 64KB Chunks, was fuer ZIP-Stream-Konsum
70
- // gut ist. Errors landen im for-await-Loop des Konsumenten
71
- // (z.B. ENOENT bei Missing-File faellt erst beim ersten chunk-
72
- // pull, nicht beim readStream-Aufruf — gleiches Lazy-Verhalten
73
- // wie inmemory + S3).
61
+ // node:fs createReadStream is an AsyncIterable<Buffer>; Buffer extends Uint8Array. This
62
+ // adapter preserves a Uint8Array surface because @types/node types the async iterator as
63
+ // AsyncIterableIterator<any>. The default highWaterMark produces 64KB chunks, which suits ZIP
64
+ // stream consumption. Errors reach the consumer's for-await loop; for example, a missing file
65
+ // raises ENOENT on the first chunk pull rather than when readStream is called, matching the
66
+ // lazy behavior of in-memory and S3 providers.
74
67
  const filePath = resolveContainedPath(key);
75
68
  const stream = createReadStream(filePath);
76
69
  return {
77
70
  async *[Symbol.asyncIterator]() {
78
71
  for await (const chunk of stream) {
79
- // Stream ohne encoding liefert Buffer. Buffer extends Uint8Array,
80
- // aber @types/node typt asyncIterator als string|Buffer. View
81
- // ohne copy auf dasselbe ArrayBuffer; runtime-check schliesst
82
- // den string-Branch aus (Stream wurde nicht mit encoding= gesetzt).
72
+ // A stream without an encoding yields Buffer. Buffer extends Uint8Array, but
73
+ // @types/node types the async iterator as string|Buffer. Take a zero-copy view
74
+ // onto the same ArrayBuffer; the runtime check rules out the string branch
75
+ // because the stream was not opened with an encoding.
83
76
  if (typeof chunk === "string") {
84
77
  throw new Error("local-provider readStream: unexpected string chunk (encoding leaked)");
85
78
  }
@@ -36,13 +36,13 @@ export type FileProviderContext = {
36
36
  * file-provider-inmemory, ...) registers an implementation via
37
37
  * `r.useExtension(EXT_FILE_PROVIDER, "<name>", { build })`.
38
38
  *
39
- * **Plugin-Author-Warnung:** `ctx` ist EXPLIZIT ein FileProviderContext, nicht
40
- * ein voller HandlerContext. Felder ausserhalb der schmalen Surface (z.B.
41
- * `ctx.tx`, `ctx.actor`, `ctx.signal`) sind im Worker-Pfad NICHT vorhanden.
42
- * Cast `ctx as unknown as HandlerContext` fliegt zur Runtime im Worker — der
43
- * Crash kommt erst in production mit dem ersten S3-Tenant. Braucht ein Plugin
44
- * Felder ausserhalb FileProviderContext: lieber FileProviderContext explizit
45
- * erweitern (sichtbarer breaking change) als ctx-cast.
39
+ * **Plugin-Author-Warning:** `ctx` is EXPLICITLY a FileProviderContext, not
40
+ * a full HandlerContext. Fields outside the narrow surface (e.g.
41
+ * `ctx.tx`, `ctx.actor`, `ctx.signal`) are NOT available on the worker path.
42
+ * Casting `ctx as unknown as HandlerContext` fails at runtime on the worker —
43
+ * the crash only appears in production with the first S3 tenant. If a plugin
44
+ * needs fields outside FileProviderContext, extend FileProviderContext
45
+ * explicitly (a visible breaking change) rather than casting `ctx`.
46
46
  */
47
47
  export type FileProviderPlugin = {
48
48
  readonly build: (ctx: FileProviderContext, tenantId: string) => Promise<FileStorageProvider>;
@@ -1,7 +1,7 @@
1
1
  // Tenant storage usage — counts bytes + files per tenant from the event log.
2
2
  //
3
- // Tracking-only for Phase 1: no hard limit, no upload gatekeeping. Apps read
4
- // the row to decide what to do (show a warning, soft-throttle, bill, …).
3
+ // Tracking-only: no hard limit, no upload gatekeeping. Apps read the row to
4
+ // decide what to do (show a warning, soft-throttle, bill, …).
5
5
  // Enforcement is a conscious deferred call — we want production numbers
6
6
  // before picking thresholds (see "Storage tracking: counted now, enforced
7
7
  // later" in core-files.md).