@cosmicdrift/kumiko-framework 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. package/README.md +9 -38
  2. package/package.json +21 -2
  3. package/src/__tests__/anonymous-access.integration.test.ts +185 -0
  4. package/src/__tests__/consumer-cli.integration.test.ts +172 -0
  5. package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
  6. package/src/__tests__/full-stack.integration.test.ts +1 -1
  7. package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
  8. package/src/__tests__/schema-cli.integration.test.ts +50 -0
  9. package/src/__tests__/store-table.integration.test.ts +93 -0
  10. package/src/api/__tests__/api.test.ts +373 -0
  11. package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
  13. package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
  14. package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
  15. package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
  16. package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
  17. package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
  18. package/src/api/__tests__/batch.integration.test.ts +101 -11
  19. package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
  20. package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
  21. package/src/api/__tests__/jwt.test.ts +200 -1
  22. package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
  23. package/src/api/__tests__/pat-scope.test.ts +36 -0
  24. package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
  25. package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
  26. package/src/api/__tests__/request-id-middleware.test.ts +51 -0
  27. package/src/api/__tests__/server-boot-guards.test.ts +71 -0
  28. package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
  29. package/src/api/__tests__/sse-broker.test.ts +57 -0
  30. package/src/api/__tests__/sse-route.test.ts +4 -0
  31. package/src/api/api-constants.ts +11 -0
  32. package/src/api/auth-middleware.ts +248 -40
  33. package/src/api/auth-routes.ts +576 -95
  34. package/src/api/index.ts +13 -4
  35. package/src/api/jwt.ts +170 -11
  36. package/src/api/pat-scope.ts +14 -0
  37. package/src/api/pii-leak-guard.ts +47 -0
  38. package/src/api/request-context.ts +3 -0
  39. package/src/api/request-id-middleware.ts +2 -0
  40. package/src/api/routes.ts +169 -2
  41. package/src/api/server.ts +112 -16
  42. package/src/api/sse-broker.ts +39 -0
  43. package/src/bun-db/__tests__/PATTERN.md +0 -1
  44. package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
  45. package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
  46. package/src/bun-db/__tests__/write-brand.test.ts +21 -0
  47. package/src/bun-db/connection.ts +3 -3
  48. package/src/bun-db/index.ts +2 -0
  49. package/src/bun-db/query.ts +105 -32
  50. package/src/consumer-cli.ts +134 -0
  51. package/src/crypto/__tests__/blind-index.test.ts +130 -0
  52. package/src/crypto/__tests__/event-pii.test.ts +161 -0
  53. package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
  54. package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
  55. package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
  56. package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
  57. package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
  58. package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
  59. package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
  60. package/src/crypto/blind-index.ts +118 -0
  61. package/src/crypto/event-pii.ts +70 -0
  62. package/src/crypto/in-memory-kms-adapter.ts +50 -0
  63. package/src/crypto/index.ts +67 -0
  64. package/src/crypto/kms-adapter.ts +2 -0
  65. package/src/crypto/pg-kms-adapter.ts +295 -0
  66. package/src/crypto/pii-field-encryption.ts +248 -0
  67. package/src/crypto/request-kms-cache.ts +38 -0
  68. package/src/crypto/subject-resolver.ts +91 -0
  69. package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
  70. package/src/db/__tests__/blind-index.integration.test.ts +248 -0
  71. package/src/db/__tests__/build-filter-where.test.ts +34 -0
  72. package/src/db/__tests__/collect-table-metas.test.ts +10 -10
  73. package/src/db/__tests__/config-seed.integration.test.ts +13 -5
  74. package/src/db/__tests__/dialect-instant.test.ts +1 -4
  75. package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
  76. package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
  77. package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
  78. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
  79. package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
  80. package/src/db/__tests__/feature-table-sources.test.ts +34 -0
  81. package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
  82. package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
  83. package/src/db/__tests__/located-timestamp.test.ts +19 -0
  84. package/src/db/__tests__/migrate-generator.test.ts +21 -0
  85. package/src/db/__tests__/migrate-runner.test.ts +61 -0
  86. package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
  87. package/src/db/__tests__/rebuild-marker.test.ts +13 -3
  88. package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
  89. package/src/db/__tests__/schema-inspection.test.ts +7 -0
  90. package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
  91. package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
  92. package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
  93. package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
  94. package/src/db/api.ts +2 -2
  95. package/src/db/apply-entity-event.ts +18 -14
  96. package/src/db/blind-index-cleanup.ts +55 -0
  97. package/src/db/bun-provider.ts +2 -2
  98. package/src/db/collect-table-metas.ts +6 -7
  99. package/src/db/config-seed.ts +9 -9
  100. package/src/db/connection.ts +7 -12
  101. package/src/db/cursor.ts +1 -18
  102. package/src/db/dialect.ts +20 -26
  103. package/src/db/entity-field-encryption.ts +81 -24
  104. package/src/db/entity-table-meta-types.ts +2 -0
  105. package/src/db/entity-table-meta.ts +63 -90
  106. package/src/db/event-store-executor-context.ts +404 -0
  107. package/src/db/event-store-executor-read.ts +275 -0
  108. package/src/db/event-store-executor-write.ts +644 -0
  109. package/src/db/event-store-executor.ts +28 -1155
  110. package/src/db/feature-table-sources.ts +6 -5
  111. package/src/db/index.ts +20 -3
  112. package/src/db/located-timestamp.ts +4 -0
  113. package/src/db/migrate-generator.ts +39 -6
  114. package/src/db/migrate-runner.ts +107 -11
  115. package/src/db/pg-error.ts +9 -1
  116. package/src/db/postgres-provider.ts +2 -2
  117. package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
  118. package/src/db/queries/backfill-pii.ts +277 -0
  119. package/src/db/queries/ddl.ts +45 -0
  120. package/src/db/queries/event-consumer.ts +35 -2
  121. package/src/db/queries/event-store.ts +126 -19
  122. package/src/db/queries/projection-rebuild.ts +22 -8
  123. package/src/db/queries/shadow-swap.ts +183 -0
  124. package/src/db/queries/test-stack.ts +4 -30
  125. package/src/db/query-api.ts +1 -0
  126. package/src/db/query.ts +1 -0
  127. package/src/db/rebuild-marker.ts +18 -2
  128. package/src/db/reference-data.ts +2 -3
  129. package/src/db/replay-migration-sql.ts +257 -0
  130. package/src/db/schema-inspection.ts +1 -1
  131. package/src/db/table-builder.ts +102 -71
  132. package/src/db/tenant-db.ts +15 -53
  133. package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
  134. package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
  135. package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
  136. package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
  137. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
  138. package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
  139. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
  140. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
  141. package/src/engine/__tests__/boot-validator.test.ts +219 -15
  142. package/src/engine/__tests__/build-app-schema.test.ts +82 -0
  143. package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
  144. package/src/engine/__tests__/build-target.test.ts +3 -11
  145. package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
  146. package/src/engine/__tests__/config-helpers.test.ts +16 -0
  147. package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
  148. package/src/engine/__tests__/define-roles.test.ts +21 -0
  149. package/src/engine/__tests__/engine.test.ts +163 -1
  150. package/src/engine/__tests__/entity-handlers.test.ts +80 -0
  151. package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
  152. package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
  153. package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
  154. package/src/engine/__tests__/factories-time.test.ts +2 -66
  155. package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
  156. package/src/engine/__tests__/feature-manifest.test.ts +31 -1
  157. package/src/engine/__tests__/field-access.test.ts +23 -1
  158. package/src/engine/__tests__/hook-phases.test.ts +5 -5
  159. package/src/engine/__tests__/membership-roles.test.ts +4 -10
  160. package/src/engine/__tests__/nav.test.ts +86 -1
  161. package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
  162. package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
  163. package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
  164. package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
  165. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
  166. package/src/engine/__tests__/post-query-hook.test.ts +7 -7
  167. package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
  168. package/src/engine/__tests__/registry.test.ts +144 -0
  169. package/src/engine/__tests__/schema-builder.test.ts +18 -0
  170. package/src/engine/__tests__/screen.test.ts +147 -1
  171. package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
  172. package/src/engine/__tests__/store-table.test.ts +229 -0
  173. package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
  174. package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
  175. package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
  176. package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
  177. package/src/engine/boot-validator/action-wiring.ts +149 -0
  178. package/src/engine/boot-validator/boot-check.ts +21 -0
  179. package/src/engine/boot-validator/config-deps.ts +40 -4
  180. package/src/engine/boot-validator/entity-handler.ts +20 -21
  181. package/src/engine/boot-validator/entity-list-screens.ts +88 -0
  182. package/src/engine/boot-validator/gdpr-storage.ts +0 -20
  183. package/src/engine/boot-validator/i18n-keys.ts +58 -4
  184. package/src/engine/boot-validator/index.ts +33 -12
  185. package/src/engine/boot-validator/nav.ts +125 -0
  186. package/src/engine/boot-validator/pii-retention.ts +95 -10
  187. package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
  188. package/src/engine/boot-validator/workspaces.ts +68 -0
  189. package/src/engine/build-app-schema.ts +16 -0
  190. package/src/engine/build-config-feature-schema.ts +44 -7
  191. package/src/engine/codemod/pipeline-codemod.ts +5 -5
  192. package/src/engine/config-helpers.ts +15 -0
  193. package/src/engine/constants.ts +32 -6
  194. package/src/engine/create-app.ts +11 -0
  195. package/src/engine/define-feature.ts +95 -947
  196. package/src/engine/define-handler.ts +28 -94
  197. package/src/engine/define-workflow.ts +1 -1
  198. package/src/engine/effective-features.ts +12 -2
  199. package/src/engine/entity-handlers.ts +76 -11
  200. package/src/engine/extensions/tenant-data.ts +19 -0
  201. package/src/engine/extensions/user-data.ts +29 -2
  202. package/src/engine/factories.ts +8 -49
  203. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
  204. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
  205. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
  206. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
  207. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
  208. package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
  209. package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
  210. package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
  211. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
  212. package/src/engine/feature-ast/extractors/events.ts +322 -0
  213. package/src/engine/feature-ast/extractors/handlers.ts +234 -0
  214. package/src/engine/feature-ast/extractors/hooks.ts +243 -0
  215. package/src/engine/feature-ast/extractors/index.ts +34 -31
  216. package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
  217. package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
  218. package/src/engine/feature-ast/extractors/round1.ts +3 -3
  219. package/src/engine/feature-ast/extractors/round5.ts +3 -3
  220. package/src/engine/feature-ast/extractors/round6.ts +2 -36
  221. package/src/engine/feature-ast/extractors/shared.ts +64 -6
  222. package/src/engine/feature-ast/index.ts +2 -4
  223. package/src/engine/feature-ast/parse.ts +130 -23
  224. package/src/engine/feature-ast/patch.ts +39 -50
  225. package/src/engine/feature-ast/patcher.ts +37 -38
  226. package/src/engine/feature-ast/patterns.ts +50 -62
  227. package/src/engine/feature-ast/render.ts +67 -44
  228. package/src/engine/feature-builder-state.ts +168 -0
  229. package/src/engine/feature-config-events-jobs.ts +403 -0
  230. package/src/engine/feature-entity-handlers.ts +208 -0
  231. package/src/engine/feature-manifest.ts +2 -1
  232. package/src/engine/feature-ui-extensions.ts +500 -0
  233. package/src/engine/field-access.ts +13 -2
  234. package/src/engine/field-helpers.ts +31 -0
  235. package/src/engine/handler-helpers.ts +26 -0
  236. package/src/engine/hook-helpers.ts +16 -0
  237. package/src/engine/index.ts +23 -7
  238. package/src/engine/membership-roles.ts +13 -0
  239. package/src/engine/object-form.ts +27 -0
  240. package/src/engine/ownership.ts +26 -79
  241. package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
  242. package/src/engine/pattern-library/library.ts +44 -1152
  243. package/src/engine/pattern-library/mixed-schemas.ts +450 -0
  244. package/src/engine/pattern-library/opaque-schemas.ts +124 -0
  245. package/src/engine/pattern-library/shared-fields.ts +75 -0
  246. package/src/engine/pattern-library/static-schemas.ts +456 -0
  247. package/src/engine/pipeline.ts +6 -11
  248. package/src/engine/registry-facade.ts +362 -0
  249. package/src/engine/registry-ingest.ts +478 -0
  250. package/src/engine/registry-state.ts +388 -0
  251. package/src/engine/registry-validate.ts +642 -0
  252. package/src/engine/registry.ts +78 -1658
  253. package/src/engine/run-pipeline.ts +1 -1
  254. package/src/engine/schema-builder.ts +1 -0
  255. package/src/engine/screen-helpers.ts +54 -0
  256. package/src/engine/soft-delete-cleanup.ts +6 -2
  257. package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
  258. package/src/engine/steps/_duration-utils.ts +2 -0
  259. package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
  260. package/src/engine/tier-resolver-extension.ts +3 -2
  261. package/src/engine/types/config.ts +2 -482
  262. package/src/engine/types/define-handler.ts +2 -0
  263. package/src/engine/types/entity-handlers.ts +2 -0
  264. package/src/engine/types/event-type-map.ts +1 -37
  265. package/src/engine/types/feature.ts +2 -972
  266. package/src/engine/types/fields.ts +2 -675
  267. package/src/engine/types/handlers.ts +2 -774
  268. package/src/engine/types/hooks.ts +2 -184
  269. package/src/engine/types/http-route.ts +1 -54
  270. package/src/engine/types/identifiers.ts +1 -47
  271. package/src/engine/types/index.ts +86 -40
  272. package/src/engine/types/nav.ts +2 -63
  273. package/src/engine/types/ownership.ts +2 -0
  274. package/src/engine/types/projection.ts +2 -138
  275. package/src/engine/types/relations.ts +1 -51
  276. package/src/engine/types/screen.ts +2 -574
  277. package/src/engine/types/step.ts +2 -334
  278. package/src/engine/types/target-ref.ts +1 -21
  279. package/src/engine/types/tree-node.ts +1 -132
  280. package/src/engine/types/workspace.ts +2 -49
  281. package/src/engine/validate-projection-allowlist.ts +6 -6
  282. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
  283. package/src/entrypoint/index.ts +49 -6
  284. package/src/errors/classes.ts +22 -1
  285. package/src/errors/field-issue.ts +0 -3
  286. package/src/errors/index.ts +1 -1
  287. package/src/errors/write-error-info.ts +10 -22
  288. package/src/es-ops/README.md +1 -1
  289. package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
  290. package/src/es-ops/context.ts +4 -2
  291. package/src/es-ops/types.ts +8 -1
  292. package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
  293. package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
  294. package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
  295. package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
  296. package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
  297. package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
  298. package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
  299. package/src/event-store/admin-api.ts +11 -4
  300. package/src/event-store/errors.ts +2 -35
  301. package/src/event-store/event-store.ts +64 -78
  302. package/src/event-store/events-schema.ts +11 -13
  303. package/src/event-store/index.ts +18 -3
  304. package/src/event-store/rebuild-dead-letter.ts +111 -0
  305. package/src/event-store/snapshot.ts +63 -40
  306. package/src/event-store/types.ts +2 -0
  307. package/src/files/__tests__/build-storage-key.test.ts +28 -0
  308. package/src/files/__tests__/file-ref-entity.test.ts +8 -0
  309. package/src/files/__tests__/files.integration.test.ts +24 -0
  310. package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
  311. package/src/files/__tests__/local-provider.test.ts +31 -0
  312. package/src/files/__tests__/provider-resolver.test.ts +70 -0
  313. package/src/files/__tests__/write-stream.test.ts +13 -0
  314. package/src/files/file-handle.ts +2 -19
  315. package/src/files/file-ref-entity.ts +2 -2
  316. package/src/files/file-routes.ts +13 -0
  317. package/src/files/index.ts +1 -1
  318. package/src/files/local-provider.ts +27 -8
  319. package/src/files/provider-resolver.ts +31 -20
  320. package/src/files/types.ts +13 -55
  321. package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
  322. package/src/i18n/required-surface-keys.ts +120 -1
  323. package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
  324. package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
  325. package/src/jobs/job-runner.ts +82 -12
  326. package/src/logging/types.ts +1 -7
  327. package/src/migrations/pending-rebuilds.ts +1 -1
  328. package/src/observability/__tests__/observability.integration.test.ts +4 -1
  329. package/src/observability/__tests__/recording-tracer.test.ts +6 -4
  330. package/src/observability/index.ts +2 -0
  331. package/src/observability/noop-provider.ts +0 -9
  332. package/src/observability/recording-tracer.ts +0 -12
  333. package/src/observability/standard-metrics.ts +64 -2
  334. package/src/observability/types/index.ts +1 -29
  335. package/src/observability/types/metric.ts +1 -56
  336. package/src/observability/types/provider.ts +1 -32
  337. package/src/observability/types/span.ts +1 -64
  338. package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
  339. package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
  340. package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
  341. package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
  342. package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
  343. package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
  344. package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
  345. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
  346. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
  347. package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
  348. package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
  349. package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
  350. package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
  351. package/src/pipeline/dispatch-batch.ts +187 -0
  352. package/src/pipeline/dispatch-query.ts +165 -0
  353. package/src/pipeline/dispatch-shared.ts +826 -0
  354. package/src/pipeline/dispatch-stream.ts +90 -0
  355. package/src/pipeline/dispatch-write.ts +451 -0
  356. package/src/pipeline/dispatcher-utils.ts +1 -1
  357. package/src/pipeline/dispatcher.ts +44 -1372
  358. package/src/pipeline/entity-cache.ts +2 -33
  359. package/src/pipeline/event-consumer-state.ts +30 -2
  360. package/src/pipeline/event-dispatcher-admin.ts +293 -0
  361. package/src/pipeline/event-dispatcher-delivery.ts +305 -0
  362. package/src/pipeline/event-dispatcher.ts +85 -542
  363. package/src/pipeline/index.ts +2 -0
  364. package/src/pipeline/msp-rebuild.ts +42 -3
  365. package/src/pipeline/multi-stream-apply-context.ts +4 -42
  366. package/src/pipeline/projection-rebuild.ts +105 -3
  367. package/src/pipeline/system-hooks.ts +144 -1
  368. package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
  369. package/src/rate-limit/resolver.ts +16 -32
  370. package/src/schema-cli.ts +76 -16
  371. package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
  372. package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
  373. package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
  374. package/src/search/index.ts +6 -0
  375. package/src/search/meilisearch-adapter.ts +13 -12
  376. package/src/search/reindex-entity.ts +177 -0
  377. package/src/search/types.ts +1 -39
  378. package/src/secrets/__tests__/contains-secret.test.ts +34 -0
  379. package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
  380. package/src/secrets/__tests__/envelope.test.ts +1 -1
  381. package/src/secrets/dek-cache.ts +26 -5
  382. package/src/secrets/envelope-cipher.ts +53 -0
  383. package/src/secrets/envelope.ts +5 -3
  384. package/src/secrets/index.ts +13 -1
  385. package/src/secrets/stored-envelope.ts +46 -0
  386. package/src/secrets/types.ts +2 -162
  387. package/src/stack/__tests__/event-collector.test.ts +42 -0
  388. package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
  389. package/src/stack/db.ts +2 -1
  390. package/src/stack/push-entity-projection-tables.ts +4 -3
  391. package/src/stack/redis.ts +8 -0
  392. package/src/stack/request-helper.ts +38 -2
  393. package/src/stack/table-helpers.ts +6 -4
  394. package/src/stack/test-stack.ts +249 -131
  395. package/src/testing/__tests__/late-bound.test.ts +32 -0
  396. package/src/testing/__tests__/wait-for.test.ts +59 -0
  397. package/src/testing/boot-validator-fixture.ts +121 -0
  398. package/src/testing/e2e-generator.ts +8 -0
  399. package/src/testing/file-provider-contract.ts +104 -0
  400. package/src/testing/handler-context.ts +3 -1
  401. package/src/testing/index.ts +5 -0
  402. package/src/testing/late-bound.ts +5 -3
  403. package/src/testing/mutable-master-key-provider.ts +29 -3
  404. package/src/testing/wait-for.ts +3 -0
  405. package/src/testing/without-ambient-temporal.ts +14 -0
  406. package/src/time/__tests__/tz-dateline.test.ts +8 -10
  407. package/src/time/geo-tz.ts +1 -32
  408. package/src/time/index.ts +1 -0
  409. package/src/time/legacy-date.ts +18 -0
  410. package/src/time/polyfill.ts +21 -38
  411. package/src/time/tz-context.ts +44 -85
  412. package/src/ui-types/app-schema.ts +12 -0
  413. package/src/ui-types/index.ts +20 -7
  414. package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
  415. package/src/utils/safe-json.ts +13 -1
  416. package/src/__tests__/raw-table.integration.test.ts +0 -116
  417. package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
  418. package/src/db/__tests__/encryption.test.ts +0 -39
  419. package/src/db/encryption.ts +0 -39
  420. package/src/db/row-helpers.ts +0 -4
  421. package/src/engine/__tests__/raw-table.test.ts +0 -150
  422. package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
  423. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
  424. package/src/engine/feature-ast/extractors/round4.ts +0 -1366
