@cosmicdrift/kumiko-framework 0.387.0 → 0.389.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 (375) hide show
  1. package/AGENTS.md +9 -1
  2. package/agents/examples/anonymous-access.md +1 -1
  3. package/agents/examples/apex-landing.md +204 -0
  4. package/agents/examples/api-worker-split.md +64 -0
  5. package/agents/examples/app-config.md +14 -0
  6. package/agents/examples/auth-mfa.md +115 -0
  7. package/agents/examples/env-composition.md +28 -1
  8. package/agents/examples/golden-path.md +13 -1
  9. package/agents/examples/legal-pages.md +1 -1
  10. package/agents/examples/lifecycle-hooks.md +55 -2
  11. package/agents/examples/list-table-options.md +466 -0
  12. package/agents/examples/pdf-audit-export.md +4 -2
  13. package/agents/examples/prod-boot-options.md +16 -0
  14. package/agents/examples/rate-limiting.md +48 -0
  15. package/agents/examples/record-detail-layout.md +692 -0
  16. package/agents/examples/seed-migration.md +212 -0
  17. package/agents/examples/state-machine.md +3 -2
  18. package/agents/examples/tenant-isolation.md +73 -1
  19. package/agents/examples/wizard-form.md +941 -0
  20. package/agents/examples/writeform-section.md +548 -0
  21. package/agents/examples.md +1 -0
  22. package/agents/features.md +19 -2
  23. package/agents/widgets.md +2 -0
  24. package/dist/api/api-constants.d.ts +1 -0
  25. package/dist/api/api-constants.js +2 -0
  26. package/dist/api/auth-routes-shared.d.ts +2 -0
  27. package/dist/api/auth-routes-shared.js +5 -0
  28. package/dist/api/client-error-route.js +10 -5
  29. package/dist/api/entity-read-access.d.ts +2 -1
  30. package/dist/api/entity-read-access.js +14 -0
  31. package/dist/api/export/export-cells-reference.d.ts +9 -0
  32. package/dist/api/export/export-cells-reference.js +21 -0
  33. package/dist/api/export/export-cells.d.ts +11 -0
  34. package/dist/api/export/export-cells.js +136 -0
  35. package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
  36. package/dist/api/export/export-ciphertext-guard.js +27 -0
  37. package/dist/api/export/export-columns.d.ts +29 -0
  38. package/dist/api/export/export-columns.js +85 -0
  39. package/dist/api/export/export-errors.d.ts +4 -0
  40. package/dist/api/export/export-errors.js +14 -0
  41. package/dist/api/export/export-route.d.ts +4 -0
  42. package/dist/api/export/export-route.js +74 -0
  43. package/dist/api/export/export-rows.d.ts +13 -0
  44. package/dist/api/export/export-rows.js +62 -0
  45. package/dist/api/export/export-screen.d.ts +12 -0
  46. package/dist/api/export/export-screen.js +47 -0
  47. package/dist/api/export/export-xlsx-writer.d.ts +4 -0
  48. package/dist/api/export/export-xlsx-writer.js +21 -0
  49. package/dist/api/extra-route.d.ts +4 -0
  50. package/dist/api/index.d.ts +3 -1
  51. package/dist/api/index.js +3 -1
  52. package/dist/api/pii-leak-guard.d.ts +3 -0
  53. package/dist/api/pii-leak-guard.js +3 -2
  54. package/dist/api/request-context.d.ts +1 -1
  55. package/dist/api/request-context.js +1 -1
  56. package/dist/api/routes.d.ts +16 -0
  57. package/dist/api/routes.js +2 -2
  58. package/dist/api/server-boot-checks.d.ts +10 -0
  59. package/dist/api/server-boot-checks.js +157 -0
  60. package/dist/api/server-event-dispatcher.d.ts +26 -0
  61. package/dist/api/server-event-dispatcher.js +208 -0
  62. package/dist/api/server-extra-routes.d.ts +11 -0
  63. package/dist/api/server-extra-routes.js +206 -0
  64. package/dist/api/server-middleware.d.ts +20 -0
  65. package/dist/api/server-middleware.js +211 -0
  66. package/dist/api/server-routes.d.ts +19 -0
  67. package/dist/api/server-routes.js +299 -0
  68. package/dist/api/server-websocket-routes.d.ts +4 -0
  69. package/dist/api/server-websocket-routes.js +108 -0
  70. package/dist/api/server-wiring.d.ts +32 -0
  71. package/dist/api/server-wiring.js +238 -0
  72. package/dist/api/server.d.ts +10 -15
  73. package/dist/api/server.js +48 -1251
  74. package/dist/api/session-cookies.js +4 -4
  75. package/dist/api/sse-route.d.ts +2 -1
  76. package/dist/api/sse-route.js +27 -13
  77. package/dist/config/index.d.ts +3 -1
  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.d.ts +1 -0
  81. package/dist/crypto/pg-kms-adapter.js +12 -18
  82. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  83. package/dist/crypto/pii-field-encryption.js +3 -1
  84. package/dist/db/aggregate-sql.d.ts +23 -0
  85. package/dist/db/aggregate-sql.js +165 -0
  86. package/dist/db/closed-connection.d.ts +9 -0
  87. package/dist/db/closed-connection.js +88 -0
  88. package/dist/db/connection.d.ts +4 -5
  89. package/dist/db/connection.js +29 -20
  90. package/dist/db/dialect.js +1 -1
  91. package/dist/db/distinct-on.d.ts +1 -1
  92. package/dist/db/driver-row-coercion.d.ts +2 -0
  93. package/dist/db/driver-row-coercion.js +186 -0
  94. package/dist/db/driver-value-preparation.d.ts +16 -0
  95. package/dist/db/driver-value-preparation.js +101 -0
  96. package/dist/db/entity-field-encryption.d.ts +1 -0
  97. package/dist/db/entity-field-encryption.js +1 -1
  98. package/dist/db/entity-table-meta.js +103 -110
  99. package/dist/db/event-store-executor-context.d.ts +2 -1
  100. package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
  101. package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
  102. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  103. package/dist/db/event-store-executor-read-aggregate.js +143 -0
  104. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  105. package/dist/db/event-store-executor-read-filter.js +206 -0
  106. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  107. package/dist/db/event-store-executor-read-paging.js +99 -0
  108. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  109. package/dist/db/event-store-executor-read-rows.js +32 -0
  110. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  111. package/dist/db/event-store-executor-read-search.js +221 -0
  112. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  113. package/dist/db/event-store-executor-read-sort.js +130 -0
  114. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  115. package/dist/db/event-store-executor-read-where.js +81 -0
  116. package/dist/db/event-store-executor-read.d.ts +2 -9
  117. package/dist/db/event-store-executor-read.js +48 -701
  118. package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
  119. package/dist/db/event-store-executor-write-atomic.js +41 -0
  120. package/dist/db/event-store-executor-write-steps.d.ts +17 -0
  121. package/dist/db/event-store-executor-write-steps.js +95 -0
  122. package/dist/db/event-store-executor-write.js +405 -509
  123. package/dist/db/event-store-executor.js +2 -1
  124. package/dist/db/executor-managed-tables.js +1 -1
  125. package/dist/db/index.d.ts +7 -3
  126. package/dist/db/index.js +6 -2
  127. package/dist/db/list-aggregate-labels.d.ts +14 -0
  128. package/dist/db/list-aggregate-labels.js +26 -0
  129. package/dist/db/migrate-generator.js +71 -86
  130. package/dist/db/migrate-runner.js +7 -4
  131. package/dist/db/pg-error.js +26 -21
  132. package/dist/db/queries/backfill-pii.js +3 -6
  133. package/dist/db/queries/database-now.js +2 -1
  134. package/dist/db/queries/shadow-swap.js +2 -3
  135. package/dist/db/query.d.ts +16 -15
  136. package/dist/db/query.js +76 -804
  137. package/dist/db/reference-data.js +19 -2
  138. package/dist/db/statement-timeout.js +2 -1
  139. package/dist/db/table-builder.js +113 -166
  140. package/dist/db/table-info.d.ts +10 -0
  141. package/dist/db/table-info.js +45 -0
  142. package/dist/db/temporal-value-guards.d.ts +6 -0
  143. package/dist/db/temporal-value-guards.js +15 -0
  144. package/dist/db/tenant-db-runner.d.ts +1 -0
  145. package/dist/db/tenant-db-runner.js +3 -0
  146. package/dist/db/tenant-db-upsert.d.ts +5 -0
  147. package/dist/db/tenant-db-upsert.js +91 -0
  148. package/dist/db/tenant-db.js +50 -10
  149. package/dist/db/where-clause.d.ts +6 -0
  150. package/dist/db/where-clause.js +236 -0
  151. package/dist/derivatives/delete-stored-file.js +2 -1
  152. package/dist/engine/boot-validator/boot-check.js +1 -1
  153. package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
  154. package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
  155. package/dist/engine/boot-validator/detail-screens.js +2 -2
  156. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  157. package/dist/engine/boot-validator/entity-handler.js +17 -0
  158. package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
  159. package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
  160. package/dist/engine/boot-validator/form-derivations.js +35 -1
  161. package/dist/engine/boot-validator/i18n-keys.js +5 -3
  162. package/dist/engine/boot-validator/index.js +2 -0
  163. package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
  164. package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
  165. package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
  166. package/dist/engine/boot-validator/list-column-layout.js +41 -0
  167. package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
  168. package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
  169. package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
  170. package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
  171. package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
  172. package/dist/engine/boot-validator/list-table-export-options.js +27 -0
  173. package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
  174. package/dist/engine/boot-validator/list-table-options.js +77 -0
  175. package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
  176. package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
  177. package/dist/engine/boot-validator/pii-retention.js +233 -155
  178. package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
  179. package/dist/engine/boot-validator/projection-list-screens.js +3 -13
  180. package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
  181. package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
  182. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  183. package/dist/engine/boot-validator/screens-actions.js +8 -1
  184. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  185. package/dist/engine/boot-validator/screens-forms.js +2 -6
  186. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  187. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  188. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  189. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  190. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  191. package/dist/engine/boot-validator/screens-kind-entity-list.js +235 -0
  192. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  193. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  194. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  195. package/dist/engine/boot-validator/screens-kind-projection-list.js +56 -0
  196. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  197. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  198. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  199. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  200. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  201. package/dist/engine/boot-validator/screens-lists.js +24 -11
  202. package/dist/engine/boot-validator/screens.d.ts +0 -2
  203. package/dist/engine/boot-validator/screens.js +99 -860
  204. package/dist/engine/build-app-schema.d.ts +2 -1
  205. package/dist/engine/build-app-schema.js +65 -97
  206. package/dist/engine/build-config-feature-schema.js +6 -1
  207. package/dist/engine/define-feature.js +13 -2
  208. package/dist/engine/define-handler.js +1 -0
  209. package/dist/engine/entity-handlers.d.ts +13 -0
  210. package/dist/engine/entity-handlers.js +10 -21
  211. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  212. package/dist/engine/entity-list-runtime-options.js +18 -0
  213. package/dist/engine/feature-ast/extractors/handlers.js +1 -0
  214. package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
  215. package/dist/engine/feature-ast/extractors/index.js +1 -1
  216. package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
  217. package/dist/engine/feature-ast/extractors/round1.js +6 -0
  218. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  219. package/dist/engine/feature-ast/index.d.ts +1 -1
  220. package/dist/engine/feature-ast/parse.js +3 -1
  221. package/dist/engine/feature-ast/patch.d.ts +2 -0
  222. package/dist/engine/feature-ast/patch.js +5 -182
  223. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -245
  224. package/dist/engine/feature-ast/pattern-change-schema.js +10 -163
  225. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  226. package/dist/engine/feature-ast/pattern-id-matchers.js +211 -0
  227. package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
  228. package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
  229. package/dist/engine/feature-ast/patterns.d.ts +14 -1
  230. package/dist/engine/feature-ast/patterns.js +44 -51
  231. package/dist/engine/feature-ast/render.js +52 -87
  232. package/dist/engine/feature-builder-state.d.ts +1 -0
  233. package/dist/engine/feature-builder-state.js +1 -0
  234. package/dist/engine/feature-entity-handlers.js +3 -0
  235. package/dist/engine/feature-manifest.d.ts +1 -0
  236. package/dist/engine/feature-manifest.js +1 -0
  237. package/dist/engine/field-access.d.ts +1 -0
  238. package/dist/engine/field-access.js +12 -0
  239. package/dist/engine/form-derivations.js +18 -7
  240. package/dist/engine/handler-field-references.js +1 -1
  241. package/dist/engine/index.d.ts +2 -2
  242. package/dist/engine/index.js +1 -1
  243. package/dist/engine/pattern-library/library.js +2 -1
  244. package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
  245. package/dist/engine/pattern-library/static-schemas.js +9 -0
  246. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  247. package/dist/engine/project-app-schema-for-roles.js +6 -4
  248. package/dist/engine/registry-ingest.js +1 -1
  249. package/dist/engine/schema-builder.js +228 -212
  250. package/dist/engine/screen-filter-ops.js +2 -0
  251. package/dist/engine/screen-helpers.js +1 -1
  252. package/dist/engine/screen-kind.d.ts +7 -0
  253. package/dist/engine/screen-kind.js +1 -0
  254. package/dist/engine/strip-disabled-features-from-app-schema.js +58 -17
  255. package/dist/engine/types/feature.d.ts +1 -0
  256. package/dist/engine/types/index.d.ts +4 -3
  257. package/dist/engine/types/screen.d.ts +1 -1
  258. package/dist/engine/types/screen.js +1 -1
  259. package/dist/engine/validate-projection-allowlist.js +2 -1
  260. package/dist/entrypoint/index.d.ts +6 -1
  261. package/dist/entrypoint/index.js +10 -1
  262. package/dist/errors/docs-url.d.ts +1 -1
  263. package/dist/errors/docs-url.js +22 -2
  264. package/dist/es-ops/context.js +1 -1
  265. package/dist/event-store/admin-api.js +1 -1
  266. package/dist/event-store/event-store.d.ts +1 -0
  267. package/dist/event-store/event-store.js +17 -8
  268. package/dist/event-store/provenance-append.js +1 -1
  269. package/dist/files/bind-pending-file-refs.js +3 -3
  270. package/dist/files/create-file-ref-from-bytes.js +7 -1
  271. package/dist/files/create-file-ref.d.ts +3 -0
  272. package/dist/files/create-file-ref.js +11 -2
  273. package/dist/files/file-handle.js +2 -3
  274. package/dist/files/file-routes.d.ts +2 -0
  275. package/dist/files/file-routes.js +40 -10
  276. package/dist/files/provider-resolver.d.ts +1 -0
  277. package/dist/files/provider-resolver.js +1 -1
  278. package/dist/files/record-write-check.d.ts +6 -0
  279. package/dist/files/record-write-check.js +13 -0
  280. package/dist/files/resolve-content-type.js +43 -0
  281. package/dist/files/upload-ledger.d.ts +2 -2
  282. package/dist/files/upload-ledger.js +11 -4
  283. package/dist/i18n/required-surface-keys.js +189 -208
  284. package/dist/internal/stack-support.d.ts +2 -1
  285. package/dist/internal/stack-support.js +1 -1
  286. package/dist/internal/testing.js +1 -1
  287. package/dist/jobs/job-runner-context.d.ts +32 -0
  288. package/dist/jobs/job-runner-context.js +305 -0
  289. package/dist/jobs/job-runner-execution.d.ts +7 -0
  290. package/dist/jobs/job-runner-execution.js +160 -0
  291. package/dist/jobs/job-runner-gates.d.ts +19 -0
  292. package/dist/jobs/job-runner-gates.js +198 -0
  293. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  294. package/dist/jobs/job-runner-helpers.js +61 -2
  295. package/dist/jobs/job-runner-scope.d.ts +44 -0
  296. package/dist/jobs/job-runner-scope.js +102 -0
  297. package/dist/jobs/job-runner-types.d.ts +12 -0
  298. package/dist/jobs/job-runner.d.ts +0 -1
  299. package/dist/jobs/job-runner.js +50 -657
  300. package/dist/observability/escape-hatch-report.d.ts +3 -0
  301. package/dist/observability/escape-hatch-report.js +21 -3
  302. package/dist/pipeline/after-rollback-sink.d.ts +6 -0
  303. package/dist/pipeline/after-rollback-sink.js +32 -0
  304. package/dist/pipeline/cascade-handler.js +165 -138
  305. package/dist/pipeline/dispatch-batch.js +8 -1
  306. package/dist/pipeline/dispatch-query.js +10 -3
  307. package/dist/pipeline/dispatch-shared.d.ts +13 -3
  308. package/dist/pipeline/dispatch-shared.js +62 -710
  309. package/dist/pipeline/dispatch-write.js +296 -189
  310. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  311. package/dist/pipeline/event-dispatcher-delivery.js +6 -6
  312. package/dist/pipeline/event-dispatcher.d.ts +1 -0
  313. package/dist/pipeline/event-dispatcher.js +6 -9
  314. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  315. package/dist/pipeline/handler-context-auth.js +54 -0
  316. package/dist/pipeline/handler-context-config.d.ts +14 -0
  317. package/dist/pipeline/handler-context-config.js +113 -0
  318. package/dist/pipeline/handler-context-db.d.ts +24 -0
  319. package/dist/pipeline/handler-context-db.js +149 -0
  320. package/dist/pipeline/handler-context-dispatch.d.ts +23 -0
  321. package/dist/pipeline/handler-context-dispatch.js +101 -0
  322. package/dist/pipeline/handler-context-events.d.ts +41 -0
  323. package/dist/pipeline/handler-context-events.js +239 -0
  324. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  325. package/dist/pipeline/handler-context-notify.js +55 -0
  326. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  327. package/dist/pipeline/handler-context-observability.js +21 -0
  328. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  329. package/dist/pipeline/handler-context-scope.js +88 -0
  330. package/dist/pipeline/notify-scope.js +12 -1
  331. package/dist/pipeline/pre-handler-gates.js +20 -4
  332. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  333. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  334. package/dist/pipeline/projection-rebuild.js +2 -1
  335. package/dist/pipeline/system-hooks.d.ts +3 -1
  336. package/dist/pipeline/system-hooks.js +55 -6
  337. package/dist/pipeline/write-result-masking.js +13 -10
  338. package/dist/schema-cli/apply-command.d.ts +2 -0
  339. package/dist/schema-cli/apply-command.js +97 -0
  340. package/dist/schema-cli/baseline-command.d.ts +2 -0
  341. package/dist/schema-cli/baseline-command.js +33 -0
  342. package/dist/schema-cli/generate-command.d.ts +2 -0
  343. package/dist/schema-cli/generate-command.js +90 -0
  344. package/dist/schema-cli/shared.d.ts +39 -0
  345. package/dist/schema-cli/shared.js +95 -0
  346. package/dist/schema-cli/status-command.d.ts +2 -0
  347. package/dist/schema-cli/status-command.js +73 -0
  348. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  349. package/dist/schema-cli/validate-boot-input.js +130 -0
  350. package/dist/schema-cli/validate-command.d.ts +2 -0
  351. package/dist/schema-cli/validate-command.js +130 -0
  352. package/dist/schema-cli.d.ts +2 -18
  353. package/dist/schema-cli.js +47 -604
  354. package/dist/search/meilisearch-adapter.js +2 -1
  355. package/dist/search/reindex-entity.js +15 -5
  356. package/dist/ui-types/index.d.ts +2 -2
  357. package/dist/ui-types/index.js +1 -1
  358. package/dist/upgrade-changelog.d.ts +2 -0
  359. package/dist/upgrade-changelog.js +14 -0
  360. package/dist/upgrade-cli.d.ts +3 -12
  361. package/dist/upgrade-cli.js +171 -197
  362. package/dist/upgrade-marker-parse.d.ts +32 -0
  363. package/dist/upgrade-marker-parse.js +73 -0
  364. package/package.json +9 -6
  365. package/src/changes.json +555 -0
  366. package/src/scripts/codemod/codemod-support.ts +1 -1
  367. package/src/scripts/codemod/db-factory.ts +1 -1
  368. package/src/scripts/codemod/feature-layout.ts +10 -2
  369. package/src/scripts/codemod/object-form.ts +41 -4
  370. package/src/scripts/codemod/package-merge.ts +58 -8
  371. package/src/scripts/codemod/pii-personal-migration.ts +175 -145
  372. package/src/scripts/codemod/testing-injection.ts +122 -42
  373. package/src/scripts/codemod/testing-move.ts +29 -7
  374. package/dist/engine/provider-plugin-context.d.ts +0 -3
  375. package/dist/engine/provider-plugin-context.js +0 -28
