@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,103 @@
1
+ // Response-Tripwire (#820): ein kumiko-pii:-Ciphertext in einer JSON-Response
2
+ // ist immer ein Bug (raw-Read am Decrypt vorbei). Dev/Test → 500 (Test wird
3
+ // rot), Prod → redact + Error-Log, ohne KMS → kein Scan (pass-through).
4
+
5
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
6
+ import { z } from "zod";
7
+ import {
8
+ configurePiiSubjectKms,
9
+ InMemoryKmsAdapter,
10
+ resetPiiSubjectKmsForTests,
11
+ } from "../../crypto";
12
+ import { defineFeature } from "../../engine/define-feature";
13
+ import { defineQueryHandler } from "../../engine/define-handler";
14
+ import { setupTestStack, type TestStack, TestUsers } from "../../stack";
15
+
16
+ const CIPHERTEXT = "kumiko-pii:v1:user:6b2f4a0e-1c9d-4f3a-9d2e-00000000000a:8e2Rkjj+ww==";
17
+
18
+ const leakyFeature = defineFeature("leaky", (r) => {
19
+ r.queryHandler(
20
+ defineQueryHandler({
21
+ name: "raw",
22
+ schema: z.object({}),
23
+ access: { openToAll: true },
24
+ handler: async () => ({ email: CIPHERTEXT, note: "plain" }),
25
+ }),
26
+ );
27
+ r.queryHandler(
28
+ defineQueryHandler({
29
+ name: "clean",
30
+ schema: z.object({}),
31
+ access: { openToAll: true },
32
+ handler: async () => ({ email: "marc@example.com" }),
33
+ }),
34
+ );
35
+ });
36
+
37
+ let stack: TestStack;
38
+ const originalNodeEnv = process.env["NODE_ENV"];
39
+
40
+ beforeAll(async () => {
41
+ stack = await setupTestStack({ features: [leakyFeature] });
42
+ });
43
+
44
+ afterAll(async () => {
45
+ await stack.cleanup();
46
+ });
47
+
48
+ afterEach(() => {
49
+ resetPiiSubjectKmsForTests();
50
+ if (originalNodeEnv === undefined) delete process.env["NODE_ENV"];
51
+ else process.env["NODE_ENV"] = originalNodeEnv;
52
+ });
53
+
54
+ async function callLeakyQuery(): Promise<Response> {
55
+ const token = await stack.jwt.sign(TestUsers.admin);
56
+ return stack.http.raw(
57
+ "POST",
58
+ "/api/query",
59
+ { type: "leaky:query:raw", payload: {} },
60
+ { Authorization: `Bearer ${token}` },
61
+ );
62
+ }
63
+
64
+ describe("piiCiphertextResponseGuard", () => {
65
+ test("no KMS configured → response passes through unscanned", async () => {
66
+ const res = await callLeakyQuery();
67
+ expect(res.status).toBe(200);
68
+ expect(await res.text()).toContain(CIPHERTEXT);
69
+ });
70
+
71
+ test("KMS active, dev: leaking response becomes a loud 500", async () => {
72
+ configurePiiSubjectKms(new InMemoryKmsAdapter());
73
+ const res = await callLeakyQuery();
74
+ expect(res.status).toBe(500);
75
+ const body = (await res.json()) as { error?: { code?: string; message?: string } };
76
+ expect(body.error?.code).toBe("pii_ciphertext_leak");
77
+ expect(body.error?.message).toContain("/api/query");
78
+ });
79
+
80
+ test("KMS active, production: leak is redacted, request succeeds", async () => {
81
+ configurePiiSubjectKms(new InMemoryKmsAdapter());
82
+ process.env["NODE_ENV"] = "production";
83
+ const res = await callLeakyQuery();
84
+ expect(res.status).toBe(200);
85
+ const text = await res.text();
86
+ expect(text).not.toContain("kumiko-pii:");
87
+ expect(text).toContain("[pii-redacted]");
88
+ expect(text).toContain("plain");
89
+ });
90
+
91
+ test("KMS active, clean response stays untouched", async () => {
92
+ configurePiiSubjectKms(new InMemoryKmsAdapter());
93
+ const token = await stack.jwt.sign(TestUsers.admin);
94
+ const res = await stack.http.raw(
95
+ "POST",
96
+ "/api/query",
97
+ { type: "leaky:query:clean", payload: {} },
98
+ { Authorization: `Bearer ${token}` },
99
+ );
100
+ expect(res.status).toBe(200);
101
+ expect(await res.text()).toContain("marc@example.com");
102
+ });
103
+ });
@@ -0,0 +1,138 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { createTestRedis, type TestRedis } from "../../stack";
3
+ import { createRedisLoginRateLimiter } from "../auth-routes";
4
+
5
+ let testRedis: TestRedis;
6
+
7
+ beforeAll(async () => {
8
+ testRedis = await createTestRedis();
9
+ });
10
+
11
+ afterAll(async () => {
12
+ await testRedis.cleanup();
13
+ });
14
+
15
+ beforeEach(async () => {
16
+ await testRedis.flushNamespace();
17
+ });
18
+
19
+ describe("createRedisLoginRateLimiter", () => {
20
+ test("allows exactly maxAttempts checks, then blocks", async () => {
21
+ const limiter = createRedisLoginRateLimiter(testRedis.redis, 3, 60_000);
22
+
23
+ expect(await limiter.check("1.2.3.4|user@test.local")).toBe(true);
24
+ expect(await limiter.check("1.2.3.4|user@test.local")).toBe(true);
25
+ expect(await limiter.check("1.2.3.4|user@test.local")).toBe(true);
26
+ expect(await limiter.check("1.2.3.4|user@test.local")).toBe(false);
27
+ });
28
+
29
+ test("reset clears the counter for that key", async () => {
30
+ const limiter = createRedisLoginRateLimiter(testRedis.redis, 1, 60_000);
31
+
32
+ expect(await limiter.check("k")).toBe(true);
33
+ expect(await limiter.check("k")).toBe(false);
34
+
35
+ await limiter.reset("k");
36
+ expect(await limiter.check("k")).toBe(true);
37
+ });
38
+
39
+ test("buckets are independent per key", async () => {
40
+ const limiter = createRedisLoginRateLimiter(testRedis.redis, 1, 60_000);
41
+
42
+ expect(await limiter.check("a")).toBe(true);
43
+ expect(await limiter.check("b")).toBe(true);
44
+ expect(await limiter.check("a")).toBe(false);
45
+ expect(await limiter.check("b")).toBe(false);
46
+ });
47
+
48
+ test("namespace keeps two limiter instances from sharing a keyspace", async () => {
49
+ const login = createRedisLoginRateLimiter(testRedis.redis, 1, 60_000, "login");
50
+ const mfa = createRedisLoginRateLimiter(testRedis.redis, 1, 60_000, "mfa-verify");
51
+
52
+ expect(await login.check("1.2.3.4")).toBe(true);
53
+ expect(await mfa.check("1.2.3.4")).toBe(true);
54
+ });
55
+
56
+ test("counter resets once windowMs elapses", async () => {
57
+ const limiter = createRedisLoginRateLimiter(testRedis.redis, 1, 150);
58
+
59
+ expect(await limiter.check("k")).toBe(true);
60
+ expect(await limiter.check("k")).toBe(false);
61
+
62
+ await Bun.sleep(200);
63
+
64
+ expect(await limiter.check("k")).toBe(true);
65
+ });
66
+
67
+ // Regression for the INCR-then-PEXPIRE race: two separate round-trips left
68
+ // a window for a crash/network blip right after the key is created
69
+ // (count===1) to skip the PEXPIRE call entirely, leaving the key
70
+ // permanently without a TTL. The single-eval fix closes that window by
71
+ // construction (one round-trip, can't be interrupted mid-way). "TTL
72
+ // exists after check()" alone doesn't distinguish old from new — the old
73
+ // two-call code also leaves a TTL set on the non-crashing path — so this
74
+ // pins the actual mechanism instead: exactly one `eval` round-trip, and
75
+ // `incr`/`pexpire` are never called directly (the only path is atomic).
76
+ test("check() goes through a single atomic eval — never incr/pexpire directly", async () => {
77
+ const limiter = createRedisLoginRateLimiter(testRedis.redis, 3, 60_000);
78
+ const key = "1.2.3.4|ttl-race@test.local";
79
+
80
+ // Manual call-counting instrumentation, not a test-library mock: each
81
+ // wrapper delegates to the real bound method on the real Redis client
82
+ // (setupTestStack), so behavior is unchanged — this only counts which
83
+ // wire commands actually went out, without the bun:test spy helper the
84
+ // integration-mock-guard forbids in *.integration.test.ts files.
85
+ const origEval = testRedis.redis.eval.bind(testRedis.redis);
86
+ const origIncr = testRedis.redis.incr.bind(testRedis.redis);
87
+ const origPexpire = testRedis.redis.pexpire.bind(testRedis.redis);
88
+ let evalCalls = 0;
89
+ let incrCalls = 0;
90
+ let pexpireCalls = 0;
91
+ testRedis.redis.eval = ((...args: Parameters<typeof origEval>) => {
92
+ evalCalls++;
93
+ return origEval(...args);
94
+ }) as typeof origEval;
95
+ testRedis.redis.incr = ((...args: Parameters<typeof origIncr>) => {
96
+ incrCalls++;
97
+ return origIncr(...args);
98
+ }) as typeof origIncr;
99
+ testRedis.redis.pexpire = ((...args: Parameters<typeof origPexpire>) => {
100
+ pexpireCalls++;
101
+ return origPexpire(...args);
102
+ }) as typeof origPexpire;
103
+ try {
104
+ expect(await limiter.check(key)).toBe(true);
105
+
106
+ expect(evalCalls).toBe(1);
107
+ expect(incrCalls).toBe(0);
108
+ expect(pexpireCalls).toBe(0);
109
+
110
+ const ttlMs = await testRedis.redis.pttl(`kumiko:auth:ratelimit:login:${key}`);
111
+ expect(ttlMs).toBeGreaterThan(0);
112
+ } finally {
113
+ testRedis.redis.eval = origEval;
114
+ testRedis.redis.incr = origIncr;
115
+ testRedis.redis.pexpire = origPexpire;
116
+ }
117
+ });
118
+
119
+ // kumiko-framework#1522 idx=5: a key that already exists WITHOUT a TTL
120
+ // (exactly what the pre-fix INCR-then-PEXPIRE race could leave behind)
121
+ // must get one applied on the very next check(), not stay permanently
122
+ // uncapped. Simulates the orphan directly via INCR (no TTL) rather than
123
+ // trying to hit the original race window, which isn't reproducible from
124
+ // outside the eval script.
125
+ test("heals a pre-existing key that has no TTL (orphaned by the old two-call race)", async () => {
126
+ const limiter = createRedisLoginRateLimiter(testRedis.redis, 5, 60_000, "heal-test");
127
+ const key = "1.2.3.4|orphan@test.local";
128
+ const redisKey = `kumiko:auth:ratelimit:heal-test:${key}`;
129
+
130
+ await testRedis.redis.incr(redisKey);
131
+ expect(await testRedis.redis.pttl(redisKey)).toBe(-1);
132
+
133
+ await limiter.check(key);
134
+
135
+ const ttlMs = await testRedis.redis.pttl(redisKey);
136
+ expect(ttlMs).toBeGreaterThan(0);
137
+ });
138
+ });
@@ -70,3 +70,54 @@ describe("requestIdMiddleware — signal propagation", () => {
70
70
  expect(captured?.aborted).toBe(true);
71
71
  });
