@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,149 @@
1
+ import { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "../screen-helpers";
2
+ import type {
3
+ EditFieldSpec,
4
+ EditLayout,
5
+ FeatureDefinition,
6
+ ListColumnSpec,
7
+ RowAction,
8
+ ToolbarAction,
9
+ } from "../types";
10
+
11
+ const FUNCTION_DROPPED_HINT =
12
+ "functions are dropped by JSON.stringify when the screen config reaches the client " +
13
+ "bundle — the action/field silently no-ops at runtime instead of failing loudly.";
14
+
15
+ function throwIfFunction(value: unknown, message: string): void {
16
+ if (typeof value === "function") {
17
+ throw new Error(message);
18
+ }
19
+ }
20
+
21
+ // rowActions/toolbarActions carry row-context extractors (payload/params),
22
+ // static visibility conditions and navigate entityId — all declarative-DSL-
23
+ // only fields (RowFieldExtractor's `{ pick }`/`{ map }`, FieldCondition's
24
+ // `{ field, eq }`/`{ field, ne }`, plain strings). A function literal here
25
+ // does not type-check against these structural DSL types — it only reaches
26
+ // here via a leaked `any`/`as any`, which this runtime check backstops.
27
+ const ACTION_FUNCTION_FIELDS = ["payload", "params", "entityId", "visible"] as const;
28
+
29
+ function validateActionNoFunctions(
30
+ featureName: string,
31
+ screenId: string,
32
+ actionKind: "rowAction" | "toolbarAction",
33
+ action: RowAction | ToolbarAction,
34
+ ): void {
35
+ const record = action as unknown as Record<string, unknown>;
36
+ for (const field of ACTION_FUNCTION_FIELDS) {
37
+ throwIfFunction(
38
+ record[field],
39
+ `[Feature ${featureName}] Screen "${screenId}" ${actionKind} "${action.id}" ${field} ` +
40
+ `is a function — ${FUNCTION_DROPPED_HINT} Use the declarative DSL ({ pick }, { map }, ` +
41
+ `"fieldName", { field, eq }) instead.`,
42
+ );
43
+ }
44
+ }
45
+
46
+ export function validateActionWiring(feature: FeatureDefinition): void {
47
+ for (const screen of Object.values(feature.screens)) {
48
+ if (screen.type !== "entityList" && screen.type !== "projectionList") continue;
49
+ for (const action of screen.rowActions ?? []) {
50
+ validateActionNoFunctions(feature.name, screen.id, "rowAction", action);
51
+ }
52
+ for (const action of screen.toolbarActions ?? []) {
53
+ validateActionNoFunctions(feature.name, screen.id, "toolbarAction", action);
54
+ }
55
+ }
56
+ }
57
+
58
+ // EditFieldSpec's visible/readOnly/required are the same FieldCondition DSL
59
+ // as rowActions — same footgun, different screen type (entityEdit layouts).
60
+ const EDIT_FIELD_FUNCTION_FIELDS = ["visible", "readOnly", "required"] as const;
61
+
62
+ function validateEditFieldNoFunctions(
63
+ featureName: string,
64
+ screenId: string,
65
+ screenType: string,
66
+ fieldSpec: EditFieldSpec,
67
+ ): void {
68
+ const normalized = normalizeEditField(fieldSpec);
69
+ const record = normalized as unknown as Record<string, unknown>;
70
+ for (const key of EDIT_FIELD_FUNCTION_FIELDS) {
71
+ throwIfFunction(
72
+ record[key],
73
+ `[Feature ${featureName}] Screen "${screenId}" (${screenType}) field "${normalized.field}" ` +
74
+ `${key} is a function — ${FUNCTION_DROPPED_HINT} Use a FieldCondition ` +
75
+ `(boolean or { field, eq }/{ field, ne }) instead.`,
76
+ );
77
+ }
78
+ throwIfFunction(
79
+ normalized.renderer,
80
+ `[Feature ${featureName}] Screen "${screenId}" (${screenType}) field "${normalized.field}" ` +
81
+ `renderer is a function — ${FUNCTION_DROPPED_HINT} Use a FormatSpec ({ format: "..." }) instead.`,
82
+ );
83
+ }
84
+
85
+ function validateColumnsNoFunctions(
86
+ featureName: string,
87
+ screenId: string,
88
+ screenType: string,
89
+ columns: readonly ListColumnSpec[],
90
+ ): void {
91
+ for (const col of columns) {
92
+ const normalized = normalizeListColumn(col);
93
+ throwIfFunction(
94
+ normalized.renderer,
95
+ `[Feature ${featureName}] Screen "${screenId}" (${screenType}) column ` +
96
+ `"${normalized.field}" renderer is a function — ${FUNCTION_DROPPED_HINT} ` +
97
+ `Use a FormatSpec ({ format: "..." }) instead.`,
98
+ );
99
+ }
100
+ }
101
+
102
+ function validateEditLayoutNoFunctions(
103
+ featureName: string,
104
+ screenId: string,
105
+ screenType: string,
106
+ layout: EditLayout,
107
+ ): void {
108
+ for (const section of layout.sections) {
109
+ if (isExtensionEditSection(section)) continue;
110
+ for (const fieldSpec of section.fields) {
111
+ validateEditFieldNoFunctions(featureName, screenId, screenType, fieldSpec);
112
+ }
113
+ }
114
+ }
115
+
116
+ // Screen types that carry an EditLayout (entityEdit's layout shape, reused
117
+ // verbatim by actionForm/configEdit/projectionDetail) vs. ones that carry
118
+ // ListColumnSpec[] (entityList/projectionList, plus dashboard "list" panels)
119
+ // both funnel through the same two checks below — a function literal in
120
+ // either shape is dropped by JSON.stringify regardless of which screen type
121
+ // hosts it.
122
+ export function validateFieldWiring(feature: FeatureDefinition): void {
123
+ for (const screen of Object.values(feature.screens)) {
124
+ if (screen.type === "entityList" || screen.type === "projectionList") {
125
+ validateColumnsNoFunctions(feature.name, screen.id, screen.type, screen.columns);
126
+ continue;
127
+ }
128
+ if (
129
+ screen.type === "entityEdit" ||
130
+ screen.type === "actionForm" ||
131
+ screen.type === "configEdit" ||
132
+ screen.type === "projectionDetail"
133
+ ) {
134
+ validateEditLayoutNoFunctions(feature.name, screen.id, screen.type, screen.layout);
135
+ continue;
136
+ }
137
+ if (screen.type === "dashboard") {
138
+ for (const panel of screen.panels) {
139
+ if (panel.kind !== "list") continue;
140
+ validateColumnsNoFunctions(
141
+ feature.name,
142
+ `${screen.id}:${panel.id}`,
143
+ "dashboard-list",
144
+ panel.columns,
145
+ );
146
+ }
147
+ }
148
+ }
149
+ }
@@ -0,0 +1,21 @@
1
+ import type { BootCheckContext, FeatureDefinition } from "../types";
2
+
3
+ // r.bootCheck(fn) lets a feature declare its own mount-invariant instead of
4
+ // relying on framework-internal knowledge (the gdpr-storage.ts guards are
5
+ // the framework-owned version of this same idea). Each registered fn gets
6
+ // the full mounted-feature set and throws to fail the boot; we wrap the
7
+ // message with the owning feature's name so the error text points back at
8
+ // the feature that declared the check.
9
+ export function validateFeatureBootChecks(features: readonly FeatureDefinition[]): void {
10
+ const ctx: BootCheckContext = { features };
11
+ for (const feature of features) {
12
+ for (const check of feature.bootChecks) {
13
+ try {
14
+ check(ctx);
15
+ } catch (err) {
16
+ const message = err instanceof Error ? err.message : String(err);
17
+ throw new Error(`[Feature ${feature.name}] r.bootCheck failed: ${message}`);
18
+ }
19
+ }
20
+ }
21
+ }
@@ -1,3 +1,4 @@
1
+ import { isEncryptedAtRest } from "../config-helpers";
1
2
  import type { FeatureDefinition } from "../types";
2
3
 
3
4
  // --- Toggleable-dependency warnings ---
@@ -79,9 +80,9 @@ export function validateConfigKeyComputed(feature: FeatureDefinition): void {
79
80
  // returns a plain value, encrypted expects cipher-text in the row. The
80
81
  // cascade doesn't know which one to prefer on write. Rejecting at boot
81
82
  // is cheaper than surprising behaviour at runtime.
82
- if (keyDef.encrypted) {
83
+ if (isEncryptedAtRest(keyDef)) {
83
84
  throw new Error(
84
- `[Feature ${feature.name}] Config key "${keyName}" has both encrypted=true and a computed resolver — these are mutually exclusive paradigms`,
85
+ `[Feature ${feature.name}] Config key "${keyName}" has both encrypted/backing="secrets" and a computed resolver — these are mutually exclusive paradigms`,
85
86
  );
86
87
  }
87
88
  }
@@ -128,9 +129,9 @@ export function validateConfigKeyAllowPerRequest(feature: FeatureDefinition): vo
128
129
  // encrypted + per-request would expose a cipher-text interpretation
129
130
  // to query-strings. The secret-value shouldn't be transported this
130
131
  // way — reject as a paradigm-mismatch.
131
- if (keyDef.encrypted) {
132
+ if (isEncryptedAtRest(keyDef)) {
132
133
  throw new Error(
133
- `[Feature ${feature.name}] Config key "${keyName}" has allowPerRequest=true but encrypted=true — secret values may not be set via query-params`,
134
+ `[Feature ${feature.name}] Config key "${keyName}" has allowPerRequest=true but encrypted/backing="secrets" — secret values may not be set via query-params`,
134
135
  );
135
136
  }
136
137
  }
@@ -160,6 +161,41 @@ export function validateConfigKeyBacking(feature: FeatureDefinition): void {
160
161
  }
161
162
  }
162
163
 
164
+ // --- Config key piiEncrypted compatibility (kumiko-platform#231/#459) ---
165
+
166
+ export function validateConfigKeyPiiEncrypted(feature: FeatureDefinition): void {
167
+ for (const [keyName, keyDef] of Object.entries(feature.configKeys)) {
168
+ if (!keyDef.piiEncrypted) continue;
169
+
170
+ if (keyDef.type !== "text") {
171
+ throw new Error(
172
+ `[Feature ${feature.name}] Config key "${keyName}" has piiEncrypted=true but type="${keyDef.type}" — piiEncrypted only applies to text keys`,
173
+ );
174
+ }
175
+
176
+ // system scope has no subject (no tenant, no user) for the subject-KMS
177
+ // to encrypt under.
178
+ if (keyDef.scope === "system") {
179
+ throw new Error(
180
+ `[Feature ${feature.name}] Config key "${keyName}" has piiEncrypted=true but scope="system" — system-scope has no subject (tenant/user) for the subject-KMS to encrypt under`,
181
+ );
182
+ }
183
+
184
+ // Mutually exclusive with the shared-cipher path: a config value is
185
+ // either subject-keyed (piiEncrypted) or master-key-keyed (encrypted/
186
+ // backing="secrets"), never both — unlike entity fields, which allow
187
+ // both markers together (double-wrap, e.g. auth-mfa.totpSecret) because
188
+ // entity rows decrypt through a fixed executor pipeline that peels both
189
+ // layers. Config values have no such pipeline; picking one keeps the
190
+ // resolver simple.
191
+ if (isEncryptedAtRest(keyDef)) {
192
+ throw new Error(
193
+ `[Feature ${feature.name}] Config key "${keyName}" has both piiEncrypted=true and encrypted/backing="secrets" — pick one: piiEncrypted (subject-KMS, user-visible) or encrypted (shared cipher, server-only values)`,
194
+ );
195
+ }
196
+ }
197
+ }
198
+
163
199
  // --- Config key cross-feature reference validation ---
164
200
 
165
201
  export function validateConfigReads(
@@ -101,29 +101,27 @@ const USER_BUCKETED_RATE_LIMIT_PER: ReadonlySet<string> = new Set(["user", "user
101
101
  // at runtime, but we fail at boot to turn an easy-to-miss security regression
102
102
  // into a loud configuration error.
103
103
  export function validateHandlerAccess(feature: FeatureDefinition): void {
104
- for (const [name, handler] of Object.entries(feature.writeHandlers)) {
105
- if (!handler.access) {
106
- throw new Error(
107
- `Write handler "${feature.name}:write:${name}" is missing an access rule. ` +
108
- `Set { roles: [...] } for role-based access, or { openToAll: true } for any authenticated user.`,
109
- );
110
- }
111
- validateAnonymousRateLimit(feature.name, "write", name, handler.access, handler.rateLimit);
112
- }
113
- for (const [name, handler] of Object.entries(feature.queryHandlers)) {
114
- if (!handler.access) {
115
- throw new Error(
116
- `Query handler "${feature.name}:query:${name}" is missing an access rule. ` +
117
- `Set { roles: [...] } for role-based access, or { openToAll: true } for any authenticated user.`,
118
- );
104
+ const kinds = [
105
+ { kind: "write" as const, label: "Write", handlers: feature.writeHandlers },
106
+ { kind: "query" as const, label: "Query", handlers: feature.queryHandlers },
107
+ { kind: "stream" as const, label: "Stream", handlers: feature.streamHandlers },
108
+ ];
109
+ for (const { kind, label, handlers } of kinds) {
110
+ for (const [name, handler] of Object.entries(handlers)) {
111
+ if (!handler.access) {
112
+ throw new Error(
113
+ `${label} handler "${feature.name}:${kind}:${name}" is missing an access rule. ` +
114
+ `Set { roles: [...] } for role-based access, or { openToAll: true } for any authenticated user.`,
115
+ );
116
+ }
117
+ validateAnonymousRateLimit(feature.name, kind, name, handler.access, handler.rateLimit);
119
118
  }
120
- validateAnonymousRateLimit(feature.name, "query", name, handler.access, handler.rateLimit);
121
119
  }
122
120
  }
123
121
 
124
122
  export function validateAnonymousRateLimit(
125
123
  featureName: string,
126
- kind: "write" | "query",
124
+ kind: "write" | "query" | "stream",
127
125
  handlerName: string,
128
126
  access: NonNullable<FeatureDefinition["writeHandlers"][string]["access"]>,
129
127
  rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"],
@@ -178,8 +176,9 @@ export function validateMultiStreamProjections(feature: FeatureDefinition): void
178
176
  //
179
177
  // Die häufigste Quelle von Konflikten ist Hand-Konstruktion:
180
178
  // { foo: { type: "timestamp", locatedBy: "fooTz" } }
181
- // ohne das `fooTz`-Feld zu deklarieren. Der `locatedTimestamp(name)` Helper
182
- // macht das Pair atomar — wer ihn nutzt, fliegt nicht durch diesen Validator.
179
+ // ohne das `fooTz`-Feld zu deklarieren. `createLocatedTimestampField()`
180
+ // erzeugt stattdessen EIN Feld vom Typ "locatedTimestamp" — wer den nutzt,
181
+ // fliegt nicht durch diesen Validator.
183
182
  export function validateLocatedTimestamps(feature: FeatureDefinition): void {
184
183
  for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
185
184
  const fields = entity.fields;
@@ -190,8 +189,8 @@ export function validateLocatedTimestamps(feature: FeatureDefinition): void {
190
189
  throw new Error(
191
190
  `Feature "${feature.name}", entity "${entityName}": field "${fieldName}" has ` +
192
191
  `locatedBy: "${field.locatedBy}" but no field with that name exists in the entity. ` +
193
- `Either declare the tz-field, or use the locatedTimestamp("${fieldName.replace(/At$/, "")}") helper ` +
194
- `to create the pair atomically.`,
192
+ `Either declare the tz-field, or use createLocatedTimestampField() ` +
193
+ `to create a single located-timestamp field instead.`,
195
194
  );
196
195
  }
197
196
  if (referenced.type !== "tz") {
@@ -0,0 +1,88 @@
1
+ import { normalizeListColumn } from "../screen-helpers";
2
+ import type { EntityListScreenDefinition, FeatureDefinition } from "../types";
3
+
4
+ /** Operator lists default searchable; low-cardinality audit trails stay opt-out. */
5
+ export const SEARCHABLE_FALSE_WHITELIST = new Set(["download-attempt-list"]);
6
+
7
+ function hasFilterableFields(feature: FeatureDefinition, entityName: string): boolean {
8
+ const entities = feature.entities;
9
+ if (!entities) return false;
10
+ const entity = entities[entityName];
11
+ if (!entity) return false;
12
+ return Object.values(entity.fields).some(
13
+ (raw) => (raw as { readonly filterable?: boolean }).filterable === true,
14
+ );
15
+ }
16
+
17
+ function hasEntityEditDetail(feature: FeatureDefinition, entityName: string): boolean {
18
+ return Object.values(feature.screens).some(
19
+ (s) => s.type === "entityEdit" && s.entity === entityName,
20
+ );
21
+ }
22
+
23
+ function validateOneEntityListScreen(
24
+ feature: FeatureDefinition,
25
+ screen: EntityListScreenDefinition,
26
+ ): void {
27
+ const prefix = `[entityList] Feature "${feature.name}" screen "${screen.id}"`;
28
+
29
+ if (screen.searchable === false && !SEARCHABLE_FALSE_WHITELIST.has(screen.id)) {
30
+ throw new Error(
31
+ `${prefix}: searchable defaults to true for operator lists — set searchable: true or add "${screen.id}" to the whitelist`,
32
+ );
33
+ }
34
+
35
+ const filtersActive = hasFilterableFields(feature, screen.entity);
36
+ const searchable = screen.searchable !== false;
37
+ if ((searchable || filtersActive) && screen.defaultSort === undefined) {
38
+ throw new Error(
39
+ `${prefix}: defaultSort required when searchable or filterable fields are active`,
40
+ );
41
+ }
42
+
43
+ if (screen.defaultSort !== undefined) {
44
+ const sortField = screen.defaultSort.field;
45
+ const col = screen.columns.find((c) => normalizeListColumn(c).field === sortField);
46
+ if (col === undefined) {
47
+ throw new Error(`${prefix}: defaultSort.field "${sortField}" is not a listed column`);
48
+ }
49
+ const entityDef = feature.entities?.[screen.entity];
50
+ const fieldDef = entityDef?.fields[sortField];
51
+ const isSortable =
52
+ fieldDef !== undefined && "sortable" in fieldDef && fieldDef.sortable === true;
53
+ if (!isSortable) {
54
+ throw new Error(`${prefix}: defaultSort column "${sortField}" must be sortable`);
55
+ }
56
+ }
57
+
58
+ if (hasEntityEditDetail(feature, screen.entity)) {
59
+ const hasNavigate = (screen.rowActions ?? []).some(
60
+ (a) => a.kind === "navigate" && (a.id === "view" || a.id === "edit"),
61
+ );
62
+ if (!hasNavigate) {
63
+ throw new Error(
64
+ `${prefix}: detail screen exists — add a navigate rowAction with id "view" or "edit"`,
65
+ );
66
+ }
67
+ }
68
+
69
+ for (const col of screen.columns) {
70
+ const normalized = normalizeListColumn(col);
71
+ if (normalized.label === undefined) {
72
+ const entity = feature.entities?.[screen.entity];
73
+ const isDerived = entity?.derivedFields?.[normalized.field] !== undefined;
74
+ if (!entity?.fields[normalized.field] && !isDerived) {
75
+ throw new Error(`${prefix}: unknown column field "${normalized.field}"`);
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ export function validateEntityListScreens(features: readonly FeatureDefinition[]): void {
82
+ for (const feature of features) {
83
+ for (const screen of Object.values(feature.screens)) {
84
+ if (screen.type !== "entityList") continue;
85
+ validateOneEntityListScreen(feature, screen);
86
+ }
87
+ }
88
+ }
@@ -57,23 +57,3 @@ export function validateGdprStoragePersistence(features: readonly FeatureDefinit
57
57
  }
58
58
  }
59
59
  }
60
-
61
- // V2: export-without-erase guard. A feature that registers an EXT_USER_DATA
62
- // export hook without a matching delete hook exports data under Art.20 but
63
- // never erases it on forget — an Art.17 violation. Registry-level signal only;
64
- // runtime no-ops (a delete hook that silently skips) are not detectable here.
65
- export function validateGdprHookCompleteness(features: readonly FeatureDefinition[]): void {
66
- for (const feature of features) {
67
- for (const usage of feature.extensionUsages) {
68
- if (usage.extensionName !== "userData") continue;
69
- const hasExport = typeof usage.options?.["export"] === "function";
70
- const hasDelete = typeof usage.options?.["delete"] === "function";
71
- if (hasExport && !hasDelete) {
72
- // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
73
- console.warn(
74
- `[kumiko:boot] Feature "${feature.name}" exports entity "${usage.entityName}" via EXT_USER_DATA but registers no delete hook — data is included in Art.20 exports but never erased on forget (Art.17 risk). Add a delete hook, or a no-op with a comment explaining why erasure is intentionally skipped.`,
75
- );
76
- }
77
- }
78
- }
79
- }
@@ -1,20 +1,68 @@
1
1
  import {
2
2
  buildEffectiveTranslationKeys,
3
+ featureHasI18nSurface,
3
4
  findTranslationLocaleGaps,
4
5
  requiredKeysFromFeature,
6
+ requiredKeysFromNav,
7
+ requiredKeysFromScreen,
8
+ requiredKeysFromWorkspace,
5
9
  } from "../../i18n/required-surface-keys";
10
+ import { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE } from "../build-config-feature-schema";
11
+ import { createRegistry } from "../registry";
6
12
  import type { FeatureDefinition } from "../types";
13
+ import type { Registry } from "../types/feature";
14
+
15
+ function requiredKeysFromGeneratedConfigHub(registry: Registry): readonly string[] {
16
+ const schema = buildConfigFeatureSchema(registry);
17
+ if (schema.navs.length === 0) return [];
18
+ const out = new Set<string>();
19
+
20
+ for (const screen of schema.screens) {
21
+ for (const key of requiredKeysFromScreen(SETTINGS_HUB_FEATURE, screen)) out.add(key);
22
+ }
23
+ for (const nav of schema.navs) {
24
+ for (const key of requiredKeysFromNav(nav)) out.add(key);
25
+ }
26
+ if (schema.workspace) {
27
+ for (const key of requiredKeysFromWorkspace(schema.workspace.definition)) out.add(key);
28
+ }
29
+ out.add("config.settings.title");
30
+ for (const scope of ["system", "tenant", "user"] as const) {
31
+ out.add(`config.settings.${scope}`);
32
+ }
33
+
34
+ return [...out];
35
+ }
36
+
37
+ function isFrameworkOwnedI18nKey(key: string): boolean {
38
+ return key.startsWith("kumiko.");
39
+ }
40
+
41
+ function hasDefinedTranslation(defined: Set<string>, key: string): boolean {
42
+ if (defined.has(key)) return true;
43
+ const colon = key.indexOf(":");
44
+ if (colon > 0) {
45
+ const feature = key.slice(0, colon);
46
+ const local = key.slice(colon + 1);
47
+ if (defined.has(`${feature}:${local}`)) return true;
48
+ }
49
+ if (key.includes(".")) {
50
+ const feature = key.split(".")[0];
51
+ if (feature && defined.has(`${feature}:${key}`)) return true;
52
+ }
53
+ return false;
54
+ }
7
55
 
8
56
  export function validateI18nSurfaceKeys(features: readonly FeatureDefinition[]): void {
9
57
  const defined = buildEffectiveTranslationKeys(features);
58
+ const registry = createRegistry(features);
10
59
 
11
60
  for (const feature of features) {
12
- // ponytail: features without r.translations are legacy — once they register
13
- // keys, surface + locale checks apply (apps, new bundled work).
14
- if (Object.keys(feature.translations ?? {}).length === 0) continue;
61
+ if (!featureHasI18nSurface(feature)) continue;
15
62
 
16
63
  for (const key of requiredKeysFromFeature(feature)) {
17
- if (!defined.has(key)) {
64
+ if (isFrameworkOwnedI18nKey(key)) continue;
65
+ if (!hasDefinedTranslation(defined, key)) {
18
66
  throw new Error(
19
67
  `[i18n] Feature "${feature.name}": required translation key missing: "${key}"`,
20
68
  );
@@ -22,6 +70,12 @@ export function validateI18nSurfaceKeys(features: readonly FeatureDefinition[]):
22
70
  }
23
71
  }
24
72
 
73
+ for (const key of requiredKeysFromGeneratedConfigHub(registry)) {
74
+ if (!hasDefinedTranslation(defined, key)) {
75
+ throw new Error(`[i18n] Settings-Hub: required translation key missing: "${key}"`);
76
+ }
77
+ }
78
+
25
79
  for (const gap of findTranslationLocaleGaps(features)) {
26
80
  throw new Error(
27
81
  `[i18n] Feature "${gap.featureName}": key "${gap.key}" missing locale(s): ${gap.missingLocales.join(", ")}`,
@@ -1,12 +1,16 @@
1
+ import { validateEntityFieldEncryptionAvailable } from "../../db/entity-field-encryption";
1
2
  import { QnTypes, qualifyEntityName } from "../qualified-name";
2
3
  import type { ClaimKeyDefinition, FeatureDefinition } from "../types";
4
+ import { validateActionWiring, validateFieldWiring } from "./action-wiring";
3
5
  import { validateApiExposureMatching, validateExtensionUsages } from "./api-ext";
6
+ import { validateFeatureBootChecks } from "./boot-check";
4
7
  import {
5
8
  validateCircularDeps,
6
9
  validateConfigKeyAllowPerRequest,
7
10
  validateConfigKeyBacking,
8
11
  validateConfigKeyBounds,
9
12
  validateConfigKeyComputed,
13
+ validateConfigKeyPiiEncrypted,
10
14
  validateConfigKeyRequired,
11
15
  validateConfigReads,
12
16
  warnOnToggleableDependencies,
@@ -26,30 +30,35 @@ import {
26
30
  validateReferenceFields,
27
31
  validateTransitions,
28
32
  } from "./entity-handler";
29
- import { validateGdprHookCompleteness, validateGdprStoragePersistence } from "./gdpr-storage";
33
+ import { validateEntityListScreens } from "./entity-list-screens";
34
+ import { validateGdprStoragePersistence } from "./gdpr-storage";
30
35
  import { validateI18nSurfaceKeys } from "./i18n-keys";
31
- import { validateOwnershipRules } from "./ownership";
32
- import { validatePiiAndRetention } from "./pii-retention";
33
36
  import {
34
37
  collectKnownRoles,
35
38
  collectNavQns,
36
- collectScreenQns,
37
- collectScreensByShortId,
38
- collectWorkspaceQns,
39
39
  collectWriteHandlerQns,
40
- validateDefaultWorkspaceUniqueness,
41
40
  validateNavCycles,
42
41
  validateNavs,
42
+ } from "./nav";
43
+ import { validateOwnershipRules } from "./ownership";
44
+ import { validatePiiAndRetention } from "./pii-retention";
45
+ import {
46
+ collectScreenQns,
47
+ collectScreensByShortId,
43
48
  validateScreenShortIdCollisions,
44
49
  validateScreens,
50
+ } from "./screens";
51
+ import {
52
+ collectWorkspaceQns,
53
+ validateDefaultWorkspaceUniqueness,
45
54
  validateWorkspaces,
46
- } from "./screens-nav";
55
+ } from "./workspaces";
47
56
 
48
57
  export { validateAppCustomScreenWriteQns } from "./custom-screen-write-qns";
49
58
  // Re-export: wird von run-dev-app.ts benötigt um Write-Handler-QNs
50
59
  // an den Codegen zu übergeben. Nicht Teil von validateBoot, aber
51
60
  // dieselbe Extraktionslogik.
52
- export { collectWriteHandlerQns } from "./screens-nav";
61
+ export { collectWriteHandlerQns } from "./nav";
53
62
 
54
63
  /**
55
64
  * Validates all feature configurations at boot time.
@@ -158,8 +167,15 @@ export function validateBoot(features: readonly FeatureDefinition[]): void {
158
167
  validateConfigKeyComputed(feature);
159
168
  validateConfigKeyAllowPerRequest(feature);
160
169
  validateConfigKeyBacking(feature);
170
+ validateConfigKeyPiiEncrypted(feature);
161
171
  validateOwnershipRules(feature, allClaimKeys, knownRoles);
162
172
  validateMultiStreamProjections(feature);
173
+ // Vor validateScreens: dessen visible/entityId-Feldref-Checks werfen für
174
+ // einen Function-Wert bereits (mit verwirrender "unknown field undefined"-
175
+ // Message, weil `typeof fn !== "boolean"` true ist), bevor der klare
176
+ // Function-Check hier überhaupt liefe.
177
+ validateActionWiring(feature);
178
+ validateFieldWiring(feature);
163
179
  validateScreens(
164
180
  feature,
165
181
  featureMap,
@@ -175,12 +191,17 @@ export function validateBoot(features: readonly FeatureDefinition[]): void {
175
191
  validateNavCycles(allNavQns);
176
192
  validateDefaultWorkspaceUniqueness(allWorkspaceQns);
177
193
  validateI18nSurfaceKeys(features);
194
+ validateEntityListScreens(features);
178
195
  validateExtensionPreSaveWiring(features);
179
196
  validateGdprStoragePersistence(features);
180
- validateGdprHookCompleteness(features);
197
+ validateFeatureBootChecks(features);
181
198
 
182
- if (hasEncryptedFields && !process.env["ENCRYPTION_KEY"]) {
183
- throw new Error("ENCRYPTION_KEY environment variable is required (encrypted fields in use)");
199
+ if (hasEncryptedFields) {
200
+ // Availability check, not env-presence: eagerly building the keyring
201
+ // catches malformed keys (wrong length, bad base64) at boot instead of
202
+ // on the first encrypted read in prod. An injected cipher (test seam,
203
+ // custom KMS provider) satisfies the requirement without env keys.
204
+ validateEntityFieldEncryptionAvailable();
184
205
  }
185
206
 
186
207
  if (hasFileFields && !process.env["FILE_STORAGE_PROVIDER"]) {