@cosmicdrift/kumiko-framework 1.0.0 → 2.0.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 (424) hide show
  1. package/README.md +9 -38
  2. package/package.json +21 -2
  3. package/src/__tests__/anonymous-access.integration.test.ts +185 -0
  4. package/src/__tests__/consumer-cli.integration.test.ts +172 -0
  5. package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
  6. package/src/__tests__/full-stack.integration.test.ts +1 -1
  7. package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
  8. package/src/__tests__/schema-cli.integration.test.ts +50 -0
  9. package/src/__tests__/store-table.integration.test.ts +93 -0
  10. package/src/api/__tests__/api.test.ts +373 -0
  11. package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
  13. package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
  14. package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
  15. package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
  16. package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
  17. package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
  18. package/src/api/__tests__/batch.integration.test.ts +101 -11
  19. package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
  20. package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
  21. package/src/api/__tests__/jwt.test.ts +200 -1
  22. package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
  23. package/src/api/__tests__/pat-scope.test.ts +36 -0
  24. package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
  25. package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
  26. package/src/api/__tests__/request-id-middleware.test.ts +51 -0
  27. package/src/api/__tests__/server-boot-guards.test.ts +71 -0
  28. package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
  29. package/src/api/__tests__/sse-broker.test.ts +57 -0
  30. package/src/api/__tests__/sse-route.test.ts +4 -0
  31. package/src/api/api-constants.ts +11 -0
  32. package/src/api/auth-middleware.ts +248 -40
  33. package/src/api/auth-routes.ts +576 -95
  34. package/src/api/index.ts +13 -4
  35. package/src/api/jwt.ts +170 -11
  36. package/src/api/pat-scope.ts +14 -0
  37. package/src/api/pii-leak-guard.ts +47 -0
  38. package/src/api/request-context.ts +3 -0
  39. package/src/api/request-id-middleware.ts +2 -0
  40. package/src/api/routes.ts +169 -2
  41. package/src/api/server.ts +112 -16
  42. package/src/api/sse-broker.ts +39 -0
  43. package/src/bun-db/__tests__/PATTERN.md +0 -1
  44. package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
  45. package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
  46. package/src/bun-db/__tests__/write-brand.test.ts +21 -0
  47. package/src/bun-db/connection.ts +3 -3
  48. package/src/bun-db/index.ts +2 -0
  49. package/src/bun-db/query.ts +105 -32
  50. package/src/consumer-cli.ts +134 -0
  51. package/src/crypto/__tests__/blind-index.test.ts +130 -0
  52. package/src/crypto/__tests__/event-pii.test.ts +161 -0
  53. package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
  54. package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
  55. package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
  56. package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
  57. package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
  58. package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
  59. package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
  60. package/src/crypto/blind-index.ts +118 -0
  61. package/src/crypto/event-pii.ts +70 -0
  62. package/src/crypto/in-memory-kms-adapter.ts +50 -0
  63. package/src/crypto/index.ts +67 -0
  64. package/src/crypto/kms-adapter.ts +2 -0
  65. package/src/crypto/pg-kms-adapter.ts +295 -0
  66. package/src/crypto/pii-field-encryption.ts +248 -0
  67. package/src/crypto/request-kms-cache.ts +38 -0
  68. package/src/crypto/subject-resolver.ts +91 -0
  69. package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
  70. package/src/db/__tests__/blind-index.integration.test.ts +248 -0
  71. package/src/db/__tests__/build-filter-where.test.ts +34 -0
  72. package/src/db/__tests__/collect-table-metas.test.ts +10 -10
  73. package/src/db/__tests__/config-seed.integration.test.ts +13 -5
  74. package/src/db/__tests__/dialect-instant.test.ts +1 -4
  75. package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
  76. package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
  77. package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
  78. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
  79. package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
  80. package/src/db/__tests__/feature-table-sources.test.ts +34 -0
  81. package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
  82. package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
  83. package/src/db/__tests__/located-timestamp.test.ts +19 -0
  84. package/src/db/__tests__/migrate-generator.test.ts +21 -0
  85. package/src/db/__tests__/migrate-runner.test.ts +61 -0
  86. package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
  87. package/src/db/__tests__/rebuild-marker.test.ts +13 -3
  88. package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
  89. package/src/db/__tests__/schema-inspection.test.ts +7 -0
  90. package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
  91. package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
  92. package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
  93. package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
  94. package/src/db/api.ts +2 -2
  95. package/src/db/apply-entity-event.ts +18 -14
  96. package/src/db/blind-index-cleanup.ts +55 -0
  97. package/src/db/bun-provider.ts +2 -2
  98. package/src/db/collect-table-metas.ts +6 -7
  99. package/src/db/config-seed.ts +9 -9
  100. package/src/db/connection.ts +7 -12
  101. package/src/db/cursor.ts +1 -18
  102. package/src/db/dialect.ts +20 -26
  103. package/src/db/entity-field-encryption.ts +81 -24
  104. package/src/db/entity-table-meta-types.ts +2 -0
  105. package/src/db/entity-table-meta.ts +63 -90
  106. package/src/db/event-store-executor-context.ts +404 -0
  107. package/src/db/event-store-executor-read.ts +275 -0
  108. package/src/db/event-store-executor-write.ts +644 -0
  109. package/src/db/event-store-executor.ts +28 -1155
  110. package/src/db/feature-table-sources.ts +6 -5
  111. package/src/db/index.ts +20 -3
  112. package/src/db/located-timestamp.ts +4 -0
  113. package/src/db/migrate-generator.ts +39 -6
  114. package/src/db/migrate-runner.ts +107 -11
  115. package/src/db/pg-error.ts +9 -1
  116. package/src/db/postgres-provider.ts +2 -2
  117. package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
  118. package/src/db/queries/backfill-pii.ts +277 -0
  119. package/src/db/queries/ddl.ts +45 -0
  120. package/src/db/queries/event-consumer.ts +35 -2
  121. package/src/db/queries/event-store.ts +126 -19
  122. package/src/db/queries/projection-rebuild.ts +22 -8
  123. package/src/db/queries/shadow-swap.ts +183 -0
  124. package/src/db/queries/test-stack.ts +4 -30
  125. package/src/db/query-api.ts +1 -0
  126. package/src/db/query.ts +1 -0
  127. package/src/db/rebuild-marker.ts +18 -2
  128. package/src/db/reference-data.ts +2 -3
  129. package/src/db/replay-migration-sql.ts +257 -0
  130. package/src/db/schema-inspection.ts +1 -1
  131. package/src/db/table-builder.ts +102 -71
  132. package/src/db/tenant-db.ts +15 -53
  133. package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
  134. package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
  135. package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
  136. package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
  137. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
  138. package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
  139. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
  140. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
  141. package/src/engine/__tests__/boot-validator.test.ts +219 -15
  142. package/src/engine/__tests__/build-app-schema.test.ts +82 -0
  143. package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
  144. package/src/engine/__tests__/build-target.test.ts +3 -11
  145. package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
  146. package/src/engine/__tests__/config-helpers.test.ts +16 -0
  147. package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
  148. package/src/engine/__tests__/define-roles.test.ts +21 -0
  149. package/src/engine/__tests__/engine.test.ts +163 -1
  150. package/src/engine/__tests__/entity-handlers.test.ts +80 -0
  151. package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
  152. package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
  153. package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
  154. package/src/engine/__tests__/factories-time.test.ts +2 -66
  155. package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
  156. package/src/engine/__tests__/feature-manifest.test.ts +31 -1
  157. package/src/engine/__tests__/field-access.test.ts +23 -1
  158. package/src/engine/__tests__/hook-phases.test.ts +5 -5
  159. package/src/engine/__tests__/membership-roles.test.ts +4 -10
  160. package/src/engine/__tests__/nav.test.ts +86 -1
  161. package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
  162. package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
  163. package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
  164. package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
  165. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
  166. package/src/engine/__tests__/post-query-hook.test.ts +7 -7
  167. package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
  168. package/src/engine/__tests__/registry.test.ts +144 -0
  169. package/src/engine/__tests__/schema-builder.test.ts +18 -0
  170. package/src/engine/__tests__/screen.test.ts +147 -1
  171. package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
  172. package/src/engine/__tests__/store-table.test.ts +229 -0
  173. package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
  174. package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
  175. package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
  176. package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
  177. package/src/engine/boot-validator/action-wiring.ts +149 -0
  178. package/src/engine/boot-validator/boot-check.ts +21 -0
  179. package/src/engine/boot-validator/config-deps.ts +40 -4
  180. package/src/engine/boot-validator/entity-handler.ts +20 -21
  181. package/src/engine/boot-validator/entity-list-screens.ts +88 -0
  182. package/src/engine/boot-validator/gdpr-storage.ts +0 -20
  183. package/src/engine/boot-validator/i18n-keys.ts +58 -4
  184. package/src/engine/boot-validator/index.ts +33 -12
  185. package/src/engine/boot-validator/nav.ts +125 -0
  186. package/src/engine/boot-validator/pii-retention.ts +95 -10
  187. package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
  188. package/src/engine/boot-validator/workspaces.ts +68 -0
  189. package/src/engine/build-app-schema.ts +16 -0
  190. package/src/engine/build-config-feature-schema.ts +44 -7
  191. package/src/engine/codemod/pipeline-codemod.ts +5 -5
  192. package/src/engine/config-helpers.ts +15 -0
  193. package/src/engine/constants.ts +32 -6
  194. package/src/engine/create-app.ts +11 -0
  195. package/src/engine/define-feature.ts +95 -947
  196. package/src/engine/define-handler.ts +28 -94
  197. package/src/engine/define-workflow.ts +1 -1
  198. package/src/engine/effective-features.ts +12 -2
  199. package/src/engine/entity-handlers.ts +76 -11
  200. package/src/engine/extensions/tenant-data.ts +19 -0
  201. package/src/engine/extensions/user-data.ts +29 -2
  202. package/src/engine/factories.ts +8 -49
  203. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
  204. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
  205. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
  206. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
  207. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
  208. package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
  209. package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
  210. package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
  211. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
  212. package/src/engine/feature-ast/extractors/events.ts +322 -0
  213. package/src/engine/feature-ast/extractors/handlers.ts +234 -0
  214. package/src/engine/feature-ast/extractors/hooks.ts +243 -0
  215. package/src/engine/feature-ast/extractors/index.ts +34 -31
  216. package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
  217. package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
  218. package/src/engine/feature-ast/extractors/round1.ts +3 -3
  219. package/src/engine/feature-ast/extractors/round5.ts +3 -3
  220. package/src/engine/feature-ast/extractors/round6.ts +2 -36
  221. package/src/engine/feature-ast/extractors/shared.ts +64 -6
  222. package/src/engine/feature-ast/index.ts +2 -4
  223. package/src/engine/feature-ast/parse.ts +130 -23
  224. package/src/engine/feature-ast/patch.ts +39 -50
  225. package/src/engine/feature-ast/patcher.ts +37 -38
  226. package/src/engine/feature-ast/patterns.ts +50 -62
  227. package/src/engine/feature-ast/render.ts +67 -44
  228. package/src/engine/feature-builder-state.ts +168 -0
  229. package/src/engine/feature-config-events-jobs.ts +403 -0
  230. package/src/engine/feature-entity-handlers.ts +208 -0
  231. package/src/engine/feature-manifest.ts +2 -1
  232. package/src/engine/feature-ui-extensions.ts +500 -0
  233. package/src/engine/field-access.ts +13 -2
  234. package/src/engine/field-helpers.ts +31 -0
  235. package/src/engine/handler-helpers.ts +26 -0
  236. package/src/engine/hook-helpers.ts +16 -0
  237. package/src/engine/index.ts +23 -7
  238. package/src/engine/membership-roles.ts +13 -0
  239. package/src/engine/object-form.ts +27 -0
  240. package/src/engine/ownership.ts +26 -79
  241. package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
  242. package/src/engine/pattern-library/library.ts +44 -1152
  243. package/src/engine/pattern-library/mixed-schemas.ts +450 -0
  244. package/src/engine/pattern-library/opaque-schemas.ts +124 -0
  245. package/src/engine/pattern-library/shared-fields.ts +75 -0
  246. package/src/engine/pattern-library/static-schemas.ts +456 -0
  247. package/src/engine/pipeline.ts +6 -11
  248. package/src/engine/registry-facade.ts +362 -0
  249. package/src/engine/registry-ingest.ts +478 -0
  250. package/src/engine/registry-state.ts +388 -0
  251. package/src/engine/registry-validate.ts +642 -0
  252. package/src/engine/registry.ts +78 -1658
  253. package/src/engine/run-pipeline.ts +1 -1
  254. package/src/engine/schema-builder.ts +1 -0
  255. package/src/engine/screen-helpers.ts +54 -0
  256. package/src/engine/soft-delete-cleanup.ts +6 -2
  257. package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
  258. package/src/engine/steps/_duration-utils.ts +2 -0
  259. package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
  260. package/src/engine/tier-resolver-extension.ts +3 -2
  261. package/src/engine/types/config.ts +2 -482
  262. package/src/engine/types/define-handler.ts +2 -0
  263. package/src/engine/types/entity-handlers.ts +2 -0
  264. package/src/engine/types/event-type-map.ts +1 -37
  265. package/src/engine/types/feature.ts +2 -972
  266. package/src/engine/types/fields.ts +2 -675
  267. package/src/engine/types/handlers.ts +2 -774
  268. package/src/engine/types/hooks.ts +2 -184
  269. package/src/engine/types/http-route.ts +1 -54
  270. package/src/engine/types/identifiers.ts +1 -47
  271. package/src/engine/types/index.ts +86 -40
  272. package/src/engine/types/nav.ts +2 -63
  273. package/src/engine/types/ownership.ts +2 -0
  274. package/src/engine/types/projection.ts +2 -138
  275. package/src/engine/types/relations.ts +1 -51
  276. package/src/engine/types/screen.ts +2 -574
  277. package/src/engine/types/step.ts +2 -334
  278. package/src/engine/types/target-ref.ts +1 -21
  279. package/src/engine/types/tree-node.ts +1 -132
  280. package/src/engine/types/workspace.ts +2 -49
  281. package/src/engine/validate-projection-allowlist.ts +6 -6
  282. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
  283. package/src/entrypoint/index.ts +49 -6
  284. package/src/errors/classes.ts +22 -1
  285. package/src/errors/field-issue.ts +0 -3
  286. package/src/errors/index.ts +1 -1
  287. package/src/errors/write-error-info.ts +10 -22
  288. package/src/es-ops/README.md +1 -1
  289. package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
  290. package/src/es-ops/context.ts +4 -2
  291. package/src/es-ops/types.ts +8 -1
  292. package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
  293. package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
  294. package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
  295. package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
  296. package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
  297. package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
  298. package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
  299. package/src/event-store/admin-api.ts +11 -4
  300. package/src/event-store/errors.ts +2 -35
  301. package/src/event-store/event-store.ts +64 -78
  302. package/src/event-store/events-schema.ts +11 -13
  303. package/src/event-store/index.ts +18 -3
  304. package/src/event-store/rebuild-dead-letter.ts +111 -0
  305. package/src/event-store/snapshot.ts +63 -40
  306. package/src/event-store/types.ts +2 -0
  307. package/src/files/__tests__/build-storage-key.test.ts +28 -0
  308. package/src/files/__tests__/file-ref-entity.test.ts +8 -0
  309. package/src/files/__tests__/files.integration.test.ts +24 -0
  310. package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
  311. package/src/files/__tests__/local-provider.test.ts +31 -0
  312. package/src/files/__tests__/provider-resolver.test.ts +70 -0
  313. package/src/files/__tests__/write-stream.test.ts +13 -0
  314. package/src/files/file-handle.ts +2 -19
  315. package/src/files/file-ref-entity.ts +2 -2
  316. package/src/files/file-routes.ts +13 -0
  317. package/src/files/index.ts +1 -1
  318. package/src/files/local-provider.ts +27 -8
  319. package/src/files/provider-resolver.ts +31 -20
  320. package/src/files/types.ts +13 -55
  321. package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
  322. package/src/i18n/required-surface-keys.ts +120 -1
  323. package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
  324. package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
  325. package/src/jobs/job-runner.ts +82 -12
  326. package/src/logging/types.ts +1 -7
  327. package/src/migrations/pending-rebuilds.ts +1 -1
  328. package/src/observability/__tests__/observability.integration.test.ts +4 -1
  329. package/src/observability/__tests__/recording-tracer.test.ts +6 -4
  330. package/src/observability/index.ts +2 -0
  331. package/src/observability/noop-provider.ts +0 -9
  332. package/src/observability/recording-tracer.ts +0 -12
  333. package/src/observability/standard-metrics.ts +64 -2
  334. package/src/observability/types/index.ts +1 -29
  335. package/src/observability/types/metric.ts +1 -56
  336. package/src/observability/types/provider.ts +1 -32
  337. package/src/observability/types/span.ts +1 -64
  338. package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
  339. package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
  340. package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
  341. package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
  342. package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
  343. package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
  344. package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
  345. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
  346. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
  347. package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
  348. package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
  349. package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
  350. package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
  351. package/src/pipeline/dispatch-batch.ts +187 -0
  352. package/src/pipeline/dispatch-query.ts +165 -0
  353. package/src/pipeline/dispatch-shared.ts +826 -0
  354. package/src/pipeline/dispatch-stream.ts +90 -0
  355. package/src/pipeline/dispatch-write.ts +451 -0
  356. package/src/pipeline/dispatcher-utils.ts +1 -1
  357. package/src/pipeline/dispatcher.ts +44 -1372
  358. package/src/pipeline/entity-cache.ts +2 -33
  359. package/src/pipeline/event-consumer-state.ts +30 -2
  360. package/src/pipeline/event-dispatcher-admin.ts +293 -0
  361. package/src/pipeline/event-dispatcher-delivery.ts +305 -0
  362. package/src/pipeline/event-dispatcher.ts +85 -542
  363. package/src/pipeline/index.ts +2 -0
  364. package/src/pipeline/msp-rebuild.ts +42 -3
  365. package/src/pipeline/multi-stream-apply-context.ts +4 -42
  366. package/src/pipeline/projection-rebuild.ts +105 -3
  367. package/src/pipeline/system-hooks.ts +144 -1
  368. package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
  369. package/src/rate-limit/resolver.ts +16 -32
  370. package/src/schema-cli.ts +76 -16
  371. package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
  372. package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
  373. package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
  374. package/src/search/index.ts +6 -0
  375. package/src/search/meilisearch-adapter.ts +13 -12
  376. package/src/search/reindex-entity.ts +177 -0
  377. package/src/search/types.ts +1 -39
  378. package/src/secrets/__tests__/contains-secret.test.ts +34 -0
  379. package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
  380. package/src/secrets/__tests__/envelope.test.ts +1 -1
  381. package/src/secrets/dek-cache.ts +26 -5
  382. package/src/secrets/envelope-cipher.ts +53 -0
  383. package/src/secrets/envelope.ts +5 -3
  384. package/src/secrets/index.ts +13 -1
  385. package/src/secrets/stored-envelope.ts +46 -0
  386. package/src/secrets/types.ts +2 -162
  387. package/src/stack/__tests__/event-collector.test.ts +42 -0
  388. package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
  389. package/src/stack/db.ts +2 -1
  390. package/src/stack/push-entity-projection-tables.ts +4 -3
  391. package/src/stack/redis.ts +8 -0
  392. package/src/stack/request-helper.ts +38 -2
  393. package/src/stack/table-helpers.ts +6 -4
  394. package/src/stack/test-stack.ts +249 -131
  395. package/src/testing/__tests__/late-bound.test.ts +32 -0
  396. package/src/testing/__tests__/wait-for.test.ts +59 -0
  397. package/src/testing/boot-validator-fixture.ts +121 -0
  398. package/src/testing/e2e-generator.ts +8 -0
  399. package/src/testing/file-provider-contract.ts +104 -0
  400. package/src/testing/handler-context.ts +3 -1
  401. package/src/testing/index.ts +5 -0
  402. package/src/testing/late-bound.ts +5 -3
  403. package/src/testing/mutable-master-key-provider.ts +29 -3
  404. package/src/testing/wait-for.ts +3 -0
  405. package/src/testing/without-ambient-temporal.ts +14 -0
  406. package/src/time/__tests__/tz-dateline.test.ts +8 -10
  407. package/src/time/geo-tz.ts +1 -32
  408. package/src/time/index.ts +1 -0
  409. package/src/time/legacy-date.ts +18 -0
  410. package/src/time/polyfill.ts +21 -38
  411. package/src/time/tz-context.ts +44 -85
  412. package/src/ui-types/app-schema.ts +12 -0
  413. package/src/ui-types/index.ts +20 -7
  414. package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
  415. package/src/utils/safe-json.ts +13 -1
  416. package/src/__tests__/raw-table.integration.test.ts +0 -116
  417. package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
  418. package/src/db/__tests__/encryption.test.ts +0 -39
  419. package/src/db/encryption.ts +0 -39
  420. package/src/db/row-helpers.ts +0 -4
  421. package/src/engine/__tests__/raw-table.test.ts +0 -150
  422. package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
  423. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
  424. package/src/engine/feature-ast/extractors/round4.ts +0 -1366
