@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,371 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { z } from "zod";
3
+ import {
4
+ AccessDeniedError,
5
+ buildErrorLog,
6
+ ConflictError,
7
+ InternalError,
8
+ isKumikoError,
9
+ KumikoError,
10
+ NotFoundError,
11
+ serializeError,
12
+ UnprocessableError,
13
+ ValidationError,
14
+ VersionConflictError,
15
+ validationErrorFromZod,
16
+ } from "../index";
17
+
18
+ describe("KumikoError: abstract base", () => {
19
+ test("sets i18nKey, details, name from subclass, and preserves cause chain", () => {
20
+ const inner = new Error("db_offline");
21
+ const err = new NotFoundError("order", 42, { cause: inner });
22
+
23
+ expect(err.name).toBe("NotFoundError");
24
+ expect(err.i18nKey).toBe("errors.notFound");
25
+ expect(err.cause).toBe(inner);
26
+ expect(isKumikoError(err)).toBe(true);
27
+ expect(isKumikoError(inner)).toBe(false);
28
+ });
29
+
30
+ describe("docsUrl getter — Self-Service-Link", () => {
31
+ test("uses details.reason when set (NotFoundError sets entity-specific reason)", () => {
32
+ const err = new NotFoundError("order", 42);
33
+ expect(err.docsUrl).toBe("https://docs.kumiko.so/errors/order_not_found");
34
+ });
35
+
36
+ test("uses details.reason when explicitly set (ConflictError-style)", () => {
37
+ const err = new ConflictError({ details: { reason: "stale_state" } });
38
+ expect(err.docsUrl).toBe("https://docs.kumiko.so/errors/stale_state");
39
+ });
40
+
41
+ test("falls back to code when details has no reason field", () => {
42
+ const err = new ConflictError({ details: { foo: "bar" } });
43
+ expect(err.docsUrl).toBe("https://docs.kumiko.so/errors/conflict");
44
+ });
45
+
46
+ test("falls back to code when details is undefined", () => {
47
+ const err = new ConflictError();
48
+ expect(err.docsUrl).toBe("https://docs.kumiko.so/errors/conflict");
49
+ });
50
+
51
+ test("respects KUMIKO_DOCS_URL env override (Self-Hosted-Kunden)", () => {
52
+ const original = process.env["KUMIKO_DOCS_URL"];
53
+ process.env["KUMIKO_DOCS_URL"] = "https://docs.acme.example";
54
+ try {
55
+ const err = new ConflictError({ details: { reason: "stale_state" } });
56
+ expect(err.docsUrl).toBe("https://docs.acme.example/errors/stale_state");
57
+ } finally {
58
+ if (original === undefined) delete process.env["KUMIKO_DOCS_URL"];
59
+ else process.env["KUMIKO_DOCS_URL"] = original;
60
+ }
61
+ });
62
+
63
+ test("serializeError exposes docsUrl in the wire response", () => {
64
+ const err = new ConflictError({ details: { reason: "stale_state" } });
65
+ const body = serializeError(err);
66
+ expect(body.error.docsUrl).toBe("https://docs.kumiko.so/errors/stale_state");
67
+ });
68
+ });
69
+ });
70
+
71
+ describe("ValidationError", () => {
72
+ test("holds field list with path, code, i18nKey", () => {
73
+ const err = new ValidationError({
74
+ fields: [{ path: "email", code: "invalid_type", i18nKey: "errors.validation.invalid_type" }],
75
+ });
76
+ expect(err.code).toBe("validation_error");
77
+ expect(err.httpStatus).toBe(400);
78
+ expect(err.details).toMatchObject({
79
+ fields: [{ path: "email", code: "invalid_type" }],
80
+ });
81
+ });
82
+
83
+ test("validationErrorFromZod maps issues to details.fields with params", () => {
84
+ const schema = z.object({ name: z.string().min(3), age: z.number().int() });
85
+ const result = schema.safeParse({ name: "x", age: "oops" });
86
+ if (result.success) throw new Error("zod did not reject");
87
+
88
+ const err = validationErrorFromZod(result.error);
89
+ expect(err).toBeInstanceOf(ValidationError);
90
+
91
+ const fields = (err.details as { fields: Array<Record<string, unknown>> }).fields;
92
+ expect(fields).toHaveLength(2);
93
+
94
+ const nameIssue = fields.find((f) => f["path"] === "name");
95
+ expect(nameIssue).toMatchObject({
96
+ code: "too_small",
97
+ i18nKey: "errors.validation.too_small",
98
+ });
99
+ expect(nameIssue?.["params"]).toMatchObject({ minimum: 3 });
100
+
101
+ const ageIssue = fields.find((f) => f["path"] === "age");
102
+ expect(ageIssue).toMatchObject({
103
+ code: "invalid_type",
104
+ i18nKey: "errors.validation.invalid_type",
105
+ });
106
+ expect(err.cause).toBe(result.error);
107
+ });
108
+
109
+ test('root-level zod issue maps to path "(root)"', () => {
110
+ const schema = z.string();
111
+ const result = schema.safeParse(123);
112
+ if (result.success) throw new Error("zod did not reject");
113
+ const err = validationErrorFromZod(result.error);
114
+ const fields = (err.details as { fields: Array<Record<string, unknown>> }).fields;
115
+ expect(fields[0]?.["path"]).toBe("(root)");
116
+ });
117
+
118
+ // Zod 4 restructured issue-specific params. This matrix pins down which
119
+ // param keys survive the bridge for each issue code — if Zod ships a new
120
+ // param name the expected block below goes stale and this test flags it.
121
+ test("zod 4: common issue codes forward their discriminating params", () => {
122
+ const cases: Array<{
123
+ label: string;
124
+ schema: z.ZodType;
125
+ input: unknown;
126
+ expectedCode: string;
127
+ expectedParams: Record<string, unknown>;
128
+ }> = [
129
+ {
130
+ label: "too_small on string min",
131
+ schema: z.string().min(3),
132
+ input: "x",
133
+ expectedCode: "too_small",
134
+ expectedParams: { minimum: 3 },
135
+ },
136
+ {
137
+ label: "too_big on string max",
138
+ schema: z.string().max(2),
139
+ input: "long",
140
+ expectedCode: "too_big",
141
+ expectedParams: { maximum: 2 },
142
+ },
143
+ {
144
+ label: "invalid_type number vs string",
145
+ schema: z.number(),
146
+ input: "nope",
147
+ expectedCode: "invalid_type",
148
+ expectedParams: { expected: "number" },
149
+ },
150
+ {
151
+ label: "invalid_format email",
152
+ schema: z.string().email(),
153
+ input: "not-an-email",
154
+ expectedCode: "invalid_format",
155
+ expectedParams: { format: "email" },
156
+ },
157
+ {
158
+ label: "not_multiple_of",
159
+ schema: z.number().multipleOf(5),
160
+ input: 7,
161
+ expectedCode: "not_multiple_of",
162
+ expectedParams: { divisor: 5 },
163
+ },
164
+ {
165
+ label: "unrecognized_keys on strict object",
166
+ schema: z.strictObject({ a: z.string() }),
167
+ input: { a: "ok", b: "extra" },
168
+ expectedCode: "unrecognized_keys",
169
+ expectedParams: { keys: ["b"] },
170
+ },
171
+ {
172
+ label: "invalid_value on enum",
173
+ schema: z.enum(["a", "b"]),
174
+ input: "c",
175
+ expectedCode: "invalid_value",
176
+ expectedParams: { values: ["a", "b"] },
177
+ },
178
+ ];
179
+
180
+ const drift: string[] = [];
181
+ for (const c of cases) {
182
+ const result = c.schema.safeParse(c.input);
183
+ if (result.success) {
184
+ drift.push(`${c.label}: zod accepted the input unexpectedly`);
185
+ continue;
186
+ }
187
+ const err = validationErrorFromZod(result.error);
188
+ type FieldShape = {
189
+ code?: string;
190
+ params?: Record<string, unknown>;
191
+ };
192
+ const fields = (err.details as { fields: FieldShape[] }).fields;
193
+ const field = fields[0];
194
+
195
+ if (field?.code !== c.expectedCode) {
196
+ drift.push(`${c.label}: code was "${field?.code}", expected "${c.expectedCode}"`);
197
+ }
198
+ const params = field?.params;
199
+ for (const [key, val] of Object.entries(c.expectedParams)) {
200
+ if (params?.[key] === undefined) {
201
+ drift.push(`${c.label}: param "${key}" missing (expected ${JSON.stringify(val)})`);
202
+ }
203
+ }
204
+ }
205
+
206
+ expect(drift).toEqual([]);
207
+ });
208
+ });
209
+
210
+ describe("AccessDeniedError", () => {
211
+ test("defaults to code=access_denied, status 403", () => {
212
+ const err = new AccessDeniedError();
213
+ expect(err.code).toBe("access_denied");
214
+ expect(err.httpStatus).toBe(403);
215
+ expect(err.i18nKey).toBe("errors.access.denied");
216
+ });
217
+ });
218
+
219
+ describe("NotFoundError", () => {
220
+ test("with id: details carries reason + entity + id, message includes id", () => {
221
+ const err = new NotFoundError("order", 42);
222
+ expect(err.httpStatus).toBe(404);
223
+ expect(err.details).toEqual({ reason: "order_not_found", entity: "order", id: "42" });
224
+ expect(err.message).toBe("order 42 not found");
225
+ expect(err.i18nParams).toMatchObject({ entity: "order", id: "42" });
226
+ });
227
+
228
+ test("without id: details carries reason + entity", () => {
229
+ const err = new NotFoundError("handler");
230
+ expect(err.details).toEqual({ reason: "handler_not_found", entity: "handler" });
231
+ expect(err.message).toBe("handler not found");
232
+ expect(err.i18nParams?.["id"]).toBeUndefined();
233
+ });
234
+
235
+ test("camelCase entity name becomes snake_case in the reason", () => {
236
+ const err = new NotFoundError("billingPeriod", 7);
237
+ expect((err.details as { reason: string }).reason).toBe("billing_period_not_found");
238
+ });
239
+
240
+ test("kebab-case entity name becomes snake_case in the reason", () => {
241
+ const err = new NotFoundError("billing-period", 7);
242
+ expect((err.details as { reason: string }).reason).toBe("billing_period_not_found");
243
+ });
244
+ });
245
+
246
+ describe("ConflictError + VersionConflictError", () => {
247
+ test("VersionConflictError narrows code, keeps status 409, exposes version details", () => {
248
+ const err = new VersionConflictError({
249
+ entityId: 42,
250
+ expectedVersion: 3,
251
+ currentVersion: 5,
252
+ });
253
+ expect(err).toBeInstanceOf(ConflictError);
254
+ expect(err.code).toBe("version_conflict");
255
+ expect(err.httpStatus).toBe(409);
256
+ expect(err.details).toMatchObject({ expectedVersion: 3, currentVersion: 5, entityId: 42 });
257
+ });
258
+
259
+ test("generic ConflictError accepts custom details", () => {
260
+ const err = new ConflictError({
261
+ i18nKey: "errors.deleteRestricted",
262
+ details: { reason: "delete_restricted", blockingEntity: "order_item" },
263
+ });
264
+ expect(err.code).toBe("conflict");
265
+ expect(err.httpStatus).toBe(409);
266
+ expect(err.details).toMatchObject({ reason: "delete_restricted" });
267
+ });
268
+ });
269
+
270
+ describe("UnprocessableError", () => {
271
+ test("reason lands in details, code stays unprocessable", () => {
272
+ const err = new UnprocessableError("order.already_cancelled", {
273
+ i18nKey: "orders.errors.alreadyCancelled",
274
+ i18nParams: { orderId: 7 },
275
+ details: { orderId: 7 },
276
+ });
277
+ expect(err.code).toBe("unprocessable");
278
+ expect(err.httpStatus).toBe(422);
279
+ expect(err.details).toMatchObject({ reason: "order.already_cancelled", orderId: 7 });
280
+ expect(err.i18nKey).toBe("orders.errors.alreadyCancelled");
281
+ });
282
+ });
283
+
284
+ describe("InternalError", () => {
285
+ test("defaults to code=internal_error, status 500, no client-facing details", () => {
286
+ const cause = new TypeError("cannot read property 'x'");
287
+ const err = new InternalError({ cause });
288
+ expect(err.code).toBe("internal_error");
289
+ expect(err.httpStatus).toBe(500);
290
+ expect(err.i18nKey).toBe("errors.internal");
291
+ expect(err.cause).toBe(cause);
292
+ });
293
+ });
294
+
295
+ describe("serializeError", () => {
296
+ test("exposes code, i18nKey, message, details, requestId, timestamp", () => {
297
+ const err = new NotFoundError("order", 42);
298
+ const body = serializeError(err, "req-abc");
299
+ expect(body.error).toMatchObject({
300
+ code: "not_found",
301
+ i18nKey: "errors.notFound",
302
+ message: "order 42 not found",
303
+ details: { reason: "order_not_found", entity: "order", id: "42" },
304
+ requestId: "req-abc",
305
+ });
306
+ expect(body.error.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
307
+ });
308
+
309
+ test("InternalError exposes cause snapshot in dev (NODE_ENV !== production)", () => {
310
+ // Dev/test path: the cause is surfaced as causeMessage / causeStack so
311
+ // developers don't have to bolt try/catch onto every handler to learn
312
+ // why a request 500'd. Production strips it (next test).
313
+ const err = new InternalError({ cause: new Error("redis_pool_exhausted") });
314
+ const body = serializeError(err, "req-xyz");
315
+ expect(body.error.code).toBe("internal_error");
316
+ expect(body.error.message).toBe("internal error");
317
+ expect(body.error.details).toMatchObject({ causeMessage: "redis_pool_exhausted" });
318
+ });
319
+
320
+ test("InternalError hides details in production (secret-safety)", () => {
321
+ const original = process.env["NODE_ENV"];
322
+ process.env["NODE_ENV"] = "production";
323
+ try {
324
+ const err = new InternalError({ cause: new Error("leak me if you can") });
325
+ const body = serializeError(err, "req-xyz");
326
+ expect(body.error.code).toBe("internal_error");
327
+ expect(body.error).not.toHaveProperty("details");
328
+ expect(body.error.message).toBe("internal error");
329
+ } finally {
330
+ if (original === undefined) delete process.env["NODE_ENV"];
331
+ else process.env["NODE_ENV"] = original;
332
+ }
333
+ });
334
+
335
+ test("omits requestId field when not provided", () => {
336
+ const body = serializeError(new AccessDeniedError());
337
+ expect(body.error).not.toHaveProperty("requestId");
338
+ });
339
+ });
340
+
341
+ describe("buildErrorLog", () => {
342
+ test("includes cause chain and stack for forensics (not exposed to client)", () => {
343
+ const inner = new Error("redis_connection_refused");
344
+ const wrap = new InternalError({ cause: inner });
345
+ const log = buildErrorLog(wrap);
346
+ expect(log).toMatchObject({
347
+ name: "InternalError",
348
+ code: "internal_error",
349
+ httpStatus: 500,
350
+ cause: { name: "Error", message: "redis_connection_refused" },
351
+ });
352
+ expect(log.stack).toContain("InternalError");
353
+ });
354
+
355
+ test("non-Error causes are represented as NonError entry", () => {
356
+ const wrap = new KumikoErrorStub({ cause: "literal-string-thrown" as unknown as Error });
357
+ const log = buildErrorLog(wrap);
358
+ expect(log.cause).toMatchObject({ name: "NonError", message: "literal-string-thrown" });
359
+ });
360
+ });
361
+
362
+ // Minimal concrete KumikoError used only for the non-Error cause test above —
363
+ // avoids creating a dedicated class in production code just to cover the
364
+ // serializeCause fallback path.
365
+ class KumikoErrorStub extends KumikoError {
366
+ readonly code = "stub";
367
+ readonly httpStatus = 500;
368
+ constructor(opts: { cause: Error }) {
369
+ super({ message: "stub", i18nKey: "stub", cause: opts.cause });
370
+ }
371
+ }
@@ -0,0 +1,109 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { failNotFound, failTransition, failUnprocessable } from "../write-error-info";
3
+
4
+ describe("failNotFound", () => {
5
+ test("baut WriteFailure mit reason=not_found + entity-id-details", () => {
6
+ const f = failNotFound("invoice", "inv-1");
7
+ expect(f.isSuccess).toBe(false);
8
+ expect(f.error.code).toBe("not_found");
9
+ expect(f.error.httpStatus).toBe(404);
10
+ expect(f.error.details).toMatchObject({ reason: "invoice_not_found", id: "inv-1" });
11
+ });
12
+ });
13
+
14
+ describe("failUnprocessable", () => {
15
+ test("baut WriteFailure mit reason + custom-details", () => {
16
+ const f = failUnprocessable("custom_business_rule", { extra: 42 });
17
+ expect(f.error.httpStatus).toBe(422);
18
+ expect(f.error.details).toMatchObject({ reason: "custom_business_rule", extra: 42 });
19
+ });
20
+ });
21
+
22
+ describe("failTransition", () => {
23
+ test("baut WriteFailure mit reason=invalid_transition + from/to/allowed", () => {
24
+ const f = failTransition("draft", "paid", ["sent"]);
25
+ expect(f.isSuccess).toBe(false);
26
+ expect(f.error.code).toBe("unprocessable");
27
+ expect(f.error.httpStatus).toBe(422);
28
+ expect(f.error.i18nKey).toBe("errors.invalidTransition");
29
+ expect(f.error.details).toMatchObject({
30
+ reason: "invalid_transition",
31
+ from: "draft",
32
+ to: "paid",
33
+ allowed: ["sent"],
34
+ });
35
+ });
36
+
37
+ test("baut sichtbare message mit allowed-Liste", () => {
38
+ const f = failTransition("draft", "paid", ["sent", "cancelled"]);
39
+ const details = f.error.details as { message: string };
40
+ expect(details.message).toContain('"draft" → "paid"');
41
+ expect(details.message).toContain("sent, cancelled");
42
+ });
43
+
44
+ test("leeres allowed → message zeigt 'none' (Terminal-State)", () => {
45
+ const f = failTransition("paid", "draft", []);
46
+ const details = f.error.details as { message: string; allowed: readonly string[] };
47
+ expect(details.allowed).toEqual([]);
48
+ expect(details.message).toContain("none");
49
+ });
50
+ });
51
+
52
+ // toWriteErrorInfo dev-cause-snapshot pinnt: ein InternalError mit
53
+ // cause überlebt den Roundtrip durch WriteErrorInfo (war vorher kein
54
+ // Cause-Feld → reraise → "internal error" ohne Diagnose). Pfad ist
55
+ // NODE_ENV-conditional, deshalb lokal toggeln und nach Test wieder
56
+ // restoren — Cross-Test-Pollution wäre teuer (andere Suites pinnen
57
+ // Production-Pfad).
58
+ describe("toWriteErrorInfo — dev cause-snapshot", () => {
59
+ test("InternalError mit cause exposed cause-Snapshot in details (dev)", async () => {
60
+ const { toWriteErrorInfo } = await import("../write-error-info");
61
+ const { InternalError } = await import("../classes");
62
+ const previous = process.env["NODE_ENV"];
63
+ process.env["NODE_ENV"] = "development";
64
+ try {
65
+ const cause = new TypeError("nope");
66
+ const err = new InternalError({ cause });
67
+ const info = toWriteErrorInfo(err);
68
+ const details = info.details as
69
+ | { causeName?: string; causeMessage?: string; causeStack?: string }
70
+ | undefined;
71
+ expect(details?.causeName).toBe("TypeError");
72
+ expect(details?.causeMessage).toBe("nope");
73
+ expect(details?.causeStack).toContain("TypeError");
74
+ } finally {
75
+ process.env["NODE_ENV"] = previous;
76
+ }
77
+ });
78
+
79
+ test("Production: InternalError lässt details undefined (kein Stack-Leak)", async () => {
80
+ const { toWriteErrorInfo } = await import("../write-error-info");
81
+ const { InternalError } = await import("../classes");
82
+ const previous = process.env["NODE_ENV"];
83
+ process.env["NODE_ENV"] = "production";
84
+ try {
85
+ const err = new InternalError({ cause: new TypeError("nope") });
86
+ const info = toWriteErrorInfo(err);
87
+ expect(info.details).toBeUndefined();
88
+ } finally {
89
+ process.env["NODE_ENV"] = previous;
90
+ }
91
+ });
92
+
93
+ test("InternalError MIT bereits gesetztem details → Author-details gewinnt (kein Overwrite)", async () => {
94
+ const { toWriteErrorInfo } = await import("../write-error-info");
95
+ const { InternalError } = await import("../classes");
96
+ const previous = process.env["NODE_ENV"];
97
+ process.env["NODE_ENV"] = "development";
98
+ try {
99
+ const err = new InternalError({
100
+ cause: new Error("hidden"),
101
+ details: { explicit: "from author" },
102
+ });
103
+ const info = toWriteErrorInfo(err);
104
+ expect(info.details).toEqual({ explicit: "from author" });
105
+ } finally {
106
+ process.env["NODE_ENV"] = previous;
107
+ }
108
+ });
109
+ });