72
72
  });
73
+ describe("requestIdMiddleware — ip + userAgent capture (603/2)", () => {
74
+ test("populates ip from x-forwarded-for and userAgent from the User-Agent header", async () => {
75
+ let captured: { ip: string | undefined; userAgent: string | undefined } = {
76
+ ip: undefined,
77
+ userAgent: undefined,
78
+ };
79
+
80
+ const app = new Hono();
81
+ app.use("/probe", requestIdMiddleware());
82
+ app.get("/probe", (c) => {
83
+ const ctx = requestContext.get();
84
+ captured = { ip: ctx?.ip, userAgent: ctx?.userAgent };
85
+ return c.text("ok");
86
+ });
87
+
88
+ const res = await app.request(
89
+ new Request("http://test.local/probe", {
90
+ method: "GET",
91
+ headers: {
92
+ "x-forwarded-for": "203.0.113.7, 10.0.0.1",
93
+ "user-agent": "Mozilla/5.0 (probe-test)",
94
+ },
95
+ }),
96
+ );
97
+
98
+ expect(res.status).toBe(200);
99
+ expect(captured.ip).toBe("203.0.113.7");
100
+ expect(captured.userAgent).toBe("Mozilla/5.0 (probe-test)");
101
+ });
102
+
103
+ test("no User-Agent header → userAgent stays undefined, not an empty string", async () => {
104
+ let captured: string | undefined = "unset";
105
+
106
+ const app = new Hono();
107
+ app.use("/probe", requestIdMiddleware());
108
+ app.get("/probe", (c) => {
109
+ captured = requestContext.get()?.userAgent;
110
+ return c.text("ok");
111
+ });
112
+
113
+ // fetch/undici always add a default User-Agent unless explicitly cleared —
114
+ // Hono's app.request accepts a raw Request, so an empty-but-present header
115
+ // simulates a client that sends the header with no value.
116
+ const res = await app.request(
117
+ new Request("http://test.local/probe", { method: "GET", headers: {} }),
118
+ );
119
+
120
+ expect(res.status).toBe(200);
121
+ expect(captured).toBeUndefined();
122
+ });
123
+ });
@@ -0,0 +1,71 @@
1
+ // buildServer boot-time guards + httpRoute verb wiring (PUT branch).
2
+
3
+ import { describe, expect, test } from "bun:test";
4
+ import {
5
+ createEntity,
6
+ createFileField,
7
+ createRegistry,
8
+ createTextField,
9
+ defineFeature,
10
+ } from "../../engine";
11
+ import { buildServer } from "../server";
12
+
13
+ const JWT_SECRET = "server-boot-guards-test-secret-min-32-chars";
14
+
15
+ describe("buildServer — file-storage provider guard", () => {
16
+ const fileFieldFeature = defineFeature("needs-files", (r) => {
17
+ r.entity(
18
+ "doc",
19
+ createEntity({
20
+ table: "boot_guard_docs",
21
+ fields: { title: createTextField(), attachment: createFileField() },
22
+ }),
23
+ );
24
+ });
25
+
26
+ test("throws when registry declares file fields but no provider is mounted", () => {
27
+ expect(() =>
28
+ buildServer({
29
+ registry: createRegistry([fileFieldFeature]),
30
+ context: {},
31
+ jwtSecret: JWT_SECRET,
32
+ }),
33
+ ).toThrow(/no file-storage provider is mounted/);
34
+ });
35
+ });
36
+
37
+ describe("buildServer — rateLimit resolver guard", () => {
38
+ test("throws when L1 global middleware requested without resolver", () => {
39
+ expect(() =>
40
+ buildServer({
41
+ registry: createRegistry([]),
42
+ context: {},
43
+ jwtSecret: JWT_SECRET,
44
+ rateLimit: { global: { limit: 100, windowSeconds: 60 } },
45
+ }),
46
+ ).toThrow(/rateLimit middleware requested but no resolver available/);
47
+ });
48
+ });
49
+
50
+ describe("buildServer — feature httpRoute PUT mounting", () => {
51
+ const putFeature = defineFeature("put-route", (r) => {
52
+ r.httpRoute({
53
+ method: "PUT",
54
+ path: "/resource/42",
55
+ anonymous: true,
56
+ handler: (c) => c.json({ method: "PUT", ok: true }),
57
+ });
58
+ });
59
+
60
+ const { app } = buildServer({
61
+ registry: createRegistry([putFeature]),
62
+ context: {},
63
+ jwtSecret: JWT_SECRET,
64
+ });
65
+
66
+ test("PUT /resource/42 reaches the declared handler", async () => {
67
+ const res = await app.request("/resource/42", { method: "PUT" });
68
+ expect(res.status).toBe(200);
69
+ expect(await res.json()).toEqual({ method: "PUT", ok: true });
70
+ });
71
+ });
@@ -0,0 +1,58 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createRegistry } from "../../engine";
3
+ import { buildServer } from "../server";
4
+
5
+ const JWT_SECRET = "x".repeat(40);
6
+ const registry = createRegistry([]);
7
+
8
+ describe("buildServer — jwtTtl default depends on sessionChecker wiring", () => {
9
+ test("no auth config → stateless default (1h)", () => {
10
+ const { jwt } = buildServer({ registry, context: {}, jwtSecret: JWT_SECRET });
11
+ expect(jwt.ttlSeconds).toBe(60 * 60);
12
+ });
13
+
14
+ test("auth without sessionChecker → stateless default (1h)", () => {
15
+ const { jwt } = buildServer({
16
+ registry,
17
+ context: {},
18
+ jwtSecret: JWT_SECRET,
19
+ auth: { membershipQuery: "unused:query" },
20
+ });
21
+ expect(jwt.ttlSeconds).toBe(60 * 60);
22
+ });
23
+
24
+ test("auth with sessionChecker → session-backed default (24h)", () => {
25
+ const { jwt } = buildServer({
26
+ registry,
27
+ context: {},
28
+ jwtSecret: JWT_SECRET,
29
+ auth: {
30
+ membershipQuery: "unused:query",
31
+ sessionChecker: async () => "live",
32
+ },
33
+ });
34
+ expect(jwt.ttlSeconds).toBe(24 * 60 * 60);
35
+ });
36
+
37
+ test("explicit jwtTtl wins regardless of sessionChecker wiring", () => {
38
+ const withChecker = buildServer({
39
+ registry,
40
+ context: {},
41
+ jwtSecret: JWT_SECRET,
42
+ jwtTtl: 42,
43
+ auth: {
44
+ membershipQuery: "unused:query",
45
+ sessionChecker: async () => "live",
46
+ },
47
+ });
48
+ expect(withChecker.jwt.ttlSeconds).toBe(42);
49
+
50
+ const withoutChecker = buildServer({
51
+ registry,
52
+ context: {},
53
+ jwtSecret: JWT_SECRET,
54
+ jwtTtl: 42,
55
+ });
56
+ expect(withoutChecker.jwt.ttlSeconds).toBe(42);
57
+ });
58
+ });
@@ -55,4 +55,61 @@ describe("SSE broker", () => {
55
55
  expect(() => broker.removeClient("unknown", "fake-id")).not.toThrow();
56
56
  expect(broker.getClientCount("unknown")).toBe(0);
57
57
  });
