@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
@@ -1,38 +1,26 @@
1
- import { crossTenantReadsGrantFor, escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
2
- import { isAgentToolCallScope, requestContext, runWithOrigin } from "../api/request-context.js";
3
- import { createHandlerCrossTenantReads } from "../db/cross-tenant-reads.js";
4
- import { runInSavepoint, selectMany } from "../db/query.js";
5
- import { createTenantDb, createUncheckedSystemDb, hasTenantColumn, } from "../db/tenant-db.js";
6
- import { createDerivativesContext } from "../derivatives/derivatives-context.js";
7
- import { mayOverrideTenant } from "../engine/cross-tenant.js";
8
- import { createSystemUser } from "../engine/system-user.js";
1
+ import { requestContext, runWithOrigin } from "../api/request-context.js";
2
+ import { createTenantDb } from "../db/tenant-db.js";
9
3
  import { declaredRateLimitOptions, isRateLimitDisabled, resolveAgentExposure, } from "../engine/types/index.js";
10
- import { AccessDeniedError, FeatureDisabledError, FrameworkReasons, InternalError, memberResolutionReadOnlyDenied, RateLimitError, RateLimitUnavailableError, VersionConflictError, writeFailure, } from "../errors/index.js";
11
- import { archiveStream as archiveStreamHelper, isStreamArchived, restoreStream as restoreStreamHelper, } from "../event-store/archive.js";
12
- import { IdempotentAppendConflictError as EventStoreIdempotentAppendConflictError, VersionConflictError as EventStoreVersionConflictError, } from "../event-store/errors.js";
13
- import { loadAggregate, loadAggregateAsOf, loadAggregateForWriting, } from "../event-store/event-store.js";
14
- import { effectiveSnapshotGeneration, loadAggregateWithSnapshot, saveSnapshot, } from "../event-store/snapshot.js";
15
- import { upcastStoredEvent, upcastStoredEvents } from "../event-store/upcaster.js";
16
- import { createTenantFileContext } from "../files/file-handle.js";
17
- import { DEFAULT_LOCALE, isValidLocaleTag } from "../i18n/request-locale.js";
4
+ import { FeatureDisabledError, InternalError, memberResolutionReadOnlyDenied, RateLimitError, RateLimitUnavailableError, } from "../errors/index.js";
18
5
  import { createFallbackLogger } from "../logging/utils.js";
19
- import { createEscapeHatchReporter, createMetricsForFactory, createMetricsHandle, createNoopMetricsHandle, emitDispatcherError, emitDispatcherHandler, getFallbackTracer, observabilityContext, } from "../observability/index.js";
6
+ import { createEscapeHatchReporter, emitDispatcherError, emitDispatcherHandler, observabilityContext, } from "../observability/index.js";
20
7
  import { buildBucketKey, buildPayloadBucketKey, normalizePayloadBucketValue, } from "../rate-limit/index.js";
21
- import { createTzContext, isValidIanaTimeZone } from "../time/index.js";
22
8
  import { isPlainObject } from "../utils/is-plain-object.js";
23
- import { INTERACTIVE_SIGN_IN_POLICY, resolveActiveMembershipFn } from "./active-membership.js";
24
- import { appendDomainEventCore } from "./append-event-core.js";
25
9
  import { resolveAuthClaims as runAuthClaimsResolver } from "./auth-claims-resolver.js";
26
10
  import { configAccessorFactoryOf } from "./config-accessor-factory.js";
27
- import { runBatch, unwrapSingle } from "./dispatch-batch.js";
28
11
  import { executeQuery } from "./dispatch-query.js";
29
- import { executeWrite } from "./dispatch-write.js";
30
- import { dispatcherSpanAttributes, isFailedWriteResult, NestedWriteRollback, } from "./dispatcher-utils.js";
31
- import { createMemberReaderFn } from "./member-reader.js";
32
- import { createDeferringNotify } from "./notify-scope.js";
33
- import { createGatedIdentitySwitch, createGatedMemberReader, createGatedProjectionReader, isSystemIdentity, systemIdentitySwitchDenied, } from "./system-identity-switch.js";
12
+ import { dispatcherSpanAttributes, isFailedWriteResult, } from "./dispatcher-utils.js";
13
+ import { buildAuthFacet } from "./handler-context-auth.js";
14
+ import { buildConfigFacet, resolveHandlerLocale, resolveHandlerTz, } from "./handler-context-config.js";
15
+ import { buildFileFragment, buildForTenantFragment, buildHandlerDbFacet, } from "./handler-context-db.js";
16
+ import { buildDispatchFacet, createCommitSinks } from "./handler-context-dispatch.js";
17
+ import { buildEventStoreFacet } from "./handler-context-events.js";
18
+ import { buildNotifyFacet } from "./handler-context-notify.js";
19
+ import { buildObservabilityFacet } from "./handler-context-observability.js";
20
+ import { buildPreSaveFragment, buildRuntimeFragment, resolveHandlerContextScope, } from "./handler-context-scope.js";
21
+ import { createGatedIdentitySwitch, isSystemIdentity } from "./system-identity-switch.js";
34
22
  import { TENANT_TIMEZONE_CONFIG_KEY } from "./tenant-timezone-config-key.js";
35
- import { buildPersonalDataGate, rootWriteOrigin } from "./write-origin.js";
23
+ import { rootWriteOrigin } from "./write-origin.js";
36
24
  export { TENANT_TIMEZONE_CONFIG_KEY };
37
25
  // Same bundled-features-free constraint as above — these name the config
38
26
  // feature's write handlers so dispatch-write.ts can invalidate
@@ -42,39 +30,15 @@ export { TENANT_TIMEZONE_CONFIG_KEY };
42
30
  export const CONFIG_WRITE_SET_TYPE = "config:write:set";
43
31
  export const CONFIG_WRITE_RESET_TYPE = "config:write:reset";
44
32
  export const DEFAULT_MEMBER_READ_TIMEOUT_MS = 10_000;
45
- // Narrowing-helper: AppContext.db ist DbConnection|TenantDb|undefined. Die
46
- // dispatch-Pfade brauchen DbConnection (oder DbTx aus Caller-Scope) für
47
- // appendEvent/projection-writes; TenantDb-Branch wird hier ausgeschlossen.
33
+ // Narrowing helper: AppContext.db is DbConnection|TenantDb|undefined. The
34
+ // dispatch paths need DbConnection (or DbTx from the caller scope) for
35
+ // appendEvent/projection writes; the TenantDb branch is excluded here.
48
36
  export function resolveDbSource(ctx, tx) {
49
37
  return tx ?? resolveAppPoolDb(ctx);
50
38
  }
51
- function resolveAppPoolDb(ctx) {
39
+ export function resolveAppPoolDb(ctx) {
52
40
  return ctx.appContext.db; // @cast-boundary db-operator
53
41
  }
54
- // ctx.appendEvent — append a domain event onto a specific aggregate stream
55
- // in the current tx, then fire matching inline projections. Core logic
56
- // lives in appendDomainEventCore; this wrapper just locates dbSource +
57
- // stringifies the SessionUser id for the shared helper.
58
- async function appendDomainEvent(ctx, args, user, tx, callerFeature) {
59
- // Sink behind every append surface, so a resolved member stays read-only even via fetchForWriting handles.
60
- if (isMemberResolutionPrincipal(user))
61
- throw memberResolutionReadOnlyDenied();
62
- const { registry } = ctx;
63
- const dbSource = resolveDbSource(ctx, tx);
64
- if (!dbSource) {
65
- throw new InternalError({
66
- message: `ctx.appendEvent("${args.type}") requires a database connection — none is configured.`,
67
- });
68
- }
69
- await appendDomainEventCore({
70
- registry,
71
- db: dbSource,
72
- tenantId: user.tenantId,
73
- userId: String(user.id),
74
- callSiteLabel: "ctx.appendEvent",
75
- callerFeature,
76
- }, args);
77
- }
78
42
  // r.systemScope() handlers must go through ctx.systemDb's guarded methods
79
43
  // (assertTenantMatch / acknowledgeCrossTenant) instead of plain ctx.db —
80
44
  // "system" mode has no tenant filter at all, so silent ctx.db or
@@ -83,7 +47,7 @@ async function appendDomainEvent(ctx, args, user, tx, callerFeature) {
83
47
  // runtime-only registry lookup, not a type-level discriminant), so instead
84
48
  // of omitting the key we hand back a Proxy that throws on first touch,
85
49
  // naming the handler and pointing at the matching ctx.systemDb escape hatch.
86
- function createSystemScopedDbGuard(handlerType, fieldName, hint) {
50
+ export function createSystemScopedDbGuard(handlerType, fieldName, hint) {
87
51
  return new Proxy({}, {
88
52
  get(_target, prop) {
89
53
  throw new InternalError({
@@ -97,14 +61,6 @@ function createSystemScopedDbGuard(handlerType, fieldName, hint) {
97
61
  export function isMemberResolutionPrincipal(user) {
98
62
  return user.origin === "member-resolution";
99
63
  }
100
- // DispatchContext.jobRunner is typed as the handleEvent-only JobRunnerRef but
101
- // holds the concrete runner at boot; narrow instead of casting.
102
- function isNotifyJobDispatcher(runner) {
103
- return (typeof runner === "object" &&
104
- runner !== null &&
105
- "dispatch" in runner &&
106
- typeof runner.dispatch === "function");
107
- }
108
64
  async function denyMemberResolutionWrite() {
109
65
  throw memberResolutionReadOnlyDenied();
110
66
  }
@@ -256,671 +212,60 @@ export function applyBeforeTransactionReadOnly(handlerContext) {
256
212
  dbOutsideTransaction: undefined,
257
213
  };
258
214
  }
259
- export async function buildHandlerContext(ctx, type, user, origin, tx, afterCommitHooks, includeDeleted, transactionScope = tx ? "inTransaction" : "withoutTransaction",
260
- // A query bridge has no commit sink of its own, but ctx.notify inside it must still wait for
261
- // the outer write's commit: the outer sink is lent to notify only, not to scheduleAfterCommit.
262
- outerNotifySink,
263
- // beforeTransaction phase: ctx.forTenant stays available but every target db denies writes.
264
- readOnlyForTenant = false) {
265
- const { registry, appContext: context, effectiveFeatures, jobRunner, lifecycle } = ctx;
266
- const isSystem = registry.isHandlerSystemScoped(type);
267
- // The outer dispatcher receives a DbConnection from the server/stack;
268
- // AppContext's `db` union also allows TenantDb (for downstream hook calls),
269
- // but at this point we're the root of the pipeline — cast is safe.
270
- const dbSource = resolveDbSource(ctx, tx);
271
- const reqCtx = requestContext.get();
272
- // global() writes are write-handler-only; SYSTEM identity switch and unsafeRaw accept write, query or stream escapeHatch.
273
- const writeHandler = registry.getWriteHandler(type);
274
- const writeEscapeHatch = writeHandler?.escapeHatch;
275
- const isPerCommand = writeHandler?.transaction === "perCommand";
276
- const denyDbWrites = transactionScope === "withoutTransaction";
277
- const handlerEscapeHatch = writeEscapeHatch ??
278
- registry.getQueryHandler(type)?.escapeHatch ??
279
- registry.getStreamHandler(type)?.escapeHatch;
280
- const identitySwitchEscapeHatch = escapeHatchFor(handlerEscapeHatch, "systemIdentity");
281
- const hasIdentitySwitchGrant = isSystem || identitySwitchEscapeHatch !== undefined;
282
- const featureName = registry.getHandlerFeature(type);
283
- const reportEscapeHatch = createEscapeHatchReporter({
284
- handler: type,
285
- tenantId: user.tenantId,
286
- actor: user.id,
287
- sink: context._escapeHatchAuditSink,
288
- log: context.log ?? createFallbackLogger("dispatcher"),
289
- window: ctx.escapeHatchReportWindow,
290
- });
291
- const identitySwitchGrantReason = identitySwitchEscapeHatch?.reason ??
292
- (isSystem ? `r.systemScope() feature "${featureName ?? "unknown"}"` : undefined);
293
- const buildTenantScopedDb = (source, signal, foreignTenantId, denyWrites = false) => createTenantDb({
294
- db: source,
295
- tenantId: foreignTenantId ?? user.tenantId,
296
- mode: isSystem && foreignTenantId === undefined ? "system" : "tenant",
297
- tracer: context.tracer,
298
- meter: context.meter,
299
- signal,
300
- grants: {
301
- ...(foreignTenantId === undefined && {
302
- globalWrites: escapeHatchFor(writeEscapeHatch, "globalWrites"),
303
- unsafeRaw: escapeHatchFor(handlerEscapeHatch, "unsafeRaw"),
304
- }),
305
- report: reportEscapeHatch,
306
- memberReadOnly: isMemberResolutionPrincipal(user),
307
- personalDataGate: buildPersonalDataGate(registry, origin),
308
- projectionRegistry: registry,
309
- ...(denyWrites && { writesDeniedReason: "without an open transaction" }),
310
- },
311
- });
312
- // Propagate the request's AbortSignal so every TenantDb query throws when
313
- // the client has disconnected — handlers with many sequential queries skip
314
- // the rest of the chain instead of burning DB-CPU for results no one reads.
315
- const db = dbSource
316
- ? buildTenantScopedDb(dbSource, reqCtx?.signal, undefined, denyDbWrites)
317
- : undefined;
318
- // Unbound pool, tenant-scoped like `db` but never tx-bound — writes
319
- // through it survive a rollback of the handler's own transaction. No
320
- // AbortSignal here: a client disconnect must not abort a durability write
321
- // that is meant to outlive the request. Computed before `systemDb` below
322
- // so its guarded escape hatch (ctx.systemDb.outsideTransaction) can wrap
323
- // this same TenantDb instead of a second, independently-built one.
324
- const outsideTxSource = ctx.outsideTransactionDb ?? resolveAppPoolDb(ctx);
325
- const rawDbOutsideTransaction = outsideTxSource
326
- ? buildTenantScopedDb(outsideTxSource, undefined)
327
- : undefined;
328
- const systemDb = isSystem && db
329
- ? createUncheckedSystemDb(db, rawDbOutsideTransaction, reportEscapeHatch)
330
- : undefined;
331
- // An agent tool call runs with the user's session but feeds the LLM context, so
332
- // cross-tenant rows must never reach it, even when unsafeRaw implies the grant.
333
- const crossTenantReadsReason = isAgentToolCallScope()
334
- ? undefined
335
- : crossTenantReadsGrantFor(handlerEscapeHatch)?.reason;
336
- const crossTenantReads = dbSource && crossTenantReadsReason !== undefined
337
- ? createHandlerCrossTenantReads({
338
- db: dbSource,
339
- tenantId: user.tenantId,
340
- tracer: context.tracer,
341
- meter: context.meter,
342
- signal: reqCtx?.signal,
343
- reason: crossTenantReadsReason,
344
- report: reportEscapeHatch,
345
- grants: {
346
- report: reportEscapeHatch,
347
- memberReadOnly: isMemberResolutionPrincipal(user),
348
- personalDataGate: buildPersonalDataGate(registry, origin),
349
- projectionRegistry: registry,
350
- },
351
- })
352
- : undefined;
353
- // Exposed as ctx.db below — the internal `db` above stays the real,
354
- // working TenantDb for this function's own use (config/derivatives
355
- // accessors, systemDb construction).
356
- const exposedDb = isSystem && db
357
- ? createSystemScopedDbGuard(type, "db", "ctx.systemDb.assertTenantMatch(...) or ctx.systemDb.acknowledgeCrossTenant(...)")
358
- : db;
359
- // Same fail-closed treatment as `exposedDb` — a system handler reaching
360
- // ctx.dbOutsideTransaction directly would be a second, unguarded door to
361
- // the same cross-tenant access ctx.db closes off above.
362
- const dbOutsideTransaction = isSystem && rawDbOutsideTransaction
363
- ? createSystemScopedDbGuard(type, "dbOutsideTransaction", "ctx.systemDb.outsideTransaction.assertTenantMatch(...) or " +
364
- "ctx.systemDb.outsideTransaction.acknowledgeCrossTenant(...)")
365
- : rawDbOutsideTransaction;
366
- const log = context.log?.child({
367
- handler: type,
368
- tenantId: user.tenantId,
369
- userId: user.id,
370
- ...(reqCtx && { requestId: reqCtx.requestId }),
371
- });
372
- // Cross-feature bridge: ctx.query/write share the current tx + afterCommitHooks sink.
373
- // queryAs/writeAs to anyone but the caller itself needs r.systemScope() or { escapeHatch } (system-identity-switch.ts).
374
- const bridgeSink = afterCommitHooks ?? [];
375
- // Query bridges and post-commit contexts get a throwaway sink: hooks pushed there never run.
376
- const hasLiveCommitSink = afterCommitHooks !== undefined && transactionScope !== "postCommit";
377
- const scheduleAfterCommit = (hook) => {
378
- bridgeSink.push(hook);
379
- };
380
- const notifyFactory = context._notifyFactory;
381
- const notifyJobDispatcher = isNotifyJobDispatcher(jobRunner) ? jobRunner : undefined;
382
- // Inside the write tx the app pool's slots can all be held by waiting write txs, so the
383
- // delivery's own DB work (broadcast lookup, attempt log, channel sends) runs on the outside-tx pool.
384
- const notifyInTransactionFactory = notifyFactory && tx !== undefined
385
- ? (notifyUser, notifyTenantId) => notifyFactory(notifyUser, notifyTenantId, notifyJobDispatcher, outsideTxSource, context._fileProviderResolver)
386
- : undefined;
387
- const notifyAfterCommitFactory = notifyFactory
388
- ? (notifyUser, notifyTenantId) => notifyFactory(notifyUser, notifyTenantId, notifyJobDispatcher, undefined, context._fileProviderResolver)
389
- : undefined;
390
- const notify = buildNotify();
391
- function buildNotify() {
392
- if (!notifyAfterCommitFactory)
393
- return undefined;
394
- const notifyAfterCommit = notifyAfterCommitFactory(user, user.tenantId);
395
- if (!notifyInTransactionFactory)
396
- return notifyAfterCommit;
397
- const notifyInTransaction = notifyInTransactionFactory(user, user.tenantId);
398
- // A rolled-back write must leave no notification behind. Without any commit to wait for (a
399
- // post-commit context) delivery stays immediate.
400
- const notifyCommitSink = hasLiveCommitSink ? bridgeSink : outerNotifySink;
401
- if (notifyCommitSink === undefined)
402
- return notifyInTransaction;
403
- return createDeferringNotify({
404
- registry,
405
- tenantId: user.tenantId,
406
- notifyAfterCommit,
407
- notifyNow: notifyInTransaction,
408
- scheduleAfterCommit: (hook) => {
409
- notifyCommitSink.push(hook);
410
- },
411
- });
412
- }
413
- // Mirror notify: only built when the config feature wired its factory.
414
- const configFactory = configAccessorFactoryOf(context);
415
- const config = configFactory && db
416
- ? configFactory({
417
- user: { id: user.id, tenantId: user.tenantId },
418
- db,
419
- secrets: context.secrets,
420
- })
421
- : undefined;
422
- // Cross-tenant accessor for SystemAdmin/system callers. The
423
- // identity is the target tenant's system user so the caller's own user-scope
424
- // values never resolve into another tenant's config; the db is tenant-mode
425
- // scoped to the target.
426
- const configFor = configFactory && dbSource
427
- ? (targetTenantId) => {
428
- if (targetTenantId === user.tenantId && config)
429
- return config;
430
- if (!isSystemIdentity(user) && !user.roles.includes("SystemAdmin")) {
431
- throw new AccessDeniedError({
432
- message: "ctx.configFor(otherTenant) requires the system role or SystemAdmin",
433
- details: { reason: "config_for_requires_system_admin" },
434
- });
435
- }
436
- const targetSystemUser = createSystemUser(targetTenantId);
437
- return configFactory({
438
- user: { id: targetSystemUser.id, tenantId: targetTenantId },
439
- db: createTenantDb({
440
- db: dbSource,
441
- tenantId: targetTenantId,
442
- mode: "tenant",
443
- tracer: context.tracer,
444
- meter: context.meter,
445
- grants: { projectionRegistry: registry },
446
- }),
447
- secrets: context.secrets,
448
- });
449
- }
450
- : undefined;
451
- // Write handlers only, and not r.systemScope() ones (they have ctx.systemDb). The gate is the
452
- // dispatcher's tenantIdOverride predicate, so no caller reaches more than it could via
453
- // tenantIdOverride. The foreign db carries no unsafeRaw/globalWrites grant and is tenant mode
454
- // bound to the target; the executor accepts streamTenantId only on a db bound to that tenant.
455
- const forTenant = !isSystem && dbSource && registry.getWriteHandler(type) !== undefined
456
- ? (targetTenantId) => {
457
- if (targetTenantId === user.tenantId && exposedDb) {
458
- // Only perCommand handlers lose forTenant writes; reservations keep their pre-transaction contract.
459
- const ownTenantDb = denyDbWrites && !isPerCommand
460
- ? buildTenantScopedDb(dbSource, reqCtx?.signal, undefined, readOnlyForTenant)
461
- : exposedDb;
462
- return { db: ownTenantDb, streamTenantId: undefined };
463
- }
464
- if (!mayOverrideTenant(user)) {
465
- throw new AccessDeniedError({
466
- details: { reason: FrameworkReasons.tenantOverrideRequiresSystemAdmin },
467
- });
468
- }
469
- return {
470
- db: buildTenantScopedDb(dbSource, reqCtx?.signal, targetTenantId, denyDbWrites && (isPerCommand || readOnlyForTenant)),
471
- streamTenantId: targetTenantId,
472
- };
473
- }
474
- : undefined;
475
- // ctx.files resolved per-tenant through file-foundation (lazy — the
476
- // provider is only resolved when a handle actually does I/O). Boot wires
477
- // _fileProviderResolver when a file-provider plugin is mounted; falls back
478
- // to a statically-injected context.files (tests).
479
- const fileResolver = context._fileProviderResolver;
480
- const files = fileResolver ? createTenantFileContext(fileResolver, user.tenantId) : context.files;
481
- // ctx.derivatives builds on ctx.files (needs a FileContext to read the
482
- // original + write the variant) plus db (to look up the FileRef row) — so
483
- // it can only be constructed exactly when files+db both resolved; falls
484
- // back to a statically-injected context.derivatives otherwise (tests).
485
- const derivatives = files && db
486
- ? createDerivativesContext({ files, registry, db, tenantId: user.tenantId })
487
- : context.derivatives;
488
- // Observability — feature-bound metrics handle, so ctx.metrics.inc("foo")
489
- // resolves to kumiko_<feature>_foo. Unknown feature falls back to noop
490
- // so legacy internal handlers don't crash.
491
- const tracer = context.tracer ?? getFallbackTracer();
492
- const meter = context.meter;
493
- const metrics = meter && featureName ? createMetricsHandle(meter, featureName) : createNoopMetricsHandle();
494
- // ctx.metricsFor(featureName) — shared/library code binds to a feature
495
- // name of its own choosing instead of the dispatching handler's
496
- // (framework#1844). Unregistered names no-op rather than throw.
497
- const metricsFor = createMetricsForFactory(meter);
498
- // A failed inner write has already appended its event and projections when a later
499
- // step (e.g. an inTransaction hook) fails, and the outer handler may swallow the
500
- // failure result. The savepoint makes the failure undo exactly the inner write; the
501
- // inner afterCommit hooks only reach the outer sink if it succeeded.
502
- const runNestedWriteInSavepoint = async (asUser, targetType, payload) => {
503
- if (registry.getWriteHandler(targetType)?.transaction === "perCommand") {
504
- return writeFailure(perCommandNestedDenied(targetType));
505
- }
506
- if (!tx)
507
- return executeWrite(ctx, targetType, payload, asUser, origin, tx, bridgeSink);
508
- const innerAfterCommitHooks = [];
509
- try {
510
- const result = await runInSavepoint(tx, async (savepointTx) => {
511
- const innerResult = await executeWrite(ctx, targetType, payload, asUser, origin, savepointTx, // @cast-boundary db-runner savepoint handle is a transaction handle
512
- innerAfterCommitHooks);
513
- if (isFailedWriteResult(innerResult))
514
- throw new NestedWriteRollback(innerResult);
515
- return innerResult;
516
- });
517
- bridgeSink.push(...innerAfterCommitHooks);
518
- return result;
519
- }
520
- catch (e) {
521
- if (e instanceof NestedWriteRollback)
522
- return e.failure;
523
- throw e;
524
- }
525
- };
526
- // Savepoints on one connection are not serialized by the driver: two parallel nested writes would
527
- // interleave their statements and a ROLLBACK TO of the first would also discard the second's rows.
528
- // Writes nested one level deeper run on their own savepoint handle and therefore their own chain.
529
- let nestedWriteChain = Promise.resolve();
530
- const executeNestedWriteInSavepoint = (asUser, targetType, payload) => {
531
- const run = nestedWriteChain.then(() => runNestedWriteInSavepoint(asUser, targetType, payload));
532
- nestedWriteChain = run.catch(() => undefined);
533
- return run;
534
- };
535
- // Without an open transaction (post-commit hooks, stream/query handlers, perCommand handlers) a bare
536
- // executeWrite would auto-commit event append, projections and inTransaction hooks separately and its
537
- // afterCommit hooks would land in a sink nobody flushes. A batch-of-one gives the write its own
538
- // transaction and its own flush.
539
- const writeAsUser = async (asUser, targetType, payload) => transactionScope === "inTransaction"
540
- ? executeNestedWriteInSavepoint(asUser, targetType, payload)
541
- : unwrapSingle(await runBatch(ctx, [{ type: targetType, payload }], asUser, undefined, origin));
542
- const identitySwitchAudit = { reason: identitySwitchGrantReason, report: reportEscapeHatch };
543
- const identitySwitch = createGatedIdentitySwitch(`handler "${type}"`, user, hasIdentitySwitchGrant, {
544
- // Inherits the caller's origin, so switching to SYSTEM cannot shed an anonymous root.
545
- queryAs: (asUser, targetType, payload) => executeQuery(ctx, targetType, payload, asUser, origin, tx, hasLiveCommitSink ? bridgeSink : outerNotifySink), // @wrapper-known semantic-alias
546
- writeAs: writeAsUser,
547
- }, identitySwitchAudit);
548
- // Lazy — creates the reader (which fails closed on SYSTEM_TENANT_ID) only
549
- // on first actual use, not on every HandlerContext build.
550
- let ungatedMemberReader;
551
- const queryAsMember = createGatedMemberReader(`handler "${type}"`, hasIdentitySwitchGrant, (userId, qn, payload) => {
552
- ungatedMemberReader ??= createMemberReaderFn(ctx, user.tenantId, tx);
553
- return ungatedMemberReader(userId, qn, payload);
554
- }, { ...identitySwitchAudit, tenantId: user.tenantId });
555
- // queryProjection works against both single-stream and multi-stream
556
- // projections. MSPs without a table cannot be queried — those are
557
- // side-effect-only consumers (no state to read back).
558
- const findProjectionTable = (qualifiedName) => registry.getAllProjections().get(qualifiedName)?.table ??
559
- registry.getAllMultiStreamProjections().get(qualifiedName)?.table;
560
- const ungatedQueryProjection = async (qualifiedName, queryOptions) => {
561
- const projTable = findProjectionTable(qualifiedName);
562
- if (!projTable) {
563
- const singleNames = [...registry.getAllProjections().keys()];
564
- const mspNames = [...registry.getAllMultiStreamProjections().keys()].filter((n) => registry.getAllMultiStreamProjections().get(n)?.table);
565
- const all = [...singleNames, ...mspNames];
566
- throw new InternalError({
567
- message: `ctx.queryProjection("${qualifiedName}") — projection not registered, or it is a ` +
568
- `table-less MSP (side-effect-only). Known queryable projections: ${all.join(", ") || "(none)"}`,
569
- });
570
- }
571
- const dbSource = resolveDbSource(ctx, tx);
572
- if (!dbSource) {
573
- throw new InternalError({
574
- message: `ctx.queryProjection("${qualifiedName}") requires a database connection — none is configured.`,
575
- });
576
- }
577
- const where = hasTenantColumn(projTable) && queryOptions?.unsafeAllTenants !== true
578
- ? { tenantId: user.tenantId }
579
- : undefined;
580
- const rows = await selectMany(dbSource, projTable, where);
581
- return rows; // @cast-boundary engine-payload
582
- };
583
- const queryProjection = createGatedProjectionReader(`handler "${type}"`, hasIdentitySwitchGrant, ungatedQueryProjection, identitySwitchAudit, (qualifiedName) => {
584
- const projTable = findProjectionTable(qualifiedName);
585
- // Unknown projection: keep the grant check first, the ungated reader reports the real error.
586
- return projTable === undefined || hasTenantColumn(projTable);
587
- });
588
- const bridge = {
589
- query: (targetType, payload) => executeQuery(ctx, targetType, payload, user, origin, tx, hasLiveCommitSink ? bridgeSink : outerNotifySink), // @wrapper-known semantic-alias
590
- queryAs: identitySwitch.queryAs,
591
- write: (targetType, payload) => writeAsUser(user, targetType, payload),
592
- writeAs: identitySwitch.writeAs,
593
- // Strict + unsafe share the same runtime — only the type-surface
594
- // differs. The strict signature is what's exposed to typed callers;
595
- // unsafe is the explicit escape-hatch for runtime-pluggable events.
596
- appendEvent: (async (args) => {
597
- await appendDomainEvent(ctx, args, user, tx, registry.getHandlerFeature(type));
598
- }), // @cast-boundary engine-bridge
599
- unsafeAppendEvent: async (args) => {
600
- await appendDomainEvent(ctx, args, user, tx, registry.getHandlerFeature(type));
601
- },
602
- // Savepoint-scoped append: catches a losing writer's VersionConflictError
603
- // without poisoning the rest of the handler's transaction. postgres.js
604
- // aborts the WHOLE begin() on an uncaught statement error
605
- // (SQLSTATE 25P02) even if the JS error is caught — runInSavepoint
606
- // wraps the append in a real SAVEPOINT so only that nested scope rolls
607
- // back on conflict, and subsequent statements in the outer tx (e.g. a
608
- // dedup-anchor insert) still succeed. Use when a handler must react to
609
- // losing a concurrent-append race instead of failing the whole write.
610
- tryAppendEvent: async (args) => {
611
- if (!tx) {
612
- throw new InternalError({
613
- message: `ctx.tryAppendEvent("${args.type}") requires an active transaction — no tx is threaded through this call.`,
614
- });
615
- }
616
- try {
617
- const event = await runInSavepoint(tx, (sp) => appendDomainEventCore({
618
- registry,
619
- db: sp,
620
- tenantId: user.tenantId,
621
- userId: String(user.id),
622
- callSiteLabel: "ctx.tryAppendEvent",
623
- callerFeature: registry.getHandlerFeature(type),
624
- }, args));
625
- return { ok: true, event };
626
- }
627
- catch (e) {
628
- if (e instanceof EventStoreVersionConflictError) {
629
- return { ok: false, conflict: e };
630
- }
631
- if (e instanceof EventStoreIdempotentAppendConflictError) {
632
- return { ok: false, conflict: e };
633
- }
634
- throw e;
635
- }
636
- },
637
- fetchForWriting: async (args) => {
638
- const dbSource = resolveDbSource(ctx, tx);
639
- if (!dbSource) {
640
- throw new InternalError({
641
- message: `ctx.fetchForWriting("${args.aggregateId}") requires a database connection — none is configured.`,
642
- });
643
- }
644
- // Stream-version authoritative (same policy as CRUD executor + Block 0), read together
645
- // with the events so the pinned version covers exactly what the handler's rules saw.
646
- const { events: storedEvents, streamVersion: fetchedVersion } = await loadAggregateForWriting(dbSource, args.aggregateId, user.tenantId, { aggregateType: args.aggregateType });
647
- const events = await upcastStoredEvents(storedEvents, registry.getEventUpcasters(), {
648
- db: dbSource,
649
- tenantId: user.tenantId,
650
- });
651
- // Optimistic concurrency: if the caller knows the version they
652
- // worked against (e.g. from a prior read-model row) and the stream
653
- // has moved on, fail fast before any downstream work.
654
- if (args.expectedVersion !== undefined && args.expectedVersion !== fetchedVersion) {
655
- throw new VersionConflictError({
656
- entityId: args.aggregateId,
657
- expectedVersion: args.expectedVersion,
658
- currentVersion: fetchedVersion,
659
- });
660
- }
661
- // Pinned into every append: a write landing between fetch and append
662
- // fails with VersionConflictError instead of stacking on a state the
663
- // caller's business rules never saw. Bumps per appendOne so consecutive
664
- // appends stay in order without re-reading the DB.
665
- let handleVersion = fetchedVersion;
666
- const appendOne = async (appendArgs) => {
667
- await appendDomainEvent(ctx, {
668
- aggregateId: args.aggregateId,
669
- aggregateType: args.aggregateType,
670
- type: appendArgs.type,
671
- payload: appendArgs.payload,
672
- expectedVersion: handleVersion,
673
- }, user, tx, registry.getHandlerFeature(type));
674
- handleVersion += 1;
675
- };
676
- return {
677
- events,
678
- get version() {
679
- return handleVersion;
680
- },
681
- appendOne,
682
- hasEvent: (type) => events.some((e) => e.type === type),
683
- };
684
- },
685
- loadAggregate: async (aggregateId, loadOptions) => {
686
- const dbSource = resolveDbSource(ctx, tx);
687
- if (!dbSource) {
688
- throw new InternalError({
689
- message: `ctx.loadAggregate("${aggregateId}") requires a database connection — none is configured.`,
690
- });
691
- }
692
- const events = loadOptions?.asOf
693
- ? await loadAggregateAsOf(dbSource, aggregateId, user.tenantId, loadOptions.asOf)
694
- : await loadAggregate(dbSource, aggregateId, user.tenantId);
695
- return upcastStoredEvents(events, registry.getEventUpcasters(), {
696
- db: dbSource,
697
- tenantId: user.tenantId,
698
- });
699
- },
700
- archiveStream: async (aggregateId, archiveArgs) => {
701
- const dbSource = resolveDbSource(ctx, tx);
702
- if (!dbSource) {
703
- throw new InternalError({
704
- message: `ctx.archiveStream("${aggregateId}") requires a database connection — none is configured.`,
705
- });
706
- }
707
- await archiveStreamHelper(dbSource, {
708
- tenantId: user.tenantId,
709
- aggregateId,
710
- aggregateType: archiveArgs.aggregateType,
711
- archivedBy: user.id,
712
- reason: archiveArgs.reason,
713
- });
714
- },
715
- restoreStream: async (aggregateId) => {
716
- const dbSource = resolveDbSource(ctx, tx);
717
- if (!dbSource) {
718
- throw new InternalError({
719
- message: `ctx.restoreStream("${aggregateId}") requires a database connection — none is configured.`,
720
- });
721
- }
722
- await restoreStreamHelper(dbSource, user.tenantId, aggregateId);
723
- },
724
- isStreamArchived: async (aggregateId) => {
725
- const dbSource = resolveDbSource(ctx, tx);
726
- if (!dbSource) {
727
- throw new InternalError({
728
- message: `ctx.isStreamArchived("${aggregateId}") requires a database connection — none is configured.`,
729
- });
730
- }
731
- return isStreamArchived(dbSource, user.tenantId, aggregateId);
732
- },
733
- snapshotAggregate: async (snapshotArgs) => {
734
- const dbSource = resolveDbSource(ctx, tx);
735
- if (!dbSource) {
736
- throw new InternalError({
737
- message: `ctx.snapshotAggregate("${snapshotArgs.aggregateId}") requires a database connection — none is configured.`,
738
- });
739
- }
740
- await saveSnapshot(dbSource, {
741
- aggregateId: snapshotArgs.aggregateId,
742
- tenantId: user.tenantId,
743
- aggregateType: snapshotArgs.aggregateType,
744
- version: snapshotArgs.version,
745
- state: snapshotArgs.state,
746
- snapshotVersion: effectiveSnapshotGeneration(snapshotArgs.snapshotVersion ?? 1, registry.getEntity(snapshotArgs.aggregateType)?.eventVersion),
747
- });
748
- },
749
- loadAggregateWithSnapshot: async (aggregateId, reducer, initial, loadOptions) => {
750
- const dbSource = resolveDbSource(ctx, tx);
751
- if (!dbSource) {
752
- throw new InternalError({
753
- message: `ctx.loadAggregateWithSnapshot("${aggregateId}") requires a database connection — none is configured.`,
754
- });
755
- }
756
- // Upcaster-aware: pass an upcastEvent callback so loadAggregateWithSnapshot
757
- // walks every delta through the registered chain before invoking the
758
- // user's (sync) reducer. Async upcasters (DB-enrichment) are awaited
759
- // inside loadAggregateWithSnapshot — feature authors never see legacy
760
- // payload shapes regardless of which load path they chose.
761
- const upcasters = registry.getEventUpcasters();
762
- const upcastCtx = { db: dbSource, tenantId: user.tenantId };
763
- return loadAggregateWithSnapshot(dbSource, aggregateId, user.tenantId, reducer, initial, {
764
- ...loadOptions,
765
- entityEventVersionOf: (aggregateType) => registry.getEntity(aggregateType)?.eventVersion,
766
- upcastEvent: (event) => upcastStoredEvent(event, upcasters, upcastCtx), // @wrapper-known semantic-alias
767
- });
768
- },
769
- queryProjection,
770
- // Thin pass-through: one resolve impl lives on the dispatcher, the
771
- // handler surface just forwards the call so both entry points (login
772
- // handler via ctx.resolveAuthClaims, switch-tenant route via
773
- // dispatcher.resolveAuthClaims) cannot drift.
774
- resolveAuthClaims: (claimsUser) => resolveAuthClaimsFn(ctx, claimsUser), // @wrapper-known semantic-alias
775
- // Thin pass-through, same reasoning as resolveAuthClaims above — one
776
- // resolve impl lives on the dispatcher so callers can't drift apart.
777
- // Internally queries memberships as SYSTEM, so it needs the same grant
778
- // as a SYSTEM queryAs/writeAs (r.systemScope() or escapeHatch).
779
- resolveActiveMembership: (userId, tenantId) => {
780
- if (!hasIdentitySwitchGrant)
781
- throw systemIdentitySwitchDenied(`handler "${type}"`);
782
- return resolveActiveMembershipFn(ctx, userId, tenantId, INTERACTIVE_SIGN_IN_POLICY); // @wrapper-known semantic-alias
783
- },
784
- // Needs the same grant as a SYSTEM queryAs.
785
- queryAsMember,
786
- // Feature-effective check for in-handler opt-in logic. Scope:
787
- // **current user's tenant** — for cross-tenant lookups (rare,
788
- // SysAdmin operations) read effectiveFeatures(otherTenantId) directly.
789
- // When the feature-toggles or tier-engine feature isn't wired (no
790
- // effectiveFeatures callback), always returns true — apps without
791
- // tier-cuts treat all features on.
792
- //
793
- // Falls back to the live trialGate when the sync set says the feature
794
- // is off — the sync set never contains trial-tier features (time-
795
- // derived, can't boot-cache), so without this a trial tenant checking
796
- // a companion feature's toggle would silently read `false` even though
797
- // the dispatch gate already lets trial-tier handlers run.
798
- hasFeature: async (featureName) => {
799
- if (!effectiveFeatures)
800
- return true;
801
- if (effectiveFeatures(user.tenantId).has(featureName))
802
- return true;
803
- if (!effectiveFeatures.trialGate)
804
- return false;
805
- return effectiveFeatures.trialGate(user.tenantId, featureName);
806
- },
807
- };
808
- // Registry is always the dispatcher's registry — injecting it here lets
809
- // tests/callers pass `context` without `registry` and still get a valid
810
- // HandlerContext. The spread-then-assign order matters: anything in
811
- // `context` can be overridden, but we want the authoritative registry
812
- // from the dispatcher's own closure to win.
813
- // ctx.tz is always present. tenant reads tenant:config:timezone through
814
- // the already-built `config` accessor (raw qualified-key — no import of
815
- // the tenant feature, framework/pipeline stays bundled-features-free);
816
- // falls back to "UTC" when config is missing or the key is unset. user
817
- // comes from SessionUser.timezone (set at login), else falls back to
818
- // tenant (createTzContext's own default). An app-injected GeoTzProvider
819
- // (context.geoTzProvider) feeds ctx.tz.fromCoordinates / fromAddress.
820
- //
821
- // Resolving this via `config()` is a config-table SELECT on every single
822
- // dispatch (fw#2462) — ctx.tenantTimezoneCache memoizes it per tenant.
823
- // dispatch-write.ts invalidates on config:write:set/reset for this key.
824
- // Only populate from a tx-free read (tx === undefined): a write-path
825
- // lookup runs inside the caller's own open transaction, and caching a
826
- // value read there could memoize state from a write earlier in that same
827
- // transaction before it's known to commit. Reads always consult the
828
- // cache regardless of tx, since serving a slightly stale cached value on
829
- // the write path is harmless (bounded by ttlMs) and avoids reintroducing
830
- // the SELECT on the hot path this fix targets.
831
- let tenantTz;
832
- if (config === undefined) {
833
- tenantTz = undefined;
834
- }
835
- else {
836
- const cachedTz = ctx.tenantTimezoneCache.get(user.tenantId);
837
- if (cachedTz) {
838
- tenantTz = cachedTz.value;
839
- }
840
- else {
841
- const generationAtRead = ctx.tenantTimezoneCache.generation();
842
- tenantTz = await config(TENANT_TIMEZONE_CONFIG_KEY);
843
- if (tx === undefined) {
844
- ctx.tenantTimezoneCache.setIfFresh(user.tenantId, typeof tenantTz === "string" ? tenantTz : undefined, generationAtRead);
845
- }
846
- }
847
- }
848
- // Guarded against garbage: an unvalidated string here (free-form config
849
- // key, legacy JWT claim predating validation) blows up every ctx.tz call
850
- // for the whole tenant with a RangeError. Fall back to UTC/tenant instead
851
- // of trusting the raw value.
852
- const safeTenantTz = typeof tenantTz === "string" && isValidIanaTimeZone(tenantTz) ? tenantTz : "UTC";
853
- const safeUserTz = user.timezone !== undefined && isValidIanaTimeZone(user.timezone) ? user.timezone : undefined;
854
- const tz = createTzContext({
855
- ...(context.geoTzProvider !== undefined ? { geoTz: context.geoTzProvider } : {}),
856
- ...(context.clock !== undefined ? { clock: context.clock } : {}),
857
- tenant: safeTenantTz,
858
- ...(safeUserTz !== undefined && { user: safeUserTz }),
859
- });
860
- // ctx.locale — request-layer signal (X-Locale header → Accept-Language,
861
- // resolved once at the HTTP boundary by request-id-middleware.ts) wins;
862
- // then SessionUser.locale (set at login, see fw#2333 — stale until
863
- // re-login, same tradeoff as ctx.tz.user); falls back to the app's
864
- // boot-configured defaultLocale, then DEFAULT_LOCALE. Mirrors ctx.tz's
865
- // Request → Session → Boot-Default chain above.
866
- const safeUserLocale = user.locale !== undefined && isValidLocaleTag(user.locale) ? user.locale : undefined;
867
- const locale = reqCtx?.locale ?? safeUserLocale ?? context.defaultLocale ?? DEFAULT_LOCALE;
215
+ export async function buildHandlerContext(ctx, type, user, origin, options = {}) {
216
+ const { tx, afterCommitHooks, includeDeleted, outerNotifySink, readOnlyForTenant = false, transactionScope = tx ? "inTransaction" : "withoutTransaction", } = options;
217
+ const scope = resolveHandlerContextScope({ ctx, type, user, origin, tx, transactionScope });
218
+ const dbFacet = buildHandlerDbFacet(scope);
219
+ const observability = buildObservabilityFacet(scope);
220
+ const sinks = createCommitSinks(afterCommitHooks, transactionScope, outerNotifySink);
221
+ const notifyFacet = buildNotifyFacet(scope, sinks, dbFacet.outsideTxSource);
222
+ const configFacet = buildConfigFacet(scope, dbFacet.db);
223
+ const forTenantFragment = buildForTenantFragment(scope, dbFacet.exposedDb, readOnlyForTenant);
224
+ const fileFragment = buildFileFragment(scope, dbFacet.db);
225
+ const dispatchFacet = buildDispatchFacet(scope, sinks);
226
+ const authFacet = buildAuthFacet(scope);
227
+ const eventStoreFacet = buildEventStoreFacet(scope);
228
+ const tz = await resolveHandlerTz(scope, configFacet.config);
229
+ const locale = resolveHandlerLocale(scope);
868
230
  // The payload-bucket digest is dispatcher-internal; handlers must not be
869
231
  // able to compute bucket keys.
870
232
  const { _rateLimitPayloadDigest: payloadDigest,
871
233
  // Bound to this call's tx: an outer context's factory must not leak into a tx-less one.
872
- _notifyInTransactionFactory: _omitOuterNotifyInTransactionFactory, _notifyAfterCommitFactory: _omitOuterNotifyAfterCommitFactory, ...contextForHandler } = context;
234
+ _notifyInTransactionFactory: _omitOuterNotifyInTransactionFactory, _notifyAfterCommitFactory: _omitOuterNotifyAfterCommitFactory, ...contextForHandler } = scope.context;
873
235
  const handlerContext = {
874
236
  ...contextForHandler,
875
237
  ...(payloadDigest && { rateLimitDigest: createHandlerBoundDigest(payloadDigest, type) }),
876
- ...(notifyInTransactionFactory && { _notifyInTransactionFactory: notifyInTransactionFactory }),
877
- ...(notifyAfterCommitFactory && { _notifyAfterCommitFactory: notifyAfterCommitFactory }),
878
- registry,
879
- db: exposedDb,
880
- dbOutsideTransaction,
881
- ...(systemDb && { systemDb }),
882
- ...(crossTenantReads && { crossTenantReads }),
883
- log,
884
- notify,
885
- ...(config && { config }),
886
- ...(configFor && { configFor }),
887
- ...(forTenant && { forTenant }),
888
- ...(files && { files }),
889
- ...(derivatives && { derivatives }),
890
- // preSave hooks need `changes`/`previous`/`isNew`, which only exist once
891
- // a handler actually starts building its write — bound here so entity
892
- // CRUD handlers (entity-handlers.ts) can forward it to the executor
893
- // (kumiko-framework#1672).
894
- ...(lifecycle && {
895
- runPreSave: (handlerName, changes, previous, isNew) => lifecycle.runPreSave(handlerName, changes, previous, isNew, context),
896
- }),
897
- tracer,
898
- metrics,
899
- metricsFor,
238
+ ...notifyFacet.factoryFragment,
239
+ // Registry is always the dispatcher's registry — injecting it here lets
240
+ // tests/callers pass `context` without `registry` and still get a valid
241
+ // HandlerContext. The spread-then-assign order matters: anything in
242
+ // `context` can be overridden, but we want the authoritative registry
243
+ // from the dispatcher's own closure to win.
244
+ registry: scope.registry,
245
+ ...dbFacet.fragment,
246
+ log: observability.log,
247
+ notify: notifyFacet.notify,
248
+ ...configFacet.fragment,
249
+ ...forTenantFragment,
250
+ ...fileFragment,
251
+ ...buildPreSaveFragment(scope),
252
+ tracer: observability.tracer,
253
+ metrics: observability.metrics,
254
+ metricsFor: observability.metricsFor,
900
255
  tz,
901
256
  locale,
902
- // Cancellation signal flows from the HTTP middleware via
903
- // requestContext. Conditional spread so non-HTTP entry-points
904
- // (jobs, dispatcher MSP-applies) don't get a phantom signal that
905
- // would always read aborted=false but feel meaningful.
906
- ...(reqCtx?.signal ? { signal: reqCtx.signal } : {}),
907
- // Propagate the feature-toggle resolver so the lifecycle pipeline,
908
- // MSP runner, and ctx.hasFeature all pull from the same source.
909
- ...(effectiveFeatures && { effectiveFeatures }),
910
- ...(ctx.cacheSync && { cacheSync: ctx.cacheSync }),
911
- // Lets write handlers call ctx.jobRunner.dispatch(...) directly, same
912
- // as a follow-up job would (test-stack.ts wires the matching runner).
913
- ...(jobRunner && { jobRunner }),
914
- // ctx.user als Convenience-Alias auf event.user. Der typisch-
915
- // intuitive Pfad „der Context kennt seinen User" — ohne den
916
- // schreiben Handler `event.user.tenantId` und brechen sich die
917
- // Finger an typo-resistenten ctx.user-Patterns. Identisch zum
918
- // event.user-Wert; Identity-Switches nutzen weiterhin queryAs/writeAs.
257
+ ...buildRuntimeFragment(scope),
258
+ // ctx.user as a convenience alias for event.user. The intuitive path—"the
259
+ // context knows its user"—lets handlers avoid writing `event.user.tenantId`
260
+ // and reduces errors in ctx.user access patterns. It is identical to
261
+ // event.user; identity switches still use queryAs/writeAs.
919
262
  user,
920
263
  _tenantId: user.tenantId,
921
- ...(hasLiveCommitSink && { scheduleAfterCommit }),
264
+ ...dispatchFacet.scheduleFragment,
922
265
  ...(includeDeleted && { includeDeleted: true }),
923
- ...bridge,
266
+ ...dispatchFacet.bridge,
267
+ ...eventStoreFacet,
268
+ ...authFacet,
924
269
  }; // @cast-boundary engine-bridge
925
270
  const scopedContext = transactionScope === "withoutTransaction"
926
271
  ? denyEventStoreSurfaces(handlerContext, withoutTransactionDenied)
@@ -970,7 +315,7 @@ export async function runHandlerInstrumented(ctx, type, operation, user, inner)
970
315
  attributes: dispatcherSpanAttributes(type, operation, user, registry.getHandlerFeature(type)),
971
316
  }, async (span) => {
972
317
  try {
973
- // #3043 — everything the handler writes, including entity-executor
318
+ // Everything the handler writes, including entity-executor
974
319
  // writes below it, is attributed to this handler.
975
320
  const result = await runWithOrigin({
976
321
  handler: type,
@@ -1160,7 +505,7 @@ async function enforceBuckets(resolver, requests) {
1160
505
  // pass through. ip-modes are commonly used at L1/L2 middleware
1161
506
  // where the IP comes from Hono directly; falling back to "skip"
1162
507
  // here keeps non-HTTP entry-points (jobs, MSPs) functional.
1163
- export async function enforceRateLimit(ctx, rateLimit, handlerName, user, isSystemScope) {
508
+ export async function enforceRateLimit(ctx, rateLimit, handlerName, user, isSystemScope, costOverride) {
1164
509
  const { appContext: context } = ctx;
1165
510
  // skip: handler explicitly opts out via rateLimit: { disabled: true }
1166
511
  if (isRateLimitDisabled(rateLimit))
@@ -1183,14 +528,15 @@ export async function enforceRateLimit(ctx, rateLimit, handlerName, user, isSyst
1183
528
  // requiring a resolver; HTTP path always has an IP + L1/L2 middleware.
1184
529
  if (bucket.kind === "skip")
1185
530
  continue;
1186
- const chargedKey = `${bucket.key}|${option.limit}|${option.windowSeconds}|${option.cost}`;
531
+ const cost = costOverride ?? option.cost;
532
+ const chargedKey = `${bucket.key}|${option.limit}|${option.windowSeconds}|${cost}`;
1187
533
  // skip: this exact bucket, limit and window was already charged by the entry dispatch
1188
534
  if (charged?.has(chargedKey))
1189
535
  continue;
1190
536
  pending.push({
1191
537
  request: {
1192
538
  bucket: bucket.key,
1193
- config: { limit: option.limit, windowSeconds: option.windowSeconds, cost: option.cost },
539
+ config: { limit: option.limit, windowSeconds: option.windowSeconds, cost },
1194
540
  },
1195
541
  chargedKey,
1196
542
  });