@cosmicdrift/kumiko-framework 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +159 -0
  2. package/package.json +91 -0
  3. package/src/__tests__/anonymous-access.integration.ts +325 -0
  4. package/src/__tests__/error-contract.integration.ts +435 -0
  5. package/src/__tests__/field-access.integration.ts +269 -0
  6. package/src/__tests__/full-stack.integration.ts +914 -0
  7. package/src/__tests__/ownership.integration.ts +449 -0
  8. package/src/__tests__/reference-data.integration.ts +198 -0
  9. package/src/__tests__/transition-guard.integration.ts +340 -0
  10. package/src/api/__tests__/api.test.ts +337 -0
  11. package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
  13. package/src/api/__tests__/batch.integration.ts +404 -0
  14. package/src/api/__tests__/body-limit.test.ts +88 -0
  15. package/src/api/__tests__/csrf-middleware.test.ts +97 -0
  16. package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
  17. package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
  18. package/src/api/__tests__/nested-write.integration.ts +213 -0
  19. package/src/api/__tests__/readiness.test.ts +76 -0
  20. package/src/api/__tests__/request-id-middleware.test.ts +72 -0
  21. package/src/api/__tests__/sse-broker.test.ts +58 -0
  22. package/src/api/__tests__/sse-route.test.ts +112 -0
  23. package/src/api/anonymous-cookie.ts +60 -0
  24. package/src/api/api-constants.ts +64 -0
  25. package/src/api/auth-middleware.ts +418 -0
  26. package/src/api/auth-routes.ts +982 -0
  27. package/src/api/csrf-middleware.ts +77 -0
  28. package/src/api/index.ts +31 -0
  29. package/src/api/jwt.ts +66 -0
  30. package/src/api/observability-middleware.ts +89 -0
  31. package/src/api/readiness.ts +132 -0
  32. package/src/api/request-context.ts +49 -0
  33. package/src/api/request-id-middleware.ts +50 -0
  34. package/src/api/route-registrars.ts +195 -0
  35. package/src/api/routes.ts +135 -0
  36. package/src/api/server.ts +640 -0
  37. package/src/api/sse-broker.ts +71 -0
  38. package/src/api/sse-route.ts +62 -0
  39. package/src/api/tokens.ts +16 -0
  40. package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
  41. package/src/db/__tests__/compound-types.test.ts +114 -0
  42. package/src/db/__tests__/connection-options.test.ts +68 -0
  43. package/src/db/__tests__/cursor.test.ts +41 -0
  44. package/src/db/__tests__/db-helpers.test.ts +369 -0
  45. package/src/db/__tests__/dialect-instant.test.ts +50 -0
  46. package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
  47. package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
  48. package/src/db/__tests__/encryption.test.ts +39 -0
  49. package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
  50. package/src/db/__tests__/event-store-executor.integration.ts +235 -0
  51. package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
  52. package/src/db/__tests__/located-timestamp.test.ts +184 -0
  53. package/src/db/__tests__/money.test.ts +199 -0
  54. package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
  55. package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
  56. package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
  57. package/src/db/__tests__/row-helpers.test.ts +59 -0
  58. package/src/db/__tests__/schema-migration.integration.ts +273 -0
  59. package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
  60. package/src/db/__tests__/table-builder-required.test.ts +216 -0
  61. package/src/db/__tests__/tenant-db.integration.ts +606 -0
  62. package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
  63. package/src/db/apply-entity-event.ts +188 -0
  64. package/src/db/assert-exists-in.ts +59 -0
  65. package/src/db/compound-types.ts +47 -0
  66. package/src/db/connection.ts +104 -0
  67. package/src/db/cursor.ts +83 -0
  68. package/src/db/dialect.ts +109 -0
  69. package/src/db/eagerload.ts +174 -0
  70. package/src/db/encryption.ts +39 -0
  71. package/src/db/event-store-executor.ts +906 -0
  72. package/src/db/index.ts +55 -0
  73. package/src/db/located-timestamp.ts +114 -0
  74. package/src/db/money.ts +120 -0
  75. package/src/db/pg-error.ts +46 -0
  76. package/src/db/reference-data.ts +77 -0
  77. package/src/db/row-helpers.ts +53 -0
  78. package/src/db/schema-inspection.ts +25 -0
  79. package/src/db/table-builder.ts +475 -0
  80. package/src/db/tenant-db.ts +434 -0
  81. package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
  82. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
  83. package/src/engine/__tests__/boot-validator.test.ts +1865 -0
  84. package/src/engine/__tests__/build-app-schema.test.ts +154 -0
  85. package/src/engine/__tests__/claim-keys.test.ts +274 -0
  86. package/src/engine/__tests__/config-helpers.test.ts +236 -0
  87. package/src/engine/__tests__/effective-features.test.ts +86 -0
  88. package/src/engine/__tests__/engine.test.ts +1461 -0
  89. package/src/engine/__tests__/entity-handlers.test.ts +274 -0
  90. package/src/engine/__tests__/event-helpers.test.ts +68 -0
  91. package/src/engine/__tests__/extends-registrar.test.ts +159 -0
  92. package/src/engine/__tests__/factories-long-text.test.ts +84 -0
  93. package/src/engine/__tests__/factories-time.test.ts +158 -0
  94. package/src/engine/__tests__/field-predicates.test.ts +48 -0
  95. package/src/engine/__tests__/hook-phases.test.ts +132 -0
  96. package/src/engine/__tests__/identifiers.test.ts +35 -0
  97. package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
  98. package/src/engine/__tests__/nav.test.ts +267 -0
  99. package/src/engine/__tests__/ownership.test.ts +421 -0
  100. package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
  101. package/src/engine/__tests__/projection-helpers.test.ts +62 -0
  102. package/src/engine/__tests__/projection.test.ts +191 -0
  103. package/src/engine/__tests__/qualified-name.test.ts +264 -0
  104. package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
  105. package/src/engine/__tests__/run-in.test.ts +38 -0
  106. package/src/engine/__tests__/schema-builder.test.ts +380 -0
  107. package/src/engine/__tests__/screen.test.ts +408 -0
  108. package/src/engine/__tests__/state-machine.test.ts +148 -0
  109. package/src/engine/__tests__/system-user.test.ts +57 -0
  110. package/src/engine/__tests__/validation-hooks.test.ts +71 -0
  111. package/src/engine/access.ts +23 -0
  112. package/src/engine/boot-validator.ts +1528 -0
  113. package/src/engine/build-app-schema.ts +125 -0
  114. package/src/engine/config-helpers.ts +115 -0
  115. package/src/engine/constants.ts +85 -0
  116. package/src/engine/create-app.ts +98 -0
  117. package/src/engine/define-feature.ts +702 -0
  118. package/src/engine/define-handler.ts +78 -0
  119. package/src/engine/define-roles.ts +19 -0
  120. package/src/engine/effective-features.ts +87 -0
  121. package/src/engine/entity-handlers.ts +364 -0
  122. package/src/engine/event-helpers.ts +73 -0
  123. package/src/engine/factories.ts +328 -0
  124. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
  125. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
  126. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
  127. package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
  128. package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
  129. package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
  130. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
  131. package/src/engine/feature-ast/extractors.ts +2562 -0
  132. package/src/engine/feature-ast/index.ts +105 -0
  133. package/src/engine/feature-ast/parse.ts +369 -0
  134. package/src/engine/feature-ast/patch.ts +525 -0
  135. package/src/engine/feature-ast/patcher.ts +518 -0
  136. package/src/engine/feature-ast/patterns.ts +434 -0
  137. package/src/engine/feature-ast/render.ts +602 -0
  138. package/src/engine/feature-ast/source-location.ts +45 -0
  139. package/src/engine/field-access.ts +120 -0
  140. package/src/engine/index.ts +254 -0
  141. package/src/engine/ownership.ts +337 -0
  142. package/src/engine/parse-ref-target.ts +22 -0
  143. package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
  144. package/src/engine/pattern-library/index.ts +24 -0
  145. package/src/engine/pattern-library/library.ts +1117 -0
  146. package/src/engine/pattern-library/types.ts +255 -0
  147. package/src/engine/projection-helpers.ts +85 -0
  148. package/src/engine/qualified-name.ts +122 -0
  149. package/src/engine/read-claim.ts +31 -0
  150. package/src/engine/registry.ts +1325 -0
  151. package/src/engine/resolve-config-or-param.ts +153 -0
  152. package/src/engine/run-in.ts +29 -0
  153. package/src/engine/schema-builder.ts +175 -0
  154. package/src/engine/screen-filter-ops.ts +51 -0
  155. package/src/engine/state-machine.ts +70 -0
  156. package/src/engine/system-user.ts +32 -0
  157. package/src/engine/types/config.ts +306 -0
  158. package/src/engine/types/event-type-map.ts +37 -0
  159. package/src/engine/types/feature.ts +574 -0
  160. package/src/engine/types/fields.ts +422 -0
  161. package/src/engine/types/handlers.ts +742 -0
  162. package/src/engine/types/hooks.ts +142 -0
  163. package/src/engine/types/http-route.ts +54 -0
  164. package/src/engine/types/identifiers.ts +47 -0
  165. package/src/engine/types/index.ts +208 -0
  166. package/src/engine/types/nav.ts +46 -0
  167. package/src/engine/types/projection.ts +132 -0
  168. package/src/engine/types/relations.ts +51 -0
  169. package/src/engine/types/screen.ts +452 -0
  170. package/src/engine/types/workspace.ts +42 -0
  171. package/src/engine/validation.ts +33 -0
  172. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
  173. package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
  174. package/src/entrypoint/index.ts +442 -0
  175. package/src/errors/__tests__/classes.test.ts +371 -0
  176. package/src/errors/__tests__/write-failures.test.ts +109 -0
  177. package/src/errors/classes.ts +249 -0
  178. package/src/errors/i18n/de.yaml +83 -0
  179. package/src/errors/i18n/en.yaml +80 -0
  180. package/src/errors/index.ts +41 -0
  181. package/src/errors/kumiko-error.ts +67 -0
  182. package/src/errors/reasons.ts +36 -0
  183. package/src/errors/serialize.ts +136 -0
  184. package/src/errors/transition-details.ts +30 -0
  185. package/src/errors/write-error-info.ts +123 -0
  186. package/src/errors/zod-bridge.ts +49 -0
  187. package/src/event-store/__tests__/admin-api.integration.ts +361 -0
  188. package/src/event-store/__tests__/event-store.integration.ts +584 -0
  189. package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
  190. package/src/event-store/__tests__/perf.integration.ts +255 -0
  191. package/src/event-store/__tests__/snapshot.integration.ts +267 -0
  192. package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
  193. package/src/event-store/__tests__/upcaster.integration.ts +460 -0
  194. package/src/event-store/admin-api.ts +257 -0
  195. package/src/event-store/archive.ts +106 -0
  196. package/src/event-store/errors.ts +35 -0
  197. package/src/event-store/event-store.ts +405 -0
  198. package/src/event-store/events-schema.ts +90 -0
  199. package/src/event-store/index.ts +50 -0
  200. package/src/event-store/snapshot.ts +210 -0
  201. package/src/event-store/upcaster-dead-letter.ts +119 -0
  202. package/src/event-store/upcaster.ts +147 -0
  203. package/src/files/__tests__/content-disposition.test.ts +123 -0
  204. package/src/files/__tests__/file-field-column.integration.ts +103 -0
  205. package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
  206. package/src/files/__tests__/file-handle.test.ts +122 -0
  207. package/src/files/__tests__/files.integration.ts +830 -0
  208. package/src/files/__tests__/storage-tracking.integration.ts +153 -0
  209. package/src/files/content-disposition.ts +55 -0
  210. package/src/files/file-handle.ts +63 -0
  211. package/src/files/file-ref-table.ts +22 -0
  212. package/src/files/file-routes.ts +353 -0
  213. package/src/files/in-memory-provider.ts +62 -0
  214. package/src/files/index.ts +29 -0
  215. package/src/files/local-provider.ts +35 -0
  216. package/src/files/storage-tracking.ts +60 -0
  217. package/src/files/types.ts +118 -0
  218. package/src/i18n/__tests__/i18n.test.ts +72 -0
  219. package/src/i18n/index.ts +29 -0
  220. package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
  221. package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
  222. package/src/jobs/__tests__/jobs.integration.ts +566 -0
  223. package/src/jobs/index.ts +2 -0
  224. package/src/jobs/job-runner.ts +574 -0
  225. package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
  226. package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
  227. package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
  228. package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
  229. package/src/lifecycle/index.ts +13 -0
  230. package/src/lifecycle/lifecycle.ts +160 -0
  231. package/src/lifecycle/signal-handlers.ts +62 -0
  232. package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
  233. package/src/logging/index.ts +3 -0
  234. package/src/logging/pino-logger.ts +64 -0
  235. package/src/logging/types.ts +7 -0
  236. package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
  237. package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
  238. package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
  239. package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
  240. package/src/migrations/index.ts +28 -0
  241. package/src/migrations/projection-detection.ts +149 -0
  242. package/src/migrations/rebuild-marker.ts +64 -0
  243. package/src/migrations/schema-drift.ts +395 -0
  244. package/src/observability/__tests__/console-provider.test.ts +67 -0
  245. package/src/observability/__tests__/metric-validator.test.ts +87 -0
  246. package/src/observability/__tests__/noop-provider.test.ts +82 -0
  247. package/src/observability/__tests__/observability.integration.ts +559 -0
  248. package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
  249. package/src/observability/__tests__/recording-meter.test.ts +101 -0
  250. package/src/observability/__tests__/recording-tracer.test.ts +110 -0
  251. package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
  252. package/src/observability/console-provider.ts +130 -0
  253. package/src/observability/context.ts +26 -0
  254. package/src/observability/fallback.ts +34 -0
  255. package/src/observability/ids.ts +25 -0
  256. package/src/observability/index.ts +79 -0
  257. package/src/observability/metric-validator.ts +86 -0
  258. package/src/observability/metrics-handle.ts +56 -0
  259. package/src/observability/noop-provider.ts +146 -0
  260. package/src/observability/prometheus-meter.ts +284 -0
  261. package/src/observability/recording-meter.ts +156 -0
  262. package/src/observability/recording-tracer.ts +198 -0
  263. package/src/observability/redis-wrapper.ts +132 -0
  264. package/src/observability/sensitive-filter.ts +108 -0
  265. package/src/observability/standard-metrics.ts +213 -0
  266. package/src/observability/types/index.ts +29 -0
  267. package/src/observability/types/metric.ts +56 -0
  268. package/src/observability/types/provider.ts +32 -0
  269. package/src/observability/types/span.ts +64 -0
  270. package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
  271. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
  272. package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
  273. package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
  274. package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
  275. package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
  276. package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
  277. package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
  278. package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
  279. package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
  280. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
  281. package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
  282. package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
  283. package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
  284. package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
  285. package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
  286. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
  287. package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
  288. package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
  289. package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
  290. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
  291. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
  292. package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
  293. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
  294. package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
  295. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
  296. package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
  297. package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
  298. package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
  299. package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
  300. package/src/pipeline/append-event-core.ts +117 -0
  301. package/src/pipeline/auth-claims-resolver.ts +103 -0
  302. package/src/pipeline/cascade-handler.ts +113 -0
  303. package/src/pipeline/dispatcher.ts +1585 -0
  304. package/src/pipeline/distributed-lock.ts +37 -0
  305. package/src/pipeline/entity-cache.ts +113 -0
  306. package/src/pipeline/event-consumer-state.ts +108 -0
  307. package/src/pipeline/event-dedup.ts +23 -0
  308. package/src/pipeline/event-dispatcher.ts +1016 -0
  309. package/src/pipeline/event-retention.ts +154 -0
  310. package/src/pipeline/idempotency.ts +76 -0
  311. package/src/pipeline/index.ts +66 -0
  312. package/src/pipeline/lifecycle-pipeline.ts +409 -0
  313. package/src/pipeline/msp-rebuild.ts +242 -0
  314. package/src/pipeline/multi-stream-apply-context.ts +115 -0
  315. package/src/pipeline/projection-rebuild.ts +334 -0
  316. package/src/pipeline/projection-state.ts +72 -0
  317. package/src/pipeline/projections-runner.ts +56 -0
  318. package/src/pipeline/redis-keys.ts +11 -0
  319. package/src/pipeline/system-hooks.ts +190 -0
  320. package/src/random/__tests__/generate.test.ts +149 -0
  321. package/src/random/generate.ts +141 -0
  322. package/src/random/index.ts +8 -0
  323. package/src/random/words.ts +392 -0
  324. package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
  325. package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
  326. package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
  327. package/src/rate-limit/bucket.ts +36 -0
  328. package/src/rate-limit/index.ts +14 -0
  329. package/src/rate-limit/middleware.ts +152 -0
  330. package/src/rate-limit/resolver.ts +267 -0
  331. package/src/redis/__tests__/redis-options.test.ts +54 -0
  332. package/src/redis/index.ts +74 -0
  333. package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
  334. package/src/search/__tests__/search-adapter.test.ts +256 -0
  335. package/src/search/in-memory-adapter.ts +123 -0
  336. package/src/search/index.ts +12 -0
  337. package/src/search/meilisearch-adapter.ts +106 -0
  338. package/src/search/types.ts +39 -0
  339. package/src/secrets/__tests__/dek-cache.test.ts +213 -0
  340. package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
  341. package/src/secrets/__tests__/envelope.test.ts +74 -0
  342. package/src/secrets/__tests__/leak-guard.test.ts +92 -0
  343. package/src/secrets/__tests__/rotation.test.ts +149 -0
  344. package/src/secrets/dek-cache.ts +116 -0
  345. package/src/secrets/env-master-key-provider.ts +162 -0
  346. package/src/secrets/envelope.ts +55 -0
  347. package/src/secrets/index.ts +19 -0
  348. package/src/secrets/leak-guard.ts +87 -0
  349. package/src/secrets/rotation.ts +34 -0
  350. package/src/secrets/types.ts +107 -0
  351. package/src/stack/db.ts +104 -0
  352. package/src/stack/event-collector.ts +23 -0
  353. package/src/stack/index.ts +32 -0
  354. package/src/stack/redis.ts +44 -0
  355. package/src/stack/request-helper.ts +168 -0
  356. package/src/stack/table-helpers.ts +104 -0
  357. package/src/stack/test-stack.ts +357 -0
  358. package/src/stack/test-users.ts +37 -0
  359. package/src/testing/__tests__/e2e-generator.test.ts +230 -0
  360. package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
  361. package/src/testing/access-assertions.ts +15 -0
  362. package/src/testing/assertions.ts +35 -0
  363. package/src/testing/e2e-generator.ts +465 -0
  364. package/src/testing/expect-error.ts +25 -0
  365. package/src/testing/handler-context.ts +125 -0
  366. package/src/testing/http-cookies.ts +52 -0
  367. package/src/testing/index.ts +41 -0
  368. package/src/testing/late-bound.ts +39 -0
  369. package/src/testing/mutable-master-key-provider.ts +31 -0
  370. package/src/testing/observability-recorder.ts +54 -0
  371. package/src/testing/shared-entities.ts +49 -0
  372. package/src/testing/utils.ts +1 -0
  373. package/src/testing/wait-for.ts +31 -0
  374. package/src/time/__tests__/polyfill.test.ts +73 -0
  375. package/src/time/__tests__/tz-context.test.ts +121 -0
  376. package/src/time/index.ts +21 -0
  377. package/src/time/polyfill.ts +70 -0
  378. package/src/time/tz-context.ts +107 -0
  379. package/src/ui-types/app-schema.ts +57 -0
  380. package/src/ui-types/index.ts +65 -0
  381. package/src/utils/__tests__/assert.test.ts +17 -0
  382. package/src/utils/__tests__/env-parse.test.ts +54 -0
  383. package/src/utils/assert.ts +18 -0
  384. package/src/utils/env-parse.ts +16 -0
  385. package/src/utils/ids.ts +16 -0
  386. package/src/utils/index.ts +5 -0
  387. package/src/utils/safe-json.ts +30 -0
  388. package/src/utils/serialization.ts +7 -0
