@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
@@ -4,6 +4,9 @@ import { SYSTEM_TENANT_ID } from "../engine/types/index.js";
4
4
  import { NotFoundError } from "../errors/index.js";
5
5
  import { createFallbackLogger } from "../logging/utils.js";
6
6
  const log = createFallbackLogger("kumiko");
7
+ // Content is checked where the mail link is built (bundled-features knows the path rules); a bad
8
+ // value must never void the whole body, or the request would silently send no mail.
9
+ const returnToField = z.string().max(2048).optional().catch(undefined);
7
10
  // Cross-device try-first handover: an optional tenant-handover grant the signup-request
8
11
  // route forwards to the handler for server-side verification and binding —
9
12
  // the grant itself never appears anywhere else (URL, mail, log, response).
@@ -15,6 +18,7 @@ export const SignupRequestBody = z.object({
15
18
  token: z.string().min(1).max(4096),
16
19
  })
17
20
  .optional(),
21
+ returnTo: returnToField,
18
22
  });
19
23
  // Shape guard for "handler not registered" — the only legitimate reason to
20
24
  // fall back to a single-tenant reply on /auth/tenants or /auth/switch-tenant.
@@ -82,4 +86,5 @@ export function registerTokenRequestRoute(opts) {
82
86
  // gains optional fields (locale, deviceId, …).
83
87
  const RequestTokenBody = z.object({
84
88
  email: z.email(),
89
+ returnTo: returnToField,
85
90
  });
@@ -37,8 +37,13 @@ function inMemoryIpRateLimit(clientIpResolver, now) {
37
37
  windows.delete(ip);
38
38
  }
39
39
  }
40
- if (windows.size > MAX_TRACKED_IPS)
41
- windows.clear();
40
+ // Map keeps insertion order and touched IPs are re-inserted, so the front is the least recently
41
+ // seen; clear() would let an attacker rotating addresses reset every other IP's window.
42
+ for (const ip of windows.keys()) {
43
+ if (windows.size <= MAX_TRACKED_IPS)
44
+ break;
45
+ windows.delete(ip);
46
+ }
42
47
  }
