@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
@@ -112,6 +112,14 @@ export function generateE2ESpec(
112
112
  // pro Field geschrieben, ohne CRUD-Zustand zu generieren wäre der
113
113
  // Spec wertlos. Branding/SMTP/etc. sind Author-spezifisch.
114
114
  if (screen.type === "configEdit") continue;
115
+ // projectionList: query-getrieben, Author-spezifische Projection —
116
+ // kein generischer CRUD-Spec ableitbar (wie actionForm/configEdit).
117
+ if (screen.type === "projectionList") continue;
118
+ // projectionDetail: dito — read-only single-row inspector, kein CRUD-Zustand.
119
+ if (screen.type === "projectionDetail") continue;
120
+ // dashboard: panel-getrieben (Stat/Chart/List-Queries) — dito, keine
121
+ // generische CRUD-Annahme möglich.
122
+ if (screen.type === "dashboard") continue;
115
123
  const { scope: feature, name: short } = parseQn(screenQn);
116
124
  const urlPath = `/t/${tenant}/${feature}/${short}`;
117
125
  const title = `${feature}/${short}`;
@@ -0,0 +1,104 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import type { FileStorageProvider } from "../files/types";
3
+
4
+ const bytes = (s: string) => new TextEncoder().encode(s);
5
+ const decode = (u: Uint8Array) => new TextDecoder().decode(u);
6
+
7
+ async function* fromChunks(chunks: Uint8Array[]): AsyncIterable<Uint8Array> {
8
+ for (const c of chunks) yield c;
9
+ }
10
+
11
+ // Mirrors describeKmsAdapterContract (crypto/__tests__/kms-adapter-contract.ts):
12
+ // pins the FileStorageProvider contract against whatever implementation the
13
+ // factory returns. Provider-specific behavior (error messages, buffer
14
+ // copies, multipart details) stays in each provider's dedicated tests.
15
+ export function describeFileProviderContract(
16
+ name: string,
17
+ factory: () => FileStorageProvider | Promise<FileStorageProvider>,
18
+ ): void {
19
+ describe(`${name} — FileStorageProvider contract`, () => {
20
+ let provider: FileStorageProvider;
21
+ // Self-cleaning: every key this contract creates gets deleted in
22
+ // afterEach, so a persistent backend (local `kumiko dev` against a
23
+ // long-lived Minio, as opposed to CI's ephemeral one) doesn't leak
24
+ // `contract/<uuid>` objects into the next developer's session. delete()
25
+ // on a key that was never actually written is covered by the
26
+ // "no-op on a missing key" contract test above, so tracking generated
27
+ // keys unconditionally (even ones a test never got around to writing)
28
+ // is safe.
29
+ let writtenKeys: string[];
30
+
31
+ function trackedKey(ext: string): string {
32
+ const key = `contract/${crypto.randomUUID()}.${ext}`;
33
+ writtenKeys.push(key);
34
+ return key;
35
+ }
36
+
37
+ beforeEach(async () => {
38
+ provider = await factory();
39
+ writtenKeys = [];
40
+ });
41
+
42
+ afterEach(async () => {
43
+ for (const key of writtenKeys) await provider.delete(key);
44
+ });
45
+
46
+ test("write + read roundtrip preserves bytes", async () => {
47
+ const key = trackedKey("bin");
48
+ await provider.write(key, bytes("hello contract"));
49
+ expect(decode(await provider.read(key))).toBe("hello contract");
50
+ });
51
+
52
+ test("write on an existing key overwrites it (last-write-wins)", async () => {
53
+ const key = trackedKey("bin");
54
+ await provider.write(key, bytes("first"));
55
+ await provider.write(key, bytes("second"));
56
+ expect(decode(await provider.read(key))).toBe("second");
57
+ });
58
+
59
+ test("read throws for a missing key", async () => {
60
+ await expect(provider.read(`contract/missing-${crypto.randomUUID()}`)).rejects.toThrow();
61
+ });
62
+
63
+ test("exists reflects write + delete", async () => {
64
+ const key = trackedKey("txt");
65
+ expect(await provider.exists(key)).toBe(false);
66
+ await provider.write(key, bytes("x"));
67
+ expect(await provider.exists(key)).toBe(true);
68
+ await provider.delete(key);
69
+ expect(await provider.exists(key)).toBe(false);
70
+ });
71
+
72
+ test("delete on a missing key is a no-op", async () => {
73
+ await expect(
74
+ provider.delete(`contract/never-existed-${crypto.randomUUID()}`),
75
+ ).resolves.toBeUndefined();
76
+ });
77
+
78
+ test("writeStream + readStream roundtrip preserves bytes", async () => {
79
+ const key = trackedKey("stream");
80
+ await provider.writeStream(key, fromChunks([bytes("foo"), bytes("bar")]));
81
+ let out = "";
82
+ for await (const chunk of provider.readStream(key)) out += decode(chunk);
83
+ expect(out).toBe("foobar");
84
+ });
85
+
86
+ test("readStream on a missing key throws on the first chunk pull", async () => {
87
+ const it = provider
88
+ .readStream(`contract/missing-${crypto.randomUUID()}`)
89
+ [Symbol.asyncIterator]();
90
+ await expect(it.next()).rejects.toThrow();
91
+ });
92
+
93
+ test("getSignedUrl, when implemented, returns a URL string", async () => {
94
+ // skip: getSignedUrl is optional on the contract — feature-detected
95
+ if (!provider.getSignedUrl) return;
96
+
97
+ const key = trackedKey("txt");
98
+ await provider.write(key, bytes("signed"));
99
+ const url = await provider.getSignedUrl(key, 60);
100
+ expect(typeof url).toBe("string");
101
+ expect(url.length).toBeGreaterThan(0);
102
+ });
103
+ });
104
+ }
@@ -46,6 +46,7 @@ export function bridgeStub(opts?: {
46
46
  | "writeAs"
47
47
  | "appendEvent"
48
48
  | "unsafeAppendEvent"
49
+ | "tryAppendEvent"
49
50
  | "fetchForWriting"
50
51
  | "loadAggregate"
51
52
  | "archiveStream"
@@ -92,6 +93,7 @@ export function bridgeStub(opts?: {
92
93
  unsafeAppendEvent: notAvailable("unsafeAppendEvent") as unknown as (
93
94
  args: AppendEventArgs,
94
95
  ) => Promise<void>,
96
+ tryAppendEvent: notAvailable("tryAppendEvent") as unknown as HandlerContext["tryAppendEvent"],
95
97
  fetchForWriting: notAvailable("fetchForWriting") as unknown as (
96
98
  args: FetchForWritingArgs,
97
99
  ) => ReturnType<HandlerContext["fetchForWriting"]>,
@@ -115,7 +117,7 @@ export function bridgeStub(opts?: {
115
117
  ) as unknown as HandlerContext["resolveAuthClaims"],
116
118
  // Stub defaults to always-enabled — matches the dispatcher's behaviour
117
119
  // when no effectiveFeatures resolver is wired (tests without toggles).
118
- hasFeature: () => true,
120
+ hasFeature: async () => true,
119
121
  metrics: createNoopMetricsHandle(),
120
122
  tracer: noopTracer,
121
123
  // Echter TzContext, kein notAvailable — Test-Code nutzt ctx.tz häufig
@@ -5,6 +5,7 @@
5
5
 
6
6
  export { rolesOf } from "./access-assertions";
7
7
  export { expectError, expectSuccess } from "./assertions";
8
+ export { withBootValidatorFixture } from "./boot-validator-fixture";
8
9
  export { type ClearableTable, clearTables, resetTestTables } from "./db-cleanup";
9
10
  export {
10
11
  type E2EGeneratorOptions,
@@ -14,6 +15,7 @@ export {
14
15
  generateZodFixture,
15
16
  } from "./e2e-generator";
16
17
  export { expectErrorIncludes } from "./expect-error";
18
+ export { describeFileProviderContract } from "./file-provider-contract";
17
19
  export { bridgeStub } from "./handler-context";
18
20
  export {
19
21
  getSetCookieRaw,
@@ -25,6 +27,8 @@ export { createLateBoundHolder, type LateBoundHolder } from "./late-bound";
25
27
  export { buildMultipartBody, patchFileInstanceofForBunTest } from "./multipart-helper";
26
28
  export {
27
29
  createMutableMasterKeyProvider,
30
+ createTestEnvelopeCipher,
31
+ createTestMasterKeyProvider,
28
32
  type MutableMasterKeyProvider,
29
33
  } from "./mutable-master-key-provider";
30
34
  export {
@@ -42,3 +46,4 @@ export {
42
46
  } from "./shared-entities";
43
47
  export { sleep } from "./utils";
44
48
  export { waitFor } from "./wait-for";
49
+ export { withoutAmbientTemporal } from "./without-ambient-temporal";
@@ -22,18 +22,20 @@ export type LateBoundHolder<T> = {
22
22
 
23
23
  export function createLateBoundHolder<T>(label = "value"): LateBoundHolder<T> {
24
24
  let value: T | undefined;
25
+ let isSet = false;
25
26
  return {
26
27
  set(v) {
27
28
  value = v;
29
+ isSet = true;
28
30
  },
29
31
  get() {
30
- if (value === undefined) {
32
+ if (!isSet) {
31
33
  throw new Error(`late-bound ${label} accessed before set() was called`);
32
34
  }
33
- return value;
35
+ return value as T;
34
36
  },
35
37
  isReady() {
36
- return value !== undefined;
38
+ return isSet;
37
39
  },
38
40
  };
39
41
  }
@@ -6,7 +6,14 @@
6
6
  // tests that want to exercise pre- and post-rotation behaviour in a
7
7
  // single suite.
8
8
 
9
- import type { MasterKeyProvider } from "../secrets";
9
+ import { randomBytes } from "node:crypto";
10
+ import {
11
+ createEnvelopeCipher,
12
+ createEnvMasterKeyProvider,
13
+ type EnvelopeCipher,
14
+ type EnvelopeCipherOptions,
15
+ type MasterKeyProvider,
16
+ } from "../secrets";
10
17
 
11
18
  export type MutableMasterKeyProvider = MasterKeyProvider & {
12
19
  // Replace the backing provider. All future wrapDek/unwrapDek/currentVersion
@@ -20,8 +27,8 @@ export function createMutableMasterKeyProvider(
20
27
  ): MutableMasterKeyProvider {
21
28
  let current = initial;
22
29
  return {
23
- wrapDek: (dek) => current.wrapDek(dek),
24
- unwrapDek: (e, v) => current.unwrapDek(e, v),
30
+ wrapDek: (dek, scope) => current.wrapDek(dek, scope),
31
+ unwrapDek: (e, v, scope) => current.unwrapDek(e, v, scope),
25
32
  currentVersion: () => current.currentVersion(),
26
33
  isAvailable: () => current.isAvailable(),
27
34
  replace: (next) => {
@@ -29,3 +36,22 @@ export function createMutableMasterKeyProvider(
29
36
  },
30
37
  };
31
38
  }
39
+
40
+ // Single-version env provider for tests — the shape every integration test
41
+ // needs to exercise encrypted config keys / entity fields without caring
42
+ // about keyring mechanics. Pass a fixed key to share it across stacks.
43
+ export function createTestMasterKeyProvider(keyBase64?: string): MasterKeyProvider {
44
+ return createEnvMasterKeyProvider({
45
+ env: {
46
+ KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: "1",
47
+ KUMIKO_SECRETS_MASTER_KEY_V1: keyBase64 ?? randomBytes(32).toString("base64"),
48
+ },
49
+ });
50
+ }
51
+
52
+ export function createTestEnvelopeCipher(
53
+ keyBase64?: string,
54
+ opts?: EnvelopeCipherOptions,
55
+ ): EnvelopeCipher {
56
+ return createEnvelopeCipher(createTestMasterKeyProvider(keyBase64), opts ?? {});
57
+ }
@@ -14,6 +14,9 @@ export async function waitFor(
14
14
  options?: { delays?: number[] },
15
15
  ): Promise<void> {
16
16
  const delays = options?.delays ?? [250, 1000, 3000];
17
+ if (delays.length === 0) {
18
+ throw new Error("waitFor: empty delay schedule");
19
+ }
17
20
  let lastError: unknown;
18
21
 
19
22
  for (let i = 0; i < delays.length; i++) {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Run `fn` with `globalThis.Temporal` deleted, then restore.
3
+ * Used by ambient-independence regression tests (kumiko-framework#1525/#1550).
4
+ */
5
+ export async function withoutAmbientTemporal<T>(fn: () => T | Promise<T>): Promise<T> {
6
+ const savedGlobal = (globalThis as { Temporal?: unknown }).Temporal;
7
+ delete (globalThis as { Temporal?: unknown }).Temporal;
8
+ try {
9
+ return await fn();
10
+ } finally {
11
+ if (savedGlobal === undefined) delete (globalThis as { Temporal?: unknown }).Temporal;
12
+ else (globalThis as { Temporal?: unknown }).Temporal = savedGlobal;
13
+ }
14
+ }
@@ -1,9 +1,7 @@
1
- // Pacific/Apia (Samoa) liegt weit östlich der Datumsgrenze bei UTC+13/+14 —
2
- // eine Vormittags-Wall-Clock dort fällt in UTC auf den VORHERIGEN Kalendertag.
3
- // Das Plan-Doc (timezones.md) nennt Apia explizit als Datumsgrenzen-Edge-Case
4
- // der TZ-Matrix. Als Datums-Ordnungs-Eigenschaft formuliert, damit der Test
5
- // unabhängig von der DST-Sicht der tzdata grün bleibt (UTC+13 wie +14 schieben
6
- // 10:00 auf den Vortag).
1
+ // Pacific/Apia sits far east of the dateline at UTC+13/+14 — a morning wall
2
+ // clock there falls on the PREVIOUS UTC calendar day. Phrased as a date-
3
+ // ordering property (not a fixed offset) so the test stays green regardless
4
+ // of the tzdata DST view (UTC+13 and +14 both push 10:00 to the prior day).
7
5
 
8
6
  import { beforeAll, describe, expect, test } from "bun:test";
9
7
  import { ensureTemporalPolyfill } from "../polyfill";
@@ -13,8 +11,8 @@ beforeAll(async () => {
13
11
  await ensureTemporalPolyfill();
14
12
  });
15
13
 
16
- describe("ctx.tz — Pacific/Apia Datumsgrenze", () => {
17
- test("Vormittags-Wall-Clock in Apia mappt auf den vorherigen UTC-Kalendertag", () => {
14
+ describe("ctx.tz — Pacific/Apia dateline", () => {
15
+ test("a morning wall clock in Apia maps to the previous UTC calendar day", () => {
18
16
  const tz = createTzContext();
19
17
  const zdt = tz.parse("2026-01-15T10:00:00", "Pacific/Apia");
20
18
  expect(zdt.timeZoneId).toBe("Pacific/Apia");
@@ -22,11 +20,11 @@ describe("ctx.tz — Pacific/Apia Datumsgrenze", () => {
22
20
  const utcDate = zdt.toInstant().toZonedDateTimeISO("UTC").toPlainDate().toString();
23
21
  expect(utcDate).toBe("2026-01-14");
24
22
 
25
- // Offset ist ein großer positiver Wert (UTC+13 oder +14).
23
+ // Offset is a large positive value (UTC+13 or +14).
26
24
  expect(zdt.offsetNanoseconds).toBeGreaterThan(12 * 3600 * 1e9);
27
25
  });
28
26
 
29
- test("Round-Trip bewahrt Wall-Clock + Instant über die Datumsgrenze", () => {
27
+ test("round-trip preserves wall clock + instant across the dateline", () => {
30
28
  const tz = createTzContext();
31
29
  const original = tz.parse("2026-01-15T10:00:00", "Pacific/Apia");
32
30
  const restored = tz.fromLocatedJson(tz.toLocatedJson(original));
@@ -1,32 +1 @@
1
- // GeoTzProvider optionaler Adapter Geo-Position → IANA-Zone. Das Framework
2
- // definiert NUR das Interface + den Injection-Seam (ctx.tz.fromCoordinates /
3
- // fromAddress); eine konkrete Implementation kommt aus einem separaten Paket
4
- // (z.B. ein geo-tz-basiertes Offline-Paket). v1-Default: kein Provider — die
5
- // fromCoordinates/fromAddress-Methoden werfen klar statt still falsch zu raten.
6
- //
7
- // `fromCoordinates` ist die PRIMÄRE Methode: Offline-geo-tz-Libs lösen
8
- // lat/lng → Zone (genau, offline, kostenlos) — sie kennen keine Postadressen.
9
- // `fromAddress` ist optional, für Provider die eine Geocoding-API (Adresse →
10
- // Zone, online) anbinden. Diese Trennung hält das Interface kompatibel mit
11
- // beiden Provider-Klassen, statt eine Adress-Form zu erzwingen die der
12
- // Offline-Fall gar nicht bedienen kann.
13
-
14
- export type GeoCoordinates = {
15
- readonly latitude: number;
16
- readonly longitude: number;
17
- };
18
-
19
- export type GeoAddress = {
20
- readonly street?: string;
21
- readonly city?: string;
22
- readonly region?: string;
23
- readonly postalCode?: string;
24
- readonly country?: string;
25
- };
26
-
27
- export type GeoTzProvider = {
28
- /** Geo-Koordinaten → IANA-Zone (offline lat/lng → tz). */
29
- readonly fromCoordinates: (coords: GeoCoordinates) => string | Promise<string>;
30
- /** Optional: Postadresse → IANA-Zone (Geocoding-API-Provider). */
31
- readonly fromAddress?: (address: GeoAddress) => string | Promise<string>;
32
- };
1
+ export type * from "@cosmicdrift/kumiko-types/geo-tz";
package/src/time/index.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  export { warnIfNonUtcServerTimeZone } from "./boot-tz-warning";
13
13
  export type { GeoAddress, GeoCoordinates, GeoTzProvider } from "./geo-tz";
14
14
  export { isValidIanaTimeZone } from "./iana";
15
+ export { instantToLegacyDate, legacyDateToInstant } from "./legacy-date";
15
16
  export { ensureTemporalPolyfill, getTemporal } from "./polyfill";
16
17
  export {
17
18
  createTzContext,
@@ -0,0 +1,18 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+
3
+ // Temporal→Date-Bridge für Lib-APIs die JS-Date verlangen (imapflow
4
+ // search({since}), nodemailer-Header, ...). Gegenstück zur
5
+ // dateToInstant-Richtung der DB-/Event-Store-Boundaries: Feature-Code
6
+ // rechnet in Temporal, konvertiert erst am Aufruf der Fremd-API — der
7
+ // no-date-api-Guard bleibt für Feature-Code scharf, die Konstruktion
8
+ // lebt hier im allowlisteten Time-Layer.
9
+
10
+ export function instantToLegacyDate(instant: Temporal.Instant): Date {
11
+ return new Date(instant.epochMilliseconds);
12
+ }
13
+
14
+ /** Gegenrichtung: Date aus einer Fremd-Lib (imapflow INTERNALDATE,
15
+ * mailparser Date-Header) → Temporal.Instant. */
16
+ export function legacyDateToInstant(date: Date): Temporal.Instant {
17
+ return Temporal.Instant.fromEpochMilliseconds(date.getTime());
18
+ }
@@ -1,66 +1,49 @@
1
- // Temporal-Polyfill-Initialisierung.
1
+ // Temporal polyfill bootstrap.
2
2
  //
3
- // Hintergrund: Temporal ist seit Anfang 2026 in Chromium 144+ und Firefox 139+
4
- // nativ verfügbar, aber nicht in Safari, iOS, oder Hermes (React Native).
5
- // Bun/Node haben es teilweise (V8-abhängig, instabil).
3
+ // Temporal is native in Chromium 144+ / Firefox 139+, but missing in Safari,
4
+ // iOS, and Hermes. Bun/Node coverage is incomplete. Boot installs
5
+ // `temporal-polyfill` once so server/web/mobile share one API.
6
6
  //
7
- // Damit kumiko-Apps universal laufen Server (Bun), Web (alle Browser),
8
- // Mobile (Hermes)installiert das Framework beim Boot einmal den
9
- // `temporal-polyfill` (FullCalendar, ~25 KB). Auf Runtimes mit nativem
10
- // Temporal ist der Aufruf ein No-Op.
11
- //
12
- // Idempotent: mehrfacher Aufruf ist sicher (Polyfill prüft selbst ob
13
- // `globalThis.Temporal` schon existiert). Wir cachen das Ergebnis trotzdem
14
- // in einem Modul-Singleton, damit Boot-Performance nicht jedes Mal das
15
- // Polyfill-Modul-Loading triggert.
7
+ // Idempotent: if `globalThis.Temporal` already exists the call is a no-op.
8
+ // The cache is the live global check not a sticky module flag so tests
9
+ // that delete the ambient global (fw#1550) still re-install on the next call.
16
10
 
17
- let polyfillInstalled = false;
18
11
  let polyfillPromise: Promise<void> | null = null;
19
12
 
20
13
  /**
21
- * Stellt sicher dass `globalThis.Temporal` verfügbar ist. Idempotent.
22
- *
23
- * - Wenn Native Temporal existiert (moderne Browser, neueres Bun): No-Op.
24
- * - Sonst: lädt `temporal-polyfill/global` (installiert globalThis.Temporal).
25
- *
26
- * Wird einmal beim Framework-Boot aufgerufen. Feature-Code muss das nicht
27
- * selbst tun — `Temporal` ist nach dem Boot überall verfügbar.
14
+ * Ensure `globalThis.Temporal` is available. Idempotent.
28
15
  */
29
16
  export async function ensureTemporalPolyfill(): Promise<void> {
30
- // skip: Idempotenz — Polyfill bereits installiert in einem früheren Aufruf.
31
- if (polyfillInstalled) return;
17
+ if ("Temporal" in globalThis) {
18
+ // skip: Temporal already on globalThis (native or prior polyfill)
19
+ return;
20
+ }
32
21
  if (polyfillPromise) {
33
22
  await polyfillPromise;
34
- // skip: Concurrent-Boot anderer Aufruf hat die Installation übernommen.
35
- return;
23
+ if ("Temporal" in globalThis) {
24
+ // skip: Concurrent boot — peer call finished install
25
+ return;
26
+ }
27
+ // Peer resolved but global still missing (torn down mid-flight).
28
+ polyfillPromise = null;
36
29
  }
37
30
 
38
31
  polyfillPromise = (async () => {
39
- // Runtime-Check entgegen Type-System-Annahme: temporal-spec deklariert
40
- // Temporal ambient, aber ohne Polyfill fehlt es zur Laufzeit. `in`-Check
41
- // ist die sauberste Prüfung, ohne Cast + ohne TS-Truthy-Warnings.
42
32
  if ("Temporal" in globalThis) {
43
- polyfillInstalled = true;
44
- // skip: Native Temporal vorhanden — Polyfill nicht nötig.
33
+ // skip: raced native/peer install
45
34
  return;
46
35
  }
47
- // Polyfill globally installieren — der Side-Effect-Import setzt
48
- // globalThis.Temporal.
49
36
  await import("temporal-polyfill/global");
50
- polyfillInstalled = true;
51
37
  })();
52
38
 
53
39
  await polyfillPromise;
54
40
  }
55
41
 
56
42
  /**
57
- * Type-safe Zugriff auf globalThis.Temporal. Wirft wenn der Polyfill noch
58
- * nicht installiert ist (Boot-Reihenfolge-Bug). Feature-Code sollte
59
- * `ensureTemporalPolyfill()` einmal awaiten und danach `Temporal` global
60
- * nutzen, oder über diesen Helper die Sicherheit haben.
43
+ * Type-safe access to globalThis.Temporal. Throws if the polyfill has not
44
+ * been installed yet (boot-order bug).
61
45
  */
62
46
  export function getTemporal(): typeof Temporal {
63
- // Runtime-Check entgegen Type-System-Annahme — siehe ensureTemporalPolyfill.
64
47
  if (!("Temporal" in globalThis)) {
65
48
  throw new Error(
66
49
  "Temporal not available. Call ensureTemporalPolyfill() during framework boot before any time-related code runs.",
@@ -1,85 +1,41 @@
1
- // ctx.tz — die Feature-Code-API für TZ-Operationen.
1
+ // ctx.tz — the feature-code API for TZ operations.
2
2
  //
3
- // Eine konsistente Form für jeden Date/Time-Bedarf im Handler-Code:
4
- // - "Jetzt als UTC-Instant" → ctx.tz.now()
5
- // - "Heute in Tenant-TZ" → ctx.tz.today(ctx.tz.tenant)
6
- // - "Wall-Clock parsen" → ctx.tz.parse("2026-04-03T10:00", "Europe/Lisbon")
7
- // - "ZonedDateTime → JSON-Pair" → ctx.tz.toLocatedJson(zdt)
8
- // - "JSON-Pair → ZonedDateTime" → ctx.tz.fromLocatedJson({ at, tz })
3
+ // A consistent shape for every date/time need in handler code:
4
+ // - "now as a UTC instant" → ctx.tz.now()
5
+ // - "today in the tenant's TZ" → ctx.tz.today(ctx.tz.tenant)
6
+ // - "parse a wall-clock string" → ctx.tz.parse("2026-04-03T10:00", "Europe/Lisbon")
7
+ // - "ZonedDateTime → JSON pair" → ctx.tz.toLocatedJson(zdt)
8
+ // - "JSON pair → ZonedDateTime" → ctx.tz.fromLocatedJson({ at, tz })
9
9
  //
10
- // Feature-Code soll NICHT mehr `new Date()` aufrufen die Lint-Regel dafür
11
- // kommt in einer späteren Iteration, wenn alle existing usages migriert sind.
10
+ // Feature code should no longer call `new Date()` — the lint rule for that
11
+ // lands in a later iteration, once all existing usages are migrated.
12
12
  //
13
- // `tenant` + `user` sind die TZ-Defaults für den aktuellen Request. Aktueller
14
- // Stand: beide default auf "UTC" — sobald tenant.timezone +
15
- // user.timezone Felder existieren, lese ich sie aus dem Request-Context.
16
-
17
- import type { GeoAddress, GeoCoordinates, GeoTzProvider } from "./geo-tz";
18
- import { ensureTemporalPolyfill, getTemporal } from "./polyfill";
19
-
20
- // JSON-Form für Wall-Clock+TZ — siehe locatedTimestamp(name) Helper in
21
- // engine/factories.ts. Zwei Felder, idiotensicher.
22
- export type LocatedTimestampJson = {
23
- /** Wall-Clock-ISO ohne Offset, z.B. "2026-04-03T10:00:00" */
24
- readonly at: string;
25
- /** IANA-Zone, z.B. "Europe/Lisbon" */
26
- readonly tz: string;
27
- };
28
-
29
- export type TzContext = {
30
- /** Default-TZ des Mandanten (aus tenant.timezone, default "UTC"). */
31
- readonly tenant: string;
32
- /** Anzeige-TZ des aktuellen Users (User-Profil-Override, fallback Tenant). */
33
- readonly user: string;
34
-
35
- /** Aktueller Moment als UTC-Instant. */
36
- now(): Temporal.Instant;
37
- /** Aktueller Moment als ZonedDateTime in der gewünschten Zone. */
38
- nowIn(tz: string): Temporal.ZonedDateTime;
39
-
40
- /** Heutiges Kalender-Datum in der gewünschten Zone. */
41
- today(tz: string): Temporal.PlainDate;
42
- /** Tagesgrenzen (00:00 bis 24:00 nächster Tag) als UTC-Instants — für DB-Range-Queries. */
43
- todayRange(tz: string): { readonly start: Temporal.Instant; readonly end: Temporal.Instant };
44
-
45
- /** Wall-Clock-String + IANA-Zone → ZonedDateTime. */
46
- parse(wallClock: string, tz: string): Temporal.ZonedDateTime;
47
-
48
- /** ZonedDateTime → UTC-Instant. */
49
- toInstant(zdt: Temporal.ZonedDateTime): Temporal.Instant;
50
-
51
- /** ZonedDateTime → JSON-Pair { at, tz } (API-Boundary). */
52
- toLocatedJson(zdt: Temporal.ZonedDateTime): LocatedTimestampJson;
53
-
54
- /** JSON-Pair { at, tz } → ZonedDateTime (Wall-Clock + IANA). */
55
- fromLocatedJson(obj: LocatedTimestampJson): Temporal.ZonedDateTime;
13
+ // `tenant` + `user` are the TZ defaults for the current request. Currently
14
+ // both default to "UTC" — once tenant.timezone + user.timezone fields
15
+ // exist, they get read from the request context here.
16
+ //
17
+ // The pure type contracts (TzContext, TzContextOptions, LocatedTimestampJson)
18
+ // live in @cosmicdrift/kumiko-types/tz-context only the factories are here.
56
19
 
57
- /** Geo-Koordinaten IANA-Zone via konfiguriertem GeoTzProvider.
58
- * Wirft wenn kein Provider konfiguriert ist (v1-Default). */
59
- fromCoordinates(coords: GeoCoordinates): Promise<string>;
60
- /** Postadresse → IANA-Zone via GeoTzProvider. Wirft wenn kein Provider
61
- * konfiguriert ist ODER der Provider kein fromAddress unterstützt (der
62
- * Offline-lat/lng-Provider tut das nicht). */
63
- fromAddress(address: GeoAddress): Promise<string>;
64
- };
20
+ import type { TzContext, TzContextOptions } from "@cosmicdrift/kumiko-types/tz-context";
21
+ import { Temporal as TemporalPolyfill } from "temporal-polyfill";
22
+ import { ensureTemporalPolyfill } from "./polyfill";
65
23
 
66
- export type TzContextOptions = {
67
- /** Tenant-Default-TZ. Default "UTC" wenn nicht gesetzt. */
68
- readonly tenant?: string;
69
- /** User-Override. Default = tenant. */
70
- readonly user?: string;
71
- /** Optionaler Geo→Zone-Adapter für ctx.tz.fromCoordinates / fromAddress.
72
- * Ohne Provider werfen diese Methoden. */
73
- readonly geoTz?: GeoTzProvider;
74
- };
24
+ // Back-compat shim: re-exported so existing `from "@cosmicdrift/kumiko-framework/time/tz-context"`
25
+ // imports keep working. Prefer importing from @cosmicdrift/kumiko-types/tz-context directly in
26
+ // new code — drop this re-export once no framework-path imports remain.
27
+ export type {
28
+ LocatedTimestampJson,
29
+ TzContext,
30
+ TzContextOptions,
31
+ } from "@cosmicdrift/kumiko-types/tz-context";
75
32
 
76
33
  /**
77
- * Factory: erzeugt einen TzContext für den aktuellen Request.
78
- * Erwartet dass ensureTemporalPolyfill() bereits gelaufen ist (passiert beim
79
- * Framework-Boot). Wenn nicht, wirft getTemporal() — kein silent failure.
34
+ * Factory: creates a TzContext for the current request.
35
+ * Uses temporal-polyfill's module export (not globalThis.Temporal) so
36
+ * buildHandlerContext stays ambient-free (fw#1525/#1550).
80
37
  */
81
38
  export function createTzContext(options: TzContextOptions = {}): TzContext {
82
- const T = getTemporal();
83
39
  const tenant = options.tenant ?? "UTC";
84
40
  const user = options.user ?? tenant;
85
41
  const geoTz = options.geoTz;
@@ -87,25 +43,26 @@ export function createTzContext(options: TzContextOptions = {}): TzContext {
87
43
  return {
88
44
  tenant,
89
45
  user,
90
- now: () => T.Now.instant(), // @wrapper-known semantic-alias
91
- nowIn: (tz: string) => T.Now.zonedDateTimeISO(tz), // @wrapper-known semantic-alias
92
- today: (tz: string) => T.Now.plainDateISO(tz), // @wrapper-known semantic-alias
46
+ now: () => TemporalPolyfill.Now.instant(), // @wrapper-known semantic-alias
47
+ nowIn: (tz: string) => TemporalPolyfill.Now.zonedDateTimeISO(tz), // @wrapper-known semantic-alias
48
+ today: (tz: string) => TemporalPolyfill.Now.plainDateISO(tz), // @wrapper-known semantic-alias
93
49
  todayRange: (tz: string) => {
94
- const today = T.Now.plainDateISO(tz);
50
+ const today = TemporalPolyfill.Now.plainDateISO(tz);
95
51
  const startZdt = today.toZonedDateTime({ timeZone: tz });
96
52
  const endZdt = today.add({ days: 1 }).toZonedDateTime({ timeZone: tz });
97
53
  return { start: startZdt.toInstant(), end: endZdt.toInstant() };
98
54
  },
99
- parse: (wallClock: string, tz: string) => T.PlainDateTime.from(wallClock).toZonedDateTime(tz),
55
+ parse: (wallClock: string, tz: string) =>
56
+ TemporalPolyfill.PlainDateTime.from(wallClock).toZonedDateTime(tz),
100
57
  toInstant: (zdt) => zdt.toInstant(),
101
58
  toLocatedJson: (zdt) => ({
102
- // Wall-Clock OHNE Offset (kein "Z", kein "+01:00") plus IANA-Name.
103
- // .toPlainDateTime().toString() liefert "YYYY-MM-DDTHH:MM:SS[.fff]"
104
- // ohne Offsetexakt unser Vertrag.
59
+ // Wall-clock WITHOUT offset (no "Z", no "+01:00") plus the IANA name.
60
+ // .toPlainDateTime().toString() returns "YYYY-MM-DDTHH:MM:SS[.fff]"
61
+ // without an offset exactly our contract.
105
62
  at: zdt.toPlainDateTime().toString(),
106
63
  tz: zdt.timeZoneId,
107
64
  }),
108
- fromLocatedJson: (obj) => T.PlainDateTime.from(obj.at).toZonedDateTime(obj.tz),
65
+ fromLocatedJson: (obj) => TemporalPolyfill.PlainDateTime.from(obj.at).toZonedDateTime(obj.tz),
109
66
  fromCoordinates: async (coords) => {
110
67
  if (geoTz === undefined) {
111
68
  throw new Error(
@@ -127,12 +84,14 @@ export function createTzContext(options: TzContextOptions = {}): TzContext {
127
84
  }
128
85
  return geoTz.fromAddress(address);
129
86
  },
130
- };
87
+ // @cast-boundary temporal-polyfill-vs-ambient: same TC39 Temporal values at
88
+ // runtime; TzContext is typed against ambient Temporal from temporal-spec.
89
+ } as TzContext;
131
90
  }
132
91
 
133
92
  /**
134
- * Convenience: stellt sicher dass der Polyfill geladen ist UND erzeugt
135
- * den TzContext in einem await. Bevorzugt verwenden in Boot-Code.
93
+ * Convenience: ensures the polyfill is loaded AND creates the TzContext in
94
+ * one await. Prefer this in boot code.
136
95
  */
137
96
  export async function createTzContextAsync(options?: TzContextOptions): Promise<TzContext> {
138
97
  await ensureTemporalPolyfill();