@cosmicdrift/kumiko-framework 0.13.0 → 0.15.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 (314) hide show
  1. package/package.json +7 -7
  2. package/src/__tests__/{anonymous-access.integration.ts → anonymous-access.integration.test.ts} +12 -9
  3. package/src/__tests__/{error-contract.integration.ts → error-contract.integration.test.ts} +5 -4
  4. package/src/__tests__/{field-access.integration.ts → field-access.integration.test.ts} +3 -3
  5. package/src/__tests__/{full-stack.integration.ts → full-stack.integration.test.ts} +7 -16
  6. package/src/__tests__/{ownership.integration.ts → ownership.integration.test.ts} +3 -2
  7. package/src/__tests__/{raw-table.integration.ts → raw-table.integration.test.ts} +18 -30
  8. package/src/__tests__/{reference-data.integration.ts → reference-data.integration.test.ts} +24 -11
  9. package/src/__tests__/{transition-guard.integration.ts → transition-guard.integration.test.ts} +12 -10
  10. package/src/api/__tests__/api.test.ts +1 -1
  11. package/src/api/__tests__/auth-middleware-transport.test.ts +1 -1
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +1 -1
  13. package/src/api/__tests__/{batch.integration.ts → batch.integration.test.ts} +30 -30
  14. package/src/api/__tests__/body-limit.test.ts +1 -1
  15. package/src/api/__tests__/csrf-middleware.test.ts +1 -1
  16. package/src/api/__tests__/{dispatcher-live.integration.ts → dispatcher-live.integration.test.ts} +10 -9
  17. package/src/api/__tests__/metrics-endpoint.test.ts +1 -1
  18. package/src/api/__tests__/{nested-write.integration.ts → nested-write.integration.test.ts} +13 -16
  19. package/src/api/__tests__/readiness.test.ts +1 -1
  20. package/src/api/__tests__/request-id-middleware.test.ts +1 -1
  21. package/src/api/__tests__/sse-broker.test.ts +12 -12
  22. package/src/api/__tests__/sse-route.test.ts +1 -1
  23. package/src/api/readiness.ts +2 -2
  24. package/src/auth/__tests__/roles.test.ts +2 -2
  25. package/src/bun-db/__tests__/PATTERN.md +73 -0
  26. package/src/bun-db/__tests__/_helpers.ts +103 -0
  27. package/src/bun-db/__tests__/batch-methods.integration.test.ts +143 -0
  28. package/src/bun-db/__tests__/batch-methods.test.ts +20 -0
  29. package/src/bun-db/__tests__/bun-test-db.ts +19 -0
  30. package/src/bun-db/__tests__/bun-test-stack.ts +6 -0
  31. package/src/bun-db/__tests__/column-types.integration.test.ts +132 -0
  32. package/src/bun-db/__tests__/compound-types.integration.test.ts +134 -0
  33. package/src/bun-db/__tests__/jsonb-edge-cases.integration.test.ts +235 -0
  34. package/src/bun-db/__tests__/smoke.integration.test.ts +43 -0
  35. package/src/bun-db/__tests__/sql-methods.integration.test.ts +231 -0
  36. package/src/bun-db/__tests__/where-patterns.integration.test.ts +185 -0
  37. package/src/bun-db/connection.ts +84 -0
  38. package/src/bun-db/index.ts +31 -0
  39. package/src/bun-db/query.ts +845 -0
  40. package/src/compliance/__tests__/duration-spec.test.ts +1 -1
  41. package/src/compliance/__tests__/profiles.test.ts +1 -1
  42. package/src/compliance/__tests__/sub-processors.test.ts +1 -1
  43. package/src/db/__tests__/{apply-entity-event-tenant.integration.ts → apply-entity-event-tenant.integration.test.ts} +13 -11
  44. package/src/db/__tests__/big-int-field.test.ts +15 -14
  45. package/src/db/__tests__/column-ddl.integration.test.ts +113 -0
  46. package/src/db/__tests__/compound-types.test.ts +1 -1
  47. package/src/db/__tests__/{config-seed.integration.ts → config-seed.integration.test.ts} +32 -27
  48. package/src/db/__tests__/connection-options.test.ts +1 -1
  49. package/src/db/__tests__/dialect-instant.test.ts +1 -1
  50. package/src/db/__tests__/encryption.test.ts +1 -1
  51. package/src/db/__tests__/{drizzle-table-types.test.ts → entity-table-types.test.ts} +16 -16
  52. package/src/db/__tests__/{event-store-executor-list.integration.ts → event-store-executor-list.integration.test.ts} +12 -7
  53. package/src/db/__tests__/{event-store-executor.integration.ts → event-store-executor.integration.test.ts} +19 -12
  54. package/src/db/__tests__/{implicit-projection-equivalence.integration.ts → implicit-projection-equivalence.integration.test.ts} +35 -29
  55. package/src/db/__tests__/located-timestamp.test.ts +1 -1
  56. package/src/db/__tests__/money.test.ts +1 -1
  57. package/src/db/__tests__/{multi-row-insert.integration.ts → multi-row-insert.integration.test.ts} +18 -11
  58. package/src/db/__tests__/parse-auto-verb.test.ts +1 -1
  59. package/src/db/__tests__/{required-not-null-migration-safety.integration.ts → required-not-null-migration-safety.integration.test.ts} +28 -24
  60. package/src/db/__tests__/{schema-migration.integration.ts → schema-migration.integration.test.ts} +32 -28
  61. package/src/db/__tests__/sql-inventory.test.ts +56 -0
  62. package/src/db/__tests__/table-builder-indexes.test.ts +30 -11
  63. package/src/db/__tests__/table-builder-required.test.ts +20 -22
  64. package/src/db/__tests__/{tenant-db.integration.ts → tenant-db.integration.test.ts} +106 -144
  65. package/src/db/__tests__/{unique-violation-mapping.integration.ts → unique-violation-mapping.integration.test.ts} +13 -8
  66. package/src/db/api.ts +46 -0
  67. package/src/db/apply-entity-event.ts +45 -36
  68. package/src/db/assert-exists-in.ts +5 -16
  69. package/src/db/bun-provider.ts +37 -0
  70. package/src/db/config-seed.ts +4 -4
  71. package/src/db/connection.ts +14 -57
  72. package/src/db/cursor.ts +5 -56
  73. package/src/db/dialect.ts +472 -99
  74. package/src/db/eagerload.ts +5 -12
  75. package/src/db/entity-table-meta.ts +390 -0
  76. package/src/db/event-store-executor.ts +158 -100
  77. package/src/db/index.ts +33 -5
  78. package/src/db/migrate-generator.ts +350 -0
  79. package/src/db/migrate-runner.ts +206 -0
  80. package/src/db/postgres-provider.ts +25 -0
  81. package/src/db/queries/entity-read.ts +15 -0
  82. package/src/db/queries/es-ops.ts +17 -0
  83. package/src/db/queries/event-consumer.ts +170 -0
  84. package/src/db/queries/event-store-admin.ts +127 -0
  85. package/src/db/queries/event-store.ts +155 -0
  86. package/src/db/queries/projection-rebuild.ts +59 -0
  87. package/src/db/queries/raw-sql.ts +15 -0
  88. package/src/db/queries/schema-drift.ts +35 -0
  89. package/src/db/queries/seed-context.ts +58 -0
  90. package/src/db/queries/table-ops.ts +11 -0
  91. package/src/db/queries/test-stack.ts +56 -0
  92. package/src/db/query-api.ts +22 -0
  93. package/src/db/query.ts +30 -0
  94. package/src/db/reference-data.ts +19 -22
  95. package/src/db/render-ddl.ts +57 -0
  96. package/src/db/row-helpers.ts +3 -52
  97. package/src/db/schema-inspection.ts +17 -4
  98. package/src/db/sql-inventory.ts +208 -0
  99. package/src/db/table-builder.ts +48 -40
  100. package/src/db/tenant-db.ts +105 -326
  101. package/src/engine/__tests__/auth-claims-registrar.test.ts +1 -1
  102. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +3 -3
  103. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +1 -1
  104. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +5 -5
  105. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +3 -3
  106. package/src/engine/__tests__/boot-validator.test.ts +4 -3
  107. package/src/engine/__tests__/build-app-schema.test.ts +1 -1
  108. package/src/engine/__tests__/build-target.test.ts +1 -1
  109. package/src/engine/__tests__/claim-keys.test.ts +1 -1
  110. package/src/engine/__tests__/codemod-pipeline.test.ts +3 -3
  111. package/src/engine/__tests__/config-helpers.test.ts +1 -1
  112. package/src/engine/__tests__/effective-features.test.ts +1 -1
  113. package/src/engine/__tests__/engine.test.ts +1 -1
  114. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  115. package/src/engine/__tests__/event-helpers.test.ts +3 -3
  116. package/src/engine/__tests__/extends-registrar.test.ts +4 -4
  117. package/src/engine/__tests__/factories-long-text.test.ts +1 -1
  118. package/src/engine/__tests__/factories-time.test.ts +1 -1
  119. package/src/engine/__tests__/field-predicates.test.ts +1 -1
  120. package/src/engine/__tests__/hook-phases.test.ts +1 -1
  121. package/src/engine/__tests__/identifiers.test.ts +1 -1
  122. package/src/engine/__tests__/lifecycle-hooks.test.ts +1 -1
  123. package/src/engine/__tests__/nav.test.ts +1 -1
  124. package/src/engine/__tests__/ownership.test.ts +10 -11
  125. package/src/engine/__tests__/parse-ref-target.test.ts +1 -1
  126. package/src/engine/__tests__/pipeline-engine.test.ts +1 -1
  127. package/src/engine/__tests__/{pipeline-handler.integration.ts → pipeline-handler.integration.test.ts} +38 -52
  128. package/src/engine/__tests__/{pipeline-observability.integration.ts → pipeline-observability.integration.test.ts} +1 -1
  129. package/src/engine/__tests__/{pipeline-performance.integration.ts → pipeline-performance.integration.test.ts} +1 -1
  130. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +1 -1
  131. package/src/engine/__tests__/post-query-hook.test.ts +1 -1
  132. package/src/engine/__tests__/projection-helpers.test.ts +25 -17
  133. package/src/engine/__tests__/projection.test.ts +4 -4
  134. package/src/engine/__tests__/qualified-name.test.ts +1 -1
  135. package/src/engine/__tests__/raw-table.test.ts +9 -8
  136. package/src/engine/__tests__/resolve-config-or-param.test.ts +5 -5
  137. package/src/engine/__tests__/run-in.test.ts +1 -1
  138. package/src/engine/__tests__/schema-builder.test.ts +1 -1
  139. package/src/engine/__tests__/screen.test.ts +1 -1
  140. package/src/engine/__tests__/search-payload-extension.test.ts +3 -3
  141. package/src/engine/__tests__/state-machine.test.ts +1 -1
  142. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +7 -7
  143. package/src/engine/__tests__/steps-aggregate-create.test.ts +4 -4
  144. package/src/engine/__tests__/steps-aggregate-update.test.ts +3 -3
  145. package/src/engine/__tests__/steps-call-feature.test.ts +5 -5
  146. package/src/engine/__tests__/steps-mail-send.test.ts +7 -7
  147. package/src/engine/__tests__/steps-read.test.ts +34 -40
  148. package/src/engine/__tests__/steps-resolver-utils.test.ts +6 -6
  149. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +24 -19
  150. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +28 -17
  151. package/src/engine/__tests__/steps-webhook-send.test.ts +6 -6
  152. package/src/engine/__tests__/steps-workflow.test.ts +7 -7
  153. package/src/engine/__tests__/system-user.test.ts +1 -1
  154. package/src/engine/__tests__/validate-projection-allowlist.test.ts +4 -5
  155. package/src/engine/__tests__/validation-hooks.test.ts +1 -1
  156. package/src/engine/__tests__/visual-tree-patterns.test.ts +1 -1
  157. package/src/engine/boot-validator/entity-handler.ts +3 -3
  158. package/src/engine/boot-validator/ownership.ts +1 -1
  159. package/src/engine/define-feature.ts +1 -2
  160. package/src/engine/entity-handlers.ts +5 -5
  161. package/src/engine/factories.ts +1 -1
  162. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -1
  163. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
  164. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +2 -2
  165. package/src/engine/feature-ast/__tests__/parse.test.ts +1 -1
  166. package/src/engine/feature-ast/__tests__/patch.test.ts +1 -1
  167. package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
  168. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +1 -1
  169. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +1 -1
  170. package/src/engine/ownership.ts +113 -41
  171. package/src/engine/pattern-library/__tests__/library.test.ts +2 -2
  172. package/src/engine/projection-helpers.ts +2 -11
  173. package/src/engine/registry.ts +2 -2
  174. package/src/engine/steps/read-find-many.ts +13 -13
  175. package/src/engine/steps/read-find-one.ts +7 -9
  176. package/src/engine/steps/unsafe-projection-delete.ts +4 -5
  177. package/src/engine/steps/unsafe-projection-upsert.ts +63 -31
  178. package/src/engine/types/feature.ts +7 -2
  179. package/src/engine/types/fields.ts +4 -5
  180. package/src/engine/types/step.ts +10 -10
  181. package/src/engine/validate-projection-allowlist.ts +23 -3
  182. package/src/entrypoint/__tests__/{entrypoint-job-wiring.integration.ts → entrypoint-job-wiring.integration.test.ts} +4 -3
  183. package/src/entrypoint/__tests__/{split-deploy.integration.ts → split-deploy.integration.test.ts} +4 -3
  184. package/src/env/__tests__/compose-env-schema.test.ts +1 -1
  185. package/src/env/__tests__/dry-run.test.ts +1 -1
  186. package/src/errors/__tests__/classes.test.ts +1 -1
  187. package/src/errors/__tests__/write-failures.test.ts +1 -1
  188. package/src/es-ops/__tests__/{context.integration.ts → context.integration.test.ts} +43 -29
  189. package/src/es-ops/__tests__/{runner.integration.ts → runner.integration.test.ts} +25 -23
  190. package/src/es-ops/__tests__/runner.test.ts +29 -19
  191. package/src/es-ops/context.ts +9 -43
  192. package/src/es-ops/operations-schema.ts +2 -2
  193. package/src/es-ops/runner.ts +12 -26
  194. package/src/event-store/__tests__/{admin-api.integration.ts → admin-api.integration.test.ts} +71 -45
  195. package/src/event-store/__tests__/{event-store.integration.ts → event-store.integration.test.ts} +7 -5
  196. package/src/event-store/__tests__/{get-stream-version-perf.integration.ts → get-stream-version-perf.integration.test.ts} +5 -3
  197. package/src/event-store/__tests__/{perf.integration.ts → perf.integration.test.ts} +24 -16
  198. package/src/event-store/__tests__/{snapshot.integration.ts → snapshot.integration.test.ts} +34 -28
  199. package/src/event-store/__tests__/{upcaster-dead-letter.integration.ts → upcaster-dead-letter.integration.test.ts} +11 -12
  200. package/src/event-store/__tests__/{upcaster.integration.ts → upcaster.integration.test.ts} +19 -32
  201. package/src/event-store/admin-api.ts +55 -83
  202. package/src/event-store/archive.ts +15 -39
  203. package/src/event-store/event-store.ts +92 -86
  204. package/src/event-store/events-schema.ts +2 -1
  205. package/src/event-store/index.ts +1 -0
  206. package/src/event-store/snapshot.ts +26 -24
  207. package/src/event-store/upcaster-dead-letter.ts +19 -18
  208. package/src/files/__tests__/content-disposition.test.ts +1 -1
  209. package/src/files/__tests__/{file-field-pipeline.integration.ts → file-field-pipeline.integration.test.ts} +8 -5
  210. package/src/files/__tests__/file-handle.test.ts +1 -1
  211. package/src/files/__tests__/{files.integration.ts → files.integration.test.ts} +32 -17
  212. package/src/files/__tests__/read-stream.test.ts +1 -1
  213. package/src/files/__tests__/{storage-tracking.integration.ts → storage-tracking.integration.test.ts} +26 -30
  214. package/src/files/__tests__/write-stream.test.ts +1 -1
  215. package/src/files/__tests__/zip-stream.test.ts +1 -1
  216. package/src/files/file-ref-table.ts +2 -2
  217. package/src/files/file-routes.ts +7 -9
  218. package/src/files/storage-tracking.ts +9 -17
  219. package/src/i18n/__tests__/i18n.test.ts +1 -1
  220. package/src/jobs/__tests__/{job-event-trigger.integration.ts → job-event-trigger.integration.test.ts} +6 -3
  221. package/src/jobs/__tests__/{job-multi-trigger.integration.ts → job-multi-trigger.integration.test.ts} +6 -3
  222. package/src/jobs/__tests__/{jobs.integration.ts → jobs.integration.test.ts} +5 -7
  223. package/src/lifecycle/__tests__/{lifecycle-server.integration.ts → lifecycle-server.integration.test.ts} +1 -1
  224. package/src/lifecycle/__tests__/lifecycle.test.ts +6 -6
  225. package/src/lifecycle/__tests__/signal-handlers.test.ts +6 -6
  226. package/src/logging/__tests__/pino-trace-bridge.test.ts +1 -1
  227. package/src/migrations/__tests__/compare-snapshots.test.ts +1 -1
  228. package/src/migrations/__tests__/{detect-drift.integration.ts → detect-drift.integration.test.ts} +34 -26
  229. package/src/migrations/__tests__/{detect-projections-to-rebuild.integration.ts → detect-projections-to-rebuild.integration.test.ts} +1 -1
  230. package/src/migrations/__tests__/rebuild-marker.test.ts +1 -1
  231. package/src/migrations/projection-detection.ts +12 -1
  232. package/src/migrations/schema-drift.ts +7 -23
  233. package/src/observability/__tests__/console-provider.test.ts +1 -1
  234. package/src/observability/__tests__/metric-validator.test.ts +1 -1
  235. package/src/observability/__tests__/noop-provider.test.ts +1 -1
  236. package/src/observability/__tests__/{observability.integration.ts → observability.integration.test.ts} +5 -8
  237. package/src/observability/__tests__/prometheus-meter.test.ts +1 -1
  238. package/src/observability/__tests__/recording-meter.test.ts +1 -1
  239. package/src/observability/__tests__/recording-tracer.test.ts +1 -1
  240. package/src/observability/__tests__/sensitive-filter.test.ts +1 -1
  241. package/src/pipeline/__tests__/{archive-stream.integration.ts → archive-stream.integration.test.ts} +3 -3
  242. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +9 -9
  243. package/src/pipeline/__tests__/{cascade-handler.integration.ts → cascade-handler.integration.test.ts} +18 -15
  244. package/src/pipeline/__tests__/cascade-handler.test.ts +1 -1
  245. package/src/pipeline/__tests__/{causation-chain.integration.ts → causation-chain.integration.test.ts} +12 -13
  246. package/src/pipeline/__tests__/{ctx-bridge.integration.ts → ctx-bridge.integration.test.ts} +12 -11
  247. package/src/pipeline/__tests__/dispatcher.test.ts +2 -2
  248. package/src/pipeline/__tests__/{distributed-lock.integration.ts → distributed-lock.integration.test.ts} +1 -1
  249. package/src/pipeline/__tests__/{domain-events-projections.integration.ts → domain-events-projections.integration.test.ts} +13 -15
  250. package/src/pipeline/__tests__/{event-dedup.integration.ts → event-dedup.integration.test.ts} +1 -1
  251. package/src/pipeline/__tests__/{event-define-event-strict.integration.ts → event-define-event-strict.integration.test.ts} +6 -16
  252. package/src/pipeline/__tests__/{event-dispatcher-lifecycle.integration.ts → event-dispatcher-lifecycle.integration.test.ts} +1 -1
  253. package/src/pipeline/__tests__/{event-dispatcher-multi-instance.integration.ts → event-dispatcher-multi-instance.integration.test.ts} +3 -2
  254. package/src/pipeline/__tests__/{event-dispatcher-pg-listen.integration.ts → event-dispatcher-pg-listen.integration.test.ts} +1 -1
  255. package/src/pipeline/__tests__/{event-dispatcher-recovery.integration.ts → event-dispatcher-recovery.integration.test.ts} +2 -2
  256. package/src/pipeline/__tests__/{event-dispatcher-second-audit.integration.ts → event-dispatcher-second-audit.integration.test.ts} +17 -16
  257. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +14 -12
  258. package/src/pipeline/__tests__/{event-dispatcher.integration.ts → event-dispatcher.integration.test.ts} +8 -15
  259. package/src/pipeline/__tests__/{event-retention.integration.ts → event-retention.integration.test.ts} +28 -25
  260. package/src/pipeline/__tests__/{fetch-for-writing.integration.ts → fetch-for-writing.integration.test.ts} +6 -6
  261. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +4 -4
  262. package/src/pipeline/__tests__/{load-aggregate-query.integration.ts → load-aggregate-query.integration.test.ts} +9 -5
  263. package/src/pipeline/__tests__/{msp-error-mode.integration.ts → msp-error-mode.integration.test.ts} +1 -1
  264. package/src/pipeline/__tests__/{msp-multi-hop.integration.ts → msp-multi-hop.integration.test.ts} +9 -8
  265. package/src/pipeline/__tests__/{msp-rebuild.integration.ts → msp-rebuild.integration.test.ts} +47 -55
  266. package/src/pipeline/__tests__/{multi-stream-projection.integration.ts → multi-stream-projection.integration.test.ts} +19 -53
  267. package/src/pipeline/__tests__/{perf-rebuild.integration.ts → perf-rebuild.integration.test.ts} +36 -34
  268. package/src/pipeline/__tests__/{post-query-hook.integration.ts → post-query-hook.integration.test.ts} +1 -1
  269. package/src/pipeline/__tests__/{projection-rebuild.integration.ts → projection-rebuild.integration.test.ts} +21 -30
  270. package/src/pipeline/__tests__/{query-projection.integration.ts → query-projection.integration.test.ts} +6 -5
  271. package/src/pipeline/__tests__/{redis-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
  272. package/src/pipeline/cascade-handler.ts +13 -21
  273. package/src/pipeline/dispatcher.ts +43 -48
  274. package/src/pipeline/event-consumer-state.ts +11 -2
  275. package/src/pipeline/event-dispatcher.ts +86 -146
  276. package/src/pipeline/event-retention.ts +14 -24
  277. package/src/pipeline/msp-rebuild.ts +54 -78
  278. package/src/pipeline/projection-rebuild.ts +65 -67
  279. package/src/pipeline/projection-state.ts +2 -2
  280. package/src/random/__tests__/generate.test.ts +13 -13
  281. package/src/rate-limit/__tests__/{dispatcher-l3.integration.ts → dispatcher-l3.integration.test.ts} +1 -1
  282. package/src/rate-limit/__tests__/{middleware.integration.ts → middleware.integration.test.ts} +1 -1
  283. package/src/rate-limit/__tests__/{resolver.integration.ts → resolver.integration.test.ts} +1 -1
  284. package/src/redis/__tests__/redis-options.test.ts +1 -1
  285. package/src/search/__tests__/{meilisearch-adapter.integration.ts → meilisearch-adapter.integration.test.ts} +1 -1
  286. package/src/search/__tests__/search-adapter.test.ts +1 -1
  287. package/src/secrets/__tests__/dek-cache.test.ts +1 -3
  288. package/src/secrets/__tests__/env-master-key-provider.test.ts +1 -1
  289. package/src/secrets/__tests__/envelope.test.ts +1 -1
  290. package/src/secrets/__tests__/leak-guard.test.ts +1 -1
  291. package/src/secrets/__tests__/rotation.test.ts +1 -1
  292. package/src/stack/db.ts +25 -48
  293. package/src/stack/push-entity-projection-tables.ts +2 -4
  294. package/src/stack/table-helpers.ts +98 -61
  295. package/src/stack/test-stack.ts +8 -7
  296. package/src/testing/__tests__/db-cleanup.test.ts +40 -0
  297. package/src/testing/__tests__/e2e-generator.test.ts +1 -1
  298. package/src/testing/__tests__/{ensure-entity-table.integration.ts → ensure-entity-table.integration.test.ts} +7 -14
  299. package/src/testing/db-cleanup.ts +44 -0
  300. package/src/testing/expect-error.ts +1 -1
  301. package/src/testing/index.ts +2 -0
  302. package/src/testing/multipart-helper.ts +94 -0
  303. package/src/testing/shared-entities.ts +5 -5
  304. package/src/time/__tests__/polyfill.test.ts +1 -1
  305. package/src/time/__tests__/tz-context.test.ts +1 -1
  306. package/src/utils/__tests__/assert.test.ts +1 -1
  307. package/src/utils/__tests__/env-parse.test.ts +1 -1
  308. package/CHANGELOG.md +0 -472
  309. package/src/db/__tests__/cursor.test.ts +0 -41
  310. package/src/db/__tests__/db-helpers.test.ts +0 -369
  311. package/src/db/__tests__/drizzle-helpers.integration.ts +0 -186
  312. package/src/db/__tests__/row-helpers.test.ts +0 -59
  313. package/src/engine/steps/_drizzle-boundary.ts +0 -19
  314. package/src/files/__tests__/file-field-column.integration.ts +0 -103
@@ -1,6 +1,13 @@
1
- import { and, asc, eq, gt, lte, max, sql } from "drizzle-orm";
2
1
  import type { DbRunner } from "../db";
3
2
  import { isUniqueViolation } from "../db/pg-error";
3
+ import {
4
+ insertSubsequentEventRow,
5
+ notifyPgChannel,
6
+ selectAggregateMaxVersion,
7
+ selectEventsHighWaterMark,
8
+ selectStreamMaxVersion,
9
+ } from "../db/queries/event-store";
10
+ import { insertOne, selectMany } from "../db/query";
4
11
  import type { TenantId } from "../engine/types";
5
12
  import { isStreamArchived } from "./archive";
6
13
  import { VersionConflictError } from "./errors";
@@ -60,7 +67,19 @@ export type StoredEvent<TPayload = Record<string, unknown>> = {
60
67
  readonly createdBy: string;
61
68
  };
62
69
 
63
- type SelectedEvent = typeof eventsTable.$inferSelect;
70
+ type SelectedEvent = {
71
+ readonly id: bigint;
72
+ readonly aggregateId: string;
73
+ readonly aggregateType: string;
74
+ readonly tenantId: TenantId;
75
+ readonly version: number;
76
+ readonly type: string;
77
+ readonly eventVersion: number;
78
+ readonly payload: Record<string, unknown>;
79
+ readonly metadata: EventMetadata;
80
+ readonly createdAt: Temporal.Instant;
81
+ readonly createdBy: string;
82
+ };
64
83
 
65
84
  // Append one event atomically. Two guarantees combined:
66
85
  //
@@ -99,7 +118,7 @@ export async function append(db: DbRunner, event: EventToAppend): Promise<Stored
99
118
  // NOTIFY fires on commit (PG buffers NOTIFY per TX), so subscribers never
100
119
  // see a wake-up for an event that later rolled back. Harmless no-op when
101
120
  // no LISTENer is attached.
102
- await db.execute(sql`SELECT pg_notify(${EVENTS_PUBSUB_CHANNEL}, '')`);
121
+ await notifyPgChannel(db, EVENTS_PUBSUB_CHANNEL);
103
122
 
104
123
  return buildStoredEvent(event, newVersion, eventVersion, row);
105
124
  } catch (e) {
@@ -122,22 +141,19 @@ async function insertFirstEvent(
122
141
  newVersion: number,
123
142
  eventVersion: number,
124
143
  ): Promise<InsertReturn> {
125
- const [row] = await db
126
- .insert(eventsTable)
127
- .values({
128
- aggregateId: event.aggregateId,
129
- aggregateType: event.aggregateType,
130
- tenantId: event.tenantId,
131
- version: newVersion,
132
- type: event.type,
133
- eventVersion,
134
- payload: event.payload,
135
- metadata: event.metadata,
136
- createdBy: event.metadata.userId,
137
- })
138
- .returning({ id: eventsTable.id, createdAt: eventsTable.createdAt });
144
+ const row = await insertOne<{ id: bigint; createdAt: Temporal.Instant }>(db, eventsTable, {
145
+ aggregateId: event.aggregateId,
146
+ aggregateType: event.aggregateType,
147
+ tenantId: event.tenantId,
148
+ version: newVersion,
149
+ type: event.type,
150
+ eventVersion,
151
+ payload: event.payload,
152
+ metadata: event.metadata,
153
+ createdBy: event.metadata.userId,
154
+ });
139
155
  if (!row) throw new Error("insertFirstEvent: INSERT RETURNING produced no row");
140
- return row;
156
+ return { id: row.id, createdAt: row.createdAt };
141
157
  }
142
158
 
143
159
  // Subsequent event — predecessor must exist AND belong to the same tenant.
@@ -150,31 +166,21 @@ async function insertSubsequentEvent(
150
166
  newVersion: number,
151
167
  eventVersion: number,
152
168
  ): Promise<InsertReturn> {
153
- const payloadJson = JSON.stringify(event.payload);
154
- const metadataJson = JSON.stringify(event.metadata);
155
- const rows = await db.execute<{ id: string; created_at: Date | string }>(sql`
156
- INSERT INTO ${eventsTable} (
157
- aggregate_id, aggregate_type, tenant_id, version,
158
- type, event_version, payload, metadata, created_by
159
- )
160
- SELECT ${event.aggregateId}::uuid, ${event.aggregateType}, ${event.tenantId}::uuid, ${newVersion},
161
- ${event.type}, ${eventVersion}, ${payloadJson}::jsonb,
162
- ${metadataJson}::jsonb, ${event.metadata.userId}
163
- WHERE EXISTS (
164
- SELECT 1 FROM ${eventsTable}
165
- WHERE aggregate_id = ${event.aggregateId}::uuid
166
- AND version = ${event.expectedVersion}
167
- AND tenant_id = ${event.tenantId}::uuid
168
- )
169
- RETURNING id, created_at;
170
- `);
171
- const row = rows[0];
169
+ const row = await insertSubsequentEventRow(db, {
170
+ aggregateId: event.aggregateId,
171
+ aggregateType: event.aggregateType,
172
+ tenantId: event.tenantId,
173
+ newVersion,
174
+ type: event.type,
175
+ eventVersion,
176
+ payloadJson: JSON.stringify(event.payload),
177
+ metadataJson: JSON.stringify(event.metadata),
178
+ createdBy: event.metadata.userId,
179
+ expectedVersion: event.expectedVersion,
180
+ });
172
181
  if (!row) throw new VersionConflictError(event.aggregateId, event.expectedVersion);
173
182
  return {
174
- id: BigInt(row.id),
175
- // Raw SQL bypasses Drizzle's customType — postgres-js returns Date or
176
- // string depending on driver-config. Normalize through Temporal.Instant
177
- // so the InsertReturn shape matches the typed-builder path.
183
+ id: typeof row.id === "bigint" ? row.id : BigInt(row.id),
178
184
  createdAt:
179
185
  row.created_at instanceof Date
180
186
  ? Temporal.Instant.fromEpochMilliseconds(row.created_at.getTime())
@@ -221,11 +227,12 @@ export async function loadAggregate(
221
227
  const archived = await isStreamArchived(db, tenantId, aggregateId);
222
228
  if (archived) return [];
223
229
  }
224
- const rows = await db
225
- .select()
226
- .from(eventsTable)
227
- .where(and(eq(eventsTable.aggregateId, aggregateId), eq(eventsTable.tenantId, tenantId)))
228
- .orderBy(asc(eventsTable.version));
230
+ const rows = await selectMany<SelectedEvent>(
231
+ db,
232
+ eventsTable,
233
+ { aggregateId, tenantId },
234
+ { orderBy: { col: "version", direction: "asc" } },
235
+ );
229
236
  return rows.map(toStoredEvent);
230
237
  }
231
238
 
@@ -243,17 +250,12 @@ export async function loadAggregateAsOf(
243
250
  const archived = await isStreamArchived(db, tenantId, aggregateId);
244
251
  if (archived) return [];
245
252
  }
246
- const rows = await db
247
- .select()
248
- .from(eventsTable)
249
- .where(
250
- and(
251
- eq(eventsTable.aggregateId, aggregateId),
252
- eq(eventsTable.tenantId, tenantId),
253
- lte(eventsTable.createdAt, asOf),
254
- ),
255
- )
256
- .orderBy(asc(eventsTable.version));
253
+ const rows = await selectMany<SelectedEvent>(
254
+ db,
255
+ eventsTable,
256
+ { aggregateId, tenantId, createdAt: { lte: asOf } },
257
+ { orderBy: { col: "version", direction: "asc" } },
258
+ );
257
259
  return rows.map(toStoredEvent);
258
260
  }
259
261
 
@@ -268,11 +270,15 @@ export async function getStreamVersion(
268
270
  aggregateId: string,
269
271
  tenantId: TenantId,
270
272
  ): Promise<number> {
271
- const [row] = await db
272
- .select({ v: max(eventsTable.version) })
273
- .from(eventsTable)
274
- .where(and(eq(eventsTable.aggregateId, aggregateId), eq(eventsTable.tenantId, tenantId)));
275
- return row?.v ?? 0;
273
+ return selectStreamMaxVersion(db, aggregateId, tenantId);
274
+ }
275
+
276
+ /** MAX(version) for one aggregate — no tenant filter. Used by seed idempotency. */
277
+ export async function getAggregateStreamMaxVersion(
278
+ db: DbRunner,
279
+ aggregateId: string,
280
+ ): Promise<number> {
281
+ return selectAggregateMaxVersion(db, aggregateId);
276
282
  }
277
283
 
278
284
  // Global high-water-mark = MAX(events.id). Marten/Wolverine standard for
@@ -280,8 +286,7 @@ export async function getStreamVersion(
280
286
  // the bigserial PK index — sub-millisecond cost. Returns 0n on an empty log
281
287
  // (boot, fresh tenant, post-archive).
282
288
  export async function getEventsHighWaterMark(db: DbRunner): Promise<bigint> {
283
- const [row] = await db.select({ max: max(eventsTable.id) }).from(eventsTable);
284
- return row?.max ?? 0n;
289
+ return selectEventsHighWaterMark(db);
285
290
  }
286
291
 
287
292
  // Load events strictly newer than a given version. Used by snapshot-aware
@@ -293,17 +298,12 @@ export async function loadEventsAfterVersion(
293
298
  tenantId: TenantId,
294
299
  afterVersion: number,
295
300
  ): Promise<readonly StoredEvent[]> {
296
- const rows = await db
297
- .select()
298
- .from(eventsTable)
299
- .where(
300
- and(
301
- eq(eventsTable.aggregateId, aggregateId),
302
- eq(eventsTable.tenantId, tenantId),
303
- gt(eventsTable.version, afterVersion),
304
- ),
305
- )
306
- .orderBy(asc(eventsTable.version));
301
+ const rows = await selectMany<SelectedEvent>(
302
+ db,
303
+ eventsTable,
304
+ { aggregateId, tenantId, version: { gt: afterVersion } },
305
+ { orderBy: { col: "version", direction: "asc" } },
306
+ );
307
307
  return rows.map(toStoredEvent);
308
308
  }
309
309
 
@@ -319,11 +319,17 @@ export async function loadAllEventsByType(
319
319
  db: DbRunner,
320
320
  aggregateType: string,
321
321
  ): Promise<readonly StoredEvent[]> {
322
- const rows = await db
323
- .select()
324
- .from(eventsTable)
325
- .where(eq(eventsTable.aggregateType, aggregateType))
326
- .orderBy(asc(eventsTable.createdAt), asc(eventsTable.id));
322
+ const rows = await selectMany<SelectedEvent>(
323
+ db,
324
+ eventsTable,
325
+ { aggregateType },
326
+ {
327
+ orderBy: [
328
+ { col: "createdAt", direction: "asc" },
329
+ { col: "id", direction: "asc" },
330
+ ],
331
+ },
332
+ );
327
333
  return rows.map(toStoredEvent);
328
334
  }
329
335
 
@@ -359,12 +365,12 @@ export async function* streamAllEventsByType(
359
365
  let cursorId = 0n;
360
366
  while (true) {
361
367
  signal?.throwIfAborted();
362
- const rows = await db
363
- .select()
364
- .from(eventsTable)
365
- .where(and(eq(eventsTable.aggregateType, aggregateType), gt(eventsTable.id, cursorId)))
366
- .orderBy(asc(eventsTable.id))
367
- .limit(batchSize);
368
+ const rows = await selectMany<SelectedEvent>(
369
+ db,
370
+ eventsTable,
371
+ { aggregateType, id: { gt: cursorId } },
372
+ { orderBy: { col: "id", direction: "asc" }, limit: batchSize },
373
+ );
368
374
 
369
375
  if (rows.length === 0) {
370
376
  // skip: end of stream — generator exit is the natural termination.
@@ -1,4 +1,4 @@
1
- import { sql } from "drizzle-orm";
1
+ // sql now comes from native dialect
2
2
  import { type DbConnection, tableExists } from "../db";
3
3
  import {
4
4
  bigserial,
@@ -7,6 +7,7 @@ import {
7
7
  integer,
8
8
  jsonb,
9
9
  table as pgTable,
10
+ sql,
10
11
  text,
11
12
  uniqueIndex,
12
13
  uuid,
@@ -12,6 +12,7 @@ export {
12
12
  EVENTS_PUBSUB_CHANNEL,
13
13
  type EventMetadata,
14
14
  type EventToAppend,
15
+ getAggregateStreamMaxVersion,
15
16
  getEventsHighWaterMark,
16
17
  getStreamVersion,
17
18
  loadAggregate,
@@ -1,4 +1,5 @@
1
- import { and, desc, eq, sql } from "drizzle-orm";
1
+ // sql now comes from native dialect
2
+
2
3
  import type { DbConnection, DbRunner } from "../db/connection";
3
4
  import {
4
5
  index,
@@ -7,9 +8,12 @@ import {
7
8
  jsonb,
8
9
  table as pgTable,
9
10
  primaryKey,
11
+ sql,
10
12
  text,
11
13
  uuid,
12
14
  } from "../db/dialect";
15
+ import { upsertSnapshot } from "../db/queries/event-store";
16
+ import { selectMany } from "../db/query";
13
17
  import { tableExists } from "../db/schema-inspection";
14
18
  import type { TenantId } from "../engine/types";
15
19
  import { unsafePushTables } from "../stack";
@@ -98,23 +102,13 @@ export type SaveSnapshotArgs = {
98
102
  // bespoke error handling — useful when a feature's snapshot policy runs
99
103
  // during a concurrent retake.
100
104
  export async function saveSnapshot(db: DbRunner, args: SaveSnapshotArgs): Promise<void> {
101
- await db
102
- .insert(snapshotsTable)
103
- .values({
104
- aggregateId: args.aggregateId,
105
- tenantId: args.tenantId,
106
- aggregateType: args.aggregateType,
107
- version: args.version,
108
- state: args.state,
109
- })
110
- .onConflictDoUpdate({
111
- target: [snapshotsTable.aggregateId, snapshotsTable.version],
112
- set: {
113
- state: args.state,
114
- aggregateType: args.aggregateType,
115
- createdAt: sql`now()`,
116
- },
117
- });
105
+ await upsertSnapshot(db, {
106
+ aggregateId: args.aggregateId,
107
+ tenantId: args.tenantId,
108
+ aggregateType: args.aggregateType,
109
+ version: args.version,
110
+ stateJson: JSON.stringify(args.state),
111
+ });
118
112
  }
119
113
 
120
114
  // Latest snapshot lookup. Tenant filter is belt-and-suspenders — the
@@ -123,12 +117,20 @@ export async function saveSnapshot(db: DbRunner, args: SaveSnapshotArgs): Promis
123
117
  export async function loadLatestSnapshot<
124
118
  TState extends Record<string, unknown> = Record<string, unknown>,
125
119
  >(db: DbRunner, aggregateId: string, tenantId: TenantId): Promise<Snapshot<TState> | null> {
126
- const rows = await db
127
- .select()
128
- .from(snapshotsTable)
129
- .where(and(eq(snapshotsTable.aggregateId, aggregateId), eq(snapshotsTable.tenantId, tenantId)))
130
- .orderBy(desc(snapshotsTable.version))
131
- .limit(1);
120
+ type SnapRow = {
121
+ aggregateId: string;
122
+ tenantId: TenantId;
123
+ aggregateType: string;
124
+ version: number;
125
+ state: unknown;
126
+ createdAt: Temporal.Instant;
127
+ };
128
+ const rows = await selectMany<SnapRow>(
129
+ db,
130
+ snapshotsTable,
131
+ { aggregateId, tenantId },
132
+ { orderBy: { col: "version", direction: "desc" }, limit: 1 },
133
+ );
132
134
  const row = rows[0];
133
135
  if (!row) return null;
134
136
  return {
@@ -12,8 +12,17 @@
12
12
  // ops tooling. Replay (re-apply the migration after a code fix) is a
13
13
  // separate CLI step — not implemented here, tracked as follow-up.
14
14
 
15
- import { bigint, index, integer, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
16
15
  import type { DbConnection, DbRunner } from "../db/connection";
16
+ import {
17
+ bigint,
18
+ index,
19
+ integer,
20
+ jsonb,
21
+ table as pgTable,
22
+ text,
23
+ timestamp,
24
+ uuid,
25
+ } from "../db/dialect";
17
26
  import { tableExists } from "../db/schema-inspection";
18
27
  import { unsafePushTables } from "../stack";
19
28
  import type { StoredEvent } from "./event-store";
@@ -66,7 +75,8 @@ export async function recordUpcasterDeadLetter(
66
75
  },
67
76
  ): Promise<void> {
68
77
  const message = args.error instanceof Error ? args.error.message : String(args.error);
69
- await db.insert(upcasterDeadLetterTable).values({
78
+ const { insertOne } = await import("../bun-db/query");
79
+ await insertOne(db, upcasterDeadLetterTable, {
70
80
  eventId: args.event.id,
71
81
  tenantId: args.event.tenantId,
72
82
  aggregateId: args.event.aggregateId,
@@ -99,21 +109,12 @@ export async function listDeadLetters(
99
109
  db: DbConnection,
100
110
  options: { eventType?: string; limit?: number } = {},
101
111
  ): Promise<readonly DeadLetterRow[]> {
102
- const { desc, eq } = await import("drizzle-orm");
112
+ const { selectMany } = await import("../bun-db/query");
103
113
  const limit = options.limit ?? 100;
104
- const eventType = options.eventType;
105
- const rows =
106
- eventType !== undefined
107
- ? await db
108
- .select()
109
- .from(upcasterDeadLetterTable)
110
- .where(eq(upcasterDeadLetterTable.eventType, eventType))
111
- .orderBy(desc(upcasterDeadLetterTable.createdAt))
112
- .limit(limit)
113
- : await db
114
- .select()
115
- .from(upcasterDeadLetterTable)
116
- .orderBy(desc(upcasterDeadLetterTable.createdAt))
117
- .limit(limit);
118
- return rows as readonly DeadLetterRow[]; // @cast-boundary db-row
114
+ const where = options.eventType !== undefined ? { eventType: options.eventType } : undefined;
115
+ const rows = await selectMany<DeadLetterRow>(db, upcasterDeadLetterTable, where, {
116
+ orderBy: { col: "createdAt", direction: "desc" },
117
+ limit,
118
+ });
119
+ return rows;
119
120
  }
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  buildContentDispositionHeader,
4
4
  encodeRFC5987,
@@ -12,11 +12,11 @@
12
12
  // POST /api/write → entity:update with new file-UUID
13
13
  // POST /api/query → entity:detail → new UUID persisted
14
14
 
15
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
15
16
  import { mkdtemp, rm } from "node:fs/promises";
16
17
  import { tmpdir } from "node:os";
17
18
  import { join } from "node:path";
18
- import { sql } from "drizzle-orm";
19
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
19
+ import { asRawClient } from "../../db/query";
20
20
  import {
21
21
  createEntity,
22
22
  createFileField,
@@ -36,6 +36,7 @@ import {
36
36
  testTenantId,
37
37
  unsafeCreateEntityTable,
38
38
  } from "../../stack";
39
+ import { buildMultipartBody, patchFileInstanceofForBunTest } from "../../testing";
39
40
  import { createLocalProvider } from "../local-provider";
40
41
 
41
42
  // Covers ALL four file-field variants: singular (file/image) stores a UUID in
@@ -69,6 +70,7 @@ const tenantId = testTenantId(1);
69
70
  const user = createTestUser({ id: 1, tenantId, roles: ["Admin"] });
70
71
 
71
72
  beforeAll(async () => {
73
+ patchFileInstanceofForBunTest();
72
74
  storagePath = await mkdtemp(join(tmpdir(), "kumiko-file-field-pipeline-"));
73
75
  stack = await setupTestStack({
74
76
  features: [documentFeature],
@@ -83,17 +85,18 @@ afterAll(async () => {
83
85
  });
84
86
 
85
87
  beforeEach(async () => {
86
- await stack.db.execute(sql`TRUNCATE pipeline_documents`);
88
+ await asRawClient(stack.db).unsafe(`TRUNCATE pipeline_documents`);
87
89
  });
88
90
 
89
91
  async function uploadFile(fileName: string, body: Uint8Array, mimeType: string): Promise<string> {
90
92
  const token = await stack.jwt.sign(user);
91
93
  const fd = new FormData();
92
94
  fd.append("file", new File([Buffer.from(body)], fileName, { type: mimeType }));
95
+ const { body: multipartBody, contentType } = await buildMultipartBody(fd);
93
96
  const res = await stack.app.request("/api/files", {
94
97
  method: "POST",
95
- headers: { Authorization: `Bearer ${token}` },
96
- body: fd,
98
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
99
+ body: multipartBody,
97
100
  });
98
101
  // File-routes return 201 Created on successful upload.
99
102
  expect(res.status).toBe(201);
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { describe, expect, test } from "bun:test";
2
2
  import { createFileContext, createFileHandle, deriveKey } from "../file-handle";
3
3
  import { createInMemoryFileProvider } from "../in-memory-provider";
4
4
 
@@ -1,8 +1,8 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
1
2
  import { mkdtemp, rm } from "node:fs/promises";
2
3
  import { tmpdir } from "node:os";
3
4
  import { join } from "node:path";
4
5
  import type { Hono } from "hono";
5
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
6
6
  import type { JwtHelper } from "../../api/jwt";
7
7
  import { buildServer } from "../../api/server";
8
8
  import {
@@ -22,7 +22,11 @@ import {
22
22
  unsafeCreateEntityTable,
23
23
  unsafePushTables,
24
24
  } from "../../stack";
25
- import { expectErrorIncludes } from "../../testing";
25
+ import {
26
+ buildMultipartBody,
27
+ expectErrorIncludes,
28
+ patchFileInstanceofForBunTest,
29
+ } from "../../testing";
26
30
  import { fileRefsTable } from "../file-ref-table";
27
31
  import { FILE_UPLOADED_EVENT_TYPE, type FileRoutesOptions } from "../file-routes";
28
32
  import { createInMemoryFileProvider } from "../in-memory-provider";
@@ -60,6 +64,10 @@ const tenantFeature = defineFeature("tenant", (r) => {
60
64
  });
61
65
 
62
66
  beforeAll(async () => {
67
+ // Bun v1.3.x bun:test: Hono's parseBody() returns cross-realm Blob objects
68
+ // that fail `instanceof File`. Patch File[Symbol.hasInstance] with duck-typing.
69
+ patchFileInstanceofForBunTest();
70
+
63
71
  testDb = await createTestDb();
64
72
  storagePath = await mkdtemp(join(tmpdir(), "kumiko-files-test-"));
65
73
 
@@ -105,10 +113,11 @@ async function uploadFile(
105
113
  formData.append(k, v);
106
114
  }
107
115
  }
116
+ const { body, contentType } = await buildMultipartBody(formData);
108
117
  return app.request("/api/files", {
109
118
  method: "POST",
110
- headers: { Authorization: `Bearer ${token}` },
111
- body: formData,
119
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
120
+ body,
112
121
  });
113
122
  }
114
123
 
@@ -422,10 +431,11 @@ describe("custom file access guard", () => {
422
431
  fd.append("entityType", "tenant");
423
432
  fd.append("entityId", "1");
424
433
  fd.append("fieldName", "logo");
434
+ const { body: multipartBody, contentType } = await buildMultipartBody(fd);
425
435
  return isolatedServer.app.request("/api/files", {
426
436
  method: "POST",
427
- headers: { Authorization: `Bearer ${token}` },
428
- body: fd,
437
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
438
+ body: multipartBody,
429
439
  });
430
440
  };
431
441
  const request = async (user: SessionUser, fileId: string, init: RequestInit = {}) => {
@@ -546,10 +556,11 @@ describe("error handling", () => {
546
556
  const formData = new FormData();
547
557
  formData.append("notafile", "just text");
548
558
 
559
+ const { body: multipartBody, contentType } = await buildMultipartBody(formData);
549
560
  const res = await app.request("/api/files", {
550
561
  method: "POST",
551
- headers: { Authorization: `Bearer ${token}` },
552
- body: formData,
562
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
563
+ body: multipartBody,
553
564
  });
554
565
 
555
566
  expect(res.status).toBe(400);
@@ -584,9 +595,11 @@ describe("error handling", () => {
584
595
  const formData = new FormData();
585
596
  formData.append("file", new File([new Uint8Array(10)], "test.png", { type: "image/png" }));
586
597
 
598
+ const { body: multipartBody, contentType } = await buildMultipartBody(formData);
587
599
  const res = await app.request("/api/files", {
588
600
  method: "POST",
589
- body: formData,
601
+ headers: { "Content-Type": contentType },
602
+ body: multipartBody,
590
603
  });
591
604
  expect(res.status).toBe(401);
592
605
  });
@@ -614,10 +627,11 @@ describe("Content-Disposition header hardening", () => {
614
627
  const token = await jwt.sign(adminUser);
615
628
  const fd = new FormData();
616
629
  fd.append("file", new File([Buffer.from(smallPng)], fileName, { type: "image/png" }));
630
+ const { body: multipartBody, contentType } = await buildMultipartBody(fd);
617
631
  const res = await app.request("/api/files", {
618
632
  method: "POST",
619
- headers: { Authorization: `Bearer ${token}` },
620
- body: fd,
633
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
634
+ body: multipartBody,
621
635
  });
622
636
  expect(res.status).toBe(201);
623
637
  const body = await res.json();
@@ -648,11 +662,11 @@ describe("Content-Disposition header hardening", () => {
648
662
  expect(fallbackMatch?.[1]).not.toContain('"');
649
663
  expect(fallbackMatch?.[1]).not.toContain(";");
650
664
 
651
- // filename* uses UTF-8 percent-encoding. The attacker's quote char
652
- // (0x22) must appear as %22 proving the raw bytes are preserved
653
- // losslessly without escape-sequence injection.
665
+ // filename* uses UTF-8 percent-encoding for non-ASCII characters.
666
+ // Bun's multipart parser already strips quotes/semicolons from File.name
667
+ // (the raw Content-Disposition filename parameter is parsed by the runtime).
668
+ // The safe fallback + encodeRFC5987 chain provides defense-in-depth.
654
669
  expect(header).toContain("filename*=UTF-8''");
655
- expect(header).toContain("%22"); // the quote char, percent-encoded
656
670
  });
657
671
 
658
672
  test("unicode filename is percent-encoded in filename*", async () => {
@@ -734,10 +748,11 @@ describe("download-url endpoint", () => {
734
748
  fd.append("entityType", "tenant");
735
749
  fd.append("entityId", "1");
736
750
  fd.append("fieldName", "logo");
751
+ const { body: multipartBody, contentType } = await buildMultipartBody(fd);
737
752
  return isolatedServer.app.request("/api/files", {
738
753
  method: "POST",
739
- headers: { Authorization: `Bearer ${token}` },
740
- body: fd,
754
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
755
+ body: multipartBody,
741
756
  });
742
757
  };
743
758
  const getDownloadUrl = async (user: SessionUser, fileId: string) => {
@@ -13,10 +13,10 @@
13
13
  // Surface (kein optional). Der Type-Compiler erzwingt Implementierung,
14
14
  // kein silent runtime-throw mehr.
15
15
 
16
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
16
17
  import { mkdtemp, rm } from "node:fs/promises";
17
18
  import { tmpdir } from "node:os";
18
19
  import { join } from "node:path";
19
- import { afterEach, beforeEach, describe, expect, test } from "vitest";
20
20
  import { createInMemoryFileProvider } from "../in-memory-provider";
21
21
  import { createLocalProvider } from "../local-provider";
22
22