@cosmicdrift/kumiko-framework 0.386.0 → 0.388.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (468) hide show
  1. package/AGENTS.md +6 -1
  2. package/LICENSE +21 -57
  3. package/README.md +2 -2
  4. package/agents/examples/anonymous-access.md +1 -1
  5. package/agents/examples/apex-landing.md +204 -0
  6. package/agents/examples/api-worker-split.md +64 -0
  7. package/agents/examples/app-config.md +14 -0
  8. package/agents/examples/auth-mfa.md +115 -0
  9. package/agents/examples/env-composition.md +28 -1
  10. package/agents/examples/legal-pages.md +1 -1
  11. package/agents/examples/prod-boot-options.md +16 -0
  12. package/agents/examples/rate-limiting.md +48 -0
  13. package/agents/examples/record-detail-layout.md +692 -0
  14. package/agents/examples/seed-migration.md +212 -0
  15. package/agents/examples/wizard-form.md +941 -0
  16. package/agents/examples/writeform-section.md +548 -0
  17. package/agents/features.md +19 -2
  18. package/agents/widgets.md +40 -39
  19. package/dist/api/api-constants.js +2 -2
  20. package/dist/api/auth-middleware.js +7 -7
  21. package/dist/api/auth-rate-limiters.js +7 -3
  22. package/dist/api/auth-routes-mfa.js +1 -1
  23. package/dist/api/auth-routes-session.js +1 -1
  24. package/dist/api/auth-routes-shared.js +7 -3
  25. package/dist/api/auth-routes-signup.js +1 -1
  26. package/dist/api/client-error-route.js +8 -2
  27. package/dist/api/client-ip.js +3 -1
  28. package/dist/api/extra-route.js +1 -1
  29. package/dist/api/index.d.ts +3 -1
  30. package/dist/api/index.js +3 -1
  31. package/dist/api/pii-leak-guard.js +4 -2
  32. package/dist/api/readiness.js +5 -5
  33. package/dist/api/redis-sse-broker.js +8 -6
  34. package/dist/api/request-context.d.ts +1 -1
  35. package/dist/api/request-context.js +1 -1
  36. package/dist/api/routes.js +2 -2
  37. package/dist/api/server-boot-checks.d.ts +10 -0
  38. package/dist/api/server-boot-checks.js +157 -0
  39. package/dist/api/server-event-dispatcher.d.ts +26 -0
  40. package/dist/api/server-event-dispatcher.js +208 -0
  41. package/dist/api/server-extra-routes.d.ts +11 -0
  42. package/dist/api/server-extra-routes.js +203 -0
  43. package/dist/api/server-middleware.d.ts +20 -0
  44. package/dist/api/server-middleware.js +211 -0
  45. package/dist/api/server-routes.d.ts +19 -0
  46. package/dist/api/server-routes.js +293 -0
  47. package/dist/api/server-websocket-routes.d.ts +4 -0
  48. package/dist/api/server-websocket-routes.js +108 -0
  49. package/dist/api/server-wiring.d.ts +32 -0
  50. package/dist/api/server-wiring.js +236 -0
  51. package/dist/api/server.d.ts +10 -15
  52. package/dist/api/server.js +48 -1250
  53. package/dist/api/session-cookies.js +4 -4
  54. package/dist/api/sse-broker.js +1 -1
  55. package/dist/api/sse-route.d.ts +13 -13
  56. package/dist/api/sse-route.js +31 -27
  57. package/dist/auth/index.js +4 -4
  58. package/dist/auth/roles.d.ts +4 -4
  59. package/dist/auth/roles.js +24 -24
  60. package/dist/compliance/duration-spec.js +18 -20
  61. package/dist/compliance/index.js +2 -3
  62. package/dist/compliance/override-schema.d.ts +4 -4
  63. package/dist/compliance/override-schema.js +19 -22
  64. package/dist/compliance/profiles.d.ts +26 -29
  65. package/dist/compliance/profiles.js +62 -69
  66. package/dist/compliance/sub-processors.d.ts +28 -30
  67. package/dist/compliance/sub-processors.js +17 -21
  68. package/dist/consumer-cli.js +4 -4
  69. package/dist/crypto/blind-index.js +18 -17
  70. package/dist/crypto/ciphertext-pattern.js +1 -1
  71. package/dist/crypto/event-pii.js +3 -3
  72. package/dist/crypto/kek-source.d.ts +1 -1
  73. package/dist/crypto/kek-source.js +3 -2
  74. package/dist/crypto/kms-adapter.d.ts +1 -1
  75. package/dist/crypto/kms-adapter.js +4 -6
  76. package/dist/crypto/kms-wiring.d.ts +1 -2
  77. package/dist/crypto/kms-wiring.js +2 -2
  78. package/dist/crypto/owner-transition-rekey.d.ts +5 -1
  79. package/dist/crypto/owner-transition-rekey.js +1 -1
  80. package/dist/crypto/pg-kms-adapter.js +2 -6
  81. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  82. package/dist/crypto/pii-field-encryption.js +7 -5
  83. package/dist/crypto/subject-resolver.d.ts +1 -1
  84. package/dist/crypto/subject-resolver.js +2 -2
  85. package/dist/db/aggregate-sql.d.ts +21 -0
  86. package/dist/db/aggregate-sql.js +155 -0
  87. package/dist/db/apply-entity-event.d.ts +6 -6
  88. package/dist/db/apply-entity-event.js +55 -55
  89. package/dist/db/blind-index-cleanup.js +7 -7
  90. package/dist/db/closed-connection.d.ts +9 -0
  91. package/dist/db/closed-connection.js +95 -0
  92. package/dist/db/collect-table-metas.js +10 -11
  93. package/dist/db/compound-types.d.ts +1 -1
  94. package/dist/db/compound-types.js +12 -12
  95. package/dist/db/cursor.js +3 -4
  96. package/dist/db/dialect.d.ts +2 -2
  97. package/dist/db/dialect.js +3 -3
  98. package/dist/db/distinct-on.d.ts +1 -1
  99. package/dist/db/driver-row-coercion.d.ts +2 -0
  100. package/dist/db/driver-row-coercion.js +186 -0
  101. package/dist/db/driver-value-preparation.d.ts +16 -0
  102. package/dist/db/driver-value-preparation.js +99 -0
  103. package/dist/db/eagerload.d.ts +3 -3
  104. package/dist/db/eagerload.js +21 -12
  105. package/dist/db/entity-field-encryption.d.ts +1 -0
  106. package/dist/db/entity-field-encryption.js +1 -1
  107. package/dist/db/entity-table-meta.d.ts +2 -2
  108. package/dist/db/entity-table-meta.js +105 -112
  109. package/dist/db/event-store-executor-context.d.ts +3 -2
  110. package/dist/db/event-store-executor-context.js +13 -13
  111. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  112. package/dist/db/event-store-executor-read-aggregate.js +112 -0
  113. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  114. package/dist/db/event-store-executor-read-filter.js +203 -0
  115. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  116. package/dist/db/event-store-executor-read-paging.js +99 -0
  117. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  118. package/dist/db/event-store-executor-read-rows.js +32 -0
  119. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  120. package/dist/db/event-store-executor-read-search.js +221 -0
  121. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  122. package/dist/db/event-store-executor-read-sort.js +130 -0
  123. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  124. package/dist/db/event-store-executor-read-where.js +81 -0
  125. package/dist/db/event-store-executor-read.d.ts +2 -9
  126. package/dist/db/event-store-executor-read.js +50 -704
  127. package/dist/db/event-store-executor-write-steps.d.ts +20 -0
  128. package/dist/db/event-store-executor-write-steps.js +100 -0
  129. package/dist/db/event-store-executor-write.js +401 -510
  130. package/dist/db/index.d.ts +6 -2
  131. package/dist/db/index.js +5 -1
  132. package/dist/db/list-aggregate-labels.d.ts +14 -0
  133. package/dist/db/list-aggregate-labels.js +26 -0
  134. package/dist/db/located-timestamp.d.ts +11 -11
  135. package/dist/db/located-timestamp.js +29 -30
  136. package/dist/db/migrate-generator.d.ts +1 -1
  137. package/dist/db/migrate-generator.js +97 -105
  138. package/dist/db/migrate-runner.js +29 -26
  139. package/dist/db/money.d.ts +6 -6
  140. package/dist/db/money.js +29 -27
  141. package/dist/db/queries/backfill-pii.js +5 -5
  142. package/dist/db/queries/ddl.js +1 -1
  143. package/dist/db/queries/es-ops.d.ts +1 -1
  144. package/dist/db/queries/es-ops.js +1 -1
  145. package/dist/db/queries/event-consumer.js +3 -3
  146. package/dist/db/queries/event-store.js +4 -2
  147. package/dist/db/queries/projection-rebuild.js +3 -3
  148. package/dist/db/queries/raw-sql.d.ts +2 -2
  149. package/dist/db/queries/raw-sql.js +2 -2
  150. package/dist/db/queries/shadow-swap.js +19 -19
  151. package/dist/db/query.d.ts +3 -16
  152. package/dist/db/query.js +8 -761
  153. package/dist/db/rebuild-marker.js +20 -21
  154. package/dist/db/reference-data.js +6 -1
  155. package/dist/db/replay-migration-sql.js +2 -2
  156. package/dist/db/schema-inspection.js +4 -5
  157. package/dist/db/table-builder.d.ts +1 -2
  158. package/dist/db/table-builder.js +137 -190
  159. package/dist/db/table-info.d.ts +10 -0
  160. package/dist/db/table-info.js +45 -0
  161. package/dist/db/temporal-value-guards.d.ts +6 -0
  162. package/dist/db/temporal-value-guards.js +15 -0
  163. package/dist/db/tenant-db-runner.d.ts +1 -0
  164. package/dist/db/tenant-db-runner.js +3 -0
  165. package/dist/db/tenant-db.d.ts +2 -2
  166. package/dist/db/tenant-db.js +48 -11
  167. package/dist/db/where-clause.d.ts +6 -0
  168. package/dist/db/where-clause.js +236 -0
  169. package/dist/derivatives/delete-stored-file.js +6 -2
  170. package/dist/derivatives/derivatives-context.js +1 -1
  171. package/dist/engine/boot-validator/access-roles.js +4 -3
  172. package/dist/engine/boot-validator/api-ext.js +14 -14
  173. package/dist/engine/boot-validator/boot-check.js +1 -1
  174. package/dist/engine/boot-validator/config-deps.js +7 -6
  175. package/dist/engine/boot-validator/detail-screens.js +4 -4
  176. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  177. package/dist/engine/boot-validator/entity-handler.js +77 -61
  178. package/dist/engine/boot-validator/form-derivations.js +35 -1
  179. package/dist/engine/boot-validator/gdpr-storage.js +4 -4
  180. package/dist/engine/boot-validator/i18n-keys.js +6 -4
  181. package/dist/engine/boot-validator/index.d.ts +2 -2
  182. package/dist/engine/boot-validator/index.js +22 -20
  183. package/dist/engine/boot-validator/nav.js +5 -5
  184. package/dist/engine/boot-validator/ownership.js +12 -9
  185. package/dist/engine/boot-validator/parent-ref.js +1 -1
  186. package/dist/engine/boot-validator/pii-retention.js +258 -187
  187. package/dist/engine/boot-validator/projection-list-screens.js +7 -7
  188. package/dist/engine/boot-validator/query-refs.js +1 -1
  189. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  190. package/dist/engine/boot-validator/screens-actions.js +23 -17
  191. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  192. package/dist/engine/boot-validator/screens-forms.js +12 -17
  193. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  194. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  195. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  196. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  197. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  198. package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
  199. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  200. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  201. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  202. package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
  203. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  204. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  205. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  206. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  207. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  208. package/dist/engine/boot-validator/screens-lists.js +26 -24
  209. package/dist/engine/boot-validator/screens.d.ts +0 -2
  210. package/dist/engine/boot-validator/screens.js +102 -869
  211. package/dist/engine/boot-validator/security-baseline.js +3 -2
  212. package/dist/engine/boot-validator/transfer-graph.js +2 -2
  213. package/dist/engine/boot-validator/workspaces.js +3 -3
  214. package/dist/engine/bridge-stub.js +3 -4
  215. package/dist/engine/build-app-schema.js +130 -170
  216. package/dist/engine/build-config-feature-schema.js +50 -50
  217. package/dist/engine/build-target.js +20 -23
  218. package/dist/engine/codemod/pipeline-codemod.js +20 -13
  219. package/dist/engine/config-helpers.js +4 -4
  220. package/dist/engine/constants.js +2 -2
  221. package/dist/engine/create-app.js +2 -2
  222. package/dist/engine/define-feature.js +21 -13
  223. package/dist/engine/define-handler.js +13 -11
  224. package/dist/engine/define-step.js +1 -1
  225. package/dist/engine/define-workflow.d.ts +5 -5
  226. package/dist/engine/define-workflow.js +6 -6
  227. package/dist/engine/entity-handlers.d.ts +13 -0
  228. package/dist/engine/entity-handlers.js +22 -34
  229. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  230. package/dist/engine/entity-list-runtime-options.js +18 -0
  231. package/dist/engine/event-def.js +2 -2
  232. package/dist/engine/extension-names.d.ts +16 -17
  233. package/dist/engine/extension-names.js +37 -38
  234. package/dist/engine/extensions/escape-hatch-usage.js +1 -1
  235. package/dist/engine/extensions/tenant-data.js +2 -2
  236. package/dist/engine/extensions/user-data.d.ts +23 -24
  237. package/dist/engine/factories.d.ts +14 -13
  238. package/dist/engine/factories.js +38 -37
  239. package/dist/engine/feature-ast/entity-field-types.js +1 -1
  240. package/dist/engine/feature-ast/extractors/handlers.js +2 -1
  241. package/dist/engine/feature-ast/extractors/hooks.js +1 -1
  242. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  243. package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
  244. package/dist/engine/feature-ast/extractors/shared.js +2 -3
  245. package/dist/engine/feature-ast/parse.js +13 -13
  246. package/dist/engine/feature-ast/patch.js +4 -182
  247. package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
  248. package/dist/engine/feature-ast/pattern-change-schema.js +2 -1
  249. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  250. package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
  251. package/dist/engine/feature-ast/patterns.d.ts +11 -2
  252. package/dist/engine/feature-ast/patterns.js +43 -51
  253. package/dist/engine/feature-ast/render.js +50 -89
  254. package/dist/engine/feature-config-events-jobs.js +15 -12
  255. package/dist/engine/feature-entity-handlers.js +12 -5
  256. package/dist/engine/feature-manifest.d.ts +9 -9
  257. package/dist/engine/feature-manifest.js +6 -7
  258. package/dist/engine/feature-ui-extensions.d.ts +3 -3
  259. package/dist/engine/feature-ui-extensions.js +30 -22
  260. package/dist/engine/field-access.js +5 -6
  261. package/dist/engine/form-derivations.js +18 -7
  262. package/dist/engine/handler-field-references.js +1 -1
  263. package/dist/engine/index.d.ts +1 -0
  264. package/dist/engine/index.js +4 -4
  265. package/dist/engine/object-form.d.ts +1 -0
  266. package/dist/engine/object-form.js +14 -4
  267. package/dist/engine/ownership.js +3 -3
  268. package/dist/engine/parse-ref-target.js +5 -7
  269. package/dist/engine/pattern-library/index.js +2 -2
  270. package/dist/engine/pattern-library/library.js +2 -2
  271. package/dist/engine/pattern-library/types.js +3 -3
  272. package/dist/engine/pipeline.js +2 -2
  273. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  274. package/dist/engine/project-app-schema-for-roles.js +7 -5
  275. package/dist/engine/projection-helpers.js +9 -9
  276. package/dist/engine/qualified-name.js +2 -2
  277. package/dist/engine/registry-facade.js +1 -1
  278. package/dist/engine/registry-ingest.js +13 -13
  279. package/dist/engine/registry-state.js +17 -19
  280. package/dist/engine/registry-validate.js +12 -12
  281. package/dist/engine/run-in.js +2 -2
  282. package/dist/engine/run-pipeline.js +7 -0
  283. package/dist/engine/schema-builder.js +251 -233
  284. package/dist/engine/screen-filter-ops.js +17 -20
  285. package/dist/engine/screen-helpers.js +4 -3
  286. package/dist/engine/screen-kind.d.ts +7 -0
  287. package/dist/engine/screen-kind.js +1 -0
  288. package/dist/engine/state-machine.d.ts +12 -12
  289. package/dist/engine/state-machine.js +7 -7
  290. package/dist/engine/steps/_resolver-utils.js +4 -3
  291. package/dist/engine/steps/for-each.js +4 -4
  292. package/dist/engine/steps/unsafe-projection-delete.js +1 -1
  293. package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
  294. package/dist/engine/steps/wait-for-event.js +4 -4
  295. package/dist/engine/steps/workflow-system-events.js +3 -3
  296. package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
  297. package/dist/engine/tier-resolver-extension.d.ts +29 -27
  298. package/dist/engine/tier-resolver-extension.js +26 -26
  299. package/dist/engine/transfer-adjacency.js +2 -2
  300. package/dist/engine/validate-projection-allowlist.js +9 -28
  301. package/dist/engine/walk-step-instances.d.ts +2 -0
  302. package/dist/engine/walk-step-instances.js +38 -0
  303. package/dist/engine/where-rule-lint.js +1 -1
  304. package/dist/engine/write-handler-qn-extract.js +1 -1
  305. package/dist/entrypoint/index.d.ts +4 -1
  306. package/dist/entrypoint/index.js +10 -4
  307. package/dist/env/index.d.ts +7 -6
  308. package/dist/env/index.js +10 -9
  309. package/dist/errors/docs-url.d.ts +1 -1
  310. package/dist/errors/docs-url.js +22 -2
  311. package/dist/errors/transition-details.js +7 -7
  312. package/dist/errors/write-error-info.d.ts +6 -6
  313. package/dist/errors/write-error-info.js +6 -6
  314. package/dist/es-ops/context.d.ts +9 -9
  315. package/dist/es-ops/context.js +29 -29
  316. package/dist/es-ops/index.js +7 -9
  317. package/dist/es-ops/operations-schema.js +4 -10
  318. package/dist/es-ops/runner.d.ts +11 -11
  319. package/dist/es-ops/runner.js +49 -53
  320. package/dist/es-ops/types.d.ts +55 -55
  321. package/dist/es-ops/types.js +11 -11
  322. package/dist/event-store/admin-api.js +7 -8
  323. package/dist/event-store/errors.js +1 -1
  324. package/dist/event-store/event-store.js +12 -6
  325. package/dist/event-store/rebuild-dead-letter.js +1 -1
  326. package/dist/files/bind-pending-file-refs.js +3 -3
  327. package/dist/files/create-file-ref-from-bytes.js +4 -1
  328. package/dist/files/feature.js +9 -10
  329. package/dist/files/file-handle.js +7 -9
  330. package/dist/files/file-ref-entity.js +15 -15
  331. package/dist/files/file-ref-table.js +7 -8
  332. package/dist/files/file-routes.d.ts +7 -0
  333. package/dist/files/file-routes.js +22 -13
  334. package/dist/files/in-memory-provider.js +8 -9
  335. package/dist/files/local-provider.js +18 -25
  336. package/dist/files/provider-resolver.d.ts +7 -7
  337. package/dist/files/resolve-content-type.js +43 -0
  338. package/dist/files/storage-tracking.js +2 -2
  339. package/dist/files/upload-ledger.js +7 -0
  340. package/dist/files/zip-stream.d.ts +12 -19
  341. package/dist/files/zip-stream.js +44 -63
  342. package/dist/i18n/required-surface-keys.d.ts +1 -1
  343. package/dist/i18n/required-surface-keys.js +188 -212
  344. package/dist/internal/stack-support.d.ts +2 -1
  345. package/dist/internal/stack-support.js +1 -1
  346. package/dist/jobs/job-runner-context.d.ts +32 -0
  347. package/dist/jobs/job-runner-context.js +305 -0
  348. package/dist/jobs/job-runner-execution.d.ts +7 -0
  349. package/dist/jobs/job-runner-execution.js +160 -0
  350. package/dist/jobs/job-runner-gates.d.ts +19 -0
  351. package/dist/jobs/job-runner-gates.js +198 -0
  352. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  353. package/dist/jobs/job-runner-helpers.js +69 -4
  354. package/dist/jobs/job-runner-schedulers.d.ts +1 -1
  355. package/dist/jobs/job-runner-schedulers.js +3 -4
  356. package/dist/jobs/job-runner-scope.d.ts +44 -0
  357. package/dist/jobs/job-runner-scope.js +102 -0
  358. package/dist/jobs/job-runner-types.d.ts +12 -0
  359. package/dist/jobs/job-runner.d.ts +0 -1
  360. package/dist/jobs/job-runner.js +57 -669
  361. package/dist/lifecycle/lifecycle.js +3 -5
  362. package/dist/lifecycle/signal-handlers.js +5 -4
  363. package/dist/logging/utils.d.ts +2 -1
  364. package/dist/logging/utils.js +38 -10
  365. package/dist/migrations/index.js +1 -1
  366. package/dist/migrations/kumiko-drift.js +14 -13
  367. package/dist/migrations/pending-rebuilds.d.ts +21 -20
  368. package/dist/migrations/pending-rebuilds.js +27 -27
  369. package/dist/migrations/projection-table-index.d.ts +3 -3
  370. package/dist/migrations/projection-table-index.js +8 -8
  371. package/dist/observability/escape-hatch-report.d.ts +3 -0
  372. package/dist/observability/escape-hatch-report.js +12 -1
  373. package/dist/observability/metric-validator.js +1 -1
  374. package/dist/observability/metrics-handle.js +3 -3
  375. package/dist/pipeline/append-event-core.js +11 -2
  376. package/dist/pipeline/cascade-handler.d.ts +1 -1
  377. package/dist/pipeline/cascade-handler.js +165 -138
  378. package/dist/pipeline/dispatch-batch.js +2 -2
  379. package/dist/pipeline/dispatch-query.js +20 -13
  380. package/dist/pipeline/dispatch-shared.d.ts +14 -3
  381. package/dist/pipeline/dispatch-shared.js +67 -721
  382. package/dist/pipeline/dispatch-stream.js +1 -1
  383. package/dist/pipeline/dispatch-write.js +297 -190
  384. package/dist/pipeline/dispatcher-utils.d.ts +1 -1
  385. package/dist/pipeline/dispatcher-utils.js +1 -1
  386. package/dist/pipeline/event-consumer-state.js +2 -2
  387. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  388. package/dist/pipeline/event-dispatcher-delivery.js +4 -6
  389. package/dist/pipeline/event-dispatcher.js +17 -28
  390. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  391. package/dist/pipeline/handler-context-auth.js +54 -0
  392. package/dist/pipeline/handler-context-config.d.ts +14 -0
  393. package/dist/pipeline/handler-context-config.js +113 -0
  394. package/dist/pipeline/handler-context-db.d.ts +24 -0
  395. package/dist/pipeline/handler-context-db.js +149 -0
  396. package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
  397. package/dist/pipeline/handler-context-dispatch.js +89 -0
  398. package/dist/pipeline/handler-context-events.d.ts +43 -0
  399. package/dist/pipeline/handler-context-events.js +237 -0
  400. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  401. package/dist/pipeline/handler-context-notify.js +55 -0
  402. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  403. package/dist/pipeline/handler-context-observability.js +21 -0
  404. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  405. package/dist/pipeline/handler-context-scope.js +88 -0
  406. package/dist/pipeline/irreversible-operation-gate.js +1 -1
  407. package/dist/pipeline/lifecycle-pipeline.js +9 -9
  408. package/dist/pipeline/member-read-only-transaction.js +1 -3
  409. package/dist/pipeline/msp-rebuild.js +1 -1
  410. package/dist/pipeline/notify-scope.js +12 -1
  411. package/dist/pipeline/pre-handler-gates.js +20 -4
  412. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  413. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  414. package/dist/pipeline/projection-rebuild.js +15 -13
  415. package/dist/pipeline/system-hooks.js +39 -41
  416. package/dist/pipeline/system-identity-switch.js +1 -3
  417. package/dist/pipeline/tenant-timezone-cache.js +1 -1
  418. package/dist/random/generate.d.ts +29 -29
  419. package/dist/random/generate.js +30 -30
  420. package/dist/random/words.js +20 -20
  421. package/dist/rate-limit/middleware.js +9 -5
  422. package/dist/redis/client.js +2 -2
  423. package/dist/redis/pubsub-signal.js +14 -17
  424. package/dist/schema-cli/apply-command.d.ts +2 -0
  425. package/dist/schema-cli/apply-command.js +97 -0
  426. package/dist/schema-cli/baseline-command.d.ts +2 -0
  427. package/dist/schema-cli/baseline-command.js +33 -0
  428. package/dist/schema-cli/generate-command.d.ts +2 -0
  429. package/dist/schema-cli/generate-command.js +90 -0
  430. package/dist/schema-cli/shared.d.ts +39 -0
  431. package/dist/schema-cli/shared.js +95 -0
  432. package/dist/schema-cli/status-command.d.ts +2 -0
  433. package/dist/schema-cli/status-command.js +73 -0
  434. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  435. package/dist/schema-cli/validate-boot-input.js +130 -0
  436. package/dist/schema-cli/validate-command.d.ts +2 -0
  437. package/dist/schema-cli/validate-command.js +130 -0
  438. package/dist/schema-cli.d.ts +2 -18
  439. package/dist/schema-cli.js +47 -510
  440. package/dist/search/index.js +4 -4
  441. package/dist/search/meilisearch-adapter.js +2 -1
  442. package/dist/search/purge-subject.js +4 -5
  443. package/dist/search/reindex-entity.js +6 -9
  444. package/dist/time/boot-tz-warning.d.ts +2 -2
  445. package/dist/time/boot-tz-warning.js +8 -8
  446. package/dist/time/iana.js +10 -10
  447. package/dist/time/index.js +9 -9
  448. package/dist/time/legacy-date.d.ts +2 -2
  449. package/dist/time/legacy-date.js +6 -8
  450. package/dist/time/polyfill.js +1 -1
  451. package/dist/time/tz-context.d.ts +1 -1
  452. package/dist/time/tz-context.js +1 -1
  453. package/dist/ui-types/app-schema.js +7 -7
  454. package/dist/ui-types/list-row-meta.js +4 -4
  455. package/dist/upgrade-cli.js +159 -130
  456. package/dist/utils/compare.js +1 -1
  457. package/package.json +5 -5
  458. package/src/changes.json +358 -0
  459. package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
  460. package/src/scripts/codemod/feature-layout.ts +10 -2
  461. package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
  462. package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
  463. package/src/scripts/codemod/object-form.ts +41 -4
  464. package/src/scripts/codemod/package-merge.ts +58 -8
  465. package/src/scripts/codemod/pii-personal-migration.ts +192 -153
  466. package/src/scripts/codemod/testing-injection.ts +122 -42
  467. package/src/scripts/codemod/testing-move.ts +29 -7
  468. package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
