@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,418 @@
1
+ import type { Context, Next } from "hono";
2
+ import { getCookie } from "hono/cookie";
3
+ import { createAnonymousUser } from "../engine/system-user";
4
+ import type { SessionUser, TenantId } from "../engine/types";
5
+ import { parseTenantId } from "../engine/types/identifiers";
6
+ import { TENANT_COOKIE_NAME, TENANT_HEADER_NAME } from "./api-constants";
7
+ import type { JwtHelper } from "./jwt";
8
+
9
+ const USER_KEY = "pipelineUser";
10
+ const AUTH_TRANSPORT_KEY = "authTransport";
11
+
12
+ // Names used across middleware and auth-routes. Kept here so csrf-middleware
13
+ // and auth-routes import them from a single source of truth — renaming a
14
+ // cookie is a coordinated change across issuer, reader and deleter.
15
+ export const AUTH_COOKIE_NAME = "kumiko_auth";
16
+ export const CSRF_COOKIE_NAME = "kumiko_csrf";
17
+ export const CSRF_HEADER_NAME = "X-CSRF-Token";
18
+
19
+ // Which wire the current request authenticated over. Downstream
20
+ // csrf-middleware reads this: cookie-auth gets a CSRF-token check, bearer
21
+ // does not (headers aren't set cross-origin by browsers, so there is no
22
+ // CSRF vector on a bearer-only client).
23
+ export type AuthTransport = "cookie" | "bearer";
24
+
25
+ // Status of a sid from the server's perspective. The sessions feature owns
26
+ // the DB-backed implementation; middleware just consults whatever function
27
+ // the app wires in.
28
+ export type AuthSessionStatus = "live" | "revoked" | "expired" | "missing";
29
+
30
+ // Called by the middleware after JWT-verify. Gets the sid AND the expected
31
+ // userId from the JWT's `sub` — the checker MUST confirm the session row
32
+ // both exists + is live AND belongs to expectedUserId. Without the userId
33
+ // cross-check, a compromised-sid-but-valid-JWT combination from two
34
+ // different users could slip through (defense-in-depth: the JWT signing
35
+ // secret is the main control, but we don't want a single leaked sid to
36
+ // matter when the attacker already knows the userId too).
37
+ export type AuthSessionChecker = (
38
+ sid: string,
39
+ expectedUserId: string,
40
+ ) => Promise<AuthSessionStatus>;
41
+
42
+ export type AuthMiddlewareOptions = {
43
+ // Called after JWT-verify when the token carries a sid. If the checker
44
+ // reports anything other than "live", the request is rejected with 401.
45
+ // Omit to run in stateless-JWT mode (any valid JWT is accepted).
46
+ readonly sessionChecker?: AuthSessionChecker;
47
+ // When true, a JWT WITHOUT a sid is rejected. Leave false during rollout
48
+ // so already-issued stateless JWTs keep working until they expire; flip
49
+ // to true once the server has been emitting sid for longer than the JWT
50
+ // TTL. Has no effect when sessionChecker is undefined.
51
+ readonly strictMode?: boolean;
52
+ // Opt-in: when set, requests without a JWT are treated as anonymous
53
+ // callers instead of being rejected with 401. The middleware synthesises
54
+ // a SessionUser with id="anonymous" and roles=["anonymous"], scoped to a
55
+ // tenantId resolved through the chain documented on AnonymousAccessConfig.
56
+ readonly anonymousAccess?: AnonymousAccessConfig;
57
+ };
58
+
59
+ // Resolves the tenant for an unauthenticated request. Returns null when no
60
+ // tenant can be determined — the middleware rejects with 400 instead of
61
+ // silently falling through. Throw only on infrastructure failures (DB down,
62
+ // cache broken) — those surface as 500. "Subdomain unknown" is null, not
63
+ // throw.
64
+ export type TenantResolver = (c: Context) => Promise<TenantId | null> | TenantId | null;
65
+
66
+ // Returns true when the tenantId names an active tenant. The middleware
67
+ // calls this after a header/cookie/resolver supplied a candidate, before
68
+ // the anonymous SessionUser is synthesised. Omit to skip the existence
69
+ // check entirely — fine for prototypes, NOT for production multi-tenant
70
+ // deployments where a caller could otherwise probe arbitrary ids.
71
+ export type TenantExists = (tenantId: TenantId) => Promise<boolean> | boolean;
72
+
73
+ export type AnonymousAccessConfig = {
74
+ // Custom resolver (e.g. subdomain parser). Consulted only when neither
75
+ // the X-Tenant header nor the kumiko_tenant cookie are present.
76
+ readonly tenantResolver?: TenantResolver;
77
+ // Single-tenant shortcut. When set, the server runs in **locked** mode:
78
+ // - no client-supplied tenant: defaultTenantId is used.
79
+ // - client supplies a matching tenant (header/cookie/resolver): allowed.
80
+ // - client supplies a non-matching tenant: 400 tenant_mismatch (the
81
+ // server is single-tenant; rejecting protects against confused clients
82
+ // who think they're talking to a different deployment).
83
+ // The framework does NOT verify defaultTenantId against the DB at boot;
84
+ // the caller is responsible (see sample for the pattern).
85
+ readonly defaultTenantId?: TenantId;
86
+ // Per-request existence check for header/cookie/resolver-supplied ids.
87
+ // Skipped for the defaultTenantId path (the caller already vetted that
88
+ // value when configuring the server).
89
+ readonly tenantExists?: TenantExists;
90
+ };
91
+
92
+ // Where the candidate tenant came from. Drives the validation policy:
93
+ // - header / cookie / resolver: untrusted, must pass tenantExists if set.
94
+ // - default: trusted (configured at boot), no per-request check.
95
+ type TenantSource = "header" | "cookie" | "resolver" | "default";
96
+
97
+ // Error-body shape matches the UnprocessableError/AccessDeniedError on the
98
+ // dispatcher path — clients parse `{error: {code, httpStatus, message,
99
+ // i18nKey, details?}}` everywhere, not a second middleware-only shape.
100
+ // All middleware rejects (missing/invalid/ambiguous token, session state,
101
+ // csrf mismatch) route through this helper so one parser covers the lot.
102
+ type MiddlewareRejectCode =
103
+ | "missing_token"
104
+ | "invalid_token"
105
+ | "ambiguous_auth"
106
+ | "session_invalid"
107
+ | "tenant_required"
108
+ | "tenant_not_found"
109
+ | "tenant_mismatch"
110
+ | "invalid_tenant_format";
111
+
112
+ function middlewareReject(
113
+ c: Context,
114
+ opts: {
115
+ code: MiddlewareRejectCode;
116
+ status: 400 | 401 | 403 | 404;
117
+ message: string;
118
+ i18nKey: string;
119
+ details?: Record<string, unknown>;
120
+ },
121
+ ): Response {
122
+ return c.json(
123
+ {
124
+ error: {
125
+ code: opts.code,
126
+ httpStatus: opts.status,
127
+ message: opts.message,
128
+ i18nKey: opts.i18nKey,
129
+ ...(opts.details ? { details: opts.details } : {}),
130
+ },
131
+ },
132
+ opts.status,
133
+ );
134
+ }
135
+
136
+ function sessionInvalid(c: Context, reason: AuthSessionStatus | "no_sid"): Response {
137
+ return middlewareReject(c, {
138
+ code: "session_invalid",
139
+ status: 401,
140
+ message: `session ${reason}`,
141
+ i18nKey: "auth.errors.sessionInvalid",
142
+ details: { reason },
143
+ });
144
+ }
145
+
146
+ // Extract the JWT from either the kumiko_auth cookie (web) or the
147
+ // Authorization Bearer header (native / server-to-server). The two paths
148
+ // are mutually exclusive: if both are present the request is rejected with
149
+ // `ambiguous_auth` to prevent a confused-deputy bug where a server-bug
150
+ // could authenticate via one transport while the other sat there ignored.
151
+ // Note: this is NOT a CSRF control — Bearer-only clients are already safe
152
+ // because browsers can't set Authorization headers cross-origin. The reject
153
+ // exists so future middleware authors can't accidentally mix transports.
154
+ function extractToken(
155
+ c: Context,
156
+ ): { token: string; transport: AuthTransport } | { error: "both" | "missing" } {
157
+ const cookieToken = getCookie(c, AUTH_COOKIE_NAME);
158
+ const header = c.req.header("Authorization");
159
+ const bearerToken = header?.startsWith("Bearer ") ? header.slice(7) : undefined;
160
+
161
+ if (cookieToken && bearerToken) return { error: "both" };
162
+ if (cookieToken) return { token: cookieToken, transport: "cookie" };
163
+ if (bearerToken) return { token: bearerToken, transport: "bearer" };
164
+ return { error: "missing" };
165
+ }
166
+
167
+ export function authMiddleware(jwt: JwtHelper, options: AuthMiddlewareOptions = {}) {
168
+ const { sessionChecker, strictMode = false, anonymousAccess } = options;
169
+
170
+ return async (c: Context, next: Next) => {
171
+ const extracted = extractToken(c);
172
+ if ("error" in extracted) {
173
+ if (extracted.error === "both") {
174
+ return middlewareReject(c, {
175
+ code: "ambiguous_auth",
176
+ status: 400,
177
+ message: "cookie and bearer transport presented simultaneously",
178
+ i18nKey: "auth.errors.ambiguousAuth",
179
+ });
180
+ }
181
+ // No JWT → either fall through as anonymous (when the server opts in)
182
+ // or reject with 401 (preserving the pre-anonymous default).
183
+ //
184
+ // Auth-routes-Sonderfall: `/api/auth/*`-Pfade die NICHT in
185
+ // PUBLIC_API_PATHS sind (tenants, switch-tenant, logout) brauchen
186
+ // einen JWT — aber keinen Tenant-Resolve. Würden sie durch
187
+ // handleAnonymous laufen, wirft resolveTenant 400 tenant_required
188
+ // wenn kein Tenant declared ist. Falsche Diagnose: das Problem ist
189
+ // missing authentication, nicht missing tenant. Daher hier direkt
190
+ // 401, ohne den anonymous-Tenant-Flow zu durchlaufen.
191
+ if (anonymousAccess) {
192
+ if (c.req.path.startsWith("/api/auth/")) {
193
+ return middlewareReject(c, {
194
+ code: "missing_token",
195
+ status: 401,
196
+ message: "no auth cookie or bearer token",
197
+ i18nKey: "auth.errors.missingToken",
198
+ });
199
+ }
200
+ return await handleAnonymous(c, anonymousAccess, next);
201
+ }
202
+ return middlewareReject(c, {
203
+ code: "missing_token",
204
+ status: 401,
205
+ message: "no auth cookie or bearer token",
206
+ i18nKey: "auth.errors.missingToken",
207
+ });
208
+ }
209
+ const { token, transport } = extracted;
210
+
211
+ let payload: Awaited<ReturnType<JwtHelper["verify"]>>;
212
+ try {
213
+ payload = await jwt.verify(token);
214
+ } catch {
215
+ return middlewareReject(c, {
216
+ code: "invalid_token",
217
+ status: 401,
218
+ message: "token verification failed",
219
+ i18nKey: "auth.errors.invalidToken",
220
+ });
221
+ }
222
+
223
+ // Session liveness check — only when both a checker is wired AND the
224
+ // token carries a sid. strictMode governs the no-sid case below so that
225
+ // both old JWTs (no sid) and rolling-deploy gaps can be handled.
226
+ if (sessionChecker) {
227
+ if (payload.jti) {
228
+ const status = await sessionChecker(payload.jti, payload.sub);
229
+ if (status !== "live") {
230
+ return sessionInvalid(c, status);
231
+ }
232
+ } else if (strictMode) {
233
+ return sessionInvalid(c, "no_sid");
234
+ }
235
+ }
236
+
237
+ // Tenant-mismatch guard: if the caller sends BOTH a JWT (carrying a
238
+ // signed tenantId) AND an X-Tenant header pointing at a different tenant,
239
+ // reject loudly. JWT always wins on the wire, so silent ignore would let
240
+ // a confused client believe it's hitting tenantB while it's actually on
241
+ // tenantA. Same defensive stance as ambiguous_auth (cookie + bearer).
242
+ const headerTenant = c.req.header(TENANT_HEADER_NAME);
243
+ if (headerTenant !== undefined && headerTenant !== payload.tenantId) {
244
+ return middlewareReject(c, {
245
+ code: "tenant_mismatch",
246
+ status: 400,
247
+ message: "JWT tenantId and X-Tenant header disagree",
248
+ i18nKey: "auth.errors.tenantMismatch",
249
+ details: { jwtTenantId: payload.tenantId, headerTenantId: headerTenant },
250
+ });
251
+ }
252
+
253
+ const user: SessionUser = {
254
+ id: payload.sub,
255
+ tenantId: payload.tenantId,
256
+ roles: payload.roles,
257
+ ...(payload.claims ? { claims: payload.claims } : {}),
258
+ ...(payload.jti ? { sid: payload.jti } : {}),
259
+ };
260
+ c.set(USER_KEY, user);
261
+ c.set(AUTH_TRANSPORT_KEY, transport);
262
+ await next();
263
+ };
264
+ }
265
+
266
+ export function getUser(c: Context): SessionUser {
267
+ // @cast-boundary engine-bridge — Hono context.get returns unknown
268
+ return c.get(USER_KEY) as SessionUser;
269
+ }
270
+
271
+ export function getAuthTransport(c: Context): AuthTransport | undefined {
272
+ // @cast-boundary engine-bridge — Hono context.get returns unknown
273
+ return c.get(AUTH_TRANSPORT_KEY) as AuthTransport | undefined;
274
+ }
275
+
276
+ // Anonymous request flow. Steps:
277
+ // 1. Read raw client-supplied tenant from X-Tenant header / cookie.
278
+ // 2. Validate format (UUID-shape) — junk strings get 400 right here.
279
+ // 3. Pick the authoritative tenant:
280
+ // - defaultTenantId set: locked single-tenant mode. A client tenant
281
+ // that disagrees with default → 400 tenant_mismatch. Otherwise
282
+ // default wins.
283
+ // - else: client tenant > resolver(req). No tenant at all → 400.
284
+ // 4. For non-default sources, run tenantExists if configured → 404.
285
+ // 5. Synthesise the anonymous SessionUser. The transport flag stays unset
286
+ // so csrf-middleware skips the double-submit check (no auth-cookie ⇒
287
+ // no CSRF vector to defend against).
288
+ async function handleAnonymous(
289
+ c: Context,
290
+ config: AnonymousAccessConfig,
291
+ next: Next,
292
+ ): Promise<Response | undefined> {
293
+ // Step 1+2: parse client-supplied tenant. Reject malformed values before
294
+ // they touch any downstream consumer (DB, cache, audit row).
295
+ const headerRaw = c.req.header(TENANT_HEADER_NAME);
296
+ const cookieRaw = getCookie(c, TENANT_COOKIE_NAME);
297
+
298
+ const headerCheck = parseClientTenant(headerRaw, "X-Tenant header");
299
+ if (headerCheck.error) return middlewareReject(c, headerCheck.error);
300
+ const cookieCheck = parseClientTenant(cookieRaw, "kumiko_tenant cookie");
301
+ if (cookieCheck.error) return middlewareReject(c, cookieCheck.error);
302
+
303
+ const clientTenant: { id: TenantId; source: "header" | "cookie" } | null =
304
+ headerCheck.tenantId !== null
305
+ ? { id: headerCheck.tenantId, source: "header" }
306
+ : cookieCheck.tenantId !== null
307
+ ? { id: cookieCheck.tenantId, source: "cookie" }
308
+ : null;
309
+
310
+ // Step 3: pick the authoritative tenant.
311
+ const resolved = await resolveTenant(c, config, clientTenant);
312
+ if ("error" in resolved) return middlewareReject(c, resolved.error);
313
+
314
+ // Step 4: existence check for untrusted sources.
315
+ if (resolved.source !== "default" && config.tenantExists) {
316
+ const exists = await config.tenantExists(resolved.tenantId);
317
+ if (!exists) {
318
+ return middlewareReject(c, {
319
+ code: "tenant_not_found",
320
+ status: 404,
321
+ message: `tenant "${resolved.tenantId}" does not exist`,
322
+ i18nKey: "auth.errors.tenantNotFound",
323
+ details: { tenantId: resolved.tenantId },
324
+ });
325
+ }
326
+ }
327
+
328
+ // Step 5: synthesise + continue.
329
+ c.set(USER_KEY, createAnonymousUser(resolved.tenantId));
330
+ await next();
331
+ // skip: anonymous path completed — Hono middleware contract returns void
332
+ // when next() ran; explicit return makes the union return-type honest.
333
+ return;
334
+ }
335
+
336
+ // Validates an X-Tenant / cookie value against the tenantId format. Returns
337
+ // `{tenantId: null}` when absent, `{tenantId: TenantId}` when valid, or
338
+ // `{error: …}` when the value is non-empty junk (so the caller can return
339
+ // 400 instead of silently treating it as "no tenant supplied").
340
+ function parseClientTenant(
341
+ raw: string | undefined,
342
+ source: string,
343
+ ): { tenantId: TenantId | null; error?: never } | { tenantId?: never; error: RejectArgs } {
344
+ if (raw === undefined || raw === "") return { tenantId: null };
345
+ const parsed = parseTenantId(raw);
346
+ if (parsed === null) {
347
+ return {
348
+ error: {
349
+ code: "invalid_tenant_format",
350
+ status: 400,
351
+ message: `${source} is not a valid tenant id`,
352
+ i18nKey: "auth.errors.invalidTenantFormat",
353
+ details: { source, value: raw },
354
+ },
355
+ };
356
+ }
357
+ return { tenantId: parsed };
358
+ }
359
+
360
+ type ResolvedTenant = { tenantId: TenantId; source: TenantSource };
361
+ type ResolveError = { error: RejectArgs };
362
+
363
+ // Implements the "client tenant vs default" precedence. Single-tenant mode
364
+ // (defaultTenantId set) is **locked**: the client either agrees with the
365
+ // default or gets tenant_mismatch — defending the deployment from confused
366
+ // clients that think they're talking to a different installation.
367
+ async function resolveTenant(
368
+ c: Context,
369
+ config: AnonymousAccessConfig,
370
+ clientTenant: { id: TenantId; source: "header" | "cookie" } | null,
371
+ ): Promise<ResolvedTenant | ResolveError> {
372
+ if (config.defaultTenantId !== undefined) {
373
+ if (clientTenant && clientTenant.id !== config.defaultTenantId) {
374
+ return {
375
+ error: {
376
+ code: "tenant_mismatch",
377
+ status: 400,
378
+ message: `${clientTenant.source} tenant disagrees with server default`,
379
+ i18nKey: "auth.errors.tenantMismatch",
380
+ details: {
381
+ clientTenantId: clientTenant.id,
382
+ defaultTenantId: config.defaultTenantId,
383
+ },
384
+ },
385
+ };
386
+ }
387
+ return { tenantId: config.defaultTenantId, source: "default" };
388
+ }
389
+
390
+ if (clientTenant) {
391
+ return { tenantId: clientTenant.id, source: clientTenant.source };
392
+ }
393
+
394
+ if (config.tenantResolver) {
395
+ const resolved = await config.tenantResolver(c);
396
+ if (resolved !== null && resolved !== undefined) {
397
+ return { tenantId: resolved, source: "resolver" };
398
+ }
399
+ }
400
+
401
+ return {
402
+ error: {
403
+ code: "tenant_required",
404
+ status: 400,
405
+ message:
406
+ "anonymous access requires a tenant (X-Tenant header, kumiko_tenant cookie, or server-side resolver)",
407
+ i18nKey: "auth.errors.tenantRequired",
408
+ },
409
+ };
410
+ }
411
+
412
+ type RejectArgs = {
413
+ code: MiddlewareRejectCode;
414
+ status: 400 | 401 | 403 | 404;
415
+ message: string;
416
+ i18nKey: string;
417
+ details?: Record<string, unknown>;
418
+ };