58
+
59
+ test("subscribeAccessInvalidation fires only listeners on the same user's channel", () => {
60
+ const broker = createSseBroker();
61
+ const onInvalidateA = mock();
62
+ const onInvalidateB = mock();
63
+
64
+ broker.subscribeAccessInvalidation("user-a", onInvalidateA);
65
+ broker.subscribeAccessInvalidation("user-b", onInvalidateB);
66
+
67
+ broker.publishAccessInvalidation("user-a");
68
+
69
+ expect(onInvalidateA).toHaveBeenCalledTimes(1);
70
+ expect(onInvalidateB).not.toHaveBeenCalled();
71
+ });
72
+
73
+ test("subscribeAccessInvalidation supports multiple listeners on the same user", () => {
74
+ const broker = createSseBroker();
75
+ const first = mock();
76
+ const second = mock();
77
+
78
+ broker.subscribeAccessInvalidation("user-a", first);
79
+ broker.subscribeAccessInvalidation("user-a", second);
80
+ broker.publishAccessInvalidation("user-a");
81
+
82
+ expect(first).toHaveBeenCalledTimes(1);
83
+ expect(second).toHaveBeenCalledTimes(1);
84
+ });
85
+
86
+ test("unsubscribe (returned closure) stops further delivery", () => {
87
+ const broker = createSseBroker();
88
+ const onInvalidate = mock();
89
+
90
+ const unsubscribe = broker.subscribeAccessInvalidation("user-a", onInvalidate);
91
+ unsubscribe();
92
+ broker.publishAccessInvalidation("user-a");
93
+
94
+ expect(onInvalidate).not.toHaveBeenCalled();
95
+ });
96
+
97
+ test("a fired listener can unsubscribe itself without skipping other listeners", () => {
98
+ const broker = createSseBroker();
99
+ let unsubscribeSelf: () => void = () => {};
100
+ const self = mock(() => unsubscribeSelf());
101
+ const other = mock();
102
+
103
+ unsubscribeSelf = broker.subscribeAccessInvalidation("user-a", self);
104
+ broker.subscribeAccessInvalidation("user-a", other);
105
+ broker.publishAccessInvalidation("user-a");
106
+
107
+ expect(self).toHaveBeenCalledTimes(1);
108
+ expect(other).toHaveBeenCalledTimes(1);
109
+ });
110
+
111
+ test("publishAccessInvalidation to a user with no listeners does nothing", () => {
112
+ const broker = createSseBroker();
113
+ expect(() => broker.publishAccessInvalidation("nobody-listening")).not.toThrow();
114
+ });
58
115
  });
