@cosmicdrift/kumiko-framework 0.386.0 → 0.388.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 (468) hide show
  1. package/AGENTS.md +6 -1
  2. package/LICENSE +21 -57
  3. package/README.md +2 -2
  4. package/agents/examples/anonymous-access.md +1 -1
  5. package/agents/examples/apex-landing.md +204 -0
  6. package/agents/examples/api-worker-split.md +64 -0
  7. package/agents/examples/app-config.md +14 -0
  8. package/agents/examples/auth-mfa.md +115 -0
  9. package/agents/examples/env-composition.md +28 -1
  10. package/agents/examples/legal-pages.md +1 -1
  11. package/agents/examples/prod-boot-options.md +16 -0
  12. package/agents/examples/rate-limiting.md +48 -0
  13. package/agents/examples/record-detail-layout.md +692 -0
  14. package/agents/examples/seed-migration.md +212 -0
  15. package/agents/examples/wizard-form.md +941 -0
  16. package/agents/examples/writeform-section.md +548 -0
  17. package/agents/features.md +19 -2
  18. package/agents/widgets.md +40 -39
  19. package/dist/api/api-constants.js +2 -2
  20. package/dist/api/auth-middleware.js +7 -7
  21. package/dist/api/auth-rate-limiters.js +7 -3
  22. package/dist/api/auth-routes-mfa.js +1 -1
  23. package/dist/api/auth-routes-session.js +1 -1
  24. package/dist/api/auth-routes-shared.js +7 -3
  25. package/dist/api/auth-routes-signup.js +1 -1
  26. package/dist/api/client-error-route.js +8 -2
  27. package/dist/api/client-ip.js +3 -1
  28. package/dist/api/extra-route.js +1 -1
  29. package/dist/api/index.d.ts +3 -1
  30. package/dist/api/index.js +3 -1
  31. package/dist/api/pii-leak-guard.js +4 -2
  32. package/dist/api/readiness.js +5 -5
  33. package/dist/api/redis-sse-broker.js +8 -6
  34. package/dist/api/request-context.d.ts +1 -1
  35. package/dist/api/request-context.js +1 -1
  36. package/dist/api/routes.js +2 -2
  37. package/dist/api/server-boot-checks.d.ts +10 -0
  38. package/dist/api/server-boot-checks.js +157 -0
  39. package/dist/api/server-event-dispatcher.d.ts +26 -0
  40. package/dist/api/server-event-dispatcher.js +208 -0
  41. package/dist/api/server-extra-routes.d.ts +11 -0
  42. package/dist/api/server-extra-routes.js +203 -0
  43. package/dist/api/server-middleware.d.ts +20 -0
  44. package/dist/api/server-middleware.js +211 -0
  45. package/dist/api/server-routes.d.ts +19 -0
  46. package/dist/api/server-routes.js +293 -0
  47. package/dist/api/server-websocket-routes.d.ts +4 -0
  48. package/dist/api/server-websocket-routes.js +108 -0
  49. package/dist/api/server-wiring.d.ts +32 -0
  50. package/dist/api/server-wiring.js +236 -0
  51. package/dist/api/server.d.ts +10 -15
  52. package/dist/api/server.js +48 -1250
  53. package/dist/api/session-cookies.js +4 -4
  54. package/dist/api/sse-broker.js +1 -1
  55. package/dist/api/sse-route.d.ts +13 -13
  56. package/dist/api/sse-route.js +31 -27
  57. package/dist/auth/index.js +4 -4
  58. package/dist/auth/roles.d.ts +4 -4
  59. package/dist/auth/roles.js +24 -24
  60. package/dist/compliance/duration-spec.js +18 -20
  61. package/dist/compliance/index.js +2 -3
  62. package/dist/compliance/override-schema.d.ts +4 -4
  63. package/dist/compliance/override-schema.js +19 -22
  64. package/dist/compliance/profiles.d.ts +26 -29
  65. package/dist/compliance/profiles.js +62 -69
  66. package/dist/compliance/sub-processors.d.ts +28 -30
  67. package/dist/compliance/sub-processors.js +17 -21
  68. package/dist/consumer-cli.js +4 -4
  69. package/dist/crypto/blind-index.js +18 -17
  70. package/dist/crypto/ciphertext-pattern.js +1 -1
  71. package/dist/crypto/event-pii.js +3 -3
  72. package/dist/crypto/kek-source.d.ts +1 -1
  73. package/dist/crypto/kek-source.js +3 -2
  74. package/dist/crypto/kms-adapter.d.ts +1 -1
  75. package/dist/crypto/kms-adapter.js +4 -6
  76. package/dist/crypto/kms-wiring.d.ts +1 -2
  77. package/dist/crypto/kms-wiring.js +2 -2
  78. package/dist/crypto/owner-transition-rekey.d.ts +5 -1
  79. package/dist/crypto/owner-transition-rekey.js +1 -1
  80. package/dist/crypto/pg-kms-adapter.js +2 -6
  81. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  82. package/dist/crypto/pii-field-encryption.js +7 -5
  83. package/dist/crypto/subject-resolver.d.ts +1 -1
  84. package/dist/crypto/subject-resolver.js +2 -2
  85. package/dist/db/aggregate-sql.d.ts +21 -0
  86. package/dist/db/aggregate-sql.js +155 -0
  87. package/dist/db/apply-entity-event.d.ts +6 -6
  88. package/dist/db/apply-entity-event.js +55 -55
  89. package/dist/db/blind-index-cleanup.js +7 -7
  90. package/dist/db/closed-connection.d.ts +9 -0
  91. package/dist/db/closed-connection.js +95 -0
  92. package/dist/db/collect-table-metas.js +10 -11
  93. package/dist/db/compound-types.d.ts +1 -1
  94. package/dist/db/compound-types.js +12 -12
  95. package/dist/db/cursor.js +3 -4
  96. package/dist/db/dialect.d.ts +2 -2
  97. package/dist/db/dialect.js +3 -3
  98. package/dist/db/distinct-on.d.ts +1 -1
  99. package/dist/db/driver-row-coercion.d.ts +2 -0
  100. package/dist/db/driver-row-coercion.js +186 -0
  101. package/dist/db/driver-value-preparation.d.ts +16 -0
  102. package/dist/db/driver-value-preparation.js +99 -0
  103. package/dist/db/eagerload.d.ts +3 -3
  104. package/dist/db/eagerload.js +21 -12
  105. package/dist/db/entity-field-encryption.d.ts +1 -0
  106. package/dist/db/entity-field-encryption.js +1 -1
  107. package/dist/db/entity-table-meta.d.ts +2 -2
  108. package/dist/db/entity-table-meta.js +105 -112
  109. package/dist/db/event-store-executor-context.d.ts +3 -2
  110. package/dist/db/event-store-executor-context.js +13 -13
  111. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  112. package/dist/db/event-store-executor-read-aggregate.js +112 -0
  113. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  114. package/dist/db/event-store-executor-read-filter.js +203 -0
  115. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  116. package/dist/db/event-store-executor-read-paging.js +99 -0
  117. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  118. package/dist/db/event-store-executor-read-rows.js +32 -0
  119. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  120. package/dist/db/event-store-executor-read-search.js +221 -0
  121. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  122. package/dist/db/event-store-executor-read-sort.js +130 -0
  123. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  124. package/dist/db/event-store-executor-read-where.js +81 -0
  125. package/dist/db/event-store-executor-read.d.ts +2 -9
  126. package/dist/db/event-store-executor-read.js +50 -704
  127. package/dist/db/event-store-executor-write-steps.d.ts +20 -0
  128. package/dist/db/event-store-executor-write-steps.js +100 -0
  129. package/dist/db/event-store-executor-write.js +401 -510
  130. package/dist/db/index.d.ts +6 -2
  131. package/dist/db/index.js +5 -1
  132. package/dist/db/list-aggregate-labels.d.ts +14 -0
  133. package/dist/db/list-aggregate-labels.js +26 -0
  134. package/dist/db/located-timestamp.d.ts +11 -11
  135. package/dist/db/located-timestamp.js +29 -30
  136. package/dist/db/migrate-generator.d.ts +1 -1
  137. package/dist/db/migrate-generator.js +97 -105
  138. package/dist/db/migrate-runner.js +29 -26
  139. package/dist/db/money.d.ts +6 -6
  140. package/dist/db/money.js +29 -27
  141. package/dist/db/queries/backfill-pii.js +5 -5
  142. package/dist/db/queries/ddl.js +1 -1
  143. package/dist/db/queries/es-ops.d.ts +1 -1
  144. package/dist/db/queries/es-ops.js +1 -1
  145. package/dist/db/queries/event-consumer.js +3 -3
  146. package/dist/db/queries/event-store.js +4 -2
  147. package/dist/db/queries/projection-rebuild.js +3 -3
  148. package/dist/db/queries/raw-sql.d.ts +2 -2
  149. package/dist/db/queries/raw-sql.js +2 -2
  150. package/dist/db/queries/shadow-swap.js +19 -19
  151. package/dist/db/query.d.ts +3 -16
  152. package/dist/db/query.js +8 -761
  153. package/dist/db/rebuild-marker.js +20 -21
  154. package/dist/db/reference-data.js +6 -1
  155. package/dist/db/replay-migration-sql.js +2 -2
  156. package/dist/db/schema-inspection.js +4 -5
  157. package/dist/db/table-builder.d.ts +1 -2
  158. package/dist/db/table-builder.js +137 -190
  159. package/dist/db/table-info.d.ts +10 -0
  160. package/dist/db/table-info.js +45 -0
  161. package/dist/db/temporal-value-guards.d.ts +6 -0
  162. package/dist/db/temporal-value-guards.js +15 -0
  163. package/dist/db/tenant-db-runner.d.ts +1 -0
  164. package/dist/db/tenant-db-runner.js +3 -0
  165. package/dist/db/tenant-db.d.ts +2 -2
  166. package/dist/db/tenant-db.js +48 -11
  167. package/dist/db/where-clause.d.ts +6 -0
  168. package/dist/db/where-clause.js +236 -0
  169. package/dist/derivatives/delete-stored-file.js +6 -2
  170. package/dist/derivatives/derivatives-context.js +1 -1
  171. package/dist/engine/boot-validator/access-roles.js +4 -3
  172. package/dist/engine/boot-validator/api-ext.js +14 -14
  173. package/dist/engine/boot-validator/boot-check.js +1 -1
  174. package/dist/engine/boot-validator/config-deps.js +7 -6
  175. package/dist/engine/boot-validator/detail-screens.js +4 -4
  176. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  177. package/dist/engine/boot-validator/entity-handler.js +77 -61
  178. package/dist/engine/boot-validator/form-derivations.js +35 -1
  179. package/dist/engine/boot-validator/gdpr-storage.js +4 -4
  180. package/dist/engine/boot-validator/i18n-keys.js +6 -4
  181. package/dist/engine/boot-validator/index.d.ts +2 -2
  182. package/dist/engine/boot-validator/index.js +22 -20
  183. package/dist/engine/boot-validator/nav.js +5 -5
  184. package/dist/engine/boot-validator/ownership.js +12 -9
  185. package/dist/engine/boot-validator/parent-ref.js +1 -1
  186. package/dist/engine/boot-validator/pii-retention.js +258 -187
  187. package/dist/engine/boot-validator/projection-list-screens.js +7 -7
  188. package/dist/engine/boot-validator/query-refs.js +1 -1
  189. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  190. package/dist/engine/boot-validator/screens-actions.js +23 -17
  191. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  192. package/dist/engine/boot-validator/screens-forms.js +12 -17
  193. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  194. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  195. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  196. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  197. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  198. package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
  199. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  200. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  201. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  202. package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
  203. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  204. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  205. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  206. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  207. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  208. package/dist/engine/boot-validator/screens-lists.js +26 -24
  209. package/dist/engine/boot-validator/screens.d.ts +0 -2
  210. package/dist/engine/boot-validator/screens.js +102 -869
  211. package/dist/engine/boot-validator/security-baseline.js +3 -2
  212. package/dist/engine/boot-validator/transfer-graph.js +2 -2
  213. package/dist/engine/boot-validator/workspaces.js +3 -3
  214. package/dist/engine/bridge-stub.js +3 -4
  215. package/dist/engine/build-app-schema.js +130 -170
  216. package/dist/engine/build-config-feature-schema.js +50 -50
  217. package/dist/engine/build-target.js +20 -23
  218. package/dist/engine/codemod/pipeline-codemod.js +20 -13
  219. package/dist/engine/config-helpers.js +4 -4
  220. package/dist/engine/constants.js +2 -2
  221. package/dist/engine/create-app.js +2 -2
  222. package/dist/engine/define-feature.js +21 -13
  223. package/dist/engine/define-handler.js +13 -11
  224. package/dist/engine/define-step.js +1 -1
  225. package/dist/engine/define-workflow.d.ts +5 -5
  226. package/dist/engine/define-workflow.js +6 -6
  227. package/dist/engine/entity-handlers.d.ts +13 -0
  228. package/dist/engine/entity-handlers.js +22 -34
  229. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  230. package/dist/engine/entity-list-runtime-options.js +18 -0
  231. package/dist/engine/event-def.js +2 -2
  232. package/dist/engine/extension-names.d.ts +16 -17
  233. package/dist/engine/extension-names.js +37 -38
  234. package/dist/engine/extensions/escape-hatch-usage.js +1 -1
  235. package/dist/engine/extensions/tenant-data.js +2 -2
  236. package/dist/engine/extensions/user-data.d.ts +23 -24
  237. package/dist/engine/factories.d.ts +14 -13
  238. package/dist/engine/factories.js +38 -37
  239. package/dist/engine/feature-ast/entity-field-types.js +1 -1
  240. package/dist/engine/feature-ast/extractors/handlers.js +2 -1
  241. package/dist/engine/feature-ast/extractors/hooks.js +1 -1
  242. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  243. package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
  244. package/dist/engine/feature-ast/extractors/shared.js +2 -3
  245. package/dist/engine/feature-ast/parse.js +13 -13
  246. package/dist/engine/feature-ast/patch.js +4 -182
  247. package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
  248. package/dist/engine/feature-ast/pattern-change-schema.js +2 -1
  249. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  250. package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
  251. package/dist/engine/feature-ast/patterns.d.ts +11 -2
  252. package/dist/engine/feature-ast/patterns.js +43 -51
  253. package/dist/engine/feature-ast/render.js +50 -89
  254. package/dist/engine/feature-config-events-jobs.js +15 -12
  255. package/dist/engine/feature-entity-handlers.js +12 -5
  256. package/dist/engine/feature-manifest.d.ts +9 -9
  257. package/dist/engine/feature-manifest.js +6 -7
  258. package/dist/engine/feature-ui-extensions.d.ts +3 -3
  259. package/dist/engine/feature-ui-extensions.js +30 -22
  260. package/dist/engine/field-access.js +5 -6
  261. package/dist/engine/form-derivations.js +18 -7
  262. package/dist/engine/handler-field-references.js +1 -1
  263. package/dist/engine/index.d.ts +1 -0
  264. package/dist/engine/index.js +4 -4
  265. package/dist/engine/object-form.d.ts +1 -0
  266. package/dist/engine/object-form.js +14 -4
  267. package/dist/engine/ownership.js +3 -3
  268. package/dist/engine/parse-ref-target.js +5 -7
  269. package/dist/engine/pattern-library/index.js +2 -2
  270. package/dist/engine/pattern-library/library.js +2 -2
  271. package/dist/engine/pattern-library/types.js +3 -3
  272. package/dist/engine/pipeline.js +2 -2
  273. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  274. package/dist/engine/project-app-schema-for-roles.js +7 -5
  275. package/dist/engine/projection-helpers.js +9 -9
  276. package/dist/engine/qualified-name.js +2 -2
  277. package/dist/engine/registry-facade.js +1 -1
  278. package/dist/engine/registry-ingest.js +13 -13
  279. package/dist/engine/registry-state.js +17 -19
  280. package/dist/engine/registry-validate.js +12 -12
  281. package/dist/engine/run-in.js +2 -2
  282. package/dist/engine/run-pipeline.js +7 -0
  283. package/dist/engine/schema-builder.js +251 -233
  284. package/dist/engine/screen-filter-ops.js +17 -20
  285. package/dist/engine/screen-helpers.js +4 -3
  286. package/dist/engine/screen-kind.d.ts +7 -0
  287. package/dist/engine/screen-kind.js +1 -0
  288. package/dist/engine/state-machine.d.ts +12 -12
  289. package/dist/engine/state-machine.js +7 -7
  290. package/dist/engine/steps/_resolver-utils.js +4 -3
  291. package/dist/engine/steps/for-each.js +4 -4
  292. package/dist/engine/steps/unsafe-projection-delete.js +1 -1
  293. package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
  294. package/dist/engine/steps/wait-for-event.js +4 -4
  295. package/dist/engine/steps/workflow-system-events.js +3 -3
  296. package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
  297. package/dist/engine/tier-resolver-extension.d.ts +29 -27
  298. package/dist/engine/tier-resolver-extension.js +26 -26
  299. package/dist/engine/transfer-adjacency.js +2 -2
  300. package/dist/engine/validate-projection-allowlist.js +9 -28
  301. package/dist/engine/walk-step-instances.d.ts +2 -0
  302. package/dist/engine/walk-step-instances.js +38 -0
  303. package/dist/engine/where-rule-lint.js +1 -1
  304. package/dist/engine/write-handler-qn-extract.js +1 -1
  305. package/dist/entrypoint/index.d.ts +4 -1
  306. package/dist/entrypoint/index.js +10 -4
  307. package/dist/env/index.d.ts +7 -6
  308. package/dist/env/index.js +10 -9
  309. package/dist/errors/docs-url.d.ts +1 -1
  310. package/dist/errors/docs-url.js +22 -2
  311. package/dist/errors/transition-details.js +7 -7
  312. package/dist/errors/write-error-info.d.ts +6 -6
  313. package/dist/errors/write-error-info.js +6 -6
  314. package/dist/es-ops/context.d.ts +9 -9
  315. package/dist/es-ops/context.js +29 -29
  316. package/dist/es-ops/index.js +7 -9
  317. package/dist/es-ops/operations-schema.js +4 -10
  318. package/dist/es-ops/runner.d.ts +11 -11
  319. package/dist/es-ops/runner.js +49 -53
  320. package/dist/es-ops/types.d.ts +55 -55
  321. package/dist/es-ops/types.js +11 -11
  322. package/dist/event-store/admin-api.js +7 -8
  323. package/dist/event-store/errors.js +1 -1
  324. package/dist/event-store/event-store.js +12 -6
  325. package/dist/event-store/rebuild-dead-letter.js +1 -1
  326. package/dist/files/bind-pending-file-refs.js +3 -3
  327. package/dist/files/create-file-ref-from-bytes.js +4 -1
  328. package/dist/files/feature.js +9 -10
  329. package/dist/files/file-handle.js +7 -9
  330. package/dist/files/file-ref-entity.js +15 -15
  331. package/dist/files/file-ref-table.js +7 -8
  332. package/dist/files/file-routes.d.ts +7 -0
  333. package/dist/files/file-routes.js +22 -13
  334. package/dist/files/in-memory-provider.js +8 -9
  335. package/dist/files/local-provider.js +18 -25
  336. package/dist/files/provider-resolver.d.ts +7 -7
  337. package/dist/files/resolve-content-type.js +43 -0
  338. package/dist/files/storage-tracking.js +2 -2
  339. package/dist/files/upload-ledger.js +7 -0
  340. package/dist/files/zip-stream.d.ts +12 -19
  341. package/dist/files/zip-stream.js +44 -63
  342. package/dist/i18n/required-surface-keys.d.ts +1 -1
  343. package/dist/i18n/required-surface-keys.js +188 -212
  344. package/dist/internal/stack-support.d.ts +2 -1
  345. package/dist/internal/stack-support.js +1 -1
  346. package/dist/jobs/job-runner-context.d.ts +32 -0
  347. package/dist/jobs/job-runner-context.js +305 -0
  348. package/dist/jobs/job-runner-execution.d.ts +7 -0
  349. package/dist/jobs/job-runner-execution.js +160 -0
  350. package/dist/jobs/job-runner-gates.d.ts +19 -0
  351. package/dist/jobs/job-runner-gates.js +198 -0
  352. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  353. package/dist/jobs/job-runner-helpers.js +69 -4
  354. package/dist/jobs/job-runner-schedulers.d.ts +1 -1
  355. package/dist/jobs/job-runner-schedulers.js +3 -4
  356. package/dist/jobs/job-runner-scope.d.ts +44 -0
  357. package/dist/jobs/job-runner-scope.js +102 -0
  358. package/dist/jobs/job-runner-types.d.ts +12 -0
  359. package/dist/jobs/job-runner.d.ts +0 -1
  360. package/dist/jobs/job-runner.js +57 -669
  361. package/dist/lifecycle/lifecycle.js +3 -5
  362. package/dist/lifecycle/signal-handlers.js +5 -4
  363. package/dist/logging/utils.d.ts +2 -1
  364. package/dist/logging/utils.js +38 -10
  365. package/dist/migrations/index.js +1 -1
  366. package/dist/migrations/kumiko-drift.js +14 -13
  367. package/dist/migrations/pending-rebuilds.d.ts +21 -20
  368. package/dist/migrations/pending-rebuilds.js +27 -27
  369. package/dist/migrations/projection-table-index.d.ts +3 -3
  370. package/dist/migrations/projection-table-index.js +8 -8
  371. package/dist/observability/escape-hatch-report.d.ts +3 -0
  372. package/dist/observability/escape-hatch-report.js +12 -1
  373. package/dist/observability/metric-validator.js +1 -1
  374. package/dist/observability/metrics-handle.js +3 -3
  375. package/dist/pipeline/append-event-core.js +11 -2
  376. package/dist/pipeline/cascade-handler.d.ts +1 -1
  377. package/dist/pipeline/cascade-handler.js +165 -138
  378. package/dist/pipeline/dispatch-batch.js +2 -2
  379. package/dist/pipeline/dispatch-query.js +20 -13
  380. package/dist/pipeline/dispatch-shared.d.ts +14 -3
  381. package/dist/pipeline/dispatch-shared.js +67 -721
  382. package/dist/pipeline/dispatch-stream.js +1 -1
  383. package/dist/pipeline/dispatch-write.js +297 -190
  384. package/dist/pipeline/dispatcher-utils.d.ts +1 -1
  385. package/dist/pipeline/dispatcher-utils.js +1 -1
  386. package/dist/pipeline/event-consumer-state.js +2 -2
  387. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  388. package/dist/pipeline/event-dispatcher-delivery.js +4 -6
  389. package/dist/pipeline/event-dispatcher.js +17 -28
  390. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  391. package/dist/pipeline/handler-context-auth.js +54 -0
  392. package/dist/pipeline/handler-context-config.d.ts +14 -0
  393. package/dist/pipeline/handler-context-config.js +113 -0
  394. package/dist/pipeline/handler-context-db.d.ts +24 -0
  395. package/dist/pipeline/handler-context-db.js +149 -0
  396. package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
  397. package/dist/pipeline/handler-context-dispatch.js +89 -0
  398. package/dist/pipeline/handler-context-events.d.ts +43 -0
  399. package/dist/pipeline/handler-context-events.js +237 -0
  400. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  401. package/dist/pipeline/handler-context-notify.js +55 -0
  402. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  403. package/dist/pipeline/handler-context-observability.js +21 -0
  404. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  405. package/dist/pipeline/handler-context-scope.js +88 -0
  406. package/dist/pipeline/irreversible-operation-gate.js +1 -1
  407. package/dist/pipeline/lifecycle-pipeline.js +9 -9
  408. package/dist/pipeline/member-read-only-transaction.js +1 -3
  409. package/dist/pipeline/msp-rebuild.js +1 -1
  410. package/dist/pipeline/notify-scope.js +12 -1
  411. package/dist/pipeline/pre-handler-gates.js +20 -4
  412. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  413. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  414. package/dist/pipeline/projection-rebuild.js +15 -13
  415. package/dist/pipeline/system-hooks.js +39 -41
  416. package/dist/pipeline/system-identity-switch.js +1 -3
  417. package/dist/pipeline/tenant-timezone-cache.js +1 -1
  418. package/dist/random/generate.d.ts +29 -29
  419. package/dist/random/generate.js +30 -30
  420. package/dist/random/words.js +20 -20
  421. package/dist/rate-limit/middleware.js +9 -5
  422. package/dist/redis/client.js +2 -2
  423. package/dist/redis/pubsub-signal.js +14 -17
  424. package/dist/schema-cli/apply-command.d.ts +2 -0
  425. package/dist/schema-cli/apply-command.js +97 -0
  426. package/dist/schema-cli/baseline-command.d.ts +2 -0
  427. package/dist/schema-cli/baseline-command.js +33 -0
  428. package/dist/schema-cli/generate-command.d.ts +2 -0
  429. package/dist/schema-cli/generate-command.js +90 -0
  430. package/dist/schema-cli/shared.d.ts +39 -0
  431. package/dist/schema-cli/shared.js +95 -0
  432. package/dist/schema-cli/status-command.d.ts +2 -0
  433. package/dist/schema-cli/status-command.js +73 -0
  434. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  435. package/dist/schema-cli/validate-boot-input.js +130 -0
  436. package/dist/schema-cli/validate-command.d.ts +2 -0
  437. package/dist/schema-cli/validate-command.js +130 -0
  438. package/dist/schema-cli.d.ts +2 -18
  439. package/dist/schema-cli.js +47 -510
  440. package/dist/search/index.js +4 -4
  441. package/dist/search/meilisearch-adapter.js +2 -1
  442. package/dist/search/purge-subject.js +4 -5
  443. package/dist/search/reindex-entity.js +6 -9
  444. package/dist/time/boot-tz-warning.d.ts +2 -2
  445. package/dist/time/boot-tz-warning.js +8 -8
  446. package/dist/time/iana.js +10 -10
  447. package/dist/time/index.js +9 -9
  448. package/dist/time/legacy-date.d.ts +2 -2
  449. package/dist/time/legacy-date.js +6 -8
  450. package/dist/time/polyfill.js +1 -1
  451. package/dist/time/tz-context.d.ts +1 -1
  452. package/dist/time/tz-context.js +1 -1
  453. package/dist/ui-types/app-schema.js +7 -7
  454. package/dist/ui-types/list-row-meta.js +4 -4
  455. package/dist/upgrade-cli.js +159 -130
  456. package/dist/utils/compare.js +1 -1
  457. package/package.json +5 -5
  458. package/src/changes.json +358 -0
  459. package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
  460. package/src/scripts/codemod/feature-layout.ts +10 -2
  461. package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
  462. package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
  463. package/src/scripts/codemod/object-form.ts +41 -4
  464. package/src/scripts/codemod/package-merge.ts +58 -8
  465. package/src/scripts/codemod/pii-personal-migration.ts +192 -153
  466. package/src/scripts/codemod/testing-injection.ts +122 -42
  467. package/src/scripts/codemod/testing-move.ts +29 -7
  468. package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
