@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,229 @@
1
+ // Unit tests for r.storeTable() — declaration-time validation + registry
2
+ // aggregation. Post-drizzle-cut merge of the former r.storeTable() (legacy
3
+ // Drizzle PgTable) and r.unmanagedTable() (EntityTableMeta) — this file
4
+ // absorbs the former unmanaged-table.test.ts coverage under the new name.
5
+ // Full DB roundtrip (setupTestStack pushes the table → INSERT / SELECT)
6
+ // lives in src/__tests__/store-table.integration.test.ts.
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import {
10
+ buildEntityTableMeta,
11
+ defineUnmanagedTable,
12
+ resolveTableName,
13
+ } from "../../db/entity-table-meta";
14
+ import { defineFeature } from "../define-feature";
15
+ import { createEntity, createTextField } from "../index";
16
+ import { createRegistry } from "../registry";
17
+
18
+ const probeMeta = defineUnmanagedTable({
19
+ tableName: "rt_probe",
20
+ columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
21
+ });
22
+ const probeMetaTwo = defineUnmanagedTable({
23
+ tableName: "rt_probe_two",
24
+ columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
25
+ });
26
+
27
+ describe("r.storeTable — declaration", () => {
28
+ test("rejects an invalid table name", () => {
29
+ const badMeta = defineUnmanagedTable({
30
+ tableName: "BadName",
31
+ columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
32
+ });
33
+ expect(() =>
34
+ defineFeature("probe", (r) => {
35
+ r.storeTable(badMeta, { reason: "test" });
36
+ }),
37
+ ).toThrow(/must be a valid identifier/);
38
+ });
39
+
40
+ test("rejects duplicate registrations within one feature", () => {
41
+ expect(() =>
42
+ defineFeature("probe", (r) => {
43
+ r.storeTable(probeMeta, { reason: "test" });
44
+ r.storeTable(probeMeta, { reason: "test" });
45
+ }),
46
+ ).toThrow(/already registered/);
47
+ });
48
+
49
+ test("rejects empty reason", () => {
50
+ expect(() =>
51
+ defineFeature("probe", (r) => {
52
+ r.storeTable(probeMeta, { reason: "" });
53
+ }),
54
+ ).toThrow(/options\.reason must be a non-empty string/);
55
+ });
56
+
57
+ test("rejects whitespace-only reason", () => {
58
+ expect(() =>
59
+ defineFeature("probe", (r) => {
60
+ r.storeTable(probeMeta, { reason: " " });
61
+ }),
62
+ ).toThrow(/options\.reason must be a non-empty string/);
63
+ });
64
+
65
+ test("rejects an EntityTableMeta whose source is not 'unmanaged' (#1209)", () => {
66
+ const managedEntity = createEntity({
67
+ table: "rt_probe_managed",
68
+ fields: { name: createTextField() },
69
+ });
70
+ const managedMeta = buildEntityTableMeta("rt-probe-managed", managedEntity);
71
+ expect(() =>
72
+ defineFeature("probe", (r) => {
73
+ r.storeTable(managedMeta, { reason: "test" });
74
+ }),
75
+ ).toThrow(/requires source: "unmanaged"/);
76
+ });
77
+
78
+ test("rejects a table name with the reserved read_ prefix (#1220)", () => {
79
+ const readPrefixed = defineUnmanagedTable({
80
+ tableName: "read_rt_probe",
81
+ columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
82
+ });
83
+ expect(() =>
84
+ defineFeature("probe", (r) => {
85
+ r.storeTable(readPrefixed, { reason: "test" });
86
+ }),
87
+ ).toThrow(/the "read_" prefix is reserved/);
88
+ });
89
+
90
+ test("accepts valid registration and stores meta + reason", () => {
91
+ const feature = defineFeature("probe", (r) => {
92
+ r.storeTable(probeMeta, {
93
+ reason: "read-side projection of an event-stream",
94
+ });
95
+ });
96
+ expect(feature.storeTables).toHaveProperty("rt_probe");
97
+ expect(feature.storeTables["rt_probe"]?.reason).toBe("read-side projection of an event-stream");
98
+ expect(feature.storeTables["rt_probe"]?.meta).toBe(probeMeta);
99
+ });
100
+
101
+ test("two store tables on one feature register under their tableName", () => {
102
+ const feature = defineFeature("dual", (r) => {
103
+ r.storeTable(probeMeta, { reason: "one" });
104
+ r.storeTable(probeMetaTwo, { reason: "two" });
105
+ });
106
+ expect(Object.keys(feature.storeTables).sort()).toEqual(["rt_probe", "rt_probe_two"]);
107
+ });
108
+
109
+ test("absent storeTables on a feature is ok", () => {
110
+ const feat = defineFeature("plain", () => {
111
+ // no r.storeTable calls
112
+ });
113
+ expect(feat.storeTables).toEqual({});
114
+ });
115
+ });
116
+
117
+ describe("createRegistry — storeTable aggregation", () => {
118
+ test("aggregates store tables across features and tags featureName", () => {
119
+ const featA = defineFeature("billing", (r) => {
120
+ r.storeTable(probeMeta, { reason: "external API cache" });
121
+ });
122
+ const featB = defineFeature("inventory", (r) => {
123
+ r.storeTable(probeMetaTwo, { reason: "imported pre-ES" });
124
+ });
125
+
126
+ const registry = createRegistry([featA, featB]);
127
+ const all = registry.getAllStoreTables();
128
+
129
+ expect(all.size).toBe(2);
130
+ expect(all.get("rt_probe")?.featureName).toBe("billing");
131
+ expect(all.get("rt_probe")?.reason).toBe("external API cache");
132
+ expect(all.get("rt_probe_two")?.featureName).toBe("inventory");
133
+ });
134
+
135
+ test("rejects cross-feature tableName collisions at boot", () => {
136
+ // Two features can't share the same physical tableName — migrate-runner
137
+ // would race two CREATE TABLE statements. Boot-validator catches it.
138
+ const featA = defineFeature("a", (r) => {
139
+ r.storeTable(probeMeta, { reason: "first" });
140
+ });
141
+ const featB = defineFeature("b", (r) => {
142
+ r.storeTable(probeMeta, { reason: "second" });
143
+ });
144
+ expect(() => createRegistry([featA, featB])).toThrow(
145
+ /Store-table "rt_probe" registered by both feature "a" and "b"/,
146
+ );
147
+ });
148
+
149
+ test("two features with distinct tableNames register cleanly", () => {
150
+ const featA = defineFeature("a", (r) => {
151
+ r.storeTable(probeMeta, { reason: "first" });
152
+ });
153
+ const featB = defineFeature("b", (r) => {
154
+ r.storeTable(probeMetaTwo, { reason: "second" });
155
+ });
156
+ expect(() => createRegistry([featA, featB])).not.toThrow();
157
+ });
158
+
159
+ test("rejects a store-table that collides with an entity's physical name", () => {
160
+ // Custom `table` override without the read_ prefix — resolveTableName's
161
+ // default would carry read_, which r.storeTable now rejects outright
162
+ // (#1220), so the collision case needs a table name storeTable can
163
+ // actually register.
164
+ const widget = createEntity({ table: "shop_widgets", fields: { name: createTextField() } });
165
+ // resolveTableName mirrors the migrate-runner — pin the exact physical name.
166
+ const physical = resolveTableName("widget", widget, "shop");
167
+ const clashing = defineUnmanagedTable({
168
+ tableName: physical,
169
+ columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
170
+ });
171
+ const entityFeature = defineFeature("shop", (r) => {
172
+ r.entity("widget", widget);
173
+ });
174
+ const tableFeature = defineFeature("other", (r) => {
175
+ r.storeTable(clashing, { reason: "clash" });
176
+ });
177
+
178
+ // Entity registered first, then the colliding store table.
179
+ expect(() => createRegistry([entityFeature, tableFeature])).toThrow(
180
+ new RegExp(`Store-table "${physical}".*collides with the physical table of entity "widget"`),
181
+ );
182
+
183
+ // Order-independent: store table registered first, then the entity.
184
+ expect(() => createRegistry([tableFeature, entityFeature])).toThrow(
185
+ new RegExp(`Entity "widget".*collides with r.storeTable\\("${physical}"\\)`),
186
+ );
187
+ });
188
+ });
189
+
190
+ describe("createRegistry — store tables with PII-annotated fields (#820)", () => {
191
+ const piiEntity = createEntity({
192
+ table: "rt_pii_probe",
193
+ fields: {
194
+ userId: createTextField({ required: true }),
195
+ ip: createTextField({ userOwned: { ownerField: "userId" } }),
196
+ },
197
+ });
198
+ const piiMeta = buildEntityTableMeta("rt-pii-probe", piiEntity, { source: "unmanaged" });
199
+
200
+ test("buildEntityTableMeta records the subject-annotated field names", () => {
201
+ expect(piiMeta.piiSubjectFields).toEqual(["ip"]);
202
+ });
203
+
204
+ test("rejects a PII-carrying store table without piiEncryptedOnWrite", () => {
205
+ const feat = defineFeature("probe", (r) => {
206
+ r.storeTable(piiMeta, { reason: "direct-write store" });
207
+ });
208
+ expect(() => createRegistry([feat])).toThrow(
209
+ /has PII-annotated fields \(ip\) but direct writes bypass the executor's PII encryption/,
210
+ );
211
+ });
212
+
213
+ test("accepts it once the feature declares piiEncryptedOnWrite", () => {
214
+ const feat = defineFeature("probe", (r) => {
215
+ r.storeTable(piiMeta, {
216
+ reason: "direct-write store",
217
+ piiEncryptedOnWrite: true,
218
+ });
219
+ });
220
+ expect(() => createRegistry([feat])).not.toThrow();
221
+ });
222
+
223
+ test("a PII-free store table needs no declaration", () => {
224
+ const feat = defineFeature("probe", (r) => {
225
+ r.storeTable(probeMeta, { reason: "plain store" });
226
+ });
227
+ expect(() => createRegistry([feat])).not.toThrow();
228
+ });
229
+ });
@@ -5,7 +5,11 @@
5
5
 
