@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
@@ -16,6 +16,12 @@ export const AUTH_COOKIE_NAME = "kumiko_auth";
16
16
  export const CSRF_COOKIE_NAME = "kumiko_csrf";
17
17
  export const CSRF_HEADER_NAME = "X-CSRF-Token";
18
18
 
19
+ // Prefix that marks a bearer token as a long-lived Personal Access Token
20
+ // rather than a JWT session. The personal-access-tokens feature mints tokens
21
+ // with this prefix and declares it as its tokenVerifier shape (kind:
22
+ // "prefix") — kept here so both sides import the same literal.
23
+ export const PAT_TOKEN_PREFIX = "kpat_";
24
+
19
25
  // Which wire the current request authenticated over. Downstream
20
26
  // csrf-middleware reads this: cookie-auth gets a CSRF-token check, bearer
21
27
  // does not (headers aren't set cross-origin by browsers, so there is no
@@ -41,21 +47,39 @@ export type AuthSessionChecker = (
41
47
  expectedUserId: string,
42
48
  ) => Promise<AuthSessionStatus>;
43
49
 
50
+ // Resolves a raw bearer token into a SessionUser, or null when no registered
51
+ // provider claims it (or the claiming provider rejects it as unknown/revoked/
52
+ // expired). Wired generically from the auth-foundation `tokenVerifier`
53
+ // extension-point registry — the middleware has no PAT/JWT-specific
54
+ // knowledge, it just consults whatever the app wired in and short-circuits
55
+ // the JWT path on a hit.
56
+ export type TokenVerifier = (rawToken: string) => Promise<SessionUser | null>;
57
+
58
+ export type TenantLifecycleStatusResolver = (
59
+ tenantId: TenantId,
60
+ ) => Promise<{ readonly status: string } | null>;
61
+
44
62
  export type AuthMiddlewareOptions = {
45
63
  // Called after JWT-verify when the token carries a sid. If the checker
46
64
  // reports anything other than "live", the request is rejected with 401.
47
65
  // Omit to run in stateless-JWT mode (any valid JWT is accepted).
48
66
  readonly sessionChecker?: AuthSessionChecker;
49
- // When true, a JWT WITHOUT a sid is rejected. Leave false during rollout
50
- // so already-issued stateless JWTs keep working until they expire; flip
51
- // to true once the server has been emitting sid for longer than the JWT
52
- // TTL. Has no effect when sessionChecker is undefined.
53
- readonly strictMode?: boolean;
67
+ // Called for bearer tokens, BEFORE jwt.verify. On a hit the middleware
68
+ // sets the returned SessionUser and skips the JWT path entirely; on a
69
+ // miss (null) falls through to jwt.verify. Wired from the auth-foundation
70
+ // `tokenVerifier` extension-point registry generic across providers
71
+ // (PAT, future JWT-provider, ...), the middleware itself has no
72
+ // provider-specific knowledge. Omit to disable non-JWT bearer auth.
73
+ readonly tokenVerifier?: TokenVerifier;
54
74
  // Opt-in: when set, requests without a JWT are treated as anonymous
55
75
  // callers instead of being rejected with 401. The middleware synthesises
56
76
  // a SessionUser with id="anonymous" and roles=["anonymous"], scoped to a
57
77
  // tenantId resolved through the chain documented on AnonymousAccessConfig.
58
- readonly anonymousAccess?: AnonymousAccessConfig;
78
+ readonly anonymousAccess?: AnonymousAccessResolved;
79
+ // Consulted after tenantId is resolved (JWT/PAT/anonymous). Returns 410
80
+ // when the tenant is in teardown (destroyRequested/destroying/destroyed).
81
+ // cancel-destruction is exempt while status=destroyRequested.
82
+ readonly resolveTenantLifecycleStatus?: TenantLifecycleStatusResolver;
59
83
  };
60
84
 
61
85
  // Resolves the tenant for an unauthenticated request. Returns null when no
@@ -72,23 +96,22 @@ export type TenantResolver = (c: Context) => Promise<TenantId | null> | TenantId
72
96
  // deployments where a caller could otherwise probe arbitrary ids.
73
97
  export type TenantExists = (tenantId: TenantId) => Promise<boolean> | boolean;
74
98
 
