@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,742 @@
1
+ import type { Redis } from "ioredis";
2
+ import type { ZodType } from "zod";
3
+ import type { DbConnection } from "../../db/connection";
4
+ import type { TenantDb } from "../../db/tenant-db";
5
+ import type { FileContext } from "../../files/file-handle";
6
+ import type { Logger } from "../../logging/types";
7
+ import type { Meter, MetricsHandle, Tracer } from "../../observability/types";
8
+ import type { EntityCache } from "../../pipeline/entity-cache";
9
+ import type { SearchAdapter } from "../../search/types";
10
+ import type { TzContext } from "../../time";
11
+ import type { ConfigAccessor, ConfigAccessorFactory, ConfigResolver } from "./config";
12
+ import type { KumikoEventTypeMap } from "./event-type-map";
13
+
14
+ // --- Access ---
15
+
16
+ // AccessRule is DEFAULT-DENY: a handler without an access rule is not reachable.
17
+ // To grant access, set one of:
18
+ // - { roles: ["Admin", ...] } — role-based allowlist (empty array denies everyone)
19
+ // - { openToAll: true } — any authenticated user may call (still requires a valid JWT)
20
+ export type AccessRule = { readonly roles: readonly string[] } | { readonly openToAll: true };
21
+
22
+ // --- Pipeline User ---
23
+
24
+ export type SessionUser = {
25
+ // UUID-string so user.id threads through the event-store (aggregate-id) and
26
+ // the projection tables (uuid PK) without casts. Auth middleware reads the
27
+ // JWT `sub` claim as a string; legacy integer ids were a pre-ES artefact.
28
+ readonly id: string;
29
+ readonly tenantId: TenantId;
30
+ readonly roles: readonly string[];
31
+ // App-specific identity facts baked into the JWT at login time.
32
+ // Populated by `r.authClaims()` hooks (not yet implemented — see the
33
+ // auth-claims design note in docs/plans). Reserved here so the type shape
34
+ // is stable when the hook system lands.
35
+ readonly claims?: Readonly<Record<string, unknown>>;
36
+ // Session-ID — transported via the JWT `jti` standard claim. Present when
37
+ // an app has wired a `sessionCreator` callback on the auth-routes config
38
+ // (e.g. via the `sessions` feature). Absent for stateless-JWT deployments.
39
+ // When present, middleware can validate that the sid is still alive before
40
+ // accepting the request (session revocation).
41
+ readonly sid?: string;
42
+ };
43
+
44
+ // --- Claim Keys (r.claimKey declarations) ---
45
+
46
+ // Declared claim shape. Features call r.claimKey("teamId", { type: "string" })
47
+ // and get back a typed handle. Feature code then uses the handle both when
48
+ // reading via readClaim(user, handle) and (optionally) when returning from
49
+ // r.authClaims hooks. Two-fold payoff:
50
+ //
51
+ // 1. Read-site is typesafe: `const teamId = readClaim(user, DriverClaims.teamId)`
52
+ // narrows to `string | undefined` automatically — no hand-written cast,
53
+ // no magic "drivers:teamId" string.
54
+ // 2. Runtime check: the resolver warns when a hook returns an inner-key
55
+ // that the feature didn't declare — catches rename/typo drift. Opt-in
56
+ // per feature: only checked when r.claimKey was used at least once.
57
+ //
58
+ // Keep the type union small and explicit. JS-side inference via ClaimKeyJsType
59
+ // maps each literal to a primitive or array — broader shapes (nested
60
+ // records) can land in "object" but lose narrowness; that's the trade-off
61
+ // for keeping the type-system simple.
62
+ export type ClaimKeyType = "string" | "number" | "boolean" | "string[]" | "object";
63
+
64
+ export type ClaimKeyJsType<T extends ClaimKeyType> = T extends "string"
65
+ ? string
66
+ : T extends "number"
67
+ ? number
68
+ : T extends "boolean"
69
+ ? boolean
70
+ : T extends "string[]"
71
+ ? readonly string[]
72
+ : T extends "object"
73
+ ? Readonly<Record<string, unknown>>
74
+ : never;
75
+
76
+ // Stored on the FeatureDefinition. `qualifiedName` is auto-set at
77
+ // registration time ("<feature>:<inner-kebab>") — same naming convention
78
+ // as auth-claim keys.
79
+ export type ClaimKeyDefinition = {
80
+ readonly shortName: string;
81
+ readonly qualifiedName: string;
82
+ readonly type: ClaimKeyType;
83
+ };
84
+
85
+ // Typed handle returned by r.claimKey(). `name` is the qualified key the
86
+ // JWT stores; `type` threads through to readClaim's generic so consumers
87
+ // get the right narrowed type without casting.
88
+ export type ClaimKeyHandle<T extends ClaimKeyType = ClaimKeyType> = {
89
+ readonly name: string;
90
+ readonly type: T;
91
+ };
92
+
93
+ // --- Auth Claims (r.authClaims hook) ---
94
+
95
+ // Features contribute "identity facts" into the JWT at login time. Claim keys
96
+ // are auto-prefixed with the feature name at merge time (`"<feature>:<key>"`)
97
+ // so two features can't collide — Reading code in a handler picks the claim
98
+ // by its prefixed key: `user.claims["drivers:teamId"]`.
99
+ //
100
+ // The context is deliberately trimmed compared to HandlerContext: login is a
101
+ // READ, not a write-path. Exposing appendEvent/loadAggregate/tz here would
102
+ // let claims hooks reach into write-time concerns — not their job, bigger
103
+ // mocking surface in tests. `db` is guaranteed tenant-scoped to the chosen
104
+ // tenant (the one the user is logging INTO, not the one making the request).
105
+ // `queryAs` lets a hook call another feature's query handler without direct
106
+ // imports — same cross-feature contract hooks otherwise follow.
107
+ export type AuthClaimsContext = {
108
+ readonly db: import("../../db/tenant-db").TenantDb;
109
+ readonly queryAs: (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
110
+ readonly config?: ConfigAccessor;
111
+ };
112
+
113
+ export type AuthClaimsFn = (
114
+ user: SessionUser,
115
+ ctx: AuthClaimsContext,
116
+ ) => Promise<Record<string, unknown>>;
117
+
118
+ // What the registry stores per registered hook. `featureName` drives the
119
+ // auto-prefix at merge time, so the registry is the source of truth for the
120
+ // naming — features never ship pre-prefixed keys.
121
+ //
122
+ // `declaredKeys` is the set of inner-keys this hook's feature declared via
123
+ // r.claimKey() — the resolver uses it to warn when a hook returns a key
124
+ // that was never declared (typo / rename drift). `undefined` when the
125
+ // feature never called r.claimKey(), in which case the resolver skips the
126
+ // check entirely (backwards-compat for features that only use r.authClaims).
127
+ export type AuthClaimsHookDef = {
128
+ readonly featureName: string;
129
+ readonly fn: AuthClaimsFn;
130
+ readonly declaredKeys?: ReadonlySet<string>;
131
+ };
132
+
133
+ // --- Handler Events ---
134
+
135
+ export type WriteEvent<TPayload = unknown> = {
136
+ readonly type: string;
137
+ readonly payload: TPayload;
138
+ readonly user: SessionUser;
139
+ };
140
+
141
+ export type QueryEvent<TPayload = unknown> = {
142
+ readonly type: string;
143
+ readonly payload: TPayload;
144
+ readonly user: SessionUser;
145
+ };
146
+
147
+ // --- Handler Results ---
148
+
149
+ import type { WriteFailure } from "../../errors/write-error-info";
150
+
151
+ export type WriteResult<TData = unknown> =
152
+ | { readonly isSuccess: true; readonly data: TData }
153
+ | WriteFailure;
154
+
155
+ /**
156
+ * Override the success-side `data` of a WriteResult while forwarding the
157
+ * failure half untouched. Useful for handlers that delegate to the
158
+ * event-store executor (which returns a SaveContext / DeleteContext
159
+ * envelope) but want to keep their own response shape — caller contract
160
+ * stays flat instead of leaking the executor's internals.
161
+ *
162
+ * ```ts
163
+ * const result = await executor.delete({ id }, user, db);
164
+ * return withResponseData(result, { userId, tenantId });
165
+ * ```
166
+ *
167
+ * On failure the same WriteFailure instance is returned — the error
168
+ * object round-trips without any wrapping, so the dispatcher / HTTP layer
169
+ * still read the original error code + httpStatus + i18nKey.
170
+ */
171
+ export function withResponseData<T>(result: WriteResult<unknown>, data: T): WriteResult<T> {
172
+ if (!result.isSuccess) return result;
173
+ return { isSuccess: true, data };
174
+ }
175
+
176
+ // --- Context Types ---
177
+
178
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
179
+ // Forward import: Registry is in feature.ts (circular type import — fine in TS)
180
+ import type { Registry } from "./feature";
181
+
182
+ // Minimal interface for job event triggers (framework-owned, concrete type in jobs/)
183
+ export type JobRunnerRef = {
184
+ handleEvent(
185
+ eventName: string,
186
+ payload: Record<string, unknown>,
187
+ user?: SessionUser,
188
+ ): Promise<void>;
189
+ };
190
+
191
+ // Priority levels for notifications
192
+ export type NotifyPriority = "critical" | "normal" | "low";
193
+
194
+ // Options passed to a NotifyFn / DeliveryService.notify. Defined here so the
195
+ // framework side and the concrete delivery implementation can't drift apart.
196
+ export type NotifyOptions = {
197
+ readonly to?: string | readonly string[] | { readonly tenant: TenantId };
198
+ readonly route?: Readonly<Record<string, string>>;
199
+ readonly data?: Readonly<Record<string, unknown>>;
200
+ readonly priority?: NotifyPriority;
201
+ // Opt-in dedup. Same key within 24h = single delivery. Use when a handler
202
+ // can be replayed (webhook retry, user double-click) and you don't want
203
+ // the notification to fire twice.
204
+ readonly idempotencyKey?: string;
205
+ };
206
+
207
+ // Minimal interface for delivery notifications (concrete type in bundled-features/delivery)
208
+ export type NotifyFn = (notificationType: string, options: NotifyOptions) => Promise<void>;
209
+
210
+ // Factory that produces a bound NotifyFn for a specific user+tenant
211
+ // Concrete implementation in bundled-features/delivery (cross-package boundary)
212
+ export type NotifyFactory = (user: SessionUser, tenantId: TenantId) => NotifyFn;
213
+
214
+ // Shared optional fields across all execution contexts
215
+ type SharedContextFields = {
216
+ readonly redis?: Redis;
217
+ readonly jobRunner?: JobRunnerRef;
218
+ readonly configResolver?: ConfigResolver;
219
+ readonly config?: ConfigAccessor;
220
+ readonly _configAccessorFactory?: ConfigAccessorFactory;
221
+ // Encryption round-trip partner for the config feature. Separate from
222
+ // configResolver so the read-only resolver contract stays clean — the
223
+ // set handler needs to encrypt on write, the resolver needs to decrypt
224
+ // on read, and both reach for the same provider. Wired via extraContext.
225
+ readonly configEncryption?: import("../../db").EncryptionProvider;
226
+ // Rate-limit resolver. Wired by the framework when the `rateLimiting`
227
+ // feature is loaded — pipeline reads handler.rateLimit and calls
228
+ // .enforce() on this resolver before access-check. Absent when the
229
+ // app didn't load the feature: handlers with rateLimit set are
230
+ // rejected at boot to surface the misconfig early.
231
+ readonly rateLimit?: import("../../rate-limit").RateLimitResolver;
232
+ readonly searchAdapter?: SearchAdapter;
233
+ // Binary storage, wrapped around the registered FileStorageProvider.
234
+ // Optional at the AppContext level — present when the app booted with
235
+ // `files.storageProvider`. Hooks/handlers use ctx.files.ref(key) instead
236
+ // of receiving binaries in payloads.
237
+ readonly files?: FileContext;
238
+ readonly entityCache?: EntityCache;
239
+ readonly notify?: NotifyFn;
240
+ readonly _notifyFactory?: NotifyFactory;
241
+ // Tenant-scoped secrets accessor. Present when the app wired a
242
+ // MasterKeyProvider at boot. Feature code reads ctx.secrets.get(...)
243
+ // to pull a plaintext secret; Secret<string> carries the brand that
244
+ // the response guard rejects on serialization.
245
+ readonly secrets?: import("../../secrets").SecretsContext;
246
+ // Raw KEK provider. Present alongside ctx.secrets — needed by the rotation
247
+ // job which deliberately operates outside the per-call audit trail (it
248
+ // processes rows system-wide, not a per-user read).
249
+ readonly masterKeyProvider?: import("../../secrets").MasterKeyProvider;
250
+ // Observability: optional at the outer boundary, always populated by the
251
+ // time a handler receives its ctx (Noop fallback when no provider is
252
+ // configured, so handler code can call ctx.tracer/ctx.metrics without
253
+ // defensive checks).
254
+ readonly tracer?: Tracer;
255
+ readonly meter?: Meter;
256
+ // Cancellation. Aborts when the HTTP client disconnects (mobile back,
257
+ // tab close). Undefined for non-HTTP entry-points (jobs, MSP-applies).
258
+ // Long-running handlers (export jobs, multi-step workflows) should
259
+ // throw `signal.throwIfAborted()` at chunk boundaries; short handlers
260
+ // can ignore it. Framework primitives (streamAllEventsByType,
261
+ // rebuildProjection) honour it automatically.
262
+ readonly signal?: AbortSignal;
263
+ // Effective feature-toggle resolver. Wired by the dispatcher when the
264
+ // feature-toggles feature is loaded — the lifecycle pipeline, MSP runner,
265
+ // and ctx.hasFeature all read from this single source. Returns the Set
266
+ // of feature names that are currently effectively enabled (after global
267
+ // overrides and r.requires() cascade). Absent = all features on.
268
+ readonly effectiveFeatures?: () => ReadonlySet<string>;
269
+ };
270
+
271
+ // All optional — used at pipeline/system boundaries.
272
+ // `db` is a DbConnection at the outer boundary (server/stack) and a TenantDb
273
+ // once a HandlerContext has been built — hooks receive the HandlerContext as
274
+ // AppContext, so the union keeps that assignment straightforward.
275
+ export type AppContext = SharedContextFields & {
276
+ readonly db?: DbConnection | TenantDb;
277
+ readonly registry?: Registry;
278
+ readonly systemUser?: SessionUser;
279
+ readonly log?: Logger;
280
+ readonly triggeredBy?: { readonly id: string; readonly tenantId: TenantId } | null;
281
+ /** Bei Job-Handler-Aufrufen die aus einem Event-Trigger heraus laufen
282
+ * (r.job mit `trigger: { on: ... }`): der Name des Handlers der das
283
+ * Event ausgelöst hat. Bei Multi-Trigger-Jobs (`on: [...]`) ist das
284
+ * die einzige Möglichkeit für den Handler zu wissen WELCHER Trigger
285
+ * gefeuert hat. Cron- und manual-Jobs lassen das Feld undefined. */
286
+ readonly triggerName?: string;
287
+ readonly _userId?: string | undefined;
288
+ readonly _handlerType?: string | undefined;
289
+ };
290
+
291
+ // Handler execution: db (tenant-scoped) + registry guaranteed.
292
+ //
293
+ // Cross-feature bridge:
294
+ // ctx.query / ctx.write run the target handler AS THE CURRENT USER,
295
+ // sharing the active tx + afterCommit queue. Field-access filters apply.
296
+ // ctx.queryAs / ctx.writeAs switch identity (e.g. SYSTEM for privileged
297
+ // lookups like "find user by email for auth" — system reads aren't filtered
298
+ // by field-access read rules).
299
+ //
300
+ // The design: handlers are the contract between features. Feature A requires
301
+ // Feature B and talks to it through B's registered handlers — never through
302
+ // direct imports of B's tables or internal types.
303
+ //
304
+ // TMap propagates the strict event-type-map through `appendEvent`. Defaults
305
+ // to the global KumikoEventTypeMap (augmented per app via
306
+ // `declare module "@cosmicdrift/kumiko-framework/engine"`). Code that bypasses the
307
+ // type-map (runtime-pluggable events) uses `appendEventUnsafe`.
308
+ export type HandlerContext<TMap extends object = KumikoEventTypeMap> = SharedContextFields & {
309
+ readonly db: TenantDb;
310
+ readonly registry: Registry;
311
+ /** Aktiver SessionUser des Handler-Aufrufs — Convenience-Alias zu
312
+ * `event.user`. Existiert weil Handler intuitiv `ctx.user.tenantId`
313
+ * schreiben (Context = "kennt seinen User") und der Pfad sonst nur
314
+ * über `event.user` läuft, was typo-anfällig ist und stillschweigend
315
+ * zu `internal_error` führt wenn der falsche Pfad gewählt wird.
316
+ * Identisch zum event.user-Wert; Identity-Switches nutzen
317
+ * weiterhin queryAs/writeAs. */
318
+ readonly user: SessionUser;
319
+ readonly systemUser?: SessionUser;
320
+ readonly log?: Logger;
321
+ readonly triggeredBy?: { readonly id: string; readonly tenantId: TenantId } | null;
322
+ readonly _userId?: string | undefined;
323
+ readonly _handlerType?: string | undefined;
324
+
325
+ readonly query: (qn: string, payload: unknown) => Promise<unknown>;
326
+ readonly queryAs: (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
327
+ readonly write: (qn: string, payload: unknown) => Promise<WriteResult>;
328
+ readonly writeAs: (user: SessionUser, qn: string, payload: unknown) => Promise<WriteResult>;
329
+
330
+ // Runtime-check whether a feature is currently effectively-enabled. Use
331
+ // inside an active handler when logic should opt into behaviour that
332
+ // depends on another toggleable feature being on (e.g. "if premiumInvoices
333
+ // is on, add extra columns to the export"). The dispatcher gate already
334
+ // blocks calls to handlers of disabled features — this is the fine-grained
335
+ // opt-in counterpart, not a substitute for the gate.
336
+ readonly hasFeature: (featureName: string) => boolean;
337
+
338
+ // Append a domain event to a specific aggregate stream in the current tx.
339
+ // Marten-aligned: every event belongs to exactly one aggregate. The runtime
340
+ // reads the current stream version, bumps it, and fires projections that
341
+ // match the event type in the same transaction. Use it when a write-handler
342
+ // wants to record a domain event alongside the auto-generated CRUD events
343
+ // (e.g. "invoice.approved" on the same invoice stream that already carries
344
+ // "invoice.created" + "invoice.updated").
345
+ readonly appendEvent: AppendEventFn<TMap>;
346
+
347
+ // Escape-hatch for runtime-pluggable features without a compile-time
348
+ // augmentation. See AppendEventUnsafeFn — same runtime as appendEvent,
349
+ // but the type-surface is `payload: unknown`. Use only when the event-
350
+ // type is not knowable at compile-time; otherwise the strict path
351
+ // (appendEvent) is the contract Designer/AI rely on.
352
+ readonly appendEventUnsafe: AppendEventUnsafeFn;
353
+
354
+ // Marten FetchForWriting equivalent: load the current stream, optionally
355
+ // enforce expectedVersion, and get a handle that appends further events
356
+ // onto that stream without re-specifying aggregateId/aggregateType.
357
+ // Fails fast with VersionConflictError when expectedVersion doesn't
358
+ // match — the write-handler never touches state it didn't expect.
359
+ readonly fetchForWriting: (args: FetchForWritingArgs) => Promise<AggregateStreamHandle>;
360
+
361
+ // Load the full stream of events for an aggregate, tenant-scoped to the
362
+ // current user. Events pass through the registered upcaster chain, so the
363
+ // payloads returned match the current schema shape regardless of when
364
+ // they were written. Use inside a queryHandler to expose Marten-style
365
+ // AggregateStreamAsync: hand the events to a reducer and return the
366
+ // derived state (live aggregation).
367
+ //
368
+ // `options.asOf` restricts to events whose createdAt is ≤ the given
369
+ // timestamp — the point-in-time / "what did this aggregate look like
370
+ // yesterday" query.
371
+ readonly loadAggregate: (
372
+ aggregateId: string,
373
+ options?: { readonly asOf?: Temporal.Instant },
374
+ ) => Promise<readonly import("../../event-store").StoredEvent[]>;
375
+
376
+ // Marten-aligned stream lifecycle. Archived streams become read-only:
377
+ // ctx.appendEvent throws ArchivedStreamError, ctx.loadAggregate returns []
378
+ // (pass { includeArchived: true } on the low-level loaders to override).
379
+ // restoreStream reopens a stream; aggregate-level lifecycle states like
380
+ // "closed" stay in the domain events, not the archive flag.
381
+ readonly archiveStream: (
382
+ aggregateId: string,
383
+ args: { readonly aggregateType: string; readonly reason?: string },
384
+ ) => Promise<void>;
385
+ readonly restoreStream: (aggregateId: string) => Promise<void>;
386
+ readonly isStreamArchived: (aggregateId: string) => Promise<boolean>;
387
+
388
+ // Cache the current state of an aggregate as a snapshot. Callers that
389
+ // hold the state (e.g. just reduced the stream in a queryHandler, or
390
+ // finished a write batch) pass it in alongside the version it reflects.
391
+ // The framework handles storage + upsert semantics; the snapshot policy
392
+ // (every N events, every M minutes, on-demand) stays with the feature.
393
+ // Snapshots are a perf optimisation — the event log remains the source
394
+ // of truth.
395
+ readonly snapshotAggregate: (args: {
396
+ readonly aggregateId: string;
397
+ readonly aggregateType: string;
398
+ readonly version: number;
399
+ readonly state: Record<string, unknown>;
400
+ }) => Promise<void>;
401
+
402
+ // Snapshot-aware rehydrate. Loads the latest snapshot (if any), runs the
403
+ // registered upcaster chain on every delta event, and folds them onto
404
+ // the snapshot state with the caller's reducer. Returns the final state,
405
+ // the latest event version, and whether a snapshot was used — the last
406
+ // lets a feature's snapshot policy make informed decisions
407
+ // (e.g. "snapshot every 100 events past the last snapshot").
408
+ //
409
+ // Archived streams behave like ctx.loadAggregate — empty result with
410
+ // version=0, not an exception.
411
+ readonly loadAggregateWithSnapshot: <TState extends Record<string, unknown>>(
412
+ aggregateId: string,
413
+ reducer: import("../../event-store").SnapshotReducer<TState>,
414
+ initial: TState,
415
+ ) => Promise<import("../../event-store").LoadAggregateWithSnapshotResult<TState>>;
416
+
417
+ // Read rows from a registered projection table, tenant-scoped to the
418
+ // current user. Marten's equivalent of session.Query<T>() — the projection
419
+ // table is the read model; this surface makes it reachable by qualified
420
+ // name without the feature having to import the drizzle-table directly.
421
+ //
422
+ // Auto-applies tenant_id filter when the projection table has a tenant_id
423
+ // column (or opt out with { allTenants: true } for system-scoped reads
424
+ // like cross-tenant analytics). Unknown projection name throws.
425
+ readonly queryProjection: <T = Record<string, unknown>>(
426
+ qualifiedName: string,
427
+ options?: { readonly allTenants?: boolean },
428
+ ) => Promise<readonly T[]>;
429
+
430
+ // Always populated — Noop when no observability provider is configured.
431
+ // Feature code can call ctx.metrics.inc(...) / ctx.tracer.startSpan(...)
432
+ // without null-checks.
433
+ readonly metrics: MetricsHandle;
434
+ readonly tracer: Tracer;
435
+
436
+ // Time + TZ helper. Feature-Code MUSS hier durch statt `new Date()` —
437
+ // ctx.tz.now() liefert Temporal.Instant, ctx.tz.parse(wallClock, tz)
438
+ // produziert ZonedDateTime, ctx.tz.toLocatedJson serialisiert für die
439
+ // API-Boundary. Lint-Regel gegen `new Date()` kommt sobald alle internen
440
+ // usages migriert sind. Tenant + User-TZ defaults aktuell "UTC", werden
441
+ // aus tenant.timezone / user.timezone gelesen sobald die Felder existieren.
442
+ readonly tz: TzContext;
443
+
444
+ // Resolve every registered r.authClaims() hook against `user` and return
445
+ // the merged claim record (keys auto-prefixed with the feature name). Used
446
+ // by login + switch-tenant write-handlers to populate SessionUser.claims
447
+ // before the JWT is signed. Thin pass-through to dispatcher.resolveAuthClaims
448
+ // so there's a single resolve impl — both entry-points can't drift.
449
+ readonly resolveAuthClaims: (user: SessionUser) => Promise<Record<string, unknown>>;
450
+ };
451
+
452
+ // Job execution: db + registry + systemUser + logging guaranteed
453
+ export type JobContext = SharedContextFields & {
454
+ readonly db: DbConnection;
455
+ readonly registry: Registry;
456
+ readonly systemUser: SessionUser;
457
+ readonly log: Logger;
458
+ readonly triggeredBy: { readonly id: string; readonly tenantId: TenantId } | null;
459
+ };
460
+
461
+ // --- Handler Functions ---
462
+
463
+ export type WriteHandlerFn<TPayload = unknown, TData = unknown> = (
464
+ event: WriteEvent<TPayload>,
465
+ context: HandlerContext,
466
+ ) => Promise<WriteResult<TData>>;
467
+
468
+ export type QueryHandlerFn<TPayload = unknown, TResult = unknown> = (
469
+ query: QueryEvent<TPayload>,
470
+ context: HandlerContext,
471
+ ) => Promise<TResult>;
472
+
473
+ // --- Event Definitions ---
474
+
475
+ /**
476
+ * Compile-time mirror of `engine/qualified-name.ts:toKebab` for camelCase
477
+ * → kebab-case. Drives the literal-type of `EventDef.name`, so that
478
+ * `r.defineEvent("foo", schema)` inside `defineFeature("driverOrders")`
479
+ * carries `name: "driver-orders:event:foo"` as a literal — strict-mode
480
+ * for `ctx.appendEvent({ type: eventDef.name, ... })` lights up.
481
+ *
482
+ * Algorithm mirrors the runtime regex pipeline:
483
+ * 1. `.` → `-` (dot acts as word-boundary)
484
+ * 2. Insert `-` between `[A-Z]+` and `[A-Z][a-z]` (so `SSEFoo` →
485
+ * `SSE-Foo`, splitting an uppercase run before a camel-hump)
486
+ * 3. Insert `-` between `[a-z0-9]` and `[A-Z]` (camelCase boundary,
487
+ * so `ticketAssigned` → `ticket-Assigned`)
488
+ * 4. lowercase everything
489
+ *
490
+ * Implemented as a state machine with one-char lookahead. State:
491
+ * - "start" — at start of string, or right after a dot-boundary
492
+ * - "upper" — last emitted char came from an uppercase letter
493
+ * - "post-letter" — last emitted char was a lowercase letter or digit
494
+ *
495
+ * Sync vs runtime is verified by `engine/__tests__/camel-to-kebab.test-d.ts`
496
+ * — the type-tests cross-check identical inputs against `toKebab()`.
497
+ */
498
+ export type CamelToKebab<S extends string> = CamelToKebabImpl<S, "start", "">;
499
+
500
+ type CamelToKebabImpl<
501
+ S extends string,
502
+ Prev extends "start" | "upper" | "post-letter",
503
+ Acc extends string,
504
+ > = S extends `${infer C}${infer Rest}`
505
+ ? CharKind<C> extends "upper"
506
+ ? Prev extends "start"
507
+ ? CamelToKebabImpl<Rest, "upper", `${Acc}${Lowercase<C>}`>
508
+ : Prev extends "post-letter"
509
+ ? CamelToKebabImpl<Rest, "upper", `${Acc}-${Lowercase<C>}`>
510
+ : // Prev = "upper" — peek next char to decide between
511
+ // continuing-the-run and splitting-before-camel-hump.
512
+ Rest extends `${infer Next}${string}`
513
+ ? CharKind<Next> extends "lower"
514
+ ? CamelToKebabImpl<Rest, "upper", `${Acc}-${Lowercase<C>}`>
515
+ : CamelToKebabImpl<Rest, "upper", `${Acc}${Lowercase<C>}`>
516
+ : `${Acc}${Lowercase<C>}`
517
+ : CharKind<C> extends "lower"
518
+ ? CamelToKebabImpl<Rest, "post-letter", `${Acc}${C}`>
519
+ : // Non-letter: dots become word-boundaries (state resets to "start"
520
+ // so the next uppercase letter doesn't pick up a redundant dash).
521
+ // Other non-letters (digits etc.) act like lowercase for transitions.
522
+ C extends "."
523
+ ? CamelToKebabImpl<Rest, "start", `${Acc}-`>
524
+ : CamelToKebabImpl<Rest, "post-letter", `${Acc}${C}`>
525
+ : Acc;
526
+
527
+ /**
528
+ * Three-way classification used by `CamelToKebab`:
529
+ * - "lower" — a lowercase letter (a-z and Unicode lowercase)
530
+ * - "upper" — an uppercase letter (A-Z and Unicode uppercase)
531
+ * - "non-letter" — digit, dot, dash, etc. (Lowercase==Uppercase for them)
532
+ */
533
+ type CharKind<C extends string> =
534
+ C extends Lowercase<C> ? (C extends Uppercase<C> ? "non-letter" : "lower") : "upper";
535
+
536
+ /**
537
+ * Builds the qualified event-name from feature + inner-name in the same
538
+ * shape the runtime emits via `qn(toKebab(feature), "event", toKebab(inner))`.
539
+ */
540
+ export type QualifiedEventName<
541
+ TFeature extends string,
542
+ TInner extends string,
543
+ > = `${CamelToKebab<TFeature>}:event:${CamelToKebab<TInner>}`;
544
+
545
+ export type EventDef<TPayload = unknown, TName extends string = string> = {
546
+ readonly name: TName;
547
+ readonly schema: ZodType<TPayload>;
548
+ // Schema generation number. Starts at 1; bumped whenever a breaking change
549
+ // to the payload shape lands together with a matching r.eventMigration that
550
+ // upcasts older stored events. Reads consult this to decide if upcasters
551
+ // need to run before the payload hits consumer code.
552
+ readonly version: number;
553
+ };
554
+
555
+ // Args for ctx.appendEvent — explicit aggregate target, Marten-style.
556
+ // `type` must match a name returned by r.defineEvent in any registered
557
+ // feature; payload is validated against that event's Zod schema before
558
+ // being written to the events-table.
559
+ //
560
+ // `headers` lands in StoredEvent.metadata.headers — Marten-conform free
561
+ // key/value space for app-specific metadata (A/B-bucket, geo-region,
562
+ // client SDK version). Framework does not interpret values; keep them
563
+ // JSON-primitive (string|number|boolean) for safe serialization.
564
+ export type AppendEventArgs = {
565
+ readonly aggregateId: string;
566
+ readonly aggregateType: string;
567
+ readonly type: string;
568
+ readonly payload: unknown;
569
+ readonly headers?: Readonly<Record<string, string | number | boolean>>;
570
+ };
571
+
572
+ // Typed-payload variant — used by the strict ctx.appendEvent. Keyed via
573
+ // the discriminator type-arg so payload inference flows from `type`-literal
574
+ // to the matching schema-payload.
575
+ //
576
+ // TMap is propagated as a generic parameter (not hard-coded to
577
+ // KumikoEventTypeMap) so the constraint `K extends keyof TMap` resolves at
578
+ // USE-site instead of definition-site. Cross-package augmentation only
579
+ // becomes visible at use-site — the App's tsc compiles the augmentation
580
+ // alongside its own code, so `keyof TMap` widens to include all augmented
581
+ // event names. Hard-coding `keyof KumikoEventTypeMap` here would resolve
582
+ // at definition-site (framework's compile) where the augmentation is
583
+ // invisible → K = never, no strict-checking. The default = KumikoEventTypeMap
584
+ // keeps existing call-sites zero-config.
585
+ export type TypedAppendEventArgs<TMap extends object, K extends keyof TMap> = {
586
+ readonly aggregateId: string;
587
+ readonly aggregateType: string;
588
+ readonly type: K;
589
+ readonly payload: TMap[K];
590
+ readonly headers?: Readonly<Record<string, string | number | boolean>>;
591
+ };
592
+
593
+ // Strict-only form. Single overload — `<K extends keyof TMap>` against the
594
+ // app's pre-bound TMap. No fallback overload: apps that need runtime-pluggable
595
+ // events (where the type-string isn't known at compile-time) reach for
596
+ // `appendEventUnsafe`.
597
+ //
598
+ // Why no fallback overload:
599
+ // A two-overload form (`(args: AppendEventArgs)` as the second sig)
600
+ // silently accepts any args via the loose overload as soon as TS can't
601
+ // prove the strict one matches. Cross-package, the strict overload's
602
+ // `K = keyof TMap` collapses to `never` when called WITHOUT a local
603
+ // wrapper (default-substitution is eager at definition-site → augmentation
604
+ // invisible). Either every caller binds TMap via wrapper → strict fires;
605
+ // or they don't, and the fallback would silently swallow every typo.
606
+ // We pick the first option and force the wrong path to fail visibly.
607
+ //
608
+ // How this is wired in practice:
609
+ // - Apps run `yarn kumiko codegen`, which writes `.kumiko/define.ts`
610
+ // with locally-bound `defineWriteHandler<TName, TSchema, TData,
611
+ // KumikoEventTypeMap>(...)` wrappers. Handlers inside those wrappers
612
+ // get a strict ctx.appendEvent.
613
+ // - Cross-package callers (e.g. bundled-features's set.write.ts) that
614
+ // can't afford a local wrapper reach for `ctx.appendEventUnsafe`
615
+ // instead — same runtime, looser type-surface.
616
+ export type AppendEventFn<TMap extends object = KumikoEventTypeMap> = <K extends keyof TMap>(
617
+ args: TypedAppendEventArgs<TMap, K>,
618
+ ) => Promise<void>;
619
+
620
+ export type AppendEventUnsafeFn = (args: AppendEventArgs) => Promise<void>;
621
+
622
+ // Args for ctx.fetchForWriting — Marten FetchForWriting equivalent. Returns
623
+ // the current stream state + a handle that appends without re-specifying
624
+ // aggregateId/aggregateType. When expectedVersion is provided, the handle
625
+ // rejects the write immediately if the stream is ahead — optimistic
626
+ // concurrency enforced BEFORE any downstream work. Without expectedVersion,
627
+ // the handle trusts whatever version the stream currently has.
628
+ export type FetchForWritingArgs = {
629
+ readonly aggregateId: string;
630
+ readonly aggregateType: string;
631
+ readonly expectedVersion?: number;
632
+ };
633
+
634
+ export type AggregateStreamHandle = {
635
+ // Snapshot at fetch time — upcasted via the registered upcaster chain,
636
+ // so payloads match the current schema regardless of when they landed.
637
+ readonly events: readonly import("../../event-store").StoredEvent[];
638
+ readonly version: number;
639
+ // Append an event on this stream. Derives aggregateId/aggregateType/
640
+ // expectedVersion from the handle automatically. Multiple calls in a
641
+ // row bump the handle's internal version and the events-table in order.
642
+ readonly appendOne: (args: { readonly type: string; readonly payload: unknown }) => Promise<void>;
643
+ };
644
+
645
+ // --- Event Upcasters (schema migration) ---
646
+ //
647
+ // Marten's Upcaster pattern adapted for TypeScript. An event's payload shape
648
+ // may evolve over releases; stored events stay immutable on disk. Features
649
+ // register step-wise transforms that upgrade v(N) payloads to v(N+1) at read
650
+ // time. The framework chains them automatically — a v1 event gets walked
651
+ // through every registered migration up to the current version before the
652
+ // payload reaches a projection apply() or ctx.appendEvent consumer.
653
+ //
654
+ // Sync transforms: just return the upgraded payload. Most schema-evolution
655
+ // (renames, additions, format-fixes) needs no IO and stays sync — fast on
656
+ // the hot path of projection-rebuild.
657
+ //
658
+ // Async transforms (Marten's "AsyncOnlyEventUpcaster"): when the upgrade
659
+ // needs DB enrichment (e.g. v1 stored only a customerId, v2 also needs the
660
+ // customer's segment which lives in a reference table), accept the optional
661
+ // ctx-arg, run the lookup via ctx.db, return a Promise. The framework
662
+ // awaits unconditionally — sync transforms return a plain value and pay
663
+ // only the await-microtask overhead. Pattern-match Marten:
664
+ // r.eventMigration("invoiceCreated", 1, 2, async (payload, ctx) => {
665
+ // const customer = await ctx.db.select().from(customersTable)...;
666
+ // return { ...payload, customerSegment: customer.segment };
667
+ // });
668
+ export type EventUpcastCtx = {
669
+ readonly db: import("../../db").DbRunner;
670
+ readonly tenantId: import("./identifiers").TenantId;
671
+ };
672
+
673
+ export type EventUpcastFn = (payload: unknown, ctx: EventUpcastCtx) => unknown | Promise<unknown>;
674
+
675
+ export type EventMigrationDef = {
676
+ // Qualified event name, matching r.defineEvent(...).name.
677
+ readonly eventName: string;
678
+ readonly fromVersion: number;
679
+ readonly toVersion: number; // must be fromVersion + 1
680
+ readonly transform: EventUpcastFn;
681
+ };
682
+
683
+ // --- References ---
684
+
685
+ // Anything that carries a name — accepted by hooks, relations, jobs, etc.
686
+ export type NameOrRef = string | { readonly name: string };
687
+
688
+ export function resolveName(ref: NameOrRef): string {
689
+ return typeof ref === "string" ? ref : ref.name;
690
+ }
691
+
692
+ export type EntityRef = {
693
+ readonly name: string;
694
+ readonly table: string;
695
+ };
696
+
697
+ export type HandlerRef = {
698
+ readonly name: string;
699
+ };
700
+
701
+ // --- Handler Definitions (stored in feature/registry) ---
702
+
703
+ // Per-handler rate limit. Bucket key derived from `per`:
704
+ // "user" → userId
705
+ // "tenant" → tenantId
706
+ // "ip" → request IP
707
+ // "user+handler" → userId + handlerName
708
+ // "tenant+handler" → tenantId + handlerName
709
+ // "ip+handler" → IP + handlerName (anonymous endpoints)
710
+ // `cost` is the tokens this handler-call deducts. Default 1 — bump for
711
+ // expensive operations (bulk export, bulk import).
712
+ export type RateLimitPer =
713
+ | "user"
714
+ | "tenant"
715
+ | "ip"
716
+ | "user+handler"
717
+ | "tenant+handler"
718
+ | "ip+handler";
719
+
720
+ export type RateLimitOption = {
721
+ readonly per: RateLimitPer;
722
+ readonly limit: number;
723
+ readonly windowSeconds: number;
724
+ readonly cost?: number;
725
+ };
726
+
727
+ export type WriteHandlerDef = {
728
+ readonly name: string;
729
+ readonly schema: ZodType;
730
+ readonly handler: WriteHandlerFn;
731
+ readonly access?: AccessRule;
732
+ readonly skipTransitionGuard?: boolean;
733
+ readonly rateLimit?: RateLimitOption;
734
+ };
735
+
736
+ export type QueryHandlerDef = {
737
+ readonly name: string;
738
+ readonly schema: ZodType;
739
+ readonly handler: QueryHandlerFn;
740
+ readonly access?: AccessRule;
741
+ readonly rateLimit?: RateLimitOption;
742
+ };