@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,337 @@
1
+ // Ownership rules — the declarative bridge between Claims and Access.
2
+ //
3
+ // Every ownership rule answers the same question: "May this user see / write
4
+ // this row (or field in this row)?". The rule is evaluated per-role: a user
5
+ // with multiple roles passes if at least one of their roles has a rule that
6
+ // accepts the row. For writes, the check is stricter — see below.
7
+ //
8
+ // Rule forms:
9
+ //
10
+ // "all" → any user with this role passes
11
+ // { from: "user:id", column: "..." } → row[column] === user.id
12
+ // { from: "claim:<featureQn>",
13
+ // column?: "..." } → row[column ?? claim.shortName] === user.claims[claim.qn]
14
+ // (string[] claim → inArray)
15
+ // { where: (user, table) => SQL } → escape hatch, arbitrary Drizzle predicate
16
+ //
17
+ // Construction: use the `from(ref, column?)` helper. It returns a FromRule
18
+ // ready to drop into an access map.
19
+
20
+ import { eq, inArray, or, type SQL, sql } from "drizzle-orm";
21
+ import type { SessionUser } from "./types";
22
+
23
+ // Reference spec supported by `from()`:
24
+ // "user:id" → user.id
25
+ // "user:tenantId" → user.tenantId (rarely needed — TenantDb scopes anyway)
26
+ // "claim:<featureName>:<key>" → user.claims["<featureName>:<key>"]
27
+ //
28
+ // The string form is keyed so the framework can look up the referenced
29
+ // Registry entry at boot (Claim-QN exists? Column type compatible?). A typed
30
+ // object form would force features to import each other's handles — the
31
+ // whole point of H.2's unified path is string-based references, no imports.
32
+ export type OwnershipRef = string;
33
+
34
+ // Resolved during `from()` — the parser eagerly splits the prefix so the
35
+ // runtime evaluator avoids string-parsing on every row. `kind` drives the
36
+ // evaluator branch; the rest is the resolved metadata.
37
+ export type FromRuleKind = "user" | "claim";
38
+
39
+ export type FromRule = {
40
+ readonly kind: "from";
41
+ readonly refKind: FromRuleKind;
42
+ // For "user:id" → "id"; for "user:tenantId" → "tenantId".
43
+ // For "claim:<featureName>:<key>" → "<featureName>:<key>" (the full QN,
44
+ // which is exactly the key under which the JWT stores the value).
45
+ readonly refPath: string;
46
+ // Row-column to match against. For claim rules defaults to the claim's
47
+ // shortName (second segment of the claim QN). For user-rules the column
48
+ // is always explicit.
49
+ readonly column: string;
50
+ };
51
+
52
+ export type WhereRule<TTable = unknown> = {
53
+ readonly kind: "where";
54
+ readonly where: (user: SessionUser, table: TTable) => SQL;
55
+ };
56
+
57
+ // "all" collapses to a primitive so map authors can write `Admin: "all"`
58
+ // without importing a helper.
59
+ export type OwnershipRule = "all" | FromRule | WhereRule;
60
+
61
+ // Per-role map: every key is a role name, value is the rule that role
62
+ // satisfies to pass the access check.
63
+ export type OwnershipMap = Readonly<Record<string, OwnershipRule>>;
64
+
65
+ // Parse an OwnershipRef into kind + resolved path + default column.
66
+ // Throws on malformed input so the error surfaces at `from()`-call-site (in
67
+ // the feature definition), not at request time.
68
+ export function from(ref: OwnershipRef, column?: string): FromRule {
69
+ const firstColon = ref.indexOf(":");
70
+ if (firstColon < 0) {
71
+ throw new Error(
72
+ `from("${ref}"): expected "user:<field>" or "claim:<featureName>:<key>" — no colon found.`,
73
+ );
74
+ }
75
+ const prefix = ref.slice(0, firstColon);
76
+ const rest = ref.slice(firstColon + 1);
77
+
78
+ if (prefix === "user") {
79
+ // "user:id" or "user:tenantId". The rest is the user-property; the
80
+ // column on the row must be given explicitly (a user.id is rarely
81
+ // named `id` on a child table — usually `ownerId`, `assigneeId`, ...).
82
+ if (!column) {
83
+ throw new Error(
84
+ `from("${ref}"): user-refs require an explicit column name — e.g. from("user:id", "assigneeId").`,
85
+ );
86
+ }
87
+ if (rest !== "id" && rest !== "tenantId") {
88
+ throw new Error(
89
+ `from("${ref}"): user-ref supports only "user:id" or "user:tenantId" (got "user:${rest}").`,
90
+ );
91
+ }
92
+ return { kind: "from", refKind: "user", refPath: rest, column };
93
+ }
94
+
95
+ if (prefix === "claim") {
96
+ // "claim:<feature>:<key>" — rest is the 2-segment claim QN.
97
+ if (!rest.includes(":")) {
98
+ throw new Error(
99
+ `from("${ref}"): claim-ref must be "claim:<featureName>:<shortName>" (got "claim:${rest}").`,
100
+ );
101
+ }
102
+ // Default column = claim shortName (second segment).
103
+ const defaultColumn = rest.slice(rest.indexOf(":") + 1);
104
+ return {
105
+ kind: "from",
106
+ refKind: "claim",
107
+ refPath: rest, // full QN, matches the key in user.claims
108
+ column: column ?? defaultColumn,
109
+ };
110
+ }
111
+
112
+ throw new Error(
113
+ `from("${ref}"): unsupported ref prefix "${prefix}". Supported: "user", "claim".`,
114
+ );
115
+ }
116
+
117
+ // Evaluate an ownership rule against a concrete row (plain data, no Drizzle).
118
+ // Used by field-level filters on query responses and by field-level
119
+ // write-checks. Entity-level rules that want SQL predicates go through
120
+ // buildOwnershipClause() (separate path, since that produces Drizzle SQL).
121
+ //
122
+ // Null/undefined claim values evaluate to `false` (no match) — safer than
123
+ // letting them match rows where the column happens to be null.
124
+ export function matchesRule(
125
+ rule: OwnershipRule,
126
+ user: SessionUser,
127
+ row: Readonly<Record<string, unknown>>,
128
+ ): boolean {
129
+ if (rule === "all") return true;
130
+ if (rule.kind === "where") {
131
+ // `where` rules produce Drizzle SQL for the DB-side filter. They don't
132
+ // have a straightforward in-memory evaluator — the feature author owns
133
+ // the semantics. Field-level filters can't use `{ where }` rules; the
134
+ // boot-validator rejects them with a clear error at registration time.
135
+ throw new Error(
136
+ "where-rules can only be evaluated at the SQL layer; boot-validator should reject them on field-level access.",
137
+ );
138
+ }
139
+
140
+ // FromRule — resolve the user-side value, compare to the row's column.
141
+ const userValue = resolveUserValue(rule, user);
142
+ if (userValue === undefined) return false;
143
+
144
+ const rowValue = row[rule.column];
145
+ if (rowValue === undefined || rowValue === null) return false;
146
+
147
+ // Array claim → membership check; scalar claim → equality.
148
+ if (Array.isArray(userValue)) {
149
+ return userValue.includes(rowValue);
150
+ }
151
+ return userValue === rowValue;
152
+ }
153
+
154
+ function resolveUserValue(rule: FromRule, user: SessionUser): unknown {
155
+ if (rule.refKind === "user") {
156
+ if (rule.refPath === "id") return user.id;
157
+ return user.tenantId;
158
+ }
159
+ // claim refPath is the full QN ("feature:shortName") — direct key lookup.
160
+ return user.claims?.[rule.refPath];
161
+ }
162
+
163
+ // Multi-role-atomic passer for field-level READ. The caller supplies the
164
+ // user, the access-map for this field, and the concrete row. Returns true
165
+ // if AT LEAST one of the user's roles is in the map and its rule matches
166
+ // the row. Missing roles skip, "all" always passes.
167
+ export function userCanReadFieldRow(
168
+ user: SessionUser,
169
+ accessMap: OwnershipMap | undefined,
170
+ row: Readonly<Record<string, unknown>>,
171
+ ): boolean {
172
+ if (!accessMap || Object.keys(accessMap).length === 0) return true; // public
173
+ for (const role of user.roles) {
174
+ const rule = accessMap[role];
175
+ if (!rule) continue;
176
+ if (matchesRule(rule, user, row)) return true;
177
+ }
178
+ return false;
179
+ }
180
+
181
+ // Multi-role-atomic check for field-level WRITE with Straddle-prevention.
182
+ // A user passes iff exactly one of their roles has a rule that accepts
183
+ // BOTH the old and the new row. OR-ing over (any-role passes old) and
184
+ // (any-role passes new) is wrong — a user with two roles could split the
185
+ // check: role A validates the old, role B validates the new, yielding
186
+ // row-grabbing by stitching two rules together. See advisor review 2026-04-19.
187
+ export function userCanWriteFieldRow(
188
+ user: SessionUser,
189
+ accessMap: OwnershipMap | undefined,
190
+ oldRow: Readonly<Record<string, unknown>>,
191
+ newRow: Readonly<Record<string, unknown>>,
192
+ ): boolean {
193
+ if (!accessMap || Object.keys(accessMap).length === 0) return true; // public
194
+ for (const role of user.roles) {
195
+ const rule = accessMap[role];
196
+ if (!rule) continue;
197
+ if (rule === "all") return true;
198
+ if (matchesRule(rule, user, oldRow) && matchesRule(rule, user, newRow)) return true;
199
+ }
200
+ return false;
201
+ }
202
+
203
+ // Normalize legacy `readonly string[]` field-access into the OwnershipMap
204
+ // shape. Each role in the array becomes a key with rule "all" (unrestricted
205
+ // for that role). Undefined stays undefined. This is a migration shim —
206
+ // long-term every feature-definition writes the map shape directly.
207
+ export function normalizeAccessEntry(
208
+ entry: OwnershipMap | readonly string[] | undefined,
209
+ ): OwnershipMap | undefined {
210
+ if (!entry) return undefined;
211
+ if (Array.isArray(entry)) {
212
+ if (entry.length === 0) return undefined;
213
+ const map: Record<string, OwnershipRule> = {};
214
+ for (const role of entry) {
215
+ map[role] = "all";
216
+ }
217
+ return map;
218
+ }
219
+ return entry as OwnershipMap; // @cast-boundary schema-walk
220
+ }
221
+
222
+ // Create-case: only the new row exists. Same Straddle protection not
223
+ // applicable (no old row to compare), but we still need per-role atomicity
224
+ // to respect "all"-rules and plain from-rules consistently.
225
+ export function userCanCreateFieldRow(
226
+ user: SessionUser,
227
+ accessMap: OwnershipMap | undefined,
228
+ newRow: Readonly<Record<string, unknown>>,
229
+ ): boolean {
230
+ if (!accessMap || Object.keys(accessMap).length === 0) return true;
231
+ for (const role of user.roles) {
232
+ const rule = accessMap[role];
233
+ if (!rule) continue;
234
+ if (rule === "all") return true;
235
+ if (matchesRule(rule, user, newRow)) return true;
236
+ }
237
+ return false;
238
+ }
239
+
240
+ // Result of buildOwnershipClause. The discriminant lets the caller handle
241
+ // the three outcomes without inspecting SQL internals:
242
+ //
243
+ // "pass" → user is unrestricted. Run the query as-is.
244
+ // "empty" → user has a role mapped but no rule accepts any row (missing
245
+ // claim, empty array, role not in map). Skip the DB call entirely
246
+ // — returning [] is equivalent and avoids a pointless roundtrip.
247
+ // "sql" → apply `.sql` as an AND to the query's where clause.
248
+ //
249
+ // "empty" vs. "pass" is the critical distinction for a safe default:
250
+ // undefined/pass = allow, empty = deny-by-construction. Mixing them up was
251
+ // the exact leak direction advisor flagged; the disjoint type prevents it.
252
+ export type OwnershipClause =
253
+ | { readonly kind: "pass" }
254
+ | { readonly kind: "empty" }
255
+ | { readonly kind: "sql"; readonly sql: SQL };
256
+
257
+ const PASS_CLAUSE: OwnershipClause = { kind: "pass" };
258
+ const EMPTY_CLAUSE: OwnershipClause = { kind: "empty" };
259
+
260
+ // Build an ownership clause for entity-level READ access. The caller
261
+ // translates the result to its query layer (see above).
262
+ //
263
+ // `table` is the Drizzle table with column objects. Unknown column on a
264
+ // from-rule is a boot-time misconfiguration; at request time we treat it
265
+ // as empty (safe default) rather than passing silently.
266
+ export function buildOwnershipClause(
267
+ user: SessionUser,
268
+ accessMap: OwnershipMap | undefined,
269
+ // biome-ignore lint/suspicious/noExplicitAny: Drizzle tables carry schema-dependent column shapes
270
+ table: any,
271
+ ): OwnershipClause {
272
+ if (!accessMap || Object.keys(accessMap).length === 0) return PASS_CLAUSE;
273
+
274
+ const clauses: SQL[] = [];
275
+ let anyRoleMatched = false;
276
+ let everyRuleCollapsedToEmpty = true;
277
+
278
+ for (const role of user.roles) {
279
+ const rule = accessMap[role];
280
+ if (!rule) continue;
281
+ anyRoleMatched = true;
282
+ // "all" = no filter at all for this role; short-circuit.
283
+ if (rule === "all") return PASS_CLAUSE;
284
+ const resolved = ruleToClause(rule, user, table);
285
+ if (resolved.kind === "sql") {
286
+ clauses.push(resolved.sql);
287
+ everyRuleCollapsedToEmpty = false;
288
+ }
289
+ // "empty" contribution from one role doesn't short-circuit: another
290
+ // role might still contribute an OR-branch. But if ALL branches are
291
+ // empty, the result is empty.
292
+ }
293
+
294
+ if (!anyRoleMatched) return EMPTY_CLAUSE;
295
+ if (everyRuleCollapsedToEmpty && clauses.length === 0) return EMPTY_CLAUSE;
296
+ if (clauses.length === 1) {
297
+ const only = clauses[0];
298
+ if (!only) return EMPTY_CLAUSE;
299
+ return { kind: "sql", sql: only };
300
+ }
301
+ // @cast-boundary db-operator — drizzle or() widened signature
302
+ // biome-ignore lint/suspicious/noExplicitAny: same reason as above
303
+ const combined = or(...(clauses as any)) as SQL;
304
+ return { kind: "sql", sql: combined };
305
+ }
306
+
307
+ type RuleClauseResult = { readonly kind: "empty" } | { readonly kind: "sql"; readonly sql: SQL };
308
+
309
+ function ruleToClause(
310
+ rule: OwnershipRule,
311
+ user: SessionUser,
312
+ // biome-ignore lint/suspicious/noExplicitAny: Drizzle tables carry schema-dependent column shapes
313
+ table: any,
314
+ ): RuleClauseResult {
315
+ if (rule === "all") {
316
+ // Caller handles "all" by short-circuit before reaching here; defensive
317
+ // fallback.
318
+ return { kind: "sql", sql: sql`true` };
319
+ }
320
+ if (rule.kind === "where") {
321
+ return { kind: "sql", sql: rule.where(user, table) };
322
+ }
323
+ // FromRule
324
+ const column = table[rule.column];
325
+ // Unknown column — boot validator should have caught this, but at request
326
+ // time we treat as empty (fail-closed).
327
+ if (!column) return { kind: "empty" };
328
+
329
+ const value = resolveUserValue(rule, user);
330
+ if (value === undefined || value === null) return { kind: "empty" };
331
+
332
+ if (Array.isArray(value)) {
333
+ if (value.length === 0) return { kind: "empty" };
334
+ return { kind: "sql", sql: inArray(column, value) };
335
+ }
336
+ return { kind: "sql", sql: eq(column, value) };
337
+ }
@@ -0,0 +1,22 @@
1
+ // Tier 2.7e Cross-Feature: ReferenceFieldDef.entity-String-Parser.
2
+ //
3
+ // Akzeptiert beide Formen:
4
+ // - "user" → same-feature ref (featureName = currentFeature)
5
+ // - "users:user" → cross-feature ref (qualifiziert)
6
+ //
7
+ // Lebt im framework-Package damit Server-Validator + Renderer (über
8
+ // Re-Export aus @cosmicdrift/kumiko-headless) denselben Parser nutzen — die
9
+ // Convention darf nicht zweimal implementiert werden.
10
+
11
+ export type ParsedRefTarget = {
12
+ readonly featureName: string;
13
+ readonly entityName: string;
14
+ };
15
+
16
+ export function parseRefTarget(raw: string, currentFeature: string): ParsedRefTarget {
17
+ const idx = raw.indexOf(":");
18
+ if (idx < 0) {
19
+ return { featureName: currentFeature, entityName: raw };
20
+ }
21
+ return { featureName: raw.slice(0, idx), entityName: raw.slice(idx + 1) };
22
+ }