99
+ // App-facing anonymous-access config (#1374). tenantResolver / tenantExists
100
+ // / resolverTrust come from auth-foundation providers (EXT_TENANT_RESOLVER /
101
+ // EXT_TENANT_EXISTENCE), resolved at boot into AnonymousAccessResolved.
102
+ //
103
+ // Single-tenant shortcut: when defaultTenantId is set, the server runs in
104
+ // **locked** mode (client must agree or get tenant_mismatch).
75
105
  export type AnonymousAccessConfig = {
76
- // Custom resolver (e.g. subdomain parser). Consulted only when neither
77
- // the X-Tenant header nor the kumiko_tenant cookie are present.
78
- readonly tenantResolver?: TenantResolver;
79
- // Single-tenant shortcut. When set, the server runs in **locked** mode:
80
- // - no client-supplied tenant: defaultTenantId is used.
81
- // - client supplies a matching tenant (header/cookie/resolver): allowed.
82
- // - client supplies a non-matching tenant: 400 tenant_mismatch (the
83
- // server is single-tenant; rejecting protects against confused clients
84
- // who think they're talking to a different deployment).
85
- // The framework does NOT verify defaultTenantId against the DB at boot;
86
- // the caller is responsible (see sample for the pattern).
87
106
  readonly defaultTenantId?: TenantId;
88
- // Per-request existence check for header/cookie/resolver-supplied ids.
89
- // Skipped for the defaultTenantId path (the caller already vetted that
90
- // value when configuring the server).
107
+ };
108
+
109
+ // Runtime config consumed by authMiddleware after boot merges registry
110
+ // providers onto the app-facing AnonymousAccessConfig.
111
+ export type AnonymousAccessResolved = AnonymousAccessConfig & {
112
+ readonly tenantResolver?: TenantResolver;
91
113
  readonly tenantExists?: TenantExists;
114
+ readonly resolverTrust?: "authoritative" | "fallback-only";
92
115
  };
93
116
 
94
117
  // Where the candidate tenant came from. Drives the validation policy:
@@ -109,14 +132,15 @@ type MiddlewareRejectCode =
109
132
  | "tenant_required"
110
133
  | "tenant_not_found"
111
134
  | "tenant_mismatch"
112
- | "invalid_tenant_format";
135
+ | "invalid_tenant_format"
136
+ | "tenant_unavailable";
113
137
 
114
138
  // @wrapper-known error-helper