@@ -0,0 +1,198 @@
1
+ /// <reference types="temporal-polyfill/global" preserve="true" />
2
+ import { prepareJobRun } from "./job-runner-context.js";
3
+ import { attemptNumberOf, buildRetryBullOpts, DEFER_COUNT_KEY, deferDelayMs, isFinalAttempt, priorAttemptsOf, priorDeferCountOf, } from "./job-runner-helpers.js";
4
+ import { ACTIVE_TENANT_IDS_QUERY_NAME, perTenantChildJobId } from "./job-runner-schedulers.js";
5
+ import { announceDropped, announceQueued, isOverPerTenantLimit, laneForJob, logFailedAttempt, } from "./job-runner-scope.js";
6
+ import { resolveMaxPerTenant } from "./max-per-tenant.js";
7
+ // Default lock-TTL for sequential jobs that didn't declare a timeout.
8
+ // 5 minutes matches BullMQ's default stalledInterval — long enough for
9
+ // any reasonable handler, short enough that a crashed worker recovers
10
+ // without manual intervention.
11
+ const SEQUENTIAL_DEFAULT_TTL_SEC = 305;
12
+ // How long to wait before re-trying a busy sequential lock. Short enough
13
+ // to feel responsive, long enough that we don't hammer Redis.
14
+ const SEQUENTIAL_RETRY_DELAY_MS = 200;
15
+ export const PER_TENANT_PREFIX = "_perTenant:";
16
+ export function perTenantTargetOf(jobName) {
17
+ return jobName.startsWith(PER_TENANT_PREFIX)
18
+ ? jobName.slice(PER_TENANT_PREFIX.length)
19
+ : undefined;
20
+ }
21
+ function missingTenantResolverError(scope, actualName) {
22
+ if (scope.registry.getQueryHandler(ACTIVE_TENANT_IDS_QUERY_NAME)) {
23
+ return new Error(`perTenant job "${actualName}": the tenant feature is mounted but the job runner has no ` +
24
+ "`context.db`: pass a DbConnection in the runner context or supply options.getActiveTenantIds");
25
+ }
26
+ return new Error(`perTenant job "${actualName}" requires either options.getActiveTenantIds or the ` +
27
+ `tenant feature mounted (it registers "${ACTIVE_TENANT_IDS_QUERY_NAME}", which the ` +
28
+ "framework uses to resolve active tenants on its own)");
29
+ }
30
+ async function enqueuePerTenantChildren(scope, bullJob, actualName) {
31
+ const { getActiveTenantIds } = scope;
32
+ if (!getActiveTenantIds) {
33
+ throw missingTenantResolverError(scope, actualName);
34
+ }
35
+ const actualDef = scope.allJobs.get(actualName);
36
+ if (!actualDef) {
37
+ throw new Error(`Unknown job: ${actualName}`);
38
+ }
39
+ const tenantIds = await getActiveTenantIds();
40
+ const targetQueue = scope.queues[laneForJob(actualDef)];
41
+ // wrapperJobId is only absent for a Job BullMQ somehow never assigned
42
+ // an id to (not observed in practice) — falling back to a constant
43
+ // there would collide every such run's children onto the same ids
44
+ // and silently swallow later runs as "duplicates", which is worse
45
+ // than the duplicate this is meant to prevent. Skip id-based dedup
46
+ // for that edge case instead: enqueue plainly, same as before.
47
+ const wrapperJobId = bullJob.id;
48
+ for (const tenantId of tenantIds) {
49
+ const childData = { ...bullJob.data, _tenantId: tenantId };
50
+ const child = await targetQueue.add(actualName, childData, {
51
+ ...buildRetryBullOpts(actualDef),
52
+ // Dedup over wrapper retries only holds as long as the children
53
+ // stay in Redis for the wrapper's whole retry window — see the
54
+ // COMPLETED_JOB_RETENTION_AGE_SEC invariant check in createJobRunner.
55
+ ...(wrapperJobId !== undefined ? { jobId: perTenantChildJobId(wrapperJobId, tenantId) } : {}),
56
+ });
57
+ await announceQueued(scope, actualName, actualDef, child, childData);
58
+ }
59
+ }
60
+ // Handle perTenant dispatch jobs — fan out to one job per tenant. The
61
+ // fan-out re-enqueues into the lane the actual job is assigned to;
62
+ // the _perTenant wrapper itself always lives in the consumer-lane
63
+ // (it's picked up by this runner's own worker).
64
+ export async function fanOutPerTenantJob(scope, bullJob, actualName) {
65
+ // These throws happen before runInSpan, so its onJobFailed never sees them
66
+ // and a drained test stack would stay green over a fan-out that never ran.
67
+ try {
68
+ await enqueuePerTenantChildren(scope, bullJob, actualName);
69
+ }
70
+ catch (err) {
71
+ // A retry may still succeed, and a succeeding wrapper reports no completion
72
+ // that could clear an earlier failure, so only the final attempt is reported.
73
+ const retries = scope.allJobs.get(actualName)?.retries ?? 0;
74
+ const finalAttempt = bullJob.attemptsMade + 1 >= retries + 1;
75
+ logFailedAttempt(scope, {
76
+ job: bullJob.name,
77
+ jobId: bullJob.id ?? "unknown",
78
+ attempt: bullJob.attemptsMade + 1,
79
+ finalAttempt,
80
+ err,
81
+ });
82
+ if (finalAttempt) {
83
+ await reportPreRunFailure(scope, bullJob.name, bullJob, err);
84
+ }
85
+ throw err;
86
+ }
87
+ }
88
+ async function reportPreRunFailure(scope, jobName, bullJob, err) {
89
+ const message = err instanceof Error ? err.message : String(err);
90
+ await scope.options.onJobFailed?.(jobName, bullJob.id ?? "unknown", message, [
91
+ { level: "error", message, timestamp: Temporal.Now.instant() },
92
+ ]);
93
+ }
94
+ // prepareJobRun and onJobStart throw before the handler span exists, so the
95
+ // span's own failure recording never sees them.
96
+ export async function startJobRun(scope, jobDef, bullJob, token) {
97
+ try {
98
+ const run = prepareJobRun(scope, jobDef, bullJob, token);
99
+ await scope.options.onJobStart?.(run.jobName, run.jobId, run.meta);
100
+ return run;
101
+ }
102
+ catch (err) {
103
+ const finalAttempt = isFinalAttempt(bullJob, jobDef);
104
+ logFailedAttempt(scope, {
105
+ job: bullJob.name,
106
+ jobId: bullJob.id ?? "unknown",
107
+ attempt: attemptNumberOf(bullJob),
108
+ finalAttempt,
109
+ err,
110
+ });
111
+ if (finalAttempt) {
112
+ await reportPreRunFailure(scope, bullJob.name, bullJob, err);
113
+ }
114
+ throw err;
115
+ }
116
+ }
117
+ export async function resolveJobDefinitionOrFail(scope, bullJob) {
118
+ const jobName = bullJob.name;
119
+ const jobDef = scope.allJobs.get(jobName);
120
+ if (jobDef)
121
+ return jobDef;
122
+ const err = new Error(`Unknown job: ${jobName}`);
123
+ // Final regardless of attempts left: a retry cannot make the job exist.
124
+ logFailedAttempt(scope, {
125
+ job: jobName,
126
+ jobId: bullJob.id ?? "unknown",
127
+ attempt: bullJob.attemptsMade + 1,
128
+ finalAttempt: true,
129
+ err,
130
+ });
131
+ await reportPreRunFailure(scope, jobName, bullJob, err);
132
+ throw err;
133
+ }
134
+ // maxPerTenant defer: checked before anything is recorded for this run, so a
135
+ // deferred pickup leaves no span, run-log entry or failure trace and does not
136
+ // consume an attempt (moveToDelayed + DelayedError, not a retry).
137
+ export async function deferOverTenantLimit(scope, jobDef, bullJob, token) {
138
+ const maxPerTenant = resolveMaxPerTenant(jobDef);
139
+ const deferTenantId = bullJob.data["_tenantId"];
140
+ if (maxPerTenant?.onLimit === "defer" &&
141
+ typeof deferTenantId === "string" &&
142
+ token !== undefined &&
143
+ bullJob.moveToDelayed &&
144
+ (await isOverPerTenantLimit(scope, bullJob.name, deferTenantId, maxPerTenant.limit, {
145
+ activeOnly: true,
146
+ self: { id: bullJob.id, processedOn: bullJob.processedOn },
147
+ }))) {
148
+ const deferCount = priorDeferCountOf(bullJob.data, DEFER_COUNT_KEY);
149
+ await bullJob.updateData?.({ ...bullJob.data, [DEFER_COUNT_KEY]: deferCount + 1 });
150
+ await bullJob.moveToDelayed(Date.now() + deferDelayMs(maxPerTenant.deferMs, deferCount), token);
151
+ return "deferred";
152
+ }
153
+ return "run";
154
+ }
155
+ async function requeueBehindSequentialLock(scope, jobDef, bullJob) {
156
+ // Re-enqueue onto the job's own lane-queue. In practice that's the
157
+ // same queue the worker just picked from (since only the consuming
158
+ // lane runs handleJob at all), but route explicitly — no implicit
159
+ // coupling to "whichever queue the caller happened to be on".
160
+ // Attempts already spent travel in _priorAttempts so a lock conflict
161
+ // on a retry cannot hand out a fresh budget and exceed `retries`.
162
+ const priorAttempts = priorAttemptsOf(bullJob.data) + bullJob.attemptsMade;
163
+ const requeuedData = { ...bullJob.data, _priorAttempts: priorAttempts };
164
+ const requeued = await scope.queues[laneForJob(jobDef)].add(bullJob.name, requeuedData, {
165
+ delay: SEQUENTIAL_RETRY_DELAY_MS,
166
+ ...buildRetryBullOpts(jobDef, priorAttempts),
167
+ });
168
+ if (bullJob.id !== undefined && bullJob.id !== requeued.id) {
169
+ await announceDropped(scope, bullJob.name, jobDef, bullJob.id, bullJob.data);
170
+ }
171
+ await announceQueued(scope, bullJob.name, jobDef, requeued, requeuedData);
172
+ }
173
+ // Sequential gate: try to claim the per-name lock. If another worker
174
+ // (or this worker on a different bullJob) holds it, re-enqueue with a
175
+ // small delay and exit *successfully* — using throw would burn the
176
+ // job's retry budget and pollute failure metrics, but a re-enqueue
177
+ // looks like an ordinary handoff to BullMQ.
178
+ export async function claimSequentialSlot(scope, jobDef, bullJob) {
179
+ const { sequentialLock } = scope;
180
+ if (jobDef.concurrency !== "sequential" || !sequentialLock) {
181
+ return { kind: "run", lockToken: null };
182
+ }
183
+ const ttlSec = jobDef.timeout ? Math.ceil(jobDef.timeout / 1000) + 5 : SEQUENTIAL_DEFAULT_TTL_SEC;
184
+ const lockToken = await sequentialLock.acquire(bullJob.name, { ttlSeconds: ttlSec });
185
+ if (lockToken)
186
+ return { kind: "run", lockToken };
187
+ await requeueBehindSequentialLock(scope, jobDef, bullJob);
188
+ return { kind: "requeued" };
189
+ }
190
+ // Release the sequential lock value-matched (Lua compare-and-delete
191
+ // inside DistributedLock). A TTL-expired lock that's been claimed by
192
+ // a different owner stays put — releasing it would break sequencing
193
+ // for the new owner.
194
+ export async function releaseSequentialSlot(scope, jobName, lockToken) {
195
+ if (lockToken && scope.sequentialLock) {
196
+ await scope.sequentialLock.release(jobName, lockToken);
197
+ }
198
+ }
@@ -3,18 +3,20 @@ import type { ConfigAccessor } from "@cosmicdrift/kumiko-types/config";
3
3
  import type { TzContext } from "@cosmicdrift/kumiko-types/tz-context";
