@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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "../screen-helpers";
|
|
2
|
+
import type {
|
|
3
|
+
EditFieldSpec,
|
|
4
|
+
EditLayout,
|
|
5
|
+
FeatureDefinition,
|
|
6
|
+
ListColumnSpec,
|
|
7
|
+
RowAction,
|
|
8
|
+
ToolbarAction,
|
|
9
|
+
} from "../types";
|
|
10
|
+
|
|
11
|
+
const FUNCTION_DROPPED_HINT =
|
|
12
|
+
"functions are dropped by JSON.stringify when the screen config reaches the client " +
|
|
13
|
+
"bundle — the action/field silently no-ops at runtime instead of failing loudly.";
|
|
14
|
+
|
|
15
|
+
function throwIfFunction(value: unknown, message: string): void {
|
|
16
|
+
if (typeof value === "function") {
|
|
17
|
+
throw new Error(message);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// rowActions/toolbarActions carry row-context extractors (payload/params),
|
|
22
|
+
// static visibility conditions and navigate entityId — all declarative-DSL-
|
|
23
|
+
// only fields (RowFieldExtractor's `{ pick }`/`{ map }`, FieldCondition's
|
|
24
|
+
// `{ field, eq }`/`{ field, ne }`, plain strings). A function literal here
|
|
25
|
+
// does not type-check against these structural DSL types — it only reaches
|
|
26
|
+
// here via a leaked `any`/`as any`, which this runtime check backstops.
|
|
27
|
+
const ACTION_FUNCTION_FIELDS = ["payload", "params", "entityId", "visible"] as const;
|
|
28
|
+
|
|
29
|
+
function validateActionNoFunctions(
|
|
30
|
+
featureName: string,
|
|
31
|
+
screenId: string,
|
|
32
|
+
actionKind: "rowAction" | "toolbarAction",
|
|
33
|
+
action: RowAction | ToolbarAction,
|
|
34
|
+
): void {
|
|
35
|
+
const record = action as unknown as Record<string, unknown>;
|
|
36
|
+
for (const field of ACTION_FUNCTION_FIELDS) {
|
|
37
|
+
throwIfFunction(
|
|
38
|
+
record[field],
|
|
39
|
+
`[Feature ${featureName}] Screen "${screenId}" ${actionKind} "${action.id}" ${field} ` +
|
|
40
|
+
`is a function — ${FUNCTION_DROPPED_HINT} Use the declarative DSL ({ pick }, { map }, ` +
|
|
41
|
+
`"fieldName", { field, eq }) instead.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateActionWiring(feature: FeatureDefinition): void {
|
|
47
|
+
for (const screen of Object.values(feature.screens)) {
|
|
48
|
+
if (screen.type !== "entityList" && screen.type !== "projectionList") continue;
|
|
49
|
+
for (const action of screen.rowActions ?? []) {
|
|
50
|
+
validateActionNoFunctions(feature.name, screen.id, "rowAction", action);
|
|
51
|
+
}
|
|
52
|
+
for (const action of screen.toolbarActions ?? []) {
|
|
53
|
+
validateActionNoFunctions(feature.name, screen.id, "toolbarAction", action);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// EditFieldSpec's visible/readOnly/required are the same FieldCondition DSL
|
|
59
|
+
// as rowActions — same footgun, different screen type (entityEdit layouts).
|
|
60
|
+
const EDIT_FIELD_FUNCTION_FIELDS = ["visible", "readOnly", "required"] as const;
|
|
61
|
+
|
|
62
|
+
function validateEditFieldNoFunctions(
|
|
63
|
+
featureName: string,
|
|
64
|
+
screenId: string,
|
|
65
|
+
screenType: string,
|
|
66
|
+
fieldSpec: EditFieldSpec,
|
|
67
|
+
): void {
|
|
68
|
+
const normalized = normalizeEditField(fieldSpec);
|
|
69
|
+
const record = normalized as unknown as Record<string, unknown>;
|
|
70
|
+
for (const key of EDIT_FIELD_FUNCTION_FIELDS) {
|
|
71
|
+
throwIfFunction(
|
|
72
|
+
record[key],
|
|
73
|
+
`[Feature ${featureName}] Screen "${screenId}" (${screenType}) field "${normalized.field}" ` +
|
|
74
|
+
`${key} is a function — ${FUNCTION_DROPPED_HINT} Use a FieldCondition ` +
|
|
75
|
+
`(boolean or { field, eq }/{ field, ne }) instead.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
throwIfFunction(
|
|
79
|
+
normalized.renderer,
|
|
80
|
+
`[Feature ${featureName}] Screen "${screenId}" (${screenType}) field "${normalized.field}" ` +
|
|
81
|
+
`renderer is a function — ${FUNCTION_DROPPED_HINT} Use a FormatSpec ({ format: "..." }) instead.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateColumnsNoFunctions(
|
|
86
|
+
featureName: string,
|
|
87
|
+
screenId: string,
|
|
88
|
+
screenType: string,
|
|
89
|
+
columns: readonly ListColumnSpec[],
|
|
90
|
+
): void {
|
|
91
|
+
for (const col of columns) {
|
|
92
|
+
const normalized = normalizeListColumn(col);
|
|
93
|
+
throwIfFunction(
|
|
94
|
+
normalized.renderer,
|
|
95
|
+
`[Feature ${featureName}] Screen "${screenId}" (${screenType}) column ` +
|
|
96
|
+
`"${normalized.field}" renderer is a function — ${FUNCTION_DROPPED_HINT} ` +
|
|
97
|
+
`Use a FormatSpec ({ format: "..." }) instead.`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateEditLayoutNoFunctions(
|
|
103
|
+
featureName: string,
|
|
104
|
+
screenId: string,
|
|
105
|
+
screenType: string,
|
|
106
|
+
layout: EditLayout,
|
|
107
|
+
): void {
|
|
108
|
+
for (const section of layout.sections) {
|
|
109
|
+
if (isExtensionEditSection(section)) continue;
|
|
110
|
+
for (const fieldSpec of section.fields) {
|
|
111
|
+
validateEditFieldNoFunctions(featureName, screenId, screenType, fieldSpec);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Screen types that carry an EditLayout (entityEdit's layout shape, reused
|
|
117
|
+
// verbatim by actionForm/configEdit/projectionDetail) vs. ones that carry
|
|
118
|
+
// ListColumnSpec[] (entityList/projectionList, plus dashboard "list" panels)
|
|
119
|
+
// both funnel through the same two checks below — a function literal in
|
|
120
|
+
// either shape is dropped by JSON.stringify regardless of which screen type
|
|
121
|
+
// hosts it.
|
|
122
|
+
export function validateFieldWiring(feature: FeatureDefinition): void {
|
|
123
|
+
for (const screen of Object.values(feature.screens)) {
|
|
124
|
+
if (screen.type === "entityList" || screen.type === "projectionList") {
|
|
125
|
+
validateColumnsNoFunctions(feature.name, screen.id, screen.type, screen.columns);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (
|
|
129
|
+
screen.type === "entityEdit" ||
|
|
130
|
+
screen.type === "actionForm" ||
|
|
131
|
+
screen.type === "configEdit" ||
|
|
132
|
+
screen.type === "projectionDetail"
|
|
133
|
+
) {
|
|
134
|
+
validateEditLayoutNoFunctions(feature.name, screen.id, screen.type, screen.layout);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (screen.type === "dashboard") {
|
|
138
|
+
for (const panel of screen.panels) {
|
|
139
|
+
if (panel.kind !== "list") continue;
|
|
140
|
+
validateColumnsNoFunctions(
|
|
141
|
+
feature.name,
|
|
142
|
+
`${screen.id}:${panel.id}`,
|
|
143
|
+
"dashboard-list",
|
|
144
|
+
panel.columns,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BootCheckContext, FeatureDefinition } from "../types";
|
|
2
|
+
|
|
3
|
+
// r.bootCheck(fn) lets a feature declare its own mount-invariant instead of
|
|
4
|
+
// relying on framework-internal knowledge (the gdpr-storage.ts guards are
|
|
5
|
+
// the framework-owned version of this same idea). Each registered fn gets
|
|
6
|
+
// the full mounted-feature set and throws to fail the boot; we wrap the
|
|
7
|
+
// message with the owning feature's name so the error text points back at
|
|
8
|
+
// the feature that declared the check.
|
|
9
|
+
export function validateFeatureBootChecks(features: readonly FeatureDefinition[]): void {
|
|
10
|
+
const ctx: BootCheckContext = { features };
|
|
11
|
+
for (const feature of features) {
|
|
12
|
+
for (const check of feature.bootChecks) {
|
|
13
|
+
try {
|
|
14
|
+
check(ctx);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17
|
+
throw new Error(`[Feature ${feature.name}] r.bootCheck failed: ${message}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isEncryptedAtRest } from "../config-helpers";
|
|
1
2
|
import type { FeatureDefinition } from "../types";
|
|
2
3
|
|
|
3
4
|
// --- Toggleable-dependency warnings ---
|
|
@@ -79,9 +80,9 @@ export function validateConfigKeyComputed(feature: FeatureDefinition): void {
|
|
|
79
80
|
// returns a plain value, encrypted expects cipher-text in the row. The
|
|
80
81
|
// cascade doesn't know which one to prefer on write. Rejecting at boot
|
|
81
82
|
// is cheaper than surprising behaviour at runtime.
|
|
82
|
-
if (keyDef
|
|
83
|
+
if (isEncryptedAtRest(keyDef)) {
|
|
83
84
|
throw new Error(
|
|
84
|
-
`[Feature ${feature.name}] Config key "${keyName}" has both encrypted=
|
|
85
|
+
`[Feature ${feature.name}] Config key "${keyName}" has both encrypted/backing="secrets" and a computed resolver — these are mutually exclusive paradigms`,
|
|
85
86
|
);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
@@ -128,9 +129,9 @@ export function validateConfigKeyAllowPerRequest(feature: FeatureDefinition): vo
|
|
|
128
129
|
// encrypted + per-request would expose a cipher-text interpretation
|
|
129
130
|
// to query-strings. The secret-value shouldn't be transported this
|
|
130
131
|
// way — reject as a paradigm-mismatch.
|
|
131
|
-
if (keyDef
|
|
132
|
+
if (isEncryptedAtRest(keyDef)) {
|
|
132
133
|
throw new Error(
|
|
133
|
-
`[Feature ${feature.name}] Config key "${keyName}" has allowPerRequest=true but encrypted=
|
|
134
|
+
`[Feature ${feature.name}] Config key "${keyName}" has allowPerRequest=true but encrypted/backing="secrets" — secret values may not be set via query-params`,
|
|
134
135
|
);
|
|
135
136
|
}
|
|
136
137
|
}
|
|
@@ -160,6 +161,41 @@ export function validateConfigKeyBacking(feature: FeatureDefinition): void {
|
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
// --- Config key piiEncrypted compatibility (kumiko-platform#231/#459) ---
|
|
165
|
+
|
|
166
|
+
export function validateConfigKeyPiiEncrypted(feature: FeatureDefinition): void {
|
|
167
|
+
for (const [keyName, keyDef] of Object.entries(feature.configKeys)) {
|
|
168
|
+
if (!keyDef.piiEncrypted) continue;
|
|
169
|
+
|
|
170
|
+
if (keyDef.type !== "text") {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`[Feature ${feature.name}] Config key "${keyName}" has piiEncrypted=true but type="${keyDef.type}" — piiEncrypted only applies to text keys`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// system scope has no subject (no tenant, no user) for the subject-KMS
|
|
177
|
+
// to encrypt under.
|
|
178
|
+
if (keyDef.scope === "system") {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`[Feature ${feature.name}] Config key "${keyName}" has piiEncrypted=true but scope="system" — system-scope has no subject (tenant/user) for the subject-KMS to encrypt under`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Mutually exclusive with the shared-cipher path: a config value is
|
|
185
|
+
// either subject-keyed (piiEncrypted) or master-key-keyed (encrypted/
|
|
186
|
+
// backing="secrets"), never both — unlike entity fields, which allow
|
|
187
|
+
// both markers together (double-wrap, e.g. auth-mfa.totpSecret) because
|
|
188
|
+
// entity rows decrypt through a fixed executor pipeline that peels both
|
|
189
|
+
// layers. Config values have no such pipeline; picking one keeps the
|
|
190
|
+
// resolver simple.
|
|
191
|
+
if (isEncryptedAtRest(keyDef)) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`[Feature ${feature.name}] Config key "${keyName}" has both piiEncrypted=true and encrypted/backing="secrets" — pick one: piiEncrypted (subject-KMS, user-visible) or encrypted (shared cipher, server-only values)`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
163
199
|
// --- Config key cross-feature reference validation ---
|
|
164
200
|
|
|
165
201
|
export function validateConfigReads(
|
|
@@ -101,29 +101,27 @@ const USER_BUCKETED_RATE_LIMIT_PER: ReadonlySet<string> = new Set(["user", "user
|
|
|
101
101
|
// at runtime, but we fail at boot to turn an easy-to-miss security regression
|
|
102
102
|
// into a loud configuration error.
|
|
103
103
|
export function validateHandlerAccess(feature: FeatureDefinition): void {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
);
|
|
104
|
+
const kinds = [
|
|
105
|
+
{ kind: "write" as const, label: "Write", handlers: feature.writeHandlers },
|
|
106
|
+
{ kind: "query" as const, label: "Query", handlers: feature.queryHandlers },
|
|
107
|
+
{ kind: "stream" as const, label: "Stream", handlers: feature.streamHandlers },
|
|
108
|
+
];
|
|
109
|
+
for (const { kind, label, handlers } of kinds) {
|
|
110
|
+
for (const [name, handler] of Object.entries(handlers)) {
|
|
111
|
+
if (!handler.access) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`${label} handler "${feature.name}:${kind}:${name}" is missing an access rule. ` +
|
|
114
|
+
`Set { roles: [...] } for role-based access, or { openToAll: true } for any authenticated user.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
validateAnonymousRateLimit(feature.name, kind, name, handler.access, handler.rateLimit);
|
|
119
118
|
}
|
|
120
|
-
validateAnonymousRateLimit(feature.name, "query", name, handler.access, handler.rateLimit);
|
|
121
119
|
}
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
export function validateAnonymousRateLimit(
|
|
125
123
|
featureName: string,
|
|
126
|
-
kind: "write" | "query",
|
|
124
|
+
kind: "write" | "query" | "stream",
|
|
127
125
|
handlerName: string,
|
|
128
126
|
access: NonNullable<FeatureDefinition["writeHandlers"][string]["access"]>,
|
|
129
127
|
rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"],
|
|
@@ -178,8 +176,9 @@ export function validateMultiStreamProjections(feature: FeatureDefinition): void
|
|
|
178
176
|
//
|
|
179
177
|
// Die häufigste Quelle von Konflikten ist Hand-Konstruktion:
|
|
180
178
|
// { foo: { type: "timestamp", locatedBy: "fooTz" } }
|
|
181
|
-
// ohne das `fooTz`-Feld zu deklarieren.
|
|
182
|
-
//
|
|
179
|
+
// ohne das `fooTz`-Feld zu deklarieren. `createLocatedTimestampField()`
|
|
180
|
+
// erzeugt stattdessen EIN Feld vom Typ "locatedTimestamp" — wer den nutzt,
|
|
181
|
+
// fliegt nicht durch diesen Validator.
|
|
183
182
|
export function validateLocatedTimestamps(feature: FeatureDefinition): void {
|
|
184
183
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
185
184
|
const fields = entity.fields;
|
|
@@ -190,8 +189,8 @@ export function validateLocatedTimestamps(feature: FeatureDefinition): void {
|
|
|
190
189
|
throw new Error(
|
|
191
190
|
`Feature "${feature.name}", entity "${entityName}": field "${fieldName}" has ` +
|
|
192
191
|
`locatedBy: "${field.locatedBy}" but no field with that name exists in the entity. ` +
|
|
193
|
-
`Either declare the tz-field, or use
|
|
194
|
-
`to create
|
|
192
|
+
`Either declare the tz-field, or use createLocatedTimestampField() ` +
|
|
193
|
+
`to create a single located-timestamp field instead.`,
|
|
195
194
|
);
|
|
196
195
|
}
|
|
197
196
|
if (referenced.type !== "tz") {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { normalizeListColumn } from "../screen-helpers";
|
|
2
|
+
import type { EntityListScreenDefinition, FeatureDefinition } from "../types";
|
|
3
|
+
|
|
4
|
+
/** Operator lists default searchable; low-cardinality audit trails stay opt-out. */
|
|
5
|
+
export const SEARCHABLE_FALSE_WHITELIST = new Set(["download-attempt-list"]);
|
|
6
|
+
|
|
7
|
+
function hasFilterableFields(feature: FeatureDefinition, entityName: string): boolean {
|
|
8
|
+
const entities = feature.entities;
|
|
9
|
+
if (!entities) return false;
|
|
10
|
+
const entity = entities[entityName];
|
|
11
|
+
if (!entity) return false;
|
|
12
|
+
return Object.values(entity.fields).some(
|
|
13
|
+
(raw) => (raw as { readonly filterable?: boolean }).filterable === true,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hasEntityEditDetail(feature: FeatureDefinition, entityName: string): boolean {
|
|
18
|
+
return Object.values(feature.screens).some(
|
|
19
|
+
(s) => s.type === "entityEdit" && s.entity === entityName,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateOneEntityListScreen(
|
|
24
|
+
feature: FeatureDefinition,
|
|
25
|
+
screen: EntityListScreenDefinition,
|
|
26
|
+
): void {
|
|
27
|
+
const prefix = `[entityList] Feature "${feature.name}" screen "${screen.id}"`;
|
|
28
|
+
|
|
29
|
+
if (screen.searchable === false && !SEARCHABLE_FALSE_WHITELIST.has(screen.id)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`${prefix}: searchable defaults to true for operator lists — set searchable: true or add "${screen.id}" to the whitelist`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const filtersActive = hasFilterableFields(feature, screen.entity);
|
|
36
|
+
const searchable = screen.searchable !== false;
|
|
37
|
+
if ((searchable || filtersActive) && screen.defaultSort === undefined) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`${prefix}: defaultSort required when searchable or filterable fields are active`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (screen.defaultSort !== undefined) {
|
|
44
|
+
const sortField = screen.defaultSort.field;
|
|
45
|
+
const col = screen.columns.find((c) => normalizeListColumn(c).field === sortField);
|
|
46
|
+
if (col === undefined) {
|
|
47
|
+
throw new Error(`${prefix}: defaultSort.field "${sortField}" is not a listed column`);
|
|
48
|
+
}
|
|
49
|
+
const entityDef = feature.entities?.[screen.entity];
|
|
50
|
+
const fieldDef = entityDef?.fields[sortField];
|
|
51
|
+
const isSortable =
|
|
52
|
+
fieldDef !== undefined && "sortable" in fieldDef && fieldDef.sortable === true;
|
|
53
|
+
if (!isSortable) {
|
|
54
|
+
throw new Error(`${prefix}: defaultSort column "${sortField}" must be sortable`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (hasEntityEditDetail(feature, screen.entity)) {
|
|
59
|
+
const hasNavigate = (screen.rowActions ?? []).some(
|
|
60
|
+
(a) => a.kind === "navigate" && (a.id === "view" || a.id === "edit"),
|
|
61
|
+
);
|
|
62
|
+
if (!hasNavigate) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`${prefix}: detail screen exists — add a navigate rowAction with id "view" or "edit"`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const col of screen.columns) {
|
|
70
|
+
const normalized = normalizeListColumn(col);
|
|
71
|
+
if (normalized.label === undefined) {
|
|
72
|
+
const entity = feature.entities?.[screen.entity];
|
|
73
|
+
const isDerived = entity?.derivedFields?.[normalized.field] !== undefined;
|
|
74
|
+
if (!entity?.fields[normalized.field] && !isDerived) {
|
|
75
|
+
throw new Error(`${prefix}: unknown column field "${normalized.field}"`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function validateEntityListScreens(features: readonly FeatureDefinition[]): void {
|
|
82
|
+
for (const feature of features) {
|
|
83
|
+
for (const screen of Object.values(feature.screens)) {
|
|
84
|
+
if (screen.type !== "entityList") continue;
|
|
85
|
+
validateOneEntityListScreen(feature, screen);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -57,23 +57,3 @@ export function validateGdprStoragePersistence(features: readonly FeatureDefinit
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
|
|
61
|
-
// V2: export-without-erase guard. A feature that registers an EXT_USER_DATA
|
|
62
|
-
// export hook without a matching delete hook exports data under Art.20 but
|
|
63
|
-
// never erases it on forget — an Art.17 violation. Registry-level signal only;
|
|
64
|
-
// runtime no-ops (a delete hook that silently skips) are not detectable here.
|
|
65
|
-
export function validateGdprHookCompleteness(features: readonly FeatureDefinition[]): void {
|
|
66
|
-
for (const feature of features) {
|
|
67
|
-
for (const usage of feature.extensionUsages) {
|
|
68
|
-
if (usage.extensionName !== "userData") continue;
|
|
69
|
-
const hasExport = typeof usage.options?.["export"] === "function";
|
|
70
|
-
const hasDelete = typeof usage.options?.["delete"] === "function";
|
|
71
|
-
if (hasExport && !hasDelete) {
|
|
72
|
-
// biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
|
|
73
|
-
console.warn(
|
|
74
|
-
`[kumiko:boot] Feature "${feature.name}" exports entity "${usage.entityName}" via EXT_USER_DATA but registers no delete hook — data is included in Art.20 exports but never erased on forget (Art.17 risk). Add a delete hook, or a no-op with a comment explaining why erasure is intentionally skipped.`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
@@ -1,20 +1,68 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildEffectiveTranslationKeys,
|
|
3
|
+
featureHasI18nSurface,
|
|
3
4
|
findTranslationLocaleGaps,
|
|
4
5
|
requiredKeysFromFeature,
|
|
6
|
+
requiredKeysFromNav,
|
|
7
|
+
requiredKeysFromScreen,
|
|
8
|
+
requiredKeysFromWorkspace,
|
|
5
9
|
} from "../../i18n/required-surface-keys";
|
|
10
|
+
import { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE } from "../build-config-feature-schema";
|
|
11
|
+
import { createRegistry } from "../registry";
|
|
6
12
|
import type { FeatureDefinition } from "../types";
|
|
13
|
+
import type { Registry } from "../types/feature";
|
|
14
|
+
|
|
15
|
+
function requiredKeysFromGeneratedConfigHub(registry: Registry): readonly string[] {
|
|
16
|
+
const schema = buildConfigFeatureSchema(registry);
|
|
17
|
+
if (schema.navs.length === 0) return [];
|
|
18
|
+
const out = new Set<string>();
|
|
19
|
+
|
|
20
|
+
for (const screen of schema.screens) {
|
|
21
|
+
for (const key of requiredKeysFromScreen(SETTINGS_HUB_FEATURE, screen)) out.add(key);
|
|
22
|
+
}
|
|
23
|
+
for (const nav of schema.navs) {
|
|
24
|
+
for (const key of requiredKeysFromNav(nav)) out.add(key);
|
|
25
|
+
}
|
|
26
|
+
if (schema.workspace) {
|
|
27
|
+
for (const key of requiredKeysFromWorkspace(schema.workspace.definition)) out.add(key);
|
|
28
|
+
}
|
|
29
|
+
out.add("config.settings.title");
|
|
30
|
+
for (const scope of ["system", "tenant", "user"] as const) {
|
|
31
|
+
out.add(`config.settings.${scope}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return [...out];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isFrameworkOwnedI18nKey(key: string): boolean {
|
|
38
|
+
return key.startsWith("kumiko.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasDefinedTranslation(defined: Set<string>, key: string): boolean {
|
|
42
|
+
if (defined.has(key)) return true;
|
|
43
|
+
const colon = key.indexOf(":");
|
|
44
|
+
if (colon > 0) {
|
|
45
|
+
const feature = key.slice(0, colon);
|
|
46
|
+
const local = key.slice(colon + 1);
|
|
47
|
+
if (defined.has(`${feature}:${local}`)) return true;
|
|
48
|
+
}
|
|
49
|
+
if (key.includes(".")) {
|
|
50
|
+
const feature = key.split(".")[0];
|
|
51
|
+
if (feature && defined.has(`${feature}:${key}`)) return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
7
55
|
|
|
8
56
|
export function validateI18nSurfaceKeys(features: readonly FeatureDefinition[]): void {
|
|
9
57
|
const defined = buildEffectiveTranslationKeys(features);
|
|
58
|
+
const registry = createRegistry(features);
|
|
10
59
|
|
|
11
60
|
for (const feature of features) {
|
|
12
|
-
|
|
13
|
-
// keys, surface + locale checks apply (apps, new bundled work).
|
|
14
|
-
if (Object.keys(feature.translations ?? {}).length === 0) continue;
|
|
61
|
+
if (!featureHasI18nSurface(feature)) continue;
|
|
15
62
|
|
|
16
63
|
for (const key of requiredKeysFromFeature(feature)) {
|
|
17
|
-
if (
|
|
64
|
+
if (isFrameworkOwnedI18nKey(key)) continue;
|
|
65
|
+
if (!hasDefinedTranslation(defined, key)) {
|
|
18
66
|
throw new Error(
|
|
19
67
|
`[i18n] Feature "${feature.name}": required translation key missing: "${key}"`,
|
|
20
68
|
);
|
|
@@ -22,6 +70,12 @@ export function validateI18nSurfaceKeys(features: readonly FeatureDefinition[]):
|
|
|
22
70
|
}
|
|
23
71
|
}
|
|
24
72
|
|
|
73
|
+
for (const key of requiredKeysFromGeneratedConfigHub(registry)) {
|
|
74
|
+
if (!hasDefinedTranslation(defined, key)) {
|
|
75
|
+
throw new Error(`[i18n] Settings-Hub: required translation key missing: "${key}"`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
25
79
|
for (const gap of findTranslationLocaleGaps(features)) {
|
|
26
80
|
throw new Error(
|
|
27
81
|
`[i18n] Feature "${gap.featureName}": key "${gap.key}" missing locale(s): ${gap.missingLocales.join(", ")}`,
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { validateEntityFieldEncryptionAvailable } from "../../db/entity-field-encryption";
|
|
1
2
|
import { QnTypes, qualifyEntityName } from "../qualified-name";
|
|
2
3
|
import type { ClaimKeyDefinition, FeatureDefinition } from "../types";
|
|
4
|
+
import { validateActionWiring, validateFieldWiring } from "./action-wiring";
|
|
3
5
|
import { validateApiExposureMatching, validateExtensionUsages } from "./api-ext";
|
|
6
|
+
import { validateFeatureBootChecks } from "./boot-check";
|
|
4
7
|
import {
|
|
5
8
|
validateCircularDeps,
|
|
6
9
|
validateConfigKeyAllowPerRequest,
|
|
7
10
|
validateConfigKeyBacking,
|
|
8
11
|
validateConfigKeyBounds,
|
|
9
12
|
validateConfigKeyComputed,
|
|
13
|
+
validateConfigKeyPiiEncrypted,
|
|
10
14
|
validateConfigKeyRequired,
|
|
11
15
|
validateConfigReads,
|
|
12
16
|
warnOnToggleableDependencies,
|
|
@@ -26,30 +30,35 @@ import {
|
|
|
26
30
|
validateReferenceFields,
|
|
27
31
|
validateTransitions,
|
|
28
32
|
} from "./entity-handler";
|
|
29
|
-
import {
|
|
33
|
+
import { validateEntityListScreens } from "./entity-list-screens";
|
|
34
|
+
import { validateGdprStoragePersistence } from "./gdpr-storage";
|
|
30
35
|
import { validateI18nSurfaceKeys } from "./i18n-keys";
|
|
31
|
-
import { validateOwnershipRules } from "./ownership";
|
|
32
|
-
import { validatePiiAndRetention } from "./pii-retention";
|
|
33
36
|
import {
|
|
34
37
|
collectKnownRoles,
|
|
35
38
|
collectNavQns,
|
|
36
|
-
collectScreenQns,
|
|
37
|
-
collectScreensByShortId,
|
|
38
|
-
collectWorkspaceQns,
|
|
39
39
|
collectWriteHandlerQns,
|
|
40
|
-
validateDefaultWorkspaceUniqueness,
|
|
41
40
|
validateNavCycles,
|
|
42
41
|
validateNavs,
|
|
42
|
+
} from "./nav";
|
|
43
|
+
import { validateOwnershipRules } from "./ownership";
|
|
44
|
+
import { validatePiiAndRetention } from "./pii-retention";
|
|
45
|
+
import {
|
|
46
|
+
collectScreenQns,
|
|
47
|
+
collectScreensByShortId,
|
|
43
48
|
validateScreenShortIdCollisions,
|
|
44
49
|
validateScreens,
|
|
50
|
+
} from "./screens";
|
|
51
|
+
import {
|
|
52
|
+
collectWorkspaceQns,
|
|
53
|
+
validateDefaultWorkspaceUniqueness,
|
|
45
54
|
validateWorkspaces,
|
|
46
|
-
} from "./
|
|
55
|
+
} from "./workspaces";
|
|
47
56
|
|
|
48
57
|
export { validateAppCustomScreenWriteQns } from "./custom-screen-write-qns";
|
|
49
58
|
// Re-export: wird von run-dev-app.ts benötigt um Write-Handler-QNs
|
|
50
59
|
// an den Codegen zu übergeben. Nicht Teil von validateBoot, aber
|
|
51
60
|
// dieselbe Extraktionslogik.
|
|
52
|
-
export { collectWriteHandlerQns } from "./
|
|
61
|
+
export { collectWriteHandlerQns } from "./nav";
|
|
53
62
|
|
|
54
63
|
/**
|
|
55
64
|
* Validates all feature configurations at boot time.
|
|
@@ -158,8 +167,15 @@ export function validateBoot(features: readonly FeatureDefinition[]): void {
|
|
|
158
167
|
validateConfigKeyComputed(feature);
|
|
159
168
|
validateConfigKeyAllowPerRequest(feature);
|
|
160
169
|
validateConfigKeyBacking(feature);
|
|
170
|
+
validateConfigKeyPiiEncrypted(feature);
|
|
161
171
|
validateOwnershipRules(feature, allClaimKeys, knownRoles);
|
|
162
172
|
validateMultiStreamProjections(feature);
|
|
173
|
+
// Vor validateScreens: dessen visible/entityId-Feldref-Checks werfen für
|
|
174
|
+
// einen Function-Wert bereits (mit verwirrender "unknown field undefined"-
|
|
175
|
+
// Message, weil `typeof fn !== "boolean"` true ist), bevor der klare
|
|
176
|
+
// Function-Check hier überhaupt liefe.
|
|
177
|
+
validateActionWiring(feature);
|
|
178
|
+
validateFieldWiring(feature);
|
|
163
179
|
validateScreens(
|
|
164
180
|
feature,
|
|
165
181
|
featureMap,
|
|
@@ -175,12 +191,17 @@ export function validateBoot(features: readonly FeatureDefinition[]): void {
|
|
|
175
191
|
validateNavCycles(allNavQns);
|
|
176
192
|
validateDefaultWorkspaceUniqueness(allWorkspaceQns);
|
|
177
193
|
validateI18nSurfaceKeys(features);
|
|
194
|
+
validateEntityListScreens(features);
|
|
178
195
|
validateExtensionPreSaveWiring(features);
|
|
179
196
|
validateGdprStoragePersistence(features);
|
|
180
|
-
|
|
197
|
+
validateFeatureBootChecks(features);
|
|
181
198
|
|
|
182
|
-
if (hasEncryptedFields
|
|
183
|
-
|
|
199
|
+
if (hasEncryptedFields) {
|
|
200
|
+
// Availability check, not env-presence: eagerly building the keyring
|
|
201
|
+
// catches malformed keys (wrong length, bad base64) at boot instead of
|
|
202
|
+
// on the first encrypted read in prod. An injected cipher (test seam,
|
|
203
|
+
// custom KMS provider) satisfies the requirement without env keys.
|
|
204
|
+
validateEntityFieldEncryptionAvailable();
|
|
184
205
|
}
|
|
185
206
|
|
|
186
207
|
if (hasFileFields && !process.env["FILE_STORAGE_PROVIDER"]) {
|