@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
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # @cosmicdrift/kumiko-framework
2
+
3
+ [![License: BUSL-1.1](https://img.shields.io/badge/License-BUSL--1.1-blue.svg)](../../LICENSE)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
5
+
6
+ Framework core for Kumiko — engine, pipeline, API, DB, event-store, and
7
+ every other bit that makes Kumiko go.
8
+
9
+ > Multi-tenant, command-based, event-sourced app framework for Bun + Hono +
10
+ > Drizzle. Define features, register entities, write commands — the framework
11
+ > wires dispatch, persistence, projections, async subscribers, and realtime
12
+ > delivery.
13
+
14
+ See the [monorepo root README](../../README.md) for the broader pitch, the
15
+ [docs/plans](../../docs/plans) directory for architecture, and [samples/](../../samples)
16
+ for runnable examples of every feature.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ yarn add @cosmicdrift/kumiko-framework
22
+ # peers you probably already have:
23
+ yarn add drizzle-orm hono ioredis zod
24
+ ```
25
+
26
+ Bun is the intended runtime. Node 20+ works for the CLI and tests.
27
+
28
+ ## At-a-glance
29
+
30
+ ```typescript
31
+ import { defineFeature, createEntity, createTextField } from "@cosmicdrift/kumiko-framework/engine";
32
+
33
+ export const taskEntity = createEntity({
34
+ fields: {
35
+ title: createTextField({ required: true, searchable: true }),
36
+ done: createTextField(),
37
+ },
38
+ softDelete: true,
39
+ });
40
+
41
+ const taskTable = buildDrizzleTable("task", taskEntity);
42
+ const taskExecutor = createEventStoreExecutor(taskTable, taskEntity, { entityName: "task" });
43
+
44
+ export const taskFeature = defineFeature("tasks", (r) => {
45
+ r.entity("task", taskEntity);
46
+
47
+ // Write handlers go through createEventStoreExecutor — events + projection in one TX,
48
+ // optimistic locking, access control, all explicit.
49
+ r.writeHandler(
50
+ "task:create",
51
+ z.object({ title: z.string() }),
52
+ async (event, ctx) => taskExecutor.create(event.payload, event.user, ctx.db),
53
+ { access: { roles: ["User"] } },
54
+ );
55
+ r.writeHandler(
56
+ "task:update",
57
+ z.object({ id: z.uuid(), version: z.number(), changes: z.object({ title: z.string().optional() }) }),
58
+ async (event, ctx) => taskExecutor.update(event.payload, event.user, ctx.db),
59
+ { access: { roles: ["User"] } },
60
+ );
61
+ r.writeHandler(
62
+ "task:delete",
63
+ z.object({ id: z.uuid() }),
64
+ async (event, ctx) => taskExecutor.delete(event.payload, event.user, ctx.db),
65
+ { access: { roles: ["Admin"] } },
66
+ );
67
+ r.queryHandler(
68
+ "task:list",
69
+ z.object({}),
70
+ async (query, ctx) => taskExecutor.list(query.payload, query.user, ctx.db),
71
+ { access: { openToAll: true } },
72
+ );
73
+ r.queryHandler(
74
+ "task:detail",
75
+ z.object({ id: z.uuid() }),
76
+ async (query, ctx) => taskExecutor.detail(query.payload, query.user, ctx.db),
77
+ { access: { openToAll: true } },
78
+ );
79
+
80
+ // Read-model fed from task events, rebuildable via the CLI
81
+ r.projection({
82
+ name: "tasks-per-day",
83
+ source: "task",
84
+ table: tasksPerDayTable,
85
+ apply: {
86
+ "task.created": async (event, tx) => {
87
+ /* count++ */
88
+ },
89
+ },
90
+ });
91
+
92
+ // Async consumer — runs after commit via the event-dispatcher,
93
+ // cursor-based, at-least-once, per-consumer dead-letter semantics.
94
+ // Omit `table` for pure side-effect handlers (mail, webhooks, ...).
95
+ r.multiStreamProjection({
96
+ name: "notify-new-task",
97
+ apply: {
98
+ "task.created": async (event) => {
99
+ // e.g. push to an external notification service
100
+ },
101
+ },
102
+ });
103
+ });
104
+ ```
105
+
106
+ ## Package exports
107
+
108
+ | Entry | What's in it |
109
+ |---|---|
110
+ | `@cosmicdrift/kumiko-framework/engine` | `defineFeature`, `createEntity`, field helpers, access rules, registry |
111
+ | `@cosmicdrift/kumiko-framework/db` | Drizzle re-exports, `createEventStoreExecutor`, table builders, tenant-db |
112
+ | `@cosmicdrift/kumiko-framework/event-store` | `events` table, `append`, `loadAggregate`, `loadAggregateAsOf` |
113
+ | `@cosmicdrift/kumiko-framework/pipeline` | Dispatcher, event-dispatcher (AsyncDaemon), projection-rebuild, SSE + search consumers |
114
+ | `@cosmicdrift/kumiko-framework/api` | `buildServer`, auth middleware, SSE route, error contract |
115
+ | `@cosmicdrift/kumiko-framework/auth` | JWT helper, password hashing, session users |
116
+ | `@cosmicdrift/kumiko-framework/search` | `SearchAdapter` interface, in-memory adapter, Meili wrapper |
117
+ | `@cosmicdrift/kumiko-framework/jobs` | BullMQ-backed job runner, cron scheduling |
118
+ | `@cosmicdrift/kumiko-framework/files` | Signed-URL upload/download, tenant-scoped storage |
119
+ | `@cosmicdrift/kumiko-framework/i18n` | i18next setup, per-feature translation registration |
120
+ | `@cosmicdrift/kumiko-framework/ui` | React hooks (Zustand stores, SSE subscription, optimistic mutations) |
121
+ | `@cosmicdrift/kumiko-framework/testing` | `setupTestStack`, `createTestDb`, request helpers |
122
+ | `@cosmicdrift/kumiko-framework/utils` | Safe JSON, qualified-name helpers |
123
+ | `@cosmicdrift/kumiko-framework/errors` | Error classes, `writeFailure`, reason contracts |
124
+
125
+ ## Core concepts
126
+
127
+ - **Feature as unit of deployment.** `defineFeature` registers entities,
128
+ write/query handlers, projections, post-event subscribers, lifecycle hooks,
129
+ access rules, and translations.
130
+ - **Commands in, state out.** Writes are commands dispatched through HTTP;
131
+ the dispatcher validates, enforces access, runs lifecycle hooks, persists
132
+ events, and triggers projections in a single TX.
133
+ - **Event-sourced by default.** Every write goes through `createEventStoreExecutor`
134
+ and appends a domain event to the aggregate stream. Auto-generated CRUD
135
+ events (`<entity>.created/updated/deleted/restored`) for record writes,
136
+ explicit `ctx.appendEvent` for domain events with intent. Projections feed
137
+ off the stream for same-TX read-after-write consistency.
138
+ - **Async side-effects via cursor.** SSE broadcast, search indexing, and
139
+ feature-registered `r.multiStreamProjection` consumers run on a single
140
+ cursor-based dispatcher (AsyncDaemon pattern). Per-consumer checkpoints,
141
+ halt-on-poison, dead-letter after N retries.
142
+ - **Multi-tenant scoping.** Every event, entity, projection, and search
143
+ index carries `tenantId`. `TenantDb` is a TX-scoped wrapper that refuses
144
+ writes outside the current tenant.
145
+ - **Optimistic concurrency.** `UNIQUE(aggregate_id, version)` on events
146
+ gives atomic append + conflict detection; `VersionConflictError` surfaces
147
+ races as a first-class value.
148
+ - **Idempotency + dedup.** Request-id backed unique index on the events
149
+ table turns retries into replays. `IdempotencyGuard` caches write
150
+ outcomes per tenant.
151
+
152
+ ## Status
153
+
154
+ This framework is pre-1.0 and evolves fast. Every feature has a runnable
155
+ sample under `samples/`; the roadmap lives in [docs/plans/uebersicht.md](../../docs/plans/uebersicht.md).
156
+
157
+ ## License
158
+
159
+ BUSL-1.1 — see [LICENSE](../../LICENSE).
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@cosmicdrift/kumiko-framework",
3
+ "version": "0.1.0",
4
+ "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
+ "license": "BUSL-1.1",
6
+ "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/cosmicdriftgamestudio/kumiko-framework.git",
10
+ "directory": "packages/framework"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/cosmicdriftgamestudio/kumiko-framework/issues"
14
+ },
15
+ "homepage": "https://kumiko.so",
16
+ "keywords": [
17
+ "framework",
18
+ "multi-tenant",
19
+ "realtime",
20
+ "command-based",
21
+ "typescript",
22
+ "hono",
23
+ "drizzle",
24
+ "zod"
25
+ ],
26
+ "type": "module",
27
+ "kumiko": {
28
+ "runtime": "runtime"
29
+ },
30
+ "exports": {
31
+ "./engine": "./src/engine/index.ts",
32
+ "./engine/types": "./src/engine/types/index.ts",
33
+ "./errors": "./src/errors/index.ts",
34
+ "./db": "./src/db/index.ts",
35
+ "./event-store": "./src/event-store/index.ts",
36
+ "./event-store/admin-api": "./src/event-store/admin-api.ts",
37
+ "./pipeline": "./src/pipeline/index.ts",
38
+ "./api": "./src/api/index.ts",
39
+ "./i18n": "./src/i18n/index.ts",
40
+ "./auth": "./src/auth/index.ts",
41
+ "./files": "./src/files/index.ts",
42
+ "./jobs": "./src/jobs/index.ts",
43
+ "./migrations": "./src/migrations/index.ts",
44
+ "./entrypoint": "./src/entrypoint/index.ts",
45
+ "./random": "./src/random/index.ts",
46
+ "./redis": "./src/redis/index.ts",
47
+ "./search": "./src/search/index.ts",
48
+ "./search/meilisearch": "./src/search/meilisearch-adapter.ts",
49
+ "./secrets": "./src/secrets/index.ts",
50
+ "./stack": "./src/stack/index.ts",
51
+ "./testing": "./src/testing/index.ts",
52
+ "./testing/handler-context": "./src/testing/handler-context.ts",
53
+ "./testing/e2e-generator": "./src/testing/e2e-generator.ts",
54
+ "./time": "./src/time/index.ts",
55
+ "./ui-types": "./src/ui-types/index.ts",
56
+ "./utils": "./src/utils/index.ts"
57
+ },
58
+ "dependencies": {
59
+ "bullmq": "^5.73.5",
60
+ "bun-types": "^1.3.12",
61
+ "drizzle-kit": "^0.31.10",
62
+ "drizzle-orm": "^0.45.2",
63
+ "hono": "^4.12.12",
64
+ "i18next": "^26.0.4",
65
+ "ioredis": "^5.6.0",
66
+ "jose": "^6.0.11",
67
+ "meilisearch": "^0.57.0",
68
+ "pino": "^10.3.1",
69
+ "postgres": "^3.4.9",
70
+ "temporal-polyfill": "^0.3.2",
71
+ "ts-morph": "^28.0.0",
72
+ "uuid": "^14.0.0",
73
+ "zod": "^4.3.6"
74
+ },
75
+ "devDependencies": {
76
+ "@cosmicdrift/kumiko-dispatcher-live": "workspace:*",
77
+ "@types/uuid": "^11.0.0",
78
+ "bun-types": "^1.2.9",
79
+ "drizzle-kit": "^0.31.0",
80
+ "pino-pretty": "^13.1.3"
81
+ },
82
+ "publishConfig": {
83
+ "registry": "https://registry.npmjs.org",
84
+ "access": "public"
85
+ },
86
+ "files": [
87
+ "src",
88
+ "README.md",
89
+ "LICENSE"
90
+ ]
91
+ }
@@ -0,0 +1,325 @@
1
+ // Full-stack proof for anonymousAccess: handlers that allow roles=["anonymous"]
2
+ // must be reachable WITHOUT a JWT, while the rest of /api/* still requires
3
+ // authentication. Covers the resolution chain (defaultTenantId, X-Tenant
4
+ // header, kumiko_tenant cookie, custom resolver) plus the rejection paths
5
+ // (no tenant, unknown tenant, openToAll-protected).
6
+
7
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
8
+ import { z } from "zod";
9
+ import { createEventStoreExecutor } from "../db/event-store-executor";
10
+ import { buildDrizzleTable } from "../db/table-builder";
11
+ import {
12
+ ANONYMOUS_USER_ID,
13
+ createEntity,
14
+ createTextField,
15
+ defineFeature,
16
+ type TenantId,
17
+ } from "../engine";
18
+ import { createEntityTable, setupTestStack, type TestStack, TestUsers } from "../stack";
19
+
20
+ const TENANT_ID = "00000000-0000-4000-8000-000000000001" as TenantId;
21
+ const OTHER_TENANT_ID = "00000000-0000-4000-8000-000000000002" as TenantId;
22
+
23
+ // --- Feature ---
24
+
25
+ const productEntity = createEntity({
26
+ table: "anon_products",
27
+ fields: {
28
+ name: createTextField({ required: true }),
29
+ },
30
+ });
31
+ const productTable = buildDrizzleTable("product", productEntity);
32
+
33
+ const orderEntity = createEntity({
34
+ table: "anon_orders",
35
+ fields: {
36
+ productName: createTextField({ required: true }),
37
+ placedBy: createTextField({ default: "" }),
38
+ },
39
+ });
40
+ const orderTable = buildDrizzleTable("order", orderEntity);
41
+
42
+ const shopFeature = defineFeature("anonshop", (r) => {
43
+ r.entity("product", productEntity);
44
+ r.entity("order", orderEntity);
45
+
46
+ // Public listing — anonymous + authenticated customers see it.
47
+ r.queryHandler(
48
+ "product:list",
49
+ z.object({}),
50
+ async (_event, ctx) => {
51
+ const rows = await ctx.db.select().from(productTable);
52
+ return rows;
53
+ },
54
+ { access: { roles: ["anonymous", "User", "Admin"] } },
55
+ );
56
+
57
+ // Authenticated-only listing — confirms openToAll still rejects anonymous.
58
+ r.queryHandler(
59
+ "product:list-auth-only",
60
+ z.object({}),
61
+ async (_event, ctx) => {
62
+ const rows = await ctx.db.select().from(productTable);
63
+ return rows;
64
+ },
65
+ { access: { openToAll: true } },
66
+ );
67
+
68
+ // Anonymous can place a guest order.
69
+ r.writeHandler(
70
+ "order:guest-checkout",
71
+ z.object({ productName: z.string().min(1) }),
72
+ async (event, ctx) => {
73
+ const crud = createEventStoreExecutor(orderTable, orderEntity, { entityName: "order" });
74
+ return crud.create(
75
+ { productName: event.payload.productName, placedBy: event.user.id },
76
+ event.user,
77
+ ctx.db,
78
+ );
79
+ },
80
+ { access: { roles: ["anonymous", "User"] } },
81
+ );
82
+
83
+ // Admin-only — confirms role-gated handlers still reject anonymous.
84
+ r.writeHandler(
85
+ "product:create",
86
+ z.object({ name: z.string().min(1) }),
87
+ async (event, ctx) => {
88
+ const crud = createEventStoreExecutor(productTable, productEntity, {
89
+ entityName: "product",
90
+ });
91
+ return crud.create({ name: event.payload.name }, event.user, ctx.db);
92
+ },
93
+ { access: { roles: ["Admin"] } },
94
+ );
95
+ });
96
+
97
+ // --- Suite ---
98
+
99
+ describe("anonymous access — single-tenant default", () => {
100
+ let stack: TestStack;
101
+
102
+ beforeAll(async () => {
103
+ stack = await setupTestStack({
104
+ features: [shopFeature],
105
+ anonymousAccess: { defaultTenantId: TENANT_ID },
106
+ });
107
+ await createEntityTable(stack.db, productEntity);
108
+ await createEntityTable(stack.db, orderEntity);
109
+ });
110
+
111
+ afterAll(() => stack.cleanup());
112
+
113
+ beforeEach(async () => {
114
+ await stack.db.delete(productTable);
115
+ await stack.db.delete(orderTable);
116
+ });
117
+
118
+ test("anonymous query succeeds without any auth headers", async () => {
119
+ // Seed a product with the admin user so the query has data to return.
120
+ await stack.http.writeOk(
121
+ "anonshop:write:product:create",
122
+ { name: "Espresso Beans" },
123
+ TestUsers.admin,
124
+ );
125
+
126
+ const res = await stack.http.raw("POST", "/api/query", {
127
+ type: "anonshop:query:product:list",
128
+ payload: {},
129
+ });
130
+
131
+ expect(res.status).toBe(200);
132
+ const body = (await res.json()) as { data: Array<{ name: string }> };
133
+ expect(body.data).toHaveLength(1);
134
+ expect(body.data[0]?.name).toBe("Espresso Beans");
135
+ });
136
+
137
+ test("anonymous write succeeds and records actor=anonymous", async () => {
138
+ const res = await stack.http.raw("POST", "/api/write", {
139
+ type: "anonshop:write:order:guest-checkout",
140
+ payload: { productName: "Espresso Beans" },
141
+ });
142
+
143
+ expect(res.status).toBe(200);
144
+ const body = (await res.json()) as { isSuccess: boolean };
145
+ expect(body.isSuccess).toBe(true);
146
+
147
+ // Verify the row landed with placedBy=anonymous in the DB. Confirms the
148
+ // synthesised SessionUser actually flows through to the handler.
149
+ const rows = await stack.db.select().from(orderTable);
150
+ expect(rows).toHaveLength(1);
151
+ expect(rows[0]?.["placedBy"]).toBe(ANONYMOUS_USER_ID);
152
+ });
153
+
154
+ test("openToAll handler rejects anonymous (regression guard)", async () => {
155
+ // The advisor-flagged regression: enabling anonymousAccess must NOT
156
+ // silently expose every existing openToAll endpoint. hasAccess refuses
157
+ // anonymous on openToAll, so the dispatcher returns AccessDenied.
158
+ const res = await stack.http.raw("POST", "/api/query", {
159
+ type: "anonshop:query:product:list-auth-only",
160
+ payload: {},
161
+ });
162
+ expect(res.status).toBe(403);
163
+ const body = (await res.json()) as { error: { code: string } };
164
+ expect(body.error.code).toBe("access_denied");
165
+ });
166
+
167
+ test("role-gated handler still rejects anonymous", async () => {
168
+ const res = await stack.http.raw("POST", "/api/write", {
169
+ type: "anonshop:write:product:create",
170
+ payload: { name: "Tea Set" },
171
+ });
172
+ expect(res.status).toBe(403);
173
+ });
174
+
175
+ test("authenticated user with JWT bypasses anonymous path entirely", async () => {
176
+ const res = await stack.http.query("anonshop:query:product:list", {}, TestUsers.admin);
177
+ expect(res.status).toBe(200);
178
+ });
179
+
180
+ test("X-Tenant header that disagrees with default → 400 tenant_mismatch", async () => {
181
+ // Single-tenant mode is locked: a client cannot override defaultTenantId
182
+ // by sending a different X-Tenant. Silent acceptance would let a
183
+ // confused client write into the wrong tenant of a single-tenant
184
+ // deployment that happened to have data for OTHER_TENANT_ID.
185
+ const res = await stack.http.raw(
186
+ "POST",
187
+ "/api/query",
188
+ { type: "anonshop:query:product:list", payload: {} },
189
+ { "X-Tenant": OTHER_TENANT_ID },
190
+ );
191
+ expect(res.status).toBe(400);
192
+ const body = (await res.json()) as { error: { code: string } };
193
+ expect(body.error.code).toBe("tenant_mismatch");
194
+ });
195
+
196
+ test("X-Tenant header matching default → accepted", async () => {
197
+ // Same default, redundant header — fine.
198
+ const res = await stack.http.raw(
199
+ "POST",
200
+ "/api/query",
201
+ { type: "anonshop:query:product:list", payload: {} },
202
+ { "X-Tenant": TENANT_ID },
203
+ );
204
+ expect(res.status).toBe(200);
205
+ });
206
+ });
207
+
208
+ describe("anonymous access — header-supplied tenant", () => {
209
+ let stack: TestStack;
210
+
211
+ beforeAll(async () => {
212
+ stack = await setupTestStack({
213
+ features: [shopFeature],
214
+ anonymousAccess: {
215
+ // No defaultTenantId — every anonymous request must declare its tenant.
216
+ tenantExists: async (id: TenantId) => id === TENANT_ID || id === OTHER_TENANT_ID,
217
+ },
218
+ });
219
+ await createEntityTable(stack.db, productEntity);
220
+ await createEntityTable(stack.db, orderEntity);
221
+ });
222
+
223
+ afterAll(() => stack.cleanup());
224
+
225
+ test("X-Tenant header resolves the tenant", async () => {
226
+ const res = await stack.http.raw(
227
+ "POST",
228
+ "/api/query",
229
+ { type: "anonshop:query:product:list", payload: {} },
230
+ { "X-Tenant": TENANT_ID },
231
+ );
232
+ expect(res.status).toBe(200);
233
+ });
234
+
235
+ test("malformed X-Tenant header → 400 invalid_tenant_format", async () => {
236
+ // Junk strings (SQL fragments, path traversals, plain typos) must never
237
+ // reach the pipeline as a TenantId. parseTenantId rejects anything that
238
+ // isn't a UUID-shape, the middleware turns that into a 400 here.
239
+ const res = await stack.http.raw(
240
+ "POST",
241
+ "/api/query",
242
+ { type: "anonshop:query:product:list", payload: {} },
243
+ { "X-Tenant": "not-a-uuid" },
244
+ );
245
+ expect(res.status).toBe(400);
246
+ const body = (await res.json()) as {
247
+ error: { code: string; details: { source: string } };
248
+ };
249
+ expect(body.error.code).toBe("invalid_tenant_format");
250
+ expect(body.error.details.source).toBe("X-Tenant header");
251
+ });
252
+
253
+ test("missing tenant → 400 tenant_required", async () => {
254
+ const res = await stack.http.raw("POST", "/api/query", {
255
+ type: "anonshop:query:product:list",
256
+ payload: {},
257
+ });
258
+ expect(res.status).toBe(400);
259
+ const body = (await res.json()) as { error: { code: string; i18nKey: string } };
260
+ expect(body.error.code).toBe("tenant_required");
261
+ expect(body.error.i18nKey).toBe("auth.errors.tenantRequired");
262
+ });
263
+
264
+ test("/api/auth/* without JWT → 401 missing_token (not 400 tenant_required)", async () => {
265
+ // Auth-routes (tenants, switch-tenant, logout) brauchen einen JWT,
266
+ // aber keinen Tenant-Resolve. Vor dem Fix fielen sie in handleAnonymous,
267
+ // das beim resolveTenant ohne X-Tenant 400 tenant_required wirft —
268
+ // falsche Diagnose, der Caller ist unauthenticated, nicht ohne Tenant.
269
+ // Login bleibt davon unberührt (in PUBLIC_API_PATHS, skipped vor auth).
270
+ const res = await stack.http.raw("GET", "/api/auth/tenants");
271
+ expect(res.status).toBe(401);
272
+ const body = (await res.json()) as { error: { code: string; i18nKey: string } };
273
+ expect(body.error.code).toBe("missing_token");
274
+ expect(body.error.i18nKey).toBe("auth.errors.missingToken");
275
+ });
276
+
277
+ test("authenticated request with conflicting X-Tenant header → 400 tenant_mismatch", async () => {
278
+ // JWT carries tenantId=TENANT_ID, but the client sends X-Tenant for a
279
+ // different tenant. Silent ignore would let the client think it's
280
+ // hitting OTHER_TENANT_ID while it's actually on TENANT_ID — defensive
281
+ // reject, same shape as ambiguous_auth.
282
+ const token = await stack.jwt.sign(TestUsers.admin);
283
+ const res = await stack.http.raw(
284
+ "POST",
285
+ "/api/query",
286
+ { type: "anonshop:query:product:list", payload: {} },
287
+ { Authorization: `Bearer ${token}`, "X-Tenant": OTHER_TENANT_ID },
288
+ );
289
+ expect(res.status).toBe(400);
290
+ const body = (await res.json()) as { error: { code: string } };
291
+ expect(body.error.code).toBe("tenant_mismatch");
292
+ });
293
+
294
+ test("unknown tenant → 404 tenant_not_found", async () => {
295
+ const res = await stack.http.raw(
296
+ "POST",
297
+ "/api/query",
298
+ { type: "anonshop:query:product:list", payload: {} },
299
+ { "X-Tenant": "00000000-0000-4000-8000-deadbeefdead" },
300
+ );
301
+ expect(res.status).toBe(404);
302
+ const body = (await res.json()) as { error: { code: string } };
303
+ expect(body.error.code).toBe("tenant_not_found");
304
+ });
305
+ });
306
+
307
+ describe("anonymous access — disabled by default", () => {
308
+ let stack: TestStack;
309
+
310
+ beforeAll(async () => {
311
+ stack = await setupTestStack({ features: [shopFeature] });
312
+ await createEntityTable(stack.db, productEntity);
313
+ await createEntityTable(stack.db, orderEntity);
314
+ });
315
+
316
+ afterAll(() => stack.cleanup());
317
+
318
+ test("missing JWT → 401 (no anonymous fall-through)", async () => {
319
+ const res = await stack.http.raw("POST", "/api/query", {
320
+ type: "anonshop:query:product:list",
321
+ payload: {},
322
+ });
323
+ expect(res.status).toBe(401);
324
+ });
325
+ });