4
4
  import type { JobsOptions, Queue } from "bullmq";
5
5
  import type { TenantDb } from "../db/tenant-db.ts";
6
- import type { AppContext, JobDefinition, JobRunIn, JobTriggerEvent } from "../engine/types/index.ts";
6
+ import type { AppContext, JobDeferral, JobDefinition, JobRunIn, JobTriggerEvent } from "../engine/types/index.ts";
7
7
  import type { Logger } from "../logging/types.ts";
8
8
  import type { Meter, SerializedTraceContext, Tracer } from "../observability/index.ts";
9
- import type { JobLogEntry, JobSubjectValue } from "./job-runner-types.ts";
9
+ import type { JobInvocation, JobLogEntry, JobSubjectValue } from "./job-runner-types.ts";
10
10
  export declare function isJobTriggerEvent(value: unknown): value is JobTriggerEvent;
11
11
  export declare function stampGatedWriteOrigin(data: Record<string, unknown>): void;
12
12
  export declare function createJobLogger(logs: JobLogEntry[], baseContext?: Record<string, unknown>): Logger;
13
13
  export declare function serializeJobSubject(subject: Readonly<Record<string, JobSubjectValue>>): string;
14
14
  export declare function jobSubjectKey(jobName: string, payload: Record<string, unknown>, fields: readonly string[] | undefined, declaration: "tenantVisibleFailure" | "tenantVisibleRun"): string | null;
