@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,500 @@
1
+ import type { EntityTableMeta } from "../db/entity-table-meta";
2
+ import { LifecycleHookTypes } from "./constants";
3
+ import type { FeatureBuilderState } from "./feature-builder-state";
4
+ import { resolveName } from "./handler-helpers";
5
+ import { isKebabSegment, toKebab } from "./qualified-name";
6
+ import type {
7
+ BootCheckFn,
8
+ EntityProjectionExtension,
9
+ HookPhase,
10
+ LifecycleHookFn,
11
+ LifecycleHookType,
12
+ MultiStreamProjectionDefinition,
13
+ NameOrRef,
14
+ PostDeleteHookFn,
15
+ PostQueryHookFn,
16
+ PostSaveHookFn,
17
+ PreDeleteHookFn,
18
+ ProjectionDefinition,
19
+ RegistrarExtensionDef,
20
+ SearchPayloadContributorFn,
21
+ StoreTableOptions,
22
+ TreeActionDef,
23
+ TreeActionsHandle,
24
+ ValidationHookFn,
25
+ } from "./types";
26
+ import { HookPhases } from "./types";
27
+ import type { HttpRouteDefinition } from "./types/http-route";
28
+ import type { NavDefinition } from "./types/nav";
29
+ import type { ScreenDefinition } from "./types/screen";
30
+ import type { WorkspaceDefinition } from "./types/workspace";
31
+
32
+ // Builds hooks/extensions/projections/screens/nav/workspace/tables/tree-actions
33
+ // registrar methods.
34
+
35
+ type EntityWideHookType = "postSave" | "preDelete" | "postDelete" | "postQuery";
36
+
37
+ function isEntityWideHookType(type: LifecycleHookType | "validation"): type is EntityWideHookType {
38
+ return (
39
+ type === LifecycleHookTypes.postSave ||
40
+ type === LifecycleHookTypes.preDelete ||
41
+ type === LifecycleHookTypes.postDelete ||
42
+ type === LifecycleHookTypes.postQuery
43
+ );
44
+ }
45
+
46
+ // r.hook(type, { allOf: entity }, fn) — "all write/query handlers of this
47
+ // entity", replacing the old r.entityHook(type, entity, fn). Hook-fn casts
48
+ // below: @cast-boundary engine-bridge — typed Dev-API (LifecycleHookFn) →
49
+ // erased Map<entityName, fn>.
50
+ function registerEntityWideHook(
51
+ state: FeatureBuilderState,
52
+ featureName: string,
53
+ type: EntityWideHookType,
54
+ entityName: string,
55
+ fn: LifecycleHookFn,
56
+ options?: { phase?: HookPhase },
57
+ ): void {
58
+ if (type === LifecycleHookTypes.postSave) {
59
+ const phase = options?.phase ?? HookPhases.afterCommit;
60
+ if (!state.entityPostSave[entityName]) state.entityPostSave[entityName] = [];
61
+ state.entityPostSave[entityName].push({
62
+ fn: fn as PostSaveHookFn,
63
+ phase,
64
+ featureName,
65
+ }); // @cast-boundary engine-bridge
66
+ } else if (type === LifecycleHookTypes.preDelete) {
67
+ if (!state.entityPreDelete[entityName]) state.entityPreDelete[entityName] = [];
68
+ state.entityPreDelete[entityName].push({
69
+ fn: fn as PreDeleteHookFn, // @cast-boundary engine-bridge
70
+ phase: HookPhases.inTransaction,
71
+ featureName,
72
+ });
73
+ } else if (type === LifecycleHookTypes.postDelete) {
74
+ const phase = options?.phase ?? HookPhases.afterCommit;
75
+ if (!state.entityPostDelete[entityName]) state.entityPostDelete[entityName] = [];
76
+ state.entityPostDelete[entityName].push({
77
+ fn: fn as PostDeleteHookFn,
78
+ phase,
79
+ featureName,
80
+ }); // @cast-boundary engine-bridge
81
+ } else {
82
+ // postQuery is unphased (no inTransaction/afterCommit semantics — fires
83
+ // synchronously after query-handler-execute, before field-access-filter)
84
+ if (!state.entityPostQuery[entityName]) state.entityPostQuery[entityName] = [];
85
+ state.entityPostQuery[entityName].push({ fn: fn as PostQueryHookFn, featureName }); // @cast-boundary engine-bridge
86
+ }
87
+ }
88
+
89
+ export function buildUiExtensionsMethods<TName extends string>(
90
+ state: FeatureBuilderState,
91
+ name: TName,
92
+ ) {
93
+ // Shared by r.nav() and r.screen()'s inline nav-sugar path — one place
94
+ // for id-validation + collision checks so future registration-time
95
+ // checks (e.g. parent-format, reserved ids) apply to both call sites
96
+ // instead of only the standalone r.nav() one.
97
+ function registerNav(navDefinition: NavDefinition): void {
98
+ if (!isKebabSegment(navDefinition.id)) {
99
+ throw new Error(
100
+ `[Feature ${name}] Nav id "${navDefinition.id}" must be kebab-case ` +
101
+ `(lowercase letters, digits, dashes; start with a letter). ` +
102
+ `Got "${navDefinition.id}" — try "${toKebab(navDefinition.id).replace(/_/g, "-")}".`,
103
+ );
104
+ }
105
+ if (state.navs[navDefinition.id]) {
106
+ throw new Error(
107
+ `[Feature ${name}] Nav entry "${navDefinition.id}" already registered. ` +
108
+ `Nav ids must be unique per feature — remove the standalone ` +
109
+ `r.nav("${navDefinition.id}", ...) call or the screen's inline nav.`,
110
+ );
111
+ }
112
+ state.navs[navDefinition.id] = navDefinition;
113
+ }
114
+
115
+ return {
116
+ hook(
117
+ type: LifecycleHookType | "validation",
118
+ target: NameOrRef | readonly NameOrRef[] | { readonly allOf: NameOrRef },
119
+ fn: LifecycleHookFn | ValidationHookFn,
120
+ options?: { phase?: HookPhase },
121
+ ): void {
122
+ // Entity-wide target ("all write/query handlers of this entity") —
123
+ // replaces the old r.entityHook(type, entity, fn).
124
+ if (
125
+ typeof target === "object" &&
126
+ target !== null &&
127
+ !Array.isArray(target) &&
128
+ "allOf" in target
129
+ ) {
130
+ if (!isEntityWideHookType(type)) {
131
+ throw new Error(
132
+ `[Feature ${name}] r.hook("${type}", { allOf }, ...) only supports ` +
133
+ `postSave/preDelete/postDelete/postQuery, not "${type}".`,
134
+ );
135
+ }
136
+ registerEntityWideHook(
137
+ state,
138
+ name,
139
+ type,
140
+ resolveName(target.allOf),
141
+ fn as LifecycleHookFn,
142
+ options,
143
+ );
144
+ // skip: entity-wide target fully handled above, nothing more to do
145
+ return;
146
+ }
147
+
148
+ const targets = Array.isArray(target) ? target : [target];
149
+ const names = targets.map(resolveName);
150
+
151
+ // Hook-fn casts unten alle: @cast-boundary engine-bridge
152
+ // — typed Dev-API (LifecycleHookFn|ValidationHookFn) → erased Map<name, fn>.
153
+ if (type === "validation") {
154
+ for (const n of names) {
155
+ state.validationHooks[n] = fn as ValidationHookFn; // @cast-boundary engine-bridge
156
+ }
157
+ // skip: validation hooks have no phase, stored and done
158
+ return;
159
+ }
160
+
161
+ if (
162
+ type === LifecycleHookTypes.preSave ||
163
+ type === LifecycleHookTypes.preQuery ||
164
+ type === LifecycleHookTypes.postQuery
165
+ ) {
166
+ if (!state.lifecycleHooks[type]) state.lifecycleHooks[type] = {};
167
+ for (const n of names) {
168
+ if (!state.lifecycleHooks[type][n]) state.lifecycleHooks[type][n] = [];
169
+ state.lifecycleHooks[type][n].push({ fn: fn as LifecycleHookFn, featureName: name }); // @cast-boundary engine-bridge
170
+ }
171
+ // skip: pre/post-hooks without phase semantics, stored and done
172
+ return;
173
+ }
174
+
175
+ // Phased storage. preDelete has no phase option (always inTransaction);
176
+ // postSave/postDelete default to afterCommit.
177
+ const phase =
178
+ type === LifecycleHookTypes.preDelete
179
+ ? HookPhases.inTransaction
180
+ : (options?.phase ?? HookPhases.afterCommit);
181
+ const bucket = state.phasedLifecycleHooks[type];
182
+ for (const n of names) {
183
+ if (!bucket[n]) bucket[n] = [];
184
+ bucket[n].push({ fn: fn as LifecycleHookFn, phase, featureName: name }); // @cast-boundary engine-bridge
185
+ }
186
+ },
187
+ searchPayloadExtension(entityRef: NameOrRef, fn: SearchPayloadContributorFn): void {
188
+ const entityName = resolveName(entityRef);
189
+ if (!state.searchPayloadExtensions[entityName])
190
+ state.searchPayloadExtensions[entityName] = [];
191
+ state.searchPayloadExtensions[entityName].push({ fn, featureName: name });
192
+ },
193
+ extendsRegistrar(extensionName: string, def: RegistrarExtensionDef): void {
194
+ state.registrarExtensions[extensionName] = def;
195
+ },
196
+ useExtension(
197
+ extensionNameOrDefinition:
198
+ | string
199
+ | ({ readonly name: string; readonly entity: NameOrRef } & Record<string, unknown>),
200
+ entityRef?: NameOrRef,
201
+ options?: Record<string, unknown>,
202
+ ): void {
203
+ const [extensionName, resolvedEntityRef, resolvedOptions] =
204
+ typeof extensionNameOrDefinition === "string"
205
+ ? [extensionNameOrDefinition, entityRef as NameOrRef, options]
206
+ : (() => {
207
+ const { name, entity, ...rest } = extensionNameOrDefinition;
208
+ return [name, entity, rest] as const;
209
+ })();
210
+ state.extensionUsages.push({
211
+ extensionName,
212
+ entityName: resolveName(resolvedEntityRef),
213
+ options: resolvedOptions,
214
+ });
215
+ },
216
+ extensionSelector(extensionName: string, key: { readonly name: string } | string): void {
217
+ if (state.extensionSelectors.some((s) => s.extensionName === extensionName)) {
218
+ throw new Error(
219
+ `[Feature ${name}] extensionSelector("${extensionName}") declared twice — ` +
220
+ `one selector key per extension point.`,
221
+ );
222
+ }
223
+ const qualifiedKey = typeof key === "string" ? key : key.name;
224
+ state.extensionSelectors.push({ extensionName, qualifiedKey });
225
+ },
226
+ /**
227
+ * Marker-Deklaration: dieses Feature stellt eine Cross-Feature-API
228
+ * unter dem genannten Namen bereit. Die eigentliche Implementation
229
+ * wird separat als Query- oder Write-Handler unter dem QN-Pattern
230
+ * registriert; r.exposesApi ist reine Boot-Check-Surface.
231
+ *
232
+ * Beispiel:
233
+ * defineFeature("compliance-profiles", (r) => {
234
+ * r.exposesApi("compliance.forTenant");
235
+ * r.queryHandler({ name: "compliance:query:for-tenant", ... });
236
+ * });
237
+ * defineFeature("user-data-rights", (r) => {
238
+ * r.requires("compliance-profiles");
239
+ * r.usesApi("compliance.forTenant");
240
+ * // ruft im Handler: ctx.callQuery("compliance:query:for-tenant", ...)
241
+ * });
242
+ */
243
+ exposesApi(apiName: string): void {
244
+ if (state.exposedApis.has(apiName)) {
245
+ throw new Error(
246
+ `[Feature ${name}] r.exposesApi("${apiName}") called twice — API names must be unique within a feature.`,
247
+ );
248
+ }
249
+ state.exposedApis.add(apiName);
250
+ },
251
+ /**
252
+ * Declares that this feature calls a cross-feature API. Boot-Validator
253
+ * checkt dass irgendein anderes Feature `r.exposesApi(name)` macht und
254
+ * dass dieses Feature `r.requires` darauf hat.
255
+ */
256
+ usesApi(apiName: string): void {
257
+ state.usedApis.add(apiName);
258
+ },
259
+ bootCheck(fn: BootCheckFn): void {
260
+ state.bootChecks.push(fn);
261
+ },
262
+ projection(definition: ProjectionDefinition): void {
263
+ // Reject names that would blow up at registry-boot when we qualify them.
264
+ // Catch it at the registration site so the stack trace points at the
265
+ // feature file, not at framework internals.
266
+ if (!isKebabSegment(definition.name)) {
267
+ throw new Error(
268
+ `[Feature ${name}] Projection name "${definition.name}" must be kebab-case ` +
269
+ `(lowercase letters, digits, dashes; start with a letter). ` +
270
+ `Got "${definition.name}" — try "${toKebab(definition.name).replace(/_/g, "-")}".`,
271
+ );
272
+ }
273
+ if (state.projections[definition.name]) {
274
+ throw new Error(
275
+ `[Feature ${name}] Projection "${definition.name}" already registered. ` +
276
+ `Projection names must be unique per feature.`,
277
+ );
278
+ }
279
+ state.projections[definition.name] = definition;
280
+ },
281
+ multiStreamProjection(definition: MultiStreamProjectionDefinition): void {
282
+ if (!isKebabSegment(definition.name)) {
283
+ throw new Error(
284
+ `[Feature ${name}] MultiStreamProjection name "${definition.name}" must be kebab-case ` +
285
+ `(lowercase letters, digits, dashes; start with a letter). ` +
286
+ `Got "${definition.name}" — try "${toKebab(definition.name).replace(/_/g, "-")}".`,
287
+ );
288
+ }
289
+ if (state.multiStreamProjections[definition.name] || state.projections[definition.name]) {
290
+ throw new Error(
291
+ `[Feature ${name}] Projection name "${definition.name}" already registered. ` +
292
+ `r.projection and r.multiStreamProjection share a namespace — pick a unique short name.`,
293
+ );
294
+ }
295
+ if (Object.keys(definition.apply).length === 0) {
296
+ throw new Error(
297
+ `[Feature ${name}] MultiStreamProjection "${definition.name}" has no apply handlers. ` +
298
+ `Declare at least one event type it reacts to, otherwise the dispatcher has nothing to route.`,
299
+ );
300
+ }
301
+ state.multiStreamProjections[definition.name] = definition;
302
+ },
303
+ extendEntityProjection(entityName: string, extension: EntityProjectionExtension): void {
304
+ if (Object.keys(extension.apply).length === 0) {
305
+ throw new Error(
306
+ `[Feature ${name}] extendEntityProjection("${entityName}") has no apply handlers. ` +
307
+ `Declare at least one event type, otherwise the rebuild replay has nothing to do.`,
308
+ );
309
+ }
310
+ // Entity existence + apply-key collisions are validated at registry
311
+ // build — r.entity may legally be called after this in the same feature.
312
+ const list = state.entityProjectionExtensions[entityName] ?? [];
313
+ list.push(extension);
314
+ state.entityProjectionExtensions[entityName] = list;
315
+ },
316
+ referenceData(
317
+ entityRefOrDefinition:
318
+ | NameOrRef
319
+ | {
320
+ readonly entity: NameOrRef;
321
+ readonly data: readonly Record<string, unknown>[];
322
+ readonly upsertKey?: string;
323
+ },
324
+ data?: readonly Record<string, unknown>[],
325
+ options?: { upsertKey?: string },
326
+ ): void {
327
+ const [entityRef, resolvedData, upsertKey] =
328
+ typeof entityRefOrDefinition === "object" && "entity" in entityRefOrDefinition
329
+ ? [
330
+ entityRefOrDefinition.entity,
331
+ entityRefOrDefinition.data,
332
+ entityRefOrDefinition.upsertKey,
333
+ ]
334
+ : [entityRefOrDefinition, data as readonly Record<string, unknown>[], options?.upsertKey];
335
+ state.referenceData.push({
336
+ entityName: resolveName(entityRef),
337
+ data: resolvedData,
338
+ upsertKey,
339
+ });
340
+ },
341
+ screen(definition: ScreenDefinition): void {
342
+ // Reject kebab-drift at registration-time so the stack trace points at
343
+ // the feature file, not at registry-boot. Same guard pattern as
344
+ // r.projection / r.multiStreamProjection.
345
+ if (!isKebabSegment(definition.id)) {
346
+ throw new Error(
347
+ `[Feature ${name}] Screen id "${definition.id}" must be kebab-case ` +
348
+ `(lowercase letters, digits, dashes; start with a letter). ` +
349
+ `Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
350
+ );
351
+ }
352
+ if (state.screens[definition.id]) {
353
+ throw new Error(
354
+ `[Feature ${name}] Screen "${definition.id}" already registered. ` +
355
+ `Screen ids must be unique per feature.`,
356
+ );
357
+ }
358
+ state.screens[definition.id] = definition;
359
+ if (definition.nav) {
360
+ // Sugar for the common "one nav entry pointing at this screen"
361
+ // case — synthesizes id/screen from the screen's own id. Beyond
362
+ // label/icon/parent/order, declare a standalone r.nav() instead.
363
+ registerNav({
364
+ id: definition.id,
365
+ label: definition.nav.label,
366
+ icon: definition.nav.icon,
367
+ parent: definition.nav.parent,
368
+ order: definition.nav.order,
369
+ screen: `${name}:screen:${definition.id}`,
370
+ });
371
+ }
372
+ },
373
+ nav(definition: NavDefinition): void {
374
+ registerNav(definition);
375
+ },
376
+ workspace(definition: WorkspaceDefinition): void {
377
+ // Same kebab guard as r.screen / r.nav so authoring-time mistakes
378
+ // surface at the feature file, not deep in registry boot.
379
+ if (!isKebabSegment(definition.id)) {
380
+ throw new Error(
381
+ `[Feature ${name}] Workspace id "${definition.id}" must be kebab-case ` +
382
+ `(lowercase letters, digits, dashes; start with a letter). ` +
383
+ `Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
384
+ );
385
+ }
386
+ if (state.workspaces[definition.id]) {
387
+ throw new Error(
388
+ `[Feature ${name}] Workspace "${definition.id}" already registered. ` +
389
+ `Workspace ids must be unique per feature.`,
390
+ );
391
+ }
392
+ state.workspaces[definition.id] = definition;
393
+ },
394
+ httpRoute(definition: HttpRouteDefinition): void {
395
+ // Path-Validation: muss mit "/" beginnen, keine /api/-Routes (die
396
+ // sind dem Dispatcher reserviert; eine HTTP-Route die /api/foo
397
+ // belegt, würde die Auth-Middleware umgehen ohne dass der Author
398
+ // das ausgesprochen hat — bewusster Block).
399
+ if (!definition.path.startsWith("/")) {
400
+ throw new Error(
401
+ `[Feature ${name}] httpRoute path "${definition.path}" must start with "/". ` +
402
+ `Got "${definition.path}".`,
403
+ );
404
+ }
405
+ if (definition.path === "/api" || definition.path.startsWith("/api/")) {
406
+ throw new Error(
407
+ `[Feature ${name}] httpRoute path "${definition.path}" is in the /api/* namespace ` +
408
+ `which is reserved for the dispatcher (write/query/batch/auth/sse). ` +
409
+ `Pick a different path or use r.queryHandler / r.writeHandler.`,
410
+ );
411
+ }
412
+ const key = `${definition.method} ${definition.path}`;
413
+ if (state.httpRoutes[key]) {
414
+ throw new Error(
415
+ `[Feature ${name}] HTTP-Route "${key}" already registered. ` +
416
+ `method + path must be unique per feature.`,
417
+ );
418
+ }
419
+ state.httpRoutes[key] = definition;
420
+ },
421
+ storeTable(meta: EntityTableMeta, options: StoreTableOptions): void {
422
+ // Name comes from the meta itself — apps already give the table a
423
+ // name when calling defineUnmanagedTable, no need to repeat it.
424
+ const tableName = meta.tableName;
425
+ if (!isKebabSegment(tableName.replace(/_/g, "-"))) {
426
+ // EntityTableMeta uses snake_case for tableName (matches Postgres
427
+ // convention); we just guard against truly broken input.
428
+ throw new Error(
429
+ `[Feature ${name}] Store-table name "${tableName}" must be a ` +
430
+ `valid identifier (lowercase letters, digits, underscores; start with a letter).`,
431
+ );
432
+ }
433
+ if (state.storeTables[tableName]) {
434
+ throw new Error(
435
+ `[Feature ${name}] r.storeTable("${tableName}") already registered. ` +
436
+ `Store-table names must be unique per feature.`,
437
+ );
438
+ }
439
+ // `read_` is reserved for r.entity()/r.projection() (managed,
440
+ // event-sourced, rebuildable). storeTable is the unmanaged
441
+ // direct-write escape hatch — the prefix must say so (#1220).
442
+ if (tableName.startsWith("read_")) {
443
+ throw new Error(
444
+ `[Feature ${name}] r.storeTable("${tableName}"): the "read_" prefix is reserved ` +
445
+ `for managed r.entity()/r.projection() tables. Pick an unprefixed name or a ` +
446
+ `distinct prefix (e.g. "store_${tableName.slice("read_".length)}").`,
447
+ );
448
+ }
449
+ // meta.source must agree with the r.storeTable() escape hatch, or the
450
+ // migrate-generator treats schema drift on this table as safe to
451
+ // DROP+rebuild-from-events — wiping direct-write data with no events
452
+ // to replay it from (#1209).
453
+ if (meta.source !== "unmanaged") {
454
+ throw new Error(
455
+ `[Feature ${name}] r.storeTable("${tableName}") was given an EntityTableMeta with ` +
456
+ `source: "${meta.source}". r.storeTable() requires source: "unmanaged" (via ` +
457
+ `defineUnmanagedTable(), or buildEntityTableMeta(..., { source: "unmanaged" })) — ` +
458
+ `otherwise the migration generator will treat schema drift on this table as safe ` +
459
+ `to DROP+rebuild, wiping any direct-write data.`,
460
+ );
461
+ }
462
+ // The `reason` is the marker that justifies the bypass — empty
463
+ // strings would defeat the audit trail. Reject early so the
464
+ // failure points at the feature file.
465
+ if (typeof options.reason !== "string" || options.reason.trim().length === 0) {
466
+ throw new Error(
467
+ `[Feature ${name}] r.storeTable("${tableName}"): options.reason must be a ` +
468
+ `non-empty string. The reason justifies the audit-trail bypass — ` +
469
+ `if you can't write one, declare data via r.entity() instead.`,
470
+ );
471
+ }
472
+ state.storeTables[tableName] = {
473
+ name: tableName,
474
+ meta,
475
+ reason: options.reason,
476
+ ...(options.piiEncryptedOnWrite && { piiEncryptedOnWrite: true }),
477
+ };
478
+ },
479
+ treeActions<const TActions extends Record<string, TreeActionDef>>(
480
+ actions: TActions,
481
+ ): TreeActionsHandle<TName, TActions> {
482
+ // Only-once-guard: zweiter Aufruf ist Author-Bug, soll am
483
+ // Feature-File aufschlagen (gleicher Stil wie r.toggleable).
484
+ if (state.treeActions !== undefined) {
485
+ throw new Error(
486
+ `[Feature ${name}] r.treeActions() already called. ` +
487
+ `Each feature may declare a single tree-actions schema.`,
488
+ );
489
+ }
490
+ state.treeActions = actions;
491
+ // Return typed handle für setup-export. Frozen damit Caller die
492
+ // Map nicht nachträglich mutieren (würde Pattern-AST + Runtime-
493
+ // Lookup divergieren lassen).
494
+ return Object.freeze({
495
+ id: name,
496
+ treeActions: actions,
497
+ });
498
+ },
499
+ };
500
+ }
@@ -2,6 +2,13 @@ import type { DbRow } from "../db/connection";
2
2
  import { normalizeAccessEntry, userCanReadFieldRow, userCanWriteFieldRow } from "./ownership";
