@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,403 @@
1
+ import { ZodObject, type ZodType, type z } from "zod";
2
+ import type { FeatureBuilderState } from "./feature-builder-state";
3
+ import { resolveName } from "./handler-helpers";
4
+ import { splitNamedDefinition, unwrapArrayForm } from "./object-form";
5
+ import { QnTypes, qn, toKebab } from "./qualified-name";
6
+ import type {
7
+ AuthClaimsFn,
8
+ ClaimKeyHandle,
9
+ ClaimKeyType,
10
+ ConfigKeyDefinition,
11
+ ConfigKeyHandle,
12
+ ConfigKeyType,
13
+ ConfigSeedDef,
14
+ DeclarativeEventMigration,
15
+ EventDef,
16
+ EventPiiFields,
17
+ EventUpcastFn,
18
+ JobDefinition,
19
+ JobHandlerFn,
20
+ MetricOptions,
21
+ NameOrRef,
22
+ NotificationDataFn,
23
+ NotificationRecipientFn,
24
+ NotificationTemplateFn,
25
+ QualifiedEventName,
26
+ SecretKeyHandle,
27
+ SecretOptions,
28
+ TranslationsDef,
29
+ } from "./types";
30
+
31
+ // Builds config/secrets/claims/events/jobs/notifications registrar methods.
32
+ export function buildConfigEventsJobsMethods<TName extends string>(
33
+ state: FeatureBuilderState,
34
+ name: TName,
35
+ ) {
36
+ // Overloaded: (keyName, def) for the single-key case, ({keys, seeds}) for
37
+ // the multi-key case — both funnel through the same qualify/register loop
38
+ // below, so a single-key call is byte-identical to what
39
+ // `{keys:{name:def}}` would have produced.
40
+ function config<T extends ConfigKeyType>(
41
+ keyName: string,
42
+ def: ConfigKeyDefinition<T>,
43
+ ): ConfigKeyHandle<T>;
44
+ function config<
45
+ TKeys extends Readonly<Record<string, ConfigKeyDefinition<ConfigKeyType>>>,
46
+ >(definition: {
47
+ readonly keys: TKeys;
48
+ readonly seeds?: Readonly<Record<string, ConfigSeedDef>>;
49
+ }): { readonly [K in keyof TKeys]: ConfigKeyHandle<TKeys[K]["type"]> };
50
+ function config(
51
+ arg1:
52
+ | string
53
+ | {
54
+ readonly keys: Record<string, ConfigKeyDefinition<ConfigKeyType>>;
55
+ readonly seeds?: Readonly<Record<string, ConfigSeedDef>>;
56
+ },
57
+ arg2?: ConfigKeyDefinition<ConfigKeyType>,
58
+ ): unknown {
59
+ // arg2 is always defined here — the two public overloads above guarantee
60
+ // it whenever arg1 is a string; the impl signature just has to widen it
61
+ // to optional to satisfy both call shapes.
62
+ const definition = typeof arg1 === "string" && arg2 ? { keys: { [arg1]: arg2 } } : arg1;
63
+ if (typeof definition === "string") {
64
+ throw new Error("config(): single-key form requires a definition as the second argument");
65
+ }
66
+ // Qualify eagerly (same as defineEvent) so the handle name matches what
67
+ // the registry stores — lazy qualification would break compile-time
68
+ // autocomplete and hand-built test registries.
69
+ const handles: Record<string, ConfigKeyHandle<ConfigKeyType>> = {};
70
+ for (const [key, keyDef] of Object.entries(definition.keys)) {
71
+ state.configKeys[key] = keyDef;
72
+ handles[key] = {
73
+ name: qn(toKebab(name), "config", toKebab(key)),
74
+ type: keyDef.type,
75
+ };
76
+ }
77
+ // Parse seeds: resolve qualified key names and validate scope
78
+ if (definition.seeds) {
79
+ for (const [shortKey, seedDef] of Object.entries(definition.seeds)) {
80
+ const keyDef = definition.keys[shortKey];
81
+ if (!keyDef) continue; // skip — boot-validator reports unknown keys
82
+ const qualifiedKey = qn(toKebab(name), "config", toKebab(shortKey));
83
+ const scope = seedDef.scope ?? keyDef.scope;
84
+ state.configSeeds.push({
85
+ key: qualifiedKey,
86
+ value: seedDef.value,
87
+ scope,
88
+ tenantId: seedDef.tenantId,
89
+ userId: seedDef.userId,
90
+ });
91
+ }
92
+ }
93
+ // Single-key call unwraps its own handle; multi-key returns the record.
94
+ return typeof arg1 === "string" ? handles[arg1] : handles;
95
+ }
96
+
97
+ // piiFields misconfiguration is a boot-time error, not a silent
98
+ // plaintext leak: both the pii field and its subjectField must exist
99
+ // on the payload schema (checkable when the schema is a ZodObject).
100
+ function validateEventPiiFields(
101
+ eventName: string,
102
+ schema: ZodType,
103
+ piiFields: EventPiiFields,
104
+ ): void {
105
+ const shape = schema instanceof ZodObject ? schema.shape : undefined;
106
+ for (const [field, spec] of Object.entries(piiFields)) {
107
+ if (field === spec.subjectField) {
108
+ throw new Error(
109
+ `[Feature ${name}] defineEvent("${eventName}"): piiFields."${field}" cannot use itself as subjectField — the subject id is a plaintext pseudonymous fk, the pii field is the value it owns.`,
110
+ );
111
+ }
112
+ for (const required of [field, spec.subjectField]) {
113
+ if (shape && !(required in shape)) {
114
+ throw new Error(
115
+ `[Feature ${name}] defineEvent("${eventName}"): piiFields references "${required}" which is not a field of the payload schema.`,
116
+ );
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ // Shared by defineEvent's `migrations` option — each entry registers a
123
+ // single upcast step, same validation/dedup as the old standalone
124
+ // r.eventMigration() call (folded into defineEvent, #1082 step 8).
125
+ function registerEventMigration(
126
+ eventName: string,
127
+ fromVersion: number,
128
+ toVersion: number,
129
+ transform: EventUpcastFn | DeclarativeEventMigration,
130
+ ): void {
131
+ if (toVersion !== fromVersion + 1) {
132
+ throw new Error(
133
+ `[Feature ${name}] defineEvent("${eventName}") migrations: only single-step migrations are allowed — toVersion must be fromVersion + 1 (got ${fromVersion} -> ${toVersion}). ` +
134
+ `Chain larger jumps by declaring each step separately.`,
135
+ );
136
+ }
137
+ if (!Number.isInteger(fromVersion) || fromVersion < 1) {
138
+ throw new Error(
139
+ `[Feature ${name}] defineEvent("${eventName}") migrations: fromVersion must be >= 1, got ${String(fromVersion)}`,
140
+ );
141
+ }
142
+ const qualified = qn(toKebab(name), "event", toKebab(eventName));
143
+ const list = state.eventMigrations[eventName] ?? [];
144
+ if (list.some((m) => m.fromVersion === fromVersion)) {
145
+ throw new Error(
146
+ `[Feature ${name}] defineEvent("${eventName}") migrations: a migration from v${fromVersion} is already declared. Each step may only be declared once.`,
147
+ );
148
+ }
149
+ const transformFn =
150
+ typeof transform === "function" ? transform : compileEventMigration(transform);
151
+ list.push({ eventName: qualified, fromVersion, toVersion, transform: transformFn });
152
+ state.eventMigrations[eventName] = list;
153
+ }
154
+
155
+ return {
156
+ config,
157
+ job(
158
+ jobNameOrDefinition: string | JobDefinition,
159
+ options?: Omit<JobDefinition, "name" | "handler">,
160
+ handler?: JobHandlerFn,
161
+ ): void {
162
+ const [jobName, jobOptions, jobHandler] =
163
+ typeof jobNameOrDefinition === "string"
164
+ ? [
165
+ jobNameOrDefinition,
166
+ options as Omit<JobDefinition, "name" | "handler">,
167
+ handler as JobHandlerFn,
168
+ ]
169
+ : (() => {
170
+ const { name, handler: h, ...rest } = jobNameOrDefinition;
171
+ return [name, rest, h] as const;
172
+ })();
173
+ // Resolve NameOrRef(s) in trigger.on. Multi-Trigger-Form: Array
174
+ // wird zu Array von resolved strings, Single bleibt single string —
175
+ // job-runner unterscheidet anhand Array.isArray.
176
+ const trigger =
177
+ "on" in jobOptions.trigger
178
+ ? {
179
+ on: Array.isArray(jobOptions.trigger.on)
180
+ ? jobOptions.trigger.on.map(resolveName)
181
+ : resolveName(jobOptions.trigger.on as NameOrRef), // @cast-boundary engine-bridge
182
+ }
183
+ : jobOptions.trigger;
184
+ state.jobs[jobName] = { ...jobOptions, trigger, name: jobName, handler: jobHandler };
185
+ },
186
+ notification(
187
+ notificationNameOrDefinition:
188
+ | string
189
+ | ({ readonly name: string } & {
190
+ readonly trigger: { readonly on: NameOrRef };
191
+ readonly recipient: NotificationRecipientFn;
192
+ readonly data: NotificationDataFn;
193
+ readonly templates?: Readonly<Record<string, NotificationTemplateFn>>;
194
+ }),
195
+ definition?: {
196
+ readonly trigger: { readonly on: NameOrRef };
197
+ readonly recipient: NotificationRecipientFn;
198
+ readonly data: NotificationDataFn;
199
+ readonly templates?: Readonly<Record<string, NotificationTemplateFn>>;
200
+ },
201
+ ): void {
202
+ const [notificationName, resolvedDefinition] =
203
+ typeof notificationNameOrDefinition === "string"
204
+ ? [notificationNameOrDefinition, definition as NonNullable<typeof definition>]
205
+ : splitNamedDefinition(notificationNameOrDefinition);
206
+ state.notifications[notificationName] = {
207
+ name: notificationName,
208
+ trigger: { on: resolveName(resolvedDefinition.trigger.on) },
209
+ recipient: resolvedDefinition.recipient,
210
+ data: resolvedDefinition.data,
211
+ templates: resolvedDefinition.templates,
212
+ };
213
+ },
214
+ translations(def: TranslationsDef): void {
215
+ state.translations = { ...state.translations, ...def.keys };
216
+ },
217
+ defineEvent: <const TInner extends string, TPayload>(
218
+ eventName: TInner,
219
+ schema: ZodType<TPayload>,
220
+ options?: {
221
+ readonly version?: number;
222
+ readonly piiFields?: EventPiiFields;
223
+ // Step-wise upcast chain for this event, folded in from the former
224
+ // standalone r.eventMigration() call (#1082 step 8) — an event and
225
+ // its schema evolution are one lifecycle, not two registrar
226
+ // concepts. Each entry's fromVersion must be unique and the chain
227
+ // from 1 to `version` must be gap-free (same validation as before).
228
+ readonly migrations?: readonly {
229
+ readonly fromVersion: number;
230
+ readonly toVersion: number;
231
+ readonly transform: EventUpcastFn | DeclarativeEventMigration;
232
+ }[];
233
+ },
234
+ ): EventDef<TPayload, QualifiedEventName<TName, TInner>> => {
235
+ // Return the fully-qualified event name so callers can pass it
236
+ // straight to ctx.appendEvent without hand-building the
237
+ // "<feature>:event:<name>" shape. Registry keeps events keyed by
238
+ // short name — qualification is the framework's job, not the feature
239
+ // author's.
240
+ //
241
+ // The runtime kebab-step (`qn(toKebab(feature), …)`) is mirrored at
242
+ // the type-level by `QualifiedEventName<TName, TInner>` so the
243
+ // returned `name` carries the literal qualified shape that the
244
+ // augmented `KumikoEventTypeMap` keys against.
245
+ const qualified = qn(toKebab(name), "event", toKebab(eventName));
246
+ const version = options?.version ?? 1;
247
+ if (!Number.isInteger(version) || version < 1) {
248
+ throw new Error(
249
+ `[Feature ${name}] defineEvent("${eventName}"): version must be a positive integer, got ${String(version)}`,
250
+ );
251
+ }
252
+ const piiFields = options?.piiFields;
253
+ if (piiFields) {
254
+ validateEventPiiFields(eventName, schema, piiFields);
255
+ }
256
+ // @cast-boundary engine-bridge — runtime-string mirrors the
257
+ // template-literal-type via QualifiedEventName + toKebab. Both
258
+ // sides are tested (CamelToKebab type-tests + scan-events kebab
259
+ // tests), so the cast is a contract, not a typing-loss.
260
+ const def: EventDef<TPayload, QualifiedEventName<TName, TInner>> = {
261
+ name: qualified as QualifiedEventName<TName, TInner>,
262
+ schema,
263
+ version,
264
+ ...(piiFields !== undefined && { piiFields }),
265
+ };
266
+ state.events[eventName] = def;
267
+ for (const m of options?.migrations ?? []) {
268
+ registerEventMigration(eventName, m.fromVersion, m.toVersion, m.transform);
269
+ }
270
+ return def;
271
+ },
272
+ readsConfig(
273
+ ...args: readonly [{ readonly keys: readonly string[] }] | readonly string[]
274
+ ): void {
275
+ state.configReads.push(...unwrapArrayForm(args, "keys"));
276
+ },
277
+ metric(
278
+ shortNameOrDefinition: string | ({ readonly name: string } & MetricOptions),
279
+ options?: MetricOptions,
280
+ ): void {
281
+ const [shortName, metricOptions] =
282
+ typeof shortNameOrDefinition === "string"
283
+ ? [shortNameOrDefinition, options as MetricOptions]
284
+ : splitNamedDefinition(shortNameOrDefinition);
285
+ if (state.metrics[shortName]) {
286
+ throw new Error(
287
+ `[Feature ${name}] Metric "${shortName}" already registered. ` +
288
+ `Metric names must be unique per feature.`,
289
+ );
290
+ }
291
+ state.metrics[shortName] = { shortName, ...metricOptions };
292
+ },
293
+ envSchema(schema: z.ZodObject<z.ZodRawShape>): void {
294
+ if (state.envSchema !== undefined) {
295
+ throw new Error(
296
+ `[Feature ${name}] r.envSchema() called twice — declare one composed Zod-object per feature.`,
297
+ );
298
+ }
299
+ state.envSchema = schema;
300
+ },
301
+ secret(
302
+ shortNameOrDefinition: string | ({ readonly name: string } & SecretOptions),
303
+ options?: SecretOptions,
304
+ ): SecretKeyHandle {
305
+ const [shortName, secretOptions] =
306
+ typeof shortNameOrDefinition === "string"
307
+ ? [shortNameOrDefinition, options as SecretOptions]
308
+ : splitNamedDefinition(shortNameOrDefinition);
309
+ if (state.secretKeys[shortName]) {
310
+ throw new Error(
311
+ `[Feature ${name}] Secret "${shortName}" already registered. ` +
312
+ `Secret key names must be unique per feature.`,
313
+ );
314
+ }
315
+ // Qualified name follows the framework's "<feature>:<type>:<name>"
316
+ // QN convention — same pattern config / jobs / events use. toKebab
317
+ // handles the common input shapes ("stripe.apiKey" → "stripe-api-key")
318
+ // so features can declare keys in their natural style without
319
+ // thinking about kebab-case on every call.
320
+ const qualifiedName = qn(toKebab(name), QnTypes.secret, toKebab(shortName));
321
+ state.secretKeys[shortName] = {
322
+ shortName,
323
+ qualifiedName,
324
+ ...secretOptions,
325
+ };
326
+ return { name: qualifiedName };
327
+ },
328
+ claimKey<T extends ClaimKeyType>(
329
+ shortNameOrDefinition: string | { readonly name: string; readonly type: T },
330
+ options?: { readonly type: T },
331
+ ): ClaimKeyHandle<T> {
332
+ const shortName =
333
+ typeof shortNameOrDefinition === "string"
334
+ ? shortNameOrDefinition
335
+ : shortNameOrDefinition.name;
336
+ const claimType: T =
337
+ typeof shortNameOrDefinition === "string"
338
+ ? (options as { readonly type: T }).type
339
+ : shortNameOrDefinition.type;
340
+ if (state.claimKeys[shortName]) {
341
+ throw new Error(
342
+ `[Feature ${name}] Claim key "${shortName}" already declared. ` +
343
+ "Claim short-names must be unique per feature.",
344
+ );
345
+ }
346
+ // Claim keys are NOT full QNs — the JWT shape is 2-segment
347
+ // "<featureName>:<shortName>" (same as Translation keys), not
348
+ // kebab-cased. The authClaims resolver prefixes with the raw
349
+ // feature.name + the raw inner key the hook returns, so the handle's
350
+ // `name` must match that literal string exactly for `readClaim` to
351
+ // find the value. kebab-conversion here would break the round-trip.
352
+ const qualifiedName = `${name}:${shortName}`;
353
+ state.claimKeys[shortName] = {
354
+ shortName,
355
+ qualifiedName,
356
+ type: claimType,
357
+ };
358
+ return { name: qualifiedName, type: claimType };
359
+ },
360
+ authClaims(fn: AuthClaimsFn): void {
361
+ state.authClaimsHooks.push(fn);
362
+ },
363
+ };
364
+ }
365
+
366
+ // Compile the declarative {rename, default, map} migration spec into an
367
+ // EventUpcastFn. Fixed order: rename → default → map.
368
+ function compileEventMigration(spec: DeclarativeEventMigration): EventUpcastFn {
369
+ // Registration-time (not replay-time) check: two rename sources mapping to
370
+ // the same target would silently drop one value on every future replay —
371
+ // event migrations run against the full production event history, so a
372
+ // typo here must fail loud at defineEvent() registration time, not at replay.
373
+ const renameTargets = new Map<string, string>();
374
+ for (const [from, to] of Object.entries(spec.rename ?? {})) {
375
+ const existing = renameTargets.get(to);
376
+ if (existing !== undefined) {
377
+ throw new Error(
378
+ `Declarative event migration: rename collision — both "${existing}" and "${from}" rename to "${to}". Only one source may map to a given target.`,
379
+ );
380
+ }
381
+ renameTargets.set(to, from);
382
+ }
383
+ return (payload) => {
384
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
385
+ throw new Error("Declarative event migration expects an object payload");
386
+ }
387
+ // @cast-boundary parse — payload is guarded as a plain object above
388
+ const next = { ...(payload as Record<string, unknown>) };
389
+ for (const [from, to] of Object.entries(spec.rename ?? {})) {
390
+ if (from in next) {
391
+ next[to] = next[from];
392
+ delete next[from];
393
+ }
394
+ }
395
+ for (const [key, value] of Object.entries(spec.default ?? {})) {
396
+ if (!(key in next)) next[key] = value;
397
+ }
398
+ for (const [key, fn] of Object.entries(spec.map ?? {})) {
399
+ if (key in next) next[key] = fn(next[key]);
400
+ }
401
+ return next;
402
+ };
403
+ }
@@ -0,0 +1,208 @@
1
+ import type { ZodType, z } from "zod";
2
+ import { toTableName } from "../db/table-builder";
3
+ import type {
4
+ QueryHandlerDefinition,
5
+ StreamHandlerDefinition,
6
+ WriteHandlerDefinition,
7
+ } from "./define-handler";
8
+ import type { RegisterEntityCrudOptions } from "./entity-handlers";
9
+ import { registerEntityCrud } from "./entity-handlers";
10
+ import type { FeatureBuilderState } from "./feature-builder-state";
11
+ import { resolveName } from "./handler-helpers";
12
+ import { splitNamedDefinition } from "./object-form";
13
+ import type {
14
+ AccessRule,
15
+ EntityDefinition,
16
+ EntityRef,
17
+ HandlerRef,
18
+ NameOrRef,
19
+ QueryHandlerFn,
20
+ RateLimitOption,
21
+ RelationDefinition,
22
+ StreamHandlerFn,
23
+ WriteHandlerFn,
24
+ } from "./types";
25
+ import type { PipelineDef } from "./types/step";
26
+
27
+ const CRUD_VERBS = new Set(["create", "update", "delete"]);
28
+
29
+ // Map handler name to entity via colon convention.
30
+ // "task:create" → entity "task". Bare CRUD verbs (create/update/delete) map
31
+ // when feature name matches an entity or the feature owns exactly one entity.
32
+ function tryMapEntity(state: FeatureBuilderState, name: string, handlerName: string): void {
33
+ const colonIdx = handlerName.indexOf(":");
34
+ if (colonIdx >= 0) {
35
+ const candidate = handlerName.slice(0, colonIdx);
36
+ if (state.entities[candidate]) {
37
+ state.handlerEntityMappings[handlerName] = candidate;
38
+ }
39
+ // skip: colon-prefixed handler processed (mapped or not), bare CRUD path not applicable
40
+ return;
41
+ }
42
+ if (CRUD_VERBS.has(handlerName)) {
43
+ if (state.entities[name]) {
44
+ state.handlerEntityMappings[handlerName] = name;
45
+ // skip: feature-name entity match is the preferred mapping
46
+ return;
47
+ }
48
+ const entityKeys = Object.keys(state.entities);
49
+ if (entityKeys.length === 1) {
50
+ state.handlerEntityMappings[handlerName] = entityKeys[0] as string;
51
+ }
52
+ }
53
+ }
54
+
55
+ // Builds entity/relation/writeHandler/queryHandler — the registrar
56
+ // methods that create or reference entities.
57
+ export function buildEntityHandlerMethods<TName extends string>(
58
+ state: FeatureBuilderState,
59
+ name: TName,
60
+ ) {
61
+ const methods = {
62
+ entity(
63
+ nameOrDefinition: string | ({ readonly name: string } & EntityDefinition),
64
+ definition?: EntityDefinition,
65
+ options?: { readonly table?: unknown },
66
+ ): EntityRef {
67
+ const [entityName, entityDefinition] =
68
+ typeof nameOrDefinition === "string"
69
+ ? [nameOrDefinition, definition as EntityDefinition]
70
+ : splitNamedDefinition(nameOrDefinition);
71
+ state.entities[entityName] = entityDefinition;
72
+ if (options?.table !== undefined) state.entityTables[entityName] = options.table;
73
+ return { name: entityName, table: entityDefinition.table ?? toTableName(entityName) };
74
+ },
75
+ relation(
76
+ entityRefOrDefinition:
77
+ | NameOrRef
78
+ | ({ readonly entity: NameOrRef; readonly name: string } & RelationDefinition),
79
+ relationName?: string,
80
+ definition?: RelationDefinition,
81
+ ): void {
82
+ const [entityRef, resolvedRelationName, resolvedDefinition] =
83
+ typeof entityRefOrDefinition === "object" && "entity" in entityRefOrDefinition
84
+ ? (() => {
85
+ const { entity, name: innerName, ...rest } = entityRefOrDefinition;
86
+ return [entity, innerName, rest as RelationDefinition] as const;
87
+ })()
88
+ : [entityRefOrDefinition, relationName as string, definition as RelationDefinition];
89
+ const entityName = resolveName(entityRef);
90
+ if (!state.relations[entityName]) state.relations[entityName] = {};
91
+ state.relations[entityName][resolvedRelationName] = resolvedDefinition;
92
+ },
93
+ writeHandler<TName extends string, TSchema extends ZodType>(
94
+ nameOrDef: string | WriteHandlerDefinition<TName, TSchema>,
95
+ schema?: TSchema,
96
+ handler?: WriteHandlerFn<z.infer<TSchema>>,
97
+ options?: { access?: AccessRule; rateLimit?: RateLimitOption },
98
+ ): HandlerRef {
99
+ if (typeof nameOrDef === "object") {
100
+ const def = nameOrDef;
101
+ state.writeHandlers[def.name] = {
102
+ name: def.name,
103
+ schema: def.schema,
104
+ // @cast-boundary engine-bridge — typed Dev-API's handler is
105
+ // generic over the schema's parsed payload (`WriteEvent<output<TSchema>>`),
106
+ // the storage form WriteHandlerFn carries `WriteEvent<unknown>`.
107
+ // Function-arg variance: TS sees the typed handler as stricter
108
+ // than the loose storage shape and rejects direct assignment.
109
+ // The runtime value is identical — the cast crosses that boundary.
110
+ // `satisfies` does not work here (it asserts assignability, which
111
+ // is what fails). Explicit cast is the right tool.
112
+ handler: def.handler as WriteHandlerFn,
113
+ ...(def.access && { access: def.access }),
114
+ ...(def.unsafeSkipTransitionGuard && { unsafeSkipTransitionGuard: true }),
115
+ ...(def.rateLimit && { rateLimit: def.rateLimit }),
116
+ // Forward the pipeline-build closure so boot-validators and
117
+ // Designer/AI tooling can inspect the step list. Absent on
118
+ // free-form handlers — defineWriteHandler only sets `perform`
119
+ // when the author used the pipeline form. Variance cast
120
+ // mirrors the handler-cast above: PipelineDef<output<TSchema>>
121
+ // is stricter than PipelineDef<unknown> for the same reason.
122
+ ...(def.perform !== undefined && {
123
+ perform: def.perform as PipelineDef,
124
+ }),
125
+ };
126
+ tryMapEntity(state, name, def.name);
127
+ return { name: def.name };
128
+ }
129
+ if (!schema || !handler)
130
+ throw new Error("writeHandler inline form requires schema + handler");
131
+ state.writeHandlers[nameOrDef] = {
132
+ name: nameOrDef,
133
+ schema,
134
+ handler: handler as WriteHandlerFn, // @cast-boundary engine-bridge
135
+ ...(options?.access && { access: options.access }),
136
+ ...(options?.rateLimit && { rateLimit: options.rateLimit }),
137
+ };
138
+ tryMapEntity(state, name, nameOrDef);
139
+ return { name: nameOrDef };
140
+ },
141
+ queryHandler<TName extends string, TSchema extends ZodType>(
142
+ nameOrDef: string | QueryHandlerDefinition<TName, TSchema>,
143
+ schema?: TSchema,
144
+ handler?: QueryHandlerFn<z.infer<TSchema>>,
145
+ options?: { access?: AccessRule; rateLimit?: RateLimitOption },
146
+ ): HandlerRef {
147
+ if (typeof nameOrDef === "object") {
148
+ const def = nameOrDef;
149
+ state.queryHandlers[def.name] = {
150
+ name: def.name,
151
+ schema: def.schema,
152
+ // @cast-boundary engine-bridge — typed Dev-API → erased internal storage
153
+ handler: def.handler as QueryHandlerFn, // @cast-boundary engine-bridge
154
+ ...(def.access && { access: def.access }),
155
+ ...(def.rateLimit && { rateLimit: def.rateLimit }),
156
+ };
157
+ tryMapEntity(state, name, def.name);
158
+ return { name: def.name };
159
+ }
160
+ if (!schema || !handler)
161
+ throw new Error("queryHandler inline form requires schema + handler");
162
+ state.queryHandlers[nameOrDef] = {
163
+ name: nameOrDef,
164
+ schema,
165
+ handler: handler as QueryHandlerFn, // @cast-boundary engine-bridge
166
+ ...(options?.access && { access: options.access }),
167
+ ...(options?.rateLimit && { rateLimit: options.rateLimit }),
168
+ };
169
+ tryMapEntity(state, name, nameOrDef);
170
+ return { name: nameOrDef };
171
+ },
172
+ streamHandler<TName extends string, TSchema extends ZodType>(
173
+ nameOrDef: string | StreamHandlerDefinition<TName, TSchema>,
174
+ schema?: TSchema,
175
+ handler?: StreamHandlerFn<z.infer<TSchema>>,
176
+ options?: { access?: AccessRule; rateLimit?: RateLimitOption },
177
+ ): HandlerRef {
178
+ if (typeof nameOrDef === "object") {
179
+ const def = nameOrDef;
180
+ state.streamHandlers[def.name] = {
181
+ name: def.name,
182
+ schema: def.schema,
183
+ // @cast-boundary engine-bridge — typed Dev-API → erased internal storage
184
+ handler: def.handler as StreamHandlerFn, // @cast-boundary engine-bridge
185
+ ...(def.access && { access: def.access }),
186
+ ...(def.rateLimit && { rateLimit: def.rateLimit }),
187
+ };
188
+ return { name: def.name };
189
+ }
190
+ if (!schema || !handler)
191
+ throw new Error("streamHandler inline form requires schema + handler");
192
+ state.streamHandlers[nameOrDef] = {
193
+ name: nameOrDef,
194
+ schema,
195
+ handler: handler as StreamHandlerFn, // @cast-boundary engine-bridge
196
+ ...(options?.access && { access: options.access }),
197
+ ...(options?.rateLimit && { rateLimit: options.rateLimit }),
198
+ };
199
+ return { name: nameOrDef };
200
+ },
201
+ };
202
+ return {
203
+ ...methods,
204
+ crud(entityName: string, entity: EntityDefinition, options?: RegisterEntityCrudOptions): void {
205
+ registerEntityCrud(methods, entityName, entity, options);
206
+ },
207
+ };
208
+ }
@@ -7,6 +7,7 @@
7
7
  // lesen.
8
8
 
9
9
  import { compareByCodepoint } from "../utils";
10
+ import { isEncryptedAtRest } from "./config-helpers";
10
11
  import { qualifyEntityName } from "./qualified-name";
11
12
  import type { Registry, UiHints } from "./types/feature";
12
13
 
@@ -106,7 +107,7 @@ export function buildManifestFromRegistry(
106
107
  type: def.type,
107
108
  scope: def.scope,
108
109
  default: def.default ?? null,
109
- encrypted: def.encrypted ?? false,
110
+ encrypted: isEncryptedAtRest(def),
110
111
  computed: def.computed !== undefined,
111
112
  options: def.options ?? null,
112
113
  bounds: def.bounds ?? null,