15
- export declare function resolveJobTz(config: ConfigAccessor | undefined, context: Pick<AppContext, "geoTzProvider" | "clock">): Promise<TzContext>;
15
+ export declare function resolveJobTz(config: ConfigAccessor | undefined, context: Pick<AppContext, "geoTzProvider" | "clock">, logger: Pick<Logger, "warn">): Promise<TzContext>;
16
16
  export declare function missingDbProxy(jobName: string): TenantDb;
17
17
  export declare function priorAttemptsOf(data: Record<string, unknown>): number;
18
+ export declare function attemptNumberOf(bullJob: JobInvocation): number;
19
+ export declare function isFinalAttempt(bullJob: JobInvocation, jobDef: JobDefinition): boolean;
18
20
  export declare function tenantFailureMessageKey(err: unknown, declaration: JobDefinition["tenantVisibleFailure"]): string | null;
19
21
  export declare const TRACE_CONTEXT_KEY = "_traceContext";
20
22
  export declare const DEFER_COUNT_KEY = "_deferCount";
@@ -42,3 +44,15 @@ export declare function payloadMatchesTriggerFilter(where: Readonly<Record<strin
42
44
  export declare const COMPLETED_JOB_RETENTION_AGE_SEC = 86400;
43
45
  export declare const FAILED_JOB_RETENTION_AGE_SEC = 604800;
44
46
  export declare function maxRetryWindowMs(jobDef: JobDefinition): number;
47
+ export declare function isJobDeferral(value: unknown): value is JobDeferral;
48
+ export declare function priorDeferCountOf(data: Record<string, unknown>, key: string): number;
49
+ export declare function handlerPayloadOf(data: Record<string, unknown>): Record<string, unknown>;
50
+ export declare function tenantIdOfJobData(data: Record<string, unknown>): string;
51
+ export declare function deferDelayMs(deferMs: number, deferCount: number): number;
52
+ export declare function isActiveBefore(other: {
53
+ readonly id?: string | undefined;
54
+ readonly processedOn?: number | undefined;
55
+ }, self: {
56
+ readonly id: string | undefined;
57
+ readonly processedOn: number | undefined;
58
+ }): boolean;
@@ -1,6 +1,7 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
2
  import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
3
3
  import { requestContext } from "../api/request-context.js";
4
+ import { SYSTEM_TENANT_ID } from "../engine/types/index.js";
4
5
  import { InternalError } from "../errors/index.js";
5
6
  import { isKumikoError } from "../errors/kumiko-error.js";
6
7
  import { createFallbackLogger, scrubLogText } from "../logging/index.js";
@@ -66,6 +67,12 @@ export function serializeJobSubject(subject) {
66
67
  .sort()
67
68
  .map((field) => [field, subject[field] ?? null]));
68
69
  }
