@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,157 @@
1
+ import { EXT_FILE_PROVIDER, EXT_PRINCIPAL_STATUS } from "../engine/extension-names.js";
2
+ import { isFileField } from "../engine/types/index.js";
3
+ import { SHARED_INSTANCE_SENTINEL } from "../pipeline/event-consumer-state.js";
4
+ import { generateId } from "../utils/index.js";
5
+ // A payload-bucket handler without a resolver would answer every call with a
6
+ // 500 at request time; fail at boot instead.
7
+ export function assertNoPayloadRateLimitsWithoutResolver(registry) {
8
+ const offenders = [];
9
+ for (const feature of registry.features.values()) {
10
+ for (const [name, handler] of Object.entries(feature.writeHandlers)) {
11
+ if (handler.additionalRateLimits !== undefined) {
12
+ offenders.push(`${feature.name}:write:${name}`);
13
+ }
14
+ }
15
+ }
16
+ // skip: every handler's payload buckets are covered by the resolver
17
+ if (offenders.length === 0)
18
+ return;
19
+ throw new Error(`Handlers declare additionalRateLimits but no RateLimitResolver is configured: ${offenders.join(", ")}. ` +
20
+ "Wire a Redis client on context.redis (or pass context.rateLimit), or drop the payload buckets " +
21
+ "(for the waitlist: emailRateLimits: []).");
22
+ }
23
+ export function assertFileStorageProviderForFileFields(registry, context) {
24
+ // File-storage is resolved per-tenant through file-foundation: a mounted
25
+ // `file-provider-*` plugin (inmemory/s3/s3-env) is the single source for
26
+ // uploads, ctx.files and the GDPR jobs.
27
+ const hasFileProvider = registry.getExtensionUsages(EXT_FILE_PROVIDER).length > 0;
28
+ // A test/advanced caller may inject the resolver directly on the context
29
+ // (e.g. a static in-memory provider) instead of mounting a provider plugin.
30
+ const hasInjectedResolver = context._fileProviderResolver !== undefined;
31
+ // Hard-fail when the registry declares file/image fields but no provider
32
+ // plugin is mounted — uploads would otherwise fail at the first request
33
+ // instead of at boot. Proves a plugin is mounted, not that each tenant
34
+ // selected one; per-tenant misconfig surfaces via createFileProviderForTenant.
35
+ if (registryDeclaresFileFields(registry) && !hasFileProvider && !hasInjectedResolver) {
36
+ throw new Error("Features declare file/image fields but no file-storage provider is mounted — " +
37
+ "mount `file-foundation` + a `file-provider-*` feature (inmemory/s3/s3-env) and " +
38
+ "select one per tenant via the 'file-foundation:config:provider' config-key.");
39
+ }
40
+ }
41
+ export function warnSearchableScreensWithoutSearchAdapter(registry, context, bootLog) {
42
+ // A screen whose search box renders (kumiko-screen.tsx gates it on
43
+ // `screen.searchable ?? entity-has-searchable-field`, the same condition
44
+ // as entitiesWithSearchableScreen) sends `payload.search` on every query. Without a
45
+ // context.searchAdapter that throws UnprocessableError at request time
46
+ // — surface the misconfig at boot instead of on the first search.
47
+ // Warn, not throw: unlike missing file-storage (uploads always fail),
48
+ // list screens still work without search: only the search box is broken.
49
+ // Several deployed apps (offlot-app, publicstatus, kumiko-studio,
50
+ // kumiko-enterprise) currently run in exactly this state.
51
+ if (!context.searchAdapter) {
52
+ const unwiredEntities = entitiesWithSearchableScreen(registry);
53
+ if (unwiredEntities.length > 0) {
54
+ bootLog.warn(`${unwiredEntities.length} entit${unwiredEntities.length === 1 ? "y" : "ies"} ` +
55
+ `have a searchable list screen but no SearchAdapter is wired on context.searchAdapter: ` +
56
+ `${unwiredEntities.join(", ")}. Search requests against ${unwiredEntities.length === 1 ? "it" : "them"} will fail with a 422 (search_adapter_not_wired) at runtime. ` +
57
+ "Wire a SearchAdapter (e.g. createMeilisearchAdapter) on context.searchAdapter, or remove `searchable: true` from the affected fields.");
58
+ }
59
+ }
60
+ }
61
+ export function resolveServerInstanceId(explicitInstanceId, bootLog) {
62
+ // Resolve the per-process instance identifier. Prefer explicit
63
+ // ServerOptions.instanceId (tests, deliberate wiring), fall back to the
64
+ // deploy-env variable, finally a boot-time UUID. Validator rejects the
65
+ // sentinel — a deliberate collision attempt would silently merge this
66
+ // instance's per-instance cursors with the shared-row cursors and
67
+ // deliver events twice to one shard while starving the other.
68
+ const resolvedInstanceId = explicitInstanceId ?? process.env["KUMIKO_INSTANCE_ID"] ?? generateId();
69
+ if (resolvedInstanceId === SHARED_INSTANCE_SENTINEL) {
70
+ throw new Error(`ServerOptions.instanceId / KUMIKO_INSTANCE_ID cannot equal the reserved sentinel "${SHARED_INSTANCE_SENTINEL}" — ` +
71
+ `pick any other stable string.`);
72
+ }
73
+ // Warn when we fell back to a random UUID. The built-in SSE consumer is
74
+ // delivery="shared" (one cursor for all pods), so it no longer
75
+ // writes a row per instance. The id still labels this process in the
76
+ // app.started event and in per-instance consumers (opt-in delivery), where a
77
+ // fresh id per boot leaves an orphaned cursor-row that pins pruneEvents
78
+ // (retention-guard uses MIN(lastProcessedEventId) across all shards). Silent
79
+ // when explicitInstanceId or KUMIKO_INSTANCE_ID is explicit — those are
80
+ // deliberate choices (the test suite sets KUMIKO_INSTANCE_ID="test-instance").
81
+ const instanceIdWasRandom = explicitInstanceId === undefined && !process.env["KUMIKO_INSTANCE_ID"];
82
+ if (instanceIdWasRandom) {
83
+ bootLog.warn(`No ServerOptions.instanceId / KUMIKO_INSTANCE_ID set — generated a random UUID (${resolvedInstanceId}). ` +
84
+ `SSE no longer needs a per-instance cursor, but the id labels this process in the app.started event and in per-instance consumers; without a stable id each restart leaves an orphaned cursor-row behind for those and pins events-retention on its last cursor. ` +
85
+ `Set KUMIKO_INSTANCE_ID to the pod name (e.g. from the Kubernetes downward API metadata.name) in production.`);
86
+ }
87
+ return resolvedInstanceId;
88
+ }
89
+ export function warnUnlimitedSystemScopeFeatures(registry, bootLog) {
90
+ // The systemScope default limit only exists once a resolver does, and an app that
91
+ // declared no explicit rateLimit anywhere never gets one auto-wired (wantsL3 stays
92
+ // false) — so its cross-tenant handlers are silently unlimited.
93
+ const unlimitedSystemScopeFeatures = [...registry.features.values()]
94
+ .filter((feature) => feature.systemScope &&
95
+ Object.keys(feature.writeHandlers).length + Object.keys(feature.queryHandlers).length > 0)
96
+ .map((feature) => feature.name);
97
+ if (unlimitedSystemScopeFeatures.length > 0) {
98
+ bootLog.warn(`r.systemScope() features have handlers but no RateLimitResolver is configured, ` +
99
+ `so their default rate limit is not enforced: ${unlimitedSystemScopeFeatures.join(", ")}. ` +
100
+ "Wire a Redis client on context.redis and declare rateLimit on at least one handler (or pass " +
101
+ "context.rateLimit) to enable the resolver.");
102
+ }
103
+ }
104
+ export function assertSinglePrincipalStatusProvider(registry, auth) {
105
+ // A membershipQuery handler is registered but principalStatus is not provided by exactly
106
+ // one feature — surface that misconfig at boot instead of an InternalError on first login/switch-tenant.
107
+ if (registry.getQueryHandler(auth.membershipQuery)) {
108
+ const principalStatusProviders = registry.getExtensionUsages(EXT_PRINCIPAL_STATUS).length;
109
+ if (principalStatusProviders === 0) {
110
+ throw new Error(`[kumiko] auth membershipQuery "${auth.membershipQuery}" is registered but no feature ` +
111
+ 'provides the "principalStatus" contract — mount the user feature (tenant switch and sign-in ' +
112
+ "need it to reject blocked principals).");
113
+ }
114
+ if (principalStatusProviders > 1) {
115
+ throw new Error(`[kumiko] auth membershipQuery "${auth.membershipQuery}" is registered but ` +
116
+ `${principalStatusProviders} features provide the "principalStatus" contract — exactly one ` +
117
+ "is expected (the bundled user feature already provides it).");
118
+ }
119
+ }
120
+ }
121
+ // Scans every feature's entities for a file/image/files/images field. Short-
122
+ // circuits on the first hit — no need to build a full inventory, we only want
123
+ // the yes/no answer for the boot check.
124
+ function registryDeclaresFileFields(registry) {
125
+ for (const feature of registry.features.values()) {
126
+ for (const entity of Object.values(feature.entities ?? {})) {
127
+ for (const field of Object.values(entity.fields)) {
128
+ if (isFileField(field))
129
+ return true;
130
+ }
131
+ }
132
+ }
133
+ return false;
134
+ }
135
+ // Entities whose entityList screen WOULD render a search box based on
136
+ // screen/field config alone: `screen.searchable` wins when set, otherwise
137
+ // the box shows iff the entity has ≥1 searchable field. `screen.searchable
138
+ // === false` is excluded — the boot-validator (entity-list-screens.ts) only
139
+ // allows that on the whitelisted download-attempt-list-style screens, which
140
+ // never render the box. This is the config-only half of the rule; the
141
+ // client additionally gates on FeatureSchema.searchAdapterMissing
142
+ // (set from the same `!options.context.searchAdapter` check in mountSchemaRoute) so the
143
+ // box is actually suppressed once this function finds a hit.
144
+ function entitiesWithSearchableScreen(registry) {
145
+ const entities = new Set();
146
+ for (const feature of registry.features.values()) {
147
+ for (const screen of Object.values(feature.screens)) {
148
+ if (screen.type !== "entityList")
149
+ continue;
150
+ const isSearchable = screen.searchable === true ||
151
+ (screen.searchable === undefined && registry.getSearchableFields(screen.entity).length > 0);
152
+ if (isSearchable)
153
+ entities.add(screen.entity);
154
+ }
155
+ }
156
+ return [...entities];
157
+ }
@@ -0,0 +1,26 @@
1
+ import type { DbConnection } from "../db/connection.ts";
2
+ import type { AppContext } from "../engine/types/index.ts";
3
+ import type { Lifecycle } from "../lifecycle/index.ts";
4
+ import type { ObservabilityProvider } from "../observability/index.ts";
5
+ import { type EventConsumer, type EventDispatcher } from "../pipeline/event-dispatcher.ts";
6
+ import type { ResolvedCacheSyncBus } from "../redis/cache-sync-bus.ts";
7
+ import type { RedisSseBroker } from "./redis-sse-broker.ts";
8
+ import type { ServerOptions } from "./server.ts";
9
+ import type { SseBroker } from "./sse-broker.ts";
10
+ export declare function createServerEventDispatcher(deps: {
11
+ readonly options: Pick<ServerOptions, "registry" | "eventDispatcher" | "dispatcherOptions" | "processLane">;
12
+ readonly context: AppContext;
13
+ readonly sseBroker: SseBroker;
14
+ readonly observability: ObservabilityProvider;
15
+ readonly fileProviderResolver: AppContext["_fileProviderResolver"];
16
+ readonly instanceId: string;
17
+ }): {
18
+ readonly baseDb: DbConnection | undefined;
19
+ readonly consumers: readonly EventConsumer[];
20
+ readonly eventDispatcher: EventDispatcher | undefined;
21
+ };
22
+ export declare function registerOwnedShutdownHooks(lifecycle: Lifecycle | undefined, owned: {
23
+ readonly eventDispatcher: EventDispatcher | undefined;
24
+ readonly ownedRedisSseBroker: RedisSseBroker | undefined;
25
+ readonly ownedCacheSync: ResolvedCacheSyncBus["owned"];
26
+ }): void;
@@ -0,0 +1,208 @@
1
+ import { withExecutorManagedWrites } from "../db/tenant-db.js";
2
+ import { createDerivativesContext } from "../derivatives/derivatives-context.js";
3
+ import { runsInLane } from "../engine/run-in.js";
4
+ import { createTenantFileContext } from "../files/file-handle.js";
5
+ import { createEventDispatcher, } from "../pipeline/event-dispatcher.js";
6
+ import { createMultiStreamApplyContext } from "../pipeline/multi-stream-apply-context.js";
7
+ import { createAccessInvalidationEventConsumer, createJobTriggerEventConsumer, createSearchEventConsumer, createSseBroadcastEventConsumer, } from "../pipeline/system-hooks.js";
8
+ import { deriveSearchAdapterConfig } from "../search/derive-search-adapter-config.js";
9
+ export function createServerEventDispatcher(deps) {
10
+ const { options, context, sseBroker, observability, fileProviderResolver, instanceId } = deps;
11
+ // Async event-dispatcher — the replacement for the old transactional
12
+ // outbox. Consumer sources:
13
+ // 1. System: SSE broadcast (iff sseBroker), Search index (iff
14
+ // context.searchAdapter).
15
+ // 2. Features: every r.multiStreamProjection registered in the registry
16
+ // becomes its own consumer row with an independent cursor. The MSP
17
+ // apply map is routed by event.type; apply receives the raw DbRunner
18
+ // of a TX-scoped, tenant-bound DB handle so per-tenant writes stay
19
+ // isolated.
20
+ //
21
+ // The dispatcher is built but NOT started here. Production boot code
22
+ // must call `.start()`; test code typically calls `.runOnce()`.
23
+ const { baseDb, searchAdapter } = resolveBaseDbAndSearchAdapter(context, options.registry);
24
+ const systemConsumers = buildSystemEventConsumers(options, sseBroker, searchAdapter);
25
+ const mspConsumers = buildMultiStreamProjectionConsumers(options, {
26
+ baseDb,
27
+ context,
28
+ fileProviderResolver,
29
+ });
30
+ const consumers = [...systemConsumers, ...mspConsumers];
31
+ const { disabled: dispatcherDisabled, systemConsumers: _systemConsumersOpt, ...dispatcherTunables } = options.eventDispatcher ?? {};
32
+ let eventDispatcher;
33
+ if (consumers.length > 0 && baseDb && !dispatcherDisabled) {
34
+ eventDispatcher = createEventDispatcher({
35
+ db: baseDb,
36
+ consumers,
37
+ context,
38
+ tracer: observability.tracer,
39
+ meter: observability.meter,
40
+ instanceId,
41
+ ...dispatcherTunables,
42
+ });
43
+ }
44
+ return { baseDb, consumers, eventDispatcher };
45
+ }
46
+ function resolveBaseDbAndSearchAdapter(context, registry) {
47
+ // @cast-boundary engine-bridge — context.db union narrows to DbConnection here
48
+ const baseDb = context.db;
49
+ // @cast-boundary engine-bridge — searchAdapter is an optional context-extension
50
+ const searchAdapter = context.searchAdapter;
51
+ // Adapter is built before the registry exists (app boot order) — hand it
52
+ // the registry-derived default config here so it can lazily configure
53
+ // each tenant index on first access instead of requiring per-tenant app
54
+ // wiring.
55
+ const derivedSearchConfig = searchAdapter ? deriveSearchAdapterConfig(registry) : undefined;
56
+ if (searchAdapter && derivedSearchConfig) {
57
+ searchAdapter.setDefaultConfig?.(derivedSearchConfig);
58
+ }
59
+ return { baseDb, searchAdapter };
60
+ }
61
+ function buildSystemEventConsumers(options, sseBroker, searchAdapter) {
62
+ const sseConsumerEnabled = options.eventDispatcher?.systemConsumers?.sse ?? true;
63
+ const searchConsumerEnabled = options.eventDispatcher?.systemConsumers?.search ?? true;
64
+ const jobTriggerConsumerEnabled = options.eventDispatcher?.systemConsumers?.jobTrigger ?? true;
65
+ const jobRunnerForTriggers = options.dispatcherOptions?.jobRunner;
66
+ const systemConsumers = [];
67
+ if (sseConsumerEnabled) {
68
+ systemConsumers.push(createSseBroadcastEventConsumer(sseBroker));
69
+ }
70
+ if (searchConsumerEnabled && searchAdapter) {
71
+ systemConsumers.push(createSearchEventConsumer(searchAdapter, options.registry));
72
+ }
73
+ if (jobTriggerConsumerEnabled && jobRunnerForTriggers) {
74
+ systemConsumers.push(createJobTriggerEventConsumer(jobRunnerForTriggers, options.registry));
75
+ }
76
+ // Default ON (global-by-default). Tests that opt out of SSE via
77
+ // systemConsumers.accessInvalidation=false (test-stack mirrors sse off)
78
+ // skip the row so retention prune suites are not blocked by a lagging
79
+ // cursor=0 consumer they never drain.
80
+ const accessInvalidationEnabled = options.eventDispatcher?.systemConsumers?.accessInvalidation ?? true;
81
+ if (accessInvalidationEnabled) {
82
+ systemConsumers.push(createAccessInvalidationEventConsumer(sseBroker));
83
+ }
84
+ return systemConsumers;
85
+ }
86
+ function buildMultiStreamProjectionConsumers(options, deps) {
87
+ const jobRunnerForTriggers = options.dispatcherOptions?.jobRunner;
88
+ const mspJobs = jobRunnerForTriggers?.dispatch
89
+ ? {
90
+ dispatch: async (jobName, payload, dispatchOptions) => {
91
+ await jobRunnerForTriggers.dispatch?.(jobName, payload, dispatchOptions);
92
+ },
93
+ }
94
+ : undefined;
95
+ // MultiStreamProjections: one EventConsumer per MSP. Handler routes by
96
+ // event.type into the MSP's apply map. MSPs aggregate cross-aggregate but
97
+ // still within one tenant by default — the applier receives the plain
98
+ // baseDb DbRunner; tenant consistency comes from the triggering event's
99
+ // own tenantId, not a per-event TenantDb scope.
100
+ //
101
+ // Lane-filter: MSPs declare `runIn` to pin them to a
102
+ // deploy-lane. An MSP with `runIn: "api"` won't be wired into the
103
+ // worker-process dispatcher (and vice versa). `runIn: "both"` (or the
104
+ // legacy undefined default of "worker") runs wherever a dispatcher is
105
+ // started — SKIP LOCKED on the consumer-cursor handles the race between
106
+ // processes that both want the same event.
107
+ const processLane = options.processLane ?? "worker";
108
+ const mspDefs = [...options.registry.getAllMultiStreamProjections().values()].filter((msp) => runsInLane(msp.runIn, processLane));
109
+ return mspDefs.map((msp) => buildMultiStreamProjectionConsumer(msp, {
110
+ registry: options.registry,
111
+ mspJobs,
112
+ ...deps,
113
+ }));
114
+ }
115
+ function buildMultiStreamProjectionConsumer(msp, deps) {
116
+ const { registry, mspJobs, baseDb, context, fileProviderResolver } = deps;
117
+ return {
118
+ name: msp.name,
119
+ // Feature-toggle gating: carry the owning feature so the event-dispatcher
120
+ // can pause this consumer when the feature is globally disabled. Events
121
+ // queue up in the store and replay cleanly from the same cursor on resume.
122
+ ...(registry.getMultiStreamProjectionFeature(msp.name) && {
123
+ featureName: registry.getMultiStreamProjectionFeature(msp.name), // @cast-boundary engine-bridge
124
+ }),
125
+ // Copy the continuous-lifecycle error policy straight onto the consumer.
126
+ // Rebuild uses its own policy (rebuildProjection reads msp.errorMode.rebuild
127
+ // directly); steady-state delivery runs through this consumer.
128
+ ...(msp.errorMode?.continuous && { errorPolicy: msp.errorMode.continuous }),
129
+ // Carry the MSP's declared delivery semantic through to the consumer.
130
+ // Default (shared) is applied inside event-dispatcher, so omitting when
131
+ // the MSP didn't declare one keeps the existing behaviour.
132
+ ...(msp.delivery && { delivery: msp.delivery }),
133
+ // Carry the MSP's declared cursor-seed through to the consumer. Default
134
+ // (beginning) is applied inside insertConsumerIfAbsent.
135
+ ...(msp.startFrom && { startFrom: msp.startFrom }),
136
+ handler: async (event, ctx) => {
137
+ const applyFn = msp.apply[event.type];
138
+ // skip: this MSP doesn't care about this event type — fast path,
139
+ // every event type passes through every MSP consumer exactly once.
140
+ if (!applyFn)
141
+ return;
142
+ if (!baseDb) {
143
+ // skip: no baseDb wired — consumers.length > 0 + baseDb check in
144
+ // createServerEventDispatcher gates dispatcher creation, so we won't reach here in
145
+ // production. Defensive return for the type-narrowing path.
146
+ return;
147
+ }
148
+ // Saga/process-manager ctx: apply can call ctx.appendEvent to cascade
149
+ // a follow-up event onto another aggregate. Uses the triggering event's
150
+ // tenantId + userId so the causal chain stays tenant-consistent.
151
+ // MSP qualified names are "<feature>:projection:<short>" — the
152
+ // prefix before the first ":" owns the MSP. Used to reject
153
+ // cross-feature ctx.appendEvent calls at emit-site.
154
+ const mspOwner = msp.name.split(":")[0];
155
+ const mspFiles = fileProviderResolver
156
+ ? createTenantFileContext(fileProviderResolver, event.tenantId)
157
+ : undefined;
158
+ const mspDb = withExecutorManagedWrites(baseDb);
159
+ const applyCtx = createMultiStreamApplyContext({
160
+ registry,
161
+ db: mspDb,
162
+ tenantId: event.tenantId,
163
+ userId: event.metadata.userId,
164
+ ...(mspOwner && { callerFeature: mspOwner }),
165
+ ...(mspFiles && { files: mspFiles }),
166
+ ...(context.secrets && { secrets: context.secrets }),
167
+ ...(mspJobs && { jobs: mspJobs }),
168
+ ...(mspFiles && {
169
+ derivatives: createDerivativesContext({
170
+ files: mspFiles,
171
+ registry,
172
+ db: baseDb,
173
+ tenantId: event.tenantId,
174
+ }),
175
+ }),
176
+ });
177
+ await applyFn(event, mspDb, applyCtx);
178
+ // Keep ctx reachable to satisfy the EventConsumerHandler signature.
179
+ void ctx;
180
+ },
181
+ };
182
+ }
183
+ export function registerOwnedShutdownHooks(lifecycle, owned) {
184
+ const { eventDispatcher, ownedRedisSseBroker, ownedCacheSync } = owned;
185
+ // Wire the event-dispatcher shutdown into the lifecycle so the caller
186
+ // doesn't have to know the dispatcher exists. Hooks drain LIFO, so this
187
+ // runs before anything registered later by the caller (e.g. DB pool close).
188
+ if (lifecycle && eventDispatcher) {
189
+ const dispatcher = eventDispatcher;
190
+ lifecycle.registerShutdownHook("eventDispatcher", async () => {
191
+ await dispatcher.stop();
192
+ });
193
+ }
194
+ // Same reasoning, scoped to the broker this function created itself — an
195
+ // app-injected sseBroker (options.sseBroker) owns its own teardown.
196
+ if (lifecycle && ownedRedisSseBroker) {
197
+ const broker = ownedRedisSseBroker;
198
+ lifecycle.registerShutdownHook("redisSseBroker", async () => {
199
+ await broker.close();
200
+ });
201
+ }
202
+ if (lifecycle && ownedCacheSync) {
203
+ const bus = ownedCacheSync;
204
+ lifecycle.registerShutdownHook("cacheSync", async () => {
205
+ await bus.close();
206
+ });
207
+ }
208
+ }
@@ -0,0 +1,11 @@
1
+ import type { Context, Hono } from "hono";
2
+ import type { WriteResult } from "../engine/types/index.ts";
3
+ import type { Dispatcher } from "../pipeline/dispatcher.ts";
4
+ import { type ExtraRouteDefinition, type RoleDispatchArgs, type SystemDispatchArgs } from "./extra-route.ts";
5
+ import type { ServerWiring } from "./server-wiring.ts";
6
+ export type PublicApiRequestMatcher = (c: Context) => boolean;
7
+ export declare function compilePublicApiRequestMatcher(extraRoutes: readonly ExtraRouteDefinition[] | undefined): PublicApiRequestMatcher;
8
+ export declare function mountExtraRoutes(app: Hono, wiring: Pick<ServerWiring, "options" | "dispatcher" | "context" | "clientIpResolver">): void;
9
+ export declare function makeDispatchSystemWrite(dispatcher: Dispatcher): (args: SystemDispatchArgs) => Promise<WriteResult>;
10
+ export declare function makeDispatchWriteAs(dispatcher: Dispatcher): (args: RoleDispatchArgs) => Promise<WriteResult>;
11
+ export declare function makeDispatchSystemQuery(dispatcher: Dispatcher): (args: SystemDispatchArgs) => Promise<unknown>;
@@ -0,0 +1,203 @@
1
+ import { ROLES } from "../auth/roles.js";
2
+ import { createSystemUser } from "../engine/system-user.js";
3
+ import { createFallbackLogger } from "../logging/index.js";
4
+ import { assertUnreachable } from "../utils/index.js";
5
+ import { NON_PUBLIC_API_PATHS, PUBLIC_API_PATHS } from "./api-constants.js";
6
+ import { getUser } from "./auth-middleware.js";
7
+ import { clientIpSourceFromHonoContext } from "./client-ip.js";
8
+ import { ExtraRouteEntries, ExtraRouteRejection, } from "./extra-route.js";
9
+ import { bodyLimitForRoutePath, DEFAULT_MAX_REQUEST_BYTES } from "./route-registrars.js";
10
+ import { isMissingOrAnonymousUser, makeSystemQuery, mountHonoRoute, unauthenticatedResponse, } from "./server-routes.js";
11
+ // Only entry:"signature" bypasses jwtGuard (verify() authenticates
12
+ // itself); entry:"anonymous" still needs the anonymousAccess fallthrough
13
+ // (tenant-by-host), entry:"user" needs c.get("user") populated.
14
+ export function compilePublicApiRequestMatcher(extraRoutes) {
15
+ const matchers = (extraRoutes ?? [])
16
+ .filter((route) => route.entry === ExtraRouteEntries.signature)
17
+ .map((route) => ({ method: route.method, pattern: honoPathToRegex(route.path) }));
18
+ return (c) => PUBLIC_API_PATHS.has(c.req.path) ||
19
+ matchers.some((m) => m.method === c.req.method && m.pattern.test(c.req.path));
20
+ }
21
+ export function mountExtraRoutes(app, wiring) {
22
+ const { options, dispatcher, context, clientIpResolver } = wiring;
23
+ // extraRoutes — declarative HTTP-routes with a
24
+ // required `entry` tier. Mounted after r.httpRoute: an
25
+ // extraRoute dispatching through `dispatcher` builds Hono's matcher, so
26
+ // this must run before any seed that also dispatches (runProdApp/
27
+ // createKumikoServer call buildServer before seeding).
28
+ if (options.extraRoutes) {
29
+ // Boot-time validation for the whole list BEFORE mounting anything —
30
+ // an app with one bad route should fail loud at boot, not mount N-1
31
+ // routes and then throw on route N.
32
+ for (const route of options.extraRoutes) {
33
+ assertValidExtraRoute(route);
34
+ }
35
+ const dispatchSystemWrite = makeDispatchSystemWrite(dispatcher);
36
+ const dispatchSystemQuery = makeDispatchSystemQuery(dispatcher);
37
+ for (const route of options.extraRoutes) {
38
+ const honoHandler = buildExtraRouteHonoHandler(route, {
39
+ app,
40
+ dispatcher,
41
+ registry: options.registry,
42
+ secrets: context.secrets,
43
+ dispatchSystemWrite,
44
+ dispatchSystemQuery,
45
+ clientIpResolver,
46
+ });
47
+ mountHonoRoute(app, route.method, route.path, honoHandler, bodyLimitForRoutePath(route.path, options.maxRequestBytes ?? DEFAULT_MAX_REQUEST_BYTES));
48
+ }
49
+ }
50
+ }
51
+ function assertValidExtraRoute(route) {
52
+ if (!isKnownExtraRouteEntry(route.entry)) {
53
+ throw new Error(`[kumiko] extraRoutes: unknown entry "${String(route.entry)}" on ` +
54
+ `"${route.method} ${route.path}" — expected "anonymous" | "user" | "signature". ` +
55
+ "A JS caller without the ExtraRouteDefinition type can hit this at boot.");
56
+ }
57
+ if (route.entry === ExtraRouteEntries.user && !route.path.startsWith("/api/")) {
58
+ throw new Error(`[kumiko] extraRoutes: entry:"user" route "${route.method} ${route.path}" must be ` +
59
+ "mounted under \"/api/\" — that's the only path prefix that rides the framework's " +
60
+ "jwtGuard chain, which is what populates deps.user.");
61
+ }
62
+ // A signature route under /api/ skips jwtGuard/origin/csrf for every
63
+ // path its pattern matches — a wildcard would switch off auth for
64
+ // unrelated /api/* handlers (e.g. "/api/*" swallows /api/write).
65
+ if (route.entry === ExtraRouteEntries.signature &&
66
+ route.path.startsWith("/api/") &&
67
+ route.path.includes("*")) {
68
+ throw new Error(`[kumiko] extraRoutes: entry:"signature" route "${route.method} ${route.path}" must not ` +
69
+ 'use a wildcard under "/api/" — it would bypass the auth chain for every matching path.');
70
+ }
71
+ // A `:param` segment is as broad as a wildcard, and the public matchers test
72
+ // the bare request path: "/:seg/write" and "/api/:hook" both match /api/write.
73
+ if (route.entry === ExtraRouteEntries.signature) {
74
+ const routePattern = honoPathToRegex(route.path);
75
+ const shadowedFrameworkPath = [...PUBLIC_API_PATHS, ...NON_PUBLIC_API_PATHS].find((path) => routePattern.test(path));
76
+ if (shadowedFrameworkPath !== undefined) {
77
+ throw new Error(`[kumiko] extraRoutes: entry:"signature" route "${route.method} ${route.path}" must not ` +
78
+ `match the framework path "${shadowedFrameworkPath}" — it would bypass the auth chain for it.`);
79
+ }
80
+ }
81
+ }
82
+ // SystemAdmin write/query builders shared by mountExtraRoutes
83
+ // and server-runtime's `wire` hook (runProdApp/createKumikoServer,
84
+ // after buildServer). Privilege-scope: SystemAdmin is the highest
85
+ // non-tenant-scoped role — reaches ANY SystemAdmin-gated handler on ANY
86
+ // tenant. Only safe for callers that already proved their own authenticity
87
+ // (signature verify(), HMAC state, ...), never exposed to a raw request.
88
+ export function makeDispatchSystemWrite(dispatcher) {
89
+ return ({ handlerQn, payload, tenantId }) => dispatcher.write(handlerQn, payload, createSystemUser(tenantId, [ROLES.SystemAdmin]));
90
+ }
91
+ // Privilege-scope: the system user gets exactly the roles the caller names —
92
+ // no implicit SystemAdmin — so a seed can write data gated on app roles.
93
+ // Same trust requirement as makeDispatchSystemWrite: never expose to a raw
94
+ // request, only to code that already proved its own authenticity.
95
+ export function makeDispatchWriteAs(dispatcher) {
96
+ return ({ handlerQn, payload, tenantId, roles }) => dispatcher.write(handlerQn, payload, createSystemUser(tenantId, roles));
97
+ }
98
+ export function makeDispatchSystemQuery(dispatcher) {
99
+ return ({ handlerQn, payload, tenantId }) => dispatcher.query(handlerQn, payload, createSystemUser(tenantId, [ROLES.SystemAdmin]));
100
+ }
101
+ // Same dispatcher.write(...) as entry:"user", but the user is getUser(c) —
102
+ // see AnonymousExtraRouteDeps.write for the privilege/tenant contract.
103
+ function makeAnonymousWrite(
104
+ // biome-ignore lint/suspicious/noExplicitAny: Hono context generics are invisible at the framework boundary
105
+ c, dispatcher) {
106
+ return (type, payload) => {
107
+ const user = getUser(c);
108
+ if (!user) {
109
+ throw new Error('[kumiko] extraRoutes: entry:"anonymous" deps.write requires this route to be mounted ' +
110
+ 'under "/api/" with anonymousAccess wired — no request-resolved session user was found.');
111
+ }
112
+ return dispatcher.write(type, payload, user);
113
+ };
114
+ }
115
+ const extraRouteLog = createFallbackLogger("extra-route");
116
+ function isKnownExtraRouteEntry(entry) {
117
+ return (entry === ExtraRouteEntries.anonymous ||
118
+ entry === ExtraRouteEntries.user ||
119
+ entry === ExtraRouteEntries.signature);
120
+ }
121
+ // Hono path pattern ("/api/foo/:bar", "/api/foo/*") → RegExp. Only the
122
+ // subset extraRoutes actually uses (static segments, `:param`, trailing
123
+ // `*`) — no inline `:param{regex}` constraints, no optional `:param?`.
124
+ function honoPathToRegex(path) {
125
+ const segments = path
126
+ .split("/")
127
+ .map((segment) => {
128
+ if (segment.startsWith(":"))
129
+ return "[^/]+";
130
+ if (segment === "*")
131
+ return ".*";
132
+ return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
133
+ })
134
+ .join("/");
135
+ return new RegExp(`^${segments}$`);
136
+ }
137
+ function buildExtraRouteHonoHandler(route, shared) {
138
+ switch (route.entry) {
139
+ case ExtraRouteEntries.anonymous:
140
+ return async (c) => route.handler(c, {
141
+ app: shared.app,
142
+ registry: shared.registry,
143
+ systemQuery: makeSystemQuery(c, shared.dispatcher, shared.clientIpResolver),
144
+ write: makeAnonymousWrite(c, shared.dispatcher),
145
+ clientIp: shared.clientIpResolver.resolve(clientIpSourceFromHonoContext(c)),
146
+ });
147
+ case ExtraRouteEntries.user:
148
+ return async (c) => {
149
+ const user = getUser(c);
150
+ if (isMissingOrAnonymousUser(user)) {
151
+ return unauthenticatedResponse(c);
152
+ }
153
+ return route.handler(c, {
154
+ app: shared.app,
155
+ registry: shared.registry,
156
+ user,
157
+ query: (type, payload) => shared.dispatcher.query(type, payload, user),
158
+ write: (type, payload) => shared.dispatcher.write(type, payload, user),
159
+ });
160
+ };
161
+ case ExtraRouteEntries.signature:
162
+ return async (c) => {
163
+ const rawBody = await c.req.text();
164
+ const headers = {};
165
+ c.req.raw.headers.forEach((value, key) => {
166
+ headers[key.toLowerCase()] = value;
167
+ });
168
+ let verified;
169
+ try {
170
+ verified = await route.verify({ rawBody, headers, params: c.req.param(), query: c.req.query() }, { registry: shared.registry, secrets: shared.secrets });
171
+ }
172
+ catch (e) {
173
+ if (e instanceof ExtraRouteRejection) {
174
+ if (e.retryAfterSeconds !== undefined) {
175
+ return c.json(e.body, e.status, { "Retry-After": String(e.retryAfterSeconds) });
176
+ }
177
+ return c.json(e.body, e.status);
178
+ }
179
+ // The body goes to unauthenticated callers; infra errors thrown by
180
+ // verify() (secrets, DB) must not leak, so the detail stays in the log.
181
+ extraRouteLog.error(`signature verify() threw on ${c.req.path}`, {
182
+ error: e instanceof Error ? e.message : String(e),
183
+ });
184
+ return c.json({
185
+ error: {
186
+ code: "extra_route_signature_invalid",
187
+ message: "signature verification failed",
188
+ },
189
+ }, 401);
190
+ }
191
+ return route.handler(c, verified, {
192
+ app: shared.app,
193
+ registry: shared.registry,
194
+ secrets: shared.secrets,
195
+ systemQuery: makeSystemQuery(c, shared.dispatcher, shared.clientIpResolver),
196
+ dispatchSystemWrite: shared.dispatchSystemWrite,
197
+ dispatchSystemQuery: shared.dispatchSystemQuery,
198
+ });
199
+ };
200
+ default:
201
+ return assertUnreachable(route, "extra route entry");
202
+ }
203
+ }
@@ -0,0 +1,20 @@
1
+ import type { Hono, MiddlewareHandler } from "hono";
2
+ import type { ObservabilityProvider } from "../observability/index.ts";
3
+ import { type TenantLifecycleStatusResolver } from "./auth-middleware.ts";
4
+ import type { ServerOptions } from "./server.ts";
5
+ import type { PublicApiRequestMatcher } from "./server-extra-routes.ts";
6
+ import type { ServerWiring } from "./server-wiring.ts";
7
+ import { type ResolvedSessionCookies } from "./session-cookies.ts";
8
+ export declare function installSessionCookies(app: Hono, auth: ServerOptions["auth"]): ResolvedSessionCookies;
9
+ export declare function installRequestId(app: Hono, wiring: Pick<ServerWiring, "options" | "clientIpResolver">): void;
10
+ export declare function installRateLimitMiddleware(app: Hono, wiring: Pick<ServerWiring, "options" | "rateLimitResolver" | "rateLimitMiddlewareRequested" | "clientIpResolver" | "observability">): MiddlewareHandler | undefined;
11
+ export declare function installObservability(app: Hono, observability: ObservabilityProvider): void;
12
+ export declare function installPiiLeakGuard(app: Hono): void;
13
+ export type AuthGuardChain = {
14
+ readonly tenantLifecycleResolver: TenantLifecycleStatusResolver | undefined;
15
+ readonly sessionOnlyHttpRouteGuards: readonly MiddlewareHandler[];
16
+ };
17
+ export declare function installAuthGuardChain(app: Hono, wiring: Pick<ServerWiring, "options" | "jwt" | "baseDb">, scope: {
18
+ readonly isPublicApiRequest: PublicApiRequestMatcher;
19
+ readonly globalRateLimitGuard: MiddlewareHandler | undefined;
20
+ }): AuthGuardChain;