6
6
  import { describe, expect, test } from "bun:test";
7
7
  import { createEntity, defineFeature } from "../index";
8
- import { findTierResolverUsage, TENANT_TIER_RESOLVER_EXT } from "../tier-resolver-extension";
8
+ import {
9
+ findTierResolverUsage,
10
+ isTierResolverPlugin,
11
+ TENANT_TIER_RESOLVER_EXT,
12
+ } from "../tier-resolver-extension";
9
13
 
10
14
  function tierResolverFeature(name: string) {
11
15
  return defineFeature(name, (r) => {
@@ -49,3 +53,17 @@ describe("findTierResolverUsage", () => {
49
53
  expect(findTierResolverUsage([])).toBeUndefined();
50
54
  });
51
55
  });
56
+
57
+ describe("isTierResolverPlugin", () => {
58
+ test("true when build is a function", () => {
59
+ expect(isTierResolverPlugin({ build: async () => new Set() })).toBe(true);
60
+ });
61
+
62
+ test("false for null, non-objects, and missing/non-fn build", () => {
63
+ expect(isTierResolverPlugin(null)).toBe(false);
64
+ expect(isTierResolverPlugin(undefined)).toBe(false);
65
+ expect(isTierResolverPlugin("x")).toBe(false);
66
+ expect(isTierResolverPlugin({})).toBe(false);
67
+ expect(isTierResolverPlugin({ build: 1 })).toBe(false);
68
+ });
69
+ });
@@ -1,15 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { buildTarget, createRegistry, defineFeature } from "../index";
3
- import type { TreeChildrenSubscribe, TreeNode } from "../types/tree-node";
4
-
5
- // Stub-Provider für Tests. Form: (ctx) => (emit) => unsubscribe.
6
- // Emittet einmal initial, kein Cleanup nötig (no-op unsubscribe).
7
- function makeStubProvider(nodes: readonly TreeNode[]): TreeChildrenSubscribe {
8
- return () => (emit) => {
9
- emit(nodes);
10
- return () => {};
11
- };
12
- }
13
3
 
14
4
  describe("r.treeActions — registrar slot", () => {
15
5
  test("feature without r.treeActions leaves the slot undefined", () => {
@@ -63,80 +53,12 @@ describe("r.treeActions — registrar slot", () => {
63
53
  });
64
54
  });
65
55
 
66
- describe("r.tree — registrar slot", () => {
67
- test("feature without r.tree leaves the slot undefined", () => {
68
- const feature = defineFeature("empty", () => {});
69
- expect(feature.treeProvider).toBeUndefined();
70
- });
71
-
72
- test("single r.tree call stores the provider function on the FeatureDefinition", () => {
73
- const provider = makeStubProvider([{ label: "Marketing" }]);
74
- const feature = defineFeature("text-content", (r) => {
75
- r.tree(provider);
76
- });
77
- expect(feature.treeProvider).toBe(provider);
78
- });
79
-
80
- test("second r.tree call throws — only-once-guard", () => {
81
- expect(() =>
82
- defineFeature("dupe", (r) => {
83
- r.tree(makeStubProvider([]));
84
- r.tree(makeStubProvider([]));
85
- }),
86
- ).toThrow(/r\.tree\(\) already called/);
87
- });
88
-
89
- test("treeActions and tree are independent slots — can declare one without the other", () => {
90
- const onlyActions = defineFeature("a", (r) => {
91
- r.treeActions({ list: {} });
92
- });
93
- expect(onlyActions.treeActions).toBeDefined();
94
- expect(onlyActions.treeProvider).toBeUndefined();
95
-
96
- const onlyProvider = defineFeature("b", (r) => {
97
- r.tree(makeStubProvider([]));
98
- });
99
- expect(onlyProvider.treeActions).toBeUndefined();
100
- expect(onlyProvider.treeProvider).toBeDefined();
101
- });
102
- });
103
-
104
- describe("Registry.getTreeProviders + getTreeActions", () => {
105
- test("empty registry returns empty providers map and undefined actions", () => {
56
+ describe("Registry.getTreeActions", () => {
57
+ test("empty registry returns undefined actions", () => {
106
58
  const reg = createRegistry([]);
107
- expect(reg.getTreeProviders().size).toBe(0);
108
59
  expect(reg.getTreeActions("nonexistent")).toBeUndefined();
109
60
  });
110
61
 
111
- test("aggregates providers from multiple features keyed by feature name", () => {
112
- const providerA = makeStubProvider([{ label: "A-root" }]);
113
- const providerB = makeStubProvider([{ label: "B-root" }]);
114
- const featureA = defineFeature("text-content", (r) => {
115
- r.tree(providerA);
116
- });
117
- const featureB = defineFeature("legal-pages", (r) => {
118
- r.tree(providerB);
119
- });
120
- const reg = createRegistry([featureA, featureB]);
121
-
122
- const providers = reg.getTreeProviders();
123
- expect(providers.size).toBe(2);
124
- expect(providers.get("text-content")).toBe(providerA);
125
- expect(providers.get("legal-pages")).toBe(providerB);
126
- });
127
-
128
- test("features without r.tree are absent from the providers map (Zero-Whitelist-Filter)", () => {
129
- const featureWithProvider = defineFeature("text-content", (r) => {
130
- r.tree(makeStubProvider([]));
131
- });
132
- const featureWithoutProvider = defineFeature("schema-editor", () => {});
133
- const reg = createRegistry([featureWithProvider, featureWithoutProvider]);
134
-
135
- const providers = reg.getTreeProviders();
136
- expect(providers.has("text-content")).toBe(true);
137
- expect(providers.has("schema-editor")).toBe(false);
138
- });
139
-
140
62
  test("getTreeActions returns the erased map for a feature that declared r.treeActions", () => {
141
63
  const feature = defineFeature("text-content", (r) => {
142
64
  r.treeActions({
@@ -153,9 +75,7 @@ describe("Registry.getTreeProviders + getTreeActions", () => {
153
75
  });
154
76
 
155
77
  test("getTreeActions returns undefined for a feature without r.treeActions", () => {
156
- const feature = defineFeature("no-actions", (r) => {
157
- r.tree(makeStubProvider([]));
158
- });
78
+ const feature = defineFeature("no-actions", () => {});
159
79
  const reg = createRegistry([feature]);
160
80
 
161
81
  expect(reg.getTreeActions("no-actions")).toBeUndefined();
@@ -169,7 +89,7 @@ describe("Registry.getTreeProviders + getTreeActions", () => {
169
89
  // returnt einen typed Handle, der via setup-export durch FeatureDefinition
170
90
  // fließt und compile-time-typisiert vom Schicht-1-buildTarget konsumiert
171
91
  // wird. Ohne diese Tests würde ein Type-Drift in einer der zwei Schichten
172
- // erst in V.1.1 sichtbar — siehe advisor-Verdict + Memory `[EventDef-
92
+ // erst spät sichtbar — siehe advisor-Verdict + Memory `[EventDef-
173
93
  // Exports-Pattern]`.
174
94
  // =============================================================================
175
95
 
@@ -219,18 +139,10 @@ describe("Schicht-1↔Schicht-2 Bridge — buildTarget against real defineFeatur
219
139
  buildTarget({ target: textContent.exports.handle, action: "delet", args: { slug: "x" } });
220
140
 
221
141
  // @ts-expect-error — slug muss string sein, nicht number
222
- buildTarget({
223
- target: textContent.exports.handle,
224
- action: "edit",
225
- args: { slug: 42 },
226
- });
142
+ buildTarget({ target: textContent.exports.handle, action: "edit", args: { slug: 42 } });
227
143
 
228
- buildTarget({
229
- target: textContent.exports.handle,
230
- action: "list",
231
- // @ts-expect-error — list hat keine args, args-Feld nicht erlaubt
232
- args: { x: 1 },
233
- });
144
+ // @ts-expect-error — list hat keine args, args-Feld nicht erlaubt
145
+ buildTarget({ target: textContent.exports.handle, action: "list", args: { x: 1 } });
234
146
 
235
147
  // @ts-expect-error — edit braucht args, fehlt
236
148
  buildTarget({ target: textContent.exports.handle, action: "edit" });
@@ -11,7 +11,7 @@ import { defineFeature } from "../define-feature";
11
11
  import { defineWriteHandler } from "../define-handler";
12
12
  import { defineStep } from "../define-step";
13
13
  import { createEntity, createTextField } from "../factories";
14
- import { pipeline } from "../pipeline";
14
+ import { stepsPipeline } from "../pipeline";
15
15
  import { validateProjectionAllowlist } from "../validate-projection-allowlist";
16
16
 
17
17
  describe("validateProjectionAllowlist", () => {
@@ -28,7 +28,7 @@ describe("validateProjectionAllowlist", () => {
28
28
  name: "log",
29
29
  schema: z.object({ msg: z.string() }),
30
30
  access: { roles: ["User"] },
31
- perform: pipeline<{ msg: string }, { ok: true }>(({ event, r }) => [
31
+ perform: stepsPipeline<{ msg: string }, { ok: true }>(({ event, r }) => [
32
32
  r.step.unsafeProjectionUpsert({
33
33
  table: demoLogTable,
34
34
  on: ["id"],
@@ -70,7 +70,7 @@ describe("validateProjectionAllowlist", () => {
70
70
  name: "sneaky",
71
71
  schema: z.object({}),
72
72
  access: { roles: ["User"] },
73
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
73
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
74
74
  r.step.unsafeProjectionUpsert({
75
75
  table: widgetsTable,
76
76
  on: ["id"],
@@ -113,7 +113,7 @@ describe("validateProjectionAllowlist", () => {
113
113
  name: "sneaky-delete",
114
114
  schema: z.object({}),
115
115
  access: { roles: ["User"] },
116
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
116
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
117
117
  r.step.unsafeProjectionDelete({
118
118
  table: widgetsTable,
119
119
  where: () => ({ id: "anything" }),
@@ -136,7 +136,7 @@ describe("validateProjectionAllowlist", () => {
136
136
  name: "purge",
137
137
  schema: z.object({}),
138
138
  access: { roles: ["User"] },
139
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
139
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
140
140
  r.step.unsafeProjectionDelete({
141
141
  table: demoLogTable,
142
142
  where: () => ({ id: "anything" }),
@@ -162,7 +162,7 @@ describe("validateProjectionAllowlist", () => {
162
162
  name: "branchedWrite",
163
163
  schema: z.object({}),
164
164
  access: { roles: ["User"] },
165
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
165
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
166
166
  r.step.branch({
167
167
  if: () => true,
168
168
  onTrue: [
@@ -195,7 +195,7 @@ describe("validateProjectionAllowlist", () => {
195
195
  name: "nestedWrite",
196
196
  schema: z.object({}),
197
197
  access: { roles: ["User"] },
198
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
198
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
199
199
  r.step.forEach({
200
200
  over: () => [],
201
201
  as: "item",
@@ -230,7 +230,7 @@ describe("validateProjectionAllowlist", () => {
230
230
  name: "loopedWrite",
231
231
  schema: z.object({}),
232
232
  access: { roles: ["User"] },
233
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
233
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
234
234
  r.step.forEach({
235
235
  over: () => [],
236
236
  as: "x",
@@ -267,7 +267,7 @@ describe("validateProjectionAllowlist", () => {
267
267
  name: "futureBuilder",
268
268
  schema: z.object({}),
269
269
  access: { roles: ["User"] },
270
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
270
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
271
271
  // Hand-crafted StepInstance simulating a future builder
272
272
  // whose kind isn't registered via defineStep yet.
273
273
  {
@@ -316,7 +316,7 @@ describe("validateProjectionAllowlist", () => {
316
316
  name: "futureBuilder",
317
317
  schema: z.object({}),
318
318
  access: { roles: ["User"] },
319
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
319
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
320
320
  {
321
321
  kind: futureKind,
322
322
  args: {
@@ -380,7 +380,7 @@ describe("validateProjectionAllowlist", () => {
380
380
  name: "sneak",
381
381
  schema: z.object({}),
382
382
  access: { roles: ["Admin"] },
383
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
383
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
384
384
  r.step.webhook.send({
385
385
  url: "https://hooks.example/sneak",
386
386
  mode: "deferred",
@@ -404,7 +404,7 @@ describe("validateProjectionAllowlist", () => {
404
404
  name: "sneak",
405
405
  schema: z.object({}),
406
406
  access: { roles: ["Admin"] },
407
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
407
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
408
408
  r.step.mail.send({
409
409
  to: "x@y.com",
410
410
  subject: "hi",
@@ -428,7 +428,7 @@ describe("validateProjectionAllowlist", () => {
428
428
  name: "sneak",
429
429
  schema: z.object({}),
430
430
  access: { roles: ["Admin"] },
431
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
431
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
432
432
  r.step.callFeature("subResult", {
433
433
  handler: "other:write:do",
434
434
  payload: () => ({}),
@@ -451,7 +451,7 @@ describe("validateProjectionAllowlist", () => {
451
451
  name: "ok",
452
452
  schema: z.object({}),
453
453
  access: { roles: ["Admin"] },
454
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
454
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
455
455
  r.step.webhook.send({
456
456
  url: "https://hooks.example/ok",
457
457
  mode: "deferred",
@@ -473,7 +473,7 @@ describe("validateProjectionAllowlist", () => {
473
473
  name: "log",
474
474
  schema: z.object({ msg: z.string() }),
475
475
  access: { roles: ["User"] },
476
- perform: pipeline<{ msg: string }, { ok: true }>(({ event, r }) => [
476
+ perform: stepsPipeline<{ msg: string }, { ok: true }>(({ event, r }) => [
477
477
  r.step.unsafeProjectionUpsert({
478
478
  table: demoLogTable,
479
479
  on: ["id"],
@@ -0,0 +1,92 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createSystemConfig, createTenantConfig, createUserConfig } from "../../config-helpers";
3
+ import type { FeatureDefinition } from "../../types";
4
+ import {
5
+ validateConfigKeyAllowPerRequest,
6
+ validateConfigKeyComputed,
7
+ validateConfigKeyPiiEncrypted,
8
+ } from "../config-deps";
9
+
10
+ function fakeFeature(configKeys: FeatureDefinition["configKeys"]): FeatureDefinition {
11
+ return { name: "test-feature", configKeys } as unknown as FeatureDefinition;
12
+ }
13
+
14
+ describe("validateConfigKeyComputed", () => {
15
+ test("rejects computed + backing:secrets (encrypted-at-rest via backing, not the encrypted flag)", () => {
16
+ const feature = fakeFeature({
17
+ apiKey: createSystemConfig("text", { backing: "secrets", computed: async () => "x" }),
18
+ });
19
+ expect(() => validateConfigKeyComputed(feature)).toThrow(/mutually exclusive/);
20
+ });
21
+
22
+ test("rejects computed + encrypted:true", () => {
23
+ const feature = fakeFeature({
24
+ apiKey: createSystemConfig("text", { encrypted: true, computed: async () => "x" }),
25
+ });
26
+ expect(() => validateConfigKeyComputed(feature)).toThrow(/mutually exclusive/);
27
+ });
28
+
29
+ test("allows computed without encryption", () => {
30
+ const feature = fakeFeature({
31
+ apiKey: createSystemConfig("text", { computed: async () => "x" }),
32
+ });
33
+ expect(() => validateConfigKeyComputed(feature)).not.toThrow();
34
+ });
35
+ });
36
+
37
+ describe("validateConfigKeyAllowPerRequest", () => {
38
+ test("rejects allowPerRequest + backing:secrets on a number key", () => {
39
+ const feature = fakeFeature({
40
+ rateLimit: createSystemConfig("number", { backing: "secrets", allowPerRequest: true }),
41
+ });
42
+ expect(() => validateConfigKeyAllowPerRequest(feature)).toThrow(
43
+ /may not be set via query-params/,
44
+ );
45
+ });
46
+ });
47
+
48
+ describe("validateConfigKeyPiiEncrypted (kumiko-platform#231/#459)", () => {
49
+ test("allows piiEncrypted on a tenant-scoped text key", () => {
50
+ const feature = fakeFeature({
51
+ billingAddress: createTenantConfig("text", { piiEncrypted: true }),
52
+ });
53
+ expect(() => validateConfigKeyPiiEncrypted(feature)).not.toThrow();
54
+ });
55
+
56
+ test("allows piiEncrypted on a user-scoped text key", () => {
57
+ const feature = fakeFeature({
58
+ phoneNumber: createUserConfig("text", { piiEncrypted: true }),
59
+ });
60
+ expect(() => validateConfigKeyPiiEncrypted(feature)).not.toThrow();
61
+ });
62
+
63
+ test("rejects piiEncrypted on a non-text key", () => {
64
+ const feature = fakeFeature({
65
+ rateLimit: { ...createTenantConfig("number"), piiEncrypted: true },
66
+ });
67
+ expect(() => validateConfigKeyPiiEncrypted(feature)).toThrow(
68
+ /piiEncrypted.*only applies to text keys/,
69
+ );
70
+ });
71
+
72
+ test("rejects piiEncrypted + scope:system", () => {
73
+ const feature = fakeFeature({
74
+ apiKey: createSystemConfig("text", { piiEncrypted: true }),
75
+ });
76
+ expect(() => validateConfigKeyPiiEncrypted(feature)).toThrow(/piiEncrypted.*scope="system"/);
77
+ });
78
+
79
+ test("rejects piiEncrypted + encrypted", () => {
80
+ const feature = fakeFeature({
81
+ iban: createTenantConfig("text", { piiEncrypted: true, encrypted: true }),
82
+ });
83
+ expect(() => validateConfigKeyPiiEncrypted(feature)).toThrow(/piiEncrypted=true and encrypted/);
84
+ });
85
+
86
+ test("rejects piiEncrypted + backing:secrets", () => {
87
+ const feature = fakeFeature({
88
+ iban: { ...createTenantConfig("text", { piiEncrypted: true }), backing: "secrets" as const },
89
+ });
90
+ expect(() => validateConfigKeyPiiEncrypted(feature)).toThrow(/piiEncrypted=true and encrypted/);
91
+ });
92
+ });