@@ -0,0 +1,47 @@
1
+ import { hasAccess } from "../../engine/access.js";
2
+ import { toKebab } from "../../engine/qualified-name.js";
3
+ import { DEFAULT_EXPORT_MAX_ROWS } from "../../engine/types/screen.js";
4
+ import { AccessDeniedError } from "../../errors/index.js";
5
+ import { resolveExportColumns } from "./export-columns.js";
6
+ import { exportRejection } from "./export-errors.js";
7
+ const SCREEN_QN = /^[A-Za-z0-9_-]+:screen:[A-Za-z0-9_-]+$/;
8
+ function findExportScreen(registry, screenQn) {
9
+ // The client sends the feature name as authored (camelCase allowed); the registry keys are kebab.
10
+ const registryQn = toKebab(screenQn);
11
+ const screen = SCREEN_QN.test(screenQn) ? registry.getScreen(registryQn) : undefined;
12
+ if (screen === undefined || (screen.type !== "entityList" && screen.type !== "projectionList")) {
13
+ throw exportRejection("export_unknown_screen", "screen");
14
+ }
15
+ return { registryQn, screen };
16
+ }
17
+ export function resolveExportPlan(registry, screenQn, requestedColumns, user) {
18
+ const { registryQn, screen } = findExportScreen(registry, screenQn);
19
+ if (screen.table?.export?.xlsx !== true)
20
+ throw exportRejection("export_not_enabled", "screen");
21
+ if (screen.access !== undefined && !hasAccess(user, screen.access)) {
22
+ throw new AccessDeniedError({ details: { screen: screenQn } });
23
+ }
24
+ // Label keys use the feature name as authored, query types the kebab form.
25
+ const featureName = registry.getScreenFeature(registryQn) ?? "";
26
+ const screenShortId = registryQn.split(":")[2] ?? "";
27
+ const maxRows = screen.table.export.maxRows ?? DEFAULT_EXPORT_MAX_ROWS;
28
+ if (screen.type === "projectionList") {
29
+ return {
30
+ screenShortId,
31
+ queryType: screen.query,
32
+ screenFilter: screen.filter,
33
+ columns: resolveExportColumns({ featureName, columns: screen.columns, entity: undefined, entityName: undefined, user }, requestedColumns),
34
+ maxRows,
35
+ };
36
+ }
37
+ const entity = registry.getEntity(screen.entity);
38
+ if (entity === undefined)
39
+ throw exportRejection("export_unknown_screen", "screen");
40
+ return {
41
+ screenShortId,
42
+ queryType: `${toKebab(featureName)}:query:${toKebab(screen.entity)}:list`,
43
+ screenFilter: screen.filter,
44
+ columns: resolveExportColumns({ featureName, columns: screen.columns, entity, entityName: screen.entity, user }, requestedColumns),
45
+ maxRows,
46
+ };
47
+ }
@@ -0,0 +1,4 @@
1
+ import { type Translate } from "./export-cells.ts";
2
+ import type { ExportColumn } from "./export-columns.ts";
3
+ import type { ExportRow } from "./export-rows.ts";
4
+ export declare function writeExportWorkbook(columns: readonly ExportColumn[], rows: readonly ExportRow[], translate: Translate, timeZone: string): Promise<Uint8Array<ArrayBuffer>>;
@@ -0,0 +1,21 @@
1
+ import writeXlsxFile, {} from "write-excel-file/node";
2
+ import { toExportCell } from "./export-cells.js";
3
+ import { guardSheetAgainstCiphertext } from "./export-ciphertext-guard.js";
4
+ function headerLabel(column, translate) {
5
+ const translated = translate(column.headerKey);
6
+ return translated === column.headerKey ? column.headerFallback : translated;
7
+ }
8
+ export async function writeExportWorkbook(columns, rows, translate, timeZone) {
9
+ const header = columns.map((column) => ({
10
+ value: headerLabel(column, translate),
11
+ type: String,
12
+ fontWeight: "bold",
13
+ }));
14
+ const body = rows.map((row) => columns.map((column) => toExportCell(column, { row, translate, timeZone })));
15
+ const sheetData = guardSheetAgainstCiphertext([header, ...body]);
16
+ const buffer = await writeXlsxFile(sheetData, {}).toBuffer();
17
+ // Buffer is typed over ArrayBufferLike; a plain copy gives Response a BodyInit-compatible view.
18
+ const bytes = new Uint8Array(buffer.byteLength);
19
+ bytes.set(buffer);
20
+ return bytes;
21
+ }
@@ -70,6 +70,10 @@ export type SignatureExtraRouteDeps = {
70
70
  * caller's authenticity (signature, HMAC state, etc.). */
71
71
  readonly dispatchSystemWrite: (args: SystemDispatchArgs) => Promise<WriteResult>;
72
72
  readonly dispatchSystemQuery: (args: SystemDispatchArgs) => Promise<unknown>;
73
+ /** Same trust as dispatchSystemWrite, but runs as the tenant's system user
74
+ * with the "system" role plus exactly `roles` (no SystemAdmin), so the write
75
+ * passes app-role access checks. */
76
+ readonly dispatchWriteAs: (args: RoleDispatchArgs) => Promise<WriteResult>;
73
77
  };
