@cosmicdrift/kumiko-framework 0.1.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 +159 -0
- package/package.json +91 -0
- package/src/__tests__/anonymous-access.integration.ts +325 -0
- package/src/__tests__/error-contract.integration.ts +435 -0
- package/src/__tests__/field-access.integration.ts +269 -0
- package/src/__tests__/full-stack.integration.ts +914 -0
- package/src/__tests__/ownership.integration.ts +449 -0
- package/src/__tests__/reference-data.integration.ts +198 -0
- package/src/__tests__/transition-guard.integration.ts +340 -0
- package/src/api/__tests__/api.test.ts +337 -0
- package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
- package/src/api/__tests__/batch.integration.ts +404 -0
- package/src/api/__tests__/body-limit.test.ts +88 -0
- package/src/api/__tests__/csrf-middleware.test.ts +97 -0
- package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
- package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
- package/src/api/__tests__/nested-write.integration.ts +213 -0
- package/src/api/__tests__/readiness.test.ts +76 -0
- package/src/api/__tests__/request-id-middleware.test.ts +72 -0
- package/src/api/__tests__/sse-broker.test.ts +58 -0
- package/src/api/__tests__/sse-route.test.ts +112 -0
- package/src/api/anonymous-cookie.ts +60 -0
- package/src/api/api-constants.ts +64 -0
- package/src/api/auth-middleware.ts +418 -0
- package/src/api/auth-routes.ts +982 -0
- package/src/api/csrf-middleware.ts +77 -0
- package/src/api/index.ts +31 -0
- package/src/api/jwt.ts +66 -0
- package/src/api/observability-middleware.ts +89 -0
- package/src/api/readiness.ts +132 -0
- package/src/api/request-context.ts +49 -0
- package/src/api/request-id-middleware.ts +50 -0
- package/src/api/route-registrars.ts +195 -0
- package/src/api/routes.ts +135 -0
- package/src/api/server.ts +640 -0
- package/src/api/sse-broker.ts +71 -0
- package/src/api/sse-route.ts +62 -0
- package/src/api/tokens.ts +16 -0
- package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
- package/src/db/__tests__/compound-types.test.ts +114 -0
- package/src/db/__tests__/connection-options.test.ts +68 -0
- package/src/db/__tests__/cursor.test.ts +41 -0
- package/src/db/__tests__/db-helpers.test.ts +369 -0
- package/src/db/__tests__/dialect-instant.test.ts +50 -0
- package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
- package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
- package/src/db/__tests__/encryption.test.ts +39 -0
- package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
- package/src/db/__tests__/event-store-executor.integration.ts +235 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
- package/src/db/__tests__/located-timestamp.test.ts +184 -0
- package/src/db/__tests__/money.test.ts +199 -0
- package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
- package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
- package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
- package/src/db/__tests__/row-helpers.test.ts +59 -0
- package/src/db/__tests__/schema-migration.integration.ts +273 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
- package/src/db/__tests__/table-builder-required.test.ts +216 -0
- package/src/db/__tests__/tenant-db.integration.ts +606 -0
- package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
- package/src/db/apply-entity-event.ts +188 -0
- package/src/db/assert-exists-in.ts +59 -0
- package/src/db/compound-types.ts +47 -0
- package/src/db/connection.ts +104 -0
- package/src/db/cursor.ts +83 -0
- package/src/db/dialect.ts +109 -0
- package/src/db/eagerload.ts +174 -0
- package/src/db/encryption.ts +39 -0
- package/src/db/event-store-executor.ts +906 -0
- package/src/db/index.ts +55 -0
- package/src/db/located-timestamp.ts +114 -0
- package/src/db/money.ts +120 -0
- package/src/db/pg-error.ts +46 -0
- package/src/db/reference-data.ts +77 -0
- package/src/db/row-helpers.ts +53 -0
- package/src/db/schema-inspection.ts +25 -0
- package/src/db/table-builder.ts +475 -0
- package/src/db/tenant-db.ts +434 -0
- package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
- package/src/engine/__tests__/boot-validator.test.ts +1865 -0
- package/src/engine/__tests__/build-app-schema.test.ts +154 -0
- package/src/engine/__tests__/claim-keys.test.ts +274 -0
- package/src/engine/__tests__/config-helpers.test.ts +236 -0
- package/src/engine/__tests__/effective-features.test.ts +86 -0
- package/src/engine/__tests__/engine.test.ts +1461 -0
- package/src/engine/__tests__/entity-handlers.test.ts +274 -0
- package/src/engine/__tests__/event-helpers.test.ts +68 -0
- package/src/engine/__tests__/extends-registrar.test.ts +159 -0
- package/src/engine/__tests__/factories-long-text.test.ts +84 -0
- package/src/engine/__tests__/factories-time.test.ts +158 -0
- package/src/engine/__tests__/field-predicates.test.ts +48 -0
- package/src/engine/__tests__/hook-phases.test.ts +132 -0
- package/src/engine/__tests__/identifiers.test.ts +35 -0
- package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
- package/src/engine/__tests__/nav.test.ts +267 -0
- package/src/engine/__tests__/ownership.test.ts +421 -0
- package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
- package/src/engine/__tests__/projection-helpers.test.ts +62 -0
- package/src/engine/__tests__/projection.test.ts +191 -0
- package/src/engine/__tests__/qualified-name.test.ts +264 -0
- package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
- package/src/engine/__tests__/run-in.test.ts +38 -0
- package/src/engine/__tests__/schema-builder.test.ts +380 -0
- package/src/engine/__tests__/screen.test.ts +408 -0
- package/src/engine/__tests__/state-machine.test.ts +148 -0
- package/src/engine/__tests__/system-user.test.ts +57 -0
- package/src/engine/__tests__/validation-hooks.test.ts +71 -0
- package/src/engine/access.ts +23 -0
- package/src/engine/boot-validator.ts +1528 -0
- package/src/engine/build-app-schema.ts +125 -0
- package/src/engine/config-helpers.ts +115 -0
- package/src/engine/constants.ts +85 -0
- package/src/engine/create-app.ts +98 -0
- package/src/engine/define-feature.ts +702 -0
- package/src/engine/define-handler.ts +78 -0
- package/src/engine/define-roles.ts +19 -0
- package/src/engine/effective-features.ts +87 -0
- package/src/engine/entity-handlers.ts +364 -0
- package/src/engine/event-helpers.ts +73 -0
- package/src/engine/factories.ts +328 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
- package/src/engine/feature-ast/extractors.ts +2562 -0
- package/src/engine/feature-ast/index.ts +105 -0
- package/src/engine/feature-ast/parse.ts +369 -0
- package/src/engine/feature-ast/patch.ts +525 -0
- package/src/engine/feature-ast/patcher.ts +518 -0
- package/src/engine/feature-ast/patterns.ts +434 -0
- package/src/engine/feature-ast/render.ts +602 -0
- package/src/engine/feature-ast/source-location.ts +45 -0
- package/src/engine/field-access.ts +120 -0
- package/src/engine/index.ts +254 -0
- package/src/engine/ownership.ts +337 -0
- package/src/engine/parse-ref-target.ts +22 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
- package/src/engine/pattern-library/index.ts +24 -0
- package/src/engine/pattern-library/library.ts +1117 -0
- package/src/engine/pattern-library/types.ts +255 -0
- package/src/engine/projection-helpers.ts +85 -0
- package/src/engine/qualified-name.ts +122 -0
- package/src/engine/read-claim.ts +31 -0
- package/src/engine/registry.ts +1325 -0
- package/src/engine/resolve-config-or-param.ts +153 -0
- package/src/engine/run-in.ts +29 -0
- package/src/engine/schema-builder.ts +175 -0
- package/src/engine/screen-filter-ops.ts +51 -0
- package/src/engine/state-machine.ts +70 -0
- package/src/engine/system-user.ts +32 -0
- package/src/engine/types/config.ts +306 -0
- package/src/engine/types/event-type-map.ts +37 -0
- package/src/engine/types/feature.ts +574 -0
- package/src/engine/types/fields.ts +422 -0
- package/src/engine/types/handlers.ts +742 -0
- package/src/engine/types/hooks.ts +142 -0
- package/src/engine/types/http-route.ts +54 -0
- package/src/engine/types/identifiers.ts +47 -0
- package/src/engine/types/index.ts +208 -0
- package/src/engine/types/nav.ts +46 -0
- package/src/engine/types/projection.ts +132 -0
- package/src/engine/types/relations.ts +51 -0
- package/src/engine/types/screen.ts +452 -0
- package/src/engine/types/workspace.ts +42 -0
- package/src/engine/validation.ts +33 -0
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
- package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
- package/src/entrypoint/index.ts +442 -0
- package/src/errors/__tests__/classes.test.ts +371 -0
- package/src/errors/__tests__/write-failures.test.ts +109 -0
- package/src/errors/classes.ts +249 -0
- package/src/errors/i18n/de.yaml +83 -0
- package/src/errors/i18n/en.yaml +80 -0
- package/src/errors/index.ts +41 -0
- package/src/errors/kumiko-error.ts +67 -0
- package/src/errors/reasons.ts +36 -0
- package/src/errors/serialize.ts +136 -0
- package/src/errors/transition-details.ts +30 -0
- package/src/errors/write-error-info.ts +123 -0
- package/src/errors/zod-bridge.ts +49 -0
- package/src/event-store/__tests__/admin-api.integration.ts +361 -0
- package/src/event-store/__tests__/event-store.integration.ts +584 -0
- package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
- package/src/event-store/__tests__/perf.integration.ts +255 -0
- package/src/event-store/__tests__/snapshot.integration.ts +267 -0
- package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
- package/src/event-store/__tests__/upcaster.integration.ts +460 -0
- package/src/event-store/admin-api.ts +257 -0
- package/src/event-store/archive.ts +106 -0
- package/src/event-store/errors.ts +35 -0
- package/src/event-store/event-store.ts +405 -0
- package/src/event-store/events-schema.ts +90 -0
- package/src/event-store/index.ts +50 -0
- package/src/event-store/snapshot.ts +210 -0
- package/src/event-store/upcaster-dead-letter.ts +119 -0
- package/src/event-store/upcaster.ts +147 -0
- package/src/files/__tests__/content-disposition.test.ts +123 -0
- package/src/files/__tests__/file-field-column.integration.ts +103 -0
- package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
- package/src/files/__tests__/file-handle.test.ts +122 -0
- package/src/files/__tests__/files.integration.ts +830 -0
- package/src/files/__tests__/storage-tracking.integration.ts +153 -0
- package/src/files/content-disposition.ts +55 -0
- package/src/files/file-handle.ts +63 -0
- package/src/files/file-ref-table.ts +22 -0
- package/src/files/file-routes.ts +353 -0
- package/src/files/in-memory-provider.ts +62 -0
- package/src/files/index.ts +29 -0
- package/src/files/local-provider.ts +35 -0
- package/src/files/storage-tracking.ts +60 -0
- package/src/files/types.ts +118 -0
- package/src/i18n/__tests__/i18n.test.ts +72 -0
- package/src/i18n/index.ts +29 -0
- package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
- package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
- package/src/jobs/__tests__/jobs.integration.ts +566 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-runner.ts +574 -0
- package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
- package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
- package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
- package/src/lifecycle/index.ts +13 -0
- package/src/lifecycle/lifecycle.ts +160 -0
- package/src/lifecycle/signal-handlers.ts +62 -0
- package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
- package/src/logging/index.ts +3 -0
- package/src/logging/pino-logger.ts +64 -0
- package/src/logging/types.ts +7 -0
- package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
- package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
- package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
- package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
- package/src/migrations/index.ts +28 -0
- package/src/migrations/projection-detection.ts +149 -0
- package/src/migrations/rebuild-marker.ts +64 -0
- package/src/migrations/schema-drift.ts +395 -0
- package/src/observability/__tests__/console-provider.test.ts +67 -0
- package/src/observability/__tests__/metric-validator.test.ts +87 -0
- package/src/observability/__tests__/noop-provider.test.ts +82 -0
- package/src/observability/__tests__/observability.integration.ts +559 -0
- package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
- package/src/observability/__tests__/recording-meter.test.ts +101 -0
- package/src/observability/__tests__/recording-tracer.test.ts +110 -0
- package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
- package/src/observability/console-provider.ts +130 -0
- package/src/observability/context.ts +26 -0
- package/src/observability/fallback.ts +34 -0
- package/src/observability/ids.ts +25 -0
- package/src/observability/index.ts +79 -0
- package/src/observability/metric-validator.ts +86 -0
- package/src/observability/metrics-handle.ts +56 -0
- package/src/observability/noop-provider.ts +146 -0
- package/src/observability/prometheus-meter.ts +284 -0
- package/src/observability/recording-meter.ts +156 -0
- package/src/observability/recording-tracer.ts +198 -0
- package/src/observability/redis-wrapper.ts +132 -0
- package/src/observability/sensitive-filter.ts +108 -0
- package/src/observability/standard-metrics.ts +213 -0
- package/src/observability/types/index.ts +29 -0
- package/src/observability/types/metric.ts +56 -0
- package/src/observability/types/provider.ts +32 -0
- package/src/observability/types/span.ts +64 -0
- package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
- package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
- package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
- package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
- package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
- package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
- package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
- package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
- package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
- package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
- package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
- package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
- package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
- package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
- package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
- package/src/pipeline/append-event-core.ts +117 -0
- package/src/pipeline/auth-claims-resolver.ts +103 -0
- package/src/pipeline/cascade-handler.ts +113 -0
- package/src/pipeline/dispatcher.ts +1585 -0
- package/src/pipeline/distributed-lock.ts +37 -0
- package/src/pipeline/entity-cache.ts +113 -0
- package/src/pipeline/event-consumer-state.ts +108 -0
- package/src/pipeline/event-dedup.ts +23 -0
- package/src/pipeline/event-dispatcher.ts +1016 -0
- package/src/pipeline/event-retention.ts +154 -0
- package/src/pipeline/idempotency.ts +76 -0
- package/src/pipeline/index.ts +66 -0
- package/src/pipeline/lifecycle-pipeline.ts +409 -0
- package/src/pipeline/msp-rebuild.ts +242 -0
- package/src/pipeline/multi-stream-apply-context.ts +115 -0
- package/src/pipeline/projection-rebuild.ts +334 -0
- package/src/pipeline/projection-state.ts +72 -0
- package/src/pipeline/projections-runner.ts +56 -0
- package/src/pipeline/redis-keys.ts +11 -0
- package/src/pipeline/system-hooks.ts +190 -0
- package/src/random/__tests__/generate.test.ts +149 -0
- package/src/random/generate.ts +141 -0
- package/src/random/index.ts +8 -0
- package/src/random/words.ts +392 -0
- package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
- package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
- package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
- package/src/rate-limit/bucket.ts +36 -0
- package/src/rate-limit/index.ts +14 -0
- package/src/rate-limit/middleware.ts +152 -0
- package/src/rate-limit/resolver.ts +267 -0
- package/src/redis/__tests__/redis-options.test.ts +54 -0
- package/src/redis/index.ts +74 -0
- package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
- package/src/search/__tests__/search-adapter.test.ts +256 -0
- package/src/search/in-memory-adapter.ts +123 -0
- package/src/search/index.ts +12 -0
- package/src/search/meilisearch-adapter.ts +106 -0
- package/src/search/types.ts +39 -0
- package/src/secrets/__tests__/dek-cache.test.ts +213 -0
- package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
- package/src/secrets/__tests__/envelope.test.ts +74 -0
- package/src/secrets/__tests__/leak-guard.test.ts +92 -0
- package/src/secrets/__tests__/rotation.test.ts +149 -0
- package/src/secrets/dek-cache.ts +116 -0
- package/src/secrets/env-master-key-provider.ts +162 -0
- package/src/secrets/envelope.ts +55 -0
- package/src/secrets/index.ts +19 -0
- package/src/secrets/leak-guard.ts +87 -0
- package/src/secrets/rotation.ts +34 -0
- package/src/secrets/types.ts +107 -0
- package/src/stack/db.ts +104 -0
- package/src/stack/event-collector.ts +23 -0
- package/src/stack/index.ts +32 -0
- package/src/stack/redis.ts +44 -0
- package/src/stack/request-helper.ts +168 -0
- package/src/stack/table-helpers.ts +104 -0
- package/src/stack/test-stack.ts +357 -0
- package/src/stack/test-users.ts +37 -0
- package/src/testing/__tests__/e2e-generator.test.ts +230 -0
- package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
- package/src/testing/access-assertions.ts +15 -0
- package/src/testing/assertions.ts +35 -0
- package/src/testing/e2e-generator.ts +465 -0
- package/src/testing/expect-error.ts +25 -0
- package/src/testing/handler-context.ts +125 -0
- package/src/testing/http-cookies.ts +52 -0
- package/src/testing/index.ts +41 -0
- package/src/testing/late-bound.ts +39 -0
- package/src/testing/mutable-master-key-provider.ts +31 -0
- package/src/testing/observability-recorder.ts +54 -0
- package/src/testing/shared-entities.ts +49 -0
- package/src/testing/utils.ts +1 -0
- package/src/testing/wait-for.ts +31 -0
- package/src/time/__tests__/polyfill.test.ts +73 -0
- package/src/time/__tests__/tz-context.test.ts +121 -0
- package/src/time/index.ts +21 -0
- package/src/time/polyfill.ts +70 -0
- package/src/time/tz-context.ts +107 -0
- package/src/ui-types/app-schema.ts +57 -0
- package/src/ui-types/index.ts +65 -0
- package/src/utils/__tests__/assert.test.ts +17 -0
- package/src/utils/__tests__/env-parse.test.ts +54 -0
- package/src/utils/assert.ts +18 -0
- package/src/utils/env-parse.ts +16 -0
- package/src/utils/ids.ts +16 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/safe-json.ts +30 -0
- package/src/utils/serialization.ts +7 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// Ownership rules — the declarative bridge between Claims and Access.
|
|
2
|
+
//
|
|
3
|
+
// Every ownership rule answers the same question: "May this user see / write
|
|
4
|
+
// this row (or field in this row)?". The rule is evaluated per-role: a user
|
|
5
|
+
// with multiple roles passes if at least one of their roles has a rule that
|
|
6
|
+
// accepts the row. For writes, the check is stricter — see below.
|
|
7
|
+
//
|
|
8
|
+
// Rule forms:
|
|
9
|
+
//
|
|
10
|
+
// "all" → any user with this role passes
|
|
11
|
+
// { from: "user:id", column: "..." } → row[column] === user.id
|
|
12
|
+
// { from: "claim:<featureQn>",
|
|
13
|
+
// column?: "..." } → row[column ?? claim.shortName] === user.claims[claim.qn]
|
|
14
|
+
// (string[] claim → inArray)
|
|
15
|
+
// { where: (user, table) => SQL } → escape hatch, arbitrary Drizzle predicate
|
|
16
|
+
//
|
|
17
|
+
// Construction: use the `from(ref, column?)` helper. It returns a FromRule
|
|
18
|
+
// ready to drop into an access map.
|
|
19
|
+
|
|
20
|
+
import { eq, inArray, or, type SQL, sql } from "drizzle-orm";
|
|
21
|
+
import type { SessionUser } from "./types";
|
|
22
|
+
|
|
23
|
+
// Reference spec supported by `from()`:
|
|
24
|
+
// "user:id" → user.id
|
|
25
|
+
// "user:tenantId" → user.tenantId (rarely needed — TenantDb scopes anyway)
|
|
26
|
+
// "claim:<featureName>:<key>" → user.claims["<featureName>:<key>"]
|
|
27
|
+
//
|
|
28
|
+
// The string form is keyed so the framework can look up the referenced
|
|
29
|
+
// Registry entry at boot (Claim-QN exists? Column type compatible?). A typed
|
|
30
|
+
// object form would force features to import each other's handles — the
|
|
31
|
+
// whole point of H.2's unified path is string-based references, no imports.
|
|
32
|
+
export type OwnershipRef = string;
|
|
33
|
+
|
|
34
|
+
// Resolved during `from()` — the parser eagerly splits the prefix so the
|
|
35
|
+
// runtime evaluator avoids string-parsing on every row. `kind` drives the
|
|
36
|
+
// evaluator branch; the rest is the resolved metadata.
|
|
37
|
+
export type FromRuleKind = "user" | "claim";
|
|
38
|
+
|
|
39
|
+
export type FromRule = {
|
|
40
|
+
readonly kind: "from";
|
|
41
|
+
readonly refKind: FromRuleKind;
|
|
42
|
+
// For "user:id" → "id"; for "user:tenantId" → "tenantId".
|
|
43
|
+
// For "claim:<featureName>:<key>" → "<featureName>:<key>" (the full QN,
|
|
44
|
+
// which is exactly the key under which the JWT stores the value).
|
|
45
|
+
readonly refPath: string;
|
|
46
|
+
// Row-column to match against. For claim rules defaults to the claim's
|
|
47
|
+
// shortName (second segment of the claim QN). For user-rules the column
|
|
48
|
+
// is always explicit.
|
|
49
|
+
readonly column: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type WhereRule<TTable = unknown> = {
|
|
53
|
+
readonly kind: "where";
|
|
54
|
+
readonly where: (user: SessionUser, table: TTable) => SQL;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// "all" collapses to a primitive so map authors can write `Admin: "all"`
|
|
58
|
+
// without importing a helper.
|
|
59
|
+
export type OwnershipRule = "all" | FromRule | WhereRule;
|
|
60
|
+
|
|
61
|
+
// Per-role map: every key is a role name, value is the rule that role
|
|
62
|
+
// satisfies to pass the access check.
|
|
63
|
+
export type OwnershipMap = Readonly<Record<string, OwnershipRule>>;
|
|
64
|
+
|
|
65
|
+
// Parse an OwnershipRef into kind + resolved path + default column.
|
|
66
|
+
// Throws on malformed input so the error surfaces at `from()`-call-site (in
|
|
67
|
+
// the feature definition), not at request time.
|
|
68
|
+
export function from(ref: OwnershipRef, column?: string): FromRule {
|
|
69
|
+
const firstColon = ref.indexOf(":");
|
|
70
|
+
if (firstColon < 0) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`from("${ref}"): expected "user:<field>" or "claim:<featureName>:<key>" — no colon found.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const prefix = ref.slice(0, firstColon);
|
|
76
|
+
const rest = ref.slice(firstColon + 1);
|
|
77
|
+
|
|
78
|
+
if (prefix === "user") {
|
|
79
|
+
// "user:id" or "user:tenantId". The rest is the user-property; the
|
|
80
|
+
// column on the row must be given explicitly (a user.id is rarely
|
|
81
|
+
// named `id` on a child table — usually `ownerId`, `assigneeId`, ...).
|
|
82
|
+
if (!column) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`from("${ref}"): user-refs require an explicit column name — e.g. from("user:id", "assigneeId").`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (rest !== "id" && rest !== "tenantId") {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`from("${ref}"): user-ref supports only "user:id" or "user:tenantId" (got "user:${rest}").`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return { kind: "from", refKind: "user", refPath: rest, column };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (prefix === "claim") {
|
|
96
|
+
// "claim:<feature>:<key>" — rest is the 2-segment claim QN.
|
|
97
|
+
if (!rest.includes(":")) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`from("${ref}"): claim-ref must be "claim:<featureName>:<shortName>" (got "claim:${rest}").`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
// Default column = claim shortName (second segment).
|
|
103
|
+
const defaultColumn = rest.slice(rest.indexOf(":") + 1);
|
|
104
|
+
return {
|
|
105
|
+
kind: "from",
|
|
106
|
+
refKind: "claim",
|
|
107
|
+
refPath: rest, // full QN, matches the key in user.claims
|
|
108
|
+
column: column ?? defaultColumn,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new Error(
|
|
113
|
+
`from("${ref}"): unsupported ref prefix "${prefix}". Supported: "user", "claim".`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Evaluate an ownership rule against a concrete row (plain data, no Drizzle).
|
|
118
|
+
// Used by field-level filters on query responses and by field-level
|
|
119
|
+
// write-checks. Entity-level rules that want SQL predicates go through
|
|
120
|
+
// buildOwnershipClause() (separate path, since that produces Drizzle SQL).
|
|
121
|
+
//
|
|
122
|
+
// Null/undefined claim values evaluate to `false` (no match) — safer than
|
|
123
|
+
// letting them match rows where the column happens to be null.
|
|
124
|
+
export function matchesRule(
|
|
125
|
+
rule: OwnershipRule,
|
|
126
|
+
user: SessionUser,
|
|
127
|
+
row: Readonly<Record<string, unknown>>,
|
|
128
|
+
): boolean {
|
|
129
|
+
if (rule === "all") return true;
|
|
130
|
+
if (rule.kind === "where") {
|
|
131
|
+
// `where` rules produce Drizzle SQL for the DB-side filter. They don't
|
|
132
|
+
// have a straightforward in-memory evaluator — the feature author owns
|
|
133
|
+
// the semantics. Field-level filters can't use `{ where }` rules; the
|
|
134
|
+
// boot-validator rejects them with a clear error at registration time.
|
|
135
|
+
throw new Error(
|
|
136
|
+
"where-rules can only be evaluated at the SQL layer; boot-validator should reject them on field-level access.",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// FromRule — resolve the user-side value, compare to the row's column.
|
|
141
|
+
const userValue = resolveUserValue(rule, user);
|
|
142
|
+
if (userValue === undefined) return false;
|
|
143
|
+
|
|
144
|
+
const rowValue = row[rule.column];
|
|
145
|
+
if (rowValue === undefined || rowValue === null) return false;
|
|
146
|
+
|
|
147
|
+
// Array claim → membership check; scalar claim → equality.
|
|
148
|
+
if (Array.isArray(userValue)) {
|
|
149
|
+
return userValue.includes(rowValue);
|
|
150
|
+
}
|
|
151
|
+
return userValue === rowValue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resolveUserValue(rule: FromRule, user: SessionUser): unknown {
|
|
155
|
+
if (rule.refKind === "user") {
|
|
156
|
+
if (rule.refPath === "id") return user.id;
|
|
157
|
+
return user.tenantId;
|
|
158
|
+
}
|
|
159
|
+
// claim refPath is the full QN ("feature:shortName") — direct key lookup.
|
|
160
|
+
return user.claims?.[rule.refPath];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Multi-role-atomic passer for field-level READ. The caller supplies the
|
|
164
|
+
// user, the access-map for this field, and the concrete row. Returns true
|
|
165
|
+
// if AT LEAST one of the user's roles is in the map and its rule matches
|
|
166
|
+
// the row. Missing roles skip, "all" always passes.
|
|
167
|
+
export function userCanReadFieldRow(
|
|
168
|
+
user: SessionUser,
|
|
169
|
+
accessMap: OwnershipMap | undefined,
|
|
170
|
+
row: Readonly<Record<string, unknown>>,
|
|
171
|
+
): boolean {
|
|
172
|
+
if (!accessMap || Object.keys(accessMap).length === 0) return true; // public
|
|
173
|
+
for (const role of user.roles) {
|
|
174
|
+
const rule = accessMap[role];
|
|
175
|
+
if (!rule) continue;
|
|
176
|
+
if (matchesRule(rule, user, row)) return true;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Multi-role-atomic check for field-level WRITE with Straddle-prevention.
|
|
182
|
+
// A user passes iff exactly one of their roles has a rule that accepts
|
|
183
|
+
// BOTH the old and the new row. OR-ing over (any-role passes old) and
|
|
184
|
+
// (any-role passes new) is wrong — a user with two roles could split the
|
|
185
|
+
// check: role A validates the old, role B validates the new, yielding
|
|
186
|
+
// row-grabbing by stitching two rules together. See advisor review 2026-04-19.
|
|
187
|
+
export function userCanWriteFieldRow(
|
|
188
|
+
user: SessionUser,
|
|
189
|
+
accessMap: OwnershipMap | undefined,
|
|
190
|
+
oldRow: Readonly<Record<string, unknown>>,
|
|
191
|
+
newRow: Readonly<Record<string, unknown>>,
|
|
192
|
+
): boolean {
|
|
193
|
+
if (!accessMap || Object.keys(accessMap).length === 0) return true; // public
|
|
194
|
+
for (const role of user.roles) {
|
|
195
|
+
const rule = accessMap[role];
|
|
196
|
+
if (!rule) continue;
|
|
197
|
+
if (rule === "all") return true;
|
|
198
|
+
if (matchesRule(rule, user, oldRow) && matchesRule(rule, user, newRow)) return true;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Normalize legacy `readonly string[]` field-access into the OwnershipMap
|
|
204
|
+
// shape. Each role in the array becomes a key with rule "all" (unrestricted
|
|
205
|
+
// for that role). Undefined stays undefined. This is a migration shim —
|
|
206
|
+
// long-term every feature-definition writes the map shape directly.
|
|
207
|
+
export function normalizeAccessEntry(
|
|
208
|
+
entry: OwnershipMap | readonly string[] | undefined,
|
|
209
|
+
): OwnershipMap | undefined {
|
|
210
|
+
if (!entry) return undefined;
|
|
211
|
+
if (Array.isArray(entry)) {
|
|
212
|
+
if (entry.length === 0) return undefined;
|
|
213
|
+
const map: Record<string, OwnershipRule> = {};
|
|
214
|
+
for (const role of entry) {
|
|
215
|
+
map[role] = "all";
|
|
216
|
+
}
|
|
217
|
+
return map;
|
|
218
|
+
}
|
|
219
|
+
return entry as OwnershipMap; // @cast-boundary schema-walk
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Create-case: only the new row exists. Same Straddle protection not
|
|
223
|
+
// applicable (no old row to compare), but we still need per-role atomicity
|
|
224
|
+
// to respect "all"-rules and plain from-rules consistently.
|
|
225
|
+
export function userCanCreateFieldRow(
|
|
226
|
+
user: SessionUser,
|
|
227
|
+
accessMap: OwnershipMap | undefined,
|
|
228
|
+
newRow: Readonly<Record<string, unknown>>,
|
|
229
|
+
): boolean {
|
|
230
|
+
if (!accessMap || Object.keys(accessMap).length === 0) return true;
|
|
231
|
+
for (const role of user.roles) {
|
|
232
|
+
const rule = accessMap[role];
|
|
233
|
+
if (!rule) continue;
|
|
234
|
+
if (rule === "all") return true;
|
|
235
|
+
if (matchesRule(rule, user, newRow)) return true;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Result of buildOwnershipClause. The discriminant lets the caller handle
|
|
241
|
+
// the three outcomes without inspecting SQL internals:
|
|
242
|
+
//
|
|
243
|
+
// "pass" → user is unrestricted. Run the query as-is.
|
|
244
|
+
// "empty" → user has a role mapped but no rule accepts any row (missing
|
|
245
|
+
// claim, empty array, role not in map). Skip the DB call entirely
|
|
246
|
+
// — returning [] is equivalent and avoids a pointless roundtrip.
|
|
247
|
+
// "sql" → apply `.sql` as an AND to the query's where clause.
|
|
248
|
+
//
|
|
249
|
+
// "empty" vs. "pass" is the critical distinction for a safe default:
|
|
250
|
+
// undefined/pass = allow, empty = deny-by-construction. Mixing them up was
|
|
251
|
+
// the exact leak direction advisor flagged; the disjoint type prevents it.
|
|
252
|
+
export type OwnershipClause =
|
|
253
|
+
| { readonly kind: "pass" }
|
|
254
|
+
| { readonly kind: "empty" }
|
|
255
|
+
| { readonly kind: "sql"; readonly sql: SQL };
|
|
256
|
+
|
|
257
|
+
const PASS_CLAUSE: OwnershipClause = { kind: "pass" };
|
|
258
|
+
const EMPTY_CLAUSE: OwnershipClause = { kind: "empty" };
|
|
259
|
+
|
|
260
|
+
// Build an ownership clause for entity-level READ access. The caller
|
|
261
|
+
// translates the result to its query layer (see above).
|
|
262
|
+
//
|
|
263
|
+
// `table` is the Drizzle table with column objects. Unknown column on a
|
|
264
|
+
// from-rule is a boot-time misconfiguration; at request time we treat it
|
|
265
|
+
// as empty (safe default) rather than passing silently.
|
|
266
|
+
export function buildOwnershipClause(
|
|
267
|
+
user: SessionUser,
|
|
268
|
+
accessMap: OwnershipMap | undefined,
|
|
269
|
+
// biome-ignore lint/suspicious/noExplicitAny: Drizzle tables carry schema-dependent column shapes
|
|
270
|
+
table: any,
|
|
271
|
+
): OwnershipClause {
|
|
272
|
+
if (!accessMap || Object.keys(accessMap).length === 0) return PASS_CLAUSE;
|
|
273
|
+
|
|
274
|
+
const clauses: SQL[] = [];
|
|
275
|
+
let anyRoleMatched = false;
|
|
276
|
+
let everyRuleCollapsedToEmpty = true;
|
|
277
|
+
|
|
278
|
+
for (const role of user.roles) {
|
|
279
|
+
const rule = accessMap[role];
|
|
280
|
+
if (!rule) continue;
|
|
281
|
+
anyRoleMatched = true;
|
|
282
|
+
// "all" = no filter at all for this role; short-circuit.
|
|
283
|
+
if (rule === "all") return PASS_CLAUSE;
|
|
284
|
+
const resolved = ruleToClause(rule, user, table);
|
|
285
|
+
if (resolved.kind === "sql") {
|
|
286
|
+
clauses.push(resolved.sql);
|
|
287
|
+
everyRuleCollapsedToEmpty = false;
|
|
288
|
+
}
|
|
289
|
+
// "empty" contribution from one role doesn't short-circuit: another
|
|
290
|
+
// role might still contribute an OR-branch. But if ALL branches are
|
|
291
|
+
// empty, the result is empty.
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!anyRoleMatched) return EMPTY_CLAUSE;
|
|
295
|
+
if (everyRuleCollapsedToEmpty && clauses.length === 0) return EMPTY_CLAUSE;
|
|
296
|
+
if (clauses.length === 1) {
|
|
297
|
+
const only = clauses[0];
|
|
298
|
+
if (!only) return EMPTY_CLAUSE;
|
|
299
|
+
return { kind: "sql", sql: only };
|
|
300
|
+
}
|
|
301
|
+
// @cast-boundary db-operator — drizzle or() widened signature
|
|
302
|
+
// biome-ignore lint/suspicious/noExplicitAny: same reason as above
|
|
303
|
+
const combined = or(...(clauses as any)) as SQL;
|
|
304
|
+
return { kind: "sql", sql: combined };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
type RuleClauseResult = { readonly kind: "empty" } | { readonly kind: "sql"; readonly sql: SQL };
|
|
308
|
+
|
|
309
|
+
function ruleToClause(
|
|
310
|
+
rule: OwnershipRule,
|
|
311
|
+
user: SessionUser,
|
|
312
|
+
// biome-ignore lint/suspicious/noExplicitAny: Drizzle tables carry schema-dependent column shapes
|
|
313
|
+
table: any,
|
|
314
|
+
): RuleClauseResult {
|
|
315
|
+
if (rule === "all") {
|
|
316
|
+
// Caller handles "all" by short-circuit before reaching here; defensive
|
|
317
|
+
// fallback.
|
|
318
|
+
return { kind: "sql", sql: sql`true` };
|
|
319
|
+
}
|
|
320
|
+
if (rule.kind === "where") {
|
|
321
|
+
return { kind: "sql", sql: rule.where(user, table) };
|
|
322
|
+
}
|
|
323
|
+
// FromRule
|
|
324
|
+
const column = table[rule.column];
|
|
325
|
+
// Unknown column — boot validator should have caught this, but at request
|
|
326
|
+
// time we treat as empty (fail-closed).
|
|
327
|
+
if (!column) return { kind: "empty" };
|
|
328
|
+
|
|
329
|
+
const value = resolveUserValue(rule, user);
|
|
330
|
+
if (value === undefined || value === null) return { kind: "empty" };
|
|
331
|
+
|
|
332
|
+
if (Array.isArray(value)) {
|
|
333
|
+
if (value.length === 0) return { kind: "empty" };
|
|
334
|
+
return { kind: "sql", sql: inArray(column, value) };
|
|
335
|
+
}
|
|
336
|
+
return { kind: "sql", sql: eq(column, value) };
|
|
337
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Tier 2.7e Cross-Feature: ReferenceFieldDef.entity-String-Parser.
|
|
2
|
+
//
|
|
3
|
+
// Akzeptiert beide Formen:
|
|
4
|
+
// - "user" → same-feature ref (featureName = currentFeature)
|
|
5
|
+
// - "users:user" → cross-feature ref (qualifiziert)
|
|
6
|
+
//
|
|
7
|
+
// Lebt im framework-Package damit Server-Validator + Renderer (über
|
|
8
|
+
// Re-Export aus @cosmicdrift/kumiko-headless) denselben Parser nutzen — die
|
|
9
|
+
// Convention darf nicht zweimal implementiert werden.
|
|
10
|
+
|
|
11
|
+
export type ParsedRefTarget = {
|
|
12
|
+
readonly featureName: string;
|
|
13
|
+
readonly entityName: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function parseRefTarget(raw: string, currentFeature: string): ParsedRefTarget {
|
|
17
|
+
const idx = raw.indexOf(":");
|
|
18
|
+
if (idx < 0) {
|
|
19
|
+
return { featureName: currentFeature, entityName: raw };
|
|
20
|
+
}
|
|
21
|
+
return { featureName: raw.slice(0, idx), entityName: raw.slice(idx + 1) };
|
|
22
|
+
}
|