@@ -0,0 +1,211 @@
1
+ import { resolveTenantLifecyclePlugin } from "../pipeline/active-membership.js";
2
+ import { authEndpointRateLimit, globalIpRateLimit } from "../rate-limit/index.js";
3
+ import { authMiddleware, getUser } from "./auth-middleware.js";
4
+ import { assertSessionCallbacksPaired } from "./auth-routes.js";
5
+ import { csrfMiddleware } from "./csrf-middleware.js";
6
+ import { observabilityMiddleware } from "./observability-middleware.js";
7
+ import { assertOriginGuardConfig, normalizeOrigin, resolveOriginGuard, } from "./origin-middleware.js";
8
+ import { patRouteGuard } from "./pat-route-guard.js";
9
+ import { piiCiphertextResponseGuard } from "./pii-leak-guard.js";
10
+ import { requestIdMiddleware } from "./request-id-middleware.js";
11
+ import { resolveSessionCookies, sessionCookiesMiddleware, } from "./session-cookies.js";
12
+ export function installSessionCookies(app, auth) {
13
+ // Resolved once; auth routes, auth/csrf middleware and the tenant-cookie
14
+ // helpers read the cookie names from the request context.
15
+ const sessionCookies = resolveSessionCookies({
16
+ secureCookies: auth?.secureCookies,
17
+ cookieDomain: auth?.cookieDomain,
18
+ retiredCookieDomains: auth?.retiredCookieDomains,
19
+ });
20
+ app.use("*", sessionCookiesMiddleware(sessionCookies));
21
+ return sessionCookies;
22
+ }
23
+ export function installRequestId(app, wiring) {
24
+ const { options, clientIpResolver } = wiring;
25
+ app.use("/api/*", requestIdMiddleware({
26
+ resolver: clientIpResolver,
27
+ ...(options.errorDocs && { errorDocs: options.errorDocs }),
28
+ }));
29
+ }
30
+ export function installRateLimitMiddleware(app, wiring) {
31
+ const { options, rateLimitResolver, clientIpResolver, observability } = wiring;
32
+ // L1/L2 rate-limit middleware run BEFORE auth so an unauthenticated
33
+ // flood can't even reach the JWT-verify code path. Wired only when
34
+ // the caller passed `rateLimit.global` or `rateLimit.auth`. The
35
+ // resolver is the auto-wired one (or `context.rateLimit` if set);
36
+ // boot-fails loudly when the caller asked for middleware without a
37
+ // working Redis to back it.
38
+ let globalRateLimitGuard;
39
+ if (wiring.rateLimitMiddlewareRequested) {
40
+ if (!rateLimitResolver) {
41
+ throw new Error("rateLimit middleware requested but no resolver available — pass `context.redis` " +
42
+ "or `context.rateLimit` so the resolver can be built.");
43
+ }
44
+ if (options.rateLimit?.global) {
45
+ globalRateLimitGuard = globalIpRateLimit({
46
+ ...options.rateLimit.global,
47
+ resolver: rateLimitResolver,
48
+ clientIpResolver,
49
+ meter: observability.meter,
50
+ });
51
+ app.use("/api/*", globalRateLimitGuard);
52
+ }
53
+ if (options.rateLimit?.auth) {
54
+ const { path: l2Path = "/api/auth/*", ...l2Opts } = options.rateLimit.auth;
55
+ app.use(l2Path, authEndpointRateLimit({ ...l2Opts, resolver: rateLimitResolver, clientIpResolver }));
56
+ }
57
+ }
58
+ return globalRateLimitGuard;
59
+ }
60
+ export function installObservability(app, observability) {
61
+ // Observability span wraps everything that follows (auth, routes).
62
+ // Must come AFTER request-id (so span can carry the id) and BEFORE auth
63
+ // (so auth-verify can be a child span once we instrument it in v2).
64
+ app.use("/api/*", observabilityMiddleware({
65
+ tracer: observability.tracer,
66
+ meter: observability.meter,
67
+ }));
68
+ }
69
+ export function installPiiLeakGuard(app) {
70
+ // Tripwire: a kumiko-pii: ciphertext in a JSON response is always a bug
71
+ // (raw read leaked past a decrypt) — dev fails loud, prod redacts.
72
+ app.use("/api/*", piiCiphertextResponseGuard());
73
+ }
74
+ export function installAuthGuardChain(app, wiring, scope) {
75
+ const { options, jwt, baseDb } = wiring;
76
+ const { isPublicApiRequest, globalRateLimitGuard } = scope;
77
+ // Auth middleware skips public paths (login, health) — those routes need
78
+ // to be callable without a valid JWT. Every other /api/* request requires
79
+ // a token (or, when anonymousAccess is wired, falls through as anonymous).
80
+ // A session-checker is forwarded when the auth-config wires one, so the
81
+ // middleware can reject revoked sids on every request.
82
+ // Mounting a tenantLifecycleStatus provider is what turns the 410 on, so no
83
+ // entrypoint can forget the wiring; `??` short-circuits, so an explicit
84
+ // auth.resolveTenantLifecycleStatus remains the override.
85
+ const tenantLifecycleResolver = options.auth?.resolveTenantLifecycleStatus ??
86
+ deriveTenantLifecycleResolver(options.registry, baseDb);
87
+ // sessionOnlyGuard deliberately does NOT get this — its callers have no
88
+ // anonymousAccess fallback, so a foreign-origin cookie there must keep
89
+ // failing through the sibling originMiddleware 403.
90
+ const normalizedForeignCookieOrigins = options.auth?.allowedOrigins?.length
91
+ ? new Set(options.auth.allowedOrigins.map(normalizeOrigin))
92
+ : undefined;
93
+ const jwtGuard = buildJwtGuard(options, jwt, tenantLifecycleResolver, normalizedForeignCookieOrigins);
94
+ useOnProtectedApiPaths(app, isPublicApiRequest, jwtGuard);
95
+ // Fail-closed PAT scoping: a PAT-authenticated caller (user.pat set by
96
+ // jwtGuard's tokenVerifier above) may only reach the dispatcher routes —
97
+ // everywhere else on /api/* is off-limits regardless of granted scopes.
98
+ const patRouteGuardMiddleware = patRouteGuard();
99
+ useOnProtectedApiPaths(app, isPublicApiRequest, patRouteGuardMiddleware);
100
+ // Without anonymousAccess a missing token 401s instead of falling through as anonymous.
101
+ const sessionOnlyGuard = buildSessionOnlyGuard(options, jwt, tenantLifecycleResolver);
102
+ // PAT rate limiting — runs AFTER the auth guard so the resolved principal is
103
+ // available. Only PAT-authenticated requests are counted (keyed by token id);
104
+ // cookie/JWT users pass through untouched. In-memory limiter is per-instance
105
+ // (see run-prod-app) — a multi-node deployment wanting a shared counter swaps
106
+ // in a Redis-backed LoginRateLimiter.
107
+ const patRateLimiter = options.auth?.patRateLimiter;
108
+ const patRateLimitGuard = patRateLimiter ? buildPatRateLimitGuard(patRateLimiter) : undefined;
109
+ if (patRateLimitGuard) {
110
+ useOnProtectedApiPaths(app, isPublicApiRequest, patRateLimitGuard);
111
+ }
112
+ // Origin-allowlist guard — additional CSRF-hardening for deployments that
113
+ // widen the auth cookie across subdomains (auth.cookieDomain). Registered
114
+ // when allowedOrigins is non-empty, or with host-only cookies when
115
+ // auth.publicOrigin is known (see resolveOriginGuard), with the same /api/* + public-skip
116
+ // scope as the CSRF guard and BEFORE it, so a disallowed cross-site POST
117
+ // surfaces as `origin_not_allowed` rather than `csrf_token_mismatch`. Fails
118
+ // closed (assertOriginGuardConfig throws) when a wide cookieDomain is set
119
+ // without an allowlist and without an explicit opt-out — that config is the
120
+ // unguarded-subdomain-XSS footgun, not a warn-and-continue case.
121
+ assertOriginGuardConfig(options.auth);
122
+ assertSessionCallbacksPaired(options.auth);
123
+ const originGuard = resolveOriginGuard(options.auth);
124
+ if (originGuard) {
125
+ useOnProtectedApiPaths(app, isPublicApiRequest, originGuard);
126
+ }
127
+ // Double-submit CSRF guard — runs only on cookie-authenticated,
128
+ // state-changing requests (POST/PUT/PATCH/DELETE). The guard reads the
129
+ // authTransport flag set by authMiddleware, so public paths (no auth)
130
+ // and bearer-authenticated paths (no cookie vector) fall straight
131
+ // through. Must be registered AFTER the auth middleware above so the
132
+ // flag is populated; registered for the same scope so /api/* routes
133
+ // are covered uniformly.
134
+ const csrfGuard = csrfMiddleware();
135
+ useOnProtectedApiPaths(app, isPublicApiRequest, csrfGuard);
136
+ // No PAT rate-limit here: patRouteGuardMiddleware rejects every PAT on httpRoutes before it could count.
137
+ // httpRoutes sit outside /api/*, so the pre-auth /api/* middlewares do not run for them: the
138
+ // global IP limit (an unauthenticated flood must not reach JWT verify / session checks) and the
139
+ // PII ciphertext tripwire are re-attached here. requestId, body limit and observability are not.
140
+ const sessionOnlyHttpRouteGuards = [
141
+ ...(globalRateLimitGuard ? [globalRateLimitGuard] : []),
142
+ piiCiphertextResponseGuard(),
143
+ sessionOnlyGuard,
144
+ patRouteGuardMiddleware,
145
+ ...(originGuard ? [originGuard] : []),
146
+ csrfGuard,
147
+ ];
148
+ return { tenantLifecycleResolver, sessionOnlyHttpRouteGuards };
149
+ }
150
+ function useOnProtectedApiPaths(app, isPublicApiRequest, guard) {
151
+ app.use("/api/*", async (c, next) => {
152
+ if (isPublicApiRequest(c))
153
+ return next();
154
+ return guard(c, next);
155
+ });
156
+ }
157
+ function collectTenantlessAnonymousWriteQns(registry) {
158
+ return new Set([...registry.getAllWriteHandlers()]
159
+ .filter(([, handler]) => handler.tenantlessAnonymous === true)
160
+ .map(([qn]) => qn));
161
+ }
162
+ function buildJwtGuard(options, jwt, tenantLifecycleResolver, normalizedForeignCookieOrigins) {
163
+ const tenantlessAnonymousQns = collectTenantlessAnonymousWriteQns(options.registry);
164
+ return authMiddleware(jwt, {
165
+ ...(tenantlessAnonymousQns.size > 0
166
+ ? { isTenantlessAnonymousWrite: (qn) => tenantlessAnonymousQns.has(qn) }
167
+ : {}),
168
+ ...(options.auth?.sessionChecker ? { sessionChecker: options.auth.sessionChecker } : {}),
169
+ ...(options.auth?.tokenVerifier ? { tokenVerifier: options.auth.tokenVerifier } : {}),
170
+ ...(tenantLifecycleResolver ? { resolveTenantLifecycleStatus: tenantLifecycleResolver } : {}),
171
+ ...(options.anonymousAccess ? { anonymousAccess: options.anonymousAccess } : {}),
172
+ ...(normalizedForeignCookieOrigins
173
+ ? { foreignCookieOrigins: normalizedForeignCookieOrigins }
174
+ : {}),
175
+ });
176
+ }
177
+ function buildSessionOnlyGuard(options, jwt, tenantLifecycleResolver) {
178
+ return authMiddleware(jwt, {
179
+ ...(options.auth?.sessionChecker ? { sessionChecker: options.auth.sessionChecker } : {}),
180
+ ...(options.auth?.tokenVerifier ? { tokenVerifier: options.auth.tokenVerifier } : {}),
181
+ ...(tenantLifecycleResolver ? { resolveTenantLifecycleStatus: tenantLifecycleResolver } : {}),
182
+ });
183
+ }
184
+ // Must run after the auth guard so getUser(c) carries the PAT.
185
+ function buildPatRateLimitGuard(patRateLimiter) {
186
+ return async (c, next) => {
187
+ const pat = getUser(c)?.pat;
188
+ if (pat && !(await patRateLimiter.check(pat.tokenId))) {
189
+ return c.json({
190
+ error: {
191
+ code: "pat_rate_limited",
192
+ httpStatus: 429,
193
+ message: "personal access token rate limit exceeded",
194
+ i18nKey: "auth.errors.patRateLimited",
195
+ },
196
+ }, 429);
197
+ }
198
+ return next();
199
+ };
200
+ }
201
+ function deriveTenantLifecycleResolver(registry, db) {
202
+ const plugin = resolveTenantLifecyclePlugin(registry, "buildServer");
203
+ if (!plugin)
204
+ return undefined;
205
+ if (!db) {
206
+ throw new Error("[kumiko] a tenantLifecycleStatus provider is mounted (tenant-lifecycle) but context.db is " +
207
+ "missing — the request-level teardown gate needs a DbConnection. Pass context.db, or wire " +
208
+ "auth.resolveTenantLifecycleStatus explicitly.");
209
+ }
210
+ return (tenantId) => plugin.resolveStatus(tenantId, { db });
211
+ }
@@ -0,0 +1,19 @@
1
+ import type { Hono, MiddlewareHandler } from "hono";
2
+ import type { HttpRouteMethod, SessionUser, TenantId } from "../engine/types/index.ts";
3
+ import type { Dispatcher } from "../pipeline/dispatcher.ts";
4
+ import { type ClientIpResolver } from "./client-ip.ts";
5
+ import type { ServerOptions } from "./server.ts";
6
+ import type { ServerWiring } from "./server-wiring.ts";
7
+ import type { ResolvedSessionCookies } from "./session-cookies.ts";
8
+ export declare function mountOperationalRoutes(app: Hono, wiring: Pick<ServerWiring, "options" | "baseDb" | "eventConsumers" | "observability" | "bootLog">): void;
9
+ export declare function mountAuthRoutes(app: Hono, wiring: Pick<ServerWiring, "options" | "dispatcher" | "jwt" | "clientIpResolver">, sessionCookies: ResolvedSessionCookies): void;
10
+ export declare function mountClientErrorRoute(app: Hono, wiring: Pick<ServerWiring, "rateLimitResolver" | "tracedRedis" | "clientIpResolver" | "observability" | "jwt">): void;
11
+ export declare function mountDispatcherRoutes(app: Hono, wiring: Pick<ServerWiring, "options" | "dispatcher" | "sseBroker">): void;
12
+ export declare function mountSchemaRoute(app: Hono, options: Pick<ServerOptions, "registry" | "dispatcherOptions" | "context">): void;
13
+ export declare function mountFileRoutes(app: Hono, wiring: Pick<ServerWiring, "options" | "dispatcher" | "fileProviderResolver">): void;
14
+ export declare function mountFeatureHttpRoutes(app: Hono, wiring: Pick<ServerWiring, "options" | "dispatcher" | "clientIpResolver" | "rateLimitResolver" | "observability">, sessionOnlyHttpRouteGuards: readonly MiddlewareHandler[]): void;
15
+ export declare function installNoRouteMatchMarker(app: Hono): void;
16
+ export declare function mountHonoRoute(app: Hono, method: HttpRouteMethod, path: string, handler: (c: import("hono").Context<any, any>) => Response | Promise<Response>, middlewares?: readonly MiddlewareHandler[]): void;
17
+ export declare function makeSystemQuery(c: import("hono").Context<any, any>, dispatcher: Dispatcher, clientIpResolver?: ClientIpResolver): (type: string, payload: unknown, tenantId: TenantId) => Promise<unknown>;
18
+ export declare function isMissingOrAnonymousUser(user: SessionUser | undefined): boolean;
19
+ export declare function unauthenticatedResponse(c: import("hono").Context): Response;
@@ -0,0 +1,293 @@
1
+ import { createProjectingTenantDb } from "../db/tenant-db.js";
2
+ import { buildAppSchema } from "../engine/build-app-schema.js";
3
+ import { projectAppSchemaForRoles } from "../engine/project-app-schema-for-roles.js";
4
+ import { resolveDisabledFeatureNames, stripDisabledFeaturesFromAppSchema, } from "../engine/strip-disabled-features-from-app-schema.js";
5
+ import { createAnonymousUser, isAnonymousSessionUser } from "../engine/system-user.js";
6
+ import { createFileRoutes, readFilesRouteOptions } from "../files/file-routes.js";
7
+ import { createRecordReadCheck } from "../files/record-read-check.js";
8
+ import { createRecordWriteCheck } from "../files/record-write-check.js";
9
+ import { createRateLimitResolver, httpRouteRateLimit } from "../rate-limit/index.js";
10
+ import { assertUnreachable } from "../utils/index.js";
11
+ import { collectAnonymousLiveEntities } from "./anonymous-live-entities.js";
12
+ import { NO_ROUTE_MATCH_HEADER_NAME, Routes } from "./api-constants.js";
13
+ import { getUser, identifyOptionalSession } from "./auth-middleware.js";
14
+ import { createAuthRoutes } from "./auth-routes.js";
15
+ import { createClientErrorRoute } from "./client-error-route.js";
16
+ import { clientIpSourceFromHonoContext } from "./client-ip.js";
17
+ import { collectEntityReadAccess } from "./entity-read-access.js";
18
+ import { computeStrongEtag, etagMatches } from "./http-cache.js";
19
+ import { requestContext } from "./request-context.js";
20
+ import { buildRequestContextData } from "./request-id-middleware.js";
21
+ import { registerHealthRoutes, registerMetricsRoute } from "./route-registrars.js";
22
+ import { createApiRoutes } from "./routes.js";
23
+ import { assertSinglePrincipalStatusProvider } from "./server-boot-checks.js";
24
+ import { createSseRoute } from "./sse-route.js";
25
+ export function mountOperationalRoutes(app, wiring) {
26
+ const { options, baseDb, eventConsumers, observability, bootLog } = wiring;
27
+ registerHealthRoutes(app, {
28
+ lifecycle: options.lifecycle,
29
+ readiness: {
30
+ db: baseDb,
31
+ redis: options.context.redis,
32
+ consumers: eventConsumers,
33
+ ...(options.readiness ?? {}),
34
+ },
35
+ });
36
+ if (options.metrics) {
37
+ registerMetricsRoute(app, observability.meter, options.metrics);
38
+ }
39
+ else if (process.env["NODE_ENV"] === "production") {
40
+ bootLog.warn("/metrics is off: no `metrics` option, so nothing is measured or scraped. " +
41
+ "Set PROMETHEUS_METRICS_TOKEN (min 32 chars) and spread resolveObservabilityWiring(token) into the server options.");
42
+ }
43
+ }
44
+ export function mountAuthRoutes(app, wiring, sessionCookies) {
45
+ const { options, dispatcher, jwt, clientIpResolver } = wiring;
46
+ if (options.auth) {
47
+ assertSinglePrincipalStatusProvider(options.registry, options.auth);
48
+ app.route("/api", createAuthRoutes(dispatcher, jwt, {
49
+ ...options.auth,
50
+ secureCookies: sessionCookies.secure,
51
+ resolver: clientIpResolver,
52
+ }));
53
+ }
54
+ }
55
+ export function mountClientErrorRoute(app, wiring) {
56
+ const { rateLimitResolver, tracedRedis, clientIpResolver, observability, jwt } = wiring;
57
+ app.route("/api", createClientErrorRoute({
58
+ resolver: rateLimitResolver ??
59
+ (tracedRedis ? createRateLimitResolver({ redis: tracedRedis }) : undefined),
60
+ clientIpResolver,
61
+ meter: observability.meter,
62
+ identifySession: (c) => identifyOptionalSession(c, jwt),
63
+ }));
64
+ }
65
+ export function mountDispatcherRoutes(app, wiring) {
66
+ const { options, dispatcher, sseBroker } = wiring;
67
+ app.route("/api", createApiRoutes(dispatcher, {
68
+ ...(options.sseHeartbeatMs !== undefined ? { sseHeartbeatMs: options.sseHeartbeatMs } : {}),
69
+ }));
70
+ app.route("/api", createSseRoute(sseBroker, {
71
+ anonymousLiveEntities: collectAnonymousLiveEntities(options.registry),
72
+ entityReadAccess: collectEntityReadAccess(options.registry),
73
+ }));
74
+ }
75
+ export function mountSchemaRoute(app, options) {
76
+ // GET /api/schema — the only way a client obtains the AppSchema. Behind
77
+ // jwtGuard (NON_PUBLIC_API_PATHS), so anonymousAccess's synthesised user
78
+ // also gets rejected below — the schema never reaches an unauthenticated
79
+ // caller. The FULL schema is built lazily once — the registry is
80
+ // immutable for this server's lifetime, so it's the same on every
81
+ // request — then projected per caller-role-set (a role only
82
+ // sees the screens/navs/workspaces/content-collections it may reach,
83
+ // not the whole app's schema) and cached per role key. Tenant is
84
+ // deliberately NOT part of the key: the projection depends only on
85
+ // roles, and per-tenant caching would grow unboundedly across tenants
86
+ // that share the same role set. What IS part of the key: the sorted set of
87
+ // features the caller's tenant has switched off (r.toggleable) — few
88
+ // distinct sets, so the cache stays small. The resolver is read per request
89
+ // because run{Dev,Prod}App fill it in after the server is built.
90
+ let fullSchema;
91
+ const projectedSchemaCache = new Map();
92
+ app.get(`/api${Routes.schema}`, async (c) => {
93
+ const user = getUser(c);
94
+ if (isMissingOrAnonymousUser(user)) {
95
+ return unauthenticatedResponse(c);
96
+ }
97
+ const effectiveFeatures = options.dispatcherOptions?.effectiveFeatures;
98
+ const disabledFeatureNames = effectiveFeatures === undefined
99
+ ? []
100
+ : await resolveDisabledFeatureNames(options.registry, effectiveFeatures, user.tenantId);
101
+ const roleKey = JSON.stringify([[...new Set(user?.roles ?? [])].sort(), disabledFeatureNames]);
102
+ let cached = projectedSchemaCache.get(roleKey);
103
+ if (cached === undefined) {
104
+ if (fullSchema === undefined) {
105
+ fullSchema = buildAppSchema(options.registry, {
106
+ searchAdapterMissing: !options.context.searchAdapter,
107
+ });
108
+ }
109
+ const projected = projectAppSchemaForRoles(stripDisabledFeaturesFromAppSchema(fullSchema, options.registry, disabledFeatureNames), user?.roles ?? []);
110
+ const json = JSON.stringify(projected);
111
+ cached = { json, etag: computeStrongEtag(json) };
112
+ projectedSchemaCache.set(roleKey, cached);
113
+ }
114
+ const cacheHeaders = { "Cache-Control": "private, no-cache", ETag: cached.etag };
115
+ if (etagMatches(c.req.header("If-None-Match") ?? null, cached.etag)) {
116
+ return c.body(null, 304, cacheHeaders);
117
+ }
118
+ return c.body(cached.json, 200, {
119
+ "Content-Type": "application/json; charset=utf-8",
120
+ ...cacheHeaders,
121
+ });
122
+ });
123
+ }
124
+ export function mountFileRoutes(app, wiring) {
125
+ const { options, dispatcher, fileProviderResolver } = wiring;
126
+ // Mount upload/download routes whenever a file provider is resolvable (a
127
+ // file-provider plugin is mounted, or a resolver was injected). They resolve
128
+ // the provider per-tenant through file-foundation — no `files` option; route
129
+ // policy comes from createFilesFeature(opts?). Gating on the resolver (not on
130
+ // declared file fields) keeps unattached uploads working and matches the old
131
+ // "files option present → routes" behavior.
132
+ if (fileProviderResolver) {
133
+ const fileDb = options.context.db; // @cast-boundary engine-bridge
134
+ if (!fileDb)
135
+ throw new Error("file routes require db in context");
136
+ const routeOptions = readFilesRouteOptions(options.registry);
137
+ const canReadRecord = createRecordReadCheck(options.registry, dispatcher.query);
138
+ app.route("/api", createFileRoutes({
139
+ db: fileDb,
140
+ registry: options.registry,
141
+ resolveProvider: fileProviderResolver,
142
+ canReadRecord,
143
+ canWriteRecord: createRecordWriteCheck({
144
+ registry: options.registry,
145
+ canReadRecord,
146
+ tenantDbFor: (tenantId) => createProjectingTenantDb(fileDb, tenantId, options.registry),
147
+ }),
148
+ ...(routeOptions.accessGuard ? { accessGuard: routeOptions.accessGuard } : {}),
149
+ ...(routeOptions.privilegedRoles ? { privilegedRoles: routeOptions.privilegedRoles } : {}),
150
+ ...(routeOptions.unclaimedUploadReaders
151
+ ? { unclaimedUploadReaders: routeOptions.unclaimedUploadReaders }
152
+ : {}),
153
+ ...(routeOptions.maxUploadSize ? { maxUploadSize: routeOptions.maxUploadSize } : {}),
154
+ }));
155
+ }
156
+ }
157
+ export function mountFeatureHttpRoutes(app, wiring, sessionOnlyHttpRouteGuards) {
158
+ // Feature-declared HTTP routes (r.httpRoute). Mount after /api/* so /api/* always
159
+ // takes precedence; feature routes must be outside that prefix (the boot validator
160
+ // enforces this). deps.app is the outer app, so the handler can call /api/query
161
+ // internally through app.fetch(...) and use the same auth path as a real HTTP call.
162
+ // deps.systemQuery deliberately bypasses that path for routes that must enforce a
163
+ // FIXED tenant (for example, legal-pages always uses SYSTEM_TENANT_ID), rather than
164
+ // spoofing it through the X-Tenant header. That header looks like a normal client
165
+ // header to anonymousAccess resolution, so resolverTrust: "authoritative" configs
166
+ // correctly reject it as a tenant override.
167
+ for (const feature of wiring.options.registry.features.values()) {
168
+ for (const route of Object.values(feature.httpRoutes)) {
169
+ mountFeatureHttpRoute(app, route, wiring, sessionOnlyHttpRouteGuards);
170
+ }
171
+ }
172
+ }
173
+ function mountFeatureHttpRoute(app, route, wiring, sessionOnlyHttpRouteGuards) {
174
+ const { dispatcher, clientIpResolver, rateLimitResolver, observability } = wiring;
175
+ // @wrapper-known semantic-alias
176
+ const honoHandler = async (c) => route.handler(c, {
177
+ app,
178
+ // createAnonymousUser, NOT createSystemUser: systemQuery's
179
+ // synthesized user must clear the SAME access gate a real
180
+ // anonymous visitor would, no more — regardless of the route's
181
+ // own `anonymous` mode. The system role would ALSO satisfy that
182
+ // gate here, but it can read fields gated to "system" that
183
+ // "anonymous" can't (filterReadFields is a plain role-in-map
184
+ // check) — a systemQuery caller reading a system-gated field
185
+ // would leak it into the response. The forced tenant already
186
+ // comes from bypassing the HTTP layer entirely; no elevated
187
+ // role is needed or wanted on top of that.
188
+ systemQuery: makeSystemQuery(c, dispatcher, clientIpResolver),
189
+ clientIp: clientIpResolver.resolve(clientIpSourceFromHonoContext(c)),
190
+ });
191
+ // Without a resolver (no Redis in the context — dev/test stacks) the
192
+ // declared limit is skipped rather than failing the boot of every app
193
+ // that mounts a bundled feature whose routes carry a default limit.
194
+ const routeLimit = route.rateLimit && rateLimitResolver
195
+ ? [
196
+ httpRouteRateLimit({
197
+ resolver: rateLimitResolver,
198
+ rateLimit: route.rateLimit,
199
+ routeKey: `${route.method} ${route.path}`,
200
+ clientIpResolver,
201
+ meter: observability.meter,
202
+ }),
203
+ ]
204
+ : [];
205
+ mountHonoRoute(app, route.method, route.path, honoHandler, [
206
+ ...routeLimit,
207
+ ...(route.anonymous ? [] : sessionOnlyHttpRouteGuards),
208
+ ]);
209
+ }
210
+ export function installNoRouteMatchMarker(app) {
211
+ // Marks "no route matched this path" so tryHonoFirst (server-runtime)
212
+ // can tell it apart from a matched route's OWN deliberate 404 (e.g.
213
+ // default-deny reads like file-derivatives' public-variant route) —
214
+ // without this, tryHonoFirst's only signal was the status code, and a
215
+ // matched route answering 404 got silently rewritten to the SPA shell
216
+ // with status 200. Must be the LAST thing wired
217
+ // on `app`: this only fires when the router found no handler at all, so
218
+ // a route's own `c.text(..., 404)` never passes through here. A handler
219
+ // that calls `c.notFound()` instead of building its own Response DOES
220
+ // route through this same handler and stays indistinguishable from a
221
+ // routing miss — pre-existing Hono semantics, not something this fix
222
+ // changes; deliberate 404s must build their own Response.
223
+ // The header is an internal signal only — tryHonoFirst and every direct
224
+ // app.fetch() passthrough strip it before a response reaches a client.
225
+ app.notFound((c) => c.text("Not Found", 404, { [NO_ROUTE_MATCH_HEADER_NAME]: "1" }));
226
+ }
227
+ // Method-switch shared by r.httpRoute and extraRoutes — one place to keep
228
+ // the two mounting paths from drifting on which Hono methods get a
229
+ // convenience-call vs. app.on().
230
+ export function mountHonoRoute(app, method, path,
231
+ // biome-ignore lint/suspicious/noExplicitAny: Hono context generics are invisible at the framework boundary
232
+ handler, middlewares = []) {
233
+ // Guards go into the route's own handler chain, never a method-gated
234
+ // app.use: Hono serves HEAD through the GET route with c.req.method still
235
+ // "HEAD", so a `method === c.req.method` gate would skip them for HEAD.
236
+ if (middlewares.length > 0) {
237
+ // [path]: only Hono's array-path overload accepts a variable-length handler spread.
238
+ app.on(method, [path], ...middlewares, handler);
239
+ // skip: guarded route already mounted with its guard chain
240
+ return;
241
+ }
242
+ switch (method) {
243
+ case "GET":
244
+ app.get(path, handler);
245
+ break;
246
+ case "POST":
247
+ app.post(path, handler);
248
+ break;
249
+ case "PUT":
250
+ app.put(path, handler);
251
+ break;
252
+ case "PATCH":
253
+ app.patch(path, handler);
254
+ break;
255
+ case "DELETE":
256
+ app.delete(path, handler);
257
+ break;
258
+ case "OPTIONS":
259
+ case "HEAD":
260
+ // Hono's on() for the methods without a convenience method.
261
+ app.on(method, path, handler);
262
+ break;
263
+ default:
264
+ assertUnreachable(method, "http method");
265
+ }
266
+ }
267
+ // Shared systemQuery builder for r.httpRoute and extraRoutes (anonymous +
268
+ // signature entries). requestContext.run must wrap the dispatcher call —
269
+ // both route kinds run outside the requestIdMiddleware chain that normally
270
+ // populates it, and `rateLimit: {per: "ip", ...}` on a handler invoked
271
+ // through systemQuery is otherwise silent dead-code (enforceRateLimit reads
272
+ // requestContext.get()?.ip, undefined without this wrap).
273
+ export function makeSystemQuery(
274
+ // biome-ignore lint/suspicious/noExplicitAny: Hono context generics are invisible at the framework boundary
275
+ c, dispatcher, clientIpResolver) {
276
+ return (type, payload, tenantId) => requestContext.run(requestContext.get() ?? buildRequestContextData(c, clientIpResolver), () => dispatcher.query(type, payload, createAnonymousUser(tenantId)));
277
+ }
278
+ // Shared by entry:"user" extra routes and GET /api/schema — both require a
279
+ // real, signed-in principal and reject the synthesised anonymousAccess user
280
+ // (SessionUser.roles includes ANONYMOUS_ROLE) the same way.
281
+ export function isMissingOrAnonymousUser(user) {
282
+ return !user || isAnonymousSessionUser(user);
283
+ }
284
+ export function unauthenticatedResponse(c) {
285
+ return c.json({
286
+ error: {
287
+ code: "unauthenticated",
288
+ httpStatus: 401,
289
+ message: "this route requires a signed-in user",
290
+ i18nKey: "auth.errors.missingToken",
291
+ },
292
+ }, 401);
293
+ }
@@ -0,0 +1,4 @@
1
+ import type { Hono } from "hono";
2
+ import { type TenantLifecycleStatusResolver } from "./auth-middleware.ts";
3
+ import type { ServerWiring } from "./server-wiring.ts";
4
+ export declare function mountWebSocketRoutes(app: Hono, wiring: Pick<ServerWiring, "options" | "dispatcher" | "clientIpResolver">, tenantLifecycleResolver: TenantLifecycleStatusResolver | undefined): void;
@@ -0,0 +1,108 @@
1
+ import { createFallbackLogger } from "../logging/index.js";
2
+ import { getAuthTokenExpiry, getUser, } from "./auth-middleware.js";
3
+ import { clientIpSourceFromHonoContext } from "./client-ip.js";
4
+ import { isWebSocketOriginAllowed, normalizeOrigin, rejectOrigin } from "./origin-middleware.js";
5
+ import { requestContext } from "./request-context.js";
6
+ import { isMissingOrAnonymousUser, unauthenticatedResponse } from "./server-routes.js";
7
+ import { buildWebSocketSessionRevalidator, createWebSocketConnectionLimiter, hasWebSocketUpgradeServer, isWebSocketUpgradeRequest, WEBSOCKET_DEFAULT_MAX_CONNECTIONS_PER_USER, WEBSOCKET_DEFAULT_MAX_MESSAGE_BYTES, } from "./websocket-route.js";
8
+ export function mountWebSocketRoutes(app, wiring, tenantLifecycleResolver) {
9
+ const { options, dispatcher, clientIpResolver } = wiring;
10
+ // r.webSocketRoute — GET routes under /api/ws/*, so the jwt/pat/rate-limit
11
+ // chain above already ran. Origin/CSRF guards skip GETs, hence the own check.
12
+ const webSocketOriginAllowlist = options.auth?.allowedOrigins?.length
13
+ ? new Set(options.auth.allowedOrigins.map(normalizeOrigin))
14
+ : undefined;
15
+ const webSocketRoutePaths = new Map();
16
+ const webSocketConnectionLimiter = createWebSocketConnectionLimiter();
17
+ for (const feature of options.registry.features.values()) {
18
+ for (const route of Object.values(feature.webSocketRoutes)) {
19
+ const owner = webSocketRoutePaths.get(route.path);
20
+ if (owner !== undefined) {
21
+ throw new Error(`[kumiko] WebSocket route "${route.path}" is declared by both feature "${owner}" and ` +
22
+ `"${feature.name}" — paths must be unique across features.`);
23
+ }
24
+ webSocketRoutePaths.set(route.path, feature.name);
25
+ app.get(route.path, buildWebSocketRouteHandler(route, {
26
+ dispatcher,
27
+ clientIpResolver,
28
+ originAllowlist: webSocketOriginAllowlist,
29
+ ...(options.auth?.sessionChecker ? { sessionChecker: options.auth.sessionChecker } : {}),
30
+ ...(tenantLifecycleResolver
31
+ ? { resolveTenantLifecycleStatus: tenantLifecycleResolver }
32
+ : {}),
33
+ connectionLimiter: webSocketConnectionLimiter,
34
+ }));
35
+ }
36
+ }
37
+ }
38
+ const webSocketLog = createFallbackLogger("websocket");
39
+ function webSocketErrorResponse(c, status, code, message) {
40
+ return c.json({ error: { code, httpStatus: status, message } }, status);
41
+ }
42
+ function buildWebSocketRouteHandler(route, shared) {
43
+ return async (c) => {
44
+ if (!isWebSocketOriginAllowed(c, shared.originAllowlist))
45
+ return rejectOrigin(c);
46
+ const user = getUser(c);
47
+ if (isMissingOrAnonymousUser(user))
48
+ return unauthenticatedResponse(c);
49
+ if (!isWebSocketUpgradeRequest(c.req.raw)) {
50
+ return webSocketErrorResponse(c, 426, "websocket_upgrade_required", "this route only accepts WebSocket upgrade requests");
51
+ }
52
+ const honoEnv = c.env;
53
+ if (!hasWebSocketUpgradeServer(honoEnv)) {
54
+ const message = "webSocketUpgradeFetch not wired: pass it to buildBunServeOptions";
55
+ webSocketLog.error(message, { path: route.path });
56
+ return webSocketErrorResponse(c, 501, "websocket_upgrade_not_wired", message);
57
+ }
58
+ // The slot is taken before connect (a refused attempt must not run it) and stays synchronous with
59
+ // the check, so concurrent upgrades can't overshoot the cap. Every path without a live socket frees it.
60
+ const releaseConnectionSlot = shared.connectionLimiter.tryAcquire(`${route.path}\n${user.tenantId}\n${user.id}`, route.maxConnectionsPerUser ?? WEBSOCKET_DEFAULT_MAX_CONNECTIONS_PER_USER);
61
+ if (!releaseConnectionSlot) {
62
+ return webSocketErrorResponse(c, 429, "websocket_connection_limit", "too many open WebSocket connections for this user");
63
+ }
64
+ let upgraded = false;
65
+ try {
66
+ const handlers = await route.connect(c, {
67
+ user,
68
+ query: (type, payload) => shared.dispatcher.query(type, payload, user),
69
+ write: (type, payload) => shared.dispatcher.write(type, payload, user),
70
+ clientIp: shared.clientIpResolver.resolve(clientIpSourceFromHonoContext(c)),
71
+ });
72
+ if (handlers instanceof Response)
73
+ return handlers;
74
+ const captured = requestContext.get();
75
+ const requestContextData = captured ? withoutAbortSignal(captured) : undefined;
76
+ const revalidateSession = buildWebSocketSessionRevalidator({
77
+ user,
78
+ sessionChecker: shared.sessionChecker,
79
+ resolveTenantLifecycleStatus: shared.resolveTenantLifecycleStatus,
80
+ tokenExpiresAtSec: getAuthTokenExpiry(c),
81
+ });
82
+ upgraded = honoEnv.server.upgrade(c.req.raw, {
83
+ data: {
84
+ handlers,
85
+ maxMessageBytes: route.maxMessageBytes ?? WEBSOCKET_DEFAULT_MAX_MESSAGE_BYTES,
86
+ requestContextData,
87
+ revalidateSession,
88
+ releaseConnectionSlot,
89
+ },
90
+ });
91
+ if (!upgraded) {
92
+ return webSocketErrorResponse(c, 400, "websocket_upgrade_failed", "the WebSocket upgrade was rejected");
93
+ }
94
+ // Bun ignores the returned Response after a successful upgrade.
95
+ return new Response(null);
96
+ }
97
+ finally {
98
+ if (!upgraded)
99
+ releaseConnectionSlot();
100
+ }
101
+ };
102
+ }
103
+ // The request's AbortSignal fires when the upgrade request ends, which would
104
+ // abort every later handler running under the captured context.
105
+ function withoutAbortSignal(data) {
106
+ const { signal: _signal, ...rest } = data;
107
+ return rest;
108
+ }