74
78
  export type SignatureExtraRoute<TVerified> = {
75
79
  readonly method: HttpRouteMethod;
@@ -27,7 +27,9 @@ export type { MetricsRouteOptions } from "./route-registrars.ts";
27
27
  export { createMetricsApp, DEFAULT_MAX_REQUEST_BYTES, isPrometheusMeter, } from "./route-registrars.ts";
28
28
  export { createApiRoutes } from "./routes.ts";
29
29
  export type { KumikoServer, ServerOptions } from "./server.ts";
30
- export { buildServer, makeDispatchSystemQuery, makeDispatchSystemWrite, makeDispatchWriteAs, withFileProviderResolver, } from "./server.ts";
30
+ export { buildServer } from "./server.ts";
31
+ export { makeDispatchSystemQuery, makeDispatchSystemWrite, makeDispatchWriteAs, } from "./server-extra-routes.ts";
32
+ export { withFileProviderResolver } from "./server-wiring.ts";
31
33
  export type { ResolvedSessionCookies, SessionCookieBase, SessionCookieNames, } from "./session-cookies.ts";
32
34
  export { AUTH_COOKIE_BASE, CSRF_COOKIE_BASE, getSessionCookies, HOST_COOKIE_PREFIX, isHostPrefixedCookieName, resolveSessionCookieNames, resolveSessionCookies, sessionCookieName, sessionCookiesMiddleware, TENANT_COOKIE_BASE, } from "./session-cookies.ts";
33
35
  export type { SseBroker, SseClient, SseEvent } from "./sse-broker.ts";
package/dist/api/index.js CHANGED
@@ -16,7 +16,9 @@ export { readonlyRequestContext as requestContext, } from "./request-context.js"
16
16
  export { buildRequestContextDataFromRequest, requestIdMiddleware, } from "./request-id-middleware.js";
17
17
  export { createMetricsApp, DEFAULT_MAX_REQUEST_BYTES, isPrometheusMeter, } from "./route-registrars.js";
18
18
  export { createApiRoutes } from "./routes.js";
19
- export { buildServer, makeDispatchSystemQuery, makeDispatchSystemWrite, makeDispatchWriteAs, withFileProviderResolver, } from "./server.js";
19
+ export { buildServer } from "./server.js";
20
+ export { makeDispatchSystemQuery, makeDispatchSystemWrite, makeDispatchWriteAs, } from "./server-extra-routes.js";
21
+ export { withFileProviderResolver } from "./server-wiring.js";
20
22
  export { AUTH_COOKIE_BASE, CSRF_COOKIE_BASE, getSessionCookies, HOST_COOKIE_PREFIX, isHostPrefixedCookieName, resolveSessionCookieNames, resolveSessionCookies, sessionCookieName, sessionCookiesMiddleware, TENANT_COOKIE_BASE, } from "./session-cookies.js";
21
23
  export { createSseBroker } from "./sse-broker.js";
22
24
  export { createSseRoute, SSE_HEARTBEAT_INTERVAL_MS } from "./sse-route.js";
@@ -1,2 +1,5 @@
1
1
  import type { MiddlewareHandler } from "hono";
2
+ export declare const CIPHERTEXT_RE: RegExp;
3
+ export declare const CIPHERTEXT_REDACT_RE: RegExp;
4
+ export declare const CIPHERTEXT_REDACTION = "[pii-redacted]";
2
5
  export declare function piiCiphertextResponseGuard(): MiddlewareHandler;
@@ -8,8 +8,9 @@ const isProductionEnv = () => process.env["NODE_ENV"] === "production";
8
8
  // Bound to the full ciphertext shape (prefix, "<kind>:<id>" subject key, base64
9
9
  // blob of at least IV + GCM tag = 28 bytes) so a user-typed "kumiko-pii:v" in a
10
10
  // note or name field cannot turn every read of that record into a 500.
11
- const CIPHERTEXT_RE = /kumiko-pii:v\d+:[^:"\s<>\\]+:[^:"\s<>\\]+:[A-Za-z0-9+/=]{40,}/;
12
- const CIPHERTEXT_REDACT_RE = new RegExp(CIPHERTEXT_RE.source, "g");
11
+ export const CIPHERTEXT_RE = /kumiko-pii:v\d+:[^:"\s<>\\]+:[^:"\s<>\\]+:[A-Za-z0-9+/=]{40,}/;
12
+ export const CIPHERTEXT_REDACT_RE = new RegExp(CIPHERTEXT_RE.source, "g");
13
+ export const CIPHERTEXT_REDACTION = "[pii-redacted]";
13
14
  // A PII subject ciphertext never belongs in an API response — its presence
14
15
  // means a raw DB read (fetchOne/selectMany) leaked to the surface. Dev/test
15
16
  // fail loud (500) so a forgotten decrypt turns the first integration test
@@ -41,6 +41,6 @@ export declare function runWithOrigin<T>(origin: {
41
41
  readonly risk: AgentRisk;
42
42
  };
43
43
  }, fn: () => T): T;