43
48
  return async (c, next) => {
44
49
  const ip = clientIpResolver.resolve(clientIpSourceFromHonoContext(c));
@@ -47,6 +52,7 @@ function inMemoryIpRateLimit(clientIpResolver, now) {
47
52
  const existing = windows.get(ip);
48
53
  const window = existing && nowMs - existing.startedAt < windowMs ? existing : { count: 0, startedAt: nowMs };
49
54
  window.count += 1;
55
+ windows.delete(ip);
50
56
  windows.set(ip, window);
51
57
  if (window.count > REPORTS_PER_WINDOW) {
52
58
  const resetAtMs = window.startedAt + windowMs;
@@ -67,6 +73,8 @@ export function createClientErrorRoute(options) {
67
73
  const app = new Hono();
68
74
  const logger = createFallbackLogger("client-errors");
69
75
  const routePath = `/api${Routes.clientErrors}`;
76
+ // Always on: the shared limiter fails open when Redis is down, this per-process window is the floor.
77
+ app.post(Routes.clientErrors, inMemoryIpRateLimit(options.clientIpResolver, options.now ?? Date.now));
70
78
  if (options.resolver) {
71
79
  app.post(Routes.clientErrors, httpRouteRateLimit({
72
80
  resolver: options.resolver,
@@ -76,9 +84,6 @@ export function createClientErrorRoute(options) {
76
84
  ...(options.meter && { meter: options.meter }),
77
85
  }));
78
86
  }
79
- else {
80
- app.post(Routes.clientErrors, inMemoryIpRateLimit(options.clientIpResolver, options.now ?? Date.now));
81
- }
82
87
  app.post(Routes.clientErrors, bodyLimit({
83
88
  maxSize: MAX_BODY_BYTES,
84
89
  onError: (c) => c.json({ error: { code: "payload_too_large" } }, 413),
@@ -1,2 +1,3 @@
1
- import type { AccessRule, Registry } from "../engine/types/index.ts";
1
+ import type { AccessRule, OwnershipMap, Registry } from "../engine/types/index.ts";
2
2
  export declare function collectEntityReadAccess(registry: Registry): ReadonlyMap<string, readonly AccessRule[]>;
3
+ export declare function collectEntityRowReadMaps(registry: Registry): ReadonlyMap<string, OwnershipMap>;
@@ -25,3 +25,17 @@ export function collectEntityReadAccess(registry) {
25
25
  }
26
26
  return rulesByEntity;
27
27
  }
28
+ // Entities whose own access.read narrows rows per role (e.g. Driver sees only assigned rows).
29
+ // A signal for such an entity must not name ids to a role that does not read every row.
30
+ export function collectEntityRowReadMaps(registry) {
31
+ const mapsByEntity = new Map();
32
+ for (const [entityName, entity] of registry.getAllEntities()) {
33
+ const readMap = entity.access?.read;
34
+ if (readMap === undefined || Object.keys(readMap).length === 0)
35
+ continue;
36
+ for (const aggregateType of new Set([entityName, toKebabCase(entityName)])) {
37
+ mapsByEntity.set(aggregateType, readMap);
38
+ }
39
+ }
40
+ return mapsByEntity;
41
+ }
@@ -0,0 +1,9 @@
1
+ import type { Cell } from "write-excel-file/node";
2
+ import type { ExportRow } from "./export-rows.ts";
3
+ export type ReferenceCellKind = {
4
+ readonly kind: "reference";
5
+ readonly labelField: string;
6
+ readonly multiple: boolean;
7
+ };
8
+ /** Labels come from the list row's eager-loaded `_refs`; a missing target shows its raw id. */
9
+ export declare function referenceCell(cell: ReferenceCellKind, field: string, row: ExportRow): Cell;
@@ -0,0 +1,21 @@
1
+ import { isPlainObject } from "../../utils/is-plain-object.js";
2
+ function labelOf(ref, labelField) {
3
+ if (!isPlainObject(ref))
4
+ return undefined;
5
+ const label = ref[labelField];
6
+ return typeof label === "string" ? label : undefined;
7
+ }
8
+ function refsOf(row, field) {
9
+ const refs = row["_refs"];
10
+ return isPlainObject(refs) ? refs[field] : undefined;
11
+ }
12
+ /** Labels come from the list row's eager-loaded `_refs`; a missing target shows its raw id. */
13
+ export function referenceCell(cell, field, row) {
14
+ const ids = cell.multiple && Array.isArray(row[field]) ? row[field] : [row[field]];
15
+ const ref = refsOf(row, field);
16
+ const labels = ids.map((id) => {
17
+ const target = Array.isArray(ref) ? ref.find((r) => isPlainObject(r) && r["id"] === id) : ref;
18
+ return labelOf(target, cell.labelField) ?? String(id);
19
+ });
20
+ return { value: labels.join(", "), type: String };
21
+ }
@@ -0,0 +1,11 @@
1
+ import type { Cell } from "write-excel-file/node";
2
+ import type { ExportColumn } from "./export-columns.ts";
3
+ import type { ExportRow } from "./export-rows.ts";
4
+ export type Translate = (key: string) => string;
5
+ export type ExportCellContext = {
6
+ readonly row: ExportRow;
7
+ readonly translate: Translate;
8
+ readonly timeZone: string;
9
+ };
10
+ /** Never emits a formula cell and never reinterprets strings: text always stays text. */
11
+ export declare function toExportCell(column: ExportColumn, context: ExportCellContext): Cell;
@@ -0,0 +1,136 @@
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
+ import { referenceCell } from "./export-cells-reference.js";
3
+ const DATE_FORMAT = "yyyy-mm-dd";
4
+ const TIMESTAMP_FORMAT = "yyyy-mm-dd hh:mm";
5
+ const PLAIN_MONEY_FORMAT = "#,##0.00";
6
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
7
+ const CURRENCY_CODE = /^[A-Z]{3}$/;
8
+ function isNullish(value) {
9
+ return value === null || value === undefined;
10
+ }
11
+ function textCell(value) {
12
+ return { value, type: String };
13
+ }
14
+ function numberCell(value, format) {
15
+ return { value, type: Number, ...(format !== undefined && { format }) };
16
+ }
17
+ function toFiniteNumber(value) {
18
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
19
+ return value !== "" && Number.isFinite(n) ? n : undefined;
20
+ }
21
+ function stringify(value) {
22
+ if (typeof value === "string")
23
+ return value;
24
+ if (typeof value === "object" && value !== null) {
25
+ // Temporal values stringify to ISO via toString; plain data goes through JSON.
26
+ const text = String(value);
27
+ return text === "[object Object]" ? JSON.stringify(value) : text;
28
+ }
29
+ return String(value);
30
+ }
31
+ function fromEpochMillis(value) {
32
+ if (value instanceof Date)
33
+ return Number.isNaN(value.getTime()) ? undefined : value;
34
+ if (typeof value === "object" && value !== null && "epochMilliseconds" in value) {
35
+ return fromEpochMillis(new Date(Number(value.epochMilliseconds)));
36
+ }
37
+ if (typeof value === "string" || typeof value === "number") {
38
+ return fromEpochMillis(new Date(value));
39
+ }
40
+ return undefined;
41
+ }
42
+ // Date-only values are anchored at UTC midnight, never parsed through the local zone.
43
+ function toUtcDate(value) {
44
+ const text = typeof value === "string" ? value : isNullish(value) ? "" : stringify(value);
45
+ if (ISO_DATE.test(text))
46
+ return fromEpochMillis(new Date(`${text}T00:00:00Z`));
47
+ return undefined;
48
+ }
49
+ function dateCell(value) {
50
+ const date = toUtcDate(value) ?? fromEpochMillis(value);
51
+ return date === undefined
52
+ ? textCell(stringify(value))
53
+ : { value: date, type: Date, format: DATE_FORMAT };
54
+ }
55
+ // The sheet has no time zone, so the instant is shifted to the wall clock of `timeZone` and
56
+ // stored as if that wall clock were UTC.
57
+ function timestampCell(value, timeZone) {
58
+ const date = fromEpochMillis(value);
59
+ if (date === undefined)
60
+ return textCell(stringify(value));
61
+ const wall = Temporal.Instant.fromEpochMilliseconds(date.getTime()).toZonedDateTimeISO(timeZone);
62
+ const shifted = new Date(Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second));
63
+ return { value: shifted, type: Date, format: TIMESTAMP_FORMAT };
64
+ }
65
+ // `at` is the wall clock in the record's own zone without offset; reading it as UTC keeps the
66
+ // digits the user saw in the list.
67
+ function locatedTimestampCell(value) {
68
+ const at = typeof value === "object" && value !== null && "at" in value ? String(value.at) : undefined;
69
+ const date = at === undefined ? undefined : fromEpochMillis(new Date(`${at}Z`));
70
+ return date === undefined
71
+ ? textCell(stringify(value))
72
+ : { value: date, type: Date, format: TIMESTAMP_FORMAT };
73
+ }
74
+ function moneyCell(value) {
75
+ if (typeof value !== "object" || value === null || !("amount" in value)) {
76
+ const n = toFiniteNumber(value);
77
+ return n === undefined ? textCell(stringify(value)) : numberCell(n, PLAIN_MONEY_FORMAT);
78
+ }
79
+ const amount = toFiniteNumber(value.amount);
80
+ if (amount === undefined)
81
+ return textCell(stringify(value.amount));
82
+ const currency = "currency" in value ? String(value.currency) : "";
83
+ const format = CURRENCY_CODE.test(currency)
84
+ ? `${PLAIN_MONEY_FORMAT} "${currency}"`
85
+ : PLAIN_MONEY_FORMAT;
86
+ return numberCell(amount, format);
87
+ }
88
+ function integerCell(value, kind) {
89
+ const n = toFiniteNumber(value);
90
+ if (n === undefined)
91
+ return textCell(stringify(value));
92
+ // A bigInt beyond 2^53 would lose digits as a spreadsheet number.
93
+ if (kind === "bigInt" && !Number.isSafeInteger(n))
94
+ return textCell(stringify(value));
95
+ return numberCell(n);
96
+ }
97
+ function optionLabel(prefix, raw, translate) {
98
+ const text = String(raw);
99
+ const label = translate(`${prefix}${text}`);
100
+ return label === `${prefix}${text}` ? text : label;
101
+ }
102
+ function autoCell(value) {
103
+ if (typeof value === "number" && Number.isFinite(value))
104
+ return numberCell(value);
105
+ if (typeof value === "boolean")
106
+ return { value, type: Boolean };
107
+ return textCell(stringify(value));
108
+ }
109
+ function optionCell(cell, value, translate) {
110
+ const values = cell.kind === "multiSelect" && Array.isArray(value) ? value : [value];
111
+ return textCell(values.map((v) => optionLabel(cell.optionLabelKeyPrefix, v, translate)).join(", "));
112
+ }
113
+ const SIMPLE_CELL_BUILDERS = {
114
+ number: (value) => integerCell(value, "number"),
115
+ decimal: (value) => integerCell(value, "decimal"),
116
+ bigInt: (value) => integerCell(value, "bigInt"),
117
+ money: (value) => moneyCell(value),
118
+ date: (value) => dateCell(value),
119
+ timestamp: timestampCell,
120
+ locatedTimestamp: (value) => locatedTimestampCell(value),
121
+ boolean: (value) => typeof value === "boolean" ? { value, type: Boolean } : textCell(stringify(value)),
122
+ auto: (value) => autoCell(value),
123
+ text: (value) => textCell(stringify(value)),
124
+ };
125
+ /** Never emits a formula cell and never reinterprets strings: text always stays text. */
126
+ export function toExportCell(column, context) {
127
+ const { cell, field } = column;
128
+ const value = context.row[field];
129
+ if (isNullish(value))
130
+ return null;
131
+ if (cell.kind === "reference")
132
+ return referenceCell(cell, field, context.row);
133
+ if ("optionLabelKeyPrefix" in cell)
134
+ return optionCell(cell, value, context.translate);
135
+ return SIMPLE_CELL_BUILDERS[cell.kind](value, context.timeZone);
136
+ }
@@ -0,0 +1,2 @@
1
+ import type { SheetData } from "write-excel-file/node";
2
+ export declare function guardSheetAgainstCiphertext(sheet: SheetData, isProduction?: boolean): SheetData;
@@ -0,0 +1,27 @@
1
+ import { InternalError } from "../../errors/index.js";
2
+ import { createFallbackLogger } from "../../logging/utils.js";
3
+ import { CIPHERTEXT_RE, CIPHERTEXT_REDACT_RE, CIPHERTEXT_REDACTION } from "../pii-leak-guard.js";
4
+ const log = createFallbackLogger("kumiko:export-xlsx");
5
+ // The response guard only scans JSON, so a workbook needs its own check: a ciphertext in a cell
6
+ // means a raw DB read reached the export. Same policy as the guard: loud outside production,
7
+ // redacted and logged in production.
8
+ function guardText(text, isProduction) {
9
+ if (!CIPHERTEXT_RE.test(text))
10
+ return text;
11
+ const detail = "XLSX export contains a PII ciphertext — a raw DB read leaked into the sheet. " +
12
+ "Decrypt before returning (decryptStoredPii / executor read path).";
13
+ if (!isProduction)
14
+ throw new InternalError({ message: detail });
15
+ log.error(detail);
16
+ return text.replace(CIPHERTEXT_REDACT_RE, CIPHERTEXT_REDACTION);
17
+ }
18
+ function guardCell(cell, isProduction) {
19
+ if (typeof cell !== "object" || cell === null || !("value" in cell))
20
+ return cell;
21
+ if (typeof cell.value !== "string")
22
+ return cell;
23
+ return { ...cell, value: guardText(cell.value, isProduction) };
24
+ }
25
+ export function guardSheetAgainstCiphertext(sheet, isProduction = process.env["NODE_ENV"] === "production") {
26
+ return sheet.map((row) => row.map((cell) => guardCell(cell, isProduction)));
27
+ }
@@ -0,0 +1,29 @@
1
+ import type { EntityDefinition, ListColumnSpec, SessionUser } from "../../engine/types/index.ts";
2
+ import type { ReferenceCellKind } from "./export-cells-reference.ts";
3
+ export type SimpleCellKind = "text" | "number" | "decimal" | "bigInt" | "money" | "date" | "timestamp" | "locatedTimestamp" | "boolean" | "auto";
4
+ export type ExportCellKind = {
5
+ readonly kind: SimpleCellKind;
6
+ } | ReferenceCellKind | {
7
+ readonly kind: "select" | "multiSelect";
8
+ /** `<feature>:entity:<entity>:field:<field>:option:` — the option value is appended. */
9
+ readonly optionLabelKeyPrefix: string;
10
+ };
11
+ export type ExportColumn = {
12
+ readonly field: string;
13
+ readonly headerKey: string;
14
+ readonly headerFallback: string;
15
+ readonly cell: ExportCellKind;
16
+ };
17
+ export type ExportColumnSource = {
18
+ readonly featureName: string;
19
+ readonly columns: readonly ListColumnSpec[];
20
+ readonly entity: EntityDefinition | undefined;
21
+ readonly entityName: string | undefined;
22
+ readonly user: Pick<SessionUser, "roles">;
23
+ };
24
+ /**
25
+ * Resolves the requested column ids against the screen's declared columns, in client order.
26
+ * Virtual columns and, on an entityList, encrypted/sensitive fields are dropped without error:
27
+ * the zip response bypasses the PII-leak guard, so this is the only place that keeps them out.
28
+ */
29
+ export declare function resolveExportColumns(source: ExportColumnSource, requested: readonly string[]): ExportColumn[];
@@ -0,0 +1,85 @@
1
+ import { holdsCiphertextOrSecret } from "../../db/entity-field-encryption.js";
2
+ import { userMayReadFieldInBulk } from "../../engine/field-access.js";
3
+ import { normalizeListColumn } from "../../engine/screen-helpers.js";
4
+ import { exportRejection } from "./export-errors.js";
5
+ const PROJECTION_PSEUDO_ENTITY = "__projection__";
6
+ const FIELD_KIND_BY_TYPE = {
7
+ number: "number",
8
+ decimal: "decimal",
9
+ bigInt: "bigInt",
10
+ money: "money",
11
+ date: "date",
12
+ timestamp: "timestamp",
13
+ locatedTimestamp: "locatedTimestamp",
14
+ boolean: "boolean",
15
+ };
16
+ function fieldLabelKey(featureName, entityName, field) {
17
+ return `${featureName}:entity:${entityName}:field:${field}`;
18
+ }
19
+ function entityCellKind(source, entity, field) {
20
+ const def = entity.fields[field];
21
+ if (def?.type === "reference") {
22
+ return {
23
+ kind: "reference",
24
+ labelField: def.labelField ?? "id",
25
+ multiple: def.multiple === true,
26
+ };
27
+ }
28
+ const type = def?.type ?? "text";
29
+ if (type === "select" || type === "multiSelect") {
30
+ return {
31
+ kind: type,
32
+ optionLabelKeyPrefix: `${fieldLabelKey(source.featureName, source.entityName ?? "", field)}:option:`,
33
+ };
34
+ }
35
+ return { kind: FIELD_KIND_BY_TYPE[type] ?? "text" };
36
+ }
37
+ function projectionCellKind(valueType) {
38
+ if (valueType === "number" || valueType === "decimal" || valueType === "bigInt") {
39
+ return { kind: valueType };
40
+ }
41
+ return { kind: valueType === "money" ? "money" : "auto" };
42
+ }
43
+ function isExportableOnEntity(source, entity, field, virtual) {
44
+ if (virtual || !userMayReadFieldInBulk(entity, field, source.user))
45
+ return false;
46
+ return !holdsCiphertextOrSecret(entity, field);
47
+ }
48
+ /**
49
+ * Resolves the requested column ids against the screen's declared columns, in client order.
50
+ * Virtual columns and, on an entityList, encrypted/sensitive fields are dropped without error:
51
+ * the zip response bypasses the PII-leak guard, so this is the only place that keeps them out.
52
+ */
53
+ export function resolveExportColumns(source, requested) {
54
+ const declared = new Map(source.columns.map((spec) => normalizeListColumn(spec)).map((c) => [c.field, c]));
55
+ const seen = new Set();
56
+ const resolved = [];
57
+ for (const field of requested) {
58
+ const column = declared.get(field);
59
+ if (column === undefined) {
60
+ throw exportRejection("export_unknown_column", "columns", { column: field });
61
+ }
62
+ if (seen.has(field))
63
+ continue;
64
+ seen.add(field);
65
+ const virtual = column.virtual === true;
66
+ const { entity } = source;
67
+ if (entity !== undefined && !isExportableOnEntity(source, entity, field, virtual))
68
+ continue;
69
+ if (entity === undefined && virtual)
70
+ continue;
71
+ const cell = entity !== undefined
72
+ ? entityCellKind(source, entity, field)
73
+ : projectionCellKind(column.valueType);
74
+ const headerEntity = source.entityName ?? PROJECTION_PSEUDO_ENTITY;
75
+ resolved.push({
76
+ field,
77
+ headerKey: column.label ?? fieldLabelKey(source.featureName, headerEntity, field),
78
+ headerFallback: column.label ?? field,
79
+ cell,
80
+ });
81
+ }
82
+ if (resolved.length === 0)
83
+ throw exportRejection("export_no_columns", "columns");
84
+ return resolved;
85
+ }
@@ -0,0 +1,4 @@
1
+ import { ValidationError } from "../../errors/index.ts";
2
+ export declare const EXPORT_TOO_MANY_I18N_KEY = "kumiko.list.export-too-many";
3
+ export type ExportRejectReason = "export_invalid_request" | "export_unknown_screen" | "export_not_enabled" | "export_unknown_column" | "export_no_columns";
4
+ export declare function exportRejection(reason: ExportRejectReason, path: string, params?: Readonly<Record<string, unknown>>): ValidationError;
@@ -0,0 +1,14 @@
1
+ import { ValidationError } from "../../errors/index.js";
2
+ export const EXPORT_TOO_MANY_I18N_KEY = "kumiko.list.export-too-many";
3
+ export function exportRejection(reason, path, params) {
4
+ return new ValidationError({
5
+ fields: [
6
+ {
7
+ path,
8
+ code: reason,
9
+ i18nKey: `kumiko.list.${reason.replace(/_/g, "-")}`,
10
+ ...(params !== undefined && { params }),
11
+ },
12
+ ],
13
+ });
14
+ }
@@ -0,0 +1,4 @@
1
+ import { Hono } from "hono";
2
+ import type { Registry } from "../../engine/types/index.ts";
3
+ import type { Dispatcher } from "../../pipeline/dispatcher.ts";
4
+ export declare function createExportRoutes(dispatcher: Dispatcher, registry: Registry): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
@@ -0,0 +1,74 @@
1
+ import { Hono } from "hono";
2
+ import * as z from "zod";
3
+ import { toKumikoError } from "../../errors/index.js";
4
+ import { createI18n, DEFAULT_LOCALE } from "../../i18n/index.js";
5
+ import { isValidIanaTimeZone } from "../../time/index.js";
6
+ import { Routes } from "../api-constants.js";
7
+ import { getUser } from "../auth-middleware.js";
8
+ import { requestContext } from "../request-context.js";
9
+ import { assertPayloadDepthAllowed, queryErrorResponse } from "../routes.js";
10
+ import { EXPORT_TOO_MANY_I18N_KEY, exportRejection } from "./export-errors.js";
11
+ import { readExportRows } from "./export-rows.js";
12
+ import { resolveExportPlan } from "./export-screen.js";
13
+ import { writeExportWorkbook } from "./export-xlsx-writer.js";
14
+ const XLSX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
15
+ const MAX_ID_LENGTH = 200;
16
+ const exportBodySchema = z.object({
17
+ screen: z.string().max(MAX_ID_LENGTH),
18
+ payload: z.record(z.string(), z.unknown()).default({}),
19
+ columns: z.array(z.string().max(MAX_ID_LENGTH)).min(1).max(MAX_ID_LENGTH),
20
+ timeZone: z.string().max(64).optional(),
21
+ });
22
+ // Browser zone first (what the user saw in the list), then the profile zone, else UTC.
23
+ function exportTimeZone(requested, profile) {
24
+ for (const candidate of [requested, profile]) {
25
+ if (candidate !== undefined && isValidIanaTimeZone(candidate))
26
+ return candidate;
27
+ }
28
+ return "UTC";
29
+ }
30
+ function downloadFileName(screenShortId, now) {
31
+ const safeId = screenShortId.replace(/[^A-Za-z0-9_-]/g, "_");
32
+ return `${safeId}-${now.toISOString().slice(0, 10)}.xlsx`;
33
+ }
34
+ export function createExportRoutes(dispatcher, registry) {
35
+ const api = new Hono();
36
+ const i18n = createI18n(registry, { defaultLocale: DEFAULT_LOCALE });
37
+ api.post(Routes.exportXlsx, async (c) => {
38
+ const user = getUser(c);
39
+ try {
40
+ const parsed = exportBodySchema.safeParse(await c.req.json().catch(() => undefined));
41
+ if (!parsed.success)
42
+ throw exportRejection("export_invalid_request", "body");
43
+ const body = parsed.data;
44
+ assertPayloadDepthAllowed(body.payload);
45
+ const plan = resolveExportPlan(registry, body.screen, body.columns, user);
46
+ const result = await readExportRows(dispatcher, plan, body.payload, user);
47
+ if (result.tooMany) {
48
+ return c.json({
49
+ error: {
50
+ code: "export_too_many_rows",
51
+ httpStatus: 413,
52
+ message: `export exceeds the limit of ${plan.maxRows} rows; narrow the filter`,
53
+ i18nKey: EXPORT_TOO_MANY_I18N_KEY,
54
+ details: { maxRows: plan.maxRows },
55
+ },
56
+ }, 413);
57
+ }
58
+ const locale = requestContext.get()?.locale ?? DEFAULT_LOCALE;
59
+ const bytes = await writeExportWorkbook(plan.columns, result.rows, (key) => i18n.t(key, locale), exportTimeZone(body.timeZone, user.timezone));
60
+ return new Response(bytes, {
61
+ status: 200,
62
+ headers: {
63
+ "Content-Type": XLSX_CONTENT_TYPE,
64
+ "Cache-Control": "no-store",
65
+ "Content-Disposition": `attachment; filename="${downloadFileName(plan.screenShortId, new Date())}"`,
66
+ },
67
+ });
68
+ }
69
+ catch (e) {
70
+ return queryErrorResponse(c, toKumikoError(e), Routes.exportXlsx);
71
+ }
72
+ });
73
+ return api;
74
+ }
@@ -0,0 +1,13 @@
1
+ import type { SessionUser } from "../../engine/types/index.ts";
2
+ import type { Dispatcher } from "../../pipeline/dispatcher.ts";
3
+ import type { ExportPlan } from "./export-screen.ts";
4
+ export type ExportRow = Readonly<Record<string, unknown>>;
5
+ export type ExportRowsResult = {
6
+ readonly tooMany: false;
7
+ readonly rows: readonly ExportRow[];
8
+ } | {
9
+ readonly tooMany: true;
10
+ };
11
+ export declare function sanitizeExportPayload(clientPayload: Readonly<Record<string, unknown>>, plan: Pick<ExportPlan, "screenFilter">): Record<string, unknown>;
12
+ /** Reads the whole filtered list page by page; gives up as soon as more than `maxRows` exist. */
13
+ export declare function readExportRows(dispatcher: Pick<Dispatcher, "query">, plan: ExportPlan, clientPayload: Readonly<Record<string, unknown>>, user: SessionUser): Promise<ExportRowsResult>;
@@ -0,0 +1,62 @@
1
+ import { MAX_LIST_LIMIT } from "../../engine/entity-handlers.js";
2
+ import { InternalError } from "../../errors/index.js";
3
+ import { isPlainObject } from "../../utils/is-plain-object.js";
4
+ // Paging, sizing and aggregation belong to the server here; `filter` is replaced from the screen
5
+ // below, so a client can neither widen the fixed screen filter nor smuggle another one in.
6
+ const CLIENT_CONTROLLED_PAGING_KEYS = [
7
+ "limit",
8
+ "offset",
9
+ "cursor",
10
+ "totalCount",
11
+ "aggregate",
12
+ "includeDeleted",
13
+ "filter",
14
+ ];
15
+ export function sanitizeExportPayload(clientPayload, plan) {
16
+ const payload = { ...clientPayload };
17
+ for (const key of CLIENT_CONTROLLED_PAGING_KEYS)
18
+ delete payload[key];
19
+ if (plan.screenFilter !== undefined)
20
+ payload["filter"] = plan.screenFilter;
21
+ return payload;
22
+ }
23
+ function parsePage(result, queryType) {
24
+ if (!isPlainObject(result) || !Array.isArray(result["rows"])) {
25
+ throw new InternalError({
26
+ message: `export: query ${queryType} did not return a { rows } envelope`,
27
+ });
28
+ }
29
+ const nextCursor = result["nextCursor"];
30
+ return {
31
+ rows: result["rows"].filter(isPlainObject),
32
+ nextCursor: typeof nextCursor === "string" && nextCursor !== "" ? nextCursor : undefined,
33
+ };
34
+ }
35
+ /** Reads the whole filtered list page by page; gives up as soon as more than `maxRows` exist. */
36
+ export async function readExportRows(dispatcher, plan, clientPayload, user) {
37
+ const base = sanitizeExportPayload(clientPayload, plan);
38
+ const rows = [];
39
+ const seenCursors = new Set();
40
+ let cursor;
41
+ for (;;) {
42
+ const limit = Math.min(MAX_LIST_LIMIT, plan.maxRows + 1 - rows.length);
43
+ const pagingPayload = cursor !== undefined
44
+ ? { ...base, limit, cursor }
45
+ : rows.length > 0
46
+ ? { ...base, limit, offset: rows.length }
47
+ : { ...base, limit };
48
+ const page = parsePage(await dispatcher.query(plan.queryType, pagingPayload, user), plan.queryType);
49
+ rows.push(...page.rows);
50
+ if (rows.length > plan.maxRows)
51
+ return { tooMany: true };
52
+ // A short or empty page is the end of the list; a repeated cursor would loop forever.
53
+ if (page.rows.length < limit || page.rows.length === 0)
54
+ return { tooMany: false, rows };
55
+ if (page.nextCursor !== undefined) {
56
+ if (seenCursors.has(page.nextCursor))
57
+ return { tooMany: false, rows };
58
+ seenCursors.add(page.nextCursor);
59
+ }
60
+ cursor = page.nextCursor;
61
+ }
62
+ }
@@ -0,0 +1,12 @@
1
+ import type { Registry, ScreenFilter, SessionUser } from "../../engine/types/index.ts";
2
+ import { type ExportColumn } from "./export-columns.ts";
3
+ export type ExportPlan = {
4
+ readonly screenShortId: string;
5
+ readonly queryType: string;
6
+ readonly screenFilter: ScreenFilter | undefined;
7
+ readonly columns: readonly ExportColumn[];
8
+ readonly maxRows: number;
9
+ };
10
+ type ExportScreenRegistry = Pick<Registry, "getScreen" | "getScreenFeature" | "getEntity">;
11
+ export declare function resolveExportPlan(registry: ExportScreenRegistry, screenQn: string, requestedColumns: readonly string[], user: Pick<SessionUser, "roles">): ExportPlan;
12
+ export {};