@@ -27,6 +27,10 @@ function createTrackingBroker(): { broker: SseBroker; subscribedChannel: Promise
27
27
  getTotalClientCount() {
28
28
  return 0;
29
29
  },
30
+ subscribeAccessInvalidation() {
31
+ return () => {};
32
+ },
33
+ publishAccessInvalidation() {},
30
34
  };
31
35
 
32
36
  return { broker, subscribedChannel };
@@ -10,8 +10,12 @@ export const Routes = {
10
10
  query: "/query",
11
11
  command: "/command",
12
12
  sse: "/sse",
13
+ stream: "/stream",
13
14
  auth: "/auth",
14
15
  authLogin: "/auth/login",
16
+ authMfaVerify: "/auth/mfa/verify",
17
+ authMfaPreauthEnableStart: "/auth/mfa/preauth-enable-start",
18
+ authMfaPreauthConfirm: "/auth/mfa/preauth-confirm",
15
19
  authLogout: "/auth/logout",
16
20
  authTenants: "/auth/tenants",
17
21
  authSwitchTenant: "/auth/switch-tenant",
@@ -19,6 +23,8 @@ export const Routes = {
19
23
  authResetPassword: "/auth/reset-password",
20
24
  authRequestEmailVerification: "/auth/request-email-verification",
21
25
  authVerifyEmail: "/auth/verify-email",
26
+ authRequestAccountUnlock: "/auth/request-account-unlock",
27
+ authConfirmAccountUnlock: "/auth/confirm-account-unlock",
22
28
  authSignupRequest: "/auth/signup-request",
23
29
  authSignupConfirm: "/auth/signup-confirm",
24
30
  // Tenant-Invite (Magic-Link): 3 separate accept-Endpoints für klare
@@ -36,10 +42,15 @@ export const Routes = {
36
42
  // The auth middleware skips these paths.
37
43
  export const PUBLIC_API_PATHS: ReadonlySet<string> = new Set([
38
44
  `/api${Routes.authLogin}`,
45
+ `/api${Routes.authMfaVerify}`,
46
+ `/api${Routes.authMfaPreauthEnableStart}`,
47
+ `/api${Routes.authMfaPreauthConfirm}`,
39
48
  `/api${Routes.authRequestPasswordReset}`,
40
49
  `/api${Routes.authResetPassword}`,
41
50
  `/api${Routes.authRequestEmailVerification}`,
42
51
  `/api${Routes.authVerifyEmail}`,
52
+ `/api${Routes.authRequestAccountUnlock}`,
53
+ `/api${Routes.authConfirmAccountUnlock}`,
43
54
  `/api${Routes.authSignupRequest}`,
44
55
  `/api${Routes.authSignupConfirm}`,
45
56
  // invite-accept braucht JWT (logged-in User, Branch 1) — NICHT public.