@cosmicdrift/kumiko-framework 0.1.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 (388) hide show
  1. package/README.md +159 -0
  2. package/package.json +91 -0
  3. package/src/__tests__/anonymous-access.integration.ts +325 -0
  4. package/src/__tests__/error-contract.integration.ts +435 -0
  5. package/src/__tests__/field-access.integration.ts +269 -0
  6. package/src/__tests__/full-stack.integration.ts +914 -0
  7. package/src/__tests__/ownership.integration.ts +449 -0
  8. package/src/__tests__/reference-data.integration.ts +198 -0
  9. package/src/__tests__/transition-guard.integration.ts +340 -0
  10. package/src/api/__tests__/api.test.ts +337 -0
  11. package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
  13. package/src/api/__tests__/batch.integration.ts +404 -0
  14. package/src/api/__tests__/body-limit.test.ts +88 -0
  15. package/src/api/__tests__/csrf-middleware.test.ts +97 -0
  16. package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
  17. package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
  18. package/src/api/__tests__/nested-write.integration.ts +213 -0
  19. package/src/api/__tests__/readiness.test.ts +76 -0
  20. package/src/api/__tests__/request-id-middleware.test.ts +72 -0
  21. package/src/api/__tests__/sse-broker.test.ts +58 -0
  22. package/src/api/__tests__/sse-route.test.ts +112 -0
  23. package/src/api/anonymous-cookie.ts +60 -0
  24. package/src/api/api-constants.ts +64 -0
  25. package/src/api/auth-middleware.ts +418 -0
  26. package/src/api/auth-routes.ts +982 -0
  27. package/src/api/csrf-middleware.ts +77 -0
  28. package/src/api/index.ts +31 -0
  29. package/src/api/jwt.ts +66 -0
  30. package/src/api/observability-middleware.ts +89 -0
  31. package/src/api/readiness.ts +132 -0
  32. package/src/api/request-context.ts +49 -0
  33. package/src/api/request-id-middleware.ts +50 -0
  34. package/src/api/route-registrars.ts +195 -0
  35. package/src/api/routes.ts +135 -0
  36. package/src/api/server.ts +640 -0
  37. package/src/api/sse-broker.ts +71 -0
  38. package/src/api/sse-route.ts +62 -0
  39. package/src/api/tokens.ts +16 -0
  40. package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
  41. package/src/db/__tests__/compound-types.test.ts +114 -0
  42. package/src/db/__tests__/connection-options.test.ts +68 -0
  43. package/src/db/__tests__/cursor.test.ts +41 -0
  44. package/src/db/__tests__/db-helpers.test.ts +369 -0
  45. package/src/db/__tests__/dialect-instant.test.ts +50 -0
  46. package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
  47. package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
  48. package/src/db/__tests__/encryption.test.ts +39 -0
  49. package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
  50. package/src/db/__tests__/event-store-executor.integration.ts +235 -0
  51. package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
  52. package/src/db/__tests__/located-timestamp.test.ts +184 -0
  53. package/src/db/__tests__/money.test.ts +199 -0
  54. package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
  55. package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
  56. package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
  57. package/src/db/__tests__/row-helpers.test.ts +59 -0
  58. package/src/db/__tests__/schema-migration.integration.ts +273 -0
  59. package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
  60. package/src/db/__tests__/table-builder-required.test.ts +216 -0
  61. package/src/db/__tests__/tenant-db.integration.ts +606 -0
  62. package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
  63. package/src/db/apply-entity-event.ts +188 -0
  64. package/src/db/assert-exists-in.ts +59 -0
  65. package/src/db/compound-types.ts +47 -0
  66. package/src/db/connection.ts +104 -0
  67. package/src/db/cursor.ts +83 -0
  68. package/src/db/dialect.ts +109 -0
  69. package/src/db/eagerload.ts +174 -0
  70. package/src/db/encryption.ts +39 -0
  71. package/src/db/event-store-executor.ts +906 -0
  72. package/src/db/index.ts +55 -0
  73. package/src/db/located-timestamp.ts +114 -0
  74. package/src/db/money.ts +120 -0
  75. package/src/db/pg-error.ts +46 -0
  76. package/src/db/reference-data.ts +77 -0
  77. package/src/db/row-helpers.ts +53 -0
  78. package/src/db/schema-inspection.ts +25 -0
  79. package/src/db/table-builder.ts +475 -0
  80. package/src/db/tenant-db.ts +434 -0
  81. package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
  82. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
  83. package/src/engine/__tests__/boot-validator.test.ts +1865 -0
  84. package/src/engine/__tests__/build-app-schema.test.ts +154 -0
  85. package/src/engine/__tests__/claim-keys.test.ts +274 -0
  86. package/src/engine/__tests__/config-helpers.test.ts +236 -0
  87. package/src/engine/__tests__/effective-features.test.ts +86 -0
  88. package/src/engine/__tests__/engine.test.ts +1461 -0
  89. package/src/engine/__tests__/entity-handlers.test.ts +274 -0
  90. package/src/engine/__tests__/event-helpers.test.ts +68 -0
  91. package/src/engine/__tests__/extends-registrar.test.ts +159 -0
  92. package/src/engine/__tests__/factories-long-text.test.ts +84 -0
  93. package/src/engine/__tests__/factories-time.test.ts +158 -0
  94. package/src/engine/__tests__/field-predicates.test.ts +48 -0
  95. package/src/engine/__tests__/hook-phases.test.ts +132 -0
  96. package/src/engine/__tests__/identifiers.test.ts +35 -0
  97. package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
  98. package/src/engine/__tests__/nav.test.ts +267 -0
  99. package/src/engine/__tests__/ownership.test.ts +421 -0
  100. package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
  101. package/src/engine/__tests__/projection-helpers.test.ts +62 -0
  102. package/src/engine/__tests__/projection.test.ts +191 -0
  103. package/src/engine/__tests__/qualified-name.test.ts +264 -0
  104. package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
  105. package/src/engine/__tests__/run-in.test.ts +38 -0
  106. package/src/engine/__tests__/schema-builder.test.ts +380 -0
  107. package/src/engine/__tests__/screen.test.ts +408 -0
  108. package/src/engine/__tests__/state-machine.test.ts +148 -0
  109. package/src/engine/__tests__/system-user.test.ts +57 -0
  110. package/src/engine/__tests__/validation-hooks.test.ts +71 -0
  111. package/src/engine/access.ts +23 -0
  112. package/src/engine/boot-validator.ts +1528 -0
  113. package/src/engine/build-app-schema.ts +125 -0
  114. package/src/engine/config-helpers.ts +115 -0
  115. package/src/engine/constants.ts +85 -0
  116. package/src/engine/create-app.ts +98 -0
  117. package/src/engine/define-feature.ts +702 -0
  118. package/src/engine/define-handler.ts +78 -0
  119. package/src/engine/define-roles.ts +19 -0
  120. package/src/engine/effective-features.ts +87 -0
  121. package/src/engine/entity-handlers.ts +364 -0
  122. package/src/engine/event-helpers.ts +73 -0
  123. package/src/engine/factories.ts +328 -0
  124. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
  125. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
  126. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
  127. package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
  128. package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
  129. package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
  130. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
  131. package/src/engine/feature-ast/extractors.ts +2562 -0
  132. package/src/engine/feature-ast/index.ts +105 -0
  133. package/src/engine/feature-ast/parse.ts +369 -0
  134. package/src/engine/feature-ast/patch.ts +525 -0
  135. package/src/engine/feature-ast/patcher.ts +518 -0
  136. package/src/engine/feature-ast/patterns.ts +434 -0
  137. package/src/engine/feature-ast/render.ts +602 -0
  138. package/src/engine/feature-ast/source-location.ts +45 -0
  139. package/src/engine/field-access.ts +120 -0
  140. package/src/engine/index.ts +254 -0
  141. package/src/engine/ownership.ts +337 -0
  142. package/src/engine/parse-ref-target.ts +22 -0
  143. package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
  144. package/src/engine/pattern-library/index.ts +24 -0
  145. package/src/engine/pattern-library/library.ts +1117 -0
  146. package/src/engine/pattern-library/types.ts +255 -0
  147. package/src/engine/projection-helpers.ts +85 -0
  148. package/src/engine/qualified-name.ts +122 -0
  149. package/src/engine/read-claim.ts +31 -0
  150. package/src/engine/registry.ts +1325 -0
  151. package/src/engine/resolve-config-or-param.ts +153 -0
  152. package/src/engine/run-in.ts +29 -0
  153. package/src/engine/schema-builder.ts +175 -0
  154. package/src/engine/screen-filter-ops.ts +51 -0
  155. package/src/engine/state-machine.ts +70 -0
  156. package/src/engine/system-user.ts +32 -0
  157. package/src/engine/types/config.ts +306 -0
  158. package/src/engine/types/event-type-map.ts +37 -0
  159. package/src/engine/types/feature.ts +574 -0
  160. package/src/engine/types/fields.ts +422 -0
  161. package/src/engine/types/handlers.ts +742 -0
  162. package/src/engine/types/hooks.ts +142 -0
  163. package/src/engine/types/http-route.ts +54 -0
  164. package/src/engine/types/identifiers.ts +47 -0
  165. package/src/engine/types/index.ts +208 -0
  166. package/src/engine/types/nav.ts +46 -0
  167. package/src/engine/types/projection.ts +132 -0
  168. package/src/engine/types/relations.ts +51 -0
  169. package/src/engine/types/screen.ts +452 -0
  170. package/src/engine/types/workspace.ts +42 -0
  171. package/src/engine/validation.ts +33 -0
  172. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
  173. package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
  174. package/src/entrypoint/index.ts +442 -0
  175. package/src/errors/__tests__/classes.test.ts +371 -0
  176. package/src/errors/__tests__/write-failures.test.ts +109 -0
  177. package/src/errors/classes.ts +249 -0
  178. package/src/errors/i18n/de.yaml +83 -0
  179. package/src/errors/i18n/en.yaml +80 -0
  180. package/src/errors/index.ts +41 -0
  181. package/src/errors/kumiko-error.ts +67 -0
  182. package/src/errors/reasons.ts +36 -0
  183. package/src/errors/serialize.ts +136 -0
  184. package/src/errors/transition-details.ts +30 -0
  185. package/src/errors/write-error-info.ts +123 -0
  186. package/src/errors/zod-bridge.ts +49 -0
  187. package/src/event-store/__tests__/admin-api.integration.ts +361 -0
  188. package/src/event-store/__tests__/event-store.integration.ts +584 -0
  189. package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
  190. package/src/event-store/__tests__/perf.integration.ts +255 -0
  191. package/src/event-store/__tests__/snapshot.integration.ts +267 -0
  192. package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
  193. package/src/event-store/__tests__/upcaster.integration.ts +460 -0
  194. package/src/event-store/admin-api.ts +257 -0
  195. package/src/event-store/archive.ts +106 -0
  196. package/src/event-store/errors.ts +35 -0
  197. package/src/event-store/event-store.ts +405 -0
  198. package/src/event-store/events-schema.ts +90 -0
  199. package/src/event-store/index.ts +50 -0
  200. package/src/event-store/snapshot.ts +210 -0
  201. package/src/event-store/upcaster-dead-letter.ts +119 -0
  202. package/src/event-store/upcaster.ts +147 -0
  203. package/src/files/__tests__/content-disposition.test.ts +123 -0
  204. package/src/files/__tests__/file-field-column.integration.ts +103 -0
  205. package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
  206. package/src/files/__tests__/file-handle.test.ts +122 -0
  207. package/src/files/__tests__/files.integration.ts +830 -0
  208. package/src/files/__tests__/storage-tracking.integration.ts +153 -0
  209. package/src/files/content-disposition.ts +55 -0
  210. package/src/files/file-handle.ts +63 -0
  211. package/src/files/file-ref-table.ts +22 -0
  212. package/src/files/file-routes.ts +353 -0
  213. package/src/files/in-memory-provider.ts +62 -0
  214. package/src/files/index.ts +29 -0
  215. package/src/files/local-provider.ts +35 -0
  216. package/src/files/storage-tracking.ts +60 -0
  217. package/src/files/types.ts +118 -0
  218. package/src/i18n/__tests__/i18n.test.ts +72 -0
  219. package/src/i18n/index.ts +29 -0
  220. package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
  221. package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
  222. package/src/jobs/__tests__/jobs.integration.ts +566 -0
  223. package/src/jobs/index.ts +2 -0
  224. package/src/jobs/job-runner.ts +574 -0
  225. package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
  226. package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
  227. package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
  228. package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
  229. package/src/lifecycle/index.ts +13 -0
  230. package/src/lifecycle/lifecycle.ts +160 -0
  231. package/src/lifecycle/signal-handlers.ts +62 -0
  232. package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
  233. package/src/logging/index.ts +3 -0
  234. package/src/logging/pino-logger.ts +64 -0
  235. package/src/logging/types.ts +7 -0
  236. package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
  237. package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
  238. package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
  239. package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
  240. package/src/migrations/index.ts +28 -0
  241. package/src/migrations/projection-detection.ts +149 -0
  242. package/src/migrations/rebuild-marker.ts +64 -0
  243. package/src/migrations/schema-drift.ts +395 -0
  244. package/src/observability/__tests__/console-provider.test.ts +67 -0
  245. package/src/observability/__tests__/metric-validator.test.ts +87 -0
  246. package/src/observability/__tests__/noop-provider.test.ts +82 -0
  247. package/src/observability/__tests__/observability.integration.ts +559 -0
  248. package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
  249. package/src/observability/__tests__/recording-meter.test.ts +101 -0
  250. package/src/observability/__tests__/recording-tracer.test.ts +110 -0
  251. package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
  252. package/src/observability/console-provider.ts +130 -0
  253. package/src/observability/context.ts +26 -0
  254. package/src/observability/fallback.ts +34 -0
  255. package/src/observability/ids.ts +25 -0
  256. package/src/observability/index.ts +79 -0
  257. package/src/observability/metric-validator.ts +86 -0
  258. package/src/observability/metrics-handle.ts +56 -0
  259. package/src/observability/noop-provider.ts +146 -0
  260. package/src/observability/prometheus-meter.ts +284 -0
  261. package/src/observability/recording-meter.ts +156 -0
  262. package/src/observability/recording-tracer.ts +198 -0
  263. package/src/observability/redis-wrapper.ts +132 -0
  264. package/src/observability/sensitive-filter.ts +108 -0
  265. package/src/observability/standard-metrics.ts +213 -0
  266. package/src/observability/types/index.ts +29 -0
  267. package/src/observability/types/metric.ts +56 -0
  268. package/src/observability/types/provider.ts +32 -0
  269. package/src/observability/types/span.ts +64 -0
  270. package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
  271. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
  272. package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
  273. package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
  274. package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
  275. package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
  276. package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
  277. package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
  278. package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
  279. package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
  280. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
  281. package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
  282. package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
  283. package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
  284. package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
  285. package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
  286. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
  287. package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
  288. package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
  289. package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
  290. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
  291. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
  292. package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
  293. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
  294. package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
  295. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
  296. package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
  297. package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
  298. package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
  299. package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
  300. package/src/pipeline/append-event-core.ts +117 -0
  301. package/src/pipeline/auth-claims-resolver.ts +103 -0
  302. package/src/pipeline/cascade-handler.ts +113 -0
  303. package/src/pipeline/dispatcher.ts +1585 -0
  304. package/src/pipeline/distributed-lock.ts +37 -0
  305. package/src/pipeline/entity-cache.ts +113 -0
  306. package/src/pipeline/event-consumer-state.ts +108 -0
  307. package/src/pipeline/event-dedup.ts +23 -0
  308. package/src/pipeline/event-dispatcher.ts +1016 -0
  309. package/src/pipeline/event-retention.ts +154 -0
  310. package/src/pipeline/idempotency.ts +76 -0
  311. package/src/pipeline/index.ts +66 -0
  312. package/src/pipeline/lifecycle-pipeline.ts +409 -0
  313. package/src/pipeline/msp-rebuild.ts +242 -0
  314. package/src/pipeline/multi-stream-apply-context.ts +115 -0
  315. package/src/pipeline/projection-rebuild.ts +334 -0
  316. package/src/pipeline/projection-state.ts +72 -0
  317. package/src/pipeline/projections-runner.ts +56 -0
  318. package/src/pipeline/redis-keys.ts +11 -0
  319. package/src/pipeline/system-hooks.ts +190 -0
  320. package/src/random/__tests__/generate.test.ts +149 -0
  321. package/src/random/generate.ts +141 -0
  322. package/src/random/index.ts +8 -0
  323. package/src/random/words.ts +392 -0
  324. package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
  325. package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
  326. package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
  327. package/src/rate-limit/bucket.ts +36 -0
  328. package/src/rate-limit/index.ts +14 -0
  329. package/src/rate-limit/middleware.ts +152 -0
  330. package/src/rate-limit/resolver.ts +267 -0
  331. package/src/redis/__tests__/redis-options.test.ts +54 -0
  332. package/src/redis/index.ts +74 -0
  333. package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
  334. package/src/search/__tests__/search-adapter.test.ts +256 -0
  335. package/src/search/in-memory-adapter.ts +123 -0
  336. package/src/search/index.ts +12 -0
  337. package/src/search/meilisearch-adapter.ts +106 -0
  338. package/src/search/types.ts +39 -0
  339. package/src/secrets/__tests__/dek-cache.test.ts +213 -0
  340. package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
  341. package/src/secrets/__tests__/envelope.test.ts +74 -0
  342. package/src/secrets/__tests__/leak-guard.test.ts +92 -0
  343. package/src/secrets/__tests__/rotation.test.ts +149 -0
  344. package/src/secrets/dek-cache.ts +116 -0
  345. package/src/secrets/env-master-key-provider.ts +162 -0
  346. package/src/secrets/envelope.ts +55 -0
  347. package/src/secrets/index.ts +19 -0
  348. package/src/secrets/leak-guard.ts +87 -0
  349. package/src/secrets/rotation.ts +34 -0
  350. package/src/secrets/types.ts +107 -0
  351. package/src/stack/db.ts +104 -0
  352. package/src/stack/event-collector.ts +23 -0
  353. package/src/stack/index.ts +32 -0
  354. package/src/stack/redis.ts +44 -0
  355. package/src/stack/request-helper.ts +168 -0
  356. package/src/stack/table-helpers.ts +104 -0
  357. package/src/stack/test-stack.ts +357 -0
  358. package/src/stack/test-users.ts +37 -0
  359. package/src/testing/__tests__/e2e-generator.test.ts +230 -0
  360. package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
  361. package/src/testing/access-assertions.ts +15 -0
  362. package/src/testing/assertions.ts +35 -0
  363. package/src/testing/e2e-generator.ts +465 -0
  364. package/src/testing/expect-error.ts +25 -0
  365. package/src/testing/handler-context.ts +125 -0
  366. package/src/testing/http-cookies.ts +52 -0
  367. package/src/testing/index.ts +41 -0
  368. package/src/testing/late-bound.ts +39 -0
  369. package/src/testing/mutable-master-key-provider.ts +31 -0
  370. package/src/testing/observability-recorder.ts +54 -0
  371. package/src/testing/shared-entities.ts +49 -0
  372. package/src/testing/utils.ts +1 -0
  373. package/src/testing/wait-for.ts +31 -0
  374. package/src/time/__tests__/polyfill.test.ts +73 -0
  375. package/src/time/__tests__/tz-context.test.ts +121 -0
  376. package/src/time/index.ts +21 -0
  377. package/src/time/polyfill.ts +70 -0
  378. package/src/time/tz-context.ts +107 -0
  379. package/src/ui-types/app-schema.ts +57 -0
  380. package/src/ui-types/index.ts +65 -0
  381. package/src/utils/__tests__/assert.test.ts +17 -0
  382. package/src/utils/__tests__/env-parse.test.ts +54 -0
  383. package/src/utils/assert.ts +18 -0
  384. package/src/utils/env-parse.ts +16 -0
  385. package/src/utils/ids.ts +16 -0
  386. package/src/utils/index.ts +5 -0
  387. package/src/utils/safe-json.ts +30 -0
  388. package/src/utils/serialization.ts +7 -0
