@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,88 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { z } from "zod";
3
+ import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
4
+ import { buildServer } from "../server";
5
+
6
+ const JWT_SECRET = "test-secret-at-least-32-chars-long!!";
7
+
8
+ const testFeature = defineFeature("blob", (r) => {
9
+ r.entity("note", createEntity({ table: "Notes", fields: { body: createTextField() } }));
10
+ r.writeHandler(
11
+ "note:create",
12
+ z.object({ body: z.string() }),
13
+ async (event) => ({ isSuccess: true, data: { body: event.payload.body } }),
14
+ { access: { openToAll: true } },
15
+ );
16
+ });
17
+
18
+ function buildApp(maxRequestBytes?: number) {
19
+ const registry = createRegistry([testFeature]);
20
+ return buildServer({
21
+ registry,
22
+ context: {},
23
+ jwtSecret: JWT_SECRET,
24
+ maxRequestBytes,
25
+ }).app;
26
+ }
27
+
28
+ function postJson(app: ReturnType<typeof buildApp>, path: string, bytes: number) {
29
+ const body = JSON.stringify({ body: "x".repeat(bytes) });
30
+ return app.request(path, {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ "Content-Length": String(body.length),
35
+ },
36
+ body,
37
+ });
38
+ }
39
+
40
+ describe("request body limit", () => {
41
+ test("rejects POST with body larger than maxRequestBytes with 413", async () => {
42
+ const app = buildApp(1024);
43
+ const res = await postJson(app, "/api/write", 2048);
44
+ expect(res.status).toBe(413);
45
+ });
46
+
47
+ test("accepts POST with body within the limit (reaches auth layer)", async () => {
48
+ const app = buildApp(10_000);
49
+ const res = await postJson(app, "/api/write", 100);
50
+ // No JWT → 401. Point is: NOT 413.
51
+ expect(res.status).toBe(401);
52
+ });
53
+
54
+ test("default limit rejects absurdly large payloads", async () => {
55
+ const app = buildApp(); // default 1 MB
56
+ const res = await postJson(app, "/api/write", 2_000_000); // 2 MB
57
+ expect(res.status).toBe(413);
58
+ });
59
+
60
+ test("default limit accepts small payloads", async () => {
61
+ const app = buildApp();
62
+ const res = await postJson(app, "/api/write", 500);
63
+ expect(res.status).toBe(401); // auth required, but size is fine
64
+ });
65
+
66
+ test("limit is not applied to /api/files (uploads have their own cap)", async () => {
67
+ // /api/files isn't mounted on this test app (no storageProvider), so a POST
68
+ // results in 404 — the point is: NOT 413. A payload that exceeds the JSON
69
+ // cap must still reach the route layer for the files router to decide.
70
+ const app = buildApp(1024);
71
+ const body = JSON.stringify({ body: "x".repeat(4096) });
72
+ const res = await app.request("/api/files", {
73
+ method: "POST",
74
+ headers: {
75
+ "Content-Type": "application/json",
76
+ "Content-Length": String(body.length),
77
+ },
78
+ body,
79
+ });
80
+ expect(res.status).not.toBe(413);
81
+ });
82
+
83
+ test("maxRequestBytes=0 disables the cap entirely", async () => {
84
+ const app = buildApp(0);
85
+ const res = await postJson(app, "/api/write", 50_000);
86
+ expect(res.status).toBe(401); // passes body-limit, reaches auth
87
+ });
88
+ });
@@ -0,0 +1,97 @@
1
+ // csrf-middleware: double-submit token check against a Hono app that
2
+ // layers authMiddleware → csrfMiddleware → handler. Covers the paths that
3
+ // matter in production: cookie + state-changing, cookie + safe, bearer.
4
+
5
+ import { Hono } from "hono";
6
+ import { describe, expect, test } from "vitest";
7
+ import { TestUsers } from "../../stack";
8
+ import {
9
+ AUTH_COOKIE_NAME,
10
+ authMiddleware,
11
+ CSRF_COOKIE_NAME,
12
+ CSRF_HEADER_NAME,
13
+ } from "../auth-middleware";
14
+ import { csrfMiddleware } from "../csrf-middleware";
15
+ import { createJwtHelper } from "../jwt";
16
+
17
+ const JWT_SECRET = "csrf-middleware-test-secret-min-32-characters-long";
18
+ const CSRF = "csrf-token-fixed-for-test";
19
+
20
+ async function buildApp(): Promise<{ app: Hono; token: string }> {
21
+ const jwt = createJwtHelper(JWT_SECRET);
22
+ const token = await jwt.sign(TestUsers.user);
23
+ const app = new Hono();
24
+ app.use("/api/*", authMiddleware(jwt));
25
+ app.use("/api/*", csrfMiddleware());
26
+ app.get("/api/ping", (c) => c.json({ ok: true }));
27
+ app.post("/api/write", (c) => c.json({ ok: true }));
28
+ return { app, token };
29
+ }
30
+
31
+ describe("csrf-middleware", () => {
32
+ test("bearer transport skips csrf check even on POST", async () => {
33
+ const { app, token } = await buildApp();
34
+ const res = await app.request("/api/write", {
35
+ method: "POST",
36
+ headers: { Authorization: `Bearer ${token}` },
37
+ });
38
+ expect(res.status).toBe(200);
39
+ });
40
+
41
+ test("cookie transport + GET → no csrf check (safe method)", async () => {
42
+ const { app, token } = await buildApp();
43
+ const res = await app.request("/api/ping", {
44
+ headers: { Cookie: `${AUTH_COOKIE_NAME}=${token}` },
45
+ });
46
+ expect(res.status).toBe(200);
47
+ });
48
+
49
+ test("cookie transport + POST + matching csrf → ok", async () => {
50
+ const { app, token } = await buildApp();
51
+ const res = await app.request("/api/write", {
52
+ method: "POST",
53
+ headers: {
54
+ Cookie: `${AUTH_COOKIE_NAME}=${token}; ${CSRF_COOKIE_NAME}=${CSRF}`,
55
+ [CSRF_HEADER_NAME]: CSRF,
56
+ },
57
+ });
58
+ expect(res.status).toBe(200);
59
+ });
60
+
61
+ test("cookie transport + POST + missing header → 403", async () => {
62
+ const { app, token } = await buildApp();
63
+ const res = await app.request("/api/write", {
64
+ method: "POST",
65
+ headers: {
66
+ Cookie: `${AUTH_COOKIE_NAME}=${token}; ${CSRF_COOKIE_NAME}=${CSRF}`,
67
+ },
68
+ });
69
+ expect(res.status).toBe(403);
70
+ const body = (await res.json()) as { error: { code: string } };
71
+ expect(body.error.code).toBe("csrf_token_mismatch");
72
+ });
73
+
74
+ test("cookie transport + POST + wrong header → 403", async () => {
75
+ const { app, token } = await buildApp();
76
+ const res = await app.request("/api/write", {
77
+ method: "POST",
78
+ headers: {
79
+ Cookie: `${AUTH_COOKIE_NAME}=${token}; ${CSRF_COOKIE_NAME}=${CSRF}`,
80
+ [CSRF_HEADER_NAME]: "wrong-value",
81
+ },
82
+ });
83
+ expect(res.status).toBe(403);
84
+ });
85
+
86
+ test("cookie transport + POST + missing csrf cookie → 403", async () => {
87
+ const { app, token } = await buildApp();
88
+ const res = await app.request("/api/write", {
89
+ method: "POST",
90
+ headers: {
91
+ Cookie: `${AUTH_COOKIE_NAME}=${token}`,
92
+ [CSRF_HEADER_NAME]: CSRF,
93
+ },
94
+ });
95
+ expect(res.status).toBe(403);
96
+ });
97
+ });
@@ -0,0 +1,216 @@
1
+ import { createLiveDispatcher } from "@cosmicdrift/kumiko-dispatcher-live";
2
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
3
+ import { z } from "zod";
4
+ import { generateToken } from "../../api/tokens";
5
+ import { createEventStoreExecutor } from "../../db/event-store-executor";
6
+ import { buildDrizzleTable } from "../../db/table-builder";
7
+ import { createEntity, createTextField, defineFeature } from "../../engine";
8
+ import { createEntityTable, setupTestStack, type TestStack, TestUsers } from "../../stack";
9
+ import { generateId } from "../../utils";
10
+
11
+ // End-to-end: UI code would call `dispatcher.write("feat:write:item:create", ...)`.
12
+ // This test wires dispatcher-live against the real Kumiko HTTP stack via
13
+ // Hono's `app.request()` (in-memory, no port) and proves the whole path:
14
+ // dispatcher-live → JSON body + headers → Hono route → dispatcher →
15
+ // write-handler → crud-executor → DB → response → dispatcher-live →
16
+ // typed WriteResult.
17
+ //
18
+ // Server-side CSRF middleware is enabled by the normal server config, so
19
+ // the dispatcher must carry X-CSRF-Token correctly or these writes would
20
+ // land as 403. The test proves that wiring end-to-end.
21
+
22
+ const itemEntity = createEntity({
23
+ table: "dispatcher_live_items",
24
+ fields: { name: createTextField({ required: true }) },
25
+ });
26
+ const itemTable = buildDrizzleTable("item", itemEntity);
27
+
28
+ const itemFeature = defineFeature("dlive", (r) => {
29
+ r.entity("item", itemEntity);
30
+
31
+ r.writeHandler(
32
+ "item:create",
33
+ z.object({ name: z.string().min(1) }),
34
+ async (event, ctx) => {
35
+ const crud = createEventStoreExecutor(itemTable, itemEntity, { entityName: "item" });
36
+ return crud.create(event.payload, event.user, ctx.db);
37
+ },
38
+ { access: { roles: ["Admin"] } },
39
+ );
40
+
41
+ r.queryHandler(
42
+ "item:list",
43
+ z.object({}).optional(),
44
+ async (_event, ctx) => {
45
+ return ctx.db.select().from(itemTable);
46
+ },
47
+ { access: { roles: ["Admin"] } },
48
+ );
49
+ });
50
+
51
+ let stack: TestStack;
52
+ const admin = TestUsers.admin;
53
+
54
+ // Wire dispatcher-live's `fetch` to Hono's in-memory `app.request`. Also
55
+ // synthesizes the auth + CSRF cookies a real browser would send: the stack
56
+ // exposes the Hono app, but the normal login-flow-based session-cookie
57
+ // setup isn't in play here — we sign a JWT directly and set both the
58
+ // `kumiko_auth` (HttpOnly JWT) and `kumiko_csrf` cookies by hand. A
59
+ // real browser login does the same server-side via auth-routes.ts.
60
+ //
61
+ // GAP: this means the real POST /auth/login round-trip (Set-Cookie
62
+ // headers, CSRF-cookie generation on the server, SameSite/HttpOnly
63
+ // flags as actually emitted) is NOT exercised here. If we ever change
64
+ // the login endpoint's cookie-setting code, this file will not catch
65
+ // regressions — the dedicated auth-routes integration test owns that
66
+ // coverage. Keep this test focused on dispatcher-live's request-side
67
+ // behaviour (envelope parsing, error mapping, CSRF-header echo).
68
+ //
69
+ // The fetch wrapper echoes the csrf cookie back into the X-CSRF-Token
70
+ // header — that's the real dispatcher-live code path; the test just
71
+ // stages the cookies first.
72
+ async function buildFetch(): Promise<{
73
+ readonly fetch: typeof fetch;
74
+ readonly csrfToken: string;
75
+ readonly authJwt: string;
76
+ }> {
77
+ const authJwt = await stack.jwt.sign(admin);
78
+ const csrfToken = generateToken();
79
+ const cookieHeader = `kumiko_auth=${authJwt}; kumiko_csrf=${csrfToken}`;
80
+
81
+ // Cast via unknown: the native fetch interface (Bun's typing) includes a
82
+ // `preconnect` method we don't need and can't meaningfully implement
83
+ // against Hono's in-memory request handler. dispatcher-live calls the
84
+ // functional shape only — preconnect is a hint, not load-bearing.
85
+ const fetchImpl = (async (url: unknown, init: RequestInit | undefined) => {
86
+ const reqInit: RequestInit = {
87
+ ...(init ?? {}),
88
+ headers: {
89
+ ...(init?.headers ?? {}),
90
+ Cookie: cookieHeader,
91
+ },
92
+ };
93
+ return stack.app.request(String(url), reqInit);
94
+ }) as unknown as typeof fetch;
95
+ return { fetch: fetchImpl, csrfToken, authJwt };
96
+ }
97
+
98
+ beforeAll(async () => {
99
+ stack = await setupTestStack({ features: [itemFeature] });
100
+ await createEntityTable(stack.db, itemEntity);
101
+ });
102
+
103
+ afterAll(async () => {
104
+ await stack.cleanup();
105
+ });
106
+
107
+ beforeEach(async () => {
108
+ await stack.db.delete(itemTable);
109
+ });
110
+
111
+ describe("dispatcher-live (integration) — full path against Kumiko server", () => {
112
+ test("write: dispatches HTTP, server persists, response maps to typed WriteResult", async () => {
113
+ const { fetch, csrfToken } = await buildFetch();
114
+ const dispatcher = createLiveDispatcher({
115
+ fetch,
116
+ readCsrf: () => csrfToken,
117
+ });
118
+
119
+ const result = await dispatcher.write<{ data?: { name?: string } }>("dlive:write:item:create", {
120
+ name: "hello-live",
121
+ });
122
+
123
+ expect(result.isSuccess).toBe(true);
124
+
125
+ // Prove the server actually persisted.
126
+ const rows = await stack.db.select().from(itemTable);
127
+ expect(rows).toHaveLength(1);
128
+ expect(rows[0]?.["name"]).toBe("hello-live");
129
+ });
130
+
131
+ test("write: validation failure surfaces as typed DispatcherError with field issues", async () => {
132
+ const { fetch, csrfToken } = await buildFetch();
133
+ const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => csrfToken });
134
+
135
+ const result = await dispatcher.write("dlive:write:item:create", { name: "" });
136
+
137
+ expect(result.isSuccess).toBe(false);
138
+ if (!result.isSuccess) {
139
+ expect(result.error.code).toBe("validation_error");
140
+ const fieldPaths = (result.error.details?.fields ?? []).map((f) => f.path);
141
+ expect(fieldPaths).toContain("name");
142
+ }
143
+ });
144
+
145
+ test("missing CSRF token: server rejects — exercises Vorarbeit-A wiring", async () => {
146
+ const { fetch } = await buildFetch();
147
+ // Dispatcher with no csrf reader — the header won't be sent.
148
+ const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => undefined });
149
+
150
+ const result = await dispatcher.write("dlive:write:item:create", { name: "no-csrf" });
151
+
152
+ expect(result.isSuccess).toBe(false);
153
+ if (!result.isSuccess) {
154
+ // The CSRF middleware raises with code "csrf_token_mismatch".
155
+ expect(result.error.code).toBe("csrf_token_mismatch");
156
+ }
157
+ });
158
+
159
+ test("query: dispatches GET-style-POST (Kumiko uses POST for query too), returns data", async () => {
160
+ // Seed a row first.
161
+ await stack.db.insert(itemTable).values({
162
+ id: generateId(),
163
+ tenantId: admin.tenantId,
164
+ name: "seed",
165
+ });
166
+
167
+ const { fetch, csrfToken } = await buildFetch();
168
+ const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => csrfToken });
169
+
170
+ const result = await dispatcher.query<unknown[]>("dlive:query:item:list", {});
171
+
172
+ expect(result.isSuccess).toBe(true);
173
+ if (result.isSuccess) {
174
+ expect(Array.isArray(result.data)).toBe(true);
175
+ expect(result.data).toHaveLength(1);
176
+ }
177
+ });
178
+
179
+ test("batch: multiple writes go through one HTTP call, atomic on the server", async () => {
180
+ const { fetch, csrfToken } = await buildFetch();
181
+ const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => csrfToken });
182
+
183
+ const result = await dispatcher.batch([
184
+ { type: "dlive:write:item:create", payload: { name: "a" } },
185
+ { type: "dlive:write:item:create", payload: { name: "b" } },
186
+ { type: "dlive:write:item:create", payload: { name: "c" } },
187
+ ]);
188
+
189
+ expect(result.isSuccess).toBe(true);
190
+
191
+ const rows = await stack.db.select().from(itemTable);
192
+ expect(rows).toHaveLength(3);
193
+ const names = rows.map((r) => r["name"]).sort();
194
+ expect(names).toEqual(["a", "b", "c"]);
195
+ });
196
+
197
+ test("batch: mid-batch failure rolls back the prior writes — atomic guarantee preserved", async () => {
198
+ const { fetch, csrfToken } = await buildFetch();
199
+ const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => csrfToken });
200
+
201
+ const result = await dispatcher.batch([
202
+ { type: "dlive:write:item:create", payload: { name: "ok-1" } },
203
+ { type: "dlive:write:item:create", payload: { name: "" } }, // fails validation
204
+ { type: "dlive:write:item:create", payload: { name: "never-runs" } },
205
+ ]);
206
+
207
+ expect(result.isSuccess).toBe(false);
208
+ if (!result.isSuccess) {
209
+ expect(result.failedIndex).toBe(1);
210
+ }
211
+
212
+ // DB must be empty — prior success within a failed batch rolls back.
213
+ const rows = await stack.db.select().from(itemTable);
214
+ expect(rows).toHaveLength(0);
215
+ });
216
+ });
@@ -0,0 +1,126 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
3
+ import {
4
+ createNoopProvider,
5
+ createPrometheusMeter,
6
+ type ObservabilityProvider,
7
+ } from "../../observability";
8
+ import { buildServer } from "../server";
9
+
10
+ const JWT = "metrics-endpoint-test-secret-minimum-32-chars!!";
11
+
12
+ const noopFeature = defineFeature("m", (r) => {
13
+ r.entity("widget", createEntity({ table: "Widgets", fields: { name: createTextField() } }));
14
+ });
15
+
16
+ // Swap the NoopProvider's meter for a PrometheusMeter. Tracer + lifecycle
17
+ // stay noop — /metrics only reads the meter.
18
+ function makeProvider(): {
19
+ provider: ObservabilityProvider;
20
+ meter: ReturnType<typeof createPrometheusMeter>;
21
+ } {
22
+ const meter = createPrometheusMeter();
23
+ const base = createNoopProvider();
24
+ const provider: ObservabilityProvider = { ...base, meter };
25
+ return { provider, meter };
26
+ }
27
+
28
+ function makeApp(opts: {
29
+ metrics?: { token?: string; path?: string };
30
+ meter?: ReturnType<typeof createPrometheusMeter>;
31
+ provider?: ObservabilityProvider;
32
+ }) {
33
+ const registry = createRegistry([noopFeature]);
34
+ const build = opts.provider
35
+ ? { provider: opts.provider, meter: opts.meter ?? null }
36
+ : makeProvider();
37
+ const args = {
38
+ registry,
39
+ context: {},
40
+ jwtSecret: JWT,
41
+ observability: build.provider,
42
+ ...(opts.metrics ? { metrics: opts.metrics } : {}),
43
+ };
44
+ return { ...buildServer(args), meter: build.meter };
45
+ }
46
+
47
+ describe("/metrics endpoint", () => {
48
+ test("returns 404 when `metrics` option is not wired (opt-in)", async () => {
49
+ const { app } = makeApp({});
50
+ const res = await app.request("/metrics");
51
+ expect(res.status).toBe(404);
52
+ });
53
+
54
+ test("returns OpenMetrics text when wired and meter is a PrometheusMeter", async () => {
55
+ const { app, meter } = makeApp({ metrics: {} });
56
+ if (!meter) throw new Error("meter missing");
57
+ // Seed a single metric so the output isn't empty.
58
+ meter.registerMetric({
59
+ name: "kumiko_test_total",
60
+ type: "counter",
61
+ description: "probe counter",
62
+ });
63
+ meter.counter("kumiko_test_total").inc(3);
64
+
65
+ const res = await app.request("/metrics");
66
+ expect(res.status).toBe(200);
67
+ expect(res.headers.get("Content-Type")).toMatch(/openmetrics-text/);
68
+ const body = await res.text();
69
+ expect(body).toContain("# HELP kumiko_test_total probe counter");
70
+ expect(body).toContain("# TYPE kumiko_test_total counter");
71
+ expect(body).toContain("kumiko_test_total 3");
72
+ expect(body).toMatch(/# EOF\n$/);
73
+ });
74
+
75
+ test("token-protected: rejects missing header with 401", async () => {
76
+ const { app } = makeApp({ metrics: { token: "scrape-secret-xyz" } });
77
+ const res = await app.request("/metrics");
78
+ expect(res.status).toBe(401);
79
+ });
80
+
81
+ test("token-protected: rejects wrong token with 401", async () => {
82
+ const { app } = makeApp({ metrics: { token: "scrape-secret-xyz" } });
83
+ const res = await app.request("/metrics", {
84
+ headers: { Authorization: "Bearer wrong-token" },
85
+ });
86
+ expect(res.status).toBe(401);
87
+ });
88
+
89
+ test("token-protected: accepts matching Bearer token", async () => {
90
+ const { app, meter } = makeApp({ metrics: { token: "scrape-secret-xyz" } });
91
+ if (!meter) throw new Error("meter missing");
92
+ meter.registerMetric({ name: "kumiko_probe", type: "counter" });
93
+ meter.counter("kumiko_probe").inc();
94
+
95
+ const res = await app.request("/metrics", {
96
+ headers: { Authorization: "Bearer scrape-secret-xyz" },
97
+ });
98
+ expect(res.status).toBe(200);
99
+ });
100
+
101
+ test("custom path: /internal/metrics", async () => {
102
+ const { app, meter } = makeApp({ metrics: { path: "/internal/metrics" } });
103
+ if (!meter) throw new Error("meter missing");
104
+ meter.registerMetric({ name: "kumiko_probe", type: "counter" });
105
+
106
+ const atDefault = await app.request("/metrics");
107
+ expect(atDefault.status).toBe(404);
108
+
109
+ const atCustom = await app.request("/internal/metrics");
110
+ expect(atCustom.status).toBe(200);
111
+ });
112
+
113
+ test("503 when meter lacks snapshot() (misconfig — non-Prometheus provider)", async () => {
114
+ // Build a provider whose meter is a raw non-Prometheus implementation —
115
+ // pretend it's a ConsoleProvider or an OTLP bridge without snapshot().
116
+ const { createNoopProvider } = await import("../../observability");
117
+ const provider = createNoopProvider();
118
+ // NoopProvider is "empty by design", register a metric so definitions
119
+ // isn't hollow, but snapshot() is still absent on the meter shape.
120
+ provider.meter.registerMetric({ name: "kumiko_noop", type: "counter" });
121
+ const { app } = makeApp({ provider, metrics: {} });
122
+ const res = await app.request("/metrics");
123
+ expect(res.status).toBe(503);
124
+ expect(await res.text()).toContain("PrometheusMeter");
125
+ });
126
+ });