@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
|
@@ -17,9 +17,10 @@ import { z } from "zod";
|
|
|
17
17
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
18
18
|
import { createRegistry, defineFeature } from "../../engine";
|
|
19
19
|
import { createArchivedStreamsTable, createEventsTable } from "../../event-store";
|
|
20
|
+
import { createNoopProvider, createPrometheusMeter } from "../../observability";
|
|
20
21
|
import { createEventConsumerStateTable } from "../../pipeline";
|
|
21
22
|
import { createTestRedis, type TestRedis, TestUsers } from "../../stack";
|
|
22
|
-
import { waitFor } from "../../testing";
|
|
23
|
+
import { sleep, waitFor } from "../../testing";
|
|
23
24
|
import { createAllInOneEntrypoint } from "../index";
|
|
24
25
|
|
|
25
26
|
const jobRuns: Array<{ name: string; payload: Record<string, unknown> }> = [];
|
|
@@ -172,3 +173,121 @@ describe("createAllInOneEntrypoint auto-wires jobRunner into command-dispatcher"
|
|
|
172
173
|
}
|
|
173
174
|
});
|
|
174
175
|
});
|
|
176
|
+
|
|
177
|
+
// Regression test for #1046 — createJobRunner is built from the caller's
|
|
178
|
+
// RAW context, BEFORE buildServer ever merges observability.tracer/meter
|
|
179
|
+
// into a context object of its own. Without threading the resolved
|
|
180
|
+
// provider into the job-runner's context too, `context.meter` stayed
|
|
181
|
+
// undefined, the queue-depth poller's `if (context.meter)` guard skipped
|
|
182
|
+
// silently, and `/metrics` showed the kumiko_job_queue_depth HELP/TYPE
|
|
183
|
+
// header but never a single data line — forever, with no error anywhere.
|
|
184
|
+
describe("createAllInOneEntrypoint — kumiko_job_queue_depth sees the SAME meter as buildServer (#1046)", () => {
|
|
185
|
+
test("passing an explicit observability provider makes the queue-depth poller emit real data for BOTH lanes", async () => {
|
|
186
|
+
const registry = createRegistry([wiringFeature]);
|
|
187
|
+
const redisUrl = `redis://${testRedis.redis.options.host}:${testRedis.redis.options.port}/${testRedis.redis.options.db}`;
|
|
188
|
+
const meter = createPrometheusMeter();
|
|
189
|
+
const observability = { ...createNoopProvider(), meter };
|
|
190
|
+
const entry = createAllInOneEntrypoint({
|
|
191
|
+
registry,
|
|
192
|
+
context: { db: testDb.db, redis: testRedis.redis },
|
|
193
|
+
jwtSecret: JWT,
|
|
194
|
+
redisUrl,
|
|
195
|
+
queueNamePrefix: `wiring-qd-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
196
|
+
observability,
|
|
197
|
+
});
|
|
198
|
+
await entry.start();
|
|
199
|
+
try {
|
|
200
|
+
// If the job-runner never saw this meter (the #1046 bug), the poller's
|
|
201
|
+
// `if (context.meter)` guard skips entirely and no slots ever appear —
|
|
202
|
+
// this assertion is the one that would have caught it.
|
|
203
|
+
const snapshot = meter.snapshot().get("kumiko_job_queue_depth");
|
|
204
|
+
expect(snapshot).toBeDefined();
|
|
205
|
+
const workerWaiting = snapshot?.slots.find(
|
|
206
|
+
(s) => s.labels?.["lane"] === "worker" && s.labels?.["state"] === "waiting",
|
|
207
|
+
);
|
|
208
|
+
const apiWaiting = snapshot?.slots.find(
|
|
209
|
+
(s) => s.labels?.["lane"] === "api" && s.labels?.["state"] === "waiting",
|
|
210
|
+
);
|
|
211
|
+
expect(workerWaiting).toBeDefined();
|
|
212
|
+
expect(apiWaiting).toBeDefined();
|
|
213
|
+
// Same instance, not a coincidentally-equal one — proves buildServer's
|
|
214
|
+
// internal registerStandardMetrics() and the job-runner's poller wrote
|
|
215
|
+
// into the identical meter, not two disconnected Noop instances.
|
|
216
|
+
expect(entry.observability.meter).toBe(meter);
|
|
217
|
+
// BullMQ's fresh Worker connections are still settling right after
|
|
218
|
+
// start() returns — stopping immediately races their own connection
|
|
219
|
+
// teardown and throws an unrelated "Connection is closed" from
|
|
220
|
+
// ioredis (same artifact documented in job-queue-depth.integration.
|
|
221
|
+
// test.ts). Production runners live far longer than this.
|
|
222
|
+
await sleep(50);
|
|
223
|
+
} finally {
|
|
224
|
+
await entry.stop();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Regression test for #1253 — effectiveFeatures was wired into
|
|
230
|
+
// dispatcherOptions (command-dispatcher's feature-gate) but never merged
|
|
231
|
+
// into the job-runner's context. A job handler reading
|
|
232
|
+
// `context.effectiveFeatures?.(tenantId)` always saw undefined in prod.
|
|
233
|
+
const featuresSeenByJob: Array<ReadonlySet<string> | undefined> = [];
|
|
234
|
+
|
|
235
|
+
const featureGateJobFeature = defineFeature("featuregate", (r) => {
|
|
236
|
+
r.writeHandler(
|
|
237
|
+
"ping",
|
|
238
|
+
z.object({ msg: z.string() }),
|
|
239
|
+
async (event) => ({
|
|
240
|
+
isSuccess: true as const,
|
|
241
|
+
data: { id: 1, msg: event.payload.msg },
|
|
242
|
+
}),
|
|
243
|
+
{ access: { openToAll: true } },
|
|
244
|
+
);
|
|
245
|
+
r.job(
|
|
246
|
+
"record-features",
|
|
247
|
+
{ trigger: { on: "featuregate:write:ping" }, runIn: "worker" },
|
|
248
|
+
async (_payload, context) => {
|
|
249
|
+
featuresSeenByJob.push(context.effectiveFeatures?.(adminUser.tenantId));
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("createAllInOneEntrypoint — job context sees dispatcherOptions.effectiveFeatures (#1253)", () => {
|
|
255
|
+
test("job handler reads the same effectiveFeatures resolver as the command-dispatcher's feature-gate", async () => {
|
|
256
|
+
featuresSeenByJob.length = 0;
|
|
257
|
+
const registry = createRegistry([featureGateJobFeature]);
|
|
258
|
+
const redisUrl = `redis://${testRedis.redis.options.host}:${testRedis.redis.options.port}/${testRedis.redis.options.db}`;
|
|
259
|
+
const entry = createAllInOneEntrypoint({
|
|
260
|
+
registry,
|
|
261
|
+
context: { db: testDb.db, redis: testRedis.redis },
|
|
262
|
+
jwtSecret: JWT,
|
|
263
|
+
redisUrl,
|
|
264
|
+
queueNamePrefix: `wiring-features-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
265
|
+
dispatcherOptions: {
|
|
266
|
+
effectiveFeatures: () => new Set(["featuregate", "some-feature"]),
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
await entry.start();
|
|
270
|
+
try {
|
|
271
|
+
const token = await entry.jwt.sign(adminUser);
|
|
272
|
+
const res = await entry.app.request("/api/write", {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: {
|
|
275
|
+
"Content-Type": "application/json",
|
|
276
|
+
Authorization: `Bearer ${token}`,
|
|
277
|
+
},
|
|
278
|
+
body: JSON.stringify({ type: "featuregate:write:ping", payload: { msg: "hi" } }),
|
|
279
|
+
});
|
|
280
|
+
const result = (await res.json()) as { isSuccess: boolean };
|
|
281
|
+
expect(result.isSuccess).toBe(true);
|
|
282
|
+
|
|
283
|
+
// Without the fix, context.effectiveFeatures is undefined and this
|
|
284
|
+
// stays empty forever — waitFor times out on the missing entry.
|
|
285
|
+
await waitFor(() => {
|
|
286
|
+
expect(featuresSeenByJob.length).toBeGreaterThan(0);
|
|
287
|
+
expect(featuresSeenByJob[0]?.has("some-feature")).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
} finally {
|
|
290
|
+
await entry.stop();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
package/src/entrypoint/index.ts
CHANGED
|
@@ -32,17 +32,19 @@
|
|
|
32
32
|
|
|
33
33
|
import type { Hono } from "hono";
|
|
34
34
|
import type { AuthRoutesConfig } from "../api/auth-routes";
|
|
35
|
-
import type { JwtHelper } from "../api/jwt";
|
|
35
|
+
import type { JwtHelper, JwtKeyring } from "../api/jwt";
|
|
36
36
|
import type { KumikoServer, ServerOptions } from "../api/server";
|
|
37
37
|
import { buildServer } from "../api/server";
|
|
38
38
|
import type { SseBroker } from "../api/sse-broker";
|
|
39
39
|
import type { PgClient } from "../db/connection";
|
|
40
|
+
import type { EffectiveFeaturesResolver } from "../engine/tier-resolver-extension";
|
|
40
41
|
import type { AppContext, JobRunIn, Registry, RunIn } from "../engine/types";
|
|
41
42
|
import type { JobRunner, JobRunnerOptions } from "../jobs/job-runner";
|
|
42
43
|
import { createJobRunner } from "../jobs/job-runner";
|
|
43
44
|
import type { Lifecycle } from "../lifecycle";
|
|
44
45
|
import { createLifecycle } from "../lifecycle";
|
|
45
46
|
import type { ObservabilityOptions, ObservabilityProvider } from "../observability";
|
|
47
|
+
import { createNoopProvider } from "../observability";
|
|
46
48
|
import type { EventDedup, EventDispatcher } from "../pipeline";
|
|
47
49
|
import type { Dispatcher, DispatcherOptions } from "../pipeline/dispatcher";
|
|
48
50
|
import type { SystemHooks } from "../pipeline/lifecycle-pipeline";
|
|
@@ -52,7 +54,7 @@ import type { SystemHooks } from "../pipeline/lifecycle-pipeline";
|
|
|
52
54
|
export type BaseEntrypointOptions = {
|
|
53
55
|
readonly registry: Registry;
|
|
54
56
|
readonly context: AppContext;
|
|
55
|
-
readonly jwtSecret: string;
|
|
57
|
+
readonly jwtSecret: string | JwtKeyring;
|
|
56
58
|
readonly jwtIssuer?: string;
|
|
57
59
|
readonly observability?: ObservabilityProvider;
|
|
58
60
|
readonly observabilityOptions?: ObservabilityOptions;
|
|
@@ -175,6 +177,31 @@ function mergeDispatcherOptions(
|
|
|
175
177
|
return { ...(caller ?? {}), jobRunner };
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
// Resolved once for the job-runner's context only — buildServer still gets
|
|
181
|
+
// the caller's original `options.observability` (unresolved) since it
|
|
182
|
+
// derives `shouldWrapRedis` from whether that was explicitly set.
|
|
183
|
+
function resolveObservability(
|
|
184
|
+
observability: ObservabilityProvider | undefined,
|
|
185
|
+
): ObservabilityProvider {
|
|
186
|
+
return observability ?? createNoopProvider();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function contextWithObservability(
|
|
190
|
+
context: AppContext,
|
|
191
|
+
observability: ObservabilityProvider,
|
|
192
|
+
effectiveFeatures: EffectiveFeaturesResolver | undefined,
|
|
193
|
+
): AppContext {
|
|
194
|
+
return {
|
|
195
|
+
...context,
|
|
196
|
+
// Same source of truth as buildServer's contextWithObservability
|
|
197
|
+
// (api/server.ts) — job handlers reading ctx.effectiveFeatures should
|
|
198
|
+
// see the same resolver as command handlers, not just dispatcherOptions.
|
|
199
|
+
...(effectiveFeatures && { effectiveFeatures }),
|
|
200
|
+
tracer: observability.tracer,
|
|
201
|
+
meter: observability.meter,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
178
205
|
// buildApiServer shapes ServerOptions from API-mode caller-options.
|
|
179
206
|
// AllInOneEntrypointOptions extends ApiEntrypointOptions, so structural
|
|
180
207
|
// subtyping makes the all-in-one path a valid caller without an explicit
|
|
@@ -291,6 +318,7 @@ function requireDispatcher(server: KumikoServer, mode: string): EventDispatcher
|
|
|
291
318
|
|
|
292
319
|
export function createApiEntrypoint(options: ApiEntrypointOptions): ApiEntrypoint {
|
|
293
320
|
const lifecycle = options.lifecycle ?? createLifecycle({ startReady: true });
|
|
321
|
+
const observability = resolveObservability(options.observability);
|
|
294
322
|
|
|
295
323
|
// Boot-validation (Welle 2.6.c) — fail loud before traffic arrives:
|
|
296
324
|
// (a) Any jobs declared + no jobs-block → command-dispatcher would
|
|
@@ -324,7 +352,11 @@ export function createApiEntrypoint(options: ApiEntrypointOptions): ApiEntrypoin
|
|
|
324
352
|
const apiJobRunner = options.jobs
|
|
325
353
|
? buildJobRunnerWithHook(
|
|
326
354
|
options.registry,
|
|
327
|
-
|
|
355
|
+
contextWithObservability(
|
|
356
|
+
options.context,
|
|
357
|
+
observability,
|
|
358
|
+
options.dispatcherOptions?.effectiveFeatures,
|
|
359
|
+
),
|
|
328
360
|
options.jobs,
|
|
329
361
|
options.jobs.runLocalJobs ? "api" : undefined,
|
|
330
362
|
lifecycle,
|
|
@@ -375,9 +407,14 @@ export function createApiEntrypoint(options: ApiEntrypointOptions): ApiEntrypoin
|
|
|
375
407
|
|
|
376
408
|
export function createWorkerEntrypoint(options: WorkerEntrypointOptions): WorkerEntrypoint {
|
|
377
409
|
const lifecycle = options.lifecycle ?? createLifecycle({ startReady: true });
|
|
410
|
+
const observability = resolveObservability(options.observability);
|
|
378
411
|
const jobRunner = buildJobRunnerWithHook(
|
|
379
412
|
options.registry,
|
|
380
|
-
|
|
413
|
+
contextWithObservability(
|
|
414
|
+
options.context,
|
|
415
|
+
observability,
|
|
416
|
+
options.dispatcherOptions?.effectiveFeatures,
|
|
417
|
+
),
|
|
381
418
|
options,
|
|
382
419
|
"worker",
|
|
383
420
|
lifecycle,
|
|
@@ -406,6 +443,12 @@ export function createWorkerEntrypoint(options: WorkerEntrypointOptions): Worker
|
|
|
406
443
|
|
|
407
444
|
export function createAllInOneEntrypoint(options: AllInOneEntrypointOptions): AllInOneEntrypoint {
|
|
408
445
|
const lifecycle = options.lifecycle ?? createLifecycle({ startReady: true });
|
|
446
|
+
const observability = resolveObservability(options.observability);
|
|
447
|
+
const jobRunnerContext = contextWithObservability(
|
|
448
|
+
options.context,
|
|
449
|
+
observability,
|
|
450
|
+
options.dispatcherOptions?.effectiveFeatures,
|
|
451
|
+
);
|
|
409
452
|
|
|
410
453
|
// All-in-one consumes BOTH lanes: two runners, each with a BullMQ worker
|
|
411
454
|
// for its own lane's queue. Both runners hold queue-clients for both
|
|
@@ -416,7 +459,7 @@ export function createAllInOneEntrypoint(options: AllInOneEntrypointOptions): Al
|
|
|
416
459
|
// its own lane in its own .start().
|
|
417
460
|
const workerJobRunner = buildJobRunnerWithHook(
|
|
418
461
|
options.registry,
|
|
419
|
-
|
|
462
|
+
jobRunnerContext,
|
|
420
463
|
options,
|
|
421
464
|
"worker",
|
|
422
465
|
lifecycle,
|
|
@@ -424,7 +467,7 @@ export function createAllInOneEntrypoint(options: AllInOneEntrypointOptions): Al
|
|
|
424
467
|
);
|
|
425
468
|
const apiJobRunner = buildJobRunnerWithHook(
|
|
426
469
|
options.registry,
|
|
427
|
-
|
|
470
|
+
jobRunnerContext,
|
|
428
471
|
options,
|
|
429
472
|
"api",
|
|
430
473
|
lifecycle,
|
package/src/errors/classes.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { toSnakeCase } from "../utils/case";
|
|
|
2
2
|
import type { FieldIssue } from "./field-issue";
|
|
3
3
|
import { type ErrorOpts, KumikoError } from "./kumiko-error";
|
|
4
4
|
|
|
5
|
-
export type { FieldIssue
|
|
5
|
+
export type { FieldIssue } from "./field-issue";
|
|
6
6
|
|
|
7
7
|
export type ValidationDetails = {
|
|
8
8
|
readonly fields: readonly FieldIssue[];
|
|
@@ -138,6 +138,27 @@ export class VersionConflictError extends ConflictError {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// The caller retried a write with an idempotencyKey it had already used —
|
|
142
|
+
// the event-store's partial unique index on metadata.idempotencyKey caught
|
|
143
|
+
// the duplicate. Distinct from unique_violation/version_conflict: this is
|
|
144
|
+
// not an error the caller should retry, it means the earlier attempt
|
|
145
|
+
// already succeeded and this one is a no-op replay.
|
|
146
|
+
export class IdempotentReplayError extends ConflictError {
|
|
147
|
+
override readonly code: string = "idempotent_replay";
|
|
148
|
+
|
|
149
|
+
constructor(
|
|
150
|
+
details: { readonly idempotencyKey: string },
|
|
151
|
+
opts?: Pick<ErrorOpts, "i18nKey" | "cause">,
|
|
152
|
+
) {
|
|
153
|
+
super({
|
|
154
|
+
message: `event with idempotencyKey "${details.idempotencyKey}" was already appended`,
|
|
155
|
+
i18nKey: opts?.i18nKey ?? "errors.idempotentReplay",
|
|
156
|
+
details,
|
|
157
|
+
...(opts?.cause && { cause: opts.cause }),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
141
162
|
// Entity-level unique-index violation. Distinct from VersionConflictError:
|
|
142
163
|
// version_conflict means "two writers raced on the events_aggregate_version
|
|
143
164
|
// _uq index" (optimistic-concurrency); unique_violation means "you tried to
|
package/src/errors/index.ts
CHANGED
|
@@ -7,13 +7,13 @@ export type {
|
|
|
7
7
|
UniqueViolationDetails,
|
|
8
8
|
UnprocessableOpts,
|
|
9
9
|
ValidationDetails,
|
|
10
|
-
ValidationFieldIssue,
|
|
11
10
|
VersionConflictDetails,
|
|
12
11
|
} from "./classes";
|
|
13
12
|
export {
|
|
14
13
|
AccessDeniedError,
|
|
15
14
|
ConflictError,
|
|
16
15
|
FeatureDisabledError,
|
|
16
|
+
IdempotentReplayError,
|
|
17
17
|
InternalError,
|
|
18
18
|
NotFoundError,
|
|
19
19
|
RateLimitError,
|
|
@@ -1,30 +1,18 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WriteErrorInfo,
|
|
3
|
+
WriteFailure,
|
|
4
|
+
} from "@cosmicdrift/kumiko-types/write-error-info-types";
|
|
1
5
|
import { NotFoundError, UnprocessableError } from "./classes";
|
|
2
6
|
import { KumikoError } from "./kumiko-error";
|
|
3
7
|
import { FrameworkReasons } from "./reasons";
|
|
4
8
|
import { buildInvalidTransitionDetails } from "./transition-details";
|
|
5
9
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
export type WriteErrorInfo
|
|
12
|
-
readonly code: string;
|
|
13
|
-
readonly httpStatus: number;
|
|
14
|
-
readonly i18nKey: string;
|
|
15
|
-
readonly i18nParams?: Readonly<Record<string, unknown>>;
|
|
16
|
-
readonly message: string;
|
|
17
|
-
readonly details?: unknown;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
// The failure half of WriteResult — `{ isSuccess: false } + error`. Named
|
|
21
|
-
// so the three write-failure factories below and WriteResult share one
|
|
22
|
-
// shape instead of restating it. Not generic: the error carries zero data,
|
|
23
|
-
// so there's nothing for the caller to narrow.
|
|
24
|
-
export type WriteFailure = {
|
|
25
|
-
readonly isSuccess: false;
|
|
26
|
-
readonly error: WriteErrorInfo;
|
|
27
|
-
};
|
|
10
|
+
// Re-exported so a caller importing this module directly (instead of via
|
|
11
|
+
// the `./errors` barrel, which already re-exports these by name) still
|
|
12
|
+
// gets the types — explicit, not a wildcard: this module isn't a public
|
|
13
|
+
// subpath export (see package.json `exports`), so the only readers are
|
|
14
|
+
// framework-internal.
|
|
15
|
+
export type { WriteErrorInfo, WriteFailure };
|
|
28
16
|
|
|
29
17
|
// Convenience for call sites that return a failed WriteResult. Keeps the
|
|
30
18
|
// pattern `return writeFailure(new NotFoundError(...))` compact so handlers
|
package/src/es-ops/README.md
CHANGED
|
@@ -7,7 +7,7 @@ ES-Operations für Kumiko-Apps. Phase 1+1.5 liefert `seed-migrations` als file-b
|
|
|
7
7
|
## Quick API
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
|
-
import { runProdApp } from "@cosmicdrift/kumiko-
|
|
10
|
+
import { runProdApp } from "@cosmicdrift/kumiko-server-runtime";
|
|
11
11
|
|
|
12
12
|
await runProdApp({
|
|
13
13
|
features: [...],
|
|
@@ -72,6 +72,14 @@ const seedTestFeature = defineFeature("esopstest", (r) => {
|
|
|
72
72
|
},
|
|
73
73
|
{ access: { openToAll: true } },
|
|
74
74
|
);
|
|
75
|
+
// Role-gated handler (no openToAll): the bare system actor has no bypass
|
|
76
|
+
// for explicit role gates — only systemWriteAs' extraRoles reaches it.
|
|
77
|
+
r.writeHandler(
|
|
78
|
+
"probe:admin-only",
|
|
79
|
+
z.object({ label: z.string().min(1) }),
|
|
80
|
+
async (event, ctx) => probeExecutor.create(event.payload, event.user, ctx.db),
|
|
81
|
+
{ access: { roles: ["SystemAdmin"] } },
|
|
82
|
+
);
|
|
75
83
|
});
|
|
76
84
|
|
|
77
85
|
let testDb: TestDb;
|
|
@@ -233,6 +241,72 @@ describe("runPendingSeedMigrations (integration)", () => {
|
|
|
233
241
|
}
|
|
234
242
|
});
|
|
235
243
|
|
|
244
|
+
test("systemWriteAs ohne extraRoles → access_denied gegen rollen-gated Handler, mit extraRoles → durch", async () => {
|
|
245
|
+
const denyDir = makeTempSeedsDir([
|
|
246
|
+
{
|
|
247
|
+
name: "2026-05-20-denied.ts",
|
|
248
|
+
content: `
|
|
249
|
+
export default {
|
|
250
|
+
description: "no extraRoles against a role-gated handler",
|
|
251
|
+
run: async (ctx) => {
|
|
252
|
+
await ctx.systemWriteAs("esopstest:write:probe:admin-only", { label: "denied" });
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
`,
|
|
256
|
+
},
|
|
257
|
+
]);
|
|
258
|
+
try {
|
|
259
|
+
await expect(
|
|
260
|
+
runPendingSeedMigrations({
|
|
261
|
+
db: testDb.db,
|
|
262
|
+
seedsDir: denyDir,
|
|
263
|
+
appliedBy: "boot",
|
|
264
|
+
registry,
|
|
265
|
+
createContext: (dbRunner) => createSeedMigrationContext({ dispatcher, dbRunner }),
|
|
266
|
+
logger: () => {},
|
|
267
|
+
}),
|
|
268
|
+
).rejects.toThrow(/access_denied/);
|
|
269
|
+
expect(await selectEvents()).toHaveLength(0);
|
|
270
|
+
} finally {
|
|
271
|
+
rmSync(denyDir, { recursive: true, force: true });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const allowDir = makeTempSeedsDir([
|
|
275
|
+
{
|
|
276
|
+
name: "2026-05-20-allowed.ts",
|
|
277
|
+
content: `
|
|
278
|
+
export default {
|
|
279
|
+
description: "extraRoles reaches a role-gated handler",
|
|
280
|
+
run: async (ctx) => {
|
|
281
|
+
await ctx.systemWriteAs(
|
|
282
|
+
"esopstest:write:probe:admin-only",
|
|
283
|
+
{ label: "allowed" },
|
|
284
|
+
undefined,
|
|
285
|
+
["SystemAdmin"],
|
|
286
|
+
);
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
`,
|
|
290
|
+
},
|
|
291
|
+
]);
|
|
292
|
+
try {
|
|
293
|
+
await runPendingSeedMigrations({
|
|
294
|
+
db: testDb.db,
|
|
295
|
+
seedsDir: allowDir,
|
|
296
|
+
appliedBy: "boot",
|
|
297
|
+
registry,
|
|
298
|
+
createContext: (dbRunner) => createSeedMigrationContext({ dispatcher, dbRunner }),
|
|
299
|
+
logger: () => {},
|
|
300
|
+
});
|
|
301
|
+
const events = await selectEvents();
|
|
302
|
+
expect(events).toHaveLength(1);
|
|
303
|
+
expect(events[0]?.type).toBe("esops-probe.created");
|
|
304
|
+
expect(events[0]?.tenant_id).toBe(SYSTEM_TENANT_ID);
|
|
305
|
+
} finally {
|
|
306
|
+
rmSync(allowDir, { recursive: true, force: true });
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
236
310
|
test("WriteResult{isSuccess:false} → throw + Marker NICHT geschrieben", async () => {
|
|
237
311
|
// Critical: ohne diese Garantie würde ein silent-failed Write den Seed
|
|
238
312
|
// als "applied" markieren → beim nächsten Boot kein retry → DB-Drift
|
package/src/es-ops/context.ts
CHANGED
|
@@ -42,13 +42,15 @@ export function createSeedMigrationContext(
|
|
|
42
42
|
const defaultSystemUser = createSystemUser(SYSTEM_TENANT_ID);
|
|
43
43
|
|
|
44
44
|
return {
|
|
45
|
-
systemWriteAs: async (handlerQualifiedName, payload, tenantIdOverride) => {
|
|
45
|
+
systemWriteAs: async (handlerQualifiedName, payload, tenantIdOverride, extraRoles) => {
|
|
46
46
|
// tenantIdOverride: baut einen System-User mit der Stream-tenantId
|
|
47
47
|
// damit der Event-Store-Executor das Aggregate im richtigen Stream
|
|
48
48
|
// findet. Verhindert die version_conflict-Falle (siehe Memory
|
|
49
49
|
// feedback_event_store_tenant_consistency.md).
|
|
50
50
|
const executor =
|
|
51
|
-
tenantIdOverride !== undefined
|
|
51
|
+
tenantIdOverride !== undefined || (extraRoles && extraRoles.length > 0)
|
|
52
|
+
? createSystemUser(tenantIdOverride ?? SYSTEM_TENANT_ID, extraRoles)
|
|
53
|
+
: defaultSystemUser;
|
|
52
54
|
const result = await args.dispatcher.write(handlerQualifiedName, payload, executor);
|
|
53
55
|
// Critical: WriteResult{isSuccess: false} würde sonst silent durchlaufen
|
|
54
56
|
// → Marker landet trotz failed-Write → Migration falsch als "applied"
|
package/src/es-ops/types.ts
CHANGED
|
@@ -95,11 +95,18 @@ export type SeedMigrationContext = {
|
|
|
95
95
|
* tenantIdOverride weglassen (Default SYSTEM_TENANT_ID).
|
|
96
96
|
* - Tenant-scope-Aggregate (memberships, tenant-config, app-data) →
|
|
97
97
|
* `tenantIdOverride: m.tenantId` (oder den Stream-Tenant aus
|
|
98
|
-
* einem find*-Helper).
|
|
98
|
+
* einem find*-Helper).
|
|
99
|
+
*
|
|
100
|
+
* **extraRoles:** hasAccess has no system-bypass — a handler with an
|
|
101
|
+
* explicit role gate (e.g. `access: { roles: ["SystemAdmin"] }` or
|
|
102
|
+
* `["anonymous"]`) otherwise rejects the bare `system` actor with
|
|
103
|
+
* `access_denied`. Pass the required role(s) here — see
|
|
104
|
+
* `createSystemUser`'s `extraRoles` doc. */
|
|
99
105
|
readonly systemWriteAs: (
|
|
100
106
|
handlerQualifiedName: string,
|
|
101
107
|
payload: unknown,
|
|
102
108
|
tenantIdOverride?: TenantId,
|
|
109
|
+
extraRoles?: readonly string[],
|
|
103
110
|
) => Promise<WriteResult>;
|
|
104
111
|
|
|
105
112
|
// Read-helpers für die häufigsten Lookups. Wachsen on-demand —
|
|
@@ -15,7 +15,7 @@ import { asRawClient, selectMany } from "../../db/query";
|
|
|
15
15
|
import { createTestDb, type TestDb } from "../../stack";
|
|
16
16
|
import { generateId as uuid } from "../../utils";
|
|
17
17
|
import { appendRaw, appendRawBatch, type RawEventToAppend } from "../admin-api";
|
|
18
|
-
import { VersionConflictError } from "../errors";
|
|
18
|
+
import { IdempotentAppendConflictError, VersionConflictError } from "../errors";
|
|
19
19
|
import { append, loadAggregate } from "../event-store";
|
|
20
20
|
import { createEventsTable, eventsTable } from "../events-schema";
|
|
21
21
|
|
|
@@ -187,6 +187,32 @@ describe("appendRaw — single event", () => {
|
|
|
187
187
|
).rejects.toBeInstanceOf(VersionConflictError);
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
+
test("appendRaw distinguishes an idempotency-key replay from a version conflict (#1499)", async () => {
|
|
191
|
+
const idempotencyKey = uuid();
|
|
192
|
+
await appendRaw(
|
|
193
|
+
testDb.db,
|
|
194
|
+
makeEvent({
|
|
195
|
+
aggregateId: uuid(),
|
|
196
|
+
expectedVersion: 0,
|
|
197
|
+
metadata: { userId: userMigration, idempotencyKey },
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// A second, different aggregate reusing the same idempotencyKey hits
|
|
202
|
+
// the tenant-scoped partial unique index — distinct from a version
|
|
203
|
+
// race on the same aggregate, and must not be reported as one.
|
|
204
|
+
await expect(
|
|
205
|
+
appendRaw(
|
|
206
|
+
testDb.db,
|
|
207
|
+
makeEvent({
|
|
208
|
+
aggregateId: uuid(),
|
|
209
|
+
expectedVersion: 0,
|
|
210
|
+
metadata: { userId: userMigration, idempotencyKey },
|
|
211
|
+
}),
|
|
212
|
+
),
|
|
213
|
+
).rejects.toBeInstanceOf(IdempotentAppendConflictError);
|
|
214
|
+
});
|
|
215
|
+
|
|
190
216
|
test("version_conflict on missing predecessor (appendRaw v=5 without v=1..4)", async () => {
|
|
191
217
|
const aggregateId = uuid();
|
|
192
218
|
// Try to write version=5 (expectedVersion=4) against an empty stream.
|