115
139
  function middlewareReject(
116
140
  c: Context,
117
141
  opts: {
118
142
  code: MiddlewareRejectCode;
119
- status: 400 | 401 | 403 | 404;
143
+ status: 400 | 401 | 403 | 404 | 410;
120
144
  message: string;
121
145
  i18nKey: string;
122
146
  details?: Record<string, unknown>;
@@ -169,7 +193,18 @@ function extractToken(
169
193
  }
170
194
 
171
195
  export function authMiddleware(jwt: JwtHelper, options: AuthMiddlewareOptions = {}) {
172
- const { sessionChecker, strictMode = false, anonymousAccess } = options;
196
+ const { sessionChecker, anonymousAccess, tokenVerifier, resolveTenantLifecycleStatus } = options;
197
+
198
+ // Fail loud at boot, not silently at request time: a tenantResolver
199
+ // without a declared resolverTrust is an ambiguous trust decision no
200
+ // sane default can make on the app's behalf (see AnonymousAccessConfig).
201
+ if (anonymousAccess?.tenantResolver && anonymousAccess.resolverTrust === undefined) {
202
+ throw new Error(
203
+ "authMiddleware: anonymousAccess.tenantResolver is set without resolverTrust — " +
204
+ 'declare "authoritative" (resolver wins over client header/cookie) or ' +
205
+ '"fallback-only" (client wins, resolver is a last resort).',
206
+ );
207
+ }
173
208
 
174
209
  return async (c: Context, next: Next) => {
175
210
  const extracted = extractToken(c);
@@ -201,7 +236,7 @@ export function authMiddleware(jwt: JwtHelper, options: AuthMiddlewareOptions =
201
236
  i18nKey: "auth.errors.missingToken",
202
237
  });
203
238
  }
204
- return await handleAnonymous(c, anonymousAccess, next);
239
+ return await handleAnonymous(c, anonymousAccess, next, resolveTenantLifecycleStatus);
205
240
  }
206
241
  return middlewareReject(c, {
207
242
  code: "missing_token",
@@ -212,6 +247,19 @@ export function authMiddleware(jwt: JwtHelper, options: AuthMiddlewareOptions =
212
247
  }
213
248
  const { token, transport } = extracted;
214
249
 
250
+ // Generic bearer-verifier path: try the wired tokenVerifier (PAT, future
251
+ // JWT-provider, ...) BEFORE jwt.verify. A hit short-circuits the JWT path
252
+ // entirely; a miss (null — no provider's shape matched, or the matching
253
+ // provider rejected the token) falls through to the normal JWT flow below,
254
+ // which rejects it uniformly as invalid_token. Cookie transport is never
255
+ // routed here (the browser holds the JWT).
256
+ if (tokenVerifier && transport === "bearer") {
257
+ const verifiedUser = await tokenVerifier(token);
258
+ if (verifiedUser) {
259
+ return await handleVerifiedBearerUser(c, verifiedUser, next, resolveTenantLifecycleStatus);
260
+ }
261
+ }
262
+
215
263
  let payload: Awaited<ReturnType<JwtHelper["verify"]>>;
216
264
  try {
217
265
  payload = await jwt.verify(token);
@@ -225,15 +273,16 @@ export function authMiddleware(jwt: JwtHelper, options: AuthMiddlewareOptions =
225
273
  }
226
274
 
227
275
  // Session liveness check — only when both a checker is wired AND the
228
- // token carries a sid. strictMode governs the no-sid case below so that
229
- // both old JWTs (no sid) and rolling-deploy gaps can be handled.
276
+ // token carries a sid.
277
+ // A checker wired without a sid on the token means the token predates
278
+ // session tracking (or the JWT was forged) — reject.
230
279
  if (sessionChecker) {
231
280
  if (payload.jti) {
232
281
  const status = await sessionChecker(payload.jti, payload.sub);
233
282
  if (status !== "live") {
234
283
  return sessionInvalid(c, status);
235
284
  }
236
- } else if (strictMode) {
285
+ } else {
237
286
  return sessionInvalid(c, "no_sid");
238
287
  }
239
288
  }
@@ -261,6 +310,12 @@ export function authMiddleware(jwt: JwtHelper, options: AuthMiddlewareOptions =
261
310
  ...(payload.claims ? { claims: payload.claims } : {}),
262
311
  ...(payload.jti ? { sid: payload.jti } : {}),
263
312
  };
313
+ const lifecycleReject = await rejectIfTenantTeardown(
314
+ c,
315
+ payload.tenantId,
316
+ resolveTenantLifecycleStatus,
317
+ );
318
+ if (lifecycleReject) return lifecycleReject;
264
319
  c.set(USER_KEY, user);
265
320
  c.set(AUTH_TRANSPORT_KEY, transport);
266
321
  await next();
@@ -277,6 +332,42 @@ export function getAuthTransport(c: Context): AuthTransport | undefined {
277
332
  return c.get(AUTH_TRANSPORT_KEY) as AuthTransport | undefined;
278
333
  }
279
334
 
335
+ // Verified-bearer request flow. `user` was already resolved by the wired
336
+ // tokenVerifier (PAT today, future JWT-provider). Apply the same
337
+ // X-Tenant-mismatch guard as the JWT path before continuing. Structured like
338
+ // handleAnonymous so authMiddleware stays flat.
339
+ async function handleVerifiedBearerUser(
340
+ c: Context,
341
+ user: SessionUser,
342
+ next: Next,
343
+ resolveTenantLifecycleStatus?: TenantLifecycleStatusResolver,
344
+ ): Promise<Response | undefined> {
345
+ // The token carries its own tenant; an X-Tenant header pointing elsewhere is
346
+ // a confused client — reject loudly, same stance as the JWT path.
347
+ const headerTenant = c.req.header(TENANT_HEADER_NAME);
348
+ if (headerTenant !== undefined && headerTenant !== user.tenantId) {
349
+ return middlewareReject(c, {
350
+ code: "tenant_mismatch",
351
+ status: 400,
352
+ message: "token tenantId and X-Tenant header disagree",
353
+ i18nKey: "auth.errors.tenantMismatch",
354
+ details: { tokenTenantId: user.tenantId, headerTenantId: headerTenant },
355
+ });
356
+ }
357
+ c.set(USER_KEY, user);
358
+ c.set(AUTH_TRANSPORT_KEY, "bearer");
359
+ const lifecycleReject = await rejectIfTenantTeardown(
360
+ c,
361
+ user.tenantId,
362
+ resolveTenantLifecycleStatus,
363
+ );
364
+ if (lifecycleReject) return lifecycleReject;
365
+ await next();
366
+ // skip: verified-bearer path completed — next() ran; explicit return keeps
367
+ // the Response|undefined union honest (same as handleAnonymous).
368
+ return;
369
+ }
370
+
280
371
  // Anonymous request flow. Steps:
281
372
  // 1. Read raw client-supplied tenant from X-Tenant header / cookie.
282
373
  // 2. Validate format (UUID-shape) — junk strings get 400 right here.
@@ -291,8 +382,9 @@ export function getAuthTransport(c: Context): AuthTransport | undefined {
291
382
  // no CSRF vector to defend against).
292
383
  async function handleAnonymous(
293
384
  c: Context,
294
- config: AnonymousAccessConfig,
385
+ config: AnonymousAccessResolved,
295
386
  next: Next,
387
+ resolveTenantLifecycleStatus?: TenantLifecycleStatusResolver,
296
388
  ): Promise<Response | undefined> {
297
389
  // Step 1+2: parse client-supplied tenant. Reject malformed values before
298
390
  // they touch any downstream consumer (DB, cache, audit row).
@@ -330,6 +422,12 @@ async function handleAnonymous(
330
422
  }
331
423
 
332
424
  // Step 5: synthesise + continue.
425
+ const lifecycleReject = await rejectIfTenantTeardown(
426
+ c,
427
+ resolved.tenantId,
428
+ resolveTenantLifecycleStatus,
429
+ );
430
+ if (lifecycleReject) return lifecycleReject;
333
431
  c.set(USER_KEY, createAnonymousUser(resolved.tenantId));
334
432
  await next();
335
433
  // skip: anonymous path completed — Hono middleware contract returns void
@@ -370,38 +468,93 @@ type ResolveError = { error: RejectArgs };
370
468
  // clients that think they're talking to a different installation.
371
469
  async function resolveTenant(
372
470
  c: Context,
373
- config: AnonymousAccessConfig,
471
+ config: AnonymousAccessResolved,
374
472
  clientTenant: { id: TenantId; source: "header" | "cookie" } | null,
375
473
  ): Promise<ResolvedTenant | ResolveError> {
376
474
  if (config.defaultTenantId !== undefined) {
377
- if (clientTenant && clientTenant.id !== config.defaultTenantId) {
475
+ return resolveAgainstDefault(config.defaultTenantId, clientTenant);
476
+ }
477
+ if (config.tenantResolver && config.resolverTrust === "authoritative") {
478
+ return await resolveWithAuthoritativeResolver(c, config.tenantResolver, clientTenant);
479
+ }
480
+ return await resolveWithClientPrecedence(c, config.tenantResolver, clientTenant);
481
+ }
482
+
483
+ // Locked single-tenant mode: the client either agrees with the default or
484
+ // gets tenant_mismatch — defending the deployment from confused clients
485
+ // that think they're talking to a different installation.
486
+ function resolveAgainstDefault(
487
+ defaultTenantId: TenantId,
488
+ clientTenant: { id: TenantId; source: "header" | "cookie" } | null,
489
+ ): ResolvedTenant | ResolveError {
490
+ if (clientTenant && clientTenant.id !== defaultTenantId) {
491
+ return {
492
+ error: {
493
+ code: "tenant_mismatch",
494
+ status: 400,
495
+ message: `${clientTenant.source} tenant disagrees with server default`,
496
+ i18nKey: "auth.errors.tenantMismatch",
497
+ details: { clientTenantId: clientTenant.id, defaultTenantId },
498
+ },
499
+ };
500
+ }
501
+ return { tenantId: defaultTenantId, source: "default" };
502
+ }
503
+
504
+ // resolverTrust: "authoritative" — the resolver is trusted over the client.
505
+ // A client-supplied tenant that disagrees is rejected, never used to
506
+ // override the resolver's answer. Falls back to the client tenant only
507
+ // when the resolver itself has no opinion (unrecognised host).
508
+ async function resolveWithAuthoritativeResolver(
509
+ c: Context,
510
+ tenantResolver: TenantResolver,
511
+ clientTenant: { id: TenantId; source: "header" | "cookie" } | null,
512
+ ): Promise<ResolvedTenant | ResolveError> {
513
+ const resolved = await tenantResolver(c);
514
+ if (resolved !== null && resolved !== undefined) {
515
+ if (clientTenant && clientTenant.id !== resolved) {
378
516
  return {
379
517
  error: {
380
518
  code: "tenant_mismatch",
381
519
  status: 400,
382
- message: `${clientTenant.source} tenant disagrees with server default`,
520
+ message: `${clientTenant.source} tenant disagrees with resolved tenant`,
383
521
  i18nKey: "auth.errors.tenantMismatch",
384
- details: {
385
- clientTenantId: clientTenant.id,
386
- defaultTenantId: config.defaultTenantId,
387
- },
522
+ details: { clientTenantId: clientTenant.id, resolvedTenantId: resolved },
388
523
  },
389
524
  };
390
525
  }
391
- return { tenantId: config.defaultTenantId, source: "default" };
526
+ return { tenantId: resolved, source: "resolver" };
392
527
  }
528
+ // Resolver had no opinion (unrecognised host) — do NOT fall back to the
529
+ // client-supplied tenant here. That would let an unrecognised-host
530
+ // request pick its own tenant via X-Tenant, the exact override this mode
531
+ // exists to prevent; it's just reached through "unknown host" instead of
532
+ // "known host, disagreeing header". Authoritative means the resolver's
533
+ // silence is final, not a delegation back to the client.
534
+ return tenantRequiredError();
535
+ }
393
536
 
537
+ // resolverTrust: "fallback-only" (or no resolver at all) — client tenant
538
+ // wins outright; the resolver only runs as a last resort when neither
539
+ // header nor cookie supplied a value.
540
+ async function resolveWithClientPrecedence(
541
+ c: Context,
542
+ tenantResolver: TenantResolver | undefined,
543
+ clientTenant: { id: TenantId; source: "header" | "cookie" } | null,
544
+ ): Promise<ResolvedTenant | ResolveError> {
394
545
  if (clientTenant) {
395
546
  return { tenantId: clientTenant.id, source: clientTenant.source };
396
547
  }
397
-
398
- if (config.tenantResolver) {
399
- const resolved = await config.tenantResolver(c);
548
+ if (tenantResolver) {
549
+ const resolved = await tenantResolver(c);
400
550
  if (resolved !== null && resolved !== undefined) {
401
551
  return { tenantId: resolved, source: "resolver" };
402
552
  }
403
553
  }
554
+ return tenantRequiredError();
555
+ }
404
556
 
557
+ function tenantRequiredError(): ResolveError {
405
558
  return {
406
559
  error: {
407
560
  code: "tenant_required",
@@ -415,8 +568,63 @@ async function resolveTenant(
415
568
 
416
569
  type RejectArgs = {
417
570
  code: MiddlewareRejectCode;
418
- status: 400 | 401 | 403 | 404;
571
+ status: 400 | 401 | 403 | 404 | 410;
419
572
  message: string;
420
573
  i18nKey: string;
421
574
  details?: Record<string, unknown>;
422
575
  };
576
+
577
+ const TENANT_LIFECYCLE_BLOCKED = new Set([
578
+ "destroyRequested",
579
+ "destroying",
580
+ "destroyFailed",
581
+ "destroyed",
582
+ ]);
583
+ const TENANT_LIFECYCLE_CANCEL_QN = "tenant-lifecycle:write:cancel-destruction";
584
+
585
+ async function requestsCancelDestruction(c: Context): Promise<boolean> {
586
+ if (c.req.method !== "POST") return false;
587
+ try {
588
+ const path = c.req.path;
589
+ const body = (await c.req.raw.clone().json()) as {
590
+ type?: string;
591
+ commands?: Array<{ type?: string }>;
592
+ };
593
+ if (path === "/api/write") {
594
+ return body.type === TENANT_LIFECYCLE_CANCEL_QN;
595
+ }
596
+ if (path === "/api/batch") {
597
+ // every(), not some(): a batch mixing the cancel command with other
598
+ // writes must NOT wave the whole batch through the teardown gate —
599
+ // only a batch consisting solely of cancel-destruction is exempt.
600
+ return (
601
+ Array.isArray(body.commands) &&
602
+ body.commands.length > 0 &&
603
+ body.commands.every((command) => command.type === TENANT_LIFECYCLE_CANCEL_QN)
604
+ );
605
+ }
606
+ } catch {
607
+ return false;
608
+ }
609
+ return false;
610
+ }
611
+
612
+ async function rejectIfTenantTeardown(
613
+ c: Context,
614
+ tenantId: TenantId,
615
+ resolveTenantLifecycleStatus: TenantLifecycleStatusResolver | undefined,
616
+ ): Promise<Response | undefined> {
617
+ if (!resolveTenantLifecycleStatus) return undefined;
618
+ const lifecycle = await resolveTenantLifecycleStatus(tenantId);
619
+ if (!lifecycle || !TENANT_LIFECYCLE_BLOCKED.has(lifecycle.status)) return undefined;
620
+ if (lifecycle.status === "destroyRequested" && (await requestsCancelDestruction(c))) {
621
+ return undefined;
622
+ }
623
+ return middlewareReject(c, {
624
+ code: "tenant_unavailable",
625
+ status: 410,
626
+ message: `tenant "${tenantId}" is unavailable (${lifecycle.status})`,
627
+ i18nKey: "auth.errors.tenantUnavailable",
628
+ details: { tenantId, status: lifecycle.status },
629
+ });
630
+ }