@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,249 @@
1
+ import { type ErrorOpts, KumikoError } from "./kumiko-error";
2
+
3
+ // Per-field validation issue. Shared shape between Zod-derived and
4
+ // hook-derived validation errors so the client sees one list.
5
+ export type ValidationFieldIssue = {
6
+ readonly path: string;
7
+ readonly code: string;
8
+ readonly i18nKey: string;
9
+ readonly params?: Readonly<Record<string, unknown>>;
10
+ };
11
+
12
+ export type ValidationDetails = {
13
+ readonly fields: readonly ValidationFieldIssue[];
14
+ };
15
+
16
+ export class ValidationError extends KumikoError {
17
+ readonly code = "validation_error";
18
+ readonly httpStatus = 400;
19
+
20
+ constructor(details: ValidationDetails, opts?: Pick<ErrorOpts, "i18nKey" | "cause">) {
21
+ // The wire `message` stays a short human-readable fallback — Zod's own
22
+ // `error.message` is a multi-line JSON blob that would look awful in any
23
+ // UI. Per-field details belong in `details.fields` (structured, i18nable);
24
+ // the full ZodError is preserved in `cause` for forensics in the log.
25
+ super({
26
+ message: "Validation failed",
27
+ i18nKey: opts?.i18nKey ?? "errors.validation.failed",
28
+ details,
29
+ ...(opts?.cause && { cause: opts.cause }),
30
+ });
31
+ }
32
+ }
33
+
34
+ // Raised by the dispatcher when a handler belongs to a feature that is
35
+ // globally disabled. Separate from AccessDenied so clients can distinguish
36
+ // "this feature is off" (retry pointless until ops flips it on) from
37
+ // "you don't have permission" (potentially retryable with a different user).
38
+ export type FeatureDisabledDetails = {
39
+ readonly reason: "feature_disabled";
40
+ readonly feature: string;
41
+ readonly handler: string;
42
+ };
43
+
44
+ export class FeatureDisabledError extends KumikoError {
45
+ readonly code = "feature_disabled";
46
+ readonly httpStatus = 403;
47
+
48
+ constructor(feature: string, handler: string, opts?: Pick<ErrorOpts, "cause">) {
49
+ super({
50
+ message: `feature ${feature} is disabled`,
51
+ i18nKey: "errors.feature.disabled",
52
+ i18nParams: { feature },
53
+ details: { reason: "feature_disabled", feature, handler } satisfies FeatureDisabledDetails,
54
+ ...(opts?.cause && { cause: opts.cause }),
55
+ });
56
+ }
57
+ }
58
+
59
+ export class AccessDeniedError extends KumikoError {
60
+ readonly code = "access_denied";
61
+ readonly httpStatus = 403;
62
+
63
+ constructor(opts?: Pick<ErrorOpts, "message" | "i18nKey" | "i18nParams" | "details" | "cause">) {
64
+ super({
65
+ message: opts?.message ?? "access denied",
66
+ i18nKey: opts?.i18nKey ?? "errors.access.denied",
67
+ ...(opts?.i18nParams && { i18nParams: opts.i18nParams }),
68
+ ...(opts?.details !== undefined && { details: opts.details }),
69
+ ...(opts?.cause && { cause: opts.cause }),
70
+ });
71
+ }
72
+ }
73
+
74
+ export type NotFoundDetails = {
75
+ readonly entity: string;
76
+ readonly id?: string;
77
+ };
78
+
79
+ export class NotFoundError extends KumikoError {
80
+ readonly code = "not_found";
81
+ readonly httpStatus = 404;
82
+
83
+ constructor(
84
+ entity: string,
85
+ id?: string | number,
86
+ opts?: Pick<ErrorOpts, "i18nKey" | "i18nParams" | "cause">,
87
+ ) {
88
+ const idStr = id !== undefined ? String(id) : undefined;
89
+ // The reason string follows `<snake_entity>_not_found` — keeps a stable,
90
+ // client-friendly tag that survives wire serialization even if the entity
91
+ // name is later renamed for display purposes.
92
+ const reason = `${toSnake(entity)}_not_found`;
93
+ const details: NotFoundDetails & { reason: string } = { reason, entity, id: idStr };
94
+ super({
95
+ message: idStr !== undefined ? `${entity} ${idStr} not found` : `${entity} not found`,
96
+ i18nKey: opts?.i18nKey ?? "errors.notFound",
97
+ i18nParams: { entity, id: idStr, ...opts?.i18nParams },
98
+ details,
99
+ cause: opts?.cause,
100
+ });
101
+ }
102
+ }
103
+
104
+ // Accepts camelCase OR kebab-case entity names and produces snake_case for
105
+ // the reason tag. New code uses kebab; legacy camelCase still flows through.
106
+ function toSnake(s: string): string {
107
+ return s
108
+ .replace(/-/g, "_")
109
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
110
+ .toLowerCase();
111
+ }
112
+
113
+ // Generic 409. Features that need a narrower shape should subclass (see
114
+ // VersionConflictError) — this way the HTTP layer stays uniform while callers
115
+ // can still instanceof on the concrete subtype in handlers.
116
+ export class ConflictError extends KumikoError {
117
+ // Widened to `string` so subclasses (VersionConflictError) can refine the
118
+ // value without TS blocking the override. The base class still enforces
119
+ // "some code is set"; concrete classes pick their own literal.
120
+ readonly code: string = "conflict";
121
+ readonly httpStatus = 409;
122
+
123
+ constructor(opts?: Pick<ErrorOpts, "message" | "i18nKey" | "i18nParams" | "details" | "cause">) {
124
+ super({
125
+ message: opts?.message ?? "conflict",
126
+ i18nKey: opts?.i18nKey ?? "errors.conflict",
127
+ ...(opts?.i18nParams && { i18nParams: opts.i18nParams }),
128
+ ...(opts?.details !== undefined && { details: opts.details }),
129
+ ...(opts?.cause && { cause: opts.cause }),
130
+ });
131
+ }
132
+ }
133
+
134
+ export type VersionConflictDetails = {
135
+ readonly entityId: number | string;
136
+ readonly expectedVersion: number;
137
+ readonly currentVersion: number;
138
+ };
139
+
140
+ export class VersionConflictError extends ConflictError {
141
+ override readonly code: string = "version_conflict";
142
+
143
+ constructor(details: VersionConflictDetails, opts?: Pick<ErrorOpts, "i18nKey" | "cause">) {
144
+ super({
145
+ message: `version conflict for entity ${details.entityId}`,
146
+ i18nKey: opts?.i18nKey ?? "errors.versionConflict",
147
+ details,
148
+ ...(opts?.cause && { cause: opts.cause }),
149
+ });
150
+ }
151
+ }
152
+
153
+ // Entity-level unique-index violation. Distinct from VersionConflictError:
154
+ // version_conflict means "two writers raced on the events_aggregate_version
155
+ // _uq index" (optimistic-concurrency); unique_violation means "you tried to
156
+ // insert a row that violates an app-declared unique-index" (e.g. duplicate
157
+ // email on a User entity, duplicate (tenantId, slug) on Article). Same 409
158
+ // because both are conflicts the client can resolve by retrying with
159
+ // different data, but distinct codes so UI can show the right message.
160
+ //
161
+ // constraintName comes from the PG error and is the *physical* DB constraint
162
+ // (e.g. "users_tenant_email_uniq"). Apps that want to map it to a friendly
163
+ // field name should do so in their own error-handler, not here.
164
+ export type UniqueViolationDetails = {
165
+ readonly entityName: string;
166
+ readonly constraintName?: string;
167
+ };
168
+
169
+ export class UniqueViolationError extends ConflictError {
170
+ override readonly code: string = "unique_violation";
171
+
172
+ constructor(details: UniqueViolationDetails, opts?: Pick<ErrorOpts, "i18nKey" | "cause">) {
173
+ super({
174
+ message: `unique constraint violated on entity ${details.entityName}${
175
+ details.constraintName ? ` (${details.constraintName})` : ""
176
+ }`,
177
+ i18nKey: opts?.i18nKey ?? "errors.uniqueViolation",
178
+ details,
179
+ ...(opts?.cause && { cause: opts.cause }),
180
+ });
181
+ }
182
+ }
183
+
184
+ // Business-rule violation. The human-readable reason lives in details.reason
185
+ // so the client can key off it without overloading the top-level code.
186
+ export type UnprocessableOpts = Pick<ErrorOpts, "i18nKey" | "i18nParams" | "cause"> & {
187
+ readonly details?: Readonly<Record<string, unknown>>;
188
+ };
189
+
190
+ export class UnprocessableError extends KumikoError {
191
+ readonly code = "unprocessable";
192
+ readonly httpStatus = 422;
193
+
194
+ constructor(reason: string, opts?: UnprocessableOpts) {
195
+ super({
196
+ message: `unprocessable: ${reason}`,
197
+ i18nKey: opts?.i18nKey ?? "errors.unprocessable",
198
+ ...(opts?.i18nParams && { i18nParams: opts.i18nParams }),
199
+ details: { reason, ...opts?.details },
200
+ ...(opts?.cause && { cause: opts.cause }),
201
+ });
202
+ }
203
+ }
204
+
205
+ // Auto-wrap target for unexpected throws. Never exposes .details to the client
206
+ // — the serializer drops it. Stack + cause live in the log only.
207
+ export class InternalError extends KumikoError {
208
+ readonly code = "internal_error";
209
+ readonly httpStatus = 500;
210
+
211
+ constructor(opts?: Pick<ErrorOpts, "message" | "i18nKey" | "i18nParams" | "details" | "cause">) {
212
+ super({
213
+ message: opts?.message ?? "internal error",
214
+ i18nKey: opts?.i18nKey ?? "errors.internal",
215
+ ...(opts?.i18nParams !== undefined && { i18nParams: opts.i18nParams }),
216
+ ...(opts?.details !== undefined && { details: opts.details }),
217
+ ...(opts?.cause !== undefined && { cause: opts.cause }),
218
+ });
219
+ }
220
+ }
221
+
222
+ // Rate-limit hit. The bucket details (limit, window, current state) live
223
+ // in `details` so a client can show "try again in N seconds" without a
224
+ // second request. Headers `Retry-After`, `X-RateLimit-*` are filled in
225
+ // by the HTTP layer from these same fields.
226
+ export type RateLimitDetails = {
227
+ readonly bucket: string;
228
+ readonly limit: number;
229
+ readonly windowSeconds: number;
230
+ readonly remaining: number;
231
+ readonly retryAfterSeconds: number;
232
+ readonly resetAt: string;
233
+ };
234
+
235
+ export class RateLimitError extends KumikoError {
236
+ readonly code = "rate_limited";
237
+ readonly httpStatus = 429;
238
+ readonly details: RateLimitDetails;
239
+
240
+ constructor(details: RateLimitDetails, opts?: Pick<ErrorOpts, "i18nKey" | "cause">) {
241
+ super({
242
+ message: `rate limited: ${details.bucket} (limit ${details.limit} per ${details.windowSeconds}s)`,
243
+ i18nKey: opts?.i18nKey ?? "errors.rate_limited",
244
+ details,
245
+ ...(opts?.cause && { cause: opts.cause }),
246
+ });
247
+ this.details = details;
248
+ }
249
+ }
@@ -0,0 +1,83 @@
1
+ # i18n-Texte für FrameworkReasons (deutsch).
2
+ #
3
+ # Schema pro Reason-Key (snake_case, identisch mit Wert in reasons.ts
4
+ # FrameworkReasons):
5
+ #
6
+ # <reason_key>:
7
+ # endUser: |
8
+ # Was der End-User in der App-UI sieht.
9
+ # Verständliche Sprache, Handlungs-Aufforderung wenn anwendbar.
10
+ # developer: |
11
+ # Technischer Hintergrund + Hinweise zum Konfigurieren / Verhindern.
12
+ # Für App-Owner und Designer-Nutzer.
13
+
14
+ stale_state:
15
+ endUser: |
16
+ Jemand anderes hat dieses Element zwischenzeitlich geändert.
17
+ Bitte Seite neu laden und nochmal speichern.
18
+ developer: |
19
+ `ConflictError` tritt auf wenn ein atomic UPDATE den Optimistic-Locking-
20
+ Race verloren hat — ein anderer Writer hat die Row zwischen Snapshot
21
+ und WHERE-Clause bewegt.
22
+
23
+ Standard-Verhalten im Client-SDK: Toast mit "Bitte neu laden" anzeigen
24
+ und Entity neu fetchen.
25
+
26
+ Wenn du Conflict-Handling pro Entity anpassen willst, siehe
27
+ [Optimistic-Locking-Konfiguration](/de/architecture/optimistic-locking/).
28
+
29
+ invalid_transition:
30
+ endUser: |
31
+ Diese Aktion ist im aktuellen Status nicht möglich.
32
+ Der Datensatz muss sich in einem bestimmten Zustand befinden,
33
+ damit dieser Schritt erlaubt ist.
34
+ developer: |
35
+ `UnprocessableError` beim `guardTransition`: ein State-Machine-Übergang
36
+ wurde abgelehnt weil der aktuelle Zustand nicht zu den erlaubten
37
+ Quell-Zuständen gehört.
38
+
39
+ `details.from`, `details.to` und `details.validTargets` enthalten die
40
+ Diagnose. Definiere erlaubte Übergänge in
41
+ [`r.entity({ stateMachine: ... })`](/de/architecture/state-machine/),
42
+ oder prüfe vor dem Aufruf den aktuellen Zustand.
43
+
44
+ field_access_denied:
45
+ endUser: |
46
+ Du darfst dieses Feld nicht ändern.
47
+ Wende dich an einen Administrator wenn du diese Berechtigung brauchst.
48
+ developer: |
49
+ `AccessDeniedError` beim Field-Level-Write-Check des Dispatchers: ein
50
+ Schreibversuch auf ein Feld wurde abgelehnt weil die aktuelle Rolle
51
+ keinen Zugriff hat.
52
+
53
+ `details.field` und `details.handler` enthalten die Diagnose.
54
+ Konfiguriere Field-Access in der Entity-Definition
55
+ (siehe [Permissions](/de/architecture/permissions/)) oder fordere die
56
+ nötige Rolle an.
57
+
58
+ delete_restricted:
59
+ endUser: |
60
+ Dieser Eintrag kann nicht gelöscht werden weil andere Datensätze noch
61
+ darauf verweisen.
62
+ Lösche zuerst die abhängigen Einträge.
63
+ developer: |
64
+ `ConflictError` beim Cascade-Delete-Guard: das Löschen wurde verhindert
65
+ weil dependent Rows existieren.
66
+
67
+ `details.blockingEntity`, `details.entity` und `details.entityId` zeigen
68
+ welche Relation blockiert. Setze `relation.onDelete` auf `"cascade"`
69
+ oder `"setNull"` wenn automatisches Aufräumen gewünscht ist, oder lösche
70
+ die abhängigen Rows zuerst.
71
+
72
+ feature_disabled:
73
+ endUser: |
74
+ Diese Funktion ist aktuell nicht verfügbar.
75
+ Versuche es später noch einmal oder kontaktiere den Administrator.
76
+ developer: |
77
+ `FeatureDisabledError`: das Feature des aufgerufenen Handlers wurde
78
+ global deaktiviert. Distinct von `access_denied` — Clients sollten
79
+ "Funktion gerade nicht verfügbar" zeigen, nicht "keine Berechtigung".
80
+
81
+ `details.feature` und `details.handler` zeigen welches Feature/Handler.
82
+ Aktiviere das Feature via Feature-Toggle oder Routing-Config (siehe
83
+ [Feature-Toggles](/de/features/feature-toggles/)).
@@ -0,0 +1,80 @@
1
+ # i18n texts for FrameworkReasons (english).
2
+ #
3
+ # Schema per reason key (snake_case, matches the value in
4
+ # reasons.ts FrameworkReasons):
5
+ #
6
+ # <reason_key>:
7
+ # endUser: |
8
+ # What the end-user sees in the app UI.
9
+ # Plain language, call-to-action where applicable.
10
+ # developer: |
11
+ # Technical background + hints on how to configure / prevent.
12
+ # For app owners and designer users.
13
+
14
+ stale_state:
15
+ endUser: |
16
+ Someone else modified this item in the meantime.
17
+ Please reload the page and try saving again.
18
+ developer: |
19
+ `ConflictError` fires when an atomic UPDATE lost the optimistic-
20
+ locking race — another writer moved the row between our snapshot and
21
+ our WHERE clause.
22
+
23
+ Default client SDK behavior: toast a "please reload" message and
24
+ re-fetch the entity.
25
+
26
+ To customize conflict handling per entity, see
27
+ [optimistic locking configuration](/en/architecture/optimistic-locking/).
28
+
29
+ invalid_transition:
30
+ endUser: |
31
+ This action isn't allowed in the current state.
32
+ The record needs to be in a specific status before this step can happen.
33
+ developer: |
34
+ `UnprocessableError` from `guardTransition`: a state-machine transition
35
+ was rejected because the current state isn't in the allowed source
36
+ states.
37
+
38
+ `details.from`, `details.to` and `details.validTargets` carry the
39
+ diagnostics. Define allowed transitions in
40
+ [`r.entity({ stateMachine: ... })`](/en/architecture/state-machine/),
41
+ or check the current state before calling.
42
+
43
+ field_access_denied:
44
+ endUser: |
45
+ You don't have permission to edit this field.
46
+ Please contact an administrator if you need this access.
47
+ developer: |
48
+ `AccessDeniedError` from the dispatcher's field-level write check: a
49
+ write to a field was rejected because the current role lacks access.
50
+
51
+ `details.field` and `details.handler` contain the diagnostics.
52
+ Configure field-level access in the entity definition (see
53
+ [Permissions](/en/architecture/permissions/)) or request the required
54
+ role.
55
+
56
+ delete_restricted:
57
+ endUser: |
58
+ This entry can't be deleted because other records still reference it.
59
+ Delete the dependent entries first.
60
+ developer: |
61
+ `ConflictError` from the cascade-delete guard: the delete was prevented
62
+ because dependent rows exist.
63
+
64
+ `details.blockingEntity`, `details.entity` and `details.entityId` show
65
+ which relation is blocking. Set `relation.onDelete` to `"cascade"` or
66
+ `"setNull"` if automatic cleanup is desired, or delete the dependent
67
+ rows first.
68
+
69
+ feature_disabled:
70
+ endUser: |
71
+ This feature is currently unavailable.
72
+ Please try again later or contact an administrator.
73
+ developer: |
74
+ `FeatureDisabledError`: the feature owning the called handler is
75
+ globally disabled. Distinct from `access_denied` — clients should show
76
+ "feature currently unavailable" rather than "no permission".
77
+
78
+ `details.feature` and `details.handler` indicate which feature and
79
+ handler. Enable the feature via feature toggle or routing config (see
80
+ [Feature Toggles](/en/features/feature-toggles/)).
@@ -0,0 +1,41 @@
1
+ export type {
2
+ FeatureDisabledDetails,
3
+ NotFoundDetails,
4
+ RateLimitDetails,
5
+ UniqueViolationDetails,
6
+ UnprocessableOpts,
7
+ ValidationDetails,
8
+ ValidationFieldIssue,
9
+ VersionConflictDetails,
10
+ } from "./classes";
11
+ export {
12
+ AccessDeniedError,
13
+ ConflictError,
14
+ FeatureDisabledError,
15
+ InternalError,
16
+ NotFoundError,
17
+ RateLimitError,
18
+ UniqueViolationError,
19
+ UnprocessableError,
20
+ ValidationError,
21
+ VersionConflictError,
22
+ } from "./classes";
23
+ export type { ErrorCtorInput, ErrorOpts } from "./kumiko-error";
24
+ export { isKumikoError, KumikoError } from "./kumiko-error";
25
+ export type { FrameworkReason } from "./reasons";
26
+ export { FrameworkReasons } from "./reasons";
27
+ export type { ErrorLogEntry, ErrorResponseBody } from "./serialize";
28
+ export { buildErrorLog, serializeError } from "./serialize";
29
+ export type { InvalidTransitionDetails } from "./transition-details";
30
+ export { buildInvalidTransitionDetails } from "./transition-details";
31
+ export type { WriteErrorInfo, WriteFailure } from "./write-error-info";
32
+
33
+ export {
34
+ failNotFound,
35
+ failTransition,
36
+ failUnprocessable,
37
+ reraiseAsKumikoError,
38
+ toWriteErrorInfo,
39
+ writeFailure,
40
+ } from "./write-error-info";
41
+ export { validationErrorFromZod } from "./zod-bridge";
@@ -0,0 +1,67 @@
1
+ // Base class for every error the framework recognizes as a contract.
2
+ // Anything that isn't a KumikoError is treated as an unexpected crash and
3
+ // auto-wrapped into InternalError by the dispatcher — so the HTTP layer never
4
+ // has to guess the status or the client-facing shape.
5
+
6
+ export type ErrorOpts = {
7
+ readonly message?: string;
8
+ readonly i18nKey?: string;
9
+ readonly i18nParams?: Readonly<Record<string, unknown>>;
10
+ readonly details?: unknown;
11
+ readonly cause?: Error;
12
+ };
13
+
14
+ export type ErrorCtorInput = {
15
+ readonly message: string;
16
+ readonly i18nKey: string;
17
+ readonly i18nParams?: Readonly<Record<string, unknown>>;
18
+ readonly details?: unknown;
19
+ readonly cause?: Error;
20
+ };
21
+
22
+ // Default-Doku-URL für Self-Service-Errors. Kann via env-var
23
+ // `KUMIKO_DOCS_URL` überschrieben werden — z.B. für Self-Hosted-Kunden
24
+ // die ihre eigene Doku-Instanz hosten.
25
+ const DEFAULT_DOCS_BASE_URL = "https://docs.kumiko.so";
26
+
27
+ function docsBaseUrl(): string {
28
+ return process.env["KUMIKO_DOCS_URL"] ?? DEFAULT_DOCS_BASE_URL;
29
+ }
30
+
31
+ export abstract class KumikoError extends Error {
32
+ abstract readonly code: string;
33
+ abstract readonly httpStatus: number;
34
+ readonly i18nKey: string;
35
+ readonly i18nParams: Readonly<Record<string, unknown>> | undefined;
36
+ readonly details: unknown;
37
+
38
+ constructor(input: ErrorCtorInput) {
39
+ super(input.message, input.cause ? { cause: input.cause } : undefined);
40
+ this.name = new.target.name;
41
+ this.i18nKey = input.i18nKey;
42
+ this.i18nParams = input.i18nParams;
43
+ this.details = input.details;
44
+ }
45
+
46
+ // Doku-URL für Self-Service. Pro-Reason-Slug aus `details.reason` wenn
47
+ // vorhanden (z.B. ConflictError → "stale_state"), sonst Fallback auf
48
+ // den Error-Code (z.B. "not_found", "validation_error"). Default-Renderer
49
+ // im Client zeigt "Mehr erfahren →" Link auf diese URL.
50
+ get docsUrl(): string {
51
+ return `${docsBaseUrl()}/errors/${this.reasonSlug}`;
52
+ }
53
+
54
+ private get reasonSlug(): string {
55
+ if (this.details && typeof this.details === "object") {
56
+ // @cast-boundary error-details — KumikoError.details ist per-error
57
+ // typed, hier reines reflection-shape für reasonSlug-Lookup.
58
+ const r = (this.details as Record<string, unknown>)["reason"];
59
+ if (typeof r === "string") return r;
60
+ }
61
+ return this.code;
62
+ }
63
+ }
64
+
65
+ export function isKumikoError(e: unknown): e is KumikoError {
66
+ return e instanceof KumikoError;
67
+ }
@@ -0,0 +1,36 @@
1
+ // Centralized registry of the reason codes the framework itself surfaces via
2
+ // `details.reason`. Declaring them here keeps them typed + greppable, and
3
+ // gives features a single source of truth when they need to branch on the
4
+ // framework-level reason (e.g. "is this a stale-state race? retry once").
5
+ //
6
+ // Features add their own local Reasons objects (see `samples/errors/` or
7
+ // `packages/bundled-features/src/tenant/constants.ts` → TenantErrors). The
8
+ // framework deliberately does NOT enforce uniqueness across features — two
9
+ // features may use the same reason string if the semantics match. The
10
+ // convention: snake_case, no spaces, no feature prefix for framework reasons.
11
+
12
+ export const FrameworkReasons = {
13
+ // ConflictError: atomic UPDATE lost the race (another writer moved the row
14
+ // between our snapshot and our WHERE clause). Client SDK default: toast +
15
+ // re-fetch.
16
+ staleState: "stale_state",
17
+
18
+ // UnprocessableError: guardTransition rejected a state-machine transition.
19
+ // Details carry `from`, `to`, and `validTargets` for debugging.
20
+ invalidTransition: "invalid_transition",
21
+
22
+ // AccessDeniedError: dispatcher's field-level write check blocked a field.
23
+ // Details carry `field` and `handler`.
24
+ fieldAccessDenied: "field_access_denied",
25
+
26
+ // ConflictError: cascade-delete guard refused because dependent rows exist.
27
+ // Details carry `blockingEntity`, `entity`, `entityId`.
28
+ deleteRestricted: "delete_restricted",
29
+
30
+ // FeatureDisabledError: handler's owning feature is globally disabled.
31
+ // Distinct from access-denied — clients should surface "feature X is
32
+ // currently unavailable" not "you don't have permission".
33
+ featureDisabled: "feature_disabled",
34
+ } as const;
35
+
36
+ export type FrameworkReason = (typeof FrameworkReasons)[keyof typeof FrameworkReasons];