@@ -0,0 +1,702 @@
1
+ import type { ZodType, z } from "zod";
2
+ import { toTableName } from "../db/table-builder";
3
+ import { LifecycleHookTypes } from "./constants";
4
+ import type { QueryHandlerDefinition, WriteHandlerDefinition } from "./define-handler";
5
+ import { isKebabSegment, QnTypes, qn, toKebab } from "./qualified-name";
6
+ import type {
7
+ AccessRule,
8
+ AuthClaimsFn,
9
+ ClaimKeyDefinition,
10
+ ClaimKeyHandle,
11
+ ClaimKeyType,
12
+ ConfigKeyDefinition,
13
+ ConfigKeyHandle,
14
+ ConfigKeyType,
15
+ EntityDefinition,
16
+ EntityRef,
17
+ EventDef,
18
+ EventMigrationDef,
19
+ EventUpcastFn,
20
+ FeatureDefinition,
21
+ FeatureMetricDef,
22
+ FeatureRegistrar,
23
+ HandlerRef,
24
+ HookMap,
25
+ HookPhase,
26
+ JobDefinition,
27
+ JobHandlerFn,
28
+ LifecycleHookFn,
29
+ LifecycleHookType,
30
+ MetricOptions,
31
+ MultiStreamProjectionDefinition,
32
+ NameOrRef,
33
+ NotificationDataFn,
34
+ NotificationDefinition,
35
+ NotificationRecipientFn,
36
+ NotificationTemplateFn,
37
+ OwnedFn,
38
+ PhasedHook,
39
+ PostDeleteHookFn,
40
+ PostSaveHookFn,
41
+ PreDeleteHookFn,
42
+ ProjectionDefinition,
43
+ QualifiedEventName,
44
+ QueryHandlerDef,
45
+ QueryHandlerFn,
46
+ RateLimitOption,
47
+ ReferenceDataDef,
48
+ RegistrarExtensionDef,
49
+ RegistrarExtensionRegistration,
50
+ RelationDefinition,
51
+ SecretKeyDefinition,
52
+ SecretKeyHandle,
53
+ SecretOptions,
54
+ TranslationKeys,
55
+ TranslationsDef,
56
+ ValidationHookFn,
57
+ WriteHandlerDef,
58
+ WriteHandlerFn,
59
+ } from "./types";
60
+ import { HookPhases } from "./types";
61
+ import { resolveName } from "./types/handlers";
62
+ import type { HttpRouteDefinition } from "./types/http-route";
63
+ import type { NavDefinition } from "./types/nav";
64
+ import type { ScreenDefinition } from "./types/screen";
65
+ import type { WorkspaceDefinition } from "./types/workspace";
66
+
67
+ const LIFECYCLE_TYPES = Object.values(LifecycleHookTypes);
68
+
69
+ // `TExports` lets the setup callback hand back a typed object that
70
+ // downstream features can import (e.g. `tenantFeature.exports.config`). The
71
+ // runtime always packs whatever setup returns into `featureDef.exports` —
72
+ // `void` returns become `undefined` and stay invisible at the call site.
73
+ //
74
+ // `TName` (with `const` inference) captures the literal feature-name from
75
+ // the call-site (`defineFeature("driverOrders", ...)` → TName="driverOrders").
76
+ // The literal threads into the FeatureRegistrar so r.defineEvent's return
77
+ // carries `name: "driver-orders:event:foo"` as a literal — strict-mode
78
+ // for `ctx.appendEvent({ type: eventDef.name, ... })` lights up. Apps
79
+ // that don't care can keep the default-string and use the wrapper-based
80
+ // strict-mode (string-literal types per call-site) like before.
81
+ export function defineFeature<const TName extends string, TExports = undefined>(
82
+ name: TName,
83
+ setup: (r: FeatureRegistrar<TName>) => TExports,
84
+ ): FeatureDefinition & { readonly exports: TExports } {
85
+ const requires: string[] = [];
86
+ const optionalRequires: string[] = [];
87
+ const entities: Record<string, EntityDefinition> = {};
88
+ const relations: Record<string, Record<string, RelationDefinition>> = {};
89
+ const writeHandlers: Record<string, WriteHandlerDef> = {};
90
+ const queryHandlers: Record<string, QueryHandlerDef> = {};
91
+ const validationHooks: Record<string, ValidationHookFn> = {};
92
+ // preSave/preQuery stay unphased (owned-fn); postSave/preDelete/postDelete
93
+ // are phased (owned-fn + phase). Each hook carries its owning feature so
94
+ // the lifecycle pipeline can filter by effectiveFeatures without a parallel
95
+ // bookkeeping structure.
96
+ const lifecycleHooks: Record<string, Record<string, OwnedFn<LifecycleHookFn>[]>> = {};
97
+ const phasedLifecycleHooks: Record<
98
+ "postSave" | "preDelete" | "postDelete",
99
+ Record<string, PhasedHook<LifecycleHookFn>[]>
100
+ > = { postSave: {}, preDelete: {}, postDelete: {} };
101
+ const configKeys: Record<string, ConfigKeyDefinition> = {};
102
+ const jobs: Record<string, JobDefinition> = {};
103
+ const events: Record<string, { name: string; schema: ZodType; version: number }> = {};
104
+ const eventMigrations: Record<string, EventMigrationDef[]> = {};
105
+ const configReads: string[] = [];
106
+ const entityPostSave: Record<string, PhasedHook<PostSaveHookFn>[]> = {};
107
+ const entityPreDelete: Record<string, PhasedHook<PreDeleteHookFn>[]> = {};
108
+ const entityPostDelete: Record<string, PhasedHook<PostDeleteHookFn>[]> = {};
109
+ const notifications: Record<string, NotificationDefinition> = {};
110
+ const registrarExtensions: Record<string, RegistrarExtensionDef> = {};
111
+ const extensionUsages: RegistrarExtensionRegistration[] = [];
112
+ const referenceData: ReferenceDataDef[] = [];
113
+ const handlerEntityMappings: Record<string, string> = {};
114
+ const metrics: Record<string, FeatureMetricDef> = {};
115
+ const secretKeys: Record<string, SecretKeyDefinition> = {};
116
+ const projections: Record<string, ProjectionDefinition> = {};
117
+ const multiStreamProjections: Record<string, MultiStreamProjectionDefinition> = {};
118
+ const authClaimsHooks: AuthClaimsFn[] = [];
119
+ const claimKeys: Record<string, ClaimKeyDefinition> = {};
120
+ const screens: Record<string, ScreenDefinition> = {};
121
+ const navs: Record<string, NavDefinition> = {};
122
+ const workspaces: Record<string, WorkspaceDefinition> = {};
123
+ const httpRoutes: Record<string, HttpRouteDefinition> = {};
124
+ let translations: TranslationKeys = {};
125
+
126
+ for (const t of LIFECYCLE_TYPES) {
127
+ lifecycleHooks[t] = {};
128
+ }
129
+
130
+ let isSystemScoped = false;
131
+ let toggleableDefault: boolean | undefined;
132
+
133
+ // Map handler name to entity via colon convention.
134
+ // "task:create" → entity "task". No colon → standalone handler, no mapping.
135
+ function tryMapEntity(handlerName: string): void {
136
+ const colonIdx = handlerName.indexOf(":");
137
+ // skip: handler name is not entity-scoped (no colon), nothing to map
138
+ if (colonIdx < 0) return;
139
+ const candidate = handlerName.slice(0, colonIdx);
140
+ if (entities[candidate]) {
141
+ handlerEntityMappings[handlerName] = candidate;
142
+ }
143
+ }
144
+
145
+ const registrar: FeatureRegistrar<TName> = {
146
+ systemScope(): void {
147
+ isSystemScoped = true;
148
+ },
149
+
150
+ requires(...featureNames: string[]): void {
151
+ requires.push(...featureNames);
152
+ },
153
+
154
+ optionalRequires(...featureNames: string[]): void {
155
+ optionalRequires.push(...featureNames);
156
+ },
157
+
158
+ toggleable(options: { default: boolean }): void {
159
+ if (toggleableDefault !== undefined) {
160
+ throw new Error(
161
+ `[Feature ${name}] r.toggleable() called twice — a feature's toggleable status is declared once`,
162
+ );
163
+ }
164
+ toggleableDefault = options.default;
165
+ },
166
+
167
+ entity(entityName: string, definition: EntityDefinition): EntityRef {
168
+ entities[entityName] = definition;
169
+ return { name: entityName, table: definition.table ?? toTableName(entityName) };
170
+ },
171
+
172
+ writeHandler<TName extends string, TSchema extends ZodType>(
173
+ nameOrDef: string | WriteHandlerDefinition<TName, TSchema>,
174
+ schema?: TSchema,
175
+ handler?: WriteHandlerFn<z.infer<TSchema>>,
176
+ options?: { access?: AccessRule; rateLimit?: RateLimitOption },
177
+ ): HandlerRef {
178
+ if (typeof nameOrDef === "object") {
179
+ const def = nameOrDef;
180
+ writeHandlers[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 WriteHandlerFn,
185
+ ...(def.access && { access: def.access }),
186
+ ...(def.skipTransitionGuard && { skipTransitionGuard: true }),
187
+ ...(def.rateLimit && { rateLimit: def.rateLimit }),
188
+ };
189
+ tryMapEntity(def.name);
190
+ return { name: def.name };
191
+ }
192
+ if (!schema || !handler)
193
+ throw new Error("writeHandler inline form requires schema + handler");
194
+ writeHandlers[nameOrDef] = {
195
+ name: nameOrDef,
196
+ schema,
197
+ handler: handler as WriteHandlerFn, // @cast-boundary engine-bridge
198
+ ...(options?.access && { access: options.access }),
199
+ ...(options?.rateLimit && { rateLimit: options.rateLimit }),
200
+ };
201
+ tryMapEntity(nameOrDef);
202
+ return { name: nameOrDef };
203
+ },
204
+
205
+ queryHandler<TName extends string, TSchema extends ZodType>(
206
+ nameOrDef: string | QueryHandlerDefinition<TName, TSchema>,
207
+ schema?: TSchema,
208
+ handler?: QueryHandlerFn<z.infer<TSchema>>,
209
+ options?: { access?: AccessRule; rateLimit?: RateLimitOption },
210
+ ): HandlerRef {
211
+ if (typeof nameOrDef === "object") {
212
+ const def = nameOrDef;
213
+ queryHandlers[def.name] = {
214
+ name: def.name,
215
+ schema: def.schema,
216
+ // @cast-boundary engine-bridge — typed Dev-API → erased internal storage
217
+ handler: def.handler as QueryHandlerFn,
218
+ ...(def.access && { access: def.access }),
219
+ ...(def.rateLimit && { rateLimit: def.rateLimit }),
220
+ };
221
+ tryMapEntity(def.name);
222
+ return { name: def.name };
223
+ }
224
+ if (!schema || !handler)
225
+ throw new Error("queryHandler inline form requires schema + handler");
226
+ queryHandlers[nameOrDef] = {
227
+ name: nameOrDef,
228
+ schema,
229
+ handler: handler as QueryHandlerFn, // @cast-boundary engine-bridge
230
+ ...(options?.access && { access: options.access }),
231
+ ...(options?.rateLimit && { rateLimit: options.rateLimit }),
232
+ };
233
+ tryMapEntity(nameOrDef);
234
+ return { name: nameOrDef };
235
+ },
236
+
237
+ relation(entityRef: NameOrRef, relationName: string, definition: RelationDefinition): void {
238
+ const entityName = resolveName(entityRef);
239
+ if (!relations[entityName]) relations[entityName] = {};
240
+ relations[entityName][relationName] = definition;
241
+ },
242
+
243
+ hook(
244
+ type: LifecycleHookType | "validation",
245
+ target: NameOrRef | readonly NameOrRef[],
246
+ fn: LifecycleHookFn | ValidationHookFn,
247
+ options?: { phase?: HookPhase },
248
+ ): void {
249
+ const targets = Array.isArray(target) ? target : [target];
250
+ const names = targets.map(resolveName);
251
+
252
+ // Hook-fn casts unten alle: @cast-boundary engine-bridge
253
+ // — typed Dev-API (LifecycleHookFn|ValidationHookFn) → erased Map<name, fn>.
254
+ if (type === "validation") {
255
+ for (const n of names) {
256
+ validationHooks[n] = fn as ValidationHookFn; // @cast-boundary engine-bridge
257
+ }
258
+ // skip: validation hooks have no phase, stored and done
259
+ return;
260
+ }
261
+
262
+ if (type === LifecycleHookTypes.preSave || type === LifecycleHookTypes.preQuery) {
263
+ if (!lifecycleHooks[type]) lifecycleHooks[type] = {};
264
+ for (const n of names) {
265
+ if (!lifecycleHooks[type][n]) lifecycleHooks[type][n] = [];
266
+ lifecycleHooks[type][n].push({ fn: fn as LifecycleHookFn, featureName: name }); // @cast-boundary engine-bridge
267
+ }
268
+ // skip: pre-hooks have no phase, stored and done
269
+ return;
270
+ }
271
+
272
+ // Phased storage. preDelete has no phase option (always inTransaction);
273
+ // postSave/postDelete default to afterCommit.
274
+ const phase =
275
+ type === LifecycleHookTypes.preDelete
276
+ ? HookPhases.inTransaction
277
+ : (options?.phase ?? HookPhases.afterCommit);
278
+ const bucket = phasedLifecycleHooks[type];
279
+ for (const n of names) {
280
+ if (!bucket[n]) bucket[n] = [];
281
+ bucket[n].push({ fn: fn as LifecycleHookFn, phase, featureName: name }); // @cast-boundary engine-bridge
282
+ }
283
+ },
284
+
285
+ entityHook(
286
+ type: "postSave" | "preDelete" | "postDelete",
287
+ entityRef: NameOrRef,
288
+ fn: LifecycleHookFn,
289
+ options?: { phase?: HookPhase },
290
+ ): void {
291
+ const entityName = resolveName(entityRef);
292
+ if (type === LifecycleHookTypes.postSave) {
293
+ const phase = options?.phase ?? HookPhases.afterCommit;
294
+ if (!entityPostSave[entityName]) entityPostSave[entityName] = [];
295
+ entityPostSave[entityName].push({ fn: fn as PostSaveHookFn, phase, featureName: name }); // @cast-boundary engine-bridge
296
+ } else if (type === LifecycleHookTypes.preDelete) {
297
+ if (!entityPreDelete[entityName]) entityPreDelete[entityName] = [];
298
+ entityPreDelete[entityName].push({
299
+ fn: fn as PreDeleteHookFn, // @cast-boundary engine-bridge
300
+ phase: HookPhases.inTransaction,
301
+ featureName: name,
302
+ });
303
+ } else if (type === LifecycleHookTypes.postDelete) {
304
+ const phase = options?.phase ?? HookPhases.afterCommit;
305
+ if (!entityPostDelete[entityName]) entityPostDelete[entityName] = [];
306
+ entityPostDelete[entityName].push({ fn: fn as PostDeleteHookFn, phase, featureName: name }); // @cast-boundary engine-bridge
307
+ }
308
+ },
309
+
310
+ config<TKeys extends Readonly<Record<string, ConfigKeyDefinition<ConfigKeyType>>>>(definition: {
311
+ readonly keys: TKeys;
312
+ }): { readonly [K in keyof TKeys]: ConfigKeyHandle<TKeys[K]["type"]> } {
313
+ // Qualify eagerly (same as defineEvent) so the handle name matches what
314
+ // the registry stores — lazy qualification would break compile-time
315
+ // autocomplete and hand-built test registries.
316
+ const handles: Record<string, ConfigKeyHandle<ConfigKeyType>> = {};
317
+ for (const [key, keyDef] of Object.entries(definition.keys)) {
318
+ configKeys[key] = keyDef;
319
+ handles[key] = {
320
+ name: qn(toKebab(name), "config", toKebab(key)),
321
+ type: keyDef.type,
322
+ };
323
+ }
324
+ return handles as {
325
+ readonly [K in keyof TKeys]: ConfigKeyHandle<TKeys[K]["type"]>;
326
+ }; // @cast-boundary engine-bridge — Mapped-Type-Inference at config()-callsite
327
+ },
328
+
329
+ job(
330
+ jobName: string,
331
+ options: Omit<JobDefinition, "name" | "handler">,
332
+ handler: JobHandlerFn,
333
+ ): void {
334
+ // Resolve NameOrRef(s) in trigger.on. Multi-Trigger-Form: Array
335
+ // wird zu Array von resolved strings, Single bleibt single string —
336
+ // job-runner unterscheidet anhand Array.isArray.
337
+ const trigger =
338
+ "on" in options.trigger
339
+ ? {
340
+ on: Array.isArray(options.trigger.on)
341
+ ? options.trigger.on.map(resolveName)
342
+ : resolveName(options.trigger.on as NameOrRef),
343
+ }
344
+ : options.trigger;
345
+ jobs[jobName] = { ...options, trigger, name: jobName, handler };
346
+ },
347
+
348
+ notification(
349
+ notificationName: string,
350
+ definition: {
351
+ readonly trigger: { readonly on: NameOrRef };
352
+ readonly recipient: NotificationRecipientFn;
353
+ readonly data: NotificationDataFn;
354
+ readonly templates?: Readonly<Record<string, NotificationTemplateFn>>;
355
+ },
356
+ ): void {
357
+ notifications[notificationName] = {
358
+ name: notificationName,
359
+ trigger: { on: resolveName(definition.trigger.on) },
360
+ recipient: definition.recipient,
361
+ data: definition.data,
362
+ templates: definition.templates,
363
+ };
364
+ },
365
+
366
+ translations(def: TranslationsDef): void {
367
+ translations = { ...translations, ...def.keys };
368
+ },
369
+
370
+ defineEvent: <const TInner extends string, TPayload>(
371
+ eventName: TInner,
372
+ schema: ZodType<TPayload>,
373
+ options?: { readonly version?: number },
374
+ ): EventDef<TPayload, QualifiedEventName<TName, TInner>> => {
375
+ // Return the fully-qualified event name so callers can pass it
376
+ // straight to ctx.appendEvent without hand-building the
377
+ // "<feature>:event:<name>" shape. Registry keeps events keyed by
378
+ // short name — qualification is the framework's job, not the feature
379
+ // author's.
380
+ //
381
+ // The runtime kebab-step (`qn(toKebab(feature), …)`) is mirrored at
382
+ // the type-level by `QualifiedEventName<TName, TInner>` so the
383
+ // returned `name` carries the literal qualified shape that the
384
+ // augmented `KumikoEventTypeMap` keys against.
385
+ const qualified = qn(toKebab(name), "event", toKebab(eventName));
386
+ const version = options?.version ?? 1;
387
+ if (!Number.isInteger(version) || version < 1) {
388
+ throw new Error(
389
+ `[Feature ${name}] defineEvent("${eventName}"): version must be a positive integer, got ${String(version)}`,
390
+ );
391
+ }
392
+ // @cast-boundary engine-bridge — runtime-string mirrors the
393
+ // template-literal-type via QualifiedEventName + toKebab. Both
394
+ // sides are tested (CamelToKebab type-tests + scan-events kebab
395
+ // tests), so the cast is a contract, not a typing-loss.
396
+ const def: EventDef<TPayload, QualifiedEventName<TName, TInner>> = {
397
+ name: qualified as QualifiedEventName<TName, TInner>,
398
+ schema,
399
+ version,
400
+ };
401
+ events[eventName] = def;
402
+ return def;
403
+ },
404
+
405
+ eventMigration(
406
+ eventName: string,
407
+ fromVersion: number,
408
+ toVersion: number,
409
+ transform: EventUpcastFn,
410
+ ): void {
411
+ if (toVersion !== fromVersion + 1) {
412
+ throw new Error(
413
+ `[Feature ${name}] eventMigration("${eventName}", ${fromVersion}, ${toVersion}): ` +
414
+ `only single-step migrations are allowed — toVersion must be fromVersion + 1. ` +
415
+ `Chain larger jumps by registering each step separately.`,
416
+ );
417
+ }
418
+ if (!Number.isInteger(fromVersion) || fromVersion < 1) {
419
+ throw new Error(
420
+ `[Feature ${name}] eventMigration("${eventName}", ...): fromVersion must be >= 1, got ${String(fromVersion)}`,
421
+ );
422
+ }
423
+ const qualified = qn(toKebab(name), "event", toKebab(eventName));
424
+ const list = eventMigrations[eventName] ?? [];
425
+ if (list.some((m) => m.fromVersion === fromVersion)) {
426
+ throw new Error(
427
+ `[Feature ${name}] eventMigration("${eventName}", ${fromVersion}, ${toVersion}): ` +
428
+ `a migration from v${fromVersion} is already registered. Each step may only be declared once.`,
429
+ );
430
+ }
431
+ list.push({ eventName: qualified, fromVersion, toVersion, transform });
432
+ eventMigrations[eventName] = list;
433
+ },
434
+
435
+ readsConfig(...qualifiedKeys: string[]): void {
436
+ configReads.push(...qualifiedKeys);
437
+ },
438
+
439
+ referenceData(
440
+ entityRef: NameOrRef,
441
+ data: readonly Record<string, unknown>[],
442
+ options?: { upsertKey?: string },
443
+ ): void {
444
+ referenceData.push({
445
+ entityName: resolveName(entityRef),
446
+ data,
447
+ upsertKey: options?.upsertKey,
448
+ });
449
+ },
450
+
451
+ extendsRegistrar(extensionName: string, def: RegistrarExtensionDef): void {
452
+ registrarExtensions[extensionName] = def;
453
+ },
454
+
455
+ useExtension(
456
+ extensionName: string,
457
+ entityRef: NameOrRef,
458
+ options?: Record<string, unknown>,
459
+ ): void {
460
+ extensionUsages.push({ extensionName, entityName: resolveName(entityRef), options });
461
+ },
462
+
463
+ metric(shortName: string, options: MetricOptions): void {
464
+ if (metrics[shortName]) {
465
+ throw new Error(
466
+ `[Feature ${name}] Metric "${shortName}" already registered. ` +
467
+ `Metric names must be unique per feature.`,
468
+ );
469
+ }
470
+ metrics[shortName] = { shortName, ...options };
471
+ },
472
+
473
+ secret(shortName: string, options: SecretOptions): SecretKeyHandle {
474
+ if (secretKeys[shortName]) {
475
+ throw new Error(
476
+ `[Feature ${name}] Secret "${shortName}" already registered. ` +
477
+ `Secret key names must be unique per feature.`,
478
+ );
479
+ }
480
+ // Qualified name follows the framework's "<feature>:<type>:<name>"
481
+ // QN convention — same pattern config / jobs / events use. toKebab
482
+ // handles the common input shapes ("stripe.apiKey" → "stripe-api-key")
483
+ // so features can declare keys in their natural style without
484
+ // thinking about kebab-case on every call.
485
+ const qualifiedName = qn(toKebab(name), QnTypes.secret, toKebab(shortName));
486
+ secretKeys[shortName] = {
487
+ shortName,
488
+ qualifiedName,
489
+ ...options,
490
+ };
491
+ return { name: qualifiedName };
492
+ },
493
+
494
+ projection(definition: ProjectionDefinition): void {
495
+ // Reject names that would blow up at registry-boot when we qualify them.
496
+ // Catch it at the registration site so the stack trace points at the
497
+ // feature file, not at framework internals.
498
+ if (!isKebabSegment(definition.name)) {
499
+ throw new Error(
500
+ `[Feature ${name}] Projection name "${definition.name}" must be kebab-case ` +
501
+ `(lowercase letters, digits, dashes; start with a letter). ` +
502
+ `Got "${definition.name}" — try "${toKebab(definition.name).replace(/_/g, "-")}".`,
503
+ );
504
+ }
505
+ if (projections[definition.name]) {
506
+ throw new Error(
507
+ `[Feature ${name}] Projection "${definition.name}" already registered. ` +
508
+ `Projection names must be unique per feature.`,
509
+ );
510
+ }
511
+ projections[definition.name] = definition;
512
+ },
513
+
514
+ multiStreamProjection(definition: MultiStreamProjectionDefinition): void {
515
+ if (!isKebabSegment(definition.name)) {
516
+ throw new Error(
517
+ `[Feature ${name}] MultiStreamProjection name "${definition.name}" must be kebab-case ` +
518
+ `(lowercase letters, digits, dashes; start with a letter). ` +
519
+ `Got "${definition.name}" — try "${toKebab(definition.name).replace(/_/g, "-")}".`,
520
+ );
521
+ }
522
+ if (multiStreamProjections[definition.name] || projections[definition.name]) {
523
+ throw new Error(
524
+ `[Feature ${name}] Projection name "${definition.name}" already registered. ` +
525
+ `r.projection and r.multiStreamProjection share a namespace — pick a unique short name.`,
526
+ );
527
+ }
528
+ if (Object.keys(definition.apply).length === 0) {
529
+ throw new Error(
530
+ `[Feature ${name}] MultiStreamProjection "${definition.name}" has no apply handlers. ` +
531
+ `Declare at least one event type it reacts to, otherwise the dispatcher has nothing to route.`,
532
+ );
533
+ }
534
+ multiStreamProjections[definition.name] = definition;
535
+ },
536
+
537
+ authClaims(fn: AuthClaimsFn): void {
538
+ authClaimsHooks.push(fn);
539
+ },
540
+
541
+ screen(definition: ScreenDefinition): void {
542
+ // Reject kebab-drift at registration-time so the stack trace points at
543
+ // the feature file, not at registry-boot. Same guard pattern as
544
+ // r.projection / r.multiStreamProjection.
545
+ if (!isKebabSegment(definition.id)) {
546
+ throw new Error(
547
+ `[Feature ${name}] Screen id "${definition.id}" must be kebab-case ` +
548
+ `(lowercase letters, digits, dashes; start with a letter). ` +
549
+ `Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
550
+ );
551
+ }
552
+ if (screens[definition.id]) {
553
+ throw new Error(
554
+ `[Feature ${name}] Screen "${definition.id}" already registered. ` +
555
+ `Screen ids must be unique per feature.`,
556
+ );
557
+ }
558
+ screens[definition.id] = definition;
559
+ },
560
+
561
+ nav(definition: NavDefinition): void {
562
+ // Reject kebab-drift at registration-time so the stack trace points at
563
+ // the feature file, not at registry-boot. Same guard pattern as
564
+ // r.projection / r.multiStreamProjection / r.screen.
565
+ if (!isKebabSegment(definition.id)) {
566
+ throw new Error(
567
+ `[Feature ${name}] Nav id "${definition.id}" must be kebab-case ` +
568
+ `(lowercase letters, digits, dashes; start with a letter). ` +
569
+ `Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
570
+ );
571
+ }
572
+ if (navs[definition.id]) {
573
+ throw new Error(
574
+ `[Feature ${name}] Nav entry "${definition.id}" already registered. ` +
575
+ `Nav ids must be unique per feature.`,
576
+ );
577
+ }
578
+ navs[definition.id] = definition;
579
+ },
580
+
581
+ workspace(definition: WorkspaceDefinition): void {
582
+ // Same kebab guard as r.screen / r.nav so authoring-time mistakes
583
+ // surface at the feature file, not deep in registry boot.
584
+ if (!isKebabSegment(definition.id)) {
585
+ throw new Error(
586
+ `[Feature ${name}] Workspace id "${definition.id}" must be kebab-case ` +
587
+ `(lowercase letters, digits, dashes; start with a letter). ` +
588
+ `Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
589
+ );
590
+ }
591
+ if (workspaces[definition.id]) {
592
+ throw new Error(
593
+ `[Feature ${name}] Workspace "${definition.id}" already registered. ` +
594
+ `Workspace ids must be unique per feature.`,
595
+ );
596
+ }
597
+ workspaces[definition.id] = definition;
598
+ },
599
+
600
+ httpRoute(definition: HttpRouteDefinition): void {
601
+ // Path-Validation: muss mit "/" beginnen, keine /api/-Routes (die
602
+ // sind dem Dispatcher reserviert; eine HTTP-Route die /api/foo
603
+ // belegt, würde die Auth-Middleware umgehen ohne dass der Author
604
+ // das ausgesprochen hat — bewusster Block).
605
+ if (!definition.path.startsWith("/")) {
606
+ throw new Error(
607
+ `[Feature ${name}] httpRoute path "${definition.path}" must start with "/". ` +
608
+ `Got "${definition.path}".`,
609
+ );
610
+ }
611
+ if (definition.path === "/api" || definition.path.startsWith("/api/")) {
612
+ throw new Error(
613
+ `[Feature ${name}] httpRoute path "${definition.path}" is in the /api/* namespace ` +
614
+ `which is reserved for the dispatcher (write/query/batch/auth/sse). ` +
615
+ `Pick a different path or use r.queryHandler / r.writeHandler.`,
616
+ );
617
+ }
618
+ const key = `${definition.method} ${definition.path}`;
619
+ if (httpRoutes[key]) {
620
+ throw new Error(
621
+ `[Feature ${name}] HTTP-Route "${key}" already registered. ` +
622
+ `method + path must be unique per feature.`,
623
+ );
624
+ }
625
+ httpRoutes[key] = definition;
626
+ },
627
+
628
+ claimKey<T extends ClaimKeyType>(
629
+ shortName: string,
630
+ options: { readonly type: T },
631
+ ): ClaimKeyHandle<T> {
632
+ if (claimKeys[shortName]) {
633
+ throw new Error(
634
+ `[Feature ${name}] Claim key "${shortName}" already declared. ` +
635
+ "Claim short-names must be unique per feature.",
636
+ );
637
+ }
638
+ // Claim keys are NOT full QNs — the JWT shape is 2-segment
639
+ // "<featureName>:<shortName>" (same as Translation keys), not
640
+ // kebab-cased. The authClaims resolver prefixes with the raw
641
+ // feature.name + the raw inner key the hook returns, so the handle's
642
+ // `name` must match that literal string exactly for `readClaim` to
643
+ // find the value. kebab-conversion here would break the round-trip.
644
+ const qualifiedName = `${name}:${shortName}`;
645
+ claimKeys[shortName] = {
646
+ shortName,
647
+ qualifiedName,
648
+ type: options.type,
649
+ };
650
+ return { name: qualifiedName, type: options.type };
651
+ },
652
+ };
653
+
654
+ const exports = setup(registrar) as TExports;
655
+
656
+ return {
657
+ name,
658
+ systemScope: isSystemScoped,
659
+ exports,
660
+ requires,
661
+ optionalRequires,
662
+ ...(toggleableDefault !== undefined && { toggleableDefault }),
663
+ entities,
664
+ relations,
665
+ writeHandlers,
666
+ queryHandlers,
667
+ translations,
668
+ hooks: {
669
+ validation: validationHooks,
670
+ preSave: lifecycleHooks["preSave"] ?? {},
671
+ postSave: phasedLifecycleHooks.postSave,
672
+ preDelete: phasedLifecycleHooks.preDelete,
673
+ postDelete: phasedLifecycleHooks.postDelete,
674
+ preQuery: lifecycleHooks["preQuery"] ?? {},
675
+ } as HookMap,
676
+ entityHooks: {
677
+ postSave: entityPostSave,
678
+ preDelete: entityPreDelete,
679
+ postDelete: entityPostDelete,
680
+ },
681
+ configKeys,
682
+ jobs,
683
+ notifications,
684
+ registrarExtensions,
685
+ extensionUsages,
686
+ referenceData,
687
+ events,
688
+ eventMigrations,
689
+ configReads,
690
+ handlerEntityMappings,
691
+ metrics,
692
+ secretKeys,
693
+ projections,
694
+ multiStreamProjections,
695
+ authClaimsHooks,
696
+ claimKeys,
697
+ screens,
698
+ navs,
699
+ workspaces,
700
+ httpRoutes,
701
+ };
702
+ }