@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,70 @@
1
+ // PII on custom-event payloads (#799). Entity CRUD events get their PII
2
+ // encrypted by the executor; events written via ctx.appendEvent / MSP-apply /
3
+ // low-level append() (delivery attempt-log, jobs run-logger) had no encrypt
4
+ // path at all. `r.defineEvent(name, schema, { piiFields })` declares which
5
+ // payload fields are PII and which payload field names the owning user;
6
+ // createRegistry publishes the catalog and append() — the single write funnel
7
+ // into kumiko_events — encrypts every catalogued field. No caller can forget.
8
+
9
+ import { requestContext } from "../api/request-context";
10
+ import type { EventPiiFields } from "../engine/types/handlers";
11
+ import { configuredPiiSubjectKms, encryptPiiValueForSubject } from "./pii-field-encryption";
12
+
13
+ export type EventPiiCatalog = ReadonlyMap<string, EventPiiFields>;
14
+
15
+ // Boot-injected like configurePiiSubjectKms — createRegistry calls this with
16
+ // the catalog collected from all defineEvent registrations.
17
+ let catalog: EventPiiCatalog = new Map();
18
+
19
+ export function configureEventPiiCatalog(next: EventPiiCatalog): void {
20
+ catalog = next;
21
+ }
22
+
23
+ export function configuredEventPiiCatalog(): EventPiiCatalog {
24
+ return catalog;
25
+ }
26
+
27
+ /** @internal test-only */
28
+ export function resetEventPiiCatalogForTests(): void {
29
+ catalog = new Map();
30
+ }
31
+
32
+ // Encrypts catalogued payload fields under the owning user's DEK. No-op when
33
+ // the event type is uncatalogued or no subject KMS is configured (plaintext
34
+ // rollout mode — the hard boot gate governs whether that is acceptable).
35
+ // A null/absent subject field (system cron runs, recipient-less skip
36
+ // attempts) leaves the value plaintext: there is no user key to shred.
37
+ export async function encryptEventPayloadPii(
38
+ eventType: string,
39
+ payload: Record<string, unknown>,
40
+ ): Promise<Record<string, unknown>> {
41
+ const piiFields = catalog.get(eventType);
42
+ if (!piiFields) return payload;
43
+ const kms = configuredPiiSubjectKms();
44
+ if (!kms) return payload;
45
+
46
+ let out: Record<string, unknown> | undefined;
47
+ for (const [field, spec] of Object.entries(piiFields)) {
48
+ const value = payload[field];
49
+ if (value === null || value === undefined) continue;
50
+ if (typeof value !== "string") {
51
+ throw new Error(
52
+ `Event "${eventType}" piiFields."${field}" must be a string payload field, got ${typeof value}`,
53
+ );
54
+ }
55
+ const subjectId = payload[spec.subjectField];
56
+ if (typeof subjectId !== "string" || subjectId.length === 0) continue;
57
+ const encrypted = await encryptPiiValueForSubject(
58
+ kms,
59
+ { kind: "user", userId: subjectId },
60
+ value,
61
+ { requestId: requestContext.get()?.requestId ?? "append-event" },
62
+ field,
63
+ );
64
+ if (encrypted !== value) {
65
+ out ??= { ...payload };
66
+ out[field] = encrypted;
67
+ }
68
+ }
69
+ return out ?? payload;
70
+ }
@@ -0,0 +1,50 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import {
3
+ KeyAlreadyExistsError,
4
+ KeyErasedError,
5
+ KeyNotFoundError,
6
+ type KmsHealth,
7
+ type LocalKeyKmsAdapter,
8
+ type SubjectDek,
9
+ type SubjectId,
10
+ type SubjectKey,
11
+ subjectIdToKey,
12
+ } from "./kms-adapter";
13
+
14
+ interface KeyEntry {
15
+ key: Buffer | null;
16
+ erased: boolean;
17
+ }
18
+
19
+ // Non-persistent adapter for tests and dev mode. Erased entries stay as
20
+ // tombstones so the create-after-erase contract holds within a process.
21
+ export class InMemoryKmsAdapter implements LocalKeyKmsAdapter {
22
+ readonly capabilities = { mode: "local-key" } as const;
23
+
24
+ private readonly keys = new Map<SubjectKey, KeyEntry>();
25
+
26
+ async createKey(subject: SubjectId): Promise<void> {
27
+ const subjectKey = subjectIdToKey(subject);
28
+ if (this.keys.has(subjectKey)) throw new KeyAlreadyExistsError(subject);
29
+ this.keys.set(subjectKey, { key: randomBytes(32), erased: false });
30
+ }
31
+
32
+ async getKey(subject: SubjectId): Promise<SubjectDek> {
33
+ const entry = this.keys.get(subjectIdToKey(subject));
34
+ if (!entry) throw new KeyNotFoundError(subject);
35
+ if (entry.erased || entry.key === null) throw new KeyErasedError(subject);
36
+ return entry.key;
37
+ }
38
+
39
+ async eraseKey(subject: SubjectId): Promise<void> {
40
+ const entry = this.keys.get(subjectIdToKey(subject));
41
+ // skip: eraseKey is contractually idempotent — unknown subject is a no-op
42
+ if (!entry) return;
43
+ entry.key = null;
44
+ entry.erased = true;
45
+ }
46
+
47
+ async health(): Promise<KmsHealth> {
48
+ return { ok: true, latencyMs: 0 };
49
+ }
50
+ }
@@ -0,0 +1,67 @@
1
+ export {
2
+ blindIndexFieldName,
3
+ collectLookupableFields,
4
+ computeBlindIndex,
5
+ computeBlindIndexValues,
6
+ configureBlindIndexKey,
7
+ configuredBlindIndexKey,
8
+ decodeBlindIndexKey,
9
+ resetBlindIndexKeyForTests,
10
+ } from "./blind-index";
11
+ export {
12
+ configuredEventPiiCatalog,
13
+ configureEventPiiCatalog,
14
+ type EventPiiCatalog,
15
+ encryptEventPayloadPii,
16
+ resetEventPiiCatalogForTests,
17
+ } from "./event-pii";
18
+ export { InMemoryKmsAdapter } from "./in-memory-kms-adapter";
19
+ export {
20
+ isLocalKeyKmsAdapter,
21
+ KeyAlreadyExistsError,
22
+ KeyErasedError,
23
+ KeyNotFoundError,
24
+ type KmsAdapter,
25
+ type KmsContext,
26
+ type KmsHealth,
27
+ type LocalKeyKmsAdapter,
28
+ type RemoteCryptoKmsAdapter,
29
+ type SubjectDek,
30
+ type SubjectId,
31
+ type SubjectKey,
32
+ subjectIdFromKey,
33
+ subjectIdToKey,
34
+ subjectKeyForTenant,
35
+ subjectKeyForUser,
36
+ } from "./kms-adapter";
37
+ export {
38
+ createPgKmsAdapter,
39
+ PgKmsAdapter,
40
+ type PgKmsAdapterOptions,
41
+ type RewrapOptions,
42
+ type RewrapResult,
43
+ rewrapSubjectKeys,
44
+ } from "./pg-kms-adapter";
45
+ export {
46
+ configuredPiiSubjectKms,
47
+ configurePiiSubjectKms,
48
+ decryptPiiFieldValues,
49
+ decryptPiiValueForSubject,
50
+ type EncryptPiiOptions,
51
+ encryptPiiFieldValues,
52
+ encryptPiiValueForSubject,
53
+ isPiiCiphertext,
54
+ PII_CIPHERTEXT_PREFIX,
55
+ PII_ERASED_SENTINEL,
56
+ resetPiiSubjectKmsForTests,
57
+ } from "./pii-field-encryption";
58
+ export {
59
+ createRequestKmsCache,
60
+ type RequestKmsCache,
61
+ } from "./request-kms-cache";
62
+ export {
63
+ collectPiiSubjectFields,
64
+ type ResolveSubjectOptions,
65
+ resolveSubjectForField,
66
+ SubjectResolutionError,
67
+ } from "./subject-resolver";
@@ -0,0 +1,2 @@
1
+ // Legacy path — re-exported for callers still importing this module directly.
2
+ export * from "@cosmicdrift/kumiko-types/kms-adapter-types";
@@ -0,0 +1,295 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
+ import postgres from "postgres";
3
+ import {
4
+ KeyAlreadyExistsError,
5
+ KeyErasedError,
6
+ KeyNotFoundError,
7
+ type KmsContext,
8
+ type KmsHealth,
9
+ type LocalKeyKmsAdapter,
10
+ type SubjectDek,
11
+ type SubjectId,
12
+ subjectIdToKey,
13
+ } from "./kms-adapter";
14
+
15
+ const IV_LENGTH = 12;
16
+ const TAG_LENGTH = 16;
17
+
18
+ // Envelope layout [iv:12][tag:16][ct] — the platform KEK wraps each subject
19
+ // DEK at rest, so a subject-keys-DB dump alone reveals no key material.
20
+ function wrapDek(kek: Buffer, dek: Buffer): Buffer {
21
+ const iv = randomBytes(IV_LENGTH);
22
+ const cipher = createCipheriv("aes-256-gcm", kek, iv);
23
+ const ciphertext = Buffer.concat([cipher.update(dek), cipher.final()]);
24
+ return Buffer.concat([iv, cipher.getAuthTag(), ciphertext]);
25
+ }
26
+
27
+ function unwrapDek(kek: Buffer, wrapped: Uint8Array): SubjectDek {
28
+ const envelope = Buffer.from(wrapped);
29
+ const iv = envelope.subarray(0, IV_LENGTH);
30
+ const tag = envelope.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
31
+ const ciphertext = envelope.subarray(IV_LENGTH + TAG_LENGTH);
32
+ const decipher = createDecipheriv("aes-256-gcm", kek, iv);
33
+ decipher.setAuthTag(tag);
34
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
35
+ }
36
+
37
+ function decodePlatformKek(base64: string): Buffer {
38
+ const kek = Buffer.from(base64, "base64");
39
+ if (kek.length !== 32) {
40
+ throw new Error(
41
+ `PgKmsAdapter: platformKek must decode to 32 bytes, got ${kek.length} — expected a base64-encoded AES-256 key (openssl rand -base64 32)`,
42
+ );
43
+ }
44
+ return kek;
45
+ }
46
+
47
+ const PG_UNIQUE_VIOLATION = "23505";
48
+
49
+ function isPgUniqueViolation(error: unknown): boolean {
50
+ return (
51
+ typeof error === "object" &&
52
+ error !== null &&
53
+ "code" in error &&
54
+ error.code === PG_UNIQUE_VIOLATION
55
+ );
56
+ }
57
+
58
+ interface SubjectKeyRow {
59
+ cipher_key: Uint8Array | null;
60
+ kek_version: number;
61
+ erased: boolean;
62
+ }
63
+
64
+ export interface PgKmsAdapterOptions {
65
+ /** Connection string of the DEDICATED subject-keys cluster — never the app DB (its backup retention must stay shorter, see kms-adapter.md). */
66
+ readonly databaseUrl: string;
67
+ /** Base64-encoded 32-byte platform KEK (PLATFORM_KEK env var). */
68
+ readonly platformKek: string;
69
+ /** Version of `platformKek` as stored in kumiko_subject_keys.kek_version.
70
+ * Bump on rotation; new wraps carry this version. Default 1. */
71
+ readonly kekVersion?: number;
72
+ /** Older KEKs by version, kept ONLY during a rotation window so rows not
73
+ * yet re-wrapped stay readable. Drop after rewrapSubjectKeys reports the
74
+ * estate fully on the active version (runbook: kek-rotation.md). */
75
+ readonly previousKeks?: Readonly<Record<number, string>>;
76
+ readonly maxConnections?: number;
77
+ }
78
+
79
+ // Default production adapter: DEKs live in a separate Postgres cluster,
80
+ // KEK-wrapped at rest. Schema is created lazily on first use — the
81
+ // subject-keys cluster has no drizzle migration pipeline of its own.
82
+ export class PgKmsAdapter implements LocalKeyKmsAdapter {
83
+ readonly capabilities = { mode: "local-key" } as const;
84
+
85
+ private readonly sql: ReturnType<typeof postgres>;
86
+ private readonly kek: Buffer;
87
+ private readonly kekVersion: number;
88
+ private readonly previousKeks: ReadonlyMap<number, Buffer>;
89
+ private schemaReady: Promise<void> | undefined;
90
+
91
+ constructor(options: PgKmsAdapterOptions) {
92
+ this.kek = decodePlatformKek(options.platformKek);
93
+ this.kekVersion = options.kekVersion ?? 1;
94
+ const previous = new Map<number, Buffer>();
95
+ for (const [version, kek] of Object.entries(options.previousKeks ?? {})) {
96
+ const v = Number(version);
97
+ if (v >= this.kekVersion) {
98
+ throw new Error(
99
+ `PgKmsAdapter: previousKeks[${v}] must be older than the active kekVersion ${this.kekVersion}`,
100
+ );
101
+ }
102
+ previous.set(v, decodePlatformKek(kek));
103
+ }
104
+ this.previousKeks = previous;
105
+ this.sql = postgres(options.databaseUrl, {
106
+ max: options.maxConnections ?? 4,
107
+ // The connection is exclusive to this adapter and its only DDL is
108
+ // idempotent IF NOT EXISTS — notices are pure boot-log noise.
109
+ onnotice: () => {},
110
+ });
111
+ }
112
+
113
+ async createKey(subject: SubjectId, ctx: KmsContext): Promise<void> {
114
+ await this.ensureSchema();
115
+ const wrapped = wrapDek(this.kek, randomBytes(32));
116
+ try {
117
+ await this.sql`
118
+ INSERT INTO kumiko_subject_keys (subject_id, cipher_key, kek_version, created_by)
119
+ VALUES (${subjectIdToKey(subject)}, ${wrapped}, ${this.kekVersion}, ${ctx.userId ?? null})`;
120
+ } catch (error) {
121
+ if (isPgUniqueViolation(error)) throw new KeyAlreadyExistsError(subject);
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ async getKey(subject: SubjectId, _ctx: KmsContext): Promise<SubjectDek> {
127
+ await this.ensureSchema();
128
+ const rows = await this.sql<SubjectKeyRow[]>`
129
+ SELECT cipher_key, kek_version, (erased_at IS NOT NULL) AS erased
130
+ FROM kumiko_subject_keys
131
+ WHERE subject_id = ${subjectIdToKey(subject)}`;
132
+ const row = rows[0];
133
+ if (!row) throw new KeyNotFoundError(subject);
134
+ if (row.erased || row.cipher_key === null) throw new KeyErasedError(subject);
135
+ return unwrapDek(this.kekFor(row.kek_version, subject), row.cipher_key);
136
+ }
137
+
138
+ // Rows keep working mid-rotation: not-yet-rewrapped DEKs name their KEK
139
+ // generation and unwrap with the matching previous key. A version with no
140
+ // configured key is a hard config error — NOT a shredded subject.
141
+ private kekFor(version: number, subject: SubjectId): Buffer {
142
+ if (version === this.kekVersion) return this.kek;
143
+ const previous = this.previousKeks.get(version);
144
+ if (previous) return previous;
145
+ throw new Error(
146
+ `PgKmsAdapter: subject ${subjectIdToKey(subject)} is wrapped with KEK version ${version} but only version ${this.kekVersion}${this.previousKeks.size > 0 ? ` (+previous ${[...this.previousKeks.keys()].join(",")})` : ""} is configured — pass previousKeks during rotation (runbook: kek-rotation.md).`,
147
+ );
148
+ }
149
+
150
+ async eraseKey(subject: SubjectId, ctx: KmsContext): Promise<void> {
151
+ await this.ensureSchema();
152
+ // Guard on erased_at IS NULL keeps repeat erases from overwriting the
153
+ // original tombstone audit fields; unknown subjects update zero rows.
154
+ await this.sql`
155
+ UPDATE kumiko_subject_keys
156
+ SET cipher_key = NULL,
157
+ erased_at = now(),
158
+ erased_by = ${ctx.userId ?? null},
159
+ erase_reason = ${ctx.eraseReason ?? null}
160
+ WHERE subject_id = ${subjectIdToKey(subject)} AND erased_at IS NULL`;
161
+ }
162
+
163
+ async health(): Promise<KmsHealth> {
164
+ const start = Date.now();
165
+ await this.ensureSchema();
166
+ await this.sql`SELECT 1`;
167
+ return { ok: true, latencyMs: Date.now() - start };
168
+ }
169
+
170
+ async close(): Promise<void> {
171
+ await this.sql.end();
172
+ }
173
+
174
+ private ensureSchema(): Promise<void> {
175
+ // A failed attempt resets the memo so a transient DB outage does not
176
+ // poison the adapter for the rest of the process lifetime.
177
+ this.schemaReady ??= this.createSchema().catch((error) => {
178
+ this.schemaReady = undefined;
179
+ throw error;
180
+ });
181
+ return this.schemaReady;
182
+ }
183
+
184
+ private async createSchema(): Promise<void> {
185
+ await this.sql`
186
+ CREATE TABLE IF NOT EXISTS kumiko_subject_keys (
187
+ subject_id TEXT PRIMARY KEY,
188
+ cipher_key BYTEA,
189
+ kek_version INTEGER NOT NULL DEFAULT 1,
190
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
191
+ created_by TEXT,
192
+ erased_at TIMESTAMPTZ,
193
+ erased_by TEXT,
194
+ erase_reason TEXT
195
+ )`;
196
+ await this.sql`
197
+ CREATE INDEX IF NOT EXISTS kumiko_subject_keys_erased_idx
198
+ ON kumiko_subject_keys (erased_at) WHERE erased_at IS NOT NULL`;
199
+ await this.sql`
200
+ CREATE INDEX IF NOT EXISTS kumiko_subject_keys_audit_idx
201
+ ON kumiko_subject_keys (created_at, erased_at)`;
202
+ }
203
+ }
204
+
205
+ export function createPgKmsAdapter(options: PgKmsAdapterOptions): PgKmsAdapter {
206
+ return new PgKmsAdapter(options);
207
+ }
208
+
209
+ export interface RewrapOptions {
210
+ readonly databaseUrl: string;
211
+ /** Older KEKs by version — every version still present in the table must be covered. */
212
+ readonly fromKeks: Readonly<Record<number, string>>;
213
+ /** The new active KEK and its version (previous active version + 1). */
214
+ readonly toKek: string;
215
+ readonly toKekVersion: number;
216
+ readonly batchSize?: number;
217
+ readonly dryRun?: boolean;
218
+ }
219
+
220
+ export interface RewrapResult {
221
+ readonly scanned: number;
222
+ readonly rewrapped: number;
223
+ readonly skippedErased: number;
224
+ readonly failures: ReadonlyArray<{ readonly subjectId: string; readonly reason: string }>;
225
+ }
226
+
227
+ // One-time KEK rotation (runbook: kek-rotation.md). Unwraps every live DEK
228
+ // wrapped with an older KEK generation and re-wraps it under the new KEK,
229
+ // bumping kek_version. Idempotent: rows already on toKekVersion are not
230
+ // selected; a second run reports 0. Erased tombstones (cipher_key NULL) are
231
+ // never touched. The UPDATE is guarded on the row's old kek_version, so a
232
+ // concurrent app writing fresh keys on the new version is never clobbered.
233
+ export async function rewrapSubjectKeys(options: RewrapOptions): Promise<RewrapResult> {
234
+ const toKek = decodePlatformKek(options.toKek);
235
+ const fromKeks = new Map<number, Buffer>();
236
+ for (const [version, kek] of Object.entries(options.fromKeks)) {
237
+ fromKeks.set(Number(version), decodePlatformKek(kek));
238
+ }
239
+ const batchSize = options.batchSize ?? 200;
240
+ const sql = postgres(options.databaseUrl, { max: 2, onnotice: () => {} });
241
+
242
+ const result = {
243
+ scanned: 0,
244
+ rewrapped: 0,
245
+ skippedErased: 0,
246
+ failures: [] as Array<{ subjectId: string; reason: string }>,
247
+ };
248
+ try {
249
+ let cursor = "";
250
+ for (;;) {
251
+ const rows = await sql<
252
+ Array<{ subject_id: string; cipher_key: Uint8Array | null; kek_version: number }>
253
+ >`
254
+ SELECT subject_id, cipher_key, kek_version
255
+ FROM kumiko_subject_keys
256
+ WHERE kek_version <> ${options.toKekVersion} AND subject_id > ${cursor}
257
+ ORDER BY subject_id ASC
258
+ LIMIT ${batchSize}`;
259
+ if (rows.length === 0) break;
260
+
261
+ for (const row of rows) {
262
+ result.scanned++;
263
+ if (row.cipher_key === null) {
264
+ result.skippedErased++;
265
+ continue;
266
+ }
267
+ try {
268
+ const fromKek = fromKeks.get(row.kek_version);
269
+ if (!fromKek) {
270
+ throw new Error(`no fromKek configured for kek_version ${row.kek_version}`);
271
+ }
272
+ const rewrapped = wrapDek(toKek, unwrapDek(fromKek, row.cipher_key));
273
+ if (!options.dryRun) {
274
+ await sql`
275
+ UPDATE kumiko_subject_keys
276
+ SET cipher_key = ${rewrapped}, kek_version = ${options.toKekVersion}
277
+ WHERE subject_id = ${row.subject_id} AND kek_version = ${row.kek_version}`;
278
+ }
279
+ result.rewrapped++;
280
+ } catch (error) {
281
+ result.failures.push({
282
+ subjectId: row.subject_id,
283
+ reason: error instanceof Error ? error.message : String(error),
284
+ });
285
+ }
286
+ }
287
+ const last = rows[rows.length - 1];
288
+ if (last === undefined) break;
289
+ cursor = last.subject_id;
290
+ }
291
+ } finally {
292
+ await sql.end();
293
+ }
294
+ return result;
295
+ }