@@ -0,0 +1,235 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
3
+ import { createBooleanField, createEntity, createTextField } from "../../engine";
4
+ import { createEventsTable } from "../../event-store";
5
+ import { createEntityTable, createTestDb, type TestDb, TestUsers, testTenantId } from "../../stack";
6
+ import { createEventStoreExecutor } from "../event-store-executor";
7
+ import { buildDrizzleTable } from "../table-builder";
8
+ import { createTenantDb, type TenantDb } from "../tenant-db";
9
+
10
+ const entity = createEntity({
11
+ table: "read_es_exec_users",
12
+ fields: {
13
+ email: createTextField({ required: true, searchable: true }),
14
+ firstName: createTextField(),
15
+ isEnabled: createBooleanField({ default: true }),
16
+ },
17
+ softDelete: true,
18
+ });
19
+ const table = buildDrizzleTable("esExecUser", entity);
20
+
21
+ let testDb: TestDb;
22
+ let tdb: TenantDb;
23
+ const adminUser = TestUsers.admin;
24
+
25
+ beforeAll(async () => {
26
+ testDb = await createTestDb();
27
+ await createEntityTable(testDb.db, entity, "esExecUser");
28
+ await createEventsTable(testDb.db);
29
+ tdb = createTenantDb(testDb.db, adminUser.tenantId);
30
+ });
31
+
32
+ afterAll(async () => {
33
+ await testDb.cleanup();
34
+ });
35
+
36
+ beforeEach(async () => {
37
+ await testDb.db.execute(sql`TRUNCATE kumiko_events, read_es_exec_users RESTART IDENTITY CASCADE`);
38
+ });
39
+
40
+ describe("event-store-executor", () => {
41
+ const crud = createEventStoreExecutor(table, entity, { entityName: "esExecUser" });
42
+
43
+ test("create appends event v1 + inserts projection row", async () => {
44
+ const result = await crud.create({ email: "test@test.de", firstName: "Test" }, adminUser, tdb);
45
+ expect(result.isSuccess).toBe(true);
46
+ if (!result.isSuccess) return;
47
+ expect(result.data.isNew).toBe(true);
48
+ expect(typeof result.data.id).toBe("string");
49
+ expect(result.data.data["email"]).toBe("test@test.de");
50
+ expect(result.data.data["tenantId"]).toBe(testTenantId(1));
51
+ expect(result.data.data["version"]).toBe(1);
52
+ });
53
+
54
+ test("update increments version + appends event", async () => {
55
+ const created = await crud.create({ email: "u@test.de" }, adminUser, tdb);
56
+ if (!created.isSuccess) throw new Error("setup failed");
57
+
58
+ const result = await crud.update(
59
+ { id: created.data.id, version: 1, changes: { firstName: "Updated" } },
60
+ adminUser,
61
+ tdb,
62
+ );
63
+ expect(result.isSuccess).toBe(true);
64
+ if (!result.isSuccess) return;
65
+ expect(result.data.data["version"]).toBe(2);
66
+ expect(result.data.data["firstName"]).toBe("Updated");
67
+ });
68
+
69
+ test("stale version → version_conflict", async () => {
70
+ const created = await crud.create({ email: "v@test.de" }, adminUser, tdb);
71
+ if (!created.isSuccess) throw new Error("setup failed");
72
+
73
+ await crud.update(
74
+ { id: created.data.id, version: 1, changes: { firstName: "First" } },
75
+ adminUser,
76
+ tdb,
77
+ );
78
+ const stale = await crud.update(
79
+ { id: created.data.id, version: 1, changes: { firstName: "Stale" } },
80
+ adminUser,
81
+ tdb,
82
+ );
83
+ expect(stale.isSuccess).toBe(false);
84
+ if (stale.isSuccess) return;
85
+ expect(stale.error.code).toBe("version_conflict");
86
+ });
87
+
88
+ test("delete soft-deletes + appends event", async () => {
89
+ const created = await crud.create({ email: "d@test.de" }, adminUser, tdb);
90
+ if (!created.isSuccess) throw new Error("setup failed");
91
+
92
+ const deleted = await crud.delete({ id: created.data.id }, adminUser, tdb);
93
+ expect(deleted.isSuccess).toBe(true);
94
+
95
+ const detail = await crud.detail({ id: created.data.id }, adminUser, tdb);
96
+ expect(detail).toBeNull();
97
+ });
98
+ });
99
+
100
+ // Sensitive-field stripping: passwords/tokens/IBANs stay in the entity row
101
+ // but MUST NOT land in the immutable event log (GDPR right-to-be-forgotten,
102
+ // secrets-rotation, audit discoverability). Fields marked `sensitive: true`
103
+ // are excluded from every event payload: create data, update changes,
104
+ // update previous, delete previous, restore previous.
105
+ const sensitiveEntity = createEntity({
106
+ table: "read_es_exec_sensitive",
107
+ fields: {
108
+ email: createTextField({ required: true }),
109
+ passwordHash: createTextField({ sensitive: true }),
110
+ apiToken: createTextField({ sensitive: true }),
111
+ },
112
+ softDelete: true,
113
+ });
114
+ const sensitiveTable = buildDrizzleTable("esExecSensitive", sensitiveEntity);
115
+
116
+ describe("event-store-executor — sensitive fields", () => {
117
+ const crud = createEventStoreExecutor(sensitiveTable, sensitiveEntity, {
118
+ entityName: "esExecSensitive",
119
+ });
120
+
121
+ beforeAll(async () => {
122
+ await createEntityTable(testDb.db, sensitiveEntity, "esExecSensitive");
123
+ });
124
+
125
+ beforeEach(async () => {
126
+ await testDb.db.execute(
127
+ sql`TRUNCATE kumiko_events, read_es_exec_sensitive RESTART IDENTITY CASCADE`,
128
+ );
129
+ });
130
+
131
+ async function lastEvent<TPayload = Record<string, unknown>>(): Promise<{
132
+ type: string;
133
+ payload: TPayload;
134
+ }> {
135
+ const rows = await testDb.db.execute<{ type: string; payload: TPayload }>(
136
+ sql`SELECT type, payload FROM kumiko_events ORDER BY id DESC LIMIT 1`,
137
+ );
138
+ const row = rows[0];
139
+ if (!row) throw new Error("no events in store");
140
+ return row;
141
+ }
142
+
143
+ test("create event payload excludes sensitive fields but entity row keeps them", async () => {
144
+ const result = await crud.create(
145
+ { email: "s@test.de", passwordHash: "pw-hash-123", apiToken: "tok-abc" },
146
+ adminUser,
147
+ tdb,
148
+ );
149
+ if (!result.isSuccess) throw new Error("create failed");
150
+ // Entity row: full data preserved.
151
+ expect(result.data.data["passwordHash"]).toBe("pw-hash-123");
152
+ expect(result.data.data["apiToken"]).toBe("tok-abc");
153
+
154
+ // Event payload: sensitive stripped, public retained.
155
+ const event = await lastEvent();
156
+ expect(event.type).toBe("esExecSensitive.created");
157
+ expect(event.payload["email"]).toBe("s@test.de");
158
+ expect(event.payload["passwordHash"]).toBeUndefined();
159
+ expect(event.payload["apiToken"]).toBeUndefined();
160
+ });
161
+
162
+ test("update event strips sensitive from BOTH changes and previous", async () => {
163
+ const created = await crud.create(
164
+ { email: "u@test.de", passwordHash: "old-hash", apiToken: "old-tok" },
165
+ adminUser,
166
+ tdb,
167
+ );
168
+ if (!created.isSuccess) throw new Error("create failed");
169
+
170
+ const result = await crud.update(
171
+ {
172
+ id: created.data.id,
173
+ version: 1,
174
+ changes: { passwordHash: "new-hash", email: "u2@test.de" },
175
+ },
176
+ adminUser,
177
+ tdb,
178
+ );
179
+ if (!result.isSuccess) throw new Error("update failed");
180
+
181
+ const event = await lastEvent<{
182
+ changes: { email?: string; passwordHash?: string };
183
+ previous: { email?: string; passwordHash?: string; apiToken?: string };
184
+ }>();
185
+ expect(event.type).toBe("esExecSensitive.updated");
186
+ // Changes: email retained (public), passwordHash stripped.
187
+ expect(event.payload.changes.email).toBe("u2@test.de");
188
+ expect(event.payload.changes.passwordHash).toBeUndefined();
189
+ // Previous: email retained, passwordHash + apiToken stripped.
190
+ expect(event.payload.previous.email).toBe("u@test.de");
191
+ expect(event.payload.previous.passwordHash).toBeUndefined();
192
+ expect(event.payload.previous.apiToken).toBeUndefined();
193
+ });
194
+
195
+ test("delete event strips sensitive from previous", async () => {
196
+ const created = await crud.create(
197
+ { email: "d@test.de", passwordHash: "pw", apiToken: "tk" },
198
+ adminUser,
199
+ tdb,
200
+ );
201
+ if (!created.isSuccess) throw new Error("create failed");
202
+
203
+ await crud.delete({ id: created.data.id }, adminUser, tdb);
204
+
205
+ type SensitivePrevious = {
206
+ previous: { email?: string; passwordHash?: string; apiToken?: string };
207
+ };
208
+ const event = await lastEvent<SensitivePrevious>();
209
+ expect(event.type).toBe("esExecSensitive.deleted");
210
+ expect(event.payload.previous.email).toBe("d@test.de");
211
+ expect(event.payload.previous.passwordHash).toBeUndefined();
212
+ expect(event.payload.previous.apiToken).toBeUndefined();
213
+ });
214
+
215
+ test("restore event strips sensitive from previous", async () => {
216
+ const created = await crud.create(
217
+ { email: "r@test.de", passwordHash: "pw", apiToken: "tk" },
218
+ adminUser,
219
+ tdb,
220
+ );
221
+ if (!created.isSuccess) throw new Error("create failed");
222
+ await crud.delete({ id: created.data.id }, adminUser, tdb);
223
+
224
+ await crud.restore({ id: created.data.id }, adminUser, tdb);
225
+
226
+ type SensitivePrevious = {
227
+ previous: { email?: string; passwordHash?: string; apiToken?: string };
228
+ };
229
+ const event = await lastEvent<SensitivePrevious>();
230
+ expect(event.type).toBe("esExecSensitive.restored");
231
+ expect(event.payload.previous.email).toBe("r@test.de");
232
+ expect(event.payload.previous.passwordHash).toBeUndefined();
233
+ expect(event.payload.previous.apiToken).toBeUndefined();
234
+ });
235
+ });
@@ -0,0 +1,304 @@
1
+ // Live==Rebuild-Equivalence für die ImplicitProjection (Sprint G).
2
+ //
3
+ // Beweist: für jede r.entity erzeugt der EventStoreExecutor (live) und
4
+ // rebuildProjection (replay über Implicit-Projection) **denselben**
5
+ // Tabellen-Stand. Ohne diesen Test können live + rebuild zwischen den
6
+ // Releases auseinanderdriften (z.B. wenn jemand die Live-Schreib-Logik
7
+ // im Executor ändert ohne applyEntityEvent anzupassen).
8
+ //
9
+ // Test-Strategie:
10
+ // 1. Live: 4 Aggregate mit verschiedenen Lifecycles (create / update /
11
+ // soft-delete / restore) durch den EventStoreExecutor jagen
12
+ // 2. Snapshot der Entity-Tabelle (nach Sortierung — ORDER BY id)
13
+ // 3. TRUNCATE der Entity-Tabelle
14
+ // 4. rebuildProjection für die ImplicitProjection
15
+ // 5. Snapshot erneut nehmen
16
+ // 6. deep-equal: identische Rows in identischer Reihenfolge
17
+
18
+ import { asc, sql } from "drizzle-orm";
19
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
20
+ import { createBooleanField, createEntity, createTextField, defineFeature } from "../../engine";
21
+ import { createRegistry } from "../../engine/registry";
22
+ import { createEventsTable } from "../../event-store";
23
+ import { rebuildProjection } from "../../pipeline";
24
+ import { createProjectionStateTable } from "../../pipeline/projection-state";
25
+ import { createEntityTable, createTestDb, type TestDb, TestUsers } from "../../stack";
26
+ import { createEventStoreExecutor } from "../event-store-executor";
27
+ import { buildDrizzleTable } from "../table-builder";
28
+ import { createTenantDb, type TenantDb } from "../tenant-db";
29
+
30
+ const userEntity = createEntity({
31
+ table: "read_implicit_users",
32
+ fields: {
33
+ email: createTextField({ required: true }),
34
+ firstName: createTextField(),
35
+ isEnabled: createBooleanField({ default: true }),
36
+ },
37
+ softDelete: true,
38
+ });
39
+
40
+ const userFeature = defineFeature("implicittest", (r) => {
41
+ r.entity("user", userEntity);
42
+ });
43
+
44
+ const userTable = buildDrizzleTable("user", userEntity);
45
+
46
+ let testDb: TestDb;
47
+ let tdb: TenantDb;
48
+ const adminUser = TestUsers.admin;
49
+
50
+ beforeAll(async () => {
51
+ testDb = await createTestDb();
52
+ await createEntityTable(testDb.db, userEntity, "user");
53
+ await createEventsTable(testDb.db);
54
+ await createProjectionStateTable(testDb.db);
55
+ tdb = createTenantDb(testDb.db, adminUser.tenantId);
56
+ });
57
+
58
+ afterAll(async () => {
59
+ await testDb.cleanup();
60
+ });
61
+
62
+ beforeEach(async () => {
63
+ await testDb.db.execute(
64
+ sql`TRUNCATE kumiko_events, read_implicit_users, kumiko_projections RESTART IDENTITY CASCADE`,
65
+ );
66
+ });
67
+
68
+ async function snapshotTable(): Promise<readonly Record<string, unknown>[]> {
69
+ const rows = await testDb.db.select().from(userTable).orderBy(asc(userTable["id"]));
70
+ return rows as readonly Record<string, unknown>[];
71
+ }
72
+
73
+ describe("implicit-projection / Live==Rebuild equivalence", () => {
74
+ test("4 aggregates × create/update/delete/restore round-trip identical to rebuild", async () => {
75
+ const crud = createEventStoreExecutor(userTable, userEntity, { entityName: "user" });
76
+
77
+ // 1. Live writes — verschiedene Lifecycle-Pfade über die 4 Aggregate.
78
+ // Aggregate A: create + update
79
+ // Aggregate B: create + update + delete (soft)
80
+ // Aggregate C: create + delete + restore
81
+ // Aggregate D: create only
82
+ const a = await crud.create({ email: "a@test.de", firstName: "Alice" }, adminUser, tdb);
83
+ if (!a.isSuccess) throw new Error("setup A failed");
84
+ await crud.update(
85
+ { id: a.data.id, version: 1, changes: { firstName: "Alice Updated" } },
86
+ adminUser,
87
+ tdb,
88
+ );
89
+
90
+ const b = await crud.create({ email: "b@test.de", firstName: "Bob" }, adminUser, tdb);
91
+ if (!b.isSuccess) throw new Error("setup B failed");
92
+ await crud.update({ id: b.data.id, version: 1, changes: { isEnabled: false } }, adminUser, tdb);
93
+ await crud.delete({ id: b.data.id }, adminUser, tdb);
94
+
95
+ const c = await crud.create({ email: "c@test.de", firstName: "Carol" }, adminUser, tdb);
96
+ if (!c.isSuccess) throw new Error("setup C failed");
97
+ await crud.delete({ id: c.data.id }, adminUser, tdb);
98
+ await crud.restore({ id: c.data.id }, adminUser, tdb);
99
+
100
+ await crud.create({ email: "d@test.de", firstName: "Dave" }, adminUser, tdb);
101
+
102
+ const liveSnapshot = await snapshotTable();
103
+
104
+ // Konkrete Erwartung an den Live-Stand: 4 erzeugte Aggregate, B ist
105
+ // soft-deleted (isDeleted=true), C ist restored (isDeleted=false),
106
+ // A und D sind unangetastet. Wenn das nicht stimmt, ist der Test
107
+ // setup buggy bevor wir den Rebuild überhaupt vergleichen.
108
+ expect(liveSnapshot).toHaveLength(4);
109
+ const byEmail = new Map(liveSnapshot.map((r) => [r["email"] as string, r]));
110
+ expect(byEmail.get("a@test.de")).toMatchObject({
111
+ firstName: "Alice Updated",
112
+ version: 2,
113
+ isDeleted: false,
114
+ });
115
+ expect(byEmail.get("b@test.de")).toMatchObject({
116
+ isEnabled: false,
117
+ version: 3,
118
+ isDeleted: true,
119
+ });
120
+ expect(byEmail.get("c@test.de")).toMatchObject({
121
+ version: 3,
122
+ isDeleted: false,
123
+ });
124
+ expect(byEmail.get("d@test.de")).toMatchObject({
125
+ firstName: "Dave",
126
+ version: 1,
127
+ isDeleted: false,
128
+ });
129
+
130
+ // 2. Rebuild from event-log — registry baut die ImplicitProjection,
131
+ // rebuildProjection findet sie über getAllProjections().
132
+ const registry = createRegistry([userFeature]);
133
+ const implicitName = "implicittest:projection:user-entity";
134
+ expect(registry.getAllProjections().has(implicitName)).toBe(true);
135
+
136
+ const result = await rebuildProjection(implicitName, {
137
+ db: testDb.db,
138
+ registry,
139
+ });
140
+
141
+ // 4 creates + 2 updates + 2 deletes + 1 restore = 9 Events. Wenn
142
+ // die ImplicitProjection silently nichts apply'd hätte, wäre der
143
+ // Count 0 — der Test würde dann den nachfolgenden deep-equal trotzdem
144
+ // verfehlen, aber explizit der Count fängt den Sub-Bug "apply lief,
145
+ // aber für die falsche Event-Anzahl".
146
+ expect(result.eventsProcessed).toBe(9);
147
+
148
+ // 3. Vergleich. Erst ID-für-ID strikt prüfen damit klar ist welche
149
+ // Felder verglichen werden — dann das Array-deep-equal als Catch-all.
150
+ const rebuildSnapshot = await snapshotTable();
151
+ expect(rebuildSnapshot).toHaveLength(liveSnapshot.length);
152
+ for (let i = 0; i < liveSnapshot.length; i++) {
153
+ const live = liveSnapshot[i];
154
+ const rebuilt = rebuildSnapshot[i];
155
+ // Diese Felder sind die User-sichtbare Truth (was sieht die UI?
156
+ // was schreibt der Audit-Log?). Wenn eines davon driftet, ist
157
+ // Live==Rebuild nicht mehr gegeben.
158
+ const fields = [
159
+ "id",
160
+ "tenantId",
161
+ "version",
162
+ "email",
163
+ "firstName",
164
+ "isEnabled",
165
+ "isDeleted",
166
+ "insertedAt",
167
+ "modifiedAt",
168
+ "deletedAt",
169
+ "insertedById",
170
+ "modifiedById",
171
+ "deletedById",
172
+ ] as const;
173
+ for (const f of fields) {
174
+ expect(rebuilt?.[f], `field "${f}" at row ${i}`).toEqual(live?.[f]);
175
+ }
176
+ }
177
+ // Catch-all: irgendein Feld das wir nicht explizit listen?
178
+ expect(rebuildSnapshot).toEqual(liveSnapshot);
179
+ });
180
+
181
+ test("ImplicitProjection ist im Registry registriert mit korrekten apply-keys", () => {
182
+ const registry = createRegistry([userFeature]);
183
+ const projection = registry.getAllProjections().get("implicittest:projection:user-entity");
184
+ expect(projection).toBeDefined();
185
+ if (!projection) return;
186
+ // 4 Auto-Verben weil softDelete=true → restored kommt dazu
187
+ expect(Object.keys(projection.apply).sort()).toEqual([
188
+ "user.created",
189
+ "user.deleted",
190
+ "user.restored",
191
+ "user.updated",
192
+ ]);
193
+ expect(projection.source).toBe("user");
194
+ });
195
+
196
+ test("ohne softDelete → keine restore-apply-key registriert", () => {
197
+ const hardDeleteEntity = createEntity({
198
+ table: "read_implicit_hard",
199
+ fields: { name: createTextField({ required: true }) },
200
+ });
201
+ const hardFeature = defineFeature("implicithard", (r) => {
202
+ r.entity("widget", hardDeleteEntity);
203
+ });
204
+ const registry = createRegistry([hardFeature]);
205
+ const projection = registry.getAllProjections().get("implicithard:projection:widget-entity");
206
+ expect(projection).toBeDefined();
207
+ if (!projection) return;
208
+ expect(Object.keys(projection.apply).sort()).toEqual([
209
+ "widget.created",
210
+ "widget.deleted",
211
+ "widget.updated",
212
+ ]);
213
+ });
214
+ });
215
+
216
+ // Sensitive-Drift ist eine bekannte Welle-3-Lücke: das Event-Log strippt
217
+ // sensitive-Felder VOR dem Append (GDPR-Annahme), die Live-Read-Tabelle
218
+ // bekommt sie über den unstripped flatData, der Rebuild-Pfad nur den
219
+ // stripped event.payload. Bei Schema-Rebuilds gehen sensitive Daten
220
+ // verloren.
221
+ //
222
+ // Dieser Test pinst die Drift explizit: Live row hat das sensitive Feld,
223
+ // Rebuild row hat NULL. Wenn Welle 3 das fixt (z.B. via separater
224
+ // sensitive-Spalte oder verschlüsseltem Event-Payload), bricht der Test
225
+ // und zwingt zu Aufmerksamkeit.
226
+
227
+ import { sql as drizzleSql, eq } from "drizzle-orm";
228
+
229
+ const sensitiveTable = "read_implicit_sensitive_users";
230
+
231
+ const sensitiveEntity = createEntity({
232
+ table: sensitiveTable,
233
+ fields: {
234
+ email: createTextField({ required: true }),
235
+ apiKey: createTextField({ sensitive: true }),
236
+ },
237
+ });
238
+
239
+ const sensitiveFeature = defineFeature("implicitsensitive", (r) => {
240
+ r.entity("sensitive-user", sensitiveEntity);
241
+ });
242
+
243
+ const sensitiveDrizzleTable = buildDrizzleTable("sensitive-user", sensitiveEntity);
244
+
245
+ describe("implicit-projection / dokumentierte Sensitive-Drift", () => {
246
+ beforeAll(async () => {
247
+ await createEntityTable(testDb.db, sensitiveEntity, "sensitive-user");
248
+ });
249
+
250
+ beforeEach(async () => {
251
+ await testDb.db.execute(
252
+ drizzleSql.raw(
253
+ `TRUNCATE ${sensitiveTable}, kumiko_events, kumiko_projections RESTART IDENTITY CASCADE`,
254
+ ),
255
+ );
256
+ });
257
+
258
+ test("Live schreibt sensitive-Felder, Rebuild lässt sie NULL (Welle-3-Roadmap)", async () => {
259
+ const crud = createEventStoreExecutor(sensitiveDrizzleTable, sensitiveEntity, {
260
+ entityName: "sensitive-user",
261
+ });
262
+
263
+ // 1. Live: create mit apiKey (sensitive). Read-Tabelle bekommt den
264
+ // Wert direkt vom Live-Pfad (unstripped flatData).
265
+ const created = await crud.create(
266
+ { email: "x@test.de", apiKey: "secret-token-abc" },
267
+ adminUser,
268
+ tdb,
269
+ );
270
+ if (!created.isSuccess) throw new Error("setup failed");
271
+
272
+ const [liveRow] = await testDb.db
273
+ .select()
274
+ .from(sensitiveDrizzleTable)
275
+ .where(eq(sensitiveDrizzleTable["id"], created.data.id as string));
276
+ expect(liveRow?.["apiKey"]).toBe("secret-token-abc");
277
+ expect(liveRow?.["email"]).toBe("x@test.de");
278
+
279
+ // 2. Verifiziere dass das Event-Log das Feld NICHT enthält (stripped).
280
+ const events = await testDb.db.execute<{ payload: Record<string, unknown> }>(
281
+ drizzleSql`SELECT payload FROM kumiko_events WHERE aggregate_id = ${created.data.id}::uuid`,
282
+ );
283
+ expect(events[0]?.payload).toBeDefined();
284
+ expect(events[0]?.payload?.["apiKey"]).toBeUndefined();
285
+ expect(events[0]?.payload?.["email"]).toBe("x@test.de");
286
+
287
+ // 3. Rebuild über die ImplicitProjection. Read-Tabelle wird aus
288
+ // event.payload neu materialisiert — apiKey ist nicht im Log,
289
+ // landet also als NULL/undefined in der rebuilt Row.
290
+ const registry = createRegistry([sensitiveFeature]);
291
+ await rebuildProjection("implicitsensitive:projection:sensitive-user-entity", {
292
+ db: testDb.db,
293
+ registry,
294
+ });
295
+
296
+ const [rebuiltRow] = await testDb.db
297
+ .select()
298
+ .from(sensitiveDrizzleTable)
299
+ .where(eq(sensitiveDrizzleTable["id"], created.data.id as string));
300
+ expect(rebuiltRow?.["email"]).toBe("x@test.de");
301
+ // DAS ist die Drift: sensitive Feld ist nach Rebuild weg.
302
+ expect(rebuiltRow?.["apiKey"]).toBeNull();
303
+ });
304
+ });