@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
@@ -0,0 +1,55 @@
1
+ // Sofortiges Blind-Index-Nulling nach einem Subject-Erase (#818).
2
+ //
3
+ // Nach kms.eraseKey ist der Ciphertext unlesbar, aber die deterministische
4
+ // bidx-Spalte bliebe bis zum nächsten Write/Rebuild matchbar — ein
5
+ // Linkage-Fenster ("hat irgendeine Row den Wert X"). Dieser Sweep schließt
6
+ // es sofort: der Ciphertext nennt sein Subject inline
7
+ // (kumiko-pii:v1:<subjectKey>:...), also findet ein LIKE-Prefix-Match exakt
8
+ // die Rows des erased Subjects — pro lookupable-Feld ein UPDATE.
9
+ //
10
+ // Rows, die der Forget-Lauf ohnehin via Executor löscht/anonymisiert,
11
+ // bekommen ihren bidx dort automatisch neu berechnet; dieser Sweep deckt
12
+ // die liegen bleibenden Rows ab (fremde Entities mit userOwned-Feldern).
13
+
14
+ import { collectLookupableFields } from "../crypto/blind-index";
15
+ import type { FeatureDefinition } from "../engine/types";
16
+ import { toSnakeCase } from "../utils/case";
17
+ import type { DbRunner } from "./connection";
18
+ import { resolveTableName } from "./entity-table-meta";
19
+ import { executeRawQuery } from "./queries/raw-sql";
20
+
21
+ function quoteIdent(name: string): string {
22
+ return `"${name.replace(/"/g, '""')}"`;
23
+ }
24
+
25
+ function escapeLikePattern(value: string): string {
26
+ return value.replace(/[\\%_]/g, (m) => `\\${m}`);
27
+ }
28
+
29
+ export async function nullBlindIndexesForSubject(
30
+ db: DbRunner,
31
+ features: ReadonlyMap<string, FeatureDefinition>,
32
+ subjectKey: string,
33
+ ): Promise<void> {
34
+ // "v%" matches any format version (v1 no-AAD, v2 AAD-bound, #1263) — the
35
+ // subject key placement is stable across versions.
36
+ const likePattern = `kumiko-pii:v%:${escapeLikePattern(subjectKey)}:%`;
37
+ for (const feature of features.values()) {
38
+ for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
39
+ const lookupable = collectLookupableFields(entity);
40
+ if (lookupable.length === 0) continue;
41
+ // Kein featureName-Prefix — der Dispatcher baut Entity-Tables ohne
42
+ // (buildEntityTable ohne featureName-Option), der Sweep muss dieselben
43
+ // Namen treffen.
44
+ const tableName = resolveTableName(entityName, entity, undefined);
45
+ for (const fieldName of lookupable) {
46
+ const snake = toSnakeCase(fieldName);
47
+ await executeRawQuery(
48
+ db,
49
+ `UPDATE ${quoteIdent(tableName)} SET ${quoteIdent(`${snake}_bidx`)} = NULL WHERE ${quoteIdent(snake)} LIKE $1`,
50
+ [likePattern],
51
+ );
52
+ }
53
+ }
54
+ }
55
+ }
@@ -8,9 +8,9 @@
8
8
  // Bun.SQL hat kein LISTEN — postgres-js-Peer für event-dispatcher.
9
9
 
10
10
  import postgres from "postgres";
11
- import type { DbConnection, DbConnectionOptions } from "./api";
11
+ import type { DbConnectionOptions, DbPoolHandle } from "./api";
12
12
 
