@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
@@ -56,21 +56,32 @@ const orderFeature = defineFeature("upcastshop", (r) => {
56
56
  const orderPriced = r.defineEvent(
57
57
  "priced",
58
58
  z.object({ totalCents: z.number().int(), currency: z.string() }),
59
- { version: 3 },
59
+ {
60
+ version: 3,
61
+ migrations: [
62
+ // v1 → v2: renamed totalEuros → total (kept as string for this step)
63
+ {
64
+ fromVersion: 1,
65
+ toVersion: 2,
66
+ transform: (payload) => {
67
+ const p = payload as { totalEuros: string };
68
+ return { total: p.totalEuros, currency: "EUR" };
69
+ },
70
+ },
71
+ // v2 → v3: parse "total" string into integer cents
72
+ {
73
+ fromVersion: 2,
74
+ toVersion: 3,
75
+ transform: (payload) => {
76
+ const p = payload as { total: string; currency: string };
77
+ const euros = Number.parseFloat(p.total);
78
+ return { totalCents: Math.round(euros * 100), currency: p.currency };
79
+ },
80
+ },
81
+ ],
82
+ },
60
83
  );
61
84
 
62
- // v1 → v2: renamed totalEuros → total (kept as string for this step)
63
- r.eventMigration("priced", 1, 2, (payload) => {
64
- const p = payload as { totalEuros: string };
65
- return { total: p.totalEuros, currency: "EUR" };
66
- });
67
- // v2 → v3: parse "total" string into integer cents
68
- r.eventMigration("priced", 2, 3, (payload) => {
69
- const p = payload as { total: string; currency: string };
70
- const euros = Number.parseFloat(p.total);
71
- return { totalCents: Math.round(euros * 100), currency: p.currency };
72
- });
73
-
74
85
  r.projection({
75
86
  name: "order-summary",
76
87
  source: "upcast-order",
@@ -309,18 +320,27 @@ describe("upcaster: async (Marten AsyncOnlyEventUpcaster — DB-Lookups)", () =>
309
320
  const placed = r.defineEvent(
310
321
  "placed",
311
322
  z.object({ customerId: z.string(), segment: z.string() }),
312
- { version: 2 },
323
+ {
324
+ version: 2,
325
+ migrations: [
326
+ {
327
+ fromVersion: 1,
328
+ toVersion: 2,
329
+ transform: async (payload, ctx) => {
330
+ const p = payload as { customerId: string };
331
+ const [row] = await selectMany(ctx.db, customerSegments, {
332
+ customerId: p.customerId,
333
+ });
334
+ return {
335
+ customerId: p.customerId,
336
+ segment: (row as { segment?: string } | undefined)?.segment ?? "UNKNOWN",
337
+ };
338
+ },
339
+ },
340
+ ],
341
+ },
313
342
  );
314
343
 
315
- r.eventMigration("placed", 1, 2, async (payload, ctx) => {
316
- const p = payload as { customerId: string };
317
- const [row] = await selectMany(ctx.db, customerSegments, { customerId: p.customerId });
318
- return {
319
- customerId: p.customerId,
320
- segment: (row as { segment?: string } | undefined)?.segment ?? "UNKNOWN",
321
- };
322
- });
323
-
324
344
  r.projection({
325
345
  name: "async-summary",
326
346
  source: "upcast-async-order",
@@ -385,26 +405,28 @@ describe("upcaster: boot-time validation", () => {
385
405
  test("defineEvent with version=N and only partial migrations fails at registry build", () => {
386
406
  const incomplete = defineFeature("holes", (r) => {
387
407
  r.entity("hole-order", orderEntity);
388
- r.defineEvent("bad", z.object({ v3: z.string() }), { version: 3 });
389
408
  // Only 1→2 registered — the 2→3 gap must be rejected.
390
- r.eventMigration("bad", 1, 2, (p) => p);
409
+ r.defineEvent("bad", z.object({ v3: z.string() }), {
410
+ version: 3,
411
+ migrations: [{ fromVersion: 1, toVersion: 2, transform: (p) => p }],
412
+ });
391
413
  });
392
414
  expect(() => createRegistry([incomplete])).toThrow(/v2.*v3|covers the step v2/);
393
415
  });
394
416
 
395
- test("migration declared but no defineEvent rejected", () => {
396
- const orphan = defineFeature("orphans", (r) => {
397
- r.entity("orph-order", orderEntity);
398
- r.eventMigration("ghost", 1, 2, (p) => p);
399
- });
400
- expect(() => createRegistry([orphan])).toThrow(/no r\.defineEvent/i);
401
- });
417
+ // "migration declared but no defineEvent" is now structurally impossible:
418
+ // migrations live in defineEvent's `migrations` option, always scoped to
419
+ // the event being defined in that same call — there is no longer a
420
+ // registrar shape that can express a dangling migration for an
421
+ // undefined event (formerly the "ghost" registry-validate error path).
402
422
 
403
423
  test("migration toVersion > defineEvent version → rejected", () => {
404
424
  const future = defineFeature("future", (r) => {
405
425
  r.entity("future-order", orderEntity);
406
- r.defineEvent("early", z.object({ x: z.number() }), { version: 1 });
407
- r.eventMigration("early", 1, 2, (p) => p);
426
+ r.defineEvent("early", z.object({ x: z.number() }), {
427
+ version: 1,
428
+ migrations: [{ fromVersion: 1, toVersion: 2, transform: (p) => p }],
429
+ });
408
430
  });
409
431
  expect(() => createRegistry([future])).toThrow(/declares only version 1/);
410
432
  });
@@ -412,34 +434,44 @@ describe("upcaster: boot-time validation", () => {
412
434
  test("non-contiguous (1→2 and 3→4 without 2→3) → rejected", () => {
413
435
  const gaps = defineFeature("gaps", (r) => {
414
436
  r.entity("gap-order", orderEntity);
415
- r.defineEvent("jumpy", z.object({ v: z.number() }), { version: 4 });
416
- r.eventMigration("jumpy", 1, 2, (p) => p);
417
- r.eventMigration("jumpy", 3, 4, (p) => p);
437
+ r.defineEvent("jumpy", z.object({ v: z.number() }), {
438
+ version: 4,
439
+ migrations: [
440
+ { fromVersion: 1, toVersion: 2, transform: (p) => p },
441
+ { fromVersion: 3, toVersion: 4, transform: (p) => p },
442
+ ],
443
+ });
418
444
  });
419
445
  expect(() => createRegistry([gaps])).toThrow(/v2.*v3|covers the step v2/);
420
446
  });
421
447
  });
422
448
 
423
449
  describe("upcaster: registrar input validation", () => {
424
- test("r.eventMigration rejects multi-step jumps", () => {
450
+ test("defineEvent migrations reject multi-step jumps", () => {
425
451
  expect(() =>
426
452
  defineFeature("bigstep", (r) => {
427
453
  r.entity("bigstep-order", orderEntity);
428
- r.defineEvent("biz", z.object({ x: z.number() }), { version: 3 });
429
- r.eventMigration("biz", 1, 3, (p) => p);
454
+ r.defineEvent("biz", z.object({ x: z.number() }), {
455
+ version: 3,
456
+ migrations: [{ fromVersion: 1, toVersion: 3, transform: (p) => p }],
457
+ });
430
458
  }),
431
459
  ).toThrow(/single-step/);
432
460
  });
433
461
 
434
- test("r.eventMigration rejects duplicate step", () => {
462
+ test("defineEvent migrations reject duplicate step", () => {
435
463
  expect(() =>
436
464
  defineFeature("dupestep", (r) => {
437
465
  r.entity("dup-order", orderEntity);
438
- r.defineEvent("dup", z.object({ x: z.number() }), { version: 2 });
439
- r.eventMigration("dup", 1, 2, (p) => p);
440
- r.eventMigration("dup", 1, 2, (p) => p);
466
+ r.defineEvent("dup", z.object({ x: z.number() }), {
467
+ version: 2,
468
+ migrations: [
469
+ { fromVersion: 1, toVersion: 2, transform: (p) => p },
470
+ { fromVersion: 1, toVersion: 2, transform: (p) => p },
471
+ ],
472
+ });
441
473
  }),
442
- ).toThrow(/already registered/);
474
+ ).toThrow(/already declared/);
443
475
  });
444
476
 
445
477
  test("r.defineEvent rejects non-positive version", () => {
@@ -8,7 +8,7 @@
8
8
  // selbst, das Guard-Script selbst.
9
9
 
10
10
  import type { DbRunner } from "../db";
11
- import { isUniqueViolation } from "../db/pg-error";
11
+ import { constraintOf, isUniqueViolation } from "../db/pg-error";
12
12
  import {
13
13
  eventPredecessorExists,
14
14
  findExistingEventVersion,
@@ -17,7 +17,7 @@ import {
17
17
  insertRawSubsequentEvent,
18
18
  } from "../db/queries/event-store-admin";
19
19
  import type { TenantId } from "../engine/types";
20
- import { VersionConflictError } from "./errors";
20
+ import { IdempotentAppendConflictError, VersionConflictError } from "./errors";
21
21
  import type { EventMetadata } from "./event-store";
22
22
 
23
23
  export type RawEventToAppend = {
@@ -39,6 +39,13 @@ export type RawEventToAppend = {
39
39
  readonly createdBy: string;
40
40
  };
41
41
 
42
+ function mapEventUniqueViolation(e: unknown, event: RawEventToAppend): Error {
43
+ if (constraintOf(e) === "events_idempotency_uq" && event.metadata.idempotencyKey) {
44
+ return new IdempotentAppendConflictError(event.tenantId, event.metadata.idempotencyKey);
45
+ }
46
+ return new VersionConflictError(event.aggregateId, event.expectedVersion);
47
+ }
48
+
42
49
  // Mirrors append()'s two-path structure: typed builder equivalent for v=0,
43
50
  // INSERT … SELECT … WHERE EXISTS gate for v>0. Caller-supplied createdAt +
44
51
  // createdBy skip the usual userResolver/now() paths.
@@ -54,7 +61,7 @@ export async function appendRaw(runner: DbRunner, event: RawEventToAppend): Prom
54
61
  }
55
62
  } catch (e) {
56
63
  if (isUniqueViolation(e)) {
57
- throw new VersionConflictError(event.aggregateId, event.expectedVersion);
64
+ throw mapEventUniqueViolation(e, event);
58
65
  }
59
66
  throw e;
60
67
  }
@@ -143,7 +150,7 @@ export async function appendRawBatch(
143
150
  if (isUniqueViolation(e)) {
144
151
  // Pre-flight ran but lost a race against a concurrent writer. Rare for
145
152
  // migration (single-runner) but possible; we can't name the exact row.
146
- throw new VersionConflictError(firstEvent.aggregateId, firstEvent.expectedVersion);
153
+ throw mapEventUniqueViolation(e, firstEvent);
147
154
  }
148
155
  throw e;
149
156
  }
@@ -1,35 +1,2 @@
1
- // Failure modes of the event-store's append() path. Surfaced as typed
2
- // errors so the executor layer can map them to the framework's
3
- // WriteResult error contract (version_conflict).
4
-
5
- export class VersionConflictError extends Error {
6
- public readonly aggregateId: string;
7
- public readonly expectedVersion: number;
8
- constructor(aggregateId: string, expectedVersion: number) {
9
- super(
10
- `Version conflict on aggregate ${aggregateId}: expected predecessor version ${expectedVersion}`,
11
- );
12
- this.name = "VersionConflictError";
13
- this.aggregateId = aggregateId;
14
- this.expectedVersion = expectedVersion;
15
- }
16
- }
17
-
18
- // Thrown when ctx.appendEvent targets an archived stream. Archived aggregates
19
- // are read-only — restoreStream() makes them writable again. The archive
20
- // state is not carried on the events themselves; it lives on the sparse
21
- // kumiko_archived_streams table. Handlers that need to branch on archive
22
- // state should call ctx.isStreamArchived(id) first.
23
- export class ArchivedStreamError extends Error {
24
- public readonly tenantId: string;
25
- public readonly aggregateId: string;
26
- constructor(tenantId: string, aggregateId: string) {
27
- super(
28
- `Aggregate ${aggregateId} on tenant ${tenantId} is archived — appendEvent is blocked. ` +
29
- `Call restoreStream() to re-open the stream before writing.`,
30
- );
31
- this.name = "ArchivedStreamError";
32
- this.tenantId = tenantId;
33
- this.aggregateId = aggregateId;
34
- }
35
- }
1
+ // Legacy path re-exported for callers still importing this module directly.
2
+ export * from "@cosmicdrift/kumiko-types/event-store-errors";
@@ -1,42 +1,29 @@
1
+ import type { EventMetadata, StoredEvent } from "@cosmicdrift/kumiko-types/event-store-types";
2
+ // Value-only import, aliased to avoid shadowing the ambient global
3
+ // `Temporal` TYPE this file's other Temporal.Instant annotations resolve
4
+ // against (StoredEvent.createdAt et al. — importing the bare name here
5
+ // conflicts with that ambient type, see #1438). Bun doesn't expose Temporal
6
+ // as a runtime value on globalThis, so the un-aliased call below crashed
7
+ // with "Temporal is not defined" outside boot paths that install it (#1480).
8
+ import { Temporal as TemporalPolyfill } from "temporal-polyfill";
9
+ import { encryptEventPayloadPii } from "../crypto/event-pii";
1
10
  import type { DbRunner } from "../db";
2
- import { isUniqueViolation } from "../db/pg-error";
11
+ import { constraintOf, isUniqueViolation } from "../db/pg-error";
3
12
  import {
4
13
  insertSubsequentEventRow,
5
14
  notifyPgChannel,
6
15
  selectAggregateMaxVersion,
7
- selectAggregateStreamTenant,
8
16
  selectEventsHighWaterMark,
9
17
  selectStreamMaxVersion,
10
18
  } from "../db/queries/event-store";
11
19
  import { insertOne, selectMany } from "../db/query";
12
20
  import type { TenantId } from "../engine/types";
13
21
  import { isStreamArchived } from "./archive";
14
- import { VersionConflictError } from "./errors";
22
+ import { IdempotentAppendConflictError, VersionConflictError } from "./errors";
15
23
  import { eventsTable } from "./events-schema";
16
24
  import { toStoredEvent } from "./row-to-stored-event";
17
25
 
18
- export type EventMetadata = {
19
- readonly userId: string;
20
- readonly requestId?: string;
21
- // End-to-end business-operation id. Root HTTP requests get it from the
22
- // x-correlation-id header (default: requestId). MSP-applies inherit it
23
- // from the triggering event. Lets you trace "which user click caused
24
- // this email 3 streams later?".
25
- readonly correlationId?: string;
26
- // Stored event id that triggered this write. Null for root commands;
27
- // set to event.id when an MSP-apply runs ctx.appendEvent. Together with
28
- // correlationId forms a causation DAG across aggregate streams.
29
- readonly causationId?: string;
30
- // Marten-conform free key/value space for app-specific metadata that
31
- // doesn't deserve its own EventMetadata field. Examples: A/B-test bucket,
32
- // feature-flag snapshot, geo-region, client SDK version. Persisted into
33
- // events.metadata jsonb (no schema change — it's already a free-form
34
- // jsonb column), survives upcasters untouched, available on every
35
- // StoredEvent.metadata.headers. Framework does not interpret values; the
36
- // app reads them when filtering/auditing. Keep values JSON-primitive
37
- // (string|number|boolean) so JSON serialization stays bulletproof.
38
- readonly headers?: Readonly<Record<string, string | number | boolean>>;
39
- };
26
+ export type { EventMetadata, StoredEvent } from "@cosmicdrift/kumiko-types/event-store-types";
40
27
 
41
28
  export type EventToAppend = {
42
29
  readonly aggregateId: string;
@@ -50,25 +37,6 @@ export type EventToAppend = {
50
37
  readonly metadata: EventMetadata;
51
38
  };
52
39
 
53
- // Generic über payload-shape. Default = Record<string, unknown> macht
54
- // alle existierenden Konsumenten backwards-compatible. Konkrete Apply-
55
- // Handler / Tests können `StoredEvent<MyEventPayload>` annotieren um
56
- // payload typed zu lesen. Type-Propagation kommt durch r.defineEvent +
57
- // SingleStreamApplyFn<T> in apply-Maps.
58
- export type StoredEvent<TPayload = Record<string, unknown>> = {
59
- readonly id: string;
60
- readonly aggregateId: string;
61
- readonly aggregateType: string;
62
- readonly tenantId: TenantId;
63
- readonly version: number;
64
- readonly type: string;
65
- readonly eventVersion: number;
66
- readonly payload: TPayload;
67
- readonly metadata: EventMetadata;
68
- readonly createdAt: Temporal.Instant;
69
- readonly createdBy: string;
70
- };
71
-
72
40
  type SelectedEvent = {
73
41
  readonly id: bigint;
74
42
  readonly aggregateId: string;
@@ -108,27 +76,36 @@ type SelectedEvent = {
108
76
  export const EVENTS_PUBSUB_CHANNEL = "kumiko_events_new";
109
77
 
110
78
  export async function append(db: DbRunner, event: EventToAppend): Promise<StoredEvent> {
111
- const newVersion = event.expectedVersion + 1;
112
- const eventVersion = event.eventVersion ?? 1;
79
+ // Event-PII (#799): stored payload AND returned echo carry ciphertext, so
80
+ // inline projections and rebuilds materialize identical rows.
81
+ const payload = await encryptEventPayloadPii(event.type, event.payload);
82
+ const toStore = payload === event.payload ? event : { ...event, payload };
83
+ const newVersion = toStore.expectedVersion + 1;
84
+ const eventVersion = toStore.eventVersion ?? 1;
113
85
 
114
86
  try {
115
87
  const row =
116
- event.expectedVersion === 0
117
- ? await insertFirstEvent(db, event, newVersion, eventVersion)
118
- : await insertSubsequentEvent(db, event, newVersion, eventVersion);
88
+ toStore.expectedVersion === 0
89
+ ? await insertFirstEvent(db, toStore, newVersion, eventVersion)
90
+ : await insertSubsequentEvent(db, toStore, newVersion, eventVersion);
119
91
 
120
92
  // NOTIFY fires on commit (PG buffers NOTIFY per TX), so subscribers never
121
93
  // see a wake-up for an event that later rolled back. Harmless no-op when
122
94
  // no LISTENer is attached.
123
95
  await notifyPgChannel(db, EVENTS_PUBSUB_CHANNEL);
124
96
 
125
- return buildStoredEvent(event, newVersion, eventVersion, row);
97
+ return buildStoredEvent(toStore, newVersion, eventVersion, row);
126
98
  } catch (e) {
127
99
  if (isUniqueViolation(e)) {
128
- // Only constraint left on the events table: events_aggregate_version_uq
129
- // on (tenant_id, aggregate_id, version). A unique violation here always
130
- // means a concurrent writer in the same tenant won the race to the
131
- // next version retry-able conflict.
100
+ // Two unique constraints on this table: events_aggregate_version_uq
101
+ // (tenant_id, aggregate_id, version) a concurrent writer won the
102
+ // race to the next version and events_idempotency_uq (tenant_id,
103
+ // metadata->>'idempotencyKey')the caller reused an idempotency key.
104
+ // constraintOf() tells them apart; unknown/renamed constraint falls
105
+ // back to VersionConflictError, the pre-existing behaviour.
106
+ if (constraintOf(e) === "events_idempotency_uq" && event.metadata.idempotencyKey) {
107
+ throw new IdempotentAppendConflictError(event.tenantId, event.metadata.idempotencyKey);
108
+ }
132
109
  throw new VersionConflictError(event.aggregateId, event.expectedVersion);
133
110
  }
134
111
  throw e;
@@ -181,12 +158,20 @@ async function insertSubsequentEvent(
181
158
  expectedVersion: event.expectedVersion,
182
159
  });
183
160
  if (!row) throw new VersionConflictError(event.aggregateId, event.expectedVersion);
161
+ const createdAt =
162
+ row.created_at instanceof Date
163
+ ? TemporalPolyfill.Instant.fromEpochMilliseconds(row.created_at.getTime())
164
+ : TemporalPolyfill.Instant.from(row.created_at);
184
165
  return {
185
166
  id: typeof row.id === "bigint" ? row.id : BigInt(row.id),
186
- createdAt:
187
- row.created_at instanceof Date
188
- ? Temporal.Instant.fromEpochMilliseconds(row.created_at.getTime())
189
- : Temporal.Instant.from(row.created_at),
167
+ // @cast-boundary temporal-polyfill-vs-ambient: same TC39 Temporal.Instant
168
+ // at runtime, two separate .d.ts sources (temporal-polyfill's bundled
169
+ // types vs the ambient temporal-spec global) disagree on a couple of
170
+ // method-overload signatures (until/equals argument types), so TS treats
171
+ // them as nominally distinct. Safe: this value is only ever consumed as
172
+ // a Temporal.Instant (comparisons, .toString(), arithmetic), never
173
+ // narrowed on the specific overload shape that differs.
174
+ createdAt: createdAt as unknown as Temporal.Instant,
190
175
  };
191
176
  }
192
177
 
@@ -276,29 +261,19 @@ export async function getStreamVersion(
276
261
  return selectStreamMaxVersion(db, aggregateId, tenantId);
277
262
  }
278
263
 
279
- /** MAX(version) for one aggregate — no tenant filter. Used by seed idempotency. */
264
+ /** MAX(version) for one aggregate — no tenant filter. SECURITY: existence-oracle,
265
+ * a caller can probe whether an aggregateId has any events regardless of tenant
266
+ * membership. Only call from seed/system-internal paths (idempotency checks
267
+ * against a known aggregateId) — never from a handler reachable with
268
+ * caller-controlled input. */
280
269
  // @wrapper-known semantic-alias
281
- export async function getAggregateStreamMaxVersion(
270
+ export async function getUnscopedAggregateStreamMaxVersion(
282
271
  db: DbRunner,
283
272
  aggregateId: string,
284
273
  ): Promise<number> {
285
274
  return selectAggregateMaxVersion(db, aggregateId);
286
275
  }
287
276
 
288
- /** Stream tenant of an aggregate (the tenant_id its events live under), with no
289
- * membership/tenant filter. Recovers the write target for a systemScope
290
- * aggregate whose stream tenant isn't one of the subject's memberships.
291
- * Returns null for unknown streams. */
292
- export async function getAggregateStreamTenant(
293
- db: DbRunner,
294
- aggregateId: string,
295
- aggregateType: string,
296
- ): Promise<TenantId | null> {
297
- const tenantId = await selectAggregateStreamTenant(db, aggregateId, aggregateType);
298
- // DB-boundary: kumiko_events.tenant_id is a TenantId-shaped uuid column.
299
- return tenantId as TenantId | null;
300
- }
301
-
302
277
  // Global high-water-mark = MAX(events.id). Marten/Wolverine standard for
303
278
  // projection/consumer lag math: lag = HWM - cursor. Single-row aggregate over
304
279
  // the bigserial PK index — sub-millisecond cost. Returns 0n on an empty log
@@ -328,15 +303,19 @@ export async function loadEventsAfterVersion(
328
303
 
329
304
  // Load every event for an aggregate_type across all tenants. Ordered by
330
305
  // (created_at, id) — chronological replay order for projection rebuilds.
331
- //
332
- // CAUTION — buffers ALL matching events in memory. Safe for smaller
333
- // aggregate-types (≤ 100k events), a memory cliff for large stores.
334
- // For >100k events use `streamAllEventsByType` (yields batchwise).
335
306
  // Mostly called from tests today — production rebuild goes through
336
307
  // projection-rebuild's own streaming path.
308
+ //
309
+ // Fails loud past LOAD_ALL_EVENTS_ROW_LIMIT rather than silently buffering
310
+ // an unbounded result set — that's the memory cliff this guard exists to
311
+ // prevent.
312
+ export const LOAD_ALL_EVENTS_ROW_LIMIT = 100_000;
313
+
314
+ /** Buffers ALL matching events in memory — a memory cliff for large stores. Use `streamAllEventsByType` (yields batchwise) for production reads; this is test-only in practice. */
337
315
  export async function loadAllEventsByType(
338
316
  db: DbRunner,
339
317
  aggregateType: string,
318
+ rowLimit: number = LOAD_ALL_EVENTS_ROW_LIMIT,
340
319
  ): Promise<readonly StoredEvent[]> {
341
320
  const rows = await selectMany<SelectedEvent>(
342
321
  db,
@@ -347,8 +326,15 @@ export async function loadAllEventsByType(
347
326
  { col: "createdAt", direction: "asc" },
348
327
  { col: "id", direction: "asc" },
349
328
  ],
329
+ limit: rowLimit + 1,
350
330
  },
351
331
  );
332
+ if (rows.length > rowLimit) {
333
+ throw new Error(
334
+ `loadAllEventsByType("${aggregateType}") exceeds ${rowLimit} rows — ` +
335
+ "use streamAllEventsByType instead of buffering the full result set in memory.",
336
+ );
337
+ }
352
338
  return rows.map(toStoredEvent);
353
339
  }
354
340
 
@@ -12,28 +12,22 @@ import {
12
12
  uniqueIndex,
13
13
  uuid,
14
14
  } from "../db/dialect";
15
+ import { ensureIdempotencyKeyIndex } from "../db/queries/event-store";
15
16
  import { unsafePushTables } from "../stack";
16
17
  import { createArchivedStreamsTable } from "./archive";
17
18
  import { createSnapshotsTable } from "./snapshot";
19
+ import type { EventMetadata } from "./types";
18
20
 
19
21
  // Event-store schema as a Drizzle table. The typed select/insert path handles
20
22
  // most operations; append() for subsequent versions uses raw SQL because
21
23
  // INSERT ... SELECT ... WHERE EXISTS isn't ergonomic in the typed builder.
22
24
  //
23
25
  // HTTP-level retry idempotency is handled by pipeline/idempotency.ts
24
- // (Redis-backed check + cached-response replay). The event-store itself
25
- // imposes no idempotency indexa single HTTP request may write N events
26
- // freely, metadata.requestId is purely a trace marker.
27
- export type EventMetadata = {
28
- readonly userId: string;
29
- readonly requestId?: string;
30
- readonly correlationId?: string;
31
- readonly causationId?: string;
32
- // App-specific free key/value (Marten "headers"). Mirror of the canonical
33
- // type in event-store.ts — kept duplicate because events-schema must stay
34
- // import-cycle-free vs the event-store module.
35
- readonly headers?: Readonly<Record<string, string | number | boolean>>;
36
- };
26
+ // (Redis-backed check + cached-response replay); metadata.requestId is
27
+ // purely a trace marker (no uniqueness constraintone request may write
28
+ // N events). Callers that need a hard per-event guarantee as a second line
29
+ // of defense set metadata.idempotencyKey, enforced by the tenant-scoped
30
+ // partial unique index ensureIdempotencyKeyIndex() creates below.
37
31
 
38
32
  export const eventsTable = pgTable(
39
33
  "kumiko_events",
@@ -86,6 +80,10 @@ export async function createEventsTable(db: DbConnection): Promise<void> {
86
80
  if (!(await tableExists(db, "public.kumiko_events"))) {
87
81
  await unsafePushTables(db, { kumikoEvents: eventsTable });
88
82
  }
83
+ // Runs unconditionally (both fresh + already-existing table) so installs
84
+ // that predate the idempotency-key index get healed the same way
85
+ // ensureSnapshotVersionColumn heals kumiko_snapshots.
86
+ await ensureIdempotencyKeyIndex(db);
89
87
  await createArchivedStreamsTable(db);
90
88
  await createSnapshotsTable(db);
91
89
  }
@@ -1,3 +1,9 @@
1
+ export {
2
+ backfillEventPiiEncryption,
3
+ type PiiBackfillFailure,
4
+ type PiiBackfillOptions,
5
+ type PiiBackfillResult,
6
+ } from "../db/queries/backfill-pii";
1
7
  export {
2
8
  type ArchiveStreamArgs,
3
9
  archivedStreamsTable,
@@ -6,16 +12,16 @@ export {
6
12
  isStreamArchived,
7
13
  restoreStream,
8
14
  } from "./archive";
9
- export { ArchivedStreamError, VersionConflictError } from "./errors";
15
+ export { ArchivedStreamError, IdempotentAppendConflictError, VersionConflictError } from "./errors";
10
16
  export {
11
17
  append,
12
18
  EVENTS_PUBSUB_CHANNEL,
13
19
  type EventMetadata,
14
20
  type EventToAppend,
15
- getAggregateStreamMaxVersion,
16
- getAggregateStreamTenant,
17
21
  getEventsHighWaterMark,
18
22
  getStreamVersion,
23
+ getUnscopedAggregateStreamMaxVersion,
24
+ LOAD_ALL_EVENTS_ROW_LIMIT,
19
25
  loadAggregate,
20
26
  loadAggregateAsOf,
21
27
  loadAllEventsByType,
@@ -24,9 +30,18 @@ export {
24
30
  streamAllEventsByType,
25
31
  } from "./event-store";
26
32
  export { createEventsTable, eventsTable } from "./events-schema";
33
+ export {
34
+ createRebuildDeadLetterTable,
35
+ listRebuildDeadLetters,
36
+ type RebuildDeadLetterRow,
37
+ rebuildDeadLetterTable,
38
+ recordRebuildDeadLetters,
39
+ type SkippedApply,
40
+ } from "./rebuild-dead-letter";
27
41
  export { toStoredEvent } from "./row-to-stored-event";
28
42
  export {
29
43
  createSnapshotsTable,
44
+ type LoadAggregateWithSnapshotOptions,
30
45
  type LoadAggregateWithSnapshotResult,
31
46
  loadAggregateWithSnapshot,
32
47
  loadLatestSnapshot,