@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
|
@@ -2,9 +2,13 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
|
|
4
4
|
import type { TenantId } from "../../engine/types/identifiers";
|
|
5
|
+
import { createSecret } from "../../secrets/types";
|
|
5
6
|
import { createTestUser } from "../../stack";
|
|
7
|
+
import { createRecordingProvider } from "../../testing";
|
|
6
8
|
import { createDispatcher } from "../dispatcher";
|
|
7
9
|
|
|
10
|
+
const streamCleanupState = { closed: false };
|
|
11
|
+
|
|
8
12
|
const echoFeature = defineFeature("echo", (r) => {
|
|
9
13
|
r.entity("item", createEntity({ table: "Items", fields: { name: createTextField() } }));
|
|
10
14
|
|
|
@@ -25,6 +29,40 @@ const echoFeature = defineFeature("echo", (r) => {
|
|
|
25
29
|
{ access: { openToAll: true } },
|
|
26
30
|
);
|
|
27
31
|
|
|
32
|
+
r.streamHandler(
|
|
33
|
+
"item:tail",
|
|
34
|
+
z.object({ count: z.number().int().min(0) }),
|
|
35
|
+
async function* (query) {
|
|
36
|
+
for (let i = 0; i < query.payload.count; i++) {
|
|
37
|
+
yield { i };
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{ access: { roles: ["Admin"] } },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
r.streamHandler(
|
|
44
|
+
"item:tail-leak",
|
|
45
|
+
z.object({}),
|
|
46
|
+
async function* () {
|
|
47
|
+
yield { apiKey: createSecret("leak-me") };
|
|
48
|
+
},
|
|
49
|
+
{ access: { roles: ["Admin"] } },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
r.streamHandler(
|
|
53
|
+
"item:tail-cleanup",
|
|
54
|
+
z.object({}),
|
|
55
|
+
async function* () {
|
|
56
|
+
try {
|
|
57
|
+
yield { i: 0 };
|
|
58
|
+
yield { i: 1 };
|
|
59
|
+
} finally {
|
|
60
|
+
streamCleanupState.closed = true;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{ access: { roles: ["Admin"] } },
|
|
64
|
+
);
|
|
65
|
+
|
|
28
66
|
r.hook("validation", "item:create", (data) => {
|
|
29
67
|
if (data["name"] === "forbidden") return [{ field: "name", error: "forbidden_name" }];
|
|
30
68
|
return null;
|
|
@@ -100,6 +138,35 @@ describe("dispatcher.write", () => {
|
|
|
100
138
|
expect(result.isSuccess).toBe(true);
|
|
101
139
|
});
|
|
102
140
|
|
|
141
|
+
test("user-bucketed handler without RateLimitResolver → internal_error", async () => {
|
|
142
|
+
// Misconfig: a handler opts into L3 rate-limit but the app never loaded the
|
|
143
|
+
// rate-limiting feature. User buckets cannot skip (unlike ip+no-IP), so we
|
|
144
|
+
// fail loud at first write instead of silently running uncapped.
|
|
145
|
+
const rlFeature = defineFeature("rl-user", (r) => {
|
|
146
|
+
r.entity("item", createEntity({ table: "Items", fields: { name: createTextField() } }));
|
|
147
|
+
r.writeHandler(
|
|
148
|
+
"item:create",
|
|
149
|
+
z.object({ name: z.string() }),
|
|
150
|
+
async (event) => ({ isSuccess: true, data: { name: event.payload.name } }),
|
|
151
|
+
{
|
|
152
|
+
access: { openToAll: true },
|
|
153
|
+
rateLimit: { per: "user", limit: 3, windowSeconds: 60 },
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
const dispatcher = createDispatcher(createRegistry([rlFeature]), {});
|
|
158
|
+
const result = await dispatcher.write(
|
|
159
|
+
"rl-user:write:item:create",
|
|
160
|
+
{ name: "should-not-run" },
|
|
161
|
+
createTestUser(),
|
|
162
|
+
);
|
|
163
|
+
expect(result.isSuccess).toBe(false);
|
|
164
|
+
if (!result.isSuccess) {
|
|
165
|
+
expect(result.error.code).toBe("internal_error");
|
|
166
|
+
expect(result.error.message).toMatch(/RateLimitResolver is configured/);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
103
170
|
test("ctx.user ist Convenience-Alias auf event.user (gleicher Wert)", async () => {
|
|
104
171
|
// Pinst dass der Handler auf ctx.user zugreifen kann ohne den
|
|
105
172
|
// typo-resistenten event.user-Pfad zu nutzen. Identitätsprüfung
|
|
@@ -185,6 +252,231 @@ describe("dispatcher.query", () => {
|
|
|
185
252
|
});
|
|
186
253
|
});
|
|
187
254
|
|
|
255
|
+
async function collect<T>(gen: AsyncGenerator<T>): Promise<T[]> {
|
|
256
|
+
const out: T[] = [];
|
|
257
|
+
for await (const chunk of gen) out.push(chunk);
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
describe("dispatcher.stream", () => {
|
|
262
|
+
test("validates and calls stream handler, yielding chunks in order", async () => {
|
|
263
|
+
const dispatcher = createTestDispatcher();
|
|
264
|
+
const chunks = await collect(
|
|
265
|
+
dispatcher.stream(
|
|
266
|
+
"echo:stream:item:tail",
|
|
267
|
+
{ count: 3 },
|
|
268
|
+
createTestUser({ roles: ["Admin"] }),
|
|
269
|
+
),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
expect(chunks).toEqual([{ i: 0 }, { i: 1 }, { i: 2 }]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("gates fire on first pull, not on the stream() call itself", async () => {
|
|
276
|
+
const dispatcher = createTestDispatcher();
|
|
277
|
+
const gen = dispatcher.stream(
|
|
278
|
+
"echo:stream:item:tail",
|
|
279
|
+
{ count: -1 },
|
|
280
|
+
createTestUser({ roles: ["Admin"] }),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
await expect(collect(gen)).rejects.toMatchObject({ code: "validation_error", httpStatus: 400 });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("rejects unauthorized user", async () => {
|
|
287
|
+
const dispatcher = createTestDispatcher();
|
|
288
|
+
const guest = createTestUser({ roles: ["Guest"] });
|
|
289
|
+
|
|
290
|
+
await expect(
|
|
291
|
+
collect(dispatcher.stream("echo:stream:item:tail", { count: 1 }, guest)),
|
|
292
|
+
).rejects.toMatchObject({ code: "access_denied" });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("throws for unknown stream handler", async () => {
|
|
296
|
+
const dispatcher = createTestDispatcher();
|
|
297
|
+
|
|
298
|
+
await expect(
|
|
299
|
+
collect(dispatcher.stream("nonexistent", {}, createTestUser({ roles: ["Admin"] }))),
|
|
300
|
+
).rejects.toMatchObject({ code: "not_found", httpStatus: 404 });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("a chunk containing a Secret<> value aborts the stream instead of leaking it", async () => {
|
|
304
|
+
const dispatcher = createTestDispatcher();
|
|
305
|
+
|
|
306
|
+
await expect(
|
|
307
|
+
collect(
|
|
308
|
+
dispatcher.stream("echo:stream:item:tail-leak", {}, createTestUser({ roles: ["Admin"] })),
|
|
309
|
+
),
|
|
310
|
+
).rejects.toMatchObject({ message: expect.stringContaining("Secret<> leaked") });
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("consumer breaking out of for-await runs the handler generator's finally block", async () => {
|
|
314
|
+
streamCleanupState.closed = false;
|
|
315
|
+
const dispatcher = createTestDispatcher();
|
|
316
|
+
const gen = dispatcher.stream(
|
|
317
|
+
"echo:stream:item:tail-cleanup",
|
|
318
|
+
{},
|
|
319
|
+
createTestUser({ roles: ["Admin"] }),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
for await (const _chunk of gen) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
expect(streamCleanupState.closed).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("activates the dispatcher span in observabilityContext for every pull, not just the first", async () => {
|
|
330
|
+
// Regression for #1516: runStreamInstrumented used to start the span but
|
|
331
|
+
// never run the stream inside observabilityContext.run(...), so a
|
|
332
|
+
// handler-side startSpan() picked up whatever ALS context happened to be
|
|
333
|
+
// active at that pull (or none) instead of the stream's dispatcher span.
|
|
334
|
+
const provider = createRecordingProvider();
|
|
335
|
+
const nestedFeature = defineFeature("nested", (r) => {
|
|
336
|
+
r.streamHandler(
|
|
337
|
+
"tail",
|
|
338
|
+
z.object({ count: z.number().int().min(0) }),
|
|
339
|
+
async function* (query, handlerCtx) {
|
|
340
|
+
for (let i = 0; i < query.payload.count; i++) {
|
|
341
|
+
const child = handlerCtx.tracer.startSpan("nested.child");
|
|
342
|
+
child.end();
|
|
343
|
+
yield { i };
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
{ access: { openToAll: true } },
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
const dispatcher = createDispatcher(createRegistry([nestedFeature]), {
|
|
350
|
+
tracer: provider.tracer,
|
|
351
|
+
meter: provider.meter,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await collect(dispatcher.stream("nested:stream:tail", { count: 3 }, createTestUser()));
|
|
355
|
+
|
|
356
|
+
const dispatcherSpan = provider.spansByName("kumiko.dispatcher.handler")[0]!;
|
|
357
|
+
const childSpans = provider.spansByName("nested.child");
|
|
358
|
+
expect(childSpans).toHaveLength(3);
|
|
359
|
+
for (const child of childSpans) {
|
|
360
|
+
expect(child.parentSpanId).toBe(dispatcherSpan.spanId);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("a close-time error on an otherwise-clean stream is reported, not swallowed", async () => {
|
|
365
|
+
// Regression for #1543: runStreamInstrumented's finally used to
|
|
366
|
+
// catch+discard any error from generator.return() unconditionally, even
|
|
367
|
+
// when nothing else had failed — an abandoned stream whose handler's
|
|
368
|
+
// own cleanup throws (closing a cursor/unsubscribing) was invisible to
|
|
369
|
+
// both the error-counter and the span status.
|
|
370
|
+
const provider = createRecordingProvider();
|
|
371
|
+
const closeFailFeature = defineFeature("closefail", (r) => {
|
|
372
|
+
r.streamHandler(
|
|
373
|
+
"tail",
|
|
374
|
+
z.object({}),
|
|
375
|
+
async function* () {
|
|
376
|
+
try {
|
|
377
|
+
yield { i: 0 };
|
|
378
|
+
yield { i: 1 };
|
|
379
|
+
} finally {
|
|
380
|
+
// biome-ignore lint/correctness/noUnsafeFinally: deliberately simulating a handler whose close-time cleanup fails
|
|
381
|
+
throw new Error("close-boom");
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
{ access: { openToAll: true } },
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
const dispatcher = createDispatcher(createRegistry([closeFailFeature]), {
|
|
388
|
+
tracer: provider.tracer,
|
|
389
|
+
meter: provider.meter,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const gen = dispatcher.stream("closefail:stream:tail", {}, createTestUser());
|
|
393
|
+
for await (const _chunk of gen) {
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const errorCounters = provider.metricEvents.filter(
|
|
398
|
+
(e) => e.type === "counter.inc" && e.name === "kumiko_dispatcher_handler_errors_total",
|
|
399
|
+
);
|
|
400
|
+
expect(errorCounters).toHaveLength(1);
|
|
401
|
+
const dispatcherSpan = provider.spansByName("kumiko.dispatcher.handler")[0]!;
|
|
402
|
+
expect(dispatcherSpan.status).toBe("error");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("draining a stream to completion emits outcome:completed", async () => {
|
|
406
|
+
// completedNormally=true only when the inner generator finishes on its
|
|
407
|
+
// own — pins the success-path label so a refactor that drops the flag
|
|
408
|
+
// cannot silently reclassify clean streams as aborted (#1551).
|
|
409
|
+
const provider = createRecordingProvider();
|
|
410
|
+
const feature = defineFeature("complete", (r) => {
|
|
411
|
+
r.streamHandler(
|
|
412
|
+
"tail",
|
|
413
|
+
z.object({}),
|
|
414
|
+
async function* () {
|
|
415
|
+
yield { i: 0 };
|
|
416
|
+
yield { i: 1 };
|
|
417
|
+
},
|
|
418
|
+
{ access: { openToAll: true } },
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
const dispatcher = createDispatcher(createRegistry([feature]), {
|
|
422
|
+
tracer: provider.tracer,
|
|
423
|
+
meter: provider.meter,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
await collect(dispatcher.stream("complete:stream:tail", {}, createTestUser()));
|
|
427
|
+
|
|
428
|
+
const duration = provider.metricEvents.find(
|
|
429
|
+
(e) =>
|
|
430
|
+
e.type === "histogram.observe" && e.name === "kumiko_dispatcher_handler_duration_seconds",
|
|
431
|
+
);
|
|
432
|
+
expect(duration).toBeDefined();
|
|
433
|
+
expect(duration?.labels).toMatchObject({
|
|
434
|
+
handler: "complete:stream:tail",
|
|
435
|
+
success: "true",
|
|
436
|
+
outcome: "completed",
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("generator.return() mid-stream emits outcome:aborted, not completed", async () => {
|
|
441
|
+
// Consumer walk-away (SSE tab-close) must not count as a clean
|
|
442
|
+
// completion — completedNormally stays false when return() interrupts
|
|
443
|
+
// the loop (#1551).
|
|
444
|
+
const provider = createRecordingProvider();
|
|
445
|
+
const feature = defineFeature("abort", (r) => {
|
|
446
|
+
r.streamHandler(
|
|
447
|
+
"tail",
|
|
448
|
+
z.object({}),
|
|
449
|
+
async function* () {
|
|
450
|
+
yield { i: 0 };
|
|
451
|
+
yield { i: 1 };
|
|
452
|
+
yield { i: 2 };
|
|
453
|
+
},
|
|
454
|
+
{ access: { openToAll: true } },
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
const dispatcher = createDispatcher(createRegistry([feature]), {
|
|
458
|
+
tracer: provider.tracer,
|
|
459
|
+
meter: provider.meter,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const gen = dispatcher.stream("abort:stream:tail", {}, createTestUser());
|
|
463
|
+
const first = await gen.next();
|
|
464
|
+
expect(first.value).toEqual({ i: 0 });
|
|
465
|
+
await gen.return(undefined);
|
|
466
|
+
|
|
467
|
+
const duration = provider.metricEvents.find(
|
|
468
|
+
(e) =>
|
|
469
|
+
e.type === "histogram.observe" && e.name === "kumiko_dispatcher_handler_duration_seconds",
|
|
470
|
+
);
|
|
471
|
+
expect(duration).toBeDefined();
|
|
472
|
+
expect(duration?.labels).toMatchObject({
|
|
473
|
+
handler: "abort:stream:tail",
|
|
474
|
+
success: "true",
|
|
475
|
+
outcome: "aborted",
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
188
480
|
// --- postQuery hooks on standalone (entity-less) queries ---
|
|
189
481
|
|
|
190
482
|
describe("dispatcher.query postQuery hooks", () => {
|
|
@@ -333,6 +625,20 @@ describe("dispatcher feature-gate", () => {
|
|
|
333
625
|
async (event) => ({ isSuccess: true, data: { name: event.payload.name } }),
|
|
334
626
|
{ access: { roles: ["Admin"] } },
|
|
335
627
|
);
|
|
628
|
+
r.streamHandler(
|
|
629
|
+
"widget:tail",
|
|
630
|
+
z.object({}).passthrough(),
|
|
631
|
+
async function* () {
|
|
632
|
+
// Never completes on its own — the mid-stream toggle-off test
|
|
633
|
+
// relies on the generator staying open past the first yield.
|
|
634
|
+
let i = 0;
|
|
635
|
+
while (true) {
|
|
636
|
+
yield { i };
|
|
637
|
+
i++;
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
{ access: { openToAll: true } },
|
|
641
|
+
);
|
|
336
642
|
});
|
|
337
643
|
}
|
|
338
644
|
|
|
@@ -368,6 +674,28 @@ describe("dispatcher feature-gate", () => {
|
|
|
368
674
|
});
|
|
369
675
|
});
|
|
370
676
|
|
|
677
|
+
test("disabling the feature mid-stream aborts an already-open stream", async () => {
|
|
678
|
+
const registry = createRegistry([toggled()]);
|
|
679
|
+
const disabled = new Set<string>();
|
|
680
|
+
const dispatcher = createDispatcher(
|
|
681
|
+
registry,
|
|
682
|
+
{},
|
|
683
|
+
{
|
|
684
|
+
effectiveFeatures: () => {
|
|
685
|
+
const all = new Set(registry.features.keys());
|
|
686
|
+
for (const d of disabled) all.delete(d);
|
|
687
|
+
return all;
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
const gen = dispatcher.stream("toggled:stream:widget:tail", {}, user);
|
|
693
|
+
expect((await gen.next()).value).toEqual({ i: 0 });
|
|
694
|
+
|
|
695
|
+
disabled.add("toggled");
|
|
696
|
+
await expect(gen.next()).rejects.toThrow(/feature toggled is disabled/);
|
|
697
|
+
});
|
|
698
|
+
|
|
371
699
|
test("write of disabled feature returns WriteFailure with feature_disabled reason", async () => {
|
|
372
700
|
const registry = createRegistry([toggled()]);
|
|
373
701
|
const disabled = new Set<string>(["toggled"]);
|
|
@@ -480,7 +808,7 @@ describe("dispatcher feature-gate", () => {
|
|
|
480
808
|
r.queryHandler(
|
|
481
809
|
"check",
|
|
482
810
|
z.object({}).passthrough(),
|
|
483
|
-
async (_event, ctx) => ({ enabled: ctx.hasFeature("toggled") }),
|
|
811
|
+
async (_event, ctx) => ({ enabled: await ctx.hasFeature("toggled") }),
|
|
484
812
|
{ access: { openToAll: true } },
|
|
485
813
|
);
|
|
486
814
|
});
|
|
@@ -504,6 +832,43 @@ describe("dispatcher feature-gate", () => {
|
|
|
504
832
|
enabled: false,
|
|
505
833
|
});
|
|
506
834
|
});
|
|
835
|
+
|
|
836
|
+
test("issue #1233: ctx.hasFeature falls back to trialGate for a companion feature the tenant doesn't own", async () => {
|
|
837
|
+
// Regression for the trial-tenant bug: a handler owned by an always-on
|
|
838
|
+
// feature checks a DIFFERENT (trial-tier) feature via ctx.hasFeature.
|
|
839
|
+
// The sync resolver never carries trial features, so without the
|
|
840
|
+
// trialGate fallback this silently reads false for a trial tenant even
|
|
841
|
+
// though the tenant is genuinely in-trial for that companion feature —
|
|
842
|
+
// mirrors packages/bundled-features/src/managed-pages/handlers/branding.query.ts
|
|
843
|
+
// checking MANAGED_PAGES_CSS_FEATURE, which no handler owns.
|
|
844
|
+
const trialTenant = "00000000-0000-4000-8000-0000000000c5" as TenantId;
|
|
845
|
+
const plainTenant = "00000000-0000-4000-8000-0000000000d6" as TenantId;
|
|
846
|
+
|
|
847
|
+
const probe = defineFeature("probe", (r) => {
|
|
848
|
+
r.queryHandler(
|
|
849
|
+
"check",
|
|
850
|
+
z.object({}).passthrough(),
|
|
851
|
+
async (_event, ctx) => ({ enabled: await ctx.hasFeature("premium-companion") }),
|
|
852
|
+
{ access: { openToAll: true } },
|
|
853
|
+
);
|
|
854
|
+
});
|
|
855
|
+
const registry = createRegistry([probe]);
|
|
856
|
+
const effectiveFeatures = Object.assign(() => new Set(["probe"]), {
|
|
857
|
+
trialGate: async (tenantId: TenantId, feature: string) =>
|
|
858
|
+
tenantId === trialTenant && feature === "premium-companion",
|
|
859
|
+
});
|
|
860
|
+
const dispatcher = createDispatcher(registry, {}, { effectiveFeatures });
|
|
861
|
+
|
|
862
|
+
const trialUser = createTestUser({ id: "u-c5", tenantId: trialTenant, roles: ["Admin"] });
|
|
863
|
+
const plainUser = createTestUser({ id: "u-d6", tenantId: plainTenant, roles: ["Admin"] });
|
|
864
|
+
|
|
865
|
+
await expect(dispatcher.query("probe:query:check", {}, trialUser)).resolves.toEqual({
|
|
866
|
+
enabled: true,
|
|
867
|
+
});
|
|
868
|
+
await expect(dispatcher.query("probe:query:check", {}, plainUser)).resolves.toEqual({
|
|
869
|
+
enabled: false,
|
|
870
|
+
});
|
|
871
|
+
});
|
|
507
872
|
});
|
|
508
873
|
|
|
509
874
|
describe("write-handler shape guard", () => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createTestDb, type TestDb } from "../../stack";
|
|
3
|
+
import { createEventConsumerStateTable } from "../event-consumer-state";
|
|
4
|
+
|
|
5
|
+
// #1362: two dispatcher instances booting concurrently against the same DB
|
|
6
|
+
// both see kumiko_event_consumers already present but rearm_count missing,
|
|
7
|
+
// and both ALTER TABLE. Without IF NOT EXISTS the loser crashes on boot
|
|
8
|
+
// with "column already exists" — a real TOCTOU since this table is
|
|
9
|
+
// explicitly multi-instance.
|
|
10
|
+
describe("createEventConsumerStateTable — concurrent boot", () => {
|
|
11
|
+
let testDb: TestDb;
|
|
12
|
+
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
testDb = await createTestDb();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterAll(async () => {
|
|
18
|
+
await testDb.cleanup();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("running it twice concurrently does not throw", async () => {
|
|
22
|
+
await createEventConsumerStateTable(testDb.db);
|
|
23
|
+
|
|
24
|
+
await expect(
|
|
25
|
+
Promise.all([
|
|
26
|
+
createEventConsumerStateTable(testDb.db),
|
|
27
|
+
createEventConsumerStateTable(testDb.db),
|
|
28
|
+
]),
|
|
29
|
+
).resolves.toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// EventConsumerErrorPolicy.maxAttempts lets a consumer override the
|
|
2
|
+
// dispatcher-wide default (kumiko-framework#1349) — a consumer depending on
|
|
3
|
+
// infra that may still be provisioning at boot (search adapter) needs more
|
|
4
|
+
// retry headroom before it gets dead-lettered. deliverEvents is a pure
|
|
5
|
+
// free function, so this is unit-level: no DB, no dispatcher lifecycle.
|
|
6
|
+
|
|
7
|
+
import { beforeAll, describe, expect, test } from "bun:test";
|
|
8
|
+
import type { AppContext } from "../../engine/types";
|
|
9
|
+
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
10
|
+
import type { EventConsumer } from "../event-dispatcher";
|
|
11
|
+
import {
|
|
12
|
+
type ConsumerStateRow,
|
|
13
|
+
deliverEvents,
|
|
14
|
+
type StoredEventRow,
|
|
15
|
+
} from "../event-dispatcher-delivery";
|
|
16
|
+
import { createSearchEventConsumer } from "../system-hooks";
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
await ensureTemporalPolyfill();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function stubContext(): AppContext {
|
|
23
|
+
return {
|
|
24
|
+
db: {} as unknown as AppContext["db"],
|
|
25
|
+
redis: {} as unknown as AppContext["redis"],
|
|
26
|
+
registry: {} as unknown as AppContext["registry"],
|
|
27
|
+
} as AppContext;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stubEvent(id: bigint): StoredEventRow {
|
|
31
|
+
return {
|
|
32
|
+
id,
|
|
33
|
+
aggregateId: "agg-1",
|
|
34
|
+
aggregateType: "widget",
|
|
35
|
+
tenantId: "tenant-1",
|
|
36
|
+
version: 1,
|
|
37
|
+
type: "widget.created",
|
|
38
|
+
eventVersion: 1,
|
|
39
|
+
payload: {},
|
|
40
|
+
metadata: { userId: "system" },
|
|
41
|
+
createdAt: Temporal.Now.instant(),
|
|
42
|
+
createdBy: "system",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stubState(): ConsumerStateRow {
|
|
47
|
+
return {
|
|
48
|
+
name: "consumer",
|
|
49
|
+
instanceId: "shared",
|
|
50
|
+
lastProcessedEventId: 0n,
|
|
51
|
+
status: "idle",
|
|
52
|
+
attempts: 0,
|
|
53
|
+
rearmCount: 0,
|
|
54
|
+
lastError: null,
|
|
55
|
+
updatedAt: Temporal.Now.instant(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("deliverEvents — per-consumer maxAttempts override", () => {
|
|
60
|
+
test("stays alive past the dispatcher-wide default when errorPolicy.maxAttempts is higher", async () => {
|
|
61
|
+
const consumer: EventConsumer = {
|
|
62
|
+
name: "search",
|
|
63
|
+
errorPolicy: { maxAttempts: 20 },
|
|
64
|
+
handler: async () => {
|
|
65
|
+
throw new Error("meilisearch not provisioned yet");
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const outcome = await deliverEvents(
|
|
70
|
+
consumer,
|
|
71
|
+
[stubEvent(1n)],
|
|
72
|
+
stubContext(),
|
|
73
|
+
/* dispatcher-wide default */ 10,
|
|
74
|
+
{ ...stubState(), attempts: 10 },
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(outcome.attempts).toBe(11);
|
|
78
|
+
expect(outcome.deadLettered).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("dead-letters at the override once attempts reach it", async () => {
|
|
82
|
+
const consumer: EventConsumer = {
|
|
83
|
+
name: "search",
|
|
84
|
+
errorPolicy: { maxAttempts: 20 },
|
|
85
|
+
handler: async () => {
|
|
86
|
+
throw new Error("meilisearch not provisioned yet");
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const outcome = await deliverEvents(consumer, [stubEvent(1n)], stubContext(), 10, {
|
|
91
|
+
...stubState(),
|
|
92
|
+
attempts: 19,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(outcome.attempts).toBe(20);
|
|
96
|
+
expect(outcome.deadLettered).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("a consumer without the override still dies at the dispatcher-wide default", async () => {
|
|
100
|
+
const consumer: EventConsumer = {
|
|
101
|
+
name: "plain",
|
|
102
|
+
handler: async () => {
|
|
103
|
+
throw new Error("boom");
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const outcome = await deliverEvents(consumer, [stubEvent(1n)], stubContext(), 10, {
|
|
108
|
+
...stubState(),
|
|
109
|
+
attempts: 9,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(outcome.attempts).toBe(10);
|
|
113
|
+
expect(outcome.deadLettered).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("createSearchEventConsumer — errorPolicy wiring", () => {
|
|
118
|
+
test("wires more retry headroom than the dispatcher-wide default", () => {
|
|
119
|
+
const consumer = createSearchEventConsumer(
|
|
120
|
+
{} as unknown as Parameters<typeof createSearchEventConsumer>[0],
|
|
121
|
+
{} as unknown as Parameters<typeof createSearchEventConsumer>[1],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(consumer.errorPolicy?.maxAttempts).toBeGreaterThan(10);
|
|
125
|
+
});
|
|
126
|
+
});
|