@@ -0,0 +1,53 @@
1
+ // String-in/string-out envelope encryption for TEXT-column stores (config
2
+ // values, entity fields). The stored string is JSON of StoredEnvelope; the
3
+ // kekVersion inside makes every value rotatable via the MasterKeyProvider
4
+ // keyring.
5
+
6
+ import type { EnvelopeCipher } from "@cosmicdrift/kumiko-types/envelope-cipher-types";
7
+ import { InternalError } from "../errors/classes";
8
+ import type { DekCache } from "./dek-cache";
9
+ import { createDekCache, withDekCache } from "./dek-cache";
10
+ import { decryptValue, encryptValue } from "./envelope";
11
+ import { decodeStoredEnvelope, encodeStoredEnvelope, isStoredEnvelope } from "./stored-envelope";
12
+ import type { MasterKeyProvider } from "./types";
13
+
14
+ export type { EnvelopeCipher } from "@cosmicdrift/kumiko-types/envelope-cipher-types";
15
+
16
+ export type EnvelopeCipherOptions = {
17
+ // Shared DEK cache — pass the app-wide instance so config/entity reads
18
+ // amortise KEK unwraps together with ctx.secrets.
19
+ readonly dekCache?: DekCache;
20
+ };
21
+
22
+ export function createEnvelopeCipher(
23
+ provider: MasterKeyProvider,
24
+ opts: EnvelopeCipherOptions = {},
25
+ ): EnvelopeCipher {
26
+ const cached = withDekCache(provider, opts.dekCache ?? createDekCache());
27
+
28
+ return {
29
+ async encrypt(plaintext, scope) {
30
+ const envelope = await encryptValue(plaintext, provider, scope);
31
+ return JSON.stringify(encodeStoredEnvelope(envelope));
32
+ },
33
+
34
+ async decrypt(stored, scope) {
35
+ let parsed: unknown;
36
+ try {
37
+ parsed = JSON.parse(stored);
38
+ } catch {
39
+ throw new InternalError({
40
+ message: "[envelope-cipher] stored value is not valid JSON",
41
+ i18nKey: "secrets.errors.envelope_malformed",
42
+ });
43
+ }
44
+ if (!isStoredEnvelope(parsed)) {
45
+ throw new InternalError({
46
+ message: "[envelope-cipher] stored JSON is not a StoredEnvelope",
47
+ i18nKey: "secrets.errors.envelope_malformed",
48
+ });
49
+ }
50
+ return decryptValue(decodeStoredEnvelope(parsed), cached, scope);
51
+ },
52
+ };
53
+ }
@@ -3,7 +3,7 @@
3
3
  // carries everything needed to decrypt later.
