@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
@@ -10,7 +10,8 @@ import { bindProjectionRegistry, projectionRegistryOf } from "./projection-regis
10
10
  import { acquireNamespacedAdvisoryLock } from "./queries/advisory-lock.js";
11
11
  import { queryDatabaseNow } from "./queries/database-now.js";
12
12
  import { hasFieldProperty, tableNameOf } from "./table-introspection.js";
13
- import { bindTenantDbRunner, denyTenantDbWrites, tenantDbRunner, tenantDbWritesDeniedError, } from "./tenant-db-runner.js";
13
+ import { bindTenantDbRunner, denyTenantDbWrites, tenantDbRunner, tenantDbWritesDeniedError, tenantDbWritesDeniedReason, } from "./tenant-db-runner.js";
14
+ import { upsertTableDenied, upsertTenantRow } from "./tenant-db-upsert.js";
14
15
  export { SYSTEM_SCOPE_CHECK_BRAND, } from "@cosmicdrift/kumiko-types/tenant-db-types";
15
16
  const declaredUnsafeRawRunners = new WeakMap();
16
17
  // The CRUD executor writes through tenantDbRunner, not insertOne, so it asks the
@@ -36,7 +37,7 @@ function boundRunner(runner, gate, projectionRegistry, executorManagedWrites) {
36
37
  if (typeof value !== "function")
37
38
  return value;
38
39
  if (prop === "reserve") {
39
- // postgres-js hands back a raw connection; re-wrap it so the gate survives.
40
+ // The driver hands back a raw connection; re-wrap it so the gate survives.
40
41
  return async (...args) => {
41
42
  const reserved = await Reflect.apply(value, target, args);
42
43
  return boundRunner(reserved, gate, projectionRegistry, executorManagedWrites);
@@ -105,9 +106,7 @@ export function assertPersonalDataWrite(db, tableName, keys, entity) {
105
106
  }
106
107
  // Framework-private (not re-exported from db/index.ts): same grant check + audit as unsafeRaw, for engine forwarding.
107
108
  export function unsafeRawForDeclaredStep(holder, reason) {
108
- if (reason.trim().length === 0) {
109
- throw new Error("unsafeRawForDeclaredStep requires a non-empty reason");
110
- }
109
+ assertReason("unsafeRawForDeclaredStep", reason);
111
110
  const runner = declaredUnsafeRawRunners.get(holder);
112
111
  if (!runner) {
113
112
  throw new InternalError({
@@ -257,7 +256,13 @@ export function createSystemDbView(db, dbOutsideTransaction, report = fallbackEs
257
256
  // same connection/transaction. For code that must run caller-supplied callbacks (e.g. config
258
257
  // validators) without handing them a cross-tenant view.
259
258
  export function scopeTenantDbToTenant(db, tenantId) {
260
- return createTenantDb({ db: tenantDbRunner(db), tenantId, mode: "tenant" });
259
+ const writesDeniedReason = tenantDbWritesDeniedReason(db);
260
+ return createTenantDb({
261
+ db: tenantDbRunner(db),
262
+ tenantId,
263
+ mode: "tenant",
264
+ ...(writesDeniedReason !== undefined && { grants: { writesDeniedReason } }),
265
+ });
261
266
  }
262
267
  // @cast-boundary tenant-db-row
263
268
  export function castTenantRows(rows) {
@@ -268,6 +273,18 @@ export { tableNameOf };
268
273
  function hasGrant(decl) {
269
274
  return decl !== undefined && decl.reason.trim().length > 0;
270
275
  }
276
+ // Tenant ids left over when the caller excludes some via ne/notIn; undefined for other operator forms.
277
+ function tenantIdsAfterExclusion(requested, allowed) {
278
+ if (!isPlainObject(requested))
279
+ return undefined;
280
+ if ("ne" in requested)
281
+ return allowed.filter((t) => t !== requested["ne"]);
282
+ if (Array.isArray(requested["notIn"])) {
283
+ const excluded = requested["notIn"];
284
+ return allowed.filter((t) => !excluded.includes(t));
285
+ }
286
+ return undefined;
287
+ }
271
288
  // Tenant ids a where.tenantId filter asks for; undefined for operator forms
272
289
  // that cannot narrow to a tenant list (ne, like, ...).
273
290
  export function requestedTenantIds(requested) {
@@ -429,7 +446,13 @@ function buildTenantDb(options) {
429
446
  const allowed = [tenantId, SYSTEM_TENANT_ID];
430
447
  const requested = where?.["tenantId"];
431
448
  if (requested !== undefined) {
432
- const requestedList = requestedTenantIds(requested) ?? [];
449
+ const requestedList = requestedTenantIds(requested);
450
+ // An explicit exclusion or empty list must keep excluding, never widen to the full scope.
451
+ if (requestedList === undefined) {
452
+ return { ...where, tenantId: tenantIdsAfterExclusion(requested, allowed) ?? allowed };
453
+ }
454
+ if (requestedList.length === 0)
455
+ return { ...where, tenantId: [] };
433
456
  const narrowed = requestedList.filter((t) => typeof t === "string" && allowed.includes(t));
434
457
  return { ...where, tenantId: narrowed.length > 0 ? narrowed : allowed };
435
458
  }
@@ -464,6 +487,14 @@ function buildTenantDb(options) {
464
487
  function globalWriteReason() {
465
488
  return grants?.globalWrites?.reason ?? "";
466
489
  }
490
+ // The table is the audit target: process-level dedup would otherwise collapse a cron's
491
+ // writes to several global tables into one audit entry.
492
+ function reportGlobalWrite(table) {
493
+ report("global-write", globalWriteReason(), {
494
+ id: tableNameOf(table),
495
+ tenantId: tenantId ?? SYSTEM_TENANT_ID,
496
+ });
497
+ }
467
498
  function personalDataDenied(table, keys) {
468
499
  if (!personalDataGate)
469
500
  return undefined;
@@ -535,7 +566,7 @@ function buildTenantDb(options) {
535
566
  personalDataDenied(table, Object.keys(values));
536
567
  if (denied)
537
568
  return Promise.reject(denied);
538
- report("global-write", globalWriteReason());
569
+ reportGlobalWrite(table);
539
570
  return withDbSpan("insert", table, async () => insertRow(table, values));
540
571
  },
541
572
  updateMany(set, where) {
@@ -549,7 +580,7 @@ function buildTenantDb(options) {
549
580
  if (!where || Object.keys(where).length === 0) {
550
581
  return Promise.reject(new Error("db.global().updateMany without where would mass-update every tenant's rows. Pass at least one where condition."));
551
582
  }
552
- report("global-write", globalWriteReason());
583
+ reportGlobalWrite(table);
553
584
  return withDbSpan("update", table, async () => updateRows(table, set, where));
554
585
  },
555
586
  deleteMany(where) {
@@ -561,7 +592,7 @@ function buildTenantDb(options) {
561
592
  if (!where || Object.keys(where).length === 0) {
562
593
  return Promise.reject(new Error("db.global().deleteMany without where would mass-delete every tenant's rows. Pass at least one where condition."));
563
594
  }
564
- report("global-write", globalWriteReason());
595
+ reportGlobalWrite(table);
565
596
  return withDbSpan("delete", table, async () => {
566
597
  await deleteRows(table, where);
567
598
  });
@@ -632,6 +663,15 @@ function buildTenantDb(options) {
632
663
  const data = insertValues(table, values);
633
664
  return withDbSpan("insert", table, async () => insertRow(table, data));
634
665
  },
666
+ upsert(table, values, options) {
667
+ const denied = writesDenied("ctx.db.upsert") ??
668
+ upsertTableDenied(table, mode) ??
669
+ personalDataDenied(table, [...Object.keys(values), ...Object.keys(options.update ?? {})]);
670
+ if (denied)
671
+ return Promise.reject(denied);
672
+ const data = insertValues(table, values);
673
+ return withDbSpan("insert", table, () => upsertTenantRow(db, table, data, options));
674
+ },
635
675
  updateMany(table, set, where) {
636
676
  const writeDenied = writesDenied("ctx.db.updateMany");
637
677
  if (writeDenied)
@@ -0,0 +1,6 @@
1
+ import type { WhereObject } from "@cosmicdrift/kumiko-types/where-clause-types";
2
+ import type { TableInfo } from "./table-info.ts";
3
+ export declare function buildWhereClause(info: TableInfo, where: WhereObject, startIndex: number, qualifier?: string): {
4
+ sqlText: string;
5
+ values: unknown[];
6
+ };
@@ -0,0 +1,236 @@
1
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
2
+ import { computeBlindIndex, configuredBlindIndexKey } from "../crypto/blind-index.js";
3
+ import { quoteIdent } from "../crypto/ciphertext-pattern.js";
4
+ import { prepareJsonbValue, prepareValue } from "./driver-value-preparation.js";
5
+ function isJsonTextMatchList(value) {
6
+ return Array.isArray(value);
7
+ }
8
+ function isWhereOperator(v) {
9
+ if (v === null || typeof v !== "object" || Array.isArray(v))
10
+ return false;
11
+ // Don't false-positive on Date / Temporal / other domain-objects.
12
+ // WhereOperator is plain object literal with at most these keys.
13
+ const keys = Object.keys(v);
14
+ if (keys.length === 0)
15
+ return false;
16
+ return keys.every((key) => Object.hasOwn(OPERATOR_EMITTERS, key));
17
+ }
18
+ function isJsonbScalar(value) {
19
+ return value !== null && typeof value !== "object";
20
+ }
21
+ function valueList(target, values) {
22
+ return values
23
+ .map((v) => {
24
+ const p = prepareValue(v, target.pgType);
25
+ return p.kind === "literal" ? p.literal : `${target.bind(p.bound)}${p.sql}`;
26
+ })
27
+ .join(", ");
28
+ }
29
+ // multiSelect (and any other jsonb-array-of-scalars column) stores an
30
+ // array — a filter value is one option, not the whole array, so eq/ne/in
31
+ // must check array containment (`@>`) instead of scalar `=`/`<>`/`IN`
32
+ // against the jsonb column, which Postgres rejects outright.
33
+ // Mirrors applyMultiSelectFilter in event-store-executor-read-filter.ts — keep in lock-step.
34
+ function jsonbContainsAny(target, candidates) {
35
+ const { column, ref, bind } = target;
36
+ if (candidates.length === 0)
37
+ return "FALSE";
38
+ const parts = [];
39
+ for (const v of candidates) {
40
+ const p = prepareJsonbValue([v]);
41
+ // prepareJsonbValue never returns kind:"literal" (only prepareValue's
42
+ // isSqlExpression branch does) — narrow anyway, PreparedValue is a union.
43
+ if (p && p.kind === "param") {
44
+ parts.push(`${ref(column)} @> ${bind(p.bound)}${p.sql}`);
45
+ }
46
+ // A jsonb column holding the scalar itself is never `@>` an array of it.
47
+ if (isJsonbScalar(v)) {
48
+ parts.push(`${ref(column)} = ${bind(JSON.stringify(v))}::text::jsonb`);
49
+ }
50
+ }
51
+ return parts.length > 0 ? `(${parts.join(" OR ")})` : "FALSE";
52
+ }
53
+ function isValidJsonTextMatch(match) {
54
+ if (match === null || typeof match !== "object")
55
+ return false;
56
+ const keys = propertyOf(match, "keys");
57
+ return (Array.isArray(keys) &&
58
+ keys.length > 0 &&
59
+ keys.every((k) => typeof k === "string") &&
60
+ typeof propertyOf(match, "eq") === "string");
61
+ }
62
+ function jsonTextMatchCondition(target, match) {
63
+ const { column, ref, bind } = target;
64
+ if (!isValidJsonTextMatch(match)) {
65
+ throw new Error(`where: jsonText on column "${column}" needs non-empty string keys and a string eq`);
66
+ }
67
+ const lookups = match.keys.map((key) => `${ref(column)}->>${bind(key)}::text`);
68
+ const lhs = lookups.length === 1 ? lookups[0] : `COALESCE(${lookups.join(", ")})`;
69
+ return `(${lhs} = ${bind(match.eq)})`;
70
+ }
71
+ function emitJsonText(target, operator) {
72
+ const { column, pgType } = target;
73
+ const operand = operator.jsonText;
74
+ if (operand === undefined)
75
+ return undefined;
76
+ if (Object.keys(operator).length > 1) {
77
+ throw new Error(`where: jsonText on column "${column}" cannot be combined with other operators`);
78
+ }
79
+ if (pgType !== "jsonb") {
80
+ throw new Error(`where: jsonText requires a jsonb column, "${column}" is ${pgType}`);
81
+ }
82
+ const matches = isJsonTextMatchList(operand) ? operand : [operand];
83
+ if (matches.length === 0)
84
+ return "FALSE";
85
+ return matches.map((match) => jsonTextMatchCondition(target, match)).join(" AND ");
86
+ }
87
+ function comparisonCondition(target, symbol, operand) {
88
+ const { column, pgType, ref, bind } = target;
89
+ const p = prepareValue(operand, pgType);
90
+ return p.kind === "literal"
91
+ ? `${ref(column)} ${symbol} ${p.literal}`
92
+ : `${ref(column)} ${symbol} ${bind(p.bound)}${p.sql}`;
93
+ }
94
+ function comparisonEmitter(key, symbol) {
95
+ return (target, operator) => {
96
+ const operand = operator[key];
97
+ if (operand === undefined)
98
+ return undefined;
99
+ // Mirrors applyMultiSelectFilter: ordering/pattern ops have no meaning on a jsonb
100
+ // option array (`like` would crash as `jsonb ~~ jsonb`), so match nothing.
101
+ if (target.pgType === "jsonb")
102
+ return "FALSE";
103
+ return comparisonCondition(target, symbol, operand);
104
+ };
105
+ }
106
+ // Mirrors applyMultiSelectFilter: an array `ne` means "does not contain all of these".
107
+ function jsonbNotEqualCondition(target, operand) {
108
+ const { column, ref, bind } = target;
109
+ if (Array.isArray(operand)) {
110
+ if (operand.length === 0)
111
+ return "FALSE";
112
+ const p = prepareJsonbValue(operand);
113
+ if (p && p.kind === "param") {
114
+ return `(${ref(column)} IS NULL OR NOT (${ref(column)} @> ${bind(p.bound)}${p.sql}))`;
115
+ }
116
+ }
117
+ if (isJsonbScalar(operand)) {
118
+ const p = prepareJsonbValue([operand]);
119
+ if (p && p.kind === "param") {
120
+ return `(${ref(column)} IS NULL OR NOT (${ref(column)} @> ${bind(p.bound)}${p.sql} OR ${ref(column)} = ${bind(JSON.stringify(operand))}::text::jsonb))`;
121
+ }
122
+ }
123
+ return undefined;
124
+ }
125
+ function emitNotEqual(target, operator) {
126
+ const operand = operator.ne;
127
+ if (operand === undefined)
128
+ return undefined;
129
+ if (operand === null && Object.keys(operator).length === 1) {
130
+ return `${target.ref(target.column)} IS NOT NULL`;
131
+ }
132
+ if (target.pgType === "jsonb") {
133
+ const jsonbCondition = jsonbNotEqualCondition(target, operand);
134
+ if (jsonbCondition !== undefined)
135
+ return jsonbCondition;
136
+ }
137
+ return comparisonCondition(target, "<>", operand);
138
+ }
139
+ function emitIn(target, operator) {
140
+ const operand = operator.in;
141
+ if (!Array.isArray(operand))
142
+ return undefined;
143
+ if (target.pgType === "jsonb")
144
+ return jsonbContainsAny(target, operand);
145
+ if (operand.length === 0)
146
+ return "FALSE";
147
+ return `${target.ref(target.column)} IN (${valueList(target, operand)})`;
148
+ }
149
+ function emitNotIn(target, operator) {
150
+ const operand = operator.notIn;
151
+ if (!Array.isArray(operand))
152
+ return undefined;
153
+ if (target.pgType === "jsonb") {
154
+ throw new Error(`where: notIn is not supported on jsonb column "${target.column}"`);
155
+ }
156
+ if (operand.length === 0)
157
+ return "TRUE";
158
+ return `${target.ref(target.column)} NOT IN (${valueList(target, operand)})`;
159
+ }
160
+ // Insertion order is the SQL emission order. jsonText comes first so its "cannot be combined" error wins over any other
161
+ // operator's error; the rest keep the historical order gt, gte, lt, lte, ne, like, in, notIn.
162
+ const OPERATOR_EMITTERS = {
163
+ jsonText: emitJsonText,
164
+ gt: comparisonEmitter("gt", ">"),
165
+ gte: comparisonEmitter("gte", ">="),
166
+ lt: comparisonEmitter("lt", "<"),
167
+ lte: comparisonEmitter("lte", "<="),
168
+ ne: emitNotEqual,
169
+ like: comparisonEmitter("like", "LIKE"),
170
+ in: emitIn,
171
+ notIn: emitNotIn,
172
+ };
173
+ function operatorConditions(target, operator) {
174
+ const conditions = [];
175
+ for (const emit of Object.values(OPERATOR_EMITTERS)) {
176
+ const condition = emit(target, operator);
177
+ if (condition !== undefined)
178
+ conditions.push(condition);
179
+ }
180
+ return conditions;
181
+ }
182
+ // Blind-index OR rewrite: if the column has a bidx
183
+ // counterpart (suffix convention `<col>_bidx`, framework-reserved)
184
+ // and a key is configured, equality matches both arms —
185
+ // plaintext legacy rows via the plaintext arm, encrypted rows
186
+ // via the HMAC. The `kumiko-pii:` prefix on ciphertexts makes
187
+ // a random match on the plaintext arm practically impossible.
188
+ function equalityCondition(target, value) {
189
+ const { info, column, pgType, ref, bind } = target;
190
+ const p = prepareValue(value, pgType);
191
+ if (p.kind === "literal")
192
+ return `${ref(column)} = ${p.literal}`;
193
+ const bidxKey = configuredBlindIndexKey();
194
+ const bidxCol = `${column}_bidx`;
195
+ if (bidxKey !== undefined &&
196
+ typeof value === "string" &&
197
+ !column.endsWith("_bidx") &&
198
+ info.hasColumn(bidxCol)) {
199
+ return `(${ref(column)} = ${bind(p.bound)} OR ${ref(bidxCol)} = ${bind(computeBlindIndex(bidxKey, value))})`;
200
+ }
201
+ return `${ref(column)} = ${bind(p.bound)}${p.sql}`;
202
+ }
203
+ function arrayCondition(target, values) {
204
+ if (values.length === 0)
205
+ return "FALSE";
206
+ if (target.pgType === "jsonb")
207
+ return jsonbContainsAny(target, values);
208
+ return `${target.ref(target.column)} IN (${valueList(target, values)})`;
209
+ }
210
+ function conditionsForValue(target, value) {
211
+ if (value === null)
212
+ return [`${target.ref(target.column)} IS NULL`];
213
+ if (Array.isArray(value))
214
+ return [arrayCondition(target, value)];
215
+ if (isWhereOperator(value))
216
+ return operatorConditions(target, value);
217
+ if (target.pgType === "jsonb" && isJsonbScalar(value)) {
218
+ return [jsonbContainsAny(target, [value])];
219
+ }
220
+ return [equalityCondition(target, value)];
221
+ }
222
+ export function buildWhereClause(info, where, startIndex, qualifier) {
223
+ const values = [];
224
+ // Single place that renders a column reference, so a join qualifier reaches
225
+ // every arm (blind index, jsonb containment, IS NULL, jsonText) alike.
226
+ const ref = (column) => qualifier === undefined ? quoteIdent(column) : `${quoteIdent(qualifier)}.${quoteIdent(column)}`;
227
+ const bind = (value) => {
228
+ values.push(value);
229
+ return `$${startIndex + values.length - 1}`;
230
+ };
231
+ const conditions = Object.entries(where).flatMap(([field, value]) => {
232
+ const column = info.columnOf(field);
233
+ return conditionsForValue({ info, column, pgType: info.pgTypeOf(column), ref, bind }, value);
234
+ });
235
+ return { sqlText: conditions.join(" AND "), values };
236
+ }
@@ -56,6 +56,7 @@ export async function deleteStoredFileAndDerivatives(originalKey, store, errorCo
56
56
  ...(await listDerivativeKeys(originalKey, store, errorContext)),
57
57
  ];
58
58
  const failedKeys = [];
59
+ const log = createFallbackLogger(errorContext);
59
60
  for (const key of keysToDelete) {
60
61
  try {
61
62
  assertSafeStorageKey(key);
@@ -63,7 +64,7 @@ export async function deleteStoredFileAndDerivatives(originalKey, store, errorCo
63
64
  }
64
65
  catch (err) {
65
66
  failedKeys.push(key);
66
- createFallbackLogger(errorContext).warn("storage delete failed", {
67
+ log.warn("storage delete failed", {
67
68
  key,
68
69
  error: err instanceof Error ? err.message : String(err),
69
70
  });
@@ -13,7 +13,7 @@ export function validateFeatureBootChecks(features) {
13
13
  }
14
14
  catch (err) {
15
15
  const message = err instanceof Error ? err.message : String(err);
16
- throw new Error(`[Feature ${feature.name}] r.bootCheck failed: ${message}. Fix the condition this check reports.`);
16
+ throw new Error(`[Feature ${feature.name}] r.bootCheck failed: ${message}`);
17
17
  }
18
18
  }
19
19
  }
@@ -0,0 +1,2 @@
1
+ export declare const BUNDLED_LISTS_OPTION_KEYS: readonly ["table", "pinFirstColumn"];
2
+ export declare function validateBundledListsOptions(options: unknown): void;
@@ -0,0 +1,36 @@
1
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
2
+ import { LIST_TABLE_OPTION_KEYS } from "../types/screen.js";
3
+ import { validateListTableOptions } from "./list-table-options.js";
4
+ export const BUNDLED_LISTS_OPTION_KEYS = ["table", "pinFirstColumn"];
5
+ // grouping and totals name fields of one list, so they cannot be shared across lists
6
+ const SCREEN_ONLY_TABLE_KEYS = ["grouping", "totals"];
7
+ export function validateBundledListsOptions(options) {
8
+ if (!isPlainObject(options)) {
9
+ throw new Error("bundledLists must be an object like { table: { columnPicker: true } }.");
10
+ }
11
+ const unknownKeys = Object.keys(options).filter((key) => !BUNDLED_LISTS_OPTION_KEYS.some((allowed) => allowed === key));
12
+ if (unknownKeys.length > 0) {
13
+ throw new Error(`bundledLists has unknown key(s) ${unknownKeys.map((k) => `"${k}"`).join(", ")}. ` +
14
+ `Allowed keys: ${BUNDLED_LISTS_OPTION_KEYS.join(", ")}.`);
15
+ }
16
+ const table = options["table"];
17
+ if (!isPlainObject(table)) {
18
+ const allowed = LIST_TABLE_OPTION_KEYS.filter((key) => !SCREEN_ONLY_TABLE_KEYS.some((screenOnly) => screenOnly === key));
19
+ throw new Error(`bundledLists.table must be an object like { columnPicker: true }. ` +
20
+ `Allowed keys: ${allowed.join(", ")}.`);
21
+ }
22
+ for (const key of SCREEN_ONLY_TABLE_KEYS) {
23
+ if (table[key] !== undefined) {
24
+ throw new Error(`bundledLists.table.${key} is not allowed: ${key} depends on the fields of one list. ` +
25
+ `Set it in the table block of that screen.`);
26
+ }
27
+ }
28
+ validateListTableOptions("bundledLists", table, [], true);
29
+ const pinFirstColumn = options["pinFirstColumn"];
30
+ if (pinFirstColumn !== undefined && typeof pinFirstColumn !== "boolean") {
31
+ throw new Error("bundledLists.pinFirstColumn must be a boolean.");
32
+ }
33
+ if (pinFirstColumn === true && table["pinning"] !== true) {
34
+ throw new Error("bundledLists.pinFirstColumn needs bundledLists.table.pinning: true.");
35
+ }
36
+ }
@@ -1,8 +1,8 @@
1
1
  import { qualifyEntityName } from "../qualified-name.js";
2
- import { findEntityFeature } from "./screens.js";
2
+ import { findEntityFeature } from "./screens-kind-shared.js";
3
3
  // Entity name → the one screen that declares detailFor: "<entity>". Built
4
4
  // once, up front (before the per-feature validateScreens loop) so both this
5
- // module's own uniqueness check AND rowAction entity-targets (in screens.ts)
5
+ // module's own uniqueness check AND rowAction entity-targets (in screens-actions.ts)
6
6
  // resolve against the same map instead of re-walking every feature's
7
7
  // screens twice.
8
8
  export function collectDetailForScreens(features, featureMap) {
@@ -9,6 +9,7 @@ export declare function validateHandlerAccess(feature: FeatureDefinition): void;
9
9
  export declare function validateAdditionalRateLimits(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
10
10
  export declare function validatePerCommandTransaction(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
11
11
  export declare function validateTenantlessAnonymous(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
12
+ export declare function validateRateLimitCost(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
12
13
  export declare function validateRateLimitDisabledReason(featureName: string, kind: "write" | "query" | "stream", handlerName: string, rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"]): void;
13
14
  export declare function validateRateLimitBucketList(featureName: string, kind: "write" | "query" | "stream", handlerName: string, rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"]): void;
14
15
  export declare function validateNoAllRoleInHandlerAccess(featureName: string, kind: "write" | "query" | "stream", handlerName: string, access: NonNullable<FeatureDefinition["writeHandlers"][string]["access"]>): void;
@@ -139,6 +139,7 @@ export function validateHandlerAccess(feature) {
139
139
  for (const [name, handler] of Object.entries(feature.writeHandlers)) {
140
140
  validateTenantlessAnonymous(feature.name, name, handler);
141
141
  validateAdditionalRateLimits(feature.name, name, handler);
142
+ validateRateLimitCost(feature.name, name, handler);
142
143
  validatePerCommandTransaction(feature.name, name, handler);
143
144
  }
144
145
  }
@@ -235,6 +236,22 @@ export function validateTenantlessAnonymous(featureName, handlerName, handler) {
235
236
  `tenant/user bucket, so one caller could exhaust it for all. Use ${IP_SCOPED_RATE_LIMIT_PER.map((allowed) => `"${allowed}"`).join(" or ")}.`);
236
237
  }
237
238
  }
239
+ export function validateRateLimitCost(featureName, handlerName, handler) {
240
+ // skip: no payload-derived cost declared
241
+ if (!handler.rateLimitCost)
242
+ return;
243
+ if (handler.rateLimit === undefined || isRateLimitDisabled(handler.rateLimit)) {
244
+ throw new Error(`write handler "${featureName}:write:${handlerName}" declares rateLimitCost without an active rateLimit — ` +
245
+ `the cost is booked on the rateLimit buckets, so declare rateLimit or drop rateLimitCost.`);
246
+ }
247
+ const isAnonymous = "roles" in handler.access && handler.access.roles.includes("anonymous");
248
+ const hasIpBucket = declaredRateLimitOptions(handler.rateLimit).some(({ per }) => IP_SCOPED_RATE_LIMIT_PER.includes(per));
249
+ if (isAnonymous || hasIpBucket) {
250
+ throw new Error(`write handler "${featureName}:write:${handlerName}" declares rateLimitCost on an anonymous or ip-limited handler — ` +
251
+ `rateLimitCost books after the parse, so such a handler would lose its pre-parse protection against junk floods. ` +
252
+ `Bulk handlers are authenticated: use a user/tenant bucket and drop "anonymous", or drop rateLimitCost.`);
253
+ }
254
+ }
238
255
  export function validateRateLimitDisabledReason(featureName, kind, handlerName, rateLimit) {
239
256
  // skip: rate limit isn't disabled, so no reason is required
240
257
  if (!isRateLimitDisabled(rateLimit))
@@ -0,0 +1,3 @@
1
+ import type { EntityDefinition } from "../types/index.ts";
2
+ import type { EntityListScreenDefinition } from "../types/screen.ts";
3
+ export declare function validateEntityListAggregateOptions(where: string, screen: EntityListScreenDefinition, entity: EntityDefinition): void;
@@ -0,0 +1,23 @@
1
+ import { isListFilterAllowed } from "../../db/entity-field-encryption.js";
2
+ import { isFieldFilterable } from "../screen-filter-ops.js";
3
+ import { entityAggregateFieldLookup, validateListTableAggregateFieldTypes, } from "./list-table-aggregate-options.js";
4
+ const RANGE_FACET_TYPES = ["number", "decimal", "money", "bigInt"];
5
+ function validateRangeFacets(where, facets, entity) {
6
+ for (const [field, config] of Object.entries(facets ?? {})) {
7
+ if (config === false || config.display !== "range")
8
+ continue;
9
+ const def = Object.hasOwn(entity.fields, field) ? entity.fields[field] : undefined;
10
+ if (def === undefined || !RANGE_FACET_TYPES.includes(def.type)) {
11
+ throw new Error(`${where} facets.${field}: display "range" needs a number, decimal, money or bigInt field.`);
12
+ }
13
+ if (!isFieldFilterable(def) || !isListFilterAllowed(entity, field, { op: "gte", value: 0 })) {
14
+ throw new Error(`${where} facets.${field}: display "range" needs filterable: true on a field that is not encrypted, personal or secret.`);
15
+ }
16
+ }
17
+ }
18
+ export function validateEntityListAggregateOptions(where, screen, entity) {
19
+ if (screen.table !== undefined) {
20
+ validateListTableAggregateFieldTypes(where, screen.table, entityAggregateFieldLookup(entity), true);
21
+ }
22
+ validateRangeFacets(where, screen.facets, entity);
23
+ }
@@ -3,15 +3,46 @@ import { parseQn } from "../qualified-name.js";
3
3
  import { formDerivationsOfSchema } from "../with-form-derivations.js";
4
4
  const NUMERIC_FORM_FIELD_TYPES = ["money", "number", "decimal"];
5
5
  const NUMERIC_SUB_FIELD_TYPES = ["money", "number", "decimal"];
6
+ function isMoneyType(type) {
7
+ return type === "money";
8
+ }
9
+ // Money operands are computed in minor units, number/decimal in major units: mixing them
10
+ // silently scales the result by 100 or 10000.
11
+ function validateUnitCompatibility({ prefix, screen, target }, derivation) {
12
+ const targetIsMoney = isMoneyType(screen.fields[target]?.type);
13
+ const moneyFlags = derivation.from.map((source) => isMoneyType(screen.fields[source]?.type));
14
+ const moneyCount = moneyFlags.filter(Boolean).length;
15
+ if (derivation.op === "sum" || derivation.op === "subtract") {
16
+ if (moneyCount !== 0 && moneyCount !== moneyFlags.length) {
17
+ throw new Error(`${prefix}.${target}: "${derivation.op}" sources must be all money or all number/decimal.`);
18
+ }
19
+ if (moneyCount > 0 !== targetIsMoney) {
20
+ throw new Error(`${prefix}.${target}: a "${derivation.op}" over ${moneyCount > 0 ? "money" : "number/decimal"} sources needs a ${moneyCount > 0 ? "money" : "number/decimal"} target.`);
21
+ }
22
+ // skip: sum/subtract fully validated above.
23
+ return;
24
+ }
25
+ if (moneyCount > 1 || (derivation.op === "percent" && moneyFlags[1] === true)) {
26
+ throw new Error(`${prefix}.${target}: "${derivation.op}" takes at most one money source${derivation.op === "percent" ? " (the base, not the rate)" : ""}.`);
27
+ }
28
+ if ((moneyCount === 1) !== targetIsMoney) {
29
+ throw new Error(`${prefix}.${target}: the target must be money exactly when a "${derivation.op}" source is money.`);
30
+ }
31
+ }
6
32
  function validateFromList({ prefix, screen, target }, derivation) {
7
33
  if (derivation.op === "percent" && derivation.from.length !== 2) {
8
34
  throw new Error(`${prefix}.${target}: "percent" needs exactly two sources (base, rate).`);
9
35
  }
10
36
  for (const source of derivation.from) {
11
- if (!(source in screen.fields)) {
37
+ const sourceField = screen.fields[source];
38
+ if (sourceField === undefined) {
12
39
  throw new Error(`${prefix}.${target}: unknown source field "${source}". Name a field of this screen.`);
13
40
  }
41
+ if (!NUMERIC_FORM_FIELD_TYPES.includes(sourceField.type)) {
42
+ throw new Error(`${prefix}.${target}: source "${source}" must be money, number or decimal, not "${sourceField.type}".`);
43
+ }
14
44
  }
45
+ validateUnitCompatibility({ prefix, screen, target }, derivation);
15
46
  }
16
47
  function validateListSum(check, derivation) {
17
48
  const { prefix, screen, target } = check;
@@ -26,6 +57,9 @@ function validateListSum(check, derivation) {
26
57
  if (cellType === undefined || !NUMERIC_SUB_FIELD_TYPES.includes(cellType)) {
27
58
  throw new Error(`${prefix}.${target}: "${derivation.cell}" must be a money, number or decimal sub-field of "${derivation.list}".`);
28
59
  }
60
+ if (isMoneyType(cellType) !== isMoneyType(screen.fields[target]?.type)) {
61
+ throw new Error(`${prefix}.${target}: a listSum of a ${isMoneyType(cellType) ? "money" : "number/decimal"} cell needs a ${isMoneyType(cellType) ? "money" : "number/decimal"} target.`);
62
+ }
29
63
  const { whenEmpty } = derivation;
30
64
  // skip: whenEmpty is optional
31
65
  if (whenEmpty === undefined)
@@ -94,9 +94,11 @@ export function validateTranslationLocaleConsistency(features) {
94
94
  }
95
95
  }
96
96
  export function validateRequiredLocales(features, requiredLocales) {
97
- const gaps = findRequiredLocaleGaps(features, requiredLocales ?? []);
98
- if (gaps.length > 0) {
97
+ const gaps = requiredLocales === undefined || requiredLocales.length === 0
98
+ ? []
99
+ : findRequiredLocaleGaps(features, requiredLocales);
100
+ if (requiredLocales !== undefined && gaps.length > 0) {
99
101
  const lines = gaps.map((gap) => `[i18n] Feature "${gap.featureName}": translation key "${gap.key}" is missing required locale(s) ${gap.missingLocales.join(", ")}.`);
100
- throw new Error(`${lines.join("\n")}\nThe app requires locales [${(requiredLocales ?? []).join(", ")}] for every translation key; load the matching locale package or add the missing translations.`);
102
+ throw new Error(`${lines.join("\n")}\nThe app requires locales [${requiredLocales.join(", ")}] for every translation key; load the matching locale package or add the missing translations.`);
101
103
  }
102
104
  }
@@ -2,6 +2,7 @@ import { validateEntityFieldEncryptionAvailable } from "../../db/entity-field-en
2
2
  import { dedupeFeatures } from "../dedupe-features.js";
3
3
  import { FILE_STORAGE_PROVIDER_ENV } from "../extension-names.js";
4
4
  import { QnTypes, qualifyEntityName } from "../qualified-name.js";
5
+ import { validateProjectionAllowlist } from "../validate-projection-allowlist.js";
5
6
  import { validateAccessDeclarations } from "./access-declarations.js";
6
7
  import { warnOnUniqueAccessRoles } from "./access-roles.js";
7
8
  import { validateActionWiring, validateFieldWiring } from "./action-wiring.js";
@@ -238,6 +239,7 @@ export function validateBoot(rawFeatures, options) {
238
239
  validateGdprStoragePersistence(features);
239
240
  validateGlobalTenancyEntities(features);
240
241
  validateFeatureBootChecks(features);
242
+ validateProjectionAllowlist(features);
241
243
  if (hasEncryptedFields) {
242
244
  // Availability check, not env-presence: eagerly building the keyring
243
245
  // catches malformed keys (wrong length, bad base64) at boot instead of
@@ -0,0 +1,3 @@
1
+ import { type RowAction } from "../types/screen.ts";
2
+ /** `where` is the full message prefix, e.g. `[Feature x] Screen "y" (entityList)`. */
3
+ export declare function validateListBulkActions(where: string, bulkActions: unknown, rowActions: readonly RowAction[] | undefined): void;