44
- export declare function runAsDirectCallEntry<T>(fn: () => T): T;
44
+ export declare function runAsAgentToolCallEntry<T>(fn: () => T): T;
45
45
  export declare function isAgentToolCallScope(): boolean;
46
46
  export declare function runWithWriteOrigin<T>(origin: WriteOrigin, fn: () => T): T;
@@ -53,7 +53,7 @@ export function runWithOrigin(origin, fn) {
53
53
  // surrounding turn/approve handler. Reserved for dispatchToolCall, so it also marks
54
54
  // the scope as an agent tool call; it always mints a scope, because an agent loop
55
55
  // running outside any request must still carry that marker.
56
- export function runAsDirectCallEntry(fn) {
56
+ export function runAsAgentToolCallEntry(fn) {
57
57
  const current = requestContext.get();
58
58
  const requestId = current?.requestId ?? requestContext.generateId();
59
59
  const { entryHandler: _entryHandler, ...rest } = current ?? {};
@@ -1,4 +1,5 @@
1
1
  import { type Context, Hono } from "hono";
2
+ import { type KumikoError } from "../errors/index.ts";
2
3
  import type { Dispatcher } from "../pipeline/dispatcher.ts";
3
4
  export declare const StreamFrame: {
4
5
  readonly chunk: "chunk";
@@ -18,4 +19,19 @@ export type SseWriter = {
18
19
  };
19
20
  export declare function pumpStream(stream: SseWriter, generator: AsyncGenerator<unknown>, heartbeatMs: number, firstPull?: Promise<IteratorResult<unknown>>): Promise<void>;
20
21
  export declare const MAX_PAYLOAD_DEPTH = 20;
22
+ export declare function assertPayloadDepthAllowed(payload: unknown): void;
23
+ export declare function queryErrorResponse(c: Context, err: KumikoError, type?: unknown): (Response & import("hono").TypedResponse<{
24
+ readonly error: {
25
+ readonly code: string;
26
+ readonly i18nKey: string;
27
+ readonly i18nParams?: {
28
+ readonly [x: string]: import("hono/utils/types").JSONValue;
29
+ } | undefined;
30
+ readonly message: string;
31
+ readonly details?: import("hono/utils/types").JSONValue | undefined;
32
+ readonly docsUrl?: string;
33
+ readonly requestId?: string;
34
+ readonly timestamp: string;
35
+ };
36
+ }, -1 | 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511, "json">) | (Response & import("hono").TypedResponse<null, -1 | 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511, "body">);
21
37
  export declare function handleUncaughtRouteError(err: Error, c: Context): Response | Promise<Response>;
@@ -312,7 +312,7 @@ async function readDispatchBody(c) {
312
312
  // jsonb serialization of custom-fields payloads) walks the same object
313
313
  // graph an attacker controls, so an uncapped depth is a stack-overflow /
314
314
  // CPU-DoS vector even under the 1MB body-byte limit.
315
- function assertPayloadDepthAllowed(payload) {
315
+ export function assertPayloadDepthAllowed(payload) {
316
316
  if (payloadDepth(payload, 0) > MAX_PAYLOAD_DEPTH) {
317
317
  throw new ValidationError({
318
318
  fields: [
@@ -428,7 +428,7 @@ function clientAbortResponse(c, requestId, type) {
428
428
  }
429
429
  // For /query + /command: no isSuccess on success (just { data } / {ok}), so we
430
430
  // keep the same lean shape on failure — only the `error` key.
431
- function queryErrorResponse(c, err, type) {
431
+ export function queryErrorResponse(c, err, type) {
432
432
  const requestId = requestContext.get()?.requestId;
433
433
  if (isClientAbort(err))
434
434
  return clientAbortResponse(c, requestId, type);
@@ -0,0 +1,10 @@
1
+ import { type AppContext, type Registry } from "../engine/types/index.ts";
2
+ import type { createFallbackLogger } from "../logging/index.ts";
3
+ import type { AuthRoutesConfig } from "./auth-routes.ts";
4
+ export type BootLogger = ReturnType<typeof createFallbackLogger>;
5
+ export declare function assertNoPayloadRateLimitsWithoutResolver(registry: Registry): void;
6
+ export declare function assertFileStorageProviderForFileFields(registry: Registry, context: AppContext): void;
7
+ export declare function warnSearchableScreensWithoutSearchAdapter(registry: Registry, context: AppContext, bootLog: BootLogger): void;
8
+ export declare function resolveServerInstanceId(explicitInstanceId: string | undefined, bootLog: BootLogger): string;
9
+ export declare function warnUnlimitedSystemScopeFeatures(registry: Registry, bootLog: BootLogger): void;
10
+ export declare function assertSinglePrincipalStatusProvider(registry: Registry, auth: AuthRoutesConfig): void;
@@ -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>;