@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,180 @@
1
+ // KEK rotation (#818 step 7): mid-rotation an adapter with the NEW active
2
+ // KEK + previousKeks reads old-generation rows; rewrapSubjectKeys migrates
3
+ // the estate to the new generation; afterwards previousKeks can be dropped.
4
+ // Erased tombstones stay untouched, unknown generations fail loud as a
5
+ // CONFIG error (not a shredded subject).
6
+
7
+ import { afterAll, beforeEach, describe, expect, test } from "bun:test";
8
+ import { randomUUID } from "node:crypto";
9
+ import postgres from "postgres";
10
+ import { createTestDb } from "../../stack/db";
11
+ import type { KmsContext, SubjectId } from "../kms-adapter";
12
+ import { createPgKmsAdapter, rewrapSubjectKeys } from "../pg-kms-adapter";
13
+
14
+ const baseUrl = process.env["TEST_DATABASE_URL"];
15
+ if (!baseUrl) throw new Error("Missing required env var: TEST_DATABASE_URL");
16
+
17
+ const testDb = await createTestDb();
18
+ const DB_URL = baseUrl.replace(/\/[^/]+$/, `/${testDb.dbName}`);
19
+
20
+ const KEK_V1 = Buffer.alloc(32, 1).toString("base64");
21
+ const KEK_V2 = Buffer.alloc(32, 2).toString("base64");
22
+ const ctx: KmsContext = { requestId: "kek-rotation-test" };
23
+ const freshUser = (): SubjectId => ({ kind: "user", userId: randomUUID() });
24
+
25
+ const raw = postgres(DB_URL, { max: 1, onnotice: () => {} });
26
+
27
+ afterAll(async () => {
28
+ await raw.end();
29
+ await testDb.cleanup();
30
+ });
31
+
32
+ beforeEach(async () => {
33
+ const boot = createPgKmsAdapter({ databaseUrl: DB_URL, platformKek: KEK_V1 });
34
+ await boot.health();
35
+ await boot.close();
36
+ await raw`TRUNCATE kumiko_subject_keys`;
37
+ });
38
+
39
+ async function seedV1Keys(count: number): Promise<SubjectId[]> {
40
+ const v1 = createPgKmsAdapter({ databaseUrl: DB_URL, platformKek: KEK_V1 });
41
+ const subjects: SubjectId[] = [];
42
+ for (let i = 0; i < count; i++) {
43
+ const subject = freshUser();
44
+ await v1.createKey(subject, ctx);
45
+ subjects.push(subject);
46
+ }
47
+ await v1.close();
48
+ return subjects;
49
+ }
50
+
51
+ describe("KEK rotation", () => {
52
+ test("mid-rotation adapter (new active + previousKeks) reads v1 rows; new writes carry v2", async () => {
53
+ const [oldSubject] = await seedV1Keys(1);
54
+ if (!oldSubject) throw new Error("seed failed");
55
+
56
+ const rotating = createPgKmsAdapter({
57
+ databaseUrl: DB_URL,
58
+ platformKek: KEK_V2,
59
+ kekVersion: 2,
60
+ previousKeks: { 1: KEK_V1 },
61
+ });
62
+ const dek = await rotating.getKey(oldSubject, ctx);
63
+ expect(dek.length).toBe(32);
64
+
65
+ const newSubject = freshUser();
66
+ await rotating.createKey(newSubject, ctx);
67
+ const versions = await raw<Array<{ kek_version: number }>>`
68
+ SELECT kek_version FROM kumiko_subject_keys ORDER BY kek_version`;
69
+ expect(versions.map((r) => r.kek_version)).toEqual([1, 2]);
70
+ await rotating.close();
71
+ });
72
+
73
+ test("unknown generation without previousKeks is a loud config error", async () => {
74
+ const [oldSubject] = await seedV1Keys(1);
75
+ if (!oldSubject) throw new Error("seed failed");
76
+
77
+ const misconfigured = createPgKmsAdapter({
78
+ databaseUrl: DB_URL,
79
+ platformKek: KEK_V2,
80
+ kekVersion: 2,
81
+ });
82
+ expect(misconfigured.getKey(oldSubject, ctx)).rejects.toThrow(/pass previousKeks/);
83
+ await misconfigured.close();
84
+ });
85
+
86
+ test("previousKeks newer than the active version is rejected at construction", () => {
87
+ expect(() =>
88
+ createPgKmsAdapter({
89
+ databaseUrl: DB_URL,
90
+ platformKek: KEK_V1,
91
+ kekVersion: 1,
92
+ previousKeks: { 2: KEK_V2 },
93
+ }),
94
+ ).toThrow(/must be older/);
95
+ });
96
+
97
+ test("rewrapSubjectKeys migrates the estate; DEKs stay identical; idempotent", async () => {
98
+ const subjects = await seedV1Keys(3);
99
+ const v1 = createPgKmsAdapter({ databaseUrl: DB_URL, platformKek: KEK_V1 });
100
+ const before = await Promise.all(subjects.map((s) => v1.getKey(s, ctx)));
101
+ await v1.close();
102
+
103
+ const result = await rewrapSubjectKeys({
104
+ databaseUrl: DB_URL,
105
+ fromKeks: { 1: KEK_V1 },
106
+ toKek: KEK_V2,
107
+ toKekVersion: 2,
108
+ batchSize: 2,
109
+ });
110
+ expect(result.failures).toEqual([]);
111
+ expect(result.rewrapped).toBe(3);
112
+
113
+ // The rotated estate is fully readable with ONLY the new KEK — and the
114
+ // unwrapped DEKs are byte-identical (stored ciphertext stays decryptable).
115
+ const v2 = createPgKmsAdapter({ databaseUrl: DB_URL, platformKek: KEK_V2, kekVersion: 2 });
116
+ const after = await Promise.all(subjects.map((s) => v2.getKey(s, ctx)));
117
+ for (const [i, dek] of after.entries()) {
118
+ expect(dek.equals(before[i] as Buffer)).toBe(true);
119
+ }
120
+ await v2.close();
121
+
122
+ const second = await rewrapSubjectKeys({
123
+ databaseUrl: DB_URL,
124
+ fromKeks: { 1: KEK_V1 },
125
+ toKek: KEK_V2,
126
+ toKekVersion: 2,
127
+ });
128
+ expect(second.scanned).toBe(0);
129
+ expect(second.rewrapped).toBe(0);
130
+ });
131
+
132
+ test("erased tombstones are skipped and stay erased", async () => {
133
+ const [gone] = await seedV1Keys(1);
134
+ if (!gone) throw new Error("seed failed");
135
+ const v1 = createPgKmsAdapter({ databaseUrl: DB_URL, platformKek: KEK_V1 });
136
+ await v1.eraseKey(gone, ctx);
137
+ await v1.close();
138
+
139
+ const result = await rewrapSubjectKeys({
140
+ databaseUrl: DB_URL,
141
+ fromKeks: { 1: KEK_V1 },
142
+ toKek: KEK_V2,
143
+ toKekVersion: 2,
144
+ });
145
+ expect(result.skippedErased).toBe(1);
146
+ expect(result.rewrapped).toBe(0);
147
+
148
+ const rows = await raw<Array<{ cipher_key: Uint8Array | null }>>`
149
+ SELECT cipher_key FROM kumiko_subject_keys`;
150
+ expect(rows[0]?.cipher_key).toBeNull();
151
+ });
152
+
153
+ test("dryRun counts but writes nothing", async () => {
154
+ await seedV1Keys(2);
155
+ const dry = await rewrapSubjectKeys({
156
+ databaseUrl: DB_URL,
157
+ fromKeks: { 1: KEK_V1 },
158
+ toKek: KEK_V2,
159
+ toKekVersion: 2,
160
+ dryRun: true,
161
+ });
162
+ expect(dry.rewrapped).toBe(2);
163
+ const versions = await raw<Array<{ kek_version: number }>>`
164
+ SELECT DISTINCT kek_version FROM kumiko_subject_keys`;
165
+ expect(versions.map((r) => r.kek_version)).toEqual([1]);
166
+ });
167
+
168
+ test("missing fromKek for a generation lands in failures, not a crash", async () => {
169
+ await seedV1Keys(1);
170
+ const result = await rewrapSubjectKeys({
171
+ databaseUrl: DB_URL,
172
+ fromKeks: {},
173
+ toKek: KEK_V2,
174
+ toKekVersion: 2,
175
+ });
176
+ expect(result.rewrapped).toBe(0);
177
+ expect(result.failures).toHaveLength(1);
178
+ expect(result.failures[0]?.reason).toContain("no fromKek configured");
179
+ });
180
+ });
@@ -0,0 +1,134 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ isLocalKeyKmsAdapter,
4
+ KeyAlreadyExistsError,
5
+ KeyErasedError,
6
+ KeyNotFoundError,
7
+ type KmsAdapter,
8
+ type KmsContext,
9
+ type SubjectId,
10
+ } from "../kms-adapter";
11
+
12
+ const ctx: KmsContext = { requestId: "contract-test" };
13
+
14
+ const userA: SubjectId = { kind: "user", userId: "6b2f4a0e-1c9d-4f3a-9d2e-000000000001" };
15
+ const userB: SubjectId = { kind: "user", userId: "6b2f4a0e-1c9d-4f3a-9d2e-000000000002" };
16
+ const tenantWithUserAId: SubjectId = {
17
+ kind: "tenant",
18
+ tenantId: "6b2f4a0e-1c9d-4f3a-9d2e-000000000001",
19
+ };
20
+
21
+ async function subjectDek(adapter: KmsAdapter, subject: SubjectId): Promise<Buffer> {
22
+ if (isLocalKeyKmsAdapter(adapter)) return adapter.getKey(subject, ctx);
23
+ // remote-crypto adapters never release keys — probe via roundtrip instead.
24
+ const probe = Buffer.from("contract-probe");
25
+ const encrypted = await adapter.encrypt(subject, probe, ctx);
26
+ return Buffer.from(await adapter.decrypt(subject, encrypted, ctx));
27
+ }
28
+
29
+ export function describeKmsAdapterContract(
30
+ name: string,
31
+ factory: () => KmsAdapter | Promise<KmsAdapter>,
32
+ ): void {
33
+ describe(`${name} — KmsAdapter contract`, () => {
34
+ let adapter: KmsAdapter;
35
+
36
+ beforeEach(async () => {
37
+ adapter = await factory();
38
+ });
39
+
40
+ test("createKey then getKey returns a 32-byte DEK", async () => {
41
+ await adapter.createKey(userA, ctx);
42
+ if (!isLocalKeyKmsAdapter(adapter)) return;
43
+ const dek = await adapter.getKey(userA, ctx);
44
+ expect(dek.length).toBe(32);
45
+ });
46
+
47
+ test("getKey is stable — same subject, same key", async () => {
48
+ await adapter.createKey(userA, ctx);
49
+ if (!isLocalKeyKmsAdapter(adapter)) return;
50
+ const first = await adapter.getKey(userA, ctx);
51
+ const second = await adapter.getKey(userA, ctx);
52
+ expect(first.equals(second)).toBe(true);
53
+ });
54
+
55
+ test("distinct subjects get distinct keys", async () => {
56
+ await adapter.createKey(userA, ctx);
57
+ await adapter.createKey(userB, ctx);
58
+ if (!isLocalKeyKmsAdapter(adapter)) return;
59
+ const a = await adapter.getKey(userA, ctx);
60
+ const b = await adapter.getKey(userB, ctx);
61
+ expect(a.equals(b)).toBe(false);
62
+ });
63
+
64
+ test("user and tenant subjects with the same raw id are distinct", async () => {
65
+ await adapter.createKey(userA, ctx);
66
+ await adapter.createKey(tenantWithUserAId, ctx);
67
+ if (!isLocalKeyKmsAdapter(adapter)) return;
68
+ const user = await adapter.getKey(userA, ctx);
69
+ const tenant = await adapter.getKey(tenantWithUserAId, ctx);
70
+ expect(user.equals(tenant)).toBe(false);
71
+ });
72
+
73
+ test("createKey twice throws KeyAlreadyExistsError", async () => {
74
+ await adapter.createKey(userA, ctx);
75
+ await expect(adapter.createKey(userA, ctx)).rejects.toBeInstanceOf(KeyAlreadyExistsError);
76
+ });
77
+
78
+ test("getKey for an unknown subject throws KeyNotFoundError", async () => {
79
+ if (!isLocalKeyKmsAdapter(adapter)) return;
80
+ await expect(adapter.getKey(userA, ctx)).rejects.toBeInstanceOf(KeyNotFoundError);
81
+ });
82
+
83
+ test("eraseKey makes getKey throw KeyErasedError", async () => {
84
+ await adapter.createKey(userA, ctx);
85
+ await adapter.eraseKey(userA, ctx);
86
+ if (!isLocalKeyKmsAdapter(adapter)) return;
87
+ await expect(adapter.getKey(userA, ctx)).rejects.toBeInstanceOf(KeyErasedError);
88
+ });
89
+
90
+ test("eraseKey is idempotent — second call is a no-op", async () => {
91
+ await adapter.createKey(userA, ctx);
92
+ await adapter.eraseKey(userA, ctx);
93
+ await adapter.eraseKey(userA, ctx);
94
+ if (!isLocalKeyKmsAdapter(adapter)) return;
95
+ await expect(adapter.getKey(userA, ctx)).rejects.toBeInstanceOf(KeyErasedError);
96
+ });
97
+
98
+ test("eraseKey for an unknown subject is a no-op", async () => {
99
+ await adapter.eraseKey(userA, ctx);
100
+ if (!isLocalKeyKmsAdapter(adapter)) return;
101
+ await expect(adapter.getKey(userA, ctx)).rejects.toBeInstanceOf(KeyNotFoundError);
102
+ });
103
+
104
+ test("createKey after eraseKey throws — the tombstone blocks a new key", async () => {
105
+ await adapter.createKey(userA, ctx);
106
+ await adapter.eraseKey(userA, ctx);
107
+ await expect(adapter.createKey(userA, ctx)).rejects.toBeInstanceOf(KeyAlreadyExistsError);
108
+ });
109
+
110
+ test("erasing one subject leaves other subjects intact", async () => {
111
+ await adapter.createKey(userA, ctx);
112
+ await adapter.createKey(userB, ctx);
113
+ await adapter.eraseKey(userA, ctx);
114
+ const dek = await subjectDek(adapter, userB);
115
+ expect(dek.length).toBeGreaterThan(0);
116
+ });
117
+
118
+ test("remote-crypto adapters roundtrip encrypt/decrypt", async () => {
119
+ if (isLocalKeyKmsAdapter(adapter)) return;
120
+ await adapter.createKey(userA, ctx);
121
+ const plaintext = Buffer.from("marc@example.com");
122
+ const ciphertext = await adapter.encrypt(userA, plaintext, ctx);
123
+ expect(Buffer.from(ciphertext).equals(plaintext)).toBe(false);
124
+ const roundtrip = await adapter.decrypt(userA, ciphertext, ctx);
125
+ expect(Buffer.from(roundtrip).equals(plaintext)).toBe(true);
126
+ });
127
+
128
+ test("health reports ok for a reachable adapter", async () => {
129
+ const health = await adapter.health();
130
+ expect(health.ok).toBe(true);
131
+ expect(health.latencyMs).toBeGreaterThanOrEqual(0);
132
+ });
133
+ });
134
+ }
@@ -0,0 +1,4 @@
1
+ import { InMemoryKmsAdapter } from "../in-memory-kms-adapter";
2
+ import { describeKmsAdapterContract } from "./kms-adapter-contract";
3
+
4
+ describeKmsAdapterContract("InMemoryKmsAdapter", () => new InMemoryKmsAdapter());
@@ -0,0 +1,117 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { randomBytes, randomUUID } from "node:crypto";
3
+ import postgres from "postgres";
4
+ import { createTestDb } from "../../stack/db";
5
+ import { type KmsContext, type SubjectId, subjectIdToKey } from "../kms-adapter";
6
+ import { PgKmsAdapter } from "../pg-kms-adapter";
7
+ import { describeKmsAdapterContract } from "./kms-adapter-contract";
8
+
9
+ const baseUrl = process.env["TEST_DATABASE_URL"];
10
+ if (!baseUrl) throw new Error("Missing required env var: TEST_DATABASE_URL");
11
+
12
+ const testDb = await createTestDb();
13
+ const databaseUrl = baseUrl.replace(/\/[^/]+$/, `/${testDb.dbName}`);
14
+ const platformKek = randomBytes(32).toString("base64");
15
+ const adapter = new PgKmsAdapter({ databaseUrl, platformKek, maxConnections: 2 });
16
+ const raw = postgres(databaseUrl, { max: 1 });
17
+
18
+ afterAll(async () => {
19
+ await adapter.close();
20
+ await raw.end();
21
+ await testDb.cleanup();
22
+ });
23
+
24
+ describeKmsAdapterContract("PgKmsAdapter", async () => {
25
+ // health() creates the schema lazily, so the truncate cannot hit a
26
+ // missing table on the first run; contract tests reuse fixed subject ids.
27
+ await adapter.health();
28
+ await raw`TRUNCATE kumiko_subject_keys`;
29
+ return adapter;
30
+ });
31
+
32
+ describe("PgKmsAdapter — pg specifics", () => {
33
+ const ctx: KmsContext = { requestId: "pg-kms-test" };
34
+ const freshUser = (): SubjectId => ({ kind: "user", userId: randomUUID() });
35
+
36
+ test("rejects a platformKek that does not decode to 32 bytes", () => {
37
+ expect(
38
+ () =>
39
+ new PgKmsAdapter({
40
+ databaseUrl,
41
+ platformKek: Buffer.from("too-short").toString("base64"),
42
+ }),
43
+ ).toThrow("32 bytes");
44
+ });
45
+
46
+ test("DEKs survive across adapter instances", async () => {
47
+ const user = freshUser();
48
+ await adapter.createKey(user, ctx);
49
+ const dek = await adapter.getKey(user, ctx);
50
+
51
+ const second = new PgKmsAdapter({ databaseUrl, platformKek, maxConnections: 1 });
52
+ try {
53
+ const rehydrated = await second.getKey(user, ctx);
54
+ expect(rehydrated.equals(dek)).toBe(true);
55
+ } finally {
56
+ await second.close();
57
+ }
58
+ });
59
+
60
+ test("a different platform KEK cannot unwrap stored DEKs", async () => {
61
+ const user = freshUser();
62
+ await adapter.createKey(user, ctx);
63
+
64
+ const wrongKek = new PgKmsAdapter({
65
+ databaseUrl,
66
+ platformKek: randomBytes(32).toString("base64"),
67
+ maxConnections: 1,
68
+ });
69
+ try {
70
+ await expect(wrongKek.getKey(user, ctx)).rejects.toThrow();
71
+ } finally {
72
+ await wrongKek.close();
73
+ }
74
+ });
75
+
76
+ test("erase leaves an audit tombstone without key material", async () => {
77
+ const user = freshUser();
78
+ await adapter.createKey(user, ctx);
79
+ await adapter.eraseKey(user, {
80
+ requestId: "pg-kms-test",
81
+ userId: "operator-1",
82
+ eraseReason: "user-forget",
83
+ });
84
+
85
+ const rows = await raw`
86
+ SELECT cipher_key, erased_at, erased_by, erase_reason
87
+ FROM kumiko_subject_keys
88
+ WHERE subject_id = ${subjectIdToKey(user)}`;
89
+ expect(rows.length).toBe(1);
90
+ expect(rows[0]?.["cipher_key"]).toBeNull();
91
+ expect(rows[0]?.["erased_at"]).not.toBeNull();
92
+ expect(rows[0]?.["erased_by"]).toBe("operator-1");
93
+ expect(rows[0]?.["erase_reason"]).toBe("user-forget");
94
+ });
95
+
96
+ test("repeat erase keeps the original tombstone audit fields", async () => {
97
+ const user = freshUser();
98
+ await adapter.createKey(user, ctx);
99
+ await adapter.eraseKey(user, {
100
+ requestId: "pg-kms-test",
101
+ userId: "operator-1",
102
+ eraseReason: "user-forget",
103
+ });
104
+ await adapter.eraseKey(user, {
105
+ requestId: "pg-kms-test",
106
+ userId: "operator-2",
107
+ eraseReason: "tenant-destroy",
108
+ });
109
+
110
+ const rows = await raw`
111
+ SELECT erased_by, erase_reason
112
+ FROM kumiko_subject_keys
113
+ WHERE subject_id = ${subjectIdToKey(user)}`;
114
+ expect(rows[0]?.["erased_by"]).toBe("operator-1");
115
+ expect(rows[0]?.["erase_reason"]).toBe("user-forget");
116
+ });
117
+ });