13
- export function createBunConnection(url: string, options: DbConnectionOptions = {}): DbConnection {
13
+ export function createBunConnection(url: string, options: DbConnectionOptions = {}): DbPoolHandle {
14
14
  const bunOpts: { max?: number; idleTimeout?: number; connectionTimeout?: number } = {};
15
15
  if (options.maxConnections !== undefined) bunOpts.max = options.maxConnections;
16
16
  if (options.idleTimeoutSeconds !== undefined) bunOpts.idleTimeout = options.idleTimeoutSeconds;
@@ -1,10 +1,9 @@
1
1
  // collectTableMetas — kanonische ENTITY_METAS-Quelle für `kumiko schema
2
2
  // generate`. Erfasst dieselben Tabellen-Quellen wie der setupTestStack-
3
- // auto-push (entities, unmanagedTables, projections, multiStreamProjections,
4
- // rawTables) — die frühere Template-Variante sammelte nur entities +
5
- // unmanagedTables, wodurch projection-only-Tabellen (z.B. billing-foundation
6
- // read_subscriptions) nie in Migrations landeten und der erste Prod-Write
7
- // crashte (#255).
3
+ // auto-push (entities, storeTables, projections, multiStreamProjections) —
4
+ // die frühere Template-Variante sammelte nur entities + storeTables, wodurch
5
+ // projection-only-Tabellen (z.B. billing-foundation read_subscriptions) nie
6
+ // in Migrations landeten und der erste Prod-Write crashte (#255).
8
7
 
9
8
  import type { FeatureDefinition } from "../engine/types";
10
9
  import { compareByCodepoint } from "../utils";
@@ -60,11 +59,11 @@ export function collectTableMetas(
60
59
  metas.push(meta);
61
60
  byName.set(meta.tableName, { meta, origin: `entity "${name}" (${feature.name})` });
62
61
  }
63
- for (const entry of Object.values(feature.unmanagedTables)) {
62
+ for (const entry of Object.values(feature.storeTables)) {
64
63
  metas.push(entry.meta);
65
64
  byName.set(entry.meta.tableName, {
66
65
  meta: entry.meta,
67
- origin: `unmanagedTable "${entry.name}" (${feature.name})`,
66
+ origin: `storeTable "${entry.name}" (${feature.name})`,
68
67
  });
69
68
  }
70
69
  }
@@ -2,8 +2,8 @@ import { v5 as uuidv5 } from "uuid";
2
2
  import type { EntityDefinition } from "../engine";
3
3
  import { createSystemUser, SYSTEM_TENANT_ID } from "../engine";
4
4
  import type { ConfigSeedDef, Registry } from "../engine/types";
5
+ import type { EnvelopeCipher } from "../secrets/envelope-cipher";
5
6
  import type { DbConnection } from "./connection";
6
- import type { EncryptionProvider } from "./encryption";
7
7
  import { createEventStoreExecutor } from "./event-store-executor";
8
8
  import type { EntityTable } from "./table-builder";
9
9
  import { createTenantDb } from "./tenant-db";
@@ -30,7 +30,7 @@ export async function seedConfigValues<E extends EntityDefinition>(
30
30
  entity: E,
31
31
  registry: Registry,
32
32
  db: DbConnection,
33
- encryption?: EncryptionProvider,
33
+ cipher?: EnvelopeCipher,
34
34
  ): Promise<{ created: number; skipped: number }> {
35
35
  let created = 0;
36
36
  let skipped = 0;
@@ -48,12 +48,12 @@ export async function seedConfigValues<E extends EntityDefinition>(
48
48
  continue;
49
49
  }
50
50
 
51
- // Encrypted keys without an encryption provider would silently write
52
- // plaintext to a column the resolver later tries to decrypt — fail
53
- // loud at boot, not on first read in prod.
54
- if (keyDef.encrypted && !encryption) {
51
+ // Encrypted keys without a cipher would silently write plaintext to a
52
+ // column the resolver later tries to decrypt — fail loud at boot, not
53
+ // on first read in prod.
54
+ if (keyDef.encrypted && !cipher) {
55
55
  throw new Error(
56
- `seedConfigValues: key "${seed.key}" is encrypted but no EncryptionProvider was supplied.`,
56
+ `seedConfigValues: key "${seed.key}" is encrypted but no EnvelopeCipher was supplied.`,
57
57
  );
58
58
  }
59
59
 
@@ -77,8 +77,8 @@ export async function seedConfigValues<E extends EntityDefinition>(
77
77
  const aggregateId = uuidv5(idSource, CONFIG_SEED_NS);
78
78
 
79
79
  let value = JSON.stringify(seed.value);
80
- if (keyDef.encrypted && encryption) {
81
- value = encryption.encrypt(value);
80
+ if (keyDef.encrypted && cipher) {
81
+ value = await cipher.encrypt(value, { tenantId });
82
82
  }
83
83
 
84
84
  const payload: Record<string, unknown> = {
@@ -1,20 +1,15 @@
1
1
  // DB-Connection-Types: provider-agnostic via db/api.ts.
2
2
  // createConnection delegiert an postgres-provider (default) oder bun-provider (DB_PROVIDER=bun).
3
+ import type { DbConnection, PgClient } from "@cosmicdrift/kumiko-types/db-connection";
3
4
  import postgres from "postgres";
4
5
  import { readPositiveIntEnv } from "../utils/env-parse";
5
6
 
6
- export { createConnection, type DbConnectionOptions } from "./api";
7
-
8
- // Legacy Types für Aufrufer die direkt diese Module importieren
9
- // biome-ignore lint/suspicious/noExplicitAny: Bun.SQL global type
10
- export type DbConnection = ReturnType<typeof postgres> | any;
11
- // biome-ignore lint/suspicious/noExplicitAny: postgres-js namespace lookup
12
- export type DbTx = postgres.TransactionSql<any> | any;
13
- export type DbRunner = DbConnection | DbTx;
14
- export type DbRow = Record<string, unknown>;
15
- export type PgClient = ReturnType<typeof postgres>;
16
-
17
- export type PgListenClient = ReturnType<typeof postgres>;
7
+ // Raw client types (postgres-js | Bun.SQL) — the name used across query/
8
+ // event-store/pipeline call sites. The structural pool handle from ./api is
9
+ // `DbPoolHandle` (createConnection's return type) to avoid colliding with this.
10
+ export type * from "@cosmicdrift/kumiko-types/db-connection";
11
+ export type { DbConnectionOptions, DbPoolHandle } from "./api";
12
+ export { createConnection } from "./api";
18
13
 
19
14
  // Legacy: postgres-js only. Neue Aufrufer: createConnection() aus api.ts.
20
15
  // guard:dup-ok — andere Layer als createPgConnection (gibt DbConnection zurück, nicht postgres-Instanz)
package/src/db/cursor.ts CHANGED
@@ -1,21 +1,4 @@
1
- import type { EntityId, TenantId } from "../engine/types/identifiers";
2
-
3
- export type CursorQueryOptions = {
4
- tenantId: TenantId;
5
- cursor?: string;
6
- limit?: number;
7
- filterIds?: readonly EntityId[];
8
- sort?: string;
9
- sortDirection?: "asc" | "desc";
10
- };
11
-
12
- export type CursorResult<T> = {
13
- rows: T[];
14
- nextCursor: string | null;
15
- /** Optional total row count — nur present wenn der Caller `totalCount: true`
16
- * in der Query setzt. */
17
- total?: number;
18
- };
1
+ export type { CursorQueryOptions, CursorResult } from "@cosmicdrift/kumiko-types/cursor-types";
19
2
 
20
3
  // String-basiert damit UUIDs (Default seit Sprint F) + Integer-Auto-Increment
21
4
  // durch denselben Cursor-Pfad laufen. UUIDv7 erfüllt die lex-Monotonie
package/src/db/dialect.ts CHANGED
@@ -12,7 +12,18 @@
12
12
  //
13
13
  // The framework no longer imports drizzle-orm at runtime — schema-files
14
14
  // use only this module.
15
-
15
+ //
16
+ // Static import, not the ambient global: Bun doesn't expose Temporal on
17
+ // globalThis, so instantToDriver crashed on timestamptz writes (#1480).
18
+
19
+ import {
20
+ type ColumnHandle,
21
+ KUMIKO_COLUMNS_SYMBOL,
22
+ KUMIKO_META_SYMBOL,
23
+ KUMIKO_NAME_SYMBOL,
24
+ type SchemaTable,
25
+ } from "@cosmicdrift/kumiko-types/schema-table-types";
26
+ import { Temporal } from "temporal-polyfill";
16
27
  import type {
17
28
  ColumnMeta,
18
29
  CompositePrimaryKeyMeta,
@@ -21,6 +32,8 @@ import type {
21
32
  PgType,
22
33
  } from "./entity-table-meta";
23
34
 
35
+ export type { ColumnHandle, SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
36
+
24
37
  // Public type aliases — historical compat for callers that used to import
25
38
  // these from drizzle-orm/pg-core. SelectQuery is no longer a meaningful
26
39
  // shape (no chain builder); TableColumns is the new SchemaTable union.
@@ -29,31 +42,6 @@ export type TableColumns<_T = any> = SchemaTable;
29
42
  // biome-ignore lint/suspicious/noExplicitAny: legacy type — chain API is gone
30
43
  export type SelectQuery = any;
31
44
 
32
- // Column handle exposed on the SchemaTable. The `name` is the SQL column
33
- // name (snake_case); legacy code accesses `table.fieldName.name` to
34
- // produce raw SQL.
35
- export type ColumnHandle = {
36
- readonly name: string;
37
- readonly pgType: PgType;
38
- readonly getSQLType: () => string;
39
- };
40
-
41
- const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
42
- const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
43
- // Shadow-proof handle on the EntityTableMeta. The column handles below are
44
- // spread as enumerable props, so an entity field named `source`/`columns`/
45
- // `tableName`/… would overwrite the matching meta key. extractTableInfo reads
46
- // the canonical meta from this symbol instead of the (shadowable) props.
47
- const KUMIKO_META_SYMBOL = Symbol.for("kumiko:schema:Meta");
48
-
49
- // SchemaTable — opaque shape with both EntityTableMeta + Symbol-based
50
- // introspection. Returned by `table(...)`.
51
- export type SchemaTable = EntityTableMeta & {
52
- readonly [KUMIKO_NAME_SYMBOL]: string;
53
- readonly [KUMIKO_COLUMNS_SYMBOL]: Record<string, ColumnHandle>;
54
- readonly [field: string]: unknown;
55
- };
56
-
57
45
  function isNumericPgType(t: PgType): t is `numeric(${number},${number})` {
58
46
  return t.startsWith("numeric(");
59
47
  }
@@ -72,6 +60,8 @@ export function pgTypeToSqlType(pgType: PgType): string {
72
60
  return "boolean";
73
61
  case "integer":
74
62
  return "integer";
63
+ case "double precision":
64
+ return "double precision";
75
65
  case "bigint":
76
66
  return "bigint";
77
67
  case "serial":
@@ -234,6 +224,10 @@ export function integer(name: string): ColumnBuilder<number> {
234
224
  return buildColumn(name, "integer") as ColumnBuilder<number>;
235
225
  }
236
226
 
227
+ export function doublePrecision(name: string): ColumnBuilder<number> {
228
+ return buildColumn(name, "double precision") as ColumnBuilder<number>;
229
+ }
230
+
237
231
  export function serial(name: string): ColumnBuilder<number> {
238
232
  return buildColumn(name, "serial") as ColumnBuilder<number>;
239
233
  }
@@ -1,6 +1,13 @@
1
- import type { EntityDefinition } from "../engine/types";
2
- import type { EncryptionProvider } from "./encryption";
3
- import { createEncryptionProvider } from "./encryption";
1
+ // Entity-field encryption (`encrypted: true` on text/longText fields),
2
+ // backed by the same envelope cipher as encrypted config values: JSON
3
+ // StoredEnvelope in the TEXT column, kekVersion per value, legacy
4
+ // single-key ciphertexts (pre-envelope ENCRYPTION_KEY era) readable via
5
+ // the cipher's legacy fallback until re-encrypted.
6
+
7
+ import type { EntityDefinition, TenantId } from "../engine/types";
8
+ import { createEnvMasterKeyProvider } from "../secrets/env-master-key-provider";
9
+ import type { EnvelopeCipher } from "../secrets/envelope-cipher";
10
+ import type { KeyScope } from "../secrets/types";
4
11
 
5
12
  export function collectEncryptedFieldNames(entity: EntityDefinition): ReadonlySet<string> {
6
13
  const names = new Set<string>();
@@ -12,70 +19,120 @@ export function collectEncryptedFieldNames(entity: EntityDefinition): ReadonlySe
12
19
  return names;
13
20
  }
14
21
 
15
- function encryptFieldValue(
22
+ // BYOK hook: thread the row's tenant to the cipher when the row carries one
23
+ // (system-scope rows may not).
24
+ function scopeOf(row: Record<string, unknown>): KeyScope | undefined {
25
+ const tenantId = row["tenantId"];
26
+ return typeof tenantId === "string" ? { tenantId: tenantId as TenantId } : undefined; // @cast-boundary db-row
27
+ }
28
+
29
+ async function encryptFieldValue(
16
30
  fieldName: string,
17
31
  value: unknown,
18
- encryption: EncryptionProvider,
19
- ): string {
32
+ cipher: EnvelopeCipher,
33
+ scope: KeyScope | undefined,
34
+ ): Promise<string> {
20
35
  if (value === null || value === undefined) {
21
36
  throw new Error(`encrypted field "${fieldName}" cannot be null or undefined`);
22
37
  }
23
38
  if (typeof value !== "string") {
24
39
  throw new Error(`encrypted field "${fieldName}" must be a string, got ${typeof value}`);
25
40
  }
26
- return encryption.encrypt(value);
41
+ return cipher.encrypt(value, scope);
27
42
  }
28
43
 
29
- export function encryptEntityFieldValues(
44
+ export async function encryptEntityFieldValues(
30
45
  row: Record<string, unknown>,
31
46
  encryptedFields: ReadonlySet<string>,
32
- encryption: EncryptionProvider,
47
+ cipher: EnvelopeCipher,
33
48
  opts?: { onlyKeys?: Iterable<string> },
34
- ): Record<string, unknown> {
49
+ ): Promise<Record<string, unknown>> {
35
50
  if (encryptedFields.size === 0) return row;
36
51
  const only = opts?.onlyKeys ? new Set(opts.onlyKeys) : null;
37
52
  const out = { ...row };
53
+ const scope = scopeOf(row);
38
54
  for (const name of encryptedFields) {
39
55
  if (only && !only.has(name)) continue;
40
56
  if (!(name in out)) continue;
41
57
  const value = out[name];
42
58
  if (value === null || value === undefined) continue;
43
- out[name] = encryptFieldValue(name, value, encryption);
59
+ out[name] = await encryptFieldValue(name, value, cipher, scope);
44
60
  }
45
61
  return out;
46
62
  }
47
63
 
48
- export function decryptEntityFieldValues(
64
+ export async function decryptEntityFieldValues(
49
65
  row: Record<string, unknown>,
50
66
  encryptedFields: ReadonlySet<string>,
51
- encryption: EncryptionProvider,
52
- ): Record<string, unknown> {
67
+ cipher: EnvelopeCipher,
68
+ ): Promise<Record<string, unknown>> {
53
69
  if (encryptedFields.size === 0) return row;
54
70
  const out = { ...row };
71
+ const scope = scopeOf(row);
55
72
  for (const name of encryptedFields) {
56
73
  const value = out[name];
57
74
  if (value === null || value === undefined) continue;
58
75
  if (typeof value !== "string") continue;
59
- out[name] = encryption.decrypt(value);
76
+ out[name] = await cipher.decrypt(value, scope);
60
77
  }
61
78
  return out;
62
79
  }
63
80
 
64
- let cachedProvider: EncryptionProvider | undefined;
81
+ // Boot-injected app-wide cipher. run{Prod,Dev}App (and test setups that
82
+ // exercise encrypted fields) call configureEntityFieldEncryption once with
83
+ // the same cipher instance the config feature uses; executors resolve it
84
+ // lazily so entities without encrypted fields never touch it.
85
+ let injectedCipher: EnvelopeCipher | undefined;
65
86
 
66
- export function resolveEntityFieldEncryption(): EncryptionProvider {
67
- if (cachedProvider) return cachedProvider;
68
- const key = process.env["ENCRYPTION_KEY"];
69
- if (!key) {
87
+ export function configureEntityFieldEncryption(cipher: EnvelopeCipher | undefined): void {
88
+ injectedCipher = cipher;
89
+ }
90
+
91
+ export function resolveEntityFieldEncryption(): EnvelopeCipher {
92
+ if (!injectedCipher) {
70
93
  throw new Error(
71
- "ENCRYPTION_KEY environment variable is required (encrypted entity fields in use)",
94
+ "entity-field encryption is not configured — encrypted entity fields need a master key " +
95
+ "(KUMIKO_SECRETS_MASTER_KEY_V<n>); run{Prod,Dev}App wire the cipher automatically, " +
96
+ "custom boots call configureEntityFieldEncryption(cipher).",
72
97
  );
73
98
  }
74
- cachedProvider = createEncryptionProvider(key);
75
- return cachedProvider;
99
+ return injectedCipher;
100
+ }
101
+
102
+ // Non-throwing probe for callers that degrade gracefully (DSGVO export
103
+ // replaces encrypted fields with an explicit marker instead of failing
104
+ // the whole export when no cipher is wired).
105
+ export function configuredEntityFieldEncryption(): EnvelopeCipher | undefined {
106
+ return injectedCipher;
76
107
  }
77
108
 
78
109
  /** @internal test-only */
79
110
  export function resetEntityFieldEncryptionCacheForTests(): void {
80
- cachedProvider = undefined;
111
+ injectedCipher = undefined;
112
+ }
113
+
114
+ // Boot-time availability probe for validateBoot: either a cipher was
115
+ // injected, or the env keyring must be constructible — eager construction
116
+ // surfaces missing AND malformed keys (wrong length, bad base64) at boot
117
+ // instead of on the first encrypted read in prod. validateBoot runs before
118
+ // configureEntityFieldEncryption, so the env probe is the common path.
119
+ export function validateEntityFieldEncryptionAvailable(): void {
120
+ // skip: an injected cipher (test seam / custom KMS) satisfies availability
121
+ if (injectedCipher) return;
122
+ try {
123
+ createEnvMasterKeyProvider({
124
+ env: {
125
+ ...process.env,
126
+ KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION:
127
+ process.env["KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION"] ?? "1",
128
+ },
129
+ });
130
+ } catch (err) {
131
+ const reason = err instanceof Error ? err.message : String(err);
132
+ throw new Error(
133
+ `encrypted entity fields in use but no usable master key (${reason}) — set ` +
134
+ "KUMIKO_SECRETS_MASTER_KEY_V1 (32 bytes, base64) or inject a cipher via " +
135
+ "configureEntityFieldEncryption().",
136
+ );
137
+ }
81
138
  }
@@ -0,0 +1,2 @@
1
+ // Legacy path — re-exported for callers still importing this module directly.
2
+ export type * from "@cosmicdrift/kumiko-types/entity-table-meta-types";
@@ -18,81 +18,26 @@
18
18
  // auf Sondercases beschränken (child-projection-tables ohne tenant,
19
19
  // append-only-logs mit serial PK, aggregate-ID ohne DEFAULT, …).
20
20
 
21
+ import { collectPiiSubjectFields } from "../crypto";
22
+ import type { EntityDefinition, EntityIndexDef, FieldDefinition } from "../engine/types";
21
23
  import type {
22
- EntityDefinition,
23
- EntityIndexDef,
24
- EntityRelations,
25
- FieldDefinition,
26
- } from "../engine/types";
24
+ BuildEntityTableMetaOptions,
25
+ ColumnMeta,
26
+ EntityTableMeta,
27
+ IndexMeta,
28
+ UnmanagedTableInput,
29
+ } from "./entity-table-meta-types";
27
30
  import { READ_MODEL_PREFIX, toSnakeCase, toTableName } from "./table-builder";
28
31
 
29
- // PG-Type-Repertoire das die Read-Model-Tabellen brauchen. Bewusst
30
- // schmal — keine Vendor-spezifischen Typen (TSVECTOR, HSTORE, etc.).
31
- // App-Author die solche braucht greift in der reviewten SQL-Migration
32
- // hand-edit ein, nicht im Generator.
33
- export type PgType =
34
- | "uuid"
35
- | "text"
36
- | "boolean"
37
- | "integer"
38
- | "bigint"
39
- | "serial"
40
- | "bigserial"
41
- | "jsonb"
42
- | "timestamptz"
43
- | "timestamptz(3)"
44
- // Exact decimal — precision/scale are encoded in the type string so the
45
- // DDL renderer and read-coercion need no side-channel metadata.
46
- | `numeric(${number},${number})`;
47
-
48
- export type ColumnMeta = {
49
- readonly name: string; // snake_case PG column name
50
- readonly pgType: PgType;
51
- readonly notNull: boolean;
52
- // Raw SQL-default-expression (e.g. `now()`, `gen_random_uuid()`,
53
- // `'[]'::jsonb`). undefined = no DEFAULT clause.
54
- readonly defaultSql?: string;
55
- readonly primaryKey?: boolean;
56
- readonly identity?: boolean;
57
- // bigint/bigserial only: JS round-trip mode. `number` = createBigIntField /
58
- // drizzle mode:"number" (safe ≤2^53). `bigint` = money cents, raw unmanaged.
59
- readonly bigintJsMode?: "number" | "bigint";
60
- };
61
-
62
- export type IndexMeta = {
63
- readonly name: string;
64
- readonly columns: readonly string[]; // snake_case PG column names
65
- readonly unique?: boolean;
66
- // Raw SQL-where-expression for partial indexes. Caller is responsible
67
- // for safety — emitted verbatim.
68
- readonly whereSql?: string;
69
- // Set wenn die EntityDefinition ein partial-Index hat (def.where als
70
- // drizzle SQL-AST), der vom Generator nicht zuverlässig renderbar ist.
71
- // Renderer emittiert das Statement dann AUSKOMMENTIERT mit Warn-Hint —
72
- // App-Author muss das WHERE manuell im generierten SQL hinzufügen.
73
- readonly needsManualWhere?: boolean;
74
- };
75
-
76
- export type CompositePrimaryKeyMeta = {
77
- readonly name: string;
78
- readonly columns: readonly string[];
79
- };
80
-
81
- export type EntityTableMeta = {
82
- readonly tableName: string;
83
- readonly columns: readonly ColumnMeta[];
84
- readonly indexes: readonly IndexMeta[];
85
- // For tables with composite PK (no single id column, e.g. snapshots
86
- // keyed by aggregate_id+version). When set, no column should have
87
- // primaryKey:true; the constraint is emitted at table-level.
88
- readonly compositePrimaryKey?: CompositePrimaryKeyMeta;
89
- // Source-hint für Diagnose/Tests — nicht funktional verwendet.
90
- // "managed" = aus EntityDefinition (mit base-columns + audit-trail).
91
- // "unmanaged" = via defineUnmanagedTable — kein Standard-Audit, App
92
- // trägt Verantwortung. Migration-Generator + Tooling können den
93
- // discriminator nutzen um Warnungen zu rendern ("X tables are unmanaged").
94
- readonly source: "managed" | "unmanaged";
95
- };
32
+ export type {
33
+ BuildEntityTableMetaOptions,
34
+ ColumnMeta,
35
+ CompositePrimaryKeyMeta,
36
+ EntityTableMeta,
37
+ IndexMeta,
38
+ PgType,
39
+ UnmanagedTableInput,
40
+ } from "./entity-table-meta-types";
96
41
 
97
42
  // Standard base-columns für event-sourced Read-Model-Tabellen. Spiegelt
98
43
  // `buildBaseColumns()` aus table-builder.ts (drizzle-Variante).
@@ -153,7 +98,7 @@ function fieldToColumnMeta(
153
98
  case "text":
154
99
  case "longText": {
155
100
  const def = fieldDefaultLiteral(field);
156
- return [
101
+ const cols: ColumnMeta[] = [
157
102
  {
158
103
  name: snake,
159
104
  pgType: "text",
@@ -161,6 +106,13 @@ function fieldToColumnMeta(
161
106
  ...(def !== undefined && { defaultSql: def }),
162
107
  },
163
108
  ];
109
+ // lookupable → HMAC-Blind-Index-Pendant. Nullable ist Pflicht:
110
+ // managedChangeRequiresRecreate würde eine NOT-NULL-Spalte ohne
111
+ // Default auf Bestandstabellen als DROP+Rebuild diffen.
112
+ if (field.type === "text" && field.lookupable === true) {
113
+ cols.push({ name: `${snake}_bidx`, pgType: "text", notNull: false });
114
+ }
115
+ return cols;
164
116
  }
165
117
  case "boolean": {
166
118
  const def = fieldDefaultLiteral(field);
@@ -192,7 +144,7 @@ function fieldToColumnMeta(
192
144
  return [
193
145
  {
194
146
  name: snake,
195
- pgType: "integer",
147
+ pgType: field.integer === true ? "integer" : "double precision",
196
148
  notNull: field.required === true,
197
149
  ...(def !== undefined && { defaultSql: def }),
198
150
  },
@@ -280,11 +232,6 @@ export function resolveTableName(
280
232
  return `${featureName}_${baseName}`;
281
233
  }
282
234
 
283
- export type BuildEntityTableMetaOptions = {
284
- readonly featureName?: string;
285
- readonly relations?: EntityRelations;
286
- };
287
-
288
235
  export function buildEntityTableMeta(
289
236
  entityName: string,
290
237
  entity: EntityDefinition,
@@ -305,10 +252,17 @@ export function buildEntityTableMeta(
305
252
  for (const c of baseCols) colByName.set(c.name, c);
306
253
 
307
254
  const fieldNameToSnake = new Map<string, string>();
255
+ const bidxSnakeByFieldSnake = new Map<string, string>();
308
256
  for (const [name, field] of Object.entries(entity.fields)) {
309
257
  const fieldCols = fieldToColumnMeta(name, field, entity);
310
258
  for (const c of fieldCols) colByName.set(c.name, c);
311
- if (fieldCols.length === 1 && fieldCols[0]) fieldNameToSnake.set(name, fieldCols[0].name);
259
+ // Multi-column fields map to their primary column when its name IS the
260
+ // field's snake (text+bidx, money+currency) — matches the toSnakeCase
261
+ // fallback below, so explicit indexes keep resolving.
262
+ const primary = fieldCols[0];
263
+ if (primary && primary.name === toSnakeCase(name)) fieldNameToSnake.set(name, primary.name);
264
+ const bidxCol = fieldCols.find((c) => c.name.endsWith("_bidx"));
265
+ if (primary && bidxCol) bidxSnakeByFieldSnake.set(primary.name, bidxCol.name);
312
266
  }
313
267
 
314
268
  // Preserve base-col order, then any new user-col-names in fields-order.
@@ -347,6 +301,12 @@ export function buildEntityTableMeta(
347
301
  indexes.push({ name: `${tableName}_${snake}_idx`, columns: [snake] });
348
302
  }
349
303
 
304
+ // lookupable-Felder: Index auf der bidx-Spalte — der OR-Rewrite der
305
+ // Query-Compiler trifft sie bei jedem Equality-Lookup.
306
+ for (const bidxSnake of bidxSnakeByFieldSnake.values()) {
307
+ indexes.push({ name: `${tableName}_${bidxSnake}_idx`, columns: [bidxSnake] });
308
+ }
309
+
350
310
  // Explizit deklarierte indexes (EntityIndexDef). `def.where` ist ein
351
311
  // SqlExpression (`sql\`…\`` aus @cosmicdrift/kumiko-framework/db) —
352
312
  // renderbar via `.text`. Unbekannte where-Shapes bleiben needsManualWhere.
@@ -364,13 +324,33 @@ export function buildEntityTableMeta(
364
324
  ...(whereSql !== undefined && { whereSql }),
365
325
  ...(def.where !== undefined && whereSql === undefined && { needsManualWhere: true }),
366
326
  });
327
+ // Unique-Index über lookupable-Spalten: partielles bidx-Pendant, damit
328
+ // Uniqueness auch für verschlüsselte Rows greift. Das Original bleibt
329
+ // für Klartext-Alt-Rows; partial (IS NOT NULL) weil bidx bei erased/
330
+ // key-losen Rows NULL ist.
331
+ if (def.unique === true && def.where === undefined) {
332
+ const bidxCols = cols.map((c) => bidxSnakeByFieldSnake.get(c) ?? c);
333
+ if (bidxCols.some((c, i) => c !== cols[i])) {
334
+ const notNullParts = bidxCols
335
+ .filter((c, i) => c !== cols[i])
336
+ .map((c) => `"${c}" IS NOT NULL`);
337
+ indexes.push({
338
+ name: `${indexName}_bidx`,
339
+ columns: bidxCols,
340
+ unique: true,
341
+ whereSql: notNullParts.join(" AND "),
342
+ });
343
+ }
344
+ }
367
345
  }
368
346
 
347
+ const piiSubjectFields = collectPiiSubjectFields(entity);
369
348
  return {
370
349
  tableName,
371
350
  columns,
372
351
  indexes,
373
- source: "managed",
352
+ source: options?.source ?? "managed",
353
+ ...(piiSubjectFields.length > 0 && { piiSubjectFields }),
374
354
  };
375
355
  }
376
356
 
@@ -385,15 +365,8 @@ export function buildEntityTableMeta(
385
365
  // jede neue defineUnmanagedTable-Stelle prüfen.
386
366
  //
387
367
  // Heutige use-cases im framework:
388
- // - `read_delivery_attempts` — id kommt aus dem Aggregate-Stream
389
- // - `read_job_run_logs` — child-table, serial PK, kein tenant-scope
390
- export type UnmanagedTableInput = {
391
- readonly tableName: string;
392
- readonly columns: readonly ColumnMeta[];
393
- readonly indexes?: readonly IndexMeta[];
394
- readonly compositePrimaryKey?: CompositePrimaryKeyMeta;
395
- };
396
-
368
+ // - `store_delivery_attempts` — id kommt aus dem Aggregate-Stream
369
+ // - `store_job_run_logs` — child-table, serial PK, kein tenant-scope
397
370
  function sqlExpressionText(where: unknown): string | undefined {
398
371
  if (
399
372
  typeof where === "object" &&