4
4
 
5
5
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
6
- import type { Envelope, MasterKeyProvider } from "./types";
6
+ import type { Envelope, KeyScope, MasterKeyProvider } from "./types";
7
7
 
8
8
  const ALGORITHM = "aes-256-gcm";
9
9
  const DEK_LENGTH = 32; // AES-256
@@ -14,6 +14,7 @@ const IV_LENGTH = 12; // GCM standard nonce length
14
14
  export async function encryptValue(
15
15
  plaintext: string,
16
16
  provider: MasterKeyProvider,
17
+ scope?: KeyScope,
17
18
  ): Promise<Envelope> {
18
19
  // Fresh DEK per value. Reusing DEKs across rows would break forward
19
20
  // secrecy (one compromised ciphertext-IV pair leaks information about
@@ -26,7 +27,7 @@ export async function encryptValue(
26
27
  const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
27
28
  const authTag = cipher.getAuthTag();
28
29
 
29
- const { encryptedDek, kekVersion } = await provider.wrapDek(dek);
30
+ const { encryptedDek, kekVersion } = await provider.wrapDek(dek, scope);
30
31
  return { ciphertext, iv, authTag, encryptedDek, kekVersion };
31
32
  } finally {
32
33
  // Zero the DEK on best effort regardless of success — if provider.wrapDek
@@ -42,8 +43,9 @@ export async function encryptValue(
42
43
  export async function decryptValue(
43
44
  envelope: Envelope,
44
45
  provider: MasterKeyProvider,
46
+ scope?: KeyScope,
45
47
  ): Promise<string> {
46
- const dek = await provider.unwrapDek(envelope.encryptedDek, envelope.kekVersion);
48
+ const dek = await provider.unwrapDek(envelope.encryptedDek, envelope.kekVersion, scope);
47
49
  try {
48
50
  const decipher = createDecipheriv(ALGORITHM, dek, envelope.iv);
49
51
  decipher.setAuthTag(envelope.authTag);
@@ -1,17 +1,29 @@
1
- export { createDekCache, type DekCache, type DekCacheOptions } from "./dek-cache";
1
+ export { createDekCache, type DekCache, type DekCacheOptions, withDekCache } from "./dek-cache";
2
2
  export {
3
3
  createEnvMasterKeyProvider,
4
4
  type EnvMasterKeyProviderOptions,
5
5
  type Keyring,
6
6
  } from "./env-master-key-provider";
7
7
  export { decryptValue, encryptValue } from "./envelope";
8
+ export {
9
+ createEnvelopeCipher,
10
+ type EnvelopeCipher,
11
+ type EnvelopeCipherOptions,
12
+ } from "./envelope-cipher";
8
13
  export { assertNoSecretLeak } from "./leak-guard";
9
14
  export { rewrapDek } from "./rotation";
15
+ export {
16
+ decodeStoredEnvelope,
17
+ encodeStoredEnvelope,
18
+ isStoredEnvelope,
19
+ type StoredEnvelope,
20
+ } from "./stored-envelope";
10
21
  export {
11
22
  type ContainsSecret,
12
23
  createSecret,
13
24
  type Envelope,
14
25
  isSecret,
26
+ type KeyScope,
15
27
  type MasterKeyProvider,
16
28
  type Secret,
17
29
  type SecretAuditContext,
@@ -0,0 +1,46 @@
1
+ // Canonical at-rest form of an Envelope: every Buffer as base64, ready for
2
+ // jsonb columns (secrets feature) or JSON-stringified TEXT columns
3
+ // (EnvelopeCipher for config values / entity fields). One wire shape for
4
+ // every envelope-encrypted store in the framework.
5
+
6
+ import type { Envelope } from "./types";
7
+
8
+ export type StoredEnvelope = {
9
+ readonly ciphertext: string; // base64
10
+ readonly iv: string; // base64
11
+ readonly authTag: string; // base64
12
+ readonly encryptedDek: string; // base64
13
+ readonly kekVersion: number;
14
+ };
15
+
16
+ export function encodeStoredEnvelope(envelope: Envelope): StoredEnvelope {
17
+ return {
18
+ ciphertext: envelope.ciphertext.toString("base64"),
19
+ iv: envelope.iv.toString("base64"),
20
+ authTag: envelope.authTag.toString("base64"),
21
+ encryptedDek: envelope.encryptedDek.toString("base64"),
22
+ kekVersion: envelope.kekVersion,
23
+ };
24
+ }
25
+
26
+ export function decodeStoredEnvelope(stored: StoredEnvelope): Envelope {
27
+ return {
28
+ ciphertext: Buffer.from(stored.ciphertext, "base64"),
29
+ iv: Buffer.from(stored.iv, "base64"),
30
+ authTag: Buffer.from(stored.authTag, "base64"),
31
+ encryptedDek: Buffer.from(stored.encryptedDek, "base64"),
32
+ kekVersion: stored.kekVersion,
33
+ };
34
+ }
35
+
36
+ export function isStoredEnvelope(value: unknown): value is StoredEnvelope {
37
+ if (!value || typeof value !== "object") return false;
38
+ const v = value as Record<string, unknown>; // @cast-boundary parse-boundary after typeof check
39
+ return (
40
+ typeof v["ciphertext"] === "string" &&
41
+ typeof v["iv"] === "string" &&
42
+ typeof v["authTag"] === "string" &&
43
+ typeof v["encryptedDek"] === "string" &&
44
+ typeof v["kekVersion"] === "number"
45
+ );
46
+ }
@@ -1,162 +1,2 @@
1
- // Envelope Encryption types. Separating DEK (per-value) from KEK (central)
2
- // is what makes key rotation cheap: on rotation we only re-wrap the small
3
- // encryptedDek, never touch the ciphertext.
4
-
5
- import type { TenantId } from "../engine";
6
-
7
- // Plaintext-secret wrapper (branded). Carries the actual string internally
8
- // but the nominal typing stops it from landing in an HTTP response by
9
- // accident — a response-serializer guard + the reveal() cost make the leak
10
- // intentional. Framework code that sees `Secret<string>` knows the caller
11
- // has already gone through the audited ctx.secrets.get path.
12
- //
13
- // The brand is a real (non-registered) Symbol so it exists at runtime for
14
- // isSecret() without clashing with user-land symbols of the same name.
15
- const SecretBrand: unique symbol = Symbol("kumiko.secret");
16
-
17
- export type Secret<T = string> = {
18
- readonly [SecretBrand]: true;
19
- readonly reveal: () => T;
20
- };
21
-
22
- // Implementation helper — bundled-features uses this to wrap a plaintext after
23
- // decryption. Kept in the framework so both sides share one canonical brand.
24
- export function createSecret<T>(value: T): Secret<T> {
25
- return {
26
- [SecretBrand]: true as const,
27
- reveal: () => value,
28
- };
29
- }
30
-
31
- // True for any object carrying the Secret brand. Used by the response guard
32
- // to reject leaks before serialization.
33
- export function isSecret(v: unknown): v is Secret<unknown> {
34
- return typeof v === "object" && v !== null && SecretBrand in v;
35
- }
36
-
37
- // --- Compile-time response guard (R6) --------------------------------------
38
- //
39
- // ContainsSecret<T> is `true` only when a Secret<> is DEFINITELY present
40
- // somewhere in T. The handler-registration guard (defineWriteHandler/
41
- // defineQueryHandler) turns a `true` into a compile error — the static twin of
42
- // assertNoSecretLeak's runtime walk.
43
- //
44
- // Biased to `false`: anything it cannot inspect — a bare generic type param (a
45
- // handler generic over its response), `unknown`/`any`, `never` — resolves to
46
- // `false` = allowed, with the runtime guard as the backstop. The alternative
47
- // (default-to-leak) false-flags every legitimate generic-over-response handler.
48
- //
49
- // Branch order is load-bearing: never/unknown/any first (uninspectable), then
50
- // Secret, then primitives (covers branded primitives like TenantId without
51
- // enumerating them), then the SafeLeaf allowlist (opaque class instances that
52
- // blind `{ [K in keyof T] }` recursion would mangle — the type-level mirror of
53
- // leak-guard.ts skipping non-plain objects), then arrays, then a "does any
54
- // field contain a secret" fold over plain objects.
55
- type Primitive = string | number | boolean | bigint | symbol | null | undefined;
56
-
57
- // Opaque built-in leaves a response legitimately carries; never recurse into
58
- // them. Extend when the bundled-features tsc sweep surfaces a real leaf type.
59
- type SafeLeaf =
60
- | Date
61
- | RegExp
62
- | Temporal.Instant
63
- | Temporal.ZonedDateTime
64
- | Temporal.PlainDate
65
- | Temporal.PlainDateTime
66
- | Temporal.PlainTime
67
- | Temporal.PlainYearMonth
68
- | Temporal.PlainMonthDay
69
- | Temporal.Duration;
70
-
71
- export type ContainsSecret<T> = [T] extends [never]
72
- ? false
73
- : unknown extends T
74
- ? false
75
- : T extends Secret<unknown>
76
- ? true
77
- : T extends Primitive
78
- ? false
79
- : T extends SafeLeaf
80
- ? false
81
- : T extends readonly (infer U)[]
82
- ? ContainsSecret<U>
83
- : T extends object
84
- ? true extends { [K in keyof T]-?: ContainsSecret<T[K]> }[keyof T]
85
- ? true
86
- : false
87
- : false;
88
-
89
- // Per-read audit context. Populated by requireSecretsContext() wrapper so
90
- // handlers don't need to pass userId/handlerName manually on every call.
91
- // Undefined for framework-internal reads (rotation job, tests) — the audit
92
- // table stays a "who touched this credential" log, not a crash-report sink.
93
- export type SecretAuditContext = {
94
- readonly userId: string;
95
- readonly handlerName: string;
96
- };
97
-
98
- // Feature code can pass either the raw qualified-name string or a typed
99
- // handle returned by r.secret. The handle form is safer — renaming the
100
- // r.secret call updates all references through the import graph.
101
- export type SecretKeyRef = string | { readonly name: string };
102
-
103
- // The ctx.secrets contract. Concrete implementation lives in bundled-features
104
- // (createSecretsContext) where the DB and MasterKeyProvider are known. This
105
- // lean interface is what the framework's HandlerContext carries so engine
106
- // code can talk about it without pulling in bundled-features.
107
- export interface SecretsContext {
108
- get(
109
- tenantId: TenantId,
110
- key: SecretKeyRef,
111
- auditCtx?: SecretAuditContext,
112
- ): Promise<Secret<string> | undefined>;
113
- // Metadata-only existence probe: no decryption, no read-audit event.
114
- // For readiness checks — use get() when the value itself is needed.
115
- has(tenantId: TenantId, key: SecretKeyRef): Promise<boolean>;
116
- set(
117
- tenantId: TenantId,
118
- key: SecretKeyRef,
119
- value: string,
120
- opts?: { redact?: (plaintext: string) => string; hint?: string; updatedBy?: string },
121
- ): Promise<void>;
122
- delete(tenantId: TenantId, key: SecretKeyRef, opts?: { deletedBy?: string }): Promise<boolean>;
123
- }
124
-
125
- export type Envelope = {
126
- // AES-256-GCM ciphertext of the plaintext, keyed with a DEK.
127
- readonly ciphertext: Buffer;
128
- // GCM nonce (12 bytes). Generated fresh per encryption.
129
- readonly iv: Buffer;
130
- // GCM auth tag (16 bytes). Guarantees the ciphertext wasn't tampered.
131
- readonly authTag: Buffer;
132
- // DEK wrapped with the current KEK. Decryption needs provider.unwrapDek
133
- // with the kekVersion to recover the DEK.
134
- readonly encryptedDek: Buffer;
135
- // Which KEK version was used to wrap the DEK. On rotation, rows with old
136
- // versions still decrypt — the provider keeps a keyring of historical KEKs.
137
- readonly kekVersion: number;
138
- };
139
-
140
- // The contract a KEK backend must fulfil. The framework sees only this
141
- // interface; concrete implementations live in separate packages
142
- // (@cosmicdrift/kumiko-secrets-vault, @cosmicdrift/kumiko-secrets-aws-kms, ...). The default is
143
- // EnvMasterKeyProvider which reads keys from environment variables.
144
- export interface MasterKeyProvider {
145
- // Wrap a fresh DEK with the current KEK. Returns the wrapped bytes + the
146
- // KEK version used — the version ends up in the Envelope so decryption
147
- // later knows which KEK to ask for.
148
- wrapDek(dek: Buffer): Promise<{ encryptedDek: Buffer; kekVersion: number }>;
149
-
150
- // Unwrap a previously-wrapped DEK. During rotation the provider must
151
- // accept older kekVersion values (2-version window minimum), otherwise
152
- // old rows become unreadable.
153
- unwrapDek(encryptedDek: Buffer, kekVersion: number): Promise<Buffer>;
154
-
155
- // Which KEK version new wraps use. Rotation flips this to a new value
156
- // and older-version reads continue to work until rows are re-wrapped.
157
- currentVersion(): number;
158
-
159
- // Health check: can the provider talk to its backend? Used by
160
- // /health/ready. Cheap probe, no KEK material read.
161
- isAvailable(): Promise<boolean>;
162
- }
1
+ // Legacy path re-exported for callers still importing this module directly.
2
+ export * from "@cosmicdrift/kumiko-types/secrets-types";
@@ -0,0 +1,42 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { SseEvent } from "../../api/sse-broker";
3
+ import type { SaveContext } from "../../engine/types";
4
+ import { createEventCollector } from "../event-collector";
5
+
6
+ const probeSseEvent: SseEvent = { type: "probe", data: {} };
7
+ const probeSaveContext: SaveContext = {
8
+ kind: "save",
9
+ id: "e1",
10
+ data: {},
11
+ changes: {},
12
+ previous: {},
13
+ isNew: true,
14
+ };
15
+
16
+ describe("createEventCollector", () => {
17
+ test("starts empty", () => {
18
+ const collector = createEventCollector();
19
+ expect(collector.sse).toEqual([]);
20
+ expect(collector.postSave).toEqual([]);
21
+ });
22
+
23
+ test("reset() clears both arrays in place, keeping the same references", () => {
24
+ const collector = createEventCollector();
25
+ const sseRef = collector.sse;
26
+ const postSaveRef = collector.postSave;
27
+ collector.sse.push(probeSseEvent);
28
+ collector.postSave.push(probeSaveContext);
29
+
30
+ expect(collector.sse).toHaveLength(1);
31
+ expect(collector.postSave).toHaveLength(1);
32
+
33
+ collector.reset();
34
+
35
+ expect(collector.sse).toHaveLength(0);
36
+ expect(collector.postSave).toHaveLength(0);
37
+ // Same array instances — callers that captured a reference before reset
38
+ // still see the cleared state, not a stale snapshot.
39
+ expect(collector.sse).toBe(sseRef);
40
+ expect(collector.postSave).toBe(postSaveRef);
41
+ });
42
+ });
@@ -0,0 +1,125 @@
1
+ // Drives the manual `ctx.jobRunner.dispatch(...)` path a write handler
2
+ // uses directly — distinct from the auto-trigger path covered by
3
+ // entrypoint-job-wiring.integration.test.ts, which reads a different
4
+ // context slot and isn't guaranteed by this test passing.
5
+
6
+ import { afterEach, describe, expect, test } from "bun:test";
7
+ import { z } from "zod";
8
+ import { defineFeature } from "../../engine";
9
+ import { InternalError, writeFailure } from "../../errors";
10
+ import { waitFor } from "../../testing";
11
+ import { setupTestStack, type TestStack } from "../test-stack";
12
+ import { TestUsers } from "../test-users";
13
+
14
+ const jobRuns: Array<{ name: string; payload: Record<string, unknown> }> = [];
15
+
16
+ const manualDispatchFeature = defineFeature("manualdispatch", (r) => {
17
+ r.writeHandler(
18
+ "create",
19
+ z.object({ note: z.string() }),
20
+ async (event, ctx) => {
21
+ // Exact repro from #983: bracket-access, dynamic context extension —
22
+ // not a typed HandlerContext field (mirrors trigger.write.ts).
23
+ const jobRunner = ctx["jobRunner"] as
24
+ | { dispatch: (name: string, payload: Record<string, unknown>) => Promise<string> }
25
+ | undefined;
26
+ if (!jobRunner) {
27
+ return writeFailure(new InternalError({ message: "no jobRunner on ctx" }));
28
+ }
29
+ await jobRunner.dispatch("manualdispatch:job:record", { note: event.payload.note });
30
+ return { isSuccess: true as const, data: { id: 1, note: event.payload.note } };
31
+ },
32
+ { access: { openToAll: true } },
33
+ );
34
+ r.job("record", { trigger: { manual: true }, runIn: "worker" }, async (payload) => {
35
+ jobRuns.push({ name: "manualdispatch:job:record", payload });
36
+ });
37
+ });
38
+
39
+ // kumiko-framework#1232: setupTestStack used to hand jobs a bare
40
+ // `{ db, registry }` context literal — nothing else buildServer's request
41
+ // path gets (tracer/meter, searchAdapter, ...). A job could pass in tests
42
+ // while reading a field only prod's context has, then break silently on
43
+ // deploy. This job records which fields actually arrived.
44
+ const jobContextFields: Array<{
45
+ hasTracer: boolean;
46
+ hasSearchAdapter: boolean;
47
+ hasEffectiveFeatures: boolean;
48
+ }> = [];
49
+
50
+ const jobContextFeature = defineFeature("jobcontextcheck", (r) => {
51
+ r.job("record", { trigger: { manual: true }, runIn: "worker" }, async (_payload, context) => {
52
+ jobContextFields.push({
53
+ hasTracer: context.tracer !== undefined,
54
+ hasSearchAdapter: context.searchAdapter !== undefined,
55
+ hasEffectiveFeatures: context.effectiveFeatures !== undefined,
56
+ });
57
+ });
58
+ });
59
+
60
+ let stack: TestStack | undefined;
61
+
62
+ afterEach(async () => {
63
+ if (stack) await stack.cleanup();
64
+ stack = undefined;
65
+ });
66
+
67
+ describe("setupTestStack({ jobs }) wires ctx.jobRunner for manual dispatch", () => {
68
+ test("write handler's ctx.jobRunner.dispatch(...) works end-to-end", async () => {
69
+ jobRuns.length = 0;
70
+ stack = await setupTestStack({
71
+ features: [manualDispatchFeature],
72
+ // consumerLane: this test has no separate consumer (unlike runDevApp's
73
+ // startDevJobRunners), so it must be the one running the job.
74
+ jobs: { consumerLane: "worker" },
75
+ });
76
+
77
+ await stack.http.writeOk("manualdispatch:write:create", { note: "hi" }, TestUsers.admin);
78
+
79
+ await waitFor(() => {
80
+ const run = jobRuns.find((e) => e.name === "manualdispatch:job:record");
81
+ expect(run).toBeDefined();
82
+ expect(run?.payload["note"]).toBe("hi");
83
+ });
84
+ });
85
+
86
+ test("without `jobs` opt-in, ctx.jobRunner stays undefined (no-op, not a throw)", async () => {
87
+ stack = await setupTestStack({ features: [manualDispatchFeature] });
88
+ expect(stack.jobRunner).toBeUndefined();
89
+
90
+ // The handler itself guards against a missing jobRunner (writeFailure,
91
+ // not a thrown TypeError) — same contract as trigger.write.ts's cast.
92
+ const err = await stack.http.writeErr(
93
+ "manualdispatch:write:create",
94
+ { note: "hi" },
95
+ TestUsers.admin,
96
+ );
97
+ expect(err.code).toBe("internal_error");
98
+ });
99
+ });
100
+
101
+ describe("setupTestStack({ jobs }) job context matches the request-path context", () => {
102
+ test("job handler's context carries tracer + searchAdapter, mirrors prod's effectiveFeatures-less job context", async () => {
103
+ jobContextFields.length = 0;
104
+ stack = await setupTestStack({
105
+ features: [jobContextFeature],
106
+ jobs: { consumerLane: "worker" },
107
+ effectiveFeatures: () => new Set(["jobcontextcheck"]),
108
+ });
109
+
110
+ await stack.jobRunner?.dispatch("jobcontextcheck:job:record");
111
+
112
+ await waitFor(() => {
113
+ expect(jobContextFields.length).toBeGreaterThan(0);
114
+ });
115
+ // #1255: effectiveFeatures deliberately stays OUT of the job/appContext
116
+ // (prod only carries it in dispatcherOptions.effectiveFeatures, consumed
117
+ // by the command-dispatcher) — putting it here would let a job read
118
+ // `ctx.effectiveFeatures` green in tests and break on deploy.
119
+ expect(jobContextFields[0]).toEqual({
120
+ hasTracer: true,
121
+ hasSearchAdapter: true,
122
+ hasEffectiveFeatures: false,
123
+ });
124
+ });
125
+ });
package/src/stack/db.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // Provider-agnostic via createConnection (DB_PROVIDER env).
3
3
  // postgres-js = default. DB_PROVIDER=bun = Bun.SQL (experimentell).
4
4
 
5
+ import type { DbConnection } from "@cosmicdrift/kumiko-types/db-connection";
5
6
  import { createConnection } from "../db/api";
6
7
  import { createDatabase, databaseExists, dropDatabaseIfExists } from "../db/queries/test-stack";
7
8
  import { ensureTemporalPolyfill } from "../time/polyfill";
@@ -18,7 +19,7 @@ function requireEnv(name: string): string {
18
19
  }
19
20
 
20
21
  export type TestDb = {
21
- db: unknown;
22
+ db: DbConnection;
22
23
  client: unknown;
23
24
  dbName: string;
24
25
  cleanup: () => Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { KUMIKO_NAME_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
1
2
  import { tableExists } from "../db/schema-inspection";
2
3
  import type { Registry } from "../engine/types";
3
4
  import { unsafePushTables } from "./table-helpers";
@@ -9,7 +10,7 @@ const logInfo = (msg: string): void => console.log(msg);
9
10
  /**
10
11
  * Push all implicit-projection tables — one per `r.entity()` — that the
11
12
  * registry knows about. setupTestStack already handles explicit
12
- * projections, MSPs, and `r.rawTable()` declarations in its own loop;
13
+ * projections, MSPs, and `r.storeTable()` declarations in its own loop;
13
14
  * implicit projections are the missing piece for a fresh boot.
14
15
  *
15
16
  * Idempotent via `tableExists` so a persistent dev DB
@@ -20,7 +21,7 @@ const logInfo = (msg: string): void => console.log(msg);
20
21
  * Lives next to `setupTestStack` because both are stack-bootstrap
21
22
  * helpers that legitimately speak the `unsafe*`-DDL layer; the
22
23
  * Table-DDL Guard's stack/** allowlist is the single shared exemption
23
- * site. Apps still declare data via `r.entity()` / `r.rawTable()` and
24
+ * site. Apps still declare data via `r.entity()` / `r.storeTable()` and
24
25
  * never call this directly.
25
26
  */
26
27
  export async function pushEntityProjectionTables(
@@ -35,7 +36,7 @@ export async function pushEntityProjectionTables(
35
36
  if (seen.has(proj.table)) continue;
36
37
  seen.add(proj.table);
37
38
  const tableRec = proj.table as unknown as Record<symbol, unknown>;
38
- const physical = tableRec[Symbol.for("kumiko:schema:Name")] as string;
39
+ const physical = tableRec[KUMIKO_NAME_SYMBOL] as string;
39
40
  if (await tableExists(stack.db, `public.${physical}`)) {
40
41
  logInfo(`[kumiko-stack] table ${physical} already exists — skipping create`);
41
42
  continue;
@@ -3,6 +3,13 @@ import { requireEnv } from "./db";
3
3
 
4
4
  export type TestRedis = {
5
5
  redis: import("ioredis").default;
6
+ // The exact REDIS_URL used to build `redis` above — for a second, unrelated
7
+ // connection (e.g. the test-stack's JobRunner) that needs its own client
8
+ // rather than sharing this one's keyPrefix. Reconstructing a URL from
9
+ // `redis.options` loses password/username/tls/path (pr-review
10
+ // kumiko-framework #1036/2) — callers needing a fresh connection should use
11
+ // this raw string, not rebuild one from parsed options.
12
+ redisUrl: string;
6
13
  /** Delete every key this test created (prefix-scoped). Replaces the old
7
14
  * `redis.flushdb()` — that wiped other parallel tests' BullMQ state. */
8
15
  flushNamespace: () => Promise<void>;
@@ -35,6 +42,7 @@ export async function createTestRedis(): Promise<TestRedis> {
35
42
 
36
43
  return {
37
44
  redis,
45
+ redisUrl,
38
46
  flushNamespace,
39
47
  cleanup: async () => {
40
48
  await flushNamespace();
@@ -1,4 +1,5 @@
1
1
  import type { Hono } from "hono";
2
+ import type { SessionCreator } from "../api/auth-routes";
2
3
  import type { JwtHelper } from "../api/jwt";
3
4
  import type { SessionUser } from "../engine/types";
4
5
 
@@ -98,9 +99,44 @@ export type RequestHelper = {
98
99
  ) => Promise<Response>;
99
100
  };
100
101
 
101
- export function createRequestHelper(app: Hono, jwt: JwtHelper): RequestHelper {
102
+ export type RequestHelperOptions = {
103
+ // When sessionChecker is wired (sessions feature), JWTs without jti are
104
+ // rejected as no_sid. Seed/test helpers that only jwt.sign(user) need a
105
+ // live sid — create one via the same sessionCreator login uses (#1372).
106
+ readonly sessionCreator?: SessionCreator;
107
+ };
108
+
109
+ export function createRequestHelper(
110
+ app: Hono,
111
+ jwt: JwtHelper,
112
+ options: RequestHelperOptions = {},
113
+ ): RequestHelper {
114
+ // sid per (user.id, tenantId), not one mint per authHeader() call:
115
+ // sessionCreator opens a live session row, so an unmemoized call mints a
116
+ // fresh one on every request a test makes for the same user — tests
117
+ // asserting on session counts / massRevoker / revokeAllOthers behavior
118
+ // then see extra live sids that have nothing to do with what they're
119
+ // testing. Keyed on tenantId too — the same user.id can hold sessions in
120
+ // more than one tenant, and sessionCreator writes the row scoped to
121
+ // `user.tenantId` (session-callbacks.ts), so a user.id-only key would
122
+ // hand a cross-tenant test its wrong tenant's sid.
123
+ // Cache the Promise (not the settled sid) so two concurrent authHeader
124
+ // calls for the same key share one mint instead of racing on a miss.
125
+ const sidByUserKey = new Map<string, Promise<string>>();
126
+
102
127
  async function authHeader(user: SessionUser): Promise<Record<string, string>> {
103
- const token = await jwt.sign(user);
128
+ let forJwt = user;
129
+ if (options.sessionCreator && !user.sid) {
130
+ const key = `${user.id}:${user.tenantId}`;
131
+ let sidPromise = sidByUserKey.get(key);
132
+ if (sidPromise === undefined) {
133
+ sidPromise = options.sessionCreator(user, { ip: "test", userAgent: "request-helper" });
134
+ sidByUserKey.set(key, sidPromise);
135
+ }
136
+ const sid = await sidPromise;
137
+ forJwt = { ...user, sid };
138
+ }
139
+ const token = await jwt.sign(forJwt);
104
140
  return { Authorization: `Bearer ${token}` };
105
141
  }
106
142
 
@@ -1,3 +1,7 @@
1
+ import {
2
+ KUMIKO_META_SYMBOL,
3
+ KUMIKO_NAME_SYMBOL,
4
+ } from "@cosmicdrift/kumiko-types/schema-table-types";
1
5
  import type { DbConnection } from "../db/connection";
2
6
  import { pgTypeToSqlType } from "../db/dialect";
3
7
  import type { ColumnMeta, EntityTableMeta } from "../db/entity-table-meta";
@@ -5,15 +9,13 @@ import {
5
9
  alterTableAddColumn,
6
10
  createIndexIfNotExists,
7
11
  executeDdlStatement,
8
- truncateTablesRestartIdentity,
9
- } from "../db/queries/test-stack";
12
+ } from "../db/queries/ddl";
13
+ import { truncateTablesRestartIdentity } from "../db/queries/test-stack";
10
14
  import { renderTableDdl } from "../db/render-ddl";
11
15
  import { tableExists } from "../db/schema-inspection";
12
16
  import { buildEntityTable, toTableName } from "../db/table-builder";
13
17
  import type { EventDispatcher } from "../pipeline";
14
18
 
15
- const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
16
- const KUMIKO_META_SYMBOL = Symbol.for("kumiko:schema:Meta");
17
19
  function tableNameOf(table: unknown): string {
18
20
  if (typeof table !== "object" || table === null) {
19
21
  throw new Error("table-helpers: table is not a SchemaTable object");