@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.
- package/README.md +9 -38
- package/package.json +21 -2
- package/src/__tests__/anonymous-access.integration.test.ts +185 -0
- package/src/__tests__/consumer-cli.integration.test.ts +172 -0
- package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
- package/src/__tests__/full-stack.integration.test.ts +1 -1
- package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
- package/src/__tests__/schema-cli.integration.test.ts +50 -0
- package/src/__tests__/store-table.integration.test.ts +93 -0
- package/src/api/__tests__/api.test.ts +373 -0
- package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
- package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
- package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
- package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
- package/src/api/__tests__/batch.integration.test.ts +101 -11
- package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
- package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
- package/src/api/__tests__/jwt.test.ts +200 -1
- package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
- package/src/api/__tests__/pat-scope.test.ts +36 -0
- package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
- package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
- package/src/api/__tests__/request-id-middleware.test.ts +51 -0
- package/src/api/__tests__/server-boot-guards.test.ts +71 -0
- package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
- package/src/api/__tests__/sse-broker.test.ts +57 -0
- package/src/api/__tests__/sse-route.test.ts +4 -0
- package/src/api/api-constants.ts +11 -0
- package/src/api/auth-middleware.ts +248 -40
- package/src/api/auth-routes.ts +576 -95
- package/src/api/index.ts +13 -4
- package/src/api/jwt.ts +170 -11
- package/src/api/pat-scope.ts +14 -0
- package/src/api/pii-leak-guard.ts +47 -0
- package/src/api/request-context.ts +3 -0
- package/src/api/request-id-middleware.ts +2 -0
- package/src/api/routes.ts +169 -2
- package/src/api/server.ts +112 -16
- package/src/api/sse-broker.ts +39 -0
- package/src/bun-db/__tests__/PATTERN.md +0 -1
- package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
- package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
- package/src/bun-db/__tests__/write-brand.test.ts +21 -0
- package/src/bun-db/connection.ts +3 -3
- package/src/bun-db/index.ts +2 -0
- package/src/bun-db/query.ts +105 -32
- package/src/consumer-cli.ts +134 -0
- package/src/crypto/__tests__/blind-index.test.ts +130 -0
- package/src/crypto/__tests__/event-pii.test.ts +161 -0
- package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
- package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
- package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
- package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
- package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
- package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
- package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
- package/src/crypto/blind-index.ts +118 -0
- package/src/crypto/event-pii.ts +70 -0
- package/src/crypto/in-memory-kms-adapter.ts +50 -0
- package/src/crypto/index.ts +67 -0
- package/src/crypto/kms-adapter.ts +2 -0
- package/src/crypto/pg-kms-adapter.ts +295 -0
- package/src/crypto/pii-field-encryption.ts +248 -0
- package/src/crypto/request-kms-cache.ts +38 -0
- package/src/crypto/subject-resolver.ts +91 -0
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
- package/src/db/__tests__/blind-index.integration.test.ts +248 -0
- package/src/db/__tests__/build-filter-where.test.ts +34 -0
- package/src/db/__tests__/collect-table-metas.test.ts +10 -10
- package/src/db/__tests__/config-seed.integration.test.ts +13 -5
- package/src/db/__tests__/dialect-instant.test.ts +1 -4
- package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
- package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
- package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
- package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
- package/src/db/__tests__/feature-table-sources.test.ts +34 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
- package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
- package/src/db/__tests__/located-timestamp.test.ts +19 -0
- package/src/db/__tests__/migrate-generator.test.ts +21 -0
- package/src/db/__tests__/migrate-runner.test.ts +61 -0
- package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
- package/src/db/__tests__/rebuild-marker.test.ts +13 -3
- package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
- package/src/db/__tests__/schema-inspection.test.ts +7 -0
- package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
- package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
- package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
- package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
- package/src/db/api.ts +2 -2
- package/src/db/apply-entity-event.ts +18 -14
- package/src/db/blind-index-cleanup.ts +55 -0
- package/src/db/bun-provider.ts +2 -2
- package/src/db/collect-table-metas.ts +6 -7
- package/src/db/config-seed.ts +9 -9
- package/src/db/connection.ts +7 -12
- package/src/db/cursor.ts +1 -18
- package/src/db/dialect.ts +20 -26
- package/src/db/entity-field-encryption.ts +81 -24
- package/src/db/entity-table-meta-types.ts +2 -0
- package/src/db/entity-table-meta.ts +63 -90
- package/src/db/event-store-executor-context.ts +404 -0
- package/src/db/event-store-executor-read.ts +275 -0
- package/src/db/event-store-executor-write.ts +644 -0
- package/src/db/event-store-executor.ts +28 -1155
- package/src/db/feature-table-sources.ts +6 -5
- package/src/db/index.ts +20 -3
- package/src/db/located-timestamp.ts +4 -0
- package/src/db/migrate-generator.ts +39 -6
- package/src/db/migrate-runner.ts +107 -11
- package/src/db/pg-error.ts +9 -1
- package/src/db/postgres-provider.ts +2 -2
- package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
- package/src/db/queries/backfill-pii.ts +277 -0
- package/src/db/queries/ddl.ts +45 -0
- package/src/db/queries/event-consumer.ts +35 -2
- package/src/db/queries/event-store.ts +126 -19
- package/src/db/queries/projection-rebuild.ts +22 -8
- package/src/db/queries/shadow-swap.ts +183 -0
- package/src/db/queries/test-stack.ts +4 -30
- package/src/db/query-api.ts +1 -0
- package/src/db/query.ts +1 -0
- package/src/db/rebuild-marker.ts +18 -2
- package/src/db/reference-data.ts +2 -3
- package/src/db/replay-migration-sql.ts +257 -0
- package/src/db/schema-inspection.ts +1 -1
- package/src/db/table-builder.ts +102 -71
- package/src/db/tenant-db.ts +15 -53
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
- package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
- package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
- package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
- package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
- package/src/engine/__tests__/boot-validator.test.ts +219 -15
- package/src/engine/__tests__/build-app-schema.test.ts +82 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
- package/src/engine/__tests__/build-target.test.ts +3 -11
- package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
- package/src/engine/__tests__/config-helpers.test.ts +16 -0
- package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
- package/src/engine/__tests__/define-roles.test.ts +21 -0
- package/src/engine/__tests__/engine.test.ts +163 -1
- package/src/engine/__tests__/entity-handlers.test.ts +80 -0
- package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
- package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
- package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
- package/src/engine/__tests__/factories-time.test.ts +2 -66
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
- package/src/engine/__tests__/feature-manifest.test.ts +31 -1
- package/src/engine/__tests__/field-access.test.ts +23 -1
- package/src/engine/__tests__/hook-phases.test.ts +5 -5
- package/src/engine/__tests__/membership-roles.test.ts +4 -10
- package/src/engine/__tests__/nav.test.ts +86 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
- package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
- package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
- package/src/engine/__tests__/post-query-hook.test.ts +7 -7
- package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
- package/src/engine/__tests__/registry.test.ts +144 -0
- package/src/engine/__tests__/schema-builder.test.ts +18 -0
- package/src/engine/__tests__/screen.test.ts +147 -1
- package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
- package/src/engine/__tests__/store-table.test.ts +229 -0
- package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
- package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
- package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
- package/src/engine/boot-validator/action-wiring.ts +149 -0
- package/src/engine/boot-validator/boot-check.ts +21 -0
- package/src/engine/boot-validator/config-deps.ts +40 -4
- package/src/engine/boot-validator/entity-handler.ts +20 -21
- package/src/engine/boot-validator/entity-list-screens.ts +88 -0
- package/src/engine/boot-validator/gdpr-storage.ts +0 -20
- package/src/engine/boot-validator/i18n-keys.ts +58 -4
- package/src/engine/boot-validator/index.ts +33 -12
- package/src/engine/boot-validator/nav.ts +125 -0
- package/src/engine/boot-validator/pii-retention.ts +95 -10
- package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
- package/src/engine/boot-validator/workspaces.ts +68 -0
- package/src/engine/build-app-schema.ts +16 -0
- package/src/engine/build-config-feature-schema.ts +44 -7
- package/src/engine/codemod/pipeline-codemod.ts +5 -5
- package/src/engine/config-helpers.ts +15 -0
- package/src/engine/constants.ts +32 -6
- package/src/engine/create-app.ts +11 -0
- package/src/engine/define-feature.ts +95 -947
- package/src/engine/define-handler.ts +28 -94
- package/src/engine/define-workflow.ts +1 -1
- package/src/engine/effective-features.ts +12 -2
- package/src/engine/entity-handlers.ts +76 -11
- package/src/engine/extensions/tenant-data.ts +19 -0
- package/src/engine/extensions/user-data.ts +29 -2
- package/src/engine/factories.ts +8 -49
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
- package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
- package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
- package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
- package/src/engine/feature-ast/extractors/events.ts +322 -0
- package/src/engine/feature-ast/extractors/handlers.ts +234 -0
- package/src/engine/feature-ast/extractors/hooks.ts +243 -0
- package/src/engine/feature-ast/extractors/index.ts +34 -31
- package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
- package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
- package/src/engine/feature-ast/extractors/round1.ts +3 -3
- package/src/engine/feature-ast/extractors/round5.ts +3 -3
- package/src/engine/feature-ast/extractors/round6.ts +2 -36
- package/src/engine/feature-ast/extractors/shared.ts +64 -6
- package/src/engine/feature-ast/index.ts +2 -4
- package/src/engine/feature-ast/parse.ts +130 -23
- package/src/engine/feature-ast/patch.ts +39 -50
- package/src/engine/feature-ast/patcher.ts +37 -38
- package/src/engine/feature-ast/patterns.ts +50 -62
- package/src/engine/feature-ast/render.ts +67 -44
- package/src/engine/feature-builder-state.ts +168 -0
- package/src/engine/feature-config-events-jobs.ts +403 -0
- package/src/engine/feature-entity-handlers.ts +208 -0
- package/src/engine/feature-manifest.ts +2 -1
- package/src/engine/feature-ui-extensions.ts +500 -0
- package/src/engine/field-access.ts +13 -2
- package/src/engine/field-helpers.ts +31 -0
- package/src/engine/handler-helpers.ts +26 -0
- package/src/engine/hook-helpers.ts +16 -0
- package/src/engine/index.ts +23 -7
- package/src/engine/membership-roles.ts +13 -0
- package/src/engine/object-form.ts +27 -0
- package/src/engine/ownership.ts +26 -79
- package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
- package/src/engine/pattern-library/library.ts +44 -1152
- package/src/engine/pattern-library/mixed-schemas.ts +450 -0
- package/src/engine/pattern-library/opaque-schemas.ts +124 -0
- package/src/engine/pattern-library/shared-fields.ts +75 -0
- package/src/engine/pattern-library/static-schemas.ts +456 -0
- package/src/engine/pipeline.ts +6 -11
- package/src/engine/registry-facade.ts +362 -0
- package/src/engine/registry-ingest.ts +478 -0
- package/src/engine/registry-state.ts +388 -0
- package/src/engine/registry-validate.ts +642 -0
- package/src/engine/registry.ts +78 -1658
- package/src/engine/run-pipeline.ts +1 -1
- package/src/engine/schema-builder.ts +1 -0
- package/src/engine/screen-helpers.ts +54 -0
- package/src/engine/soft-delete-cleanup.ts +6 -2
- package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
- package/src/engine/steps/_duration-utils.ts +2 -0
- package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
- package/src/engine/tier-resolver-extension.ts +3 -2
- package/src/engine/types/config.ts +2 -482
- package/src/engine/types/define-handler.ts +2 -0
- package/src/engine/types/entity-handlers.ts +2 -0
- package/src/engine/types/event-type-map.ts +1 -37
- package/src/engine/types/feature.ts +2 -972
- package/src/engine/types/fields.ts +2 -675
- package/src/engine/types/handlers.ts +2 -774
- package/src/engine/types/hooks.ts +2 -184
- package/src/engine/types/http-route.ts +1 -54
- package/src/engine/types/identifiers.ts +1 -47
- package/src/engine/types/index.ts +86 -40
- package/src/engine/types/nav.ts +2 -63
- package/src/engine/types/ownership.ts +2 -0
- package/src/engine/types/projection.ts +2 -138
- package/src/engine/types/relations.ts +1 -51
- package/src/engine/types/screen.ts +2 -574
- package/src/engine/types/step.ts +2 -334
- package/src/engine/types/target-ref.ts +1 -21
- package/src/engine/types/tree-node.ts +1 -132
- package/src/engine/types/workspace.ts +2 -49
- package/src/engine/validate-projection-allowlist.ts +6 -6
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
- package/src/entrypoint/index.ts +49 -6
- package/src/errors/classes.ts +22 -1
- package/src/errors/field-issue.ts +0 -3
- package/src/errors/index.ts +1 -1
- package/src/errors/write-error-info.ts +10 -22
- package/src/es-ops/README.md +1 -1
- package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
- package/src/es-ops/context.ts +4 -2
- package/src/es-ops/types.ts +8 -1
- package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
- package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
- package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
- package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
- package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
- package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
- package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
- package/src/event-store/admin-api.ts +11 -4
- package/src/event-store/errors.ts +2 -35
- package/src/event-store/event-store.ts +64 -78
- package/src/event-store/events-schema.ts +11 -13
- package/src/event-store/index.ts +18 -3
- package/src/event-store/rebuild-dead-letter.ts +111 -0
- package/src/event-store/snapshot.ts +63 -40
- package/src/event-store/types.ts +2 -0
- package/src/files/__tests__/build-storage-key.test.ts +28 -0
- package/src/files/__tests__/file-ref-entity.test.ts +8 -0
- package/src/files/__tests__/files.integration.test.ts +24 -0
- package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
- package/src/files/__tests__/local-provider.test.ts +31 -0
- package/src/files/__tests__/provider-resolver.test.ts +70 -0
- package/src/files/__tests__/write-stream.test.ts +13 -0
- package/src/files/file-handle.ts +2 -19
- package/src/files/file-ref-entity.ts +2 -2
- package/src/files/file-routes.ts +13 -0
- package/src/files/index.ts +1 -1
- package/src/files/local-provider.ts +27 -8
- package/src/files/provider-resolver.ts +31 -20
- package/src/files/types.ts +13 -55
- package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
- package/src/i18n/required-surface-keys.ts +120 -1
- package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
- package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
- package/src/jobs/job-runner.ts +82 -12
- package/src/logging/types.ts +1 -7
- package/src/migrations/pending-rebuilds.ts +1 -1
- package/src/observability/__tests__/observability.integration.test.ts +4 -1
- package/src/observability/__tests__/recording-tracer.test.ts +6 -4
- package/src/observability/index.ts +2 -0
- package/src/observability/noop-provider.ts +0 -9
- package/src/observability/recording-tracer.ts +0 -12
- package/src/observability/standard-metrics.ts +64 -2
- package/src/observability/types/index.ts +1 -29
- package/src/observability/types/metric.ts +1 -56
- package/src/observability/types/provider.ts +1 -32
- package/src/observability/types/span.ts +1 -64
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
- package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
- package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
- package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
- package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
- package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
- package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
- package/src/pipeline/dispatch-batch.ts +187 -0
- package/src/pipeline/dispatch-query.ts +165 -0
- package/src/pipeline/dispatch-shared.ts +826 -0
- package/src/pipeline/dispatch-stream.ts +90 -0
- package/src/pipeline/dispatch-write.ts +451 -0
- package/src/pipeline/dispatcher-utils.ts +1 -1
- package/src/pipeline/dispatcher.ts +44 -1372
- package/src/pipeline/entity-cache.ts +2 -33
- package/src/pipeline/event-consumer-state.ts +30 -2
- package/src/pipeline/event-dispatcher-admin.ts +293 -0
- package/src/pipeline/event-dispatcher-delivery.ts +305 -0
- package/src/pipeline/event-dispatcher.ts +85 -542
- package/src/pipeline/index.ts +2 -0
- package/src/pipeline/msp-rebuild.ts +42 -3
- package/src/pipeline/multi-stream-apply-context.ts +4 -42
- package/src/pipeline/projection-rebuild.ts +105 -3
- package/src/pipeline/system-hooks.ts +144 -1
- package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
- package/src/rate-limit/resolver.ts +16 -32
- package/src/schema-cli.ts +76 -16
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
- package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
- package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
- package/src/search/index.ts +6 -0
- package/src/search/meilisearch-adapter.ts +13 -12
- package/src/search/reindex-entity.ts +177 -0
- package/src/search/types.ts +1 -39
- package/src/secrets/__tests__/contains-secret.test.ts +34 -0
- package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/dek-cache.ts +26 -5
- package/src/secrets/envelope-cipher.ts +53 -0
- package/src/secrets/envelope.ts +5 -3
- package/src/secrets/index.ts +13 -1
- package/src/secrets/stored-envelope.ts +46 -0
- package/src/secrets/types.ts +2 -162
- package/src/stack/__tests__/event-collector.test.ts +42 -0
- package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
- package/src/stack/db.ts +2 -1
- package/src/stack/push-entity-projection-tables.ts +4 -3
- package/src/stack/redis.ts +8 -0
- package/src/stack/request-helper.ts +38 -2
- package/src/stack/table-helpers.ts +6 -4
- package/src/stack/test-stack.ts +249 -131
- package/src/testing/__tests__/late-bound.test.ts +32 -0
- package/src/testing/__tests__/wait-for.test.ts +59 -0
- package/src/testing/boot-validator-fixture.ts +121 -0
- package/src/testing/e2e-generator.ts +8 -0
- package/src/testing/file-provider-contract.ts +104 -0
- package/src/testing/handler-context.ts +3 -1
- package/src/testing/index.ts +5 -0
- package/src/testing/late-bound.ts +5 -3
- package/src/testing/mutable-master-key-provider.ts +29 -3
- package/src/testing/wait-for.ts +3 -0
- package/src/testing/without-ambient-temporal.ts +14 -0
- package/src/time/__tests__/tz-dateline.test.ts +8 -10
- package/src/time/geo-tz.ts +1 -32
- package/src/time/index.ts +1 -0
- package/src/time/legacy-date.ts +18 -0
- package/src/time/polyfill.ts +21 -38
- package/src/time/tz-context.ts +44 -85
- package/src/ui-types/app-schema.ts +12 -0
- package/src/ui-types/index.ts +20 -7
- package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
- package/src/utils/safe-json.ts +13 -1
- package/src/__tests__/raw-table.integration.test.ts +0 -116
- package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
- package/src/db/__tests__/encryption.test.ts +0 -39
- package/src/db/encryption.ts +0 -39
- package/src/db/row-helpers.ts +0 -4
- package/src/engine/__tests__/raw-table.test.ts +0 -150
- package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
- 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
|
package/src/testing/index.ts
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/testing/wait-for.ts
CHANGED
|
@@ -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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
|
17
|
-
test("
|
|
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
|
|
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("
|
|
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));
|
package/src/time/geo-tz.ts
CHANGED
|
@@ -1,32 +1 @@
|
|
|
1
|
-
|
|
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
|
+
}
|
package/src/time/polyfill.ts
CHANGED
|
@@ -1,66 +1,49 @@
|
|
|
1
|
-
// Temporal
|
|
1
|
+
// Temporal polyfill bootstrap.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
-
*
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
58
|
-
*
|
|
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.",
|
package/src/time/tz-context.ts
CHANGED
|
@@ -1,85 +1,41 @@
|
|
|
1
|
-
// ctx.tz —
|
|
1
|
+
// ctx.tz — the feature-code API for TZ operations.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// - "
|
|
5
|
-
// - "
|
|
6
|
-
// - "
|
|
7
|
-
// - "ZonedDateTime → JSON
|
|
8
|
-
// - "JSON
|
|
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
|
|
11
|
-
//
|
|
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`
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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:
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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: () =>
|
|
91
|
-
nowIn: (tz: string) =>
|
|
92
|
-
today: (tz: string) =>
|
|
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 =
|
|
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) =>
|
|
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-
|
|
103
|
-
// .toPlainDateTime().toString()
|
|
104
|
-
//
|
|
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) =>
|
|
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:
|
|
135
|
-
*
|
|
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();
|