@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
package/README.md
CHANGED
|
@@ -28,9 +28,10 @@ Bun is the intended runtime and test runner.
|
|
|
28
28
|
## At-a-glance
|
|
29
29
|
|
|
30
30
|
```typescript
|
|
31
|
-
import {
|
|
31
|
+
import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
32
32
|
|
|
33
33
|
export const taskEntity = createEntity({
|
|
34
|
+
table: "read_tasks",
|
|
34
35
|
fields: {
|
|
35
36
|
title: createTextField({ required: true, searchable: true }),
|
|
36
37
|
done: createTextField(),
|
|
@@ -38,44 +39,14 @@ export const taskEntity = createEntity({
|
|
|
38
39
|
softDelete: true,
|
|
39
40
|
});
|
|
40
41
|
|
|
41
|
-
const taskTable = buildDrizzleTable("task", taskEntity);
|
|
42
|
-
const taskExecutor = createEventStoreExecutor(taskTable, taskEntity, { entityName: "task" });
|
|
43
|
-
|
|
44
42
|
export const taskFeature = defineFeature("tasks", (r) => {
|
|
45
|
-
r.
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
async (event, ctx) => taskExecutor.create(event.payload, event.user, ctx.db),
|
|
53
|
-
{ access: { roles: ["User"] } },
|
|
54
|
-
);
|
|
55
|
-
r.writeHandler(
|
|
56
|
-
"task:update",
|
|
57
|
-
z.object({ id: z.uuid(), version: z.number(), changes: z.object({ title: z.string().optional() }) }),
|
|
58
|
-
async (event, ctx) => taskExecutor.update(event.payload, event.user, ctx.db),
|
|
59
|
-
{ access: { roles: ["User"] } },
|
|
60
|
-
);
|
|
61
|
-
r.writeHandler(
|
|
62
|
-
"task:delete",
|
|
63
|
-
z.object({ id: z.uuid() }),
|
|
64
|
-
async (event, ctx) => taskExecutor.delete(event.payload, event.user, ctx.db),
|
|
65
|
-
{ access: { roles: ["Admin"] } },
|
|
66
|
-
);
|
|
67
|
-
r.queryHandler(
|
|
68
|
-
"task:list",
|
|
69
|
-
z.object({}),
|
|
70
|
-
async (query, ctx) => taskExecutor.list(query.payload, query.user, ctx.db),
|
|
71
|
-
{ access: { openToAll: true } },
|
|
72
|
-
);
|
|
73
|
-
r.queryHandler(
|
|
74
|
-
"task:detail",
|
|
75
|
-
z.object({ id: z.uuid() }),
|
|
76
|
-
async (query, ctx) => taskExecutor.detail(query.payload, query.user, ctx.db),
|
|
77
|
-
{ access: { openToAll: true } },
|
|
78
|
-
);
|
|
43
|
+
// r.crud wires create/update/delete/restore + list/detail queries in one
|
|
44
|
+
// call — events + projection in the same TX, optimistic locking, access
|
|
45
|
+
// control, all explicit via the options below.
|
|
46
|
+
r.crud("task", taskEntity, {
|
|
47
|
+
write: { access: { roles: ["User"] } },
|
|
48
|
+
read: { access: { openToAll: true } },
|
|
49
|
+
});
|
|
79
50
|
|
|
80
51
|
// Read-model fed from task events, rebuildable via the CLI
|
|
81
52
|
r.projection({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -99,6 +99,10 @@
|
|
|
99
99
|
"types": "./src/jobs/index.ts",
|
|
100
100
|
"default": "./src/jobs/index.ts"
|
|
101
101
|
},
|
|
102
|
+
"./observability": {
|
|
103
|
+
"types": "./src/observability/index.ts",
|
|
104
|
+
"default": "./src/observability/index.ts"
|
|
105
|
+
},
|
|
102
106
|
"./migrations": {
|
|
103
107
|
"types": "./src/migrations/index.ts",
|
|
104
108
|
"default": "./src/migrations/index.ts"
|
|
@@ -131,10 +135,22 @@
|
|
|
131
135
|
"types": "./src/secrets/index.ts",
|
|
132
136
|
"default": "./src/secrets/index.ts"
|
|
133
137
|
},
|
|
138
|
+
"./crypto": {
|
|
139
|
+
"types": "./src/crypto/index.ts",
|
|
140
|
+
"default": "./src/crypto/index.ts"
|
|
141
|
+
},
|
|
142
|
+
"./logging": {
|
|
143
|
+
"types": "./src/logging/index.ts",
|
|
144
|
+
"default": "./src/logging/index.ts"
|
|
145
|
+
},
|
|
134
146
|
"./schema-cli": {
|
|
135
147
|
"types": "./src/schema-cli.ts",
|
|
136
148
|
"default": "./src/schema-cli.ts"
|
|
137
149
|
},
|
|
150
|
+
"./consumer-cli": {
|
|
151
|
+
"types": "./src/consumer-cli.ts",
|
|
152
|
+
"default": "./src/consumer-cli.ts"
|
|
153
|
+
},
|
|
138
154
|
"./stack": {
|
|
139
155
|
"types": "./src/stack/index.ts",
|
|
140
156
|
"default": "./src/stack/index.ts"
|
|
@@ -180,8 +196,11 @@
|
|
|
180
196
|
"uuid": "^14.0.0",
|
|
181
197
|
"zod": "^4.4.3"
|
|
182
198
|
},
|
|
199
|
+
"peerDependencies": {
|
|
200
|
+
"@cosmicdrift/kumiko-types": "^2.0.0"
|
|
201
|
+
},
|
|
183
202
|
"devDependencies": {
|
|
184
|
-
"@cosmicdrift/kumiko-dispatcher-live": "
|
|
203
|
+
"@cosmicdrift/kumiko-dispatcher-live": "2.0.0",
|
|
185
204
|
"bun-types": "^1.3.13",
|
|
186
205
|
"pino-pretty": "^13.1.3"
|
|
187
206
|
},
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
+
import type { AnonymousAccessConfig } from "../api/auth-middleware";
|
|
11
12
|
import { createEventStoreExecutor } from "../db/event-store-executor";
|
|
12
13
|
import { asRawClient, selectMany } from "../db/query";
|
|
13
14
|
import { buildEntityTable } from "../db/table-builder";
|
|
@@ -307,6 +308,190 @@ describe("anonymous access — header-supplied tenant", () => {
|
|
|
307
308
|
});
|
|
308
309
|
});
|
|
309
310
|
|
|
311
|
+
describe("anonymous access — resolverTrust: authoritative", () => {
|
|
312
|
+
let stack: TestStack;
|
|
313
|
+
|
|
314
|
+
beforeAll(async () => {
|
|
315
|
+
// Simulates a subdomain resolver: TENANT_ID is "known" (the resolver
|
|
316
|
+
// recognises it), everything else is an unrecognised host (null).
|
|
317
|
+
stack = await setupTestStack({
|
|
318
|
+
features: [shopFeature],
|
|
319
|
+
anonymousAccess: {
|
|
320
|
+
tenantResolver: () => TENANT_ID,
|
|
321
|
+
resolverTrust: "authoritative",
|
|
322
|
+
tenantExists: async (id: TenantId) => id === TENANT_ID || id === OTHER_TENANT_ID,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
await unsafeCreateEntityTable(stack.db, productEntity);
|
|
326
|
+
await unsafeCreateEntityTable(stack.db, orderEntity);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
afterAll(() => stack.cleanup());
|
|
330
|
+
|
|
331
|
+
test("no client tenant → resolver wins", async () => {
|
|
332
|
+
const res = await stack.http.raw("POST", "/api/query", {
|
|
333
|
+
type: "anonshop:query:product:list",
|
|
334
|
+
payload: {},
|
|
335
|
+
});
|
|
336
|
+
expect(res.status).toBe(200);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("X-Tenant header agreeing with the resolver → accepted", async () => {
|
|
340
|
+
const res = await stack.http.raw(
|
|
341
|
+
"POST",
|
|
342
|
+
"/api/query",
|
|
343
|
+
{ type: "anonshop:query:product:list", payload: {} },
|
|
344
|
+
{ "X-Tenant": TENANT_ID },
|
|
345
|
+
);
|
|
346
|
+
expect(res.status).toBe(200);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("X-Tenant header disagreeing with the resolver → 400 tenant_mismatch, resolver's tenant is NOT overridden", async () => {
|
|
350
|
+
// The core #51 regression: a header claiming a different (real, active)
|
|
351
|
+
// tenant than the one the resolver derived must never win — that would
|
|
352
|
+
// let a guest on tenantA's subdomain write into tenantB by forging a
|
|
353
|
+
// header. Reject, don't silently prefer either side.
|
|
354
|
+
const res = await stack.http.raw(
|
|
355
|
+
"POST",
|
|
356
|
+
"/api/query",
|
|
357
|
+
{ type: "anonshop:query:product:list", payload: {} },
|
|
358
|
+
{ "X-Tenant": OTHER_TENANT_ID },
|
|
359
|
+
);
|
|
360
|
+
expect(res.status).toBe(400);
|
|
361
|
+
const body = (await res.json()) as { error: { code: string } };
|
|
362
|
+
expect(body.error.code).toBe("tenant_mismatch");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// pr-review kumiko-framework #1052/3 — resolveTenant treats header and
|
|
366
|
+
// cookie identically, but every case above only exercised the header.
|
|
367
|
+
// These two lock in that the cookie path behaves the same way.
|
|
368
|
+
test("kumiko_tenant cookie agreeing with the resolver → accepted", async () => {
|
|
369
|
+
const res = await stack.http.raw(
|
|
370
|
+
"POST",
|
|
371
|
+
"/api/query",
|
|
372
|
+
{ type: "anonshop:query:product:list", payload: {} },
|
|
373
|
+
{ Cookie: `kumiko_tenant=${TENANT_ID}` },
|
|
374
|
+
);
|
|
375
|
+
expect(res.status).toBe(200);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("kumiko_tenant cookie disagreeing with the resolver → 400 tenant_mismatch, same as a disagreeing header", async () => {
|
|
379
|
+
const res = await stack.http.raw(
|
|
380
|
+
"POST",
|
|
381
|
+
"/api/query",
|
|
382
|
+
{ type: "anonshop:query:product:list", payload: {} },
|
|
383
|
+
{ Cookie: `kumiko_tenant=${OTHER_TENANT_ID}` },
|
|
384
|
+
);
|
|
385
|
+
expect(res.status).toBe(400);
|
|
386
|
+
const body = (await res.json()) as { error: { code: string } };
|
|
387
|
+
expect(body.error.code).toBe("tenant_mismatch");
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe("anonymous access — resolverTrust: authoritative, resolver returns null", () => {
|
|
392
|
+
let stack: TestStack;
|
|
393
|
+
|
|
394
|
+
beforeAll(async () => {
|
|
395
|
+
// Resolver never recognises the host (e.g. an unmapped/unknown
|
|
396
|
+
// subdomain) — always returns null.
|
|
397
|
+
stack = await setupTestStack({
|
|
398
|
+
features: [shopFeature],
|
|
399
|
+
anonymousAccess: {
|
|
400
|
+
tenantResolver: () => null,
|
|
401
|
+
resolverTrust: "authoritative",
|
|
402
|
+
tenantExists: async (id: TenantId) => id === TENANT_ID || id === OTHER_TENANT_ID,
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
await unsafeCreateEntityTable(stack.db, productEntity);
|
|
406
|
+
await unsafeCreateEntityTable(stack.db, orderEntity);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
afterAll(() => stack.cleanup());
|
|
410
|
+
|
|
411
|
+
test("resolver null + X-Tenant header present → 400 tenant_required, header does NOT pick the tenant", async () => {
|
|
412
|
+
// If this fell back to the client header, an unrecognised-host request
|
|
413
|
+
// could still choose its own tenant via X-Tenant — the same override
|
|
414
|
+
// "authoritative" exists to prevent, just reached via "unknown host"
|
|
415
|
+
// instead of "known host, disagreeing header".
|
|
416
|
+
const res = await stack.http.raw(
|
|
417
|
+
"POST",
|
|
418
|
+
"/api/query",
|
|
419
|
+
{ type: "anonshop:query:product:list", payload: {} },
|
|
420
|
+
{ "X-Tenant": TENANT_ID },
|
|
421
|
+
);
|
|
422
|
+
expect(res.status).toBe(400);
|
|
423
|
+
const body = (await res.json()) as { error: { code: string } };
|
|
424
|
+
expect(body.error.code).toBe("tenant_required");
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("resolver null + no client tenant → 400 tenant_required", async () => {
|
|
428
|
+
const res = await stack.http.raw("POST", "/api/query", {
|
|
429
|
+
type: "anonshop:query:product:list",
|
|
430
|
+
payload: {},
|
|
431
|
+
});
|
|
432
|
+
expect(res.status).toBe(400);
|
|
433
|
+
const body = (await res.json()) as { error: { code: string } };
|
|
434
|
+
expect(body.error.code).toBe("tenant_required");
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
describe("anonymous access — resolverTrust: fallback-only", () => {
|
|
439
|
+
let stack: TestStack;
|
|
440
|
+
|
|
441
|
+
beforeAll(async () => {
|
|
442
|
+
stack = await setupTestStack({
|
|
443
|
+
features: [shopFeature],
|
|
444
|
+
anonymousAccess: {
|
|
445
|
+
tenantResolver: () => TENANT_ID,
|
|
446
|
+
resolverTrust: "fallback-only",
|
|
447
|
+
tenantExists: async (id: TenantId) => id === TENANT_ID || id === OTHER_TENANT_ID,
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
await unsafeCreateEntityTable(stack.db, productEntity);
|
|
451
|
+
await unsafeCreateEntityTable(stack.db, orderEntity);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
afterAll(() => stack.cleanup());
|
|
455
|
+
|
|
456
|
+
test("X-Tenant header disagreeing with the resolver → header wins (documented convenience-fallback behaviour)", async () => {
|
|
457
|
+
// Opposite of "authoritative": a client tenant that disagrees with the
|
|
458
|
+
// resolver is accepted, not rejected — this mode is for resolvers that
|
|
459
|
+
// carry no more trust than the client's own claim.
|
|
460
|
+
const res = await stack.http.raw(
|
|
461
|
+
"POST",
|
|
462
|
+
"/api/query",
|
|
463
|
+
{ type: "anonshop:query:product:list", payload: {} },
|
|
464
|
+
{ "X-Tenant": OTHER_TENANT_ID },
|
|
465
|
+
);
|
|
466
|
+
expect(res.status).toBe(200);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("no client tenant → resolver runs as fallback", async () => {
|
|
470
|
+
const res = await stack.http.raw("POST", "/api/query", {
|
|
471
|
+
type: "anonshop:query:product:list",
|
|
472
|
+
payload: {},
|
|
473
|
+
});
|
|
474
|
+
expect(res.status).toBe(200);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
describe("anonymous access — tenantResolver without resolverTrust", () => {
|
|
479
|
+
test("authMiddleware throws at boot, not silently at request time", async () => {
|
|
480
|
+
await expect(
|
|
481
|
+
setupTestStack({
|
|
482
|
+
features: [shopFeature],
|
|
483
|
+
// Simulates a caller that bypasses the compiler (JS consumer, an
|
|
484
|
+
// `as any` cast) — the discriminated union stops well-typed callers,
|
|
485
|
+
// this proves the runtime guard behind it also fires.
|
|
486
|
+
anonymousAccess: {
|
|
487
|
+
tenantResolver: () => TENANT_ID,
|
|
488
|
+
resolverTrust: undefined,
|
|
489
|
+
} as unknown as AnonymousAccessConfig,
|
|
490
|
+
}),
|
|
491
|
+
).rejects.toThrow(/resolverTrust/);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
310
495
|
describe("anonymous access — disabled by default", () => {
|
|
311
496
|
let stack: TestStack;
|
|
312
497
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Integration test for `runConsumerCli` (kumiko-framework#1351).
|
|
2
|
+
//
|
|
3
|
+
// Recovery semantics themselves are already covered by
|
|
4
|
+
// event-dispatcher-recovery — this only exercises the CLI layer: argv
|
|
5
|
+
// parsing, DB-connection handling, exit codes, output formatting.
|
|
6
|
+
|
|
7
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
8
|
+
import { type BunTestDb, createTestDb } from "../bun-db/__tests__/bun-test-db";
|
|
9
|
+
import { type ConsumerCliOut, runConsumerCli } from "../consumer-cli";
|
|
10
|
+
import { insertConsumerIfAbsent, markConsumerRebuildFailed } from "../db/queries/event-consumer";
|
|
11
|
+
import { asRawClient } from "../db/query";
|
|
12
|
+
import { createEventConsumerStateTable } from "../pipeline";
|
|
13
|
+
import { ensureTemporalPolyfill } from "../time/polyfill";
|
|
14
|
+
|
|
15
|
+
const SHARED = "__shared__";
|
|
16
|
+
|
|
17
|
+
let testDb: BunTestDb;
|
|
18
|
+
let testUrl: string;
|
|
19
|
+
let prevDbUrl: string | undefined;
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
await ensureTemporalPolyfill();
|
|
23
|
+
testDb = await createTestDb();
|
|
24
|
+
const baseUrl = process.env["TEST_DATABASE_URL"];
|
|
25
|
+
if (!baseUrl) throw new Error("TEST_DATABASE_URL not set — required for this test file");
|
|
26
|
+
testUrl = baseUrl.replace(/\/[^/]+$/, `/${testDb.dbName}`);
|
|
27
|
+
await createEventConsumerStateTable(testDb.db);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterAll(async () => {
|
|
31
|
+
await testDb.cleanup();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
await asRawClient(testDb.db).unsafe(`TRUNCATE TABLE "kumiko_event_consumers"`);
|
|
36
|
+
if (prevDbUrl === undefined) delete process.env["DATABASE_URL"];
|
|
37
|
+
else process.env["DATABASE_URL"] = prevDbUrl;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function captureOut(): { out: ConsumerCliOut; lines: string[] } {
|
|
41
|
+
const lines: string[] = [];
|
|
42
|
+
return {
|
|
43
|
+
out: { log: (l: string) => lines.push(l), err: (l: string) => lines.push(`ERR ${l}`) },
|
|
44
|
+
lines,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("runConsumerCli status", () => {
|
|
49
|
+
test("unknown consumer → exit 1, not-found message", async () => {
|
|
50
|
+
prevDbUrl = process.env["DATABASE_URL"];
|
|
51
|
+
process.env["DATABASE_URL"] = testUrl;
|
|
52
|
+
const { out, lines } = captureOut();
|
|
53
|
+
const code = await runConsumerCli(["status", "no-such-consumer"], out);
|
|
54
|
+
expect(code).toBe(1);
|
|
55
|
+
expect(lines.join("\n")).toContain("not found");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("known consumer → exit 0, reports status/cursor/attempts", async () => {
|
|
59
|
+
prevDbUrl = process.env["DATABASE_URL"];
|
|
60
|
+
process.env["DATABASE_URL"] = testUrl;
|
|
61
|
+
await insertConsumerIfAbsent(testDb.db, "test:consumer:foo", SHARED);
|
|
62
|
+
const { out, lines } = captureOut();
|
|
63
|
+
const code = await runConsumerCli(["status", "test:consumer:foo"], out);
|
|
64
|
+
expect(code).toBe(0);
|
|
65
|
+
const joined = lines.join("\n");
|
|
66
|
+
expect(joined).toContain("test:consumer:foo");
|
|
67
|
+
expect(joined).toContain("status: idle");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("missing <name> → exit 1, usage message", async () => {
|
|
71
|
+
prevDbUrl = process.env["DATABASE_URL"];
|
|
72
|
+
process.env["DATABASE_URL"] = testUrl;
|
|
73
|
+
const { out, lines } = captureOut();
|
|
74
|
+
const code = await runConsumerCli(["status"], out);
|
|
75
|
+
expect(code).toBe(1);
|
|
76
|
+
expect(lines.join("\n")).toContain("Usage:");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("missing DATABASE_URL → exit 1", async () => {
|
|
80
|
+
prevDbUrl = process.env["DATABASE_URL"];
|
|
81
|
+
delete process.env["DATABASE_URL"];
|
|
82
|
+
const { out, lines } = captureOut();
|
|
83
|
+
const code = await runConsumerCli(["status", "whatever"], out);
|
|
84
|
+
expect(code).toBe(1);
|
|
85
|
+
expect(lines.join("\n")).toContain("DATABASE_URL not set");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("--instance-id flag → status looks up the per-instance consumer, not shared", async () => {
|
|
89
|
+
prevDbUrl = process.env["DATABASE_URL"];
|
|
90
|
+
process.env["DATABASE_URL"] = testUrl;
|
|
91
|
+
await insertConsumerIfAbsent(testDb.db, "test:consumer:inst", "inst-1");
|
|
92
|
+
const { out, lines } = captureOut();
|
|
93
|
+
const code = await runConsumerCli(
|
|
94
|
+
["status", "test:consumer:inst", "--instance-id", "inst-1"],
|
|
95
|
+
out,
|
|
96
|
+
);
|
|
97
|
+
expect(code).toBe(0);
|
|
98
|
+
const joined = lines.join("\n");
|
|
99
|
+
expect(joined).toContain('instance_id="inst-1"');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("--instance-id=<id> form is recognised (#1412)", async () => {
|
|
103
|
+
prevDbUrl = process.env["DATABASE_URL"];
|
|
104
|
+
process.env["DATABASE_URL"] = testUrl;
|
|
105
|
+
await insertConsumerIfAbsent(testDb.db, "test:consumer:eq", "inst-2");
|
|
106
|
+
const { out, lines } = captureOut();
|
|
107
|
+
const code = await runConsumerCli(["status", "test:consumer:eq", "--instance-id=inst-2"], out);
|
|
108
|
+
expect(code).toBe(0);
|
|
109
|
+
expect(lines.join("\n")).toContain('instance_id="inst-2"');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("--instance-id before the name still parses the name correctly (#1412)", async () => {
|
|
113
|
+
prevDbUrl = process.env["DATABASE_URL"];
|
|
114
|
+
process.env["DATABASE_URL"] = testUrl;
|
|
115
|
+
await insertConsumerIfAbsent(testDb.db, "test:consumer:order", "inst-3");
|
|
116
|
+
const { out, lines } = captureOut();
|
|
117
|
+
const code = await runConsumerCli(
|
|
118
|
+
["status", "--instance-id", "inst-3", "test:consumer:order"],
|
|
119
|
+
out,
|
|
120
|
+
);
|
|
121
|
+
expect(code).toBe(0);
|
|
122
|
+
expect(lines.join("\n")).toContain('instance_id="inst-3"');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("--instance-id with a missing value errors instead of silently falling back to shared (#1412)", async () => {
|
|
126
|
+
prevDbUrl = process.env["DATABASE_URL"];
|
|
127
|
+
process.env["DATABASE_URL"] = testUrl;
|
|
128
|
+
const { out, lines } = captureOut();
|
|
129
|
+
const code = await runConsumerCli(["status", "test:consumer:foo", "--instance-id"], out);
|
|
130
|
+
expect(code).toBe(1);
|
|
131
|
+
expect(lines.join("\n")).toContain("--instance-id braucht einen Wert");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("runConsumerCli — unknown/empty subcommand exit codes", () => {
|
|
136
|
+
test("no subcommand at all → exit 0 (usage, not an error)", async () => {
|
|
137
|
+
const { out, lines } = captureOut();
|
|
138
|
+
const code = await runConsumerCli([], out);
|
|
139
|
+
expect(code).toBe(0);
|
|
140
|
+
expect(lines.join("\n")).toContain("Subcommands:");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("an unrecognized subcommand → exit 1", async () => {
|
|
144
|
+
const { out, lines } = captureOut();
|
|
145
|
+
const code = await runConsumerCli(["bogus"], out);
|
|
146
|
+
expect(code).toBe(1);
|
|
147
|
+
expect(lines.join("\n")).toContain("Subcommands:");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("runConsumerCli restart", () => {
|
|
152
|
+
test("dead consumer → idle, exit 0", async () => {
|
|
153
|
+
prevDbUrl = process.env["DATABASE_URL"];
|
|
154
|
+
process.env["DATABASE_URL"] = testUrl;
|
|
155
|
+
await insertConsumerIfAbsent(testDb.db, "test:consumer:bar", SHARED);
|
|
156
|
+
await markConsumerRebuildFailed(testDb.db, "test:consumer:bar", SHARED, "boom");
|
|
157
|
+
const { out, lines } = captureOut();
|
|
158
|
+
const code = await runConsumerCli(["restart", "test:consumer:bar"], out);
|
|
159
|
+
expect(code).toBe(0);
|
|
160
|
+
expect(lines.join("\n")).toContain("→ idle");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("non-dead consumer → propagated error, exit 1", async () => {
|
|
164
|
+
prevDbUrl = process.env["DATABASE_URL"];
|
|
165
|
+
process.env["DATABASE_URL"] = testUrl;
|
|
166
|
+
await insertConsumerIfAbsent(testDb.db, "test:consumer:baz", SHARED);
|
|
167
|
+
const { out, lines } = captureOut();
|
|
168
|
+
const code = await runConsumerCli(["restart", "test:consumer:baz"], out);
|
|
169
|
+
expect(code).toBe(1);
|
|
170
|
+
expect(lines.join("\n")).toContain("not dead");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Issue #912 — Teilbare Entity-Permalinks. Der Copy-Link-Button selbst ist
|
|
2
|
+
// UI-Zucker (baut eine URL, kopiert sie); die eigentliche Anforderung des
|
|
3
|
+
// Issues ist "ein Permalink darf nie Auth/Tenant-Isolation umgehen". Ein
|
|
4
|
+
// entityEdit-Permalink öffnet beim Klick exactly denselben
|
|
5
|
+
// `<feature>:query:<entity>:detail`-Call, den jede Entity schon für ihr
|
|
6
|
+
// Update-Formular nutzt — es gibt keinen separaten Permalink-Resolve-Pfad.
|
|
7
|
+
// Dieser Test beweist den Deny für beide Achsen, die einen Leak verhindern
|
|
8
|
+
// müssten: Rollen-Gate (dispatcher-seitig, default-deny) und Tenant-Scoping
|
|
9
|
+
// (strukturell über ctx.db).
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test } from "bun:test";
|
|
12
|
+
import { setupTestStackFromFeatures } from "@cosmicdrift/kumiko-dev-server/setup-test-stack-from-features";
|
|
13
|
+
import {
|
|
14
|
+
createEntity,
|
|
15
|
+
createTextField,
|
|
16
|
+
defineEntityCreateHandler,
|
|
17
|
+
defineEntityDetailHandler,
|
|
18
|
+
defineFeature,
|
|
19
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
20
|
+
import {
|
|
21
|
+
createTestUser,
|
|
22
|
+
pushEntityProjectionTables,
|
|
23
|
+
testTenantId,
|
|
24
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
25
|
+
|
|
26
|
+
const noteEntity = createEntity({
|
|
27
|
+
table: "read_permalink_notes",
|
|
28
|
+
fields: {
|
|
29
|
+
title: createTextField({ required: true, maxLength: 160, allowPlaintext: "is-business-data" }),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const permalinkFeature = defineFeature("permalink-fixture", (r) => {
|
|
34
|
+
r.entity("note", noteEntity);
|
|
35
|
+
const access = { roles: ["TenantAdmin"] } as const;
|
|
36
|
+
r.writeHandler(defineEntityCreateHandler("note", noteEntity, { access }));
|
|
37
|
+
r.queryHandler(defineEntityDetailHandler("note", noteEntity, { access }));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("entityEdit permalink open-path denies cleanly", () => {
|
|
41
|
+
test("unauthorized role → AccessDeniedError, not the record", async () => {
|
|
42
|
+
const stack = await setupTestStackFromFeatures([permalinkFeature]);
|
|
43
|
+
try {
|
|
44
|
+
const tenantId = testTenantId(701);
|
|
45
|
+
const owner = createTestUser({ id: 701, tenantId, roles: ["TenantAdmin"] });
|
|
46
|
+
const outsider = createTestUser({ id: 702, tenantId, roles: ["TeamMember"] });
|
|
47
|
+
await pushEntityProjectionTables(stack, stack.registry);
|
|
48
|
+
|
|
49
|
+
const created = await stack.http.writeOk<{ id: string }>(
|
|
50
|
+
"permalink-fixture:write:note:create",
|
|
51
|
+
{ title: "Q3 Forecast" },
|
|
52
|
+
owner,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const res = await stack.http.query(
|
|
56
|
+
"permalink-fixture:query:note:detail",
|
|
57
|
+
{ id: created.id },
|
|
58
|
+
outsider,
|
|
59
|
+
);
|
|
60
|
+
expect(res.ok).toBe(false);
|
|
61
|
+
const body = (await res.json()) as { isSuccess?: boolean; error?: { code?: string } };
|
|
62
|
+
expect(body.isSuccess).not.toBe(true);
|
|
63
|
+
expect(body.error?.code).toBe("access_denied");
|
|
64
|
+
} finally {
|
|
65
|
+
await stack.cleanup();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("cross-tenant open → not-found, not the other tenant's record", async () => {
|
|
70
|
+
const stack = await setupTestStackFromFeatures([permalinkFeature]);
|
|
71
|
+
try {
|
|
72
|
+
const tenantA = testTenantId(703);
|
|
73
|
+
const tenantB = testTenantId(704);
|
|
74
|
+
const ownerA = createTestUser({ id: 703, tenantId: tenantA, roles: ["TenantAdmin"] });
|
|
75
|
+
const adminB = createTestUser({ id: 704, tenantId: tenantB, roles: ["TenantAdmin"] });
|
|
76
|
+
await pushEntityProjectionTables(stack, stack.registry);
|
|
77
|
+
|
|
78
|
+
const created = await stack.http.writeOk<{ id: string }>(
|
|
79
|
+
"permalink-fixture:write:note:create",
|
|
80
|
+
{ title: "Tenant A Secret" },
|
|
81
|
+
ownerA,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const row = await stack.http.queryOk<Record<string, unknown> | null>(
|
|
85
|
+
"permalink-fixture:query:note:detail",
|
|
86
|
+
{ id: created.id },
|
|
87
|
+
adminB,
|
|
88
|
+
);
|
|
89
|
+
expect(row).toBeNull();
|
|
90
|
+
} finally {
|
|
91
|
+
await stack.cleanup();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -182,7 +182,7 @@ const userFeature = defineFeature("users", (r) => {
|
|
|
182
182
|
{ access: { openToAll: true } },
|
|
183
183
|
);
|
|
184
184
|
|
|
185
|
-
r.
|
|
185
|
+
r.hook("postSave", { allOf: user }, async (result) => {
|
|
186
186
|
featurePostSaveLog.push(result);
|
|
187
187
|
});
|
|
188
188
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { afterEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { runSchemaCli, type SchemaCliOut } from "../schema-cli";
|
|
6
|
+
import * as timeModule from "../time";
|
|
7
|
+
|
|
8
|
+
function captureOut(): { out: SchemaCliOut; log: string[]; err: string[] } {
|
|
9
|
+
const log: string[] = [];
|
|
10
|
+
const err: string[] = [];
|
|
11
|
+
return { out: { log: (l) => log.push(l), err: (l) => err.push(l) }, log, err };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("runSchemaCli — Temporal polyfill", () => {
|
|
15
|
+
let appCwd: string;
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
if (appCwd) rmSync(appCwd, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("calls ensureTemporalPolyfill on every invocation", async () => {
|
|
22
|
+
// runProdApp/runDevApp call ensureTemporalPolyfill() at boot; the
|
|
23
|
+
// standalone CLI (migrate-db initContainer, `bun kumiko.js schema apply`)
|
|
24
|
+
// never goes through that boot path. Without this, a projection rebuild's
|
|
25
|
+
// tz/timestamp coercion throws "Temporal is not defined" on any runtime
|
|
26
|
+
// that lacks native Temporal — deterministically, since the crashed
|
|
27
|
+
// process still records the migration as applied and the rebuild is
|
|
28
|
+
// never retried on the next run.
|
|
29
|
+
//
|
|
30
|
+
// Asserting on globalThis.Temporal directly doesn't work here: the test
|
|
31
|
+
// harness's own preload (test-setup/base.preload.ts) already calls
|
|
32
|
+
// ensureTemporalPolyfill() once per process, and its idempotency cache
|
|
33
|
+
// (a module-level flag, not re-derived from globalThis) short-circuits
|
|
34
|
+
// any later call regardless of what a test does to globalThis.Temporal in
|
|
35
|
+
// between. Spying on the imported binding sidesteps that cache entirely.
|
|
36
|
+
appCwd = mkdtempSync(join(tmpdir(), "kumiko-schema-cli-temporal-"));
|
|
37
|
+
mkdirSync(join(appCwd, "kumiko"), { recursive: true });
|
|
38
|
+
const spy = spyOn(timeModule, "ensureTemporalPolyfill");
|
|
39
|
+
try {
|
|
40
|
+
const cap = captureOut();
|
|
41
|
+
const code = await runSchemaCli([], appCwd, cap.out);
|
|
42
|
+
expect(code).toBe(0);
|
|
43
|
+
expect(spy).toHaveBeenCalled();
|
|
44
|
+
} finally {
|
|
45
|
+
spy.mockRestore();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|