package/src/api/server.ts CHANGED
@@ -3,6 +3,7 @@ import type { DbConnection, PgClient } from "../db/connection";
3
3
  import { createTenantDb } from "../db/tenant-db";
4
4
  import { EXT_FILE_PROVIDER } from "../engine/extension-names";
5
5
  import { runsInLane } from "../engine/run-in";
6
+ import { createAnonymousUser } from "../engine/system-user";
6
7
  import {
7
8
  type AppContext,
8
9
  isFileField,
@@ -33,6 +34,8 @@ import { createEventDispatcher } from "../pipeline/event-dispatcher";
33
34
  import { createLifecycleHooks, type SystemHooks } from "../pipeline/lifecycle-pipeline";
34
35
  import { createMultiStreamApplyContext } from "../pipeline/multi-stream-apply-context";
35
36
  import {
37
+ createAccessInvalidationEventConsumer,
38
+ createJobTriggerEventConsumer,
36
39
  createSearchEventConsumer,
37
40
  createSseBroadcastEventConsumer,
38
41
  } from "../pipeline/system-hooks";
@@ -46,12 +49,13 @@ import {
46
49
  import type { SearchAdapter } from "../search/types";
47
50
  import { assertUnreachable, generateId } from "../utils";
48
51
  import { PUBLIC_API_PATHS } from "./api-constants";
49
- import { type AnonymousAccessConfig, authMiddleware } from "./auth-middleware";
52
+ import { type AnonymousAccessResolved, authMiddleware, getUser } from "./auth-middleware";
50
53
  import { type AuthRoutesConfig, createAuthRoutes } from "./auth-routes";
51
54
  import { csrfMiddleware } from "./csrf-middleware";
52
- import { createJwtHelper, type JwtHelper } from "./jwt";
55
+ import { createJwtHelper, type JwtHelper, type JwtKeyring } from "./jwt";
53
56
  import { observabilityMiddleware } from "./observability-middleware";
54
57
  import { assertOriginGuardConfig, originMiddleware } from "./origin-middleware";
58
+ import { piiCiphertextResponseGuard } from "./pii-leak-guard";
55
59
  import { requestIdMiddleware } from "./request-id-middleware";
56
60
  import {
57
61
  DEFAULT_MAX_REQUEST_BYTES,
@@ -67,8 +71,13 @@ import { createSseRoute } from "./sse-route";
67
71
  export type ServerOptions = {
68
72
  registry: Registry;
69
73
  context: AppContext;
70
- jwtSecret: string;
74
+ jwtSecret: string | JwtKeyring;
71
75
  jwtIssuer?: string;
76
+ // JWT lifetime in seconds. Explicit always wins. When omitted, the default
77
+ // depends on `auth.sessionChecker`: wired (revocation possible) keeps the
78
+ // long-lived 24h default; unwired (stateless JWTs, no revocation) drops to
79
+ // 1h so a leaked stateless token has a much smaller exposure window.
80
+ jwtTtl?: number;
72
81
  dispatcherOptions?: Omit<DispatcherOptions, "lifecycle">;
73
82
  systemHooks?: SystemHooks;
74
83
  eventDedup?: EventDedup;
@@ -92,16 +101,25 @@ export type ServerOptions = {
92
101
  pollIntervalMs?: number;
93
102
  batchSize?: number;
94
103
  maxAttempts?: number;
104
+ rearmCooldownMs?: number;
105
+ maxRearmCount?: number;
95
106
  // Opt out of building the dispatcher even if consumers exist — e.g. ops
96
107
  // runs a dedicated dispatcher process, or a test needs to control the
97
108
  // consumer lifecycle manually.
98
109
  disabled?: boolean;
99
- // Opt out of the auto-built system consumers (SSE, Search) while still
100
- // running feature r.multiStreamProjection consumers. Useful for tests
101
- // that assert only on subscriber behaviour, or for a deployment that
102
- // routes SSE via a different transport. Default: both enabled when the
103
- // respective dependency (sseBroker / context.searchAdapter) is available.
104
- systemConsumers?: { sse?: boolean; search?: boolean };
110
+ // Opt out of the auto-built system consumers (SSE, Search, Job-Trigger)
111
+ // while still running feature r.multiStreamProjection consumers. Useful
112
+ // for tests that assert only on subscriber behaviour, or for a
113
+ // deployment that routes SSE via a different transport. Default: sse/
114
+ // search enabled when the respective dependency (sseBroker /
115
+ // context.searchAdapter) is available; jobTrigger enabled when a
116
+ // jobRunner is wired via dispatcherOptions.
117
+ systemConsumers?: {
118
+ sse?: boolean;
119
+ search?: boolean;
120
+ jobTrigger?: boolean;
121
+ accessInvalidation?: boolean;
122
+ };
105
123
  // Raw postgres.js client for LISTEN/NOTIFY wake-up (Sprint E.4). When
106
124
  // present, `.start()` subscribes to EVENTS_PUBSUB_CHANNEL — delivery
107
125
  // latency drops from pollIntervalMs to TCP-round-trip. The poll timer
@@ -192,9 +210,10 @@ export type ServerOptions = {
192
210
  instanceId?: string;
193
211
  // Opt-in: serve unauthenticated requests on handlers that allow
194
212
  // roles=["anonymous"]. When omitted, every /api/* request still requires
195
- // a valid JWT (status quo). See AnonymousAccessConfig for the resolution
196
- // chain (header cookie resolver → defaultTenantId).
197
- anonymousAccess?: AnonymousAccessConfig;
213
+ // a valid JWT (status quo). App-facing config is AnonymousAccessConfig
214
+ // (defaultTenantId only); run{Prod,Dev}App merge auth-foundation tenant
215
+ // providers into AnonymousAccessResolved before calling buildServer.
216
+ anonymousAccess?: AnonymousAccessResolved;
198
217
  };
199
218
 
200
219
  export type KumikoServer = {
@@ -236,7 +255,15 @@ export function buildServer(options: ServerOptions): KumikoServer {
236
255
  );
237
256
  }
238
257
 
239
- const jwt = createJwtHelper(options.jwtSecret, options.jwtIssuer);
258
+ // Stateless JWTs (no sessionChecker → no revocation) default to a shorter
259
+ // TTL than session-backed ones, since a leaked stateless token can't be
260
+ // revoked and stays valid until it expires. Explicit jwtTtl always wins.
261
+ const defaultJwtTtl = options.auth?.sessionChecker ? 24 * 60 * 60 : 60 * 60;
262
+ const jwt = createJwtHelper(
263
+ options.jwtSecret,
264
+ options.jwtIssuer,
265
+ options.jwtTtl ?? defaultJwtTtl,
266
+ );
240
267
  const sseBroker = options.sseBroker ?? createSseBroker();
241
268
 
242
269
  // Resolve the per-process instance identifier. Prefer explicit
@@ -358,6 +385,7 @@ export function buildServer(options: ServerOptions): KumikoServer {
358
385
  const dispatcher = createDispatcher(options.registry, contextWithObservability, {
359
386
  ...options.dispatcherOptions,
360
387
  lifecycle,
388
+ sseBroker,
361
389
  });
362
390
 
363
391
  // Async event-dispatcher — the replacement for the old transactional
@@ -380,6 +408,8 @@ export function buildServer(options: ServerOptions): KumikoServer {
380
408
 
381
409
  const sseConsumerEnabled = options.eventDispatcher?.systemConsumers?.sse ?? true;
382
410
  const searchConsumerEnabled = options.eventDispatcher?.systemConsumers?.search ?? true;
411
+ const jobTriggerConsumerEnabled = options.eventDispatcher?.systemConsumers?.jobTrigger ?? true;
412
+ const jobRunnerForTriggers = options.dispatcherOptions?.jobRunner;
383
413
 
384
414
  const systemConsumers: EventConsumer[] = [];
385
415
  if (sseConsumerEnabled) {
@@ -388,6 +418,18 @@ export function buildServer(options: ServerOptions): KumikoServer {
388
418
  if (searchConsumerEnabled && searchAdapter) {
389
419
  systemConsumers.push(createSearchEventConsumer(searchAdapter, options.registry));
390
420
  }
421
+ if (jobTriggerConsumerEnabled && jobRunnerForTriggers) {
422
+ systemConsumers.push(createJobTriggerEventConsumer(jobRunnerForTriggers, options.registry));
423
+ }
424
+ // Default ON (#1524 global-by-default). Tests that opt out of SSE via
425
+ // systemConsumers.accessInvalidation=false (test-stack mirrors sse off)
426
+ // skip the row so retention prune suites are not blocked by a lagging
427
+ // cursor=0 consumer they never drain.
428
+ const accessInvalidationEnabled =
429
+ options.eventDispatcher?.systemConsumers?.accessInvalidation ?? true;
430
+ if (accessInvalidationEnabled) {
431
+ systemConsumers.push(createAccessInvalidationEventConsumer(sseBroker));
432
+ }
391
433
 
392
434
  // MultiStreamProjections: one EventConsumer per MSP. Handler routes by
393
435
  // event.type into the MSP's apply map. MSPs aggregate cross-aggregate but
@@ -553,6 +595,10 @@ export function buildServer(options: ServerOptions): KumikoServer {
553
595
  }),
554
596
  );
555
597
 
598
+ // Tripwire: a kumiko-pii: ciphertext in a JSON response is always a bug
599
+ // (raw read leaked past a decrypt) — dev fails loud, prod redacts (#820).
600
+ app.use("/api/*", piiCiphertextResponseGuard());
601
+
556
602
  // Auth middleware skips public paths (login, health) — those routes need
557
603
  // to be callable without a valid JWT. Every other /api/* request requires
558
604
  // a token (or, when anonymousAccess is wired, falls through as anonymous).
@@ -560,7 +606,10 @@ export function buildServer(options: ServerOptions): KumikoServer {
560
606
  // middleware can reject revoked sids on every request.
561
607
  const jwtGuard = authMiddleware(jwt, {
562
608
  ...(options.auth?.sessionChecker ? { sessionChecker: options.auth.sessionChecker } : {}),
563
- ...(options.auth?.sessionStrictMode ? { strictMode: options.auth.sessionStrictMode } : {}),
609
+ ...(options.auth?.tokenVerifier ? { tokenVerifier: options.auth.tokenVerifier } : {}),
610
+ ...(options.auth?.resolveTenantLifecycleStatus
611
+ ? { resolveTenantLifecycleStatus: options.auth.resolveTenantLifecycleStatus }
612
+ : {}),
564
613
  ...(options.anonymousAccess ? { anonymousAccess: options.anonymousAccess } : {}),
565
614
  });
566
615
  app.use("/api/*", async (c, next) => {
@@ -568,6 +617,33 @@ export function buildServer(options: ServerOptions): KumikoServer {
568
617
  return jwtGuard(c, next);
569
618
  });
570
619
 
620
+ // PAT rate limiting — runs AFTER the auth guard so the resolved principal is
621
+ // available. Only PAT-authenticated requests are counted (keyed by token id);
622
+ // cookie/JWT users pass through untouched. In-memory limiter is per-instance
623
+ // (see run-prod-app) — a multi-node deployment wanting a shared counter swaps
624
+ // in a Redis-backed LoginRateLimiter.
625
+ const patRateLimiter = options.auth?.patRateLimiter;
626
+ if (patRateLimiter) {
627
+ app.use("/api/*", async (c, next) => {
628
+ if (PUBLIC_API_PATHS.has(c.req.path)) return next();
629
+ const pat = getUser(c)?.pat;
630
+ if (pat && !(await patRateLimiter.check(pat.tokenId))) {
631
+ return c.json(
632
+ {
633
+ error: {
634
+ code: "pat_rate_limited",
635
+ httpStatus: 429,
636
+ message: "personal access token rate limit exceeded",
637
+ i18nKey: "auth.errors.patRateLimited",
638
+ },
639
+ },
640
+ 429,
641
+ );
642
+ }
643
+ return next();
644
+ });
645
+ }
646
+
571
647
  // Origin-allowlist guard — additional CSRF-hardening for deployments that
572
648
  // widen the auth cookie across subdomains (auth.cookieDomain). Registered
573
649
  // only when allowedOrigins is non-empty, with the same /api/* + public-skip
@@ -634,12 +710,32 @@ export function buildServer(options: ServerOptions): KumikoServer {
634
710
  // /api/* immer Vorrang hat — feature-Routes liegen ohnehin außerhalb
635
711
  // (Boot-Validator blockt /api-Prefix). deps.app ist die Outer-App, sodass
636
712
  // der Handler /api/query intern via app.fetch(...) nutzen kann (gleicher
637
- // Auth-Pfad wie ein echter HTTP-Call).
713
+ // Auth-Pfad wie ein echter HTTP-Call). deps.systemQuery umgeht diesen
714
+ // Pfad bewusst — für Routes die einen FESTEN Tenant erzwingen müssen
715
+ // (z.B. legal-pages → immer SYSTEM_TENANT_ID), statt ihn per X-Tenant-
716
+ // Header vorzutäuschen (das sieht für die anonymousAccess-Auflösung
717
+ // wie ein normaler Client-Header aus und wird von resolverTrust:
718
+ // "authoritative"-Configs zurecht als Tenant-Override abgelehnt).
638
719
  for (const feature of options.registry.features.values()) {
639
720
  for (const route of Object.values(feature.httpRoutes)) {
640
721
  // @wrapper-known semantic-alias
641
722
  const honoHandler = async (c: import("hono").Context): Promise<Response> =>
642
- route.handler(c, { app });
723
+ route.handler(c, {
724
+ app,
725
+ systemQuery: (type, payload, tenantId) =>
726
+ // createAnonymousUser, NOT createSystemUser: httpRoute handlers
727
+ // using systemQuery are, by construction, `anonymous: true`
728
+ // public routes — the synthesized user must clear the SAME
729
+ // access gate a real anonymous visitor would, no more. The
730
+ // system role would ALSO satisfy that gate here, but it can
731
+ // read fields gated to "system" that "anonymous" can't
732
+ // (filterReadFields is a plain role-in-map check) — a future
733
+ // systemQuery caller reading a system-gated field would leak
734
+ // it into a public response. The forced tenant already comes
735
+ // from bypassing the HTTP layer entirely; no elevated role
736
+ // is needed or wanted on top of that.
737
+ dispatcher.query(type, payload, createAnonymousUser(tenantId)),
738
+ });
643
739
  switch (route.method) {
644
740
  case "GET":
645
741
  app.get(route.path, honoHandler);
@@ -1,3 +1,4 @@
1
+ import { userAccessChannel } from "../engine/constants";
1
2
  import { generateId } from "../utils";
2
3
 
3
4
  export type SseClient = {
@@ -17,10 +18,18 @@ export type SseBroker = {
17
18
  pushToChannel(channel: string, event: SseEvent): void;
18
19
  getClientCount(channel: string): number;
19
20
  getTotalClientCount(): number;
21
+ // Internal (non-SSE-client) subscription, e.g. dispatch-stream watching
22
+ // for mid-stream access revocation. Kept separate from addClient/
23
+ // pushToChannel: those count towards getClientCount/getTotalClientCount
24
+ // (real SSE connections) and their send/close shape doesn't fit a plain
25
+ // callback listener. Returns an unsubscribe function.
26
+ subscribeAccessInvalidation(userId: string, onInvalidate: () => void): () => void;
27
+ publishAccessInvalidation(userId: string): void;
20
28
  };
21
29
 
22
30
  export function createSseBroker(): SseBroker {
23
31
  const channels = new Map<string, Map<string, SseClient>>();
32
+ const accessInvalidationListeners = new Map<string, Map<string, () => void>>();
24
33
 
25
34
  function getOrCreateChannel(channel: string): Map<string, SseClient> {
26
35
  let clients = channels.get(channel);
@@ -67,5 +76,35 @@ export function createSseBroker(): SseBroker {
67
76
  }
68
77
  return total;
69
78
  },
79
+
80
+ subscribeAccessInvalidation(userId, onInvalidate) {
81
+ const channel = userAccessChannel(userId);
82
+ const listenerId = generateId();
83
+ let listeners = accessInvalidationListeners.get(channel);
84
+ if (!listeners) {
85
+ listeners = new Map();
86
+ accessInvalidationListeners.set(channel, listeners);
87
+ }
88
+ listeners.set(listenerId, onInvalidate);
89
+ return () => {
90
+ const current = accessInvalidationListeners.get(channel);
91
+ // skip: already unsubscribed (e.g. stream ended after a publish already fired)
92
+ if (!current) return;
93
+ current.delete(listenerId);
94
+ if (current.size === 0) accessInvalidationListeners.delete(channel);
95
+ };
96
+ },
97
+
98
+ publishAccessInvalidation(userId) {
99
+ const channel = userAccessChannel(userId);
100
+ const listeners = accessInvalidationListeners.get(channel);
101
+ // skip: no live stream is watching this user right now
102
+ if (!listeners) return;
103
+ // Snapshot before iterating — a fired listener unsubscribes itself,
104
+ // which would mutate `listeners` mid-iteration otherwise.
105
+ for (const onInvalidate of [...listeners.values()]) {
106
+ onInvalidate();
107
+ }
108
+ },
70
109
  };
71
110
  }
@@ -8,7 +8,6 @@ db/postgres-provider.ts postgres-js Factory (default, stabil)
8
8
  db/bun-provider.ts Bun.SQL Factory (DB_PROVIDER=bun, experimentell)
9
9
  stack/db.ts createTestDb() — provider-agnostisch via createConnection
10
10
  stack/test-stack.ts setupTestStack() — provider-agnostisch via createTestDb
11
- bun-db/__tests__/bun-test-stack.ts Alias: setupBunTestStack → setupTestStack
12
11
  bun-db/__tests__/bun-test-db.ts Alias: createBunTestDb → createTestDb
13
12
  ```
14
13
 
@@ -0,0 +1,41 @@
1
+ // kumiko-framework#1480: instantFromDriver referenced the global `Temporal`
2
+ // without importing it. Bun doesn't expose Temporal as a globalThis property
3
+ // reliably — any storeTable read of a timestamptz column crashed with
4
+ // "Temporal is not defined" unless some other boot path happened to install
5
+ // the polyfill globally first (order-dependent, easy to miss in a fresh
6
+ // process). The fix is a static `import { Temporal } from "temporal-polyfill"`
7
+ // in query.ts, so this test deletes globalThis.Temporal before calling
8
+ // coerceRow — proving the coercion no longer depends on the global at all.
9
+
10
+ import { describe, expect, test } from "bun:test";
11
+ import { Temporal } from "temporal-polyfill";
12
+ import { coerceRow, type TableInfo } from "../query";
13
+
14
+ function timestamptzTableInfo(): TableInfo {
15
+ return {
16
+ name: "probe",
17
+ columnOf: (f) => f,
18
+ pgTypeOf: (c) => (c === "updated_at" ? "timestamptz" : undefined),
19
+ bigintJsModeOf: () => undefined,
20
+ fieldOf: (c) => c,
21
+ hasColumn: () => true,
22
+ };
23
+ }
24
+
25
+ describe("coerceRow — timestamptz → Temporal.Instant", () => {
26
+ test("coerces without relying on a global Temporal", () => {
27
+ const savedGlobal = (globalThis as { Temporal?: unknown }).Temporal;
28
+ delete (globalThis as { Temporal?: unknown }).Temporal;
29
+ try {
30
+ const row = { updated_at: new Date("2026-04-18T10:00:00Z") };
31
+ const result = coerceRow(row, timestamptzTableInfo());
32
+ expect(result.updated_at).toBeInstanceOf(Temporal.Instant);
33
+ expect((result.updated_at as unknown as Temporal.Instant).toString()).toBe(
34
+ "2026-04-18T10:00:00Z",
35
+ );
36
+ } finally {
37
+ if (savedGlobal === undefined) delete (globalThis as { Temporal?: unknown }).Temporal;
38
+ else (globalThis as { Temporal?: unknown }).Temporal = savedGlobal;
39
+ }
40
+ });
41
+ });
@@ -0,0 +1,79 @@
1
+ // #1163: Bun.SQL can hand out a closed connection under load (AbortError
2
+ // "The connection was closed."). Pure reads retry exactly once on a fresh
3
+ // pool checkout; tx handles, non-matching errors, and genuine user aborts
4
+ // must NOT retry.
5
+
6
+ import { describe, expect, test } from "bun:test";
7
+ import { buildEntityTable } from "../../db/table-builder";
8
+ import { selectMany } from "../query";
9
+
10
+ function closedConnectionError(): Error {
11
+ return Object.assign(new Error("The connection was closed."), { name: "AbortError" });
12
+ }
13
+
14
+ type FakeClient = {
15
+ unsafe: (sql: string, params?: readonly unknown[]) => Promise<readonly unknown[]>;
16
+ begin?: () => never;
17
+ calls: number;
18
+ };
19
+
20
+ function fakeClient(failures: Error[], opts: { tx?: boolean } = {}): FakeClient {
21
+ const remaining = [...failures];
22
+ const client: FakeClient = {
23
+ calls: 0,
24
+ unsafe: async () => {
25
+ client.calls++;
26
+ const err = remaining.shift();
27
+ if (err) throw err;
28
+ return [{ id: "r1", title: "ok", tenant_id: "t1", inserted_at: null, updated_at: null }];
29
+ },
30
+ };
31
+ // A top-level pool client has begin(); a transaction handle does not.
32
+ if (!opts.tx)
33
+ client.begin = () => {
34
+ throw new Error("not used in test");
35
+ };
36
+ return client;
37
+ }
38
+
39
+ const table = buildEntityTable("note", {
40
+ fields: { title: { type: "text", required: true } },
41
+ });
42
+
43
+ describe("selectMany — closed-connection retry (#1163)", () => {
44
+ test("retries once on AbortError 'connection was closed' and returns rows", async () => {
45
+ const db = fakeClient([closedConnectionError()]);
46
+ const rows = await selectMany(db, table);
47
+ expect(rows).toHaveLength(1);
48
+ expect(rows[0]?.title).toBe("ok");
49
+ expect(db.calls).toBe(2);
50
+ });
51
+
52
+ test("gives up after the single retry when the connection stays closed", async () => {
53
+ const db = fakeClient([closedConnectionError(), closedConnectionError()]);
54
+ await expect(selectMany(db, table)).rejects.toThrow("connection was closed");
55
+ expect(db.calls).toBe(2);
56
+ });
57
+
58
+ test("never retries on a transaction handle (no begin)", async () => {
59
+ const db = fakeClient([closedConnectionError()], { tx: true });
60
+ await expect(selectMany(db, table)).rejects.toThrow("connection was closed");
61
+ expect(db.calls).toBe(1);
62
+ });
63
+
64
+ test("does not retry a genuine user abort (different message)", async () => {
65
+ const userAbort = Object.assign(new Error("The operation was aborted."), {
66
+ name: "AbortError",
67
+ });
68
+ const db = fakeClient([userAbort]);
69
+ await expect(selectMany(db, table)).rejects.toThrow("operation was aborted");
70
+ expect(db.calls).toBe(1);
71
+ });
72
+
73
+ test("does not retry generic query errors", async () => {
74
+ const syntax = Object.assign(new Error("syntax error at or near"), { name: "PostgresError" });
75
+ const db = fakeClient([syntax]);
76
+ await expect(selectMany(db, table)).rejects.toThrow("syntax error");
77
+ expect(db.calls).toBe(1);
78
+ });
79
+ });
@@ -8,6 +8,7 @@
8
8
  import { expect, test } from "bun:test";
9
9
  import { defineUnmanagedTable } from "../../db/entity-table-meta";
10
10
  import { buildEntityTable } from "../../db/table-builder";
11
+ import type { TenantDb } from "../../db/tenant-db";
11
12
  import { createEntity, createTextField } from "../../engine";
12
13
  import { type AnyDb, deleteMany, insertOne, selectMany, updateMany } from "../query";
13
14
 
@@ -41,8 +42,28 @@ async function _readAllowsManagedEntity(db: AnyDb): Promise<void> {
41
42
  await selectMany(db, brandedEntity, { id: "1" });
42
43
  }
43
44
 
45
+ // Method-form (ctx.db.insertOne/updateMany/deleteMany) rejects the brand too — a
46
+ // projection written past its event stream is wiped on rebuild whether the write
47
+ // went through the free function or the TenantDb method.
48
+ async function _methodFormRejectsManagedEntity(db: TenantDb): Promise<void> {
49
+ // @ts-expect-error — managed EntityTable is executor-only; method-form insert is a compile error.
50
+ await db.insertOne(brandedEntity, { title: "x" });
51
+ // @ts-expect-error — method-form update on a managed EntityTable is rejected.
52
+ await db.updateMany(brandedEntity, { title: "y" }, { id: "1" });
53
+ // @ts-expect-error — method-form delete on a managed EntityTable is rejected.
54
+ await db.deleteMany(brandedEntity, { id: "1" });
55
+ }
56
+
57
+ async function _methodFormReadAllowsManagedEntity(db: TenantDb): Promise<void> {
58
+ // Method-form reads on a managed EntityTable stay fine (reads keep the plain param).
59
+ await db.selectMany(brandedEntity, { id: "1" });
60
+ await db.fetchOne(brandedEntity, { id: "1" });
61
+ }
62
+
44
63
  test("ES-write brand: compile-time contracts are wired", () => {
45
64
  expect(_writeRejectsManagedEntity).toBeDefined();
46
65
  expect(_writeAllowsUnmanagedTable).toBeDefined();
47
66
  expect(_readAllowsManagedEntity).toBeDefined();
67
+ expect(_methodFormRejectsManagedEntity).toBeDefined();
68
+ expect(_methodFormReadAllowsManagedEntity).toBeDefined();
48
69
  });
@@ -6,9 +6,12 @@
6
6
  // event-dispatcher.ts. Bun.sql 1.2.20 hat kein listen() (PR oven-sh/bun#25511
7
7
  // pending). Nach Landung des Bun-LISTEN-Supports: peer raus.
8
8
 
9
+ import type { PgListenClient } from "@cosmicdrift/kumiko-types/db-connection";
9
10
  import postgres from "postgres";
10
11
  import { readPositiveIntEnv } from "../utils/env-parse";
11
12
 
13
+ export type { PgListenClient };
14
+
12
15
  // Bun.SQL ist callable als tagged template `sql\`...\`` PLUS hat methods
13
16
  // (.begin / .unsafe / .end / .file / .reserve etc.). DbConnection-Type
14
17
  // reflektiert die Instance-Shape.
@@ -21,9 +24,6 @@ export type BunDbTx = BunDbConnection;
21
24
  // Beide austauschbar im normalen call-path.
22
25
  export type BunDbRunner = BunDbConnection | BunDbTx;
23
26
 
24
- // Postgres-js peer NUR für event-dispatcher LISTEN.
25
- export type PgListenClient = ReturnType<typeof postgres>;
26
-
27
27
  export type BunDbConnectionOptions = {
28
28
  readonly maxConnections?: number;
29
29
  readonly idleTimeoutSeconds?: number;
@@ -11,6 +11,7 @@ export type {
11
11
  export { bunDbConnectionOptionsFromEnv, createBunDbConnection } from "./connection";
12
12
  export type { SelectOptions, TableInfo, WhereObject, WhereOperator, WhereValue } from "./query";
13
13
  export {
14
+ asEntityTableMeta,
14
15
  asRawClient,
15
16
  countWhere,
16
17
  type DeleteManyBatchedOptions,
@@ -23,6 +24,7 @@ export {
23
24
  incrementCounter,
24
25
  insertMany,
25
26
  insertOne,
27
+ runInSavepoint,
26
28
  selectMany,
27
29
  transaction,
28
30
  type UpsertOnConflictOptions,