70
+ function isJobSubjectValue(value) {
71
+ return (value === null ||
72
+ typeof value === "string" ||
73
+ typeof value === "number" ||
74
+ typeof value === "boolean");
75
+ }
69
76
  // Stable identity for the declared payload fields: sorted keys, so two runs
70
77
  // with the same subject values produce the same string and the later one
71
78
  // replaces the earlier record. Non-primitives would serialize into something
@@ -76,15 +83,29 @@ export function jobSubjectKey(jobName, payload, fields, declaration) {
76
83
  const subject = {};
77
84
  for (const field of fields) {
78
85
  const value = payload[field] ?? null;
79
- if (value !== null && typeof value === "object") {
86
+ if (!isJobSubjectValue(value)) {
80
87
  throw new Error(`Job "${jobName}": ${declaration}.subjectFields["${field}"] must be a primitive, got ${typeof value}`);
81
88
  }
82
89
  subject[field] = value;
83
90
  }
84
91
  return serializeJobSubject(subject);
85
92
  }
86
- export async function resolveJobTz(config, context) {
87
- const tenantTz = config ? await config(TENANT_TIMEZONE_CONFIG_KEY) : undefined;
93
+ // A job that never reads ctx.tz must not fail because the tenant config read did.
94
+ async function readTenantTimezoneOrUndefined(config, logger) {
95
+ if (!config)
96
+ return undefined;
97
+ try {
98
+ return await config(TENANT_TIMEZONE_CONFIG_KEY);
99
+ }
100
+ catch (err) {
101
+ logger.warn("tenant timezone config read failed, falling back to UTC", {
102
+ error: scrubLogText(err instanceof Error ? err.message : String(err)),
103
+ });
104
+ return undefined;
105
+ }
106
+ }
107
+ export async function resolveJobTz(config, context, logger) {
108
+ const tenantTz = await readTenantTimezoneOrUndefined(config, logger);
88
109
  return createTzContext({
89
110
  ...(context.geoTzProvider !== undefined && { geoTz: context.geoTzProvider }),
90
111
  ...(context.clock !== undefined && { clock: context.clock }),
@@ -107,6 +128,14 @@ export function priorAttemptsOf(data) {
107
128
  const value = data["_priorAttempts"];
108
129
  return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : 0;
109
130
  }
131
+ export function attemptNumberOf(bullJob) {
132
+ return bullJob.attemptsMade + 1 + priorAttemptsOf(bullJob.data);
133
+ }
134
+ // BullMQ stops retrying once attemptsMade reaches the configured attempts
135
+ // (`retries + 1`), so this is the attempt whose failure is final.
136
+ export function isFinalAttempt(bullJob, jobDef) {
137
+ return bullJob.retryable === false || attemptNumberOf(bullJob) >= (jobDef.retries ?? 0) + 1;
138
+ }
110
139
  // Only a translation key ever travels to the tenant: the thrown error's own
111
140
  // i18nKey when it carries one, otherwise the key declared at the job. The
112
141
  // error message itself can echo provider or payload content and stays on the
@@ -250,7 +279,7 @@ export function payloadMatchesTriggerFilter(where, payload) {
250
279
  // on the children still existing in Redis when a wrapper retry re-derives
251
280
  // their ids. The margin here is generous — queue wait time isn't bounded by
252
281
  // backoff — to cover realistic windows. Audit trail lives in read_job_runs,
253
- // not BullMQ (fw#3199).
282
+ // not BullMQ.
254
283
  export const COMPLETED_JOB_RETENTION_AGE_SEC = 86_400; // 24h
255
284
  export const FAILED_JOB_RETENTION_AGE_SEC = 604_800; // 7d
256
285
  // Total wall-clock time BullMQ can hold a perTenant wrapper across all its
@@ -263,3 +292,39 @@ export function maxRetryWindowMs(jobDef) {
263
292
  const type = typeof jobDef.backoff === "string" ? jobDef.backoff : jobDef.backoff.type;
264
293
  return type === "exponential" ? delayMs * (2 ** jobDef.retries - 1) : jobDef.retries * delayMs;
265
294
  }
295
+ export function isJobDeferral(value) {
296
+ return (typeof value === "object" && value !== null && "kind" in value && value.kind === "job-deferral");
297
+ }
298
+ export function priorDeferCountOf(data, key) {
299
+ const prior = data[key];
300
+ return typeof prior === "number" ? prior : 0;
301
+ }
302
+ export function handlerPayloadOf(data) {
303
+ const payload = {};
304
+ for (const [k, v] of Object.entries(data)) {
305
+ if (!k.startsWith("_"))
306
+ payload[k] = v;
307
+ }
308
+ return payload;
309
+ }
310
+ export function tenantIdOfJobData(data) {
311
+ return (data["_tenantId"] ?? // @cast-boundary dynamic-key
312
+ data["tenantId"] ?? // @cast-boundary dynamic-key
313
+ SYSTEM_TENANT_ID);
314
+ }
315
+ // Growing delay bounds Redis and worker load while one tenant has a large
316
+ // backlog: every deferred job would otherwise be re-checked every deferMs.
317
+ export function deferDelayMs(deferMs, deferCount) {
318
+ const grown = Math.min(deferMs * 2 ** Math.min(deferCount, DEFER_BACKOFF_MAX_DOUBLINGS), DEFER_DELAY_CAP_MS);
319
+ return grown + Math.floor(Math.random() * deferMs);
320
+ }
321
+ export function isActiveBefore(other, self) {
322
+ if (other.id === self.id)
323
+ return false;
324
+ if (self.processedOn === undefined)
325
+ return true;
326
+ const otherProcessedOn = other.processedOn ?? 0;
327
+ if (otherProcessedOn !== self.processedOn)
328
+ return otherProcessedOn < self.processedOn;
329
+ return (other.id ?? "") < (self.id ?? "");
330
+ }
@@ -7,7 +7,7 @@ export declare const ACTIVE_TENANT_IDS_QUERY_NAME = "tenant:query:active-tenant-
7
7
  * BullMQ job ids are `repeat:<schedulerId>:<millis>`. Colons inside the
8
8
  * scheduler id push the segment count to ≥5, which BullMQ's legacy heuristic
9
9
  * treated as old repeatables — spawning a new scheduler entry every tick and
10
- * leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828, fw#1603 /
10
+ * leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828,
11
11
  * publicstatus Redis OOM). Strip `.` and `:` from the job QN.
12
12
  */
13
13
  export declare function schedulerIdForJobName(jobName: string): string;
@@ -19,13 +19,13 @@ export const ACTIVE_TENANT_IDS_QUERY_NAME = "tenant:query:active-tenant-ids";
19
19
  * BullMQ job ids are `repeat:<schedulerId>:<millis>`. Colons inside the
20
20
  * scheduler id push the segment count to ≥5, which BullMQ's legacy heuristic
21
21
  * treated as old repeatables — spawning a new scheduler entry every tick and
22
- * leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828, fw#1603 /
22
+ * leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828,
23
23
  * publicstatus Redis OOM). Strip `.` and `:` from the job QN.
24
24
  */
25
25
  export function schedulerIdForJobName(jobName) {
26
26
  return `scheduler-${jobName.replace(/[.:]/g, "-")}`;
27
27
  }
28
- // Same colon-in-BullMQ-id hazard as schedulerIdForJobName (fw#1603/#1604) —
28
+ // Same colon-in-BullMQ-id hazard as schedulerIdForJobName —
29
29
  // a QN like "publicstatus:job:uptime-probe" must not leave ":" in the id.
30
30
  export function bootJobIdForJobName(jobName) {
31
31
  return `boot-${jobName.replace(/[.:]/g, "-")}`;
@@ -97,8 +97,7 @@ export async function removeOrphanJobSchedulers(queue, expectedSchedulerIds, lan
97
97
  // duplicates — safe only as long as the child job hash outlives the
98
98
  // wrapper's retry window, enforced at construction against
99
99
  // COMPLETED_JOB_RETENTION_AGE_SEC (see the invariant check in
100
- // createJobRunner). Same colon-in-BullMQ-id hazard as schedulerIdForJobName
101
- // (fw#1603/#1604) — the wrapper id already contains ":", so strip
100
+ // createJobRunner). The wrapper id already contains ":", so strip
102
101
  // separators from both halves before joining instead of interpolating raw.
103
102
  export function perTenantChildJobId(wrapperJobId, tenantId) {
104
103
  const sanitize = (value) => value.replace(/[.:]/g, "-");
@@ -0,0 +1,44 @@
1
+ import type { Queue } from "bullmq";
2
+ import type { AppContext, DispatchWriteRef, JobDefinition, JobRunIn, Registry, TenantId } from "../engine/types/index.ts";
3
+ import type { createFallbackLogger } from "../logging/index.ts";
4
+ import type { EscapeHatchProcessDedup, EscapeHatchReportWindow, Tracer } from "../observability/index.ts";
5
+ import type { DistributedLock } from "../pipeline/distributed-lock.ts";
6
+ import type { JobRunner, JobRunnerOptions } from "./job-runner-types.ts";
7
+ export type JobRunnerScope = {
8
+ readonly registry: Registry;
9
+ readonly context: AppContext;
10
+ readonly options: JobRunnerOptions;
11
+ readonly allJobs: ReadonlyMap<string, JobDefinition>;
12
+ readonly queues: Readonly<Record<JobRunIn, Queue>>;
13
+ readonly consumerLane: JobRunIn | undefined;
14
+ readonly tracer: Tracer;
15
+ readonly errorLogger: ReturnType<typeof createFallbackLogger>;
16
+ readonly sequentialLock: DistributedLock | null;
17
+ readonly getActiveTenantIds: (() => Promise<TenantId[]>) | undefined;
18
+ readonly escapeHatchReportWindow: EscapeHatchReportWindow;
19
+ readonly systemCronEscapeHatchDedup: EscapeHatchProcessDedup;
20
+ readonly getSelfRunner: () => JobRunner | undefined;
21
+ readonly getDispatchWriteRef: () => DispatchWriteRef | undefined;
22
+ };
23
+ export declare function laneForJob(def: {
24
+ readonly runIn?: JobRunIn | undefined;
25
+ }): JobRunIn;
26
+ export declare function isOverPerTenantLimit(scope: JobRunnerScope, jobName: string, tenantId: string, max: number, countFilter?: {
27
+ readonly activeOnly?: boolean;
28
+ readonly self?: {
29
+ readonly id: string | undefined;
30
+ readonly processedOn: number | undefined;
31
+ };
32
+ }): Promise<boolean>;
33
+ export declare function announceQueued(scope: JobRunnerScope, jobName: string, jobDef: JobDefinition, queuedJob: {
34
+ readonly id?: string | undefined;
35
+ readonly timestamp: number;
36
+ }, data: Record<string, unknown>): Promise<void>;
37
+ export declare function announceDropped(scope: JobRunnerScope, jobName: string, jobDef: JobDefinition, bullJobId: string, data: Record<string, unknown>): Promise<void>;
38
+ export declare function logFailedAttempt(scope: JobRunnerScope, entry: {
39
+ readonly job: string;
40
+ readonly jobId: string;
41
+ readonly attempt: number;
42
+ readonly finalAttempt: boolean;
43
+ readonly err: unknown;
44
+ }): void;
@@ -0,0 +1,102 @@
1
+ import { UnrecoverableError } from "bullmq";
2
+ import { SYSTEM_TENANT_ID } from "../engine/types/index.js";
3
+ import { scrubLogText } from "../logging/index.js";
4
+ import { handlerPayloadOf, isActiveBefore, jobSubjectKey, tenantIdOfJobData, } from "./job-runner-helpers.js";
5
+ // Resolve the lane for a job — "worker" is the default because that's the
6
+ // sensible prod lane (heavy async off the request path). Jobs that opted
7
+ // into "api" must have been validated at registry boot already.
8
+ export function laneForJob(def) {
9
+ return def.runIn ?? "worker";
10
+ }
11
+ // Counts active + waiting jobs with this name for this tenant across
12
+ // BOTH lane queues. Jobs with the same name should only live in one
13
+ // lane (jobDef.runIn is static), but walking both is cheap and avoids
14
+ // a subtle bug if someone ever reassigns a job to a different lane
15
+ // between deploys while old queue contents are still draining.
16
+ // `activeOnly` + `self` serve the defer check, which runs inside the job being
17
+ // counted: it must not see waiting jobs or itself, and only counts jobs that
18
+ // became active before it. Without that order, workers that fetch a burst of
19
+ // jobs together all see each other as active and all defer, again and again.
20
+ export async function isOverPerTenantLimit(scope, jobName, tenantId, max, countFilter = {}) {
21
+ const { queues } = scope;
22
+ const results = await Promise.all([
23
+ queues.api.getActive(),
24
+ queues.worker.getActive(),
25
+ ...(countFilter.activeOnly ? [] : [queues.api.getWaiting(), queues.worker.getWaiting()]),
26
+ ]);
27
+ let count = 0;
28
+ for (const list of results) {
29
+ for (const j of list) {
30
+ if (j.name !== jobName)
31
+ continue;
32
+ if (countFilter.self && !isActiveBefore(j, countFilter.self))
33
+ continue;
34
+ const t = j.data?._tenantId; // @cast-boundary dynamic-key
35
+ if (t === tenantId) {
36
+ count += 1;
37
+ if (count >= max)
38
+ return true;
39
+ }
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+ // Tenant-visible queued state. A callback or subject problem must never fail
45
+ // the enqueue; a bad subject config surfaces as a failed run instead.
46
+ export async function announceQueued(scope, jobName, jobDef, queuedJob, data) {
47
+ const { options } = scope;
48
+ const bullJobId = queuedJob.id;
49
+ const declaration = jobDef.tenantVisibleRun;
50
+ // skip: job did not opt in, no logger wired, or BullMQ returned no id
51
+ if (!declaration || !options.onJobQueued || bullJobId === undefined)
52
+ return;
53
+ const tenantId = tenantIdOfJobData(data);
54
+ // skip: tenant-less run — no tenant-scoped query could ever read it
55
+ if (tenantId === SYSTEM_TENANT_ID)
56
+ return;
57
+ try {
58
+ const subject = jobSubjectKey(jobName, handlerPayloadOf(data), declaration.subjectFields, "tenantVisibleRun");
59
+ await options.onJobQueued(jobName, bullJobId, {
60
+ tenantId,
61
+ subject,
62
+ jobCreatedAtMs: queuedJob.timestamp,
63
+ });
64
+ }
65
+ catch (err) {
66
+ scope.errorLogger.warn("tenant-visible queued state not recorded", {
67
+ job: jobName,
68
+ error: err instanceof Error ? err.message : String(err),
69
+ });
70
+ }
71
+ }
72
+ export async function announceDropped(scope, jobName, jobDef, bullJobId, data) {
73
+ const { options } = scope;
74
+ // skip: job did not opt in or no logger wired
75
+ if (!jobDef.tenantVisibleRun || !options.onJobDropped)
76
+ return;
77
+ const tenantId = tenantIdOfJobData(data);
78
+ // skip: tenant-less run — announceQueued never recorded a row for it
79
+ if (tenantId === SYSTEM_TENANT_ID)
80
+ return;
81
+ try {
82
+ await options.onJobDropped(jobName, bullJobId, { tenantId });
83
+ }
84
+ catch (err) {
85
+ scope.errorLogger.warn("tenant-visible queued state not cleared", {
86
+ job: jobName,
87
+ error: err instanceof Error ? err.message : String(err),
88
+ });
89
+ }
90
+ }
91
+ // Only err.message, never payload or the logs array: job payloads can carry PII.
92
+ export function logFailedAttempt(scope, entry) {
93
+ scope.errorLogger.error("job failed", {
94
+ job: entry.job,
95
+ jobId: entry.jobId,
96
+ attempt: entry.attempt,
97
+ // BullMQ never retries an UnrecoverableError, whatever attempts remain.
98
+ final: entry.finalAttempt || entry.err instanceof UnrecoverableError,
99
+ error: scrubLogText(entry.err instanceof Error ? entry.err.message : String(entry.err)),
100
+ ...(entry.err instanceof Error && entry.err.stack && { stack: scrubLogText(entry.err.stack) }),
101
+ });
102
+ }
@@ -19,6 +19,7 @@ export type JobMeta = {
19
19
  export type JobQueuedMeta = {
20
20
  readonly tenantId: string;
21
21
  readonly subject: string | null;
22
+ readonly jobCreatedAtMs: number;
22
23
  };
23
24
  export type JobDroppedMeta = {
24
25
  readonly tenantId: string;
@@ -63,3 +64,14 @@ export type JobRunnerOptions = {
63
64
  onJobQueued?: (jobName: string, jobId: string, meta: JobQueuedMeta) => void;
64
65
  onJobDropped?: (jobName: string, jobId: string, meta: JobDroppedMeta) => void;
65
66
  };
67
+ export type JobInvocation = {
68
+ readonly id?: string | undefined;
69
+ readonly name: string;
70
+ readonly data: Record<string, unknown>;
71
+ readonly attemptsMade: number;
72
+ /** False for inline invocations (boot gates): nothing retries a throw. */
73
+ readonly retryable?: boolean;
74
+ readonly processedOn?: number | undefined;
75
+ moveToDelayed?(timestamp: number, token?: string): Promise<void>;
76
+ updateData?(data: Record<string, unknown>): Promise<void>;
77
+ };
@@ -1,4 +1,3 @@
1
- /// <reference types="temporal-polyfill/global" preserve="true" />
2
1
  import type { JobRunner, JobRunnerOptions } from "./job-runner-types.ts";
3
2
  export { serializeJobSubject } from "./job-runner-helpers.ts";
4
3
  export { bootJobIdForJobName, ORPHAN_SCHEDULER_GRACE_MS, schedulerIdForJobName, } from "./job-runner-schedulers.ts";