3
3
  import type { EntityDefinition, SessionUser } from "./types";
4
4
 
5
+ // piiEncrypted fields deliberately break that silence (kumiko-platform#463):
6
+ // the field's whole point is "a legitimate reader may see the plaintext",
7
+ // so an unauthorized reader seeing the field exist-but-masked is the
8
+ // intended signal, not a leak — unlike a secret whose existence itself
9
+ // should stay hidden.
10
+ export const PII_MASKED_VALUE = "••••••";
11
+
5
12
  // Field-level read filtering. Returns a copy of `data` with fields stripped
6
13
  // if the user's roles don't grant read access OR the ownership-rule for the
7
14
  // matching role doesn't accept this concrete row. Fields without access
@@ -11,7 +18,8 @@ import type { EntityDefinition, SessionUser } from "./types";
11
18
  // one place in the ownership system where silence is the right default:
12
19
  // reporting "you tried to read X but can't" leaks the field's existence.
13
20
  // Writes do the opposite (loud error) because a silent drop there masks
14
- // save-bugs.
21
+ // save-bugs. piiEncrypted fields are the one exception — see
22
+ // PII_MASKED_VALUE above.
15
23
  export function filterReadFields(
16
24
  entity: EntityDefinition,
17
25
  data: Readonly<Record<string, unknown>>,
@@ -29,7 +37,10 @@ export function filterReadFields(
29
37
 
30
38
  const accessMap = normalizeAccessEntry(field.access?.read);
31
39
  if (!userCanReadFieldRow(user, accessMap, data)) {
32
- continue; // entire field stripped
40
+ if ("piiEncrypted" in field && field.piiEncrypted === true) {
41
+ result[key] = PII_MASKED_VALUE;
42
+ }
43
+ continue; // entire field stripped (masked instead, for piiEncrypted)
33
44
  }
34
45
 
35
46
  // For embedded fields: filter sub-fields with access restrictions
@@ -0,0 +1,31 @@
1
+ import type { AnyFileFieldDef, FieldDefinition } from "./types/fields";
2
+
3
+ // --- Currency ---
4
+
5
+ export const DEFAULT_CURRENCIES = [
6
+ "EUR",
7
+ "USD",
8
+ "GBP",
9
+ "CHF",
10
+ "JPY",
11
+ "SEK",
12
+ "NOK",
13
+ "DKK",
14
+ "PLN",
15
+ "CZK",
16
+ "CAD",
17
+ "AUD",
18
+ "NZD",
19
+ "CNY",
20
+ "INR",
21
+ ] as const;
22
+
23
+ export function isFileField(field: FieldDefinition | undefined): field is AnyFileFieldDef {
24
+ if (!field) return false;
25
+ return (
26
+ field.type === "file" ||
27
+ field.type === "image" ||
28
+ field.type === "files" ||
29
+ field.type === "images"
30
+ );
31
+ }
@@ -0,0 +1,26 @@
1
+ import type { NameOrRef, WriteResult } from "./types/handlers";
2
+
3
+ /**
4
+ * Override the success-side `data` of a WriteResult while forwarding the
5
+ * failure half untouched. Useful for handlers that delegate to the
6
+ * event-store executor (which returns a SaveContext / DeleteContext
7
+ * envelope) but want to keep their own response shape — caller contract
8
+ * stays flat instead of leaking the executor's internals.
9
+ *
10
+ * ```ts
11
+ * const result = await executor.delete({ id }, user, db);
12
+ * return withResponseData(result, { userId, tenantId });
13
+ * ```
14
+ *
15
+ * On failure the same WriteFailure instance is returned — the error
16
+ * object round-trips without any wrapping, so the dispatcher / HTTP layer
17
+ * still read the original error code + httpStatus + i18nKey.
18
+ */
19
+ export function withResponseData<T>(result: WriteResult<unknown>, data: T): WriteResult<T> {
20
+ if (!result.isSuccess) return result;
21
+ return { isSuccess: true, data };
22
+ }
23
+
24
+ export function resolveName(ref: NameOrRef): string {
25
+ return typeof ref === "string" ? ref : ref.name;
26
+ }
@@ -0,0 +1,16 @@
1
+ // --- Hook Phases ---
2
+ //
3
+ // inTransaction: Hook runs inside the DB transaction. Failures roll back
4
+ // the entire write. Use for: DB-based side-effects (counter updates,
5
+ // dependent entity writes).
6
+ //
7
+ // afterCommit (default): Hook runs after the transaction commits. Failures
8
+ // are logged but don't affect the write. Use for: external systems
9
+ // (SSE broadcast, search index, email, webhooks).
10
+
11
+ import type { HookPhase } from "@cosmicdrift/kumiko-types/hook-phase";
12
+
13
+ export const HookPhases = {
14
+ inTransaction: "inTransaction",
15
+ afterCommit: "afterCommit",
16
+ } as const satisfies Record<HookPhase, HookPhase>;