@cosmicdrift/kumiko-framework 0.386.0 → 0.388.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/AGENTS.md +6 -1
- package/LICENSE +21 -57
- package/README.md +2 -2
- package/agents/examples/anonymous-access.md +1 -1
- package/agents/examples/apex-landing.md +204 -0
- package/agents/examples/api-worker-split.md +64 -0
- package/agents/examples/app-config.md +14 -0
- package/agents/examples/auth-mfa.md +115 -0
- package/agents/examples/env-composition.md +28 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/prod-boot-options.md +16 -0
- package/agents/examples/rate-limiting.md +48 -0
- package/agents/examples/record-detail-layout.md +692 -0
- package/agents/examples/seed-migration.md +212 -0
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +40 -39
- package/dist/api/api-constants.js +2 -2
- package/dist/api/auth-middleware.js +7 -7
- package/dist/api/auth-rate-limiters.js +7 -3
- package/dist/api/auth-routes-mfa.js +1 -1
- package/dist/api/auth-routes-session.js +1 -1
- package/dist/api/auth-routes-shared.js +7 -3
- package/dist/api/auth-routes-signup.js +1 -1
- package/dist/api/client-error-route.js +8 -2
- package/dist/api/client-ip.js +3 -1
- package/dist/api/extra-route.js +1 -1
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/pii-leak-guard.js +4 -2
- package/dist/api/readiness.js +5 -5
- package/dist/api/redis-sse-broker.js +8 -6
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.js +2 -2
- package/dist/api/server-boot-checks.d.ts +10 -0
- package/dist/api/server-boot-checks.js +157 -0
- package/dist/api/server-event-dispatcher.d.ts +26 -0
- package/dist/api/server-event-dispatcher.js +208 -0
- package/dist/api/server-extra-routes.d.ts +11 -0
- package/dist/api/server-extra-routes.js +203 -0
- package/dist/api/server-middleware.d.ts +20 -0
- package/dist/api/server-middleware.js +211 -0
- package/dist/api/server-routes.d.ts +19 -0
- package/dist/api/server-routes.js +293 -0
- package/dist/api/server-websocket-routes.d.ts +4 -0
- package/dist/api/server-websocket-routes.js +108 -0
- package/dist/api/server-wiring.d.ts +32 -0
- package/dist/api/server-wiring.js +236 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1250
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-broker.js +1 -1
- package/dist/api/sse-route.d.ts +13 -13
- package/dist/api/sse-route.js +31 -27
- package/dist/auth/index.js +4 -4
- package/dist/auth/roles.d.ts +4 -4
- package/dist/auth/roles.js +24 -24
- package/dist/compliance/duration-spec.js +18 -20
- package/dist/compliance/index.js +2 -3
- package/dist/compliance/override-schema.d.ts +4 -4
- package/dist/compliance/override-schema.js +19 -22
- package/dist/compliance/profiles.d.ts +26 -29
- package/dist/compliance/profiles.js +62 -69
- package/dist/compliance/sub-processors.d.ts +28 -30
- package/dist/compliance/sub-processors.js +17 -21
- package/dist/consumer-cli.js +4 -4
- package/dist/crypto/blind-index.js +18 -17
- package/dist/crypto/ciphertext-pattern.js +1 -1
- package/dist/crypto/event-pii.js +3 -3
- package/dist/crypto/kek-source.d.ts +1 -1
- package/dist/crypto/kek-source.js +3 -2
- package/dist/crypto/kms-adapter.d.ts +1 -1
- package/dist/crypto/kms-adapter.js +4 -6
- package/dist/crypto/kms-wiring.d.ts +1 -2
- package/dist/crypto/kms-wiring.js +2 -2
- package/dist/crypto/owner-transition-rekey.d.ts +5 -1
- package/dist/crypto/owner-transition-rekey.js +1 -1
- package/dist/crypto/pg-kms-adapter.js +2 -6
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +7 -5
- package/dist/crypto/subject-resolver.d.ts +1 -1
- package/dist/crypto/subject-resolver.js +2 -2
- package/dist/db/aggregate-sql.d.ts +21 -0
- package/dist/db/aggregate-sql.js +155 -0
- package/dist/db/apply-entity-event.d.ts +6 -6
- package/dist/db/apply-entity-event.js +55 -55
- package/dist/db/blind-index-cleanup.js +7 -7
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +95 -0
- package/dist/db/collect-table-metas.js +10 -11
- package/dist/db/compound-types.d.ts +1 -1
- package/dist/db/compound-types.js +12 -12
- package/dist/db/cursor.js +3 -4
- package/dist/db/dialect.d.ts +2 -2
- package/dist/db/dialect.js +3 -3
- package/dist/db/distinct-on.d.ts +1 -1
- package/dist/db/driver-row-coercion.d.ts +2 -0
- package/dist/db/driver-row-coercion.js +186 -0
- package/dist/db/driver-value-preparation.d.ts +16 -0
- package/dist/db/driver-value-preparation.js +99 -0
- package/dist/db/eagerload.d.ts +3 -3
- package/dist/db/eagerload.js +21 -12
- package/dist/db/entity-field-encryption.d.ts +1 -0
- package/dist/db/entity-field-encryption.js +1 -1
- package/dist/db/entity-table-meta.d.ts +2 -2
- package/dist/db/entity-table-meta.js +105 -112
- package/dist/db/event-store-executor-context.d.ts +3 -2
- package/dist/db/event-store-executor-context.js +13 -13
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +112 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +203 -0
- package/dist/db/event-store-executor-read-paging.d.ts +14 -0
- package/dist/db/event-store-executor-read-paging.js +99 -0
- package/dist/db/event-store-executor-read-rows.d.ts +6 -0
- package/dist/db/event-store-executor-read-rows.js +32 -0
- package/dist/db/event-store-executor-read-search.d.ts +28 -0
- package/dist/db/event-store-executor-read-search.js +221 -0
- package/dist/db/event-store-executor-read-sort.d.ts +41 -0
- package/dist/db/event-store-executor-read-sort.js +130 -0
- package/dist/db/event-store-executor-read-where.d.ts +24 -0
- package/dist/db/event-store-executor-read-where.js +81 -0
- package/dist/db/event-store-executor-read.d.ts +2 -9
- package/dist/db/event-store-executor-read.js +50 -704
- package/dist/db/event-store-executor-write-steps.d.ts +20 -0
- package/dist/db/event-store-executor-write-steps.js +100 -0
- package/dist/db/event-store-executor-write.js +401 -510
- package/dist/db/index.d.ts +6 -2
- package/dist/db/index.js +5 -1
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/located-timestamp.d.ts +11 -11
- package/dist/db/located-timestamp.js +29 -30
- package/dist/db/migrate-generator.d.ts +1 -1
- package/dist/db/migrate-generator.js +97 -105
- package/dist/db/migrate-runner.js +29 -26
- package/dist/db/money.d.ts +6 -6
- package/dist/db/money.js +29 -27
- package/dist/db/queries/backfill-pii.js +5 -5
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/es-ops.d.ts +1 -1
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +3 -3
- package/dist/db/queries/event-store.js +4 -2
- package/dist/db/queries/projection-rebuild.js +3 -3
- package/dist/db/queries/raw-sql.d.ts +2 -2
- package/dist/db/queries/raw-sql.js +2 -2
- package/dist/db/queries/shadow-swap.js +19 -19
- package/dist/db/query.d.ts +3 -16
- package/dist/db/query.js +8 -761
- package/dist/db/rebuild-marker.js +20 -21
- package/dist/db/reference-data.js +6 -1
- package/dist/db/replay-migration-sql.js +2 -2
- package/dist/db/schema-inspection.js +4 -5
- package/dist/db/table-builder.d.ts +1 -2
- package/dist/db/table-builder.js +137 -190
- package/dist/db/table-info.d.ts +10 -0
- package/dist/db/table-info.js +45 -0
- package/dist/db/temporal-value-guards.d.ts +6 -0
- package/dist/db/temporal-value-guards.js +15 -0
- package/dist/db/tenant-db-runner.d.ts +1 -0
- package/dist/db/tenant-db-runner.js +3 -0
- package/dist/db/tenant-db.d.ts +2 -2
- package/dist/db/tenant-db.js +48 -11
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +6 -2
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/access-roles.js +4 -3
- package/dist/engine/boot-validator/api-ext.js +14 -14
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +7 -6
- package/dist/engine/boot-validator/detail-screens.js +4 -4
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +77 -61
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/gdpr-storage.js +4 -4
- package/dist/engine/boot-validator/i18n-keys.js +6 -4
- package/dist/engine/boot-validator/index.d.ts +2 -2
- package/dist/engine/boot-validator/index.js +22 -20
- package/dist/engine/boot-validator/nav.js +5 -5
- package/dist/engine/boot-validator/ownership.js +12 -9
- package/dist/engine/boot-validator/parent-ref.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +258 -187
- package/dist/engine/boot-validator/projection-list-screens.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +1 -1
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +23 -17
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +12 -17
- package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
- package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
- package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
- package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
- package/dist/engine/boot-validator/screens-lists.js +26 -24
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +102 -869
- package/dist/engine/boot-validator/security-baseline.js +3 -2
- package/dist/engine/boot-validator/transfer-graph.js +2 -2
- package/dist/engine/boot-validator/workspaces.js +3 -3
- package/dist/engine/bridge-stub.js +3 -4
- package/dist/engine/build-app-schema.js +130 -170
- package/dist/engine/build-config-feature-schema.js +50 -50
- package/dist/engine/build-target.js +20 -23
- package/dist/engine/codemod/pipeline-codemod.js +20 -13
- package/dist/engine/config-helpers.js +4 -4
- package/dist/engine/constants.js +2 -2
- package/dist/engine/create-app.js +2 -2
- package/dist/engine/define-feature.js +21 -13
- package/dist/engine/define-handler.js +13 -11
- package/dist/engine/define-step.js +1 -1
- package/dist/engine/define-workflow.d.ts +5 -5
- package/dist/engine/define-workflow.js +6 -6
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +22 -34
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/event-def.js +2 -2
- package/dist/engine/extension-names.d.ts +16 -17
- package/dist/engine/extension-names.js +37 -38
- package/dist/engine/extensions/escape-hatch-usage.js +1 -1
- package/dist/engine/extensions/tenant-data.js +2 -2
- package/dist/engine/extensions/user-data.d.ts +23 -24
- package/dist/engine/factories.d.ts +14 -13
- package/dist/engine/factories.js +38 -37
- package/dist/engine/feature-ast/entity-field-types.js +1 -1
- package/dist/engine/feature-ast/extractors/handlers.js +2 -1
- package/dist/engine/feature-ast/extractors/hooks.js +1 -1
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
- package/dist/engine/feature-ast/extractors/shared.js +2 -3
- package/dist/engine/feature-ast/parse.js +13 -13
- package/dist/engine/feature-ast/patch.js +4 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +2 -1
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
- package/dist/engine/feature-ast/patterns.d.ts +11 -2
- package/dist/engine/feature-ast/patterns.js +43 -51
- package/dist/engine/feature-ast/render.js +50 -89
- package/dist/engine/feature-config-events-jobs.js +15 -12
- package/dist/engine/feature-entity-handlers.js +12 -5
- package/dist/engine/feature-manifest.d.ts +9 -9
- package/dist/engine/feature-manifest.js +6 -7
- package/dist/engine/feature-ui-extensions.d.ts +3 -3
- package/dist/engine/feature-ui-extensions.js +30 -22
- package/dist/engine/field-access.js +5 -6
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +4 -4
- package/dist/engine/object-form.d.ts +1 -0
- package/dist/engine/object-form.js +14 -4
- package/dist/engine/ownership.js +3 -3
- package/dist/engine/parse-ref-target.js +5 -7
- package/dist/engine/pattern-library/index.js +2 -2
- package/dist/engine/pattern-library/library.js +2 -2
- package/dist/engine/pattern-library/types.js +3 -3
- package/dist/engine/pipeline.js +2 -2
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +7 -5
- package/dist/engine/projection-helpers.js +9 -9
- package/dist/engine/qualified-name.js +2 -2
- package/dist/engine/registry-facade.js +1 -1
- package/dist/engine/registry-ingest.js +13 -13
- package/dist/engine/registry-state.js +17 -19
- package/dist/engine/registry-validate.js +12 -12
- package/dist/engine/run-in.js +2 -2
- package/dist/engine/run-pipeline.js +7 -0
- package/dist/engine/schema-builder.js +251 -233
- package/dist/engine/screen-filter-ops.js +17 -20
- package/dist/engine/screen-helpers.js +4 -3
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/state-machine.d.ts +12 -12
- package/dist/engine/state-machine.js +7 -7
- package/dist/engine/steps/_resolver-utils.js +4 -3
- package/dist/engine/steps/for-each.js +4 -4
- package/dist/engine/steps/unsafe-projection-delete.js +1 -1
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
- package/dist/engine/steps/wait-for-event.js +4 -4
- package/dist/engine/steps/workflow-system-events.js +3 -3
- package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
- package/dist/engine/tier-resolver-extension.d.ts +29 -27
- package/dist/engine/tier-resolver-extension.js +26 -26
- package/dist/engine/transfer-adjacency.js +2 -2
- package/dist/engine/validate-projection-allowlist.js +9 -28
- package/dist/engine/walk-step-instances.d.ts +2 -0
- package/dist/engine/walk-step-instances.js +38 -0
- package/dist/engine/where-rule-lint.js +1 -1
- package/dist/engine/write-handler-qn-extract.js +1 -1
- package/dist/entrypoint/index.d.ts +4 -1
- package/dist/entrypoint/index.js +10 -4
- package/dist/env/index.d.ts +7 -6
- package/dist/env/index.js +10 -9
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/errors/transition-details.js +7 -7
- package/dist/errors/write-error-info.d.ts +6 -6
- package/dist/errors/write-error-info.js +6 -6
- package/dist/es-ops/context.d.ts +9 -9
- package/dist/es-ops/context.js +29 -29
- package/dist/es-ops/index.js +7 -9
- package/dist/es-ops/operations-schema.js +4 -10
- package/dist/es-ops/runner.d.ts +11 -11
- package/dist/es-ops/runner.js +49 -53
- package/dist/es-ops/types.d.ts +55 -55
- package/dist/es-ops/types.js +11 -11
- package/dist/event-store/admin-api.js +7 -8
- package/dist/event-store/errors.js +1 -1
- package/dist/event-store/event-store.js +12 -6
- package/dist/event-store/rebuild-dead-letter.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +4 -1
- package/dist/files/feature.js +9 -10
- package/dist/files/file-handle.js +7 -9
- package/dist/files/file-ref-entity.js +15 -15
- package/dist/files/file-ref-table.js +7 -8
- package/dist/files/file-routes.d.ts +7 -0
- package/dist/files/file-routes.js +22 -13
- package/dist/files/in-memory-provider.js +8 -9
- package/dist/files/local-provider.js +18 -25
- package/dist/files/provider-resolver.d.ts +7 -7
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/storage-tracking.js +2 -2
- package/dist/files/upload-ledger.js +7 -0
- package/dist/files/zip-stream.d.ts +12 -19
- package/dist/files/zip-stream.js +44 -63
- package/dist/i18n/required-surface-keys.d.ts +1 -1
- package/dist/i18n/required-surface-keys.js +188 -212
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-context.d.ts +32 -0
- package/dist/jobs/job-runner-context.js +305 -0
- package/dist/jobs/job-runner-execution.d.ts +7 -0
- package/dist/jobs/job-runner-execution.js +160 -0
- package/dist/jobs/job-runner-gates.d.ts +19 -0
- package/dist/jobs/job-runner-gates.js +198 -0
- package/dist/jobs/job-runner-helpers.d.ts +17 -3
- package/dist/jobs/job-runner-helpers.js +69 -4
- package/dist/jobs/job-runner-schedulers.d.ts +1 -1
- package/dist/jobs/job-runner-schedulers.js +3 -4
- package/dist/jobs/job-runner-scope.d.ts +44 -0
- package/dist/jobs/job-runner-scope.js +102 -0
- package/dist/jobs/job-runner-types.d.ts +12 -0
- package/dist/jobs/job-runner.d.ts +0 -1
- package/dist/jobs/job-runner.js +57 -669
- package/dist/lifecycle/lifecycle.js +3 -5
- package/dist/lifecycle/signal-handlers.js +5 -4
- package/dist/logging/utils.d.ts +2 -1
- package/dist/logging/utils.js +38 -10
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/kumiko-drift.js +14 -13
- package/dist/migrations/pending-rebuilds.d.ts +21 -20
- package/dist/migrations/pending-rebuilds.js +27 -27
- package/dist/migrations/projection-table-index.d.ts +3 -3
- package/dist/migrations/projection-table-index.js +8 -8
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +12 -1
- package/dist/observability/metric-validator.js +1 -1
- package/dist/observability/metrics-handle.js +3 -3
- package/dist/pipeline/append-event-core.js +11 -2
- package/dist/pipeline/cascade-handler.d.ts +1 -1
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +2 -2
- package/dist/pipeline/dispatch-query.js +20 -13
- package/dist/pipeline/dispatch-shared.d.ts +14 -3
- package/dist/pipeline/dispatch-shared.js +67 -721
- package/dist/pipeline/dispatch-stream.js +1 -1
- package/dist/pipeline/dispatch-write.js +297 -190
- package/dist/pipeline/dispatcher-utils.d.ts +1 -1
- package/dist/pipeline/dispatcher-utils.js +1 -1
- package/dist/pipeline/event-consumer-state.js +2 -2
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +4 -6
- package/dist/pipeline/event-dispatcher.js +17 -28
- package/dist/pipeline/handler-context-auth.d.ts +9 -0
- package/dist/pipeline/handler-context-auth.js +54 -0
- package/dist/pipeline/handler-context-config.d.ts +14 -0
- package/dist/pipeline/handler-context-config.js +113 -0
- package/dist/pipeline/handler-context-db.d.ts +24 -0
- package/dist/pipeline/handler-context-db.js +149 -0
- package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
- package/dist/pipeline/handler-context-dispatch.js +89 -0
- package/dist/pipeline/handler-context-events.d.ts +43 -0
- package/dist/pipeline/handler-context-events.js +237 -0
- package/dist/pipeline/handler-context-notify.d.ts +12 -0
- package/dist/pipeline/handler-context-notify.js +55 -0
- package/dist/pipeline/handler-context-observability.d.ts +7 -0
- package/dist/pipeline/handler-context-observability.js +21 -0
- package/dist/pipeline/handler-context-scope.d.ts +48 -0
- package/dist/pipeline/handler-context-scope.js +88 -0
- package/dist/pipeline/irreversible-operation-gate.js +1 -1
- package/dist/pipeline/lifecycle-pipeline.js +9 -9
- package/dist/pipeline/member-read-only-transaction.js +1 -3
- package/dist/pipeline/msp-rebuild.js +1 -1
- package/dist/pipeline/notify-scope.js +12 -1
- package/dist/pipeline/pre-handler-gates.js +20 -4
- package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
- package/dist/pipeline/pre-transaction-reservations.js +9 -3
- package/dist/pipeline/projection-rebuild.js +15 -13
- package/dist/pipeline/system-hooks.js +39 -41
- package/dist/pipeline/system-identity-switch.js +1 -3
- package/dist/pipeline/tenant-timezone-cache.js +1 -1
- package/dist/random/generate.d.ts +29 -29
- package/dist/random/generate.js +30 -30
- package/dist/random/words.js +20 -20
- package/dist/rate-limit/middleware.js +9 -5
- package/dist/redis/client.js +2 -2
- package/dist/redis/pubsub-signal.js +14 -17
- package/dist/schema-cli/apply-command.d.ts +2 -0
- package/dist/schema-cli/apply-command.js +97 -0
- package/dist/schema-cli/baseline-command.d.ts +2 -0
- package/dist/schema-cli/baseline-command.js +33 -0
- package/dist/schema-cli/generate-command.d.ts +2 -0
- package/dist/schema-cli/generate-command.js +90 -0
- package/dist/schema-cli/shared.d.ts +39 -0
- package/dist/schema-cli/shared.js +95 -0
- package/dist/schema-cli/status-command.d.ts +2 -0
- package/dist/schema-cli/status-command.js +73 -0
- package/dist/schema-cli/validate-boot-input.d.ts +5 -0
- package/dist/schema-cli/validate-boot-input.js +130 -0
- package/dist/schema-cli/validate-command.d.ts +2 -0
- package/dist/schema-cli/validate-command.js +130 -0
- package/dist/schema-cli.d.ts +2 -18
- package/dist/schema-cli.js +47 -510
- package/dist/search/index.js +4 -4
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/search/purge-subject.js +4 -5
- package/dist/search/reindex-entity.js +6 -9
- package/dist/time/boot-tz-warning.d.ts +2 -2
- package/dist/time/boot-tz-warning.js +8 -8
- package/dist/time/iana.js +10 -10
- package/dist/time/index.js +9 -9
- package/dist/time/legacy-date.d.ts +2 -2
- package/dist/time/legacy-date.js +6 -8
- package/dist/time/polyfill.js +1 -1
- package/dist/time/tz-context.d.ts +1 -1
- package/dist/time/tz-context.js +1 -1
- package/dist/ui-types/app-schema.js +7 -7
- package/dist/ui-types/list-row-meta.js +4 -4
- package/dist/upgrade-cli.js +159 -130
- package/dist/utils/compare.js +1 -1
- package/package.json +5 -5
- package/src/changes.json +358 -0
- package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
- package/src/scripts/codemod/feature-layout.ts +10 -2
- package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
- package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
- package/src/scripts/codemod/object-form.ts +41 -4
- package/src/scripts/codemod/package-merge.ts +58 -8
- package/src/scripts/codemod/pii-personal-migration.ts +192 -153
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
|
@@ -6,16 +6,14 @@ import { access as accessPresets } from "../config-helpers.js";
|
|
|
6
6
|
import { parseRefTarget } from "../parse-ref-target.js";
|
|
7
7
|
import { isSelectOptionTone } from "./select-option-tone.js";
|
|
8
8
|
export const FILE_FIELD_TYPES = new Set(["file", "image", "files", "images"]);
|
|
9
|
-
// Field
|
|
10
|
-
// `userOwned` / `tenantOwned` / `allowPlaintext
|
|
11
|
-
//
|
|
9
|
+
// Field names that typically contain PII. Without a `pii: true` /
|
|
10
|
+
// `userOwned` / `tenantOwned` / `allowPlaintext` marker → boot warning.
|
|
11
|
+
// Compare in lower case for case-insensitive matching (displayName vs displayname).
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
// - `name`
|
|
15
|
-
// tenant.name, role.name)
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// Quelle: docs/plans/datenschutz/crypto-shredding.md Boot-Validation-Sektion.
|
|
13
|
+
// Deliberately NOT included:
|
|
14
|
+
// - `name` alone — too many business contexts (product.name,
|
|
15
|
+
// tenant.name, role.name) are not PII. Personal names are detected
|
|
16
|
+
// through displayName / firstName / lastName / fullName.
|
|
19
17
|
export const PII_DIRECT_NAME_HINTS = new Set([
|
|
20
18
|
"email",
|
|
21
19
|
"phone",
|
|
@@ -42,8 +40,8 @@ export const PII_DIRECT_NAME_HINTS = new Set([
|
|
|
42
40
|
"iban",
|
|
43
41
|
"bic",
|
|
44
42
|
]);
|
|
45
|
-
// Field
|
|
46
|
-
// User-Forget
|
|
43
|
+
// Field names that typically contain user-generated content —
|
|
44
|
+
// User-Forget must encrypt these with the author subject key.
|
|
47
45
|
export const PII_USER_OWNED_NAME_HINTS = new Set([
|
|
48
46
|
"body",
|
|
49
47
|
"text",
|
|
@@ -141,6 +139,7 @@ export function validateHandlerAccess(feature) {
|
|
|
141
139
|
for (const [name, handler] of Object.entries(feature.writeHandlers)) {
|
|
142
140
|
validateTenantlessAnonymous(feature.name, name, handler);
|
|
143
141
|
validateAdditionalRateLimits(feature.name, name, handler);
|
|
142
|
+
validateRateLimitCost(feature.name, name, handler);
|
|
144
143
|
validatePerCommandTransaction(feature.name, name, handler);
|
|
145
144
|
}
|
|
146
145
|
}
|
|
@@ -237,6 +236,22 @@ export function validateTenantlessAnonymous(featureName, handlerName, handler) {
|
|
|
237
236
|
`tenant/user bucket, so one caller could exhaust it for all. Use ${IP_SCOPED_RATE_LIMIT_PER.map((allowed) => `"${allowed}"`).join(" or ")}.`);
|
|
238
237
|
}
|
|
239
238
|
}
|
|
239
|
+
export function validateRateLimitCost(featureName, handlerName, handler) {
|
|
240
|
+
// skip: no payload-derived cost declared
|
|
241
|
+
if (!handler.rateLimitCost)
|
|
242
|
+
return;
|
|
243
|
+
if (handler.rateLimit === undefined || isRateLimitDisabled(handler.rateLimit)) {
|
|
244
|
+
throw new Error(`write handler "${featureName}:write:${handlerName}" declares rateLimitCost without an active rateLimit — ` +
|
|
245
|
+
`the cost is booked on the rateLimit buckets, so declare rateLimit or drop rateLimitCost.`);
|
|
246
|
+
}
|
|
247
|
+
const isAnonymous = "roles" in handler.access && handler.access.roles.includes("anonymous");
|
|
248
|
+
const hasIpBucket = declaredRateLimitOptions(handler.rateLimit).some(({ per }) => IP_SCOPED_RATE_LIMIT_PER.includes(per));
|
|
249
|
+
if (isAnonymous || hasIpBucket) {
|
|
250
|
+
throw new Error(`write handler "${featureName}:write:${handlerName}" declares rateLimitCost on an anonymous or ip-limited handler — ` +
|
|
251
|
+
`rateLimitCost books after the parse, so such a handler would lose its pre-parse protection against junk floods. ` +
|
|
252
|
+
`Bulk handlers are authenticated: use a user/tenant bucket and drop "anonymous", or drop rateLimitCost.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
240
255
|
export function validateRateLimitDisabledReason(featureName, kind, handlerName, rateLimit) {
|
|
241
256
|
// skip: rate limit isn't disabled, so no reason is required
|
|
242
257
|
if (!isRateLimitDisabled(rateLimit))
|
|
@@ -305,18 +320,18 @@ export function validateAnonymousRateLimit(featureName, kind, handlerName, acces
|
|
|
305
320
|
`Use rateLimit.per="ip" or "ip+handler" for anonymous endpoints.`);
|
|
306
321
|
}
|
|
307
322
|
}
|
|
308
|
-
// --- MultiStreamProjection delivery
|
|
323
|
+
// --- MultiStreamProjection delivery invariant ---
|
|
309
324
|
//
|
|
310
|
-
// `delivery: "per-instance"`
|
|
311
|
-
// N
|
|
312
|
-
//
|
|
313
|
-
// kumiko_event_consumers
|
|
314
|
-
//
|
|
325
|
+
// `delivery: "per-instance"` with a `table` is a semantic trap:
|
|
326
|
+
// N dispatcher instances would write the same INSERT/UPDATE rows in parallel
|
|
327
|
+
// (races / duplicates), and a rebuild would affect only one row in
|
|
328
|
+
// kumiko_event_consumers (the SHARED_INSTANCE_SENTINEL row),
|
|
329
|
+
// while live cursors reside in per-instance rows → cursor divergence.
|
|
315
330
|
//
|
|
316
|
-
//
|
|
317
|
-
// in-memory cache invalidation).
|
|
318
|
-
// shared delivery —
|
|
319
|
-
//
|
|
331
|
+
// The invariant is: per-instance consumers are side-effect-only (SSE,
|
|
332
|
+
// in-memory cache invalidation). Materializing a table requires
|
|
333
|
+
// shared delivery — it is exactly-once globally and gives the rebuild
|
|
334
|
+
// a single cursor to reset.
|
|
320
335
|
export function validateMultiStreamProjections(feature) {
|
|
321
336
|
for (const [name, msp] of Object.entries(feature.multiStreamProjections)) {
|
|
322
337
|
if (msp.delivery === "per-instance" && msp.table !== undefined) {
|
|
@@ -326,18 +341,19 @@ export function validateMultiStreamProjections(feature) {
|
|
|
326
341
|
}
|
|
327
342
|
}
|
|
328
343
|
}
|
|
329
|
-
// --- Located-
|
|
344
|
+
// --- Located-timestamp validation ---
|
|
330
345
|
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
346
|
+
// When a field with `type: "timestamp"` has a `locatedBy` marker, the
|
|
347
|
+
// referenced field must exist in the same entity AND have type `tz`.
|
|
348
|
+
// Otherwise neither the DB wrapper nor the JSON serializer knows which TZ
|
|
349
|
+
// belongs to the wall clock → silent data loss when reading in another
|
|
350
|
+
// server TZ.
|
|
335
351
|
//
|
|
336
|
-
//
|
|
352
|
+
// Manual construction is the most common source of conflicts:
|
|
337
353
|
// { foo: { type: "timestamp", locatedBy: "fooTz" } }
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
354
|
+
// without declaring the `fooTz` field. `createLocatedTimestampField()`
|
|
355
|
+
// instead creates ONE field of type "locatedTimestamp" — using it avoids
|
|
356
|
+
// this validator.
|
|
341
357
|
export function validateLocatedTimestamps(feature) {
|
|
342
358
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
343
359
|
const fields = entity.fields;
|
|
@@ -359,18 +375,18 @@ export function validateLocatedTimestamps(feature) {
|
|
|
359
375
|
}
|
|
360
376
|
}
|
|
361
377
|
}
|
|
362
|
-
// --- Entity-
|
|
378
|
+
// --- Entity-index validation ---
|
|
363
379
|
//
|
|
364
|
-
// entity.indexes
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
// in
|
|
368
|
-
// generate
|
|
380
|
+
// entity.indexes declares composite/unique indexes over multiple field
|
|
381
|
+
// columns. Common errors include misspelled field names, an empty column
|
|
382
|
+
// list, and an index on a field for which the DB has no column (file/image
|
|
383
|
+
// in the multi-value variant). Catch these at boot, long before drizzle-kit
|
|
384
|
+
// fails during the generate run.
|
|
369
385
|
//
|
|
370
|
-
// `tenantId
|
|
371
|
-
//
|
|
372
|
-
// (`["tenantId", "key"]`
|
|
373
|
-
//
|
|
386
|
+
// A `tenantId`-only index is redundant because buildEntityTable creates
|
|
387
|
+
// that index automatically. Composite forms remain allowed
|
|
388
|
+
// (`["tenantId", "key"]` is useful); only the tenantId-only single-column
|
|
389
|
+
// form is rejected.
|
|
374
390
|
export function validateEntityIndexes(feature) {
|
|
375
391
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
376
392
|
if (!entity.indexes)
|
|
@@ -397,21 +413,21 @@ export function validateEntityIndexes(feature) {
|
|
|
397
413
|
`a jsonb array cannot be indexed as a column. Use a single-value field or remove from the index.`);
|
|
398
414
|
}
|
|
399
415
|
if (field && field.type === "longText") {
|
|
400
|
-
// longText
|
|
401
|
-
// BTREE
|
|
402
|
-
// (PG
|
|
403
|
-
//
|
|
404
|
-
// longText
|
|
405
|
-
//
|
|
406
|
-
//
|
|
416
|
+
// longText semantically means "potentially-megabytes content" — a
|
|
417
|
+
// BTREE index on a 1-MB column is a performance disaster
|
|
418
|
+
// (PG would need to dereference TOAST pages for every
|
|
419
|
+
// index lookup). This is consistent with the type-level decision
|
|
420
|
+
// that longText has no sortable/searchable/filterable flags. Anyone
|
|
421
|
+
// who truly needs indexing should use `text` with the
|
|
422
|
+
// corresponding scaling trade-offs.
|
|
407
423
|
throw new Error(`${where}: column "${col}" is a longText field — these cannot be indexed. ` +
|
|
408
424
|
`Use \`text\` if you need indexing, or rely on the SearchAdapter (Meilisearch) for full-text search on long content.`);
|
|
409
425
|
}
|
|
410
426
|
}
|
|
411
|
-
// UNIQUE
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
// column
|
|
427
|
+
// A UNIQUE constraint on tenantId is semantically meaningful (1:1 tenant→entity)
|
|
428
|
+
// and NOT redundant — buildEntityTable's automatic index is only a
|
|
429
|
+
// performance hint, not a constraint. Only the tenantId-only,
|
|
430
|
+
// single-column, non-unique form is rejected.
|
|
415
431
|
if (def.columns.length === 1 && def.columns[0] === "tenantId" && !def.unique) {
|
|
416
432
|
throw new Error(`${where}: single-column index on "tenantId" is redundant — ` +
|
|
417
433
|
`buildEntityTable always creates one automatically. Remove this entry.`);
|
|
@@ -436,9 +452,9 @@ export function validateEncryptedFields(feature) {
|
|
|
436
452
|
let found = false;
|
|
437
453
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
438
454
|
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
439
|
-
//
|
|
440
|
-
// searchable/sortable
|
|
441
|
-
// (longText
|
|
455
|
+
// Both string-typed fields can be encrypted. The
|
|
456
|
+
// searchable/sortable conflict checks apply only to `text`
|
|
457
|
+
// (`longText` does not have these flags at the type level).
|
|
442
458
|
if (field.type !== "text" && field.type !== "longText")
|
|
443
459
|
continue;
|
|
444
460
|
if (!field.encrypted)
|
|
@@ -491,7 +507,7 @@ const NUMERIC_EMBEDDED_SUB_TYPES = new Set(["number", "money", "decimal"]);
|
|
|
491
507
|
function isValidEmbeddedDecimalScale(scale) {
|
|
492
508
|
return Number.isInteger(scale) && scale >= 0 && scale <= 15;
|
|
493
509
|
}
|
|
494
|
-
//
|
|
510
|
+
// ReferenceFieldDef validation, shared by
|
|
495
511
|
// top-level reference fields and reference sub-fields of an embedded field
|
|
496
512
|
// (only the field-path in error messages differs, e.g. "accountId" vs
|
|
497
513
|
// "lines.accountId", so a failure is locatable either way).
|
|
@@ -552,7 +568,7 @@ function validateReferenceTarget(entityName, fieldPath, refString, labelField, o
|
|
|
552
568
|
// (use-reference-lookup).
|
|
553
569
|
validateReferenceOptionsQuery(entityName, fieldPath, optionsQuery, feature, queryHandlers);
|
|
554
570
|
}
|
|
555
|
-
//
|
|
571
|
+
// The picker may source its options from a query handler instead of
|
|
556
572
|
// the referenced entity's table. A typo'd QN would only surface as a 404 on
|
|
557
573
|
// first Combobox open, so it is pinned at boot — same treatment as
|
|
558
574
|
// `DashboardFilterDefinition.optionsQuery` (boot-validator/query-refs.ts).
|
|
@@ -571,7 +587,7 @@ function validateReferenceOptionsQuery(entityName, fieldPath, optionsQuery, feat
|
|
|
571
587
|
`is declared via r.queryHandler(...).`);
|
|
572
588
|
}
|
|
573
589
|
}
|
|
574
|
-
//
|
|
590
|
+
// ReferenceFieldDef validation for top-level
|
|
575
591
|
// reference fields (self-reference, entity → entity, is allowed). The actual
|
|
576
592
|
// checks run in validateReferenceTarget — shared with the reference
|
|
577
593
|
// sub-fields of an embedded field (validateEmbeddedFields).
|
|
@@ -705,10 +721,10 @@ function validateEmbeddedListMetadata(fieldName, entityName, field, entity) {
|
|
|
705
721
|
}
|
|
706
722
|
// --- MultiSelect field validation ---
|
|
707
723
|
//
|
|
708
|
-
// options
|
|
709
|
-
// default —
|
|
710
|
-
//
|
|
711
|
-
//
|
|
724
|
+
// options must be non-empty (otherwise the field cannot be used), and
|
|
725
|
+
// default — when set — must be a subset of options. Both conditions would
|
|
726
|
+
// also fail at runtime in the Zod schema; boot validation only provides
|
|
727
|
+
// the earliest clear error messages.
|
|
712
728
|
// Exhaustive over MultiSelectFieldDef["columns"] (the compiler flags a union
|
|
713
729
|
// change); the TS union is gone at runtime for JS/JSON-sourced definitions.
|
|
714
730
|
const CHECKBOX_COLUMN_COUNTS = {
|
|
@@ -3,15 +3,46 @@ import { parseQn } from "../qualified-name.js";
|
|
|
3
3
|
import { formDerivationsOfSchema } from "../with-form-derivations.js";
|
|
4
4
|
const NUMERIC_FORM_FIELD_TYPES = ["money", "number", "decimal"];
|
|
5
5
|
const NUMERIC_SUB_FIELD_TYPES = ["money", "number", "decimal"];
|
|
6
|
+
function isMoneyType(type) {
|
|
7
|
+
return type === "money";
|
|
8
|
+
}
|
|
9
|
+
// Money operands are computed in minor units, number/decimal in major units: mixing them
|
|
10
|
+
// silently scales the result by 100 or 10000.
|
|
11
|
+
function validateUnitCompatibility({ prefix, screen, target }, derivation) {
|
|
12
|
+
const targetIsMoney = isMoneyType(screen.fields[target]?.type);
|
|
13
|
+
const moneyFlags = derivation.from.map((source) => isMoneyType(screen.fields[source]?.type));
|
|
14
|
+
const moneyCount = moneyFlags.filter(Boolean).length;
|
|
15
|
+
if (derivation.op === "sum" || derivation.op === "subtract") {
|
|
16
|
+
if (moneyCount !== 0 && moneyCount !== moneyFlags.length) {
|
|
17
|
+
throw new Error(`${prefix}.${target}: "${derivation.op}" sources must be all money or all number/decimal.`);
|
|
18
|
+
}
|
|
19
|
+
if (moneyCount > 0 !== targetIsMoney) {
|
|
20
|
+
throw new Error(`${prefix}.${target}: a "${derivation.op}" over ${moneyCount > 0 ? "money" : "number/decimal"} sources needs a ${moneyCount > 0 ? "money" : "number/decimal"} target.`);
|
|
21
|
+
}
|
|
22
|
+
// skip: sum/subtract fully validated above.
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (moneyCount > 1 || (derivation.op === "percent" && moneyFlags[1] === true)) {
|
|
26
|
+
throw new Error(`${prefix}.${target}: "${derivation.op}" takes at most one money source${derivation.op === "percent" ? " (the base, not the rate)" : ""}.`);
|
|
27
|
+
}
|
|
28
|
+
if ((moneyCount === 1) !== targetIsMoney) {
|
|
29
|
+
throw new Error(`${prefix}.${target}: the target must be money exactly when a "${derivation.op}" source is money.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
6
32
|
function validateFromList({ prefix, screen, target }, derivation) {
|
|
7
33
|
if (derivation.op === "percent" && derivation.from.length !== 2) {
|
|
8
34
|
throw new Error(`${prefix}.${target}: "percent" needs exactly two sources (base, rate).`);
|
|
9
35
|
}
|
|
10
36
|
for (const source of derivation.from) {
|
|
11
|
-
|
|
37
|
+
const sourceField = screen.fields[source];
|
|
38
|
+
if (sourceField === undefined) {
|
|
12
39
|
throw new Error(`${prefix}.${target}: unknown source field "${source}". Name a field of this screen.`);
|
|
13
40
|
}
|
|
41
|
+
if (!NUMERIC_FORM_FIELD_TYPES.includes(sourceField.type)) {
|
|
42
|
+
throw new Error(`${prefix}.${target}: source "${source}" must be money, number or decimal, not "${sourceField.type}".`);
|
|
43
|
+
}
|
|
14
44
|
}
|
|
45
|
+
validateUnitCompatibility({ prefix, screen, target }, derivation);
|
|
15
46
|
}
|
|
16
47
|
function validateListSum(check, derivation) {
|
|
17
48
|
const { prefix, screen, target } = check;
|
|
@@ -26,6 +57,9 @@ function validateListSum(check, derivation) {
|
|
|
26
57
|
if (cellType === undefined || !NUMERIC_SUB_FIELD_TYPES.includes(cellType)) {
|
|
27
58
|
throw new Error(`${prefix}.${target}: "${derivation.cell}" must be a money, number or decimal sub-field of "${derivation.list}".`);
|
|
28
59
|
}
|
|
60
|
+
if (isMoneyType(cellType) !== isMoneyType(screen.fields[target]?.type)) {
|
|
61
|
+
throw new Error(`${prefix}.${target}: a listSum of a ${isMoneyType(cellType) ? "money" : "number/decimal"} cell needs a ${isMoneyType(cellType) ? "money" : "number/decimal"} target.`);
|
|
62
|
+
}
|
|
29
63
|
const { whenEmpty } = derivation;
|
|
30
64
|
// skip: whenEmpty is optional
|
|
31
65
|
if (whenEmpty === undefined)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createFallbackLogger } from "../../logging/utils.js";
|
|
2
|
+
const log = createFallbackLogger("kumiko:boot");
|
|
1
3
|
// Providers whose bytes do not survive a process restart. Only "inmemory"
|
|
2
4
|
// today; extend if another ephemeral bundled provider lands.
|
|
3
5
|
const EPHEMERAL_PROVIDERS = new Set(["inmemory"]);
|
|
@@ -30,8 +32,7 @@ export function validateGdprStoragePersistence(features) {
|
|
|
30
32
|
}
|
|
31
33
|
const persistent = [...registeredProviders].filter((p) => !EPHEMERAL_PROVIDERS.has(p));
|
|
32
34
|
if (persistent.length === 0) {
|
|
33
|
-
|
|
34
|
-
console.warn("[kumiko:boot] user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount createFileProviderS3Feature() or createFileProviderS3Feature({ fromEnv: true }) and select it via the file-foundation provider config.");
|
|
35
|
+
log.warn("user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount createFileProviderS3Feature() or createFileProviderS3Feature({ fromEnv: true }) and select it via the file-foundation provider config.");
|
|
35
36
|
// skip: ephemeral store already warned; the s3-env env-var check below is moot
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
@@ -41,8 +42,7 @@ export function validateGdprStoragePersistence(features) {
|
|
|
41
42
|
if (persistent.length === 1 && persistent[0] === "s3-env") {
|
|
42
43
|
const missing = S3_ENV_REQUIRED.filter((v) => !process.env[v]);
|
|
43
44
|
if (missing.length > 0) {
|
|
44
|
-
|
|
45
|
-
console.warn(`[kumiko:boot] createFileProviderS3Feature({ fromEnv: true }) (provider "s3-env") is the GDPR file store but these env vars are unset: ${missing.join(", ")}. The provider throws on the first export (inside the cron), not here — set them so GDPR storage works.`);
|
|
45
|
+
log.warn(`createFileProviderS3Feature({ fromEnv: true }) (provider "s3-env") is the GDPR file store but these env vars are unset: ${missing.join(", ")}. The provider throws on the first export (inside the cron), not here — set them so GDPR storage works.`);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -11,7 +11,7 @@ function requiredKeysFromGeneratedConfigHub(schema) {
|
|
|
11
11
|
const out = new Set();
|
|
12
12
|
// The generator's section titles + mask-title field-label overrides are
|
|
13
13
|
// dot-form i18n references (`${feature}.settings`), never literal display
|
|
14
|
-
// text, so treatDotFormAsKey bypasses isI18nKey's colon-only check
|
|
14
|
+
// text, so treatDotFormAsKey bypasses isI18nKey's colon-only check.
|
|
15
15
|
for (const screen of schema.screens) {
|
|
16
16
|
for (const key of requiredKeysFromScreen(SETTINGS_HUB_FEATURE, screen, {
|
|
17
17
|
treatDotFormAsKey: true,
|
|
@@ -94,9 +94,11 @@ export function validateTranslationLocaleConsistency(features) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
export function validateRequiredLocales(features, requiredLocales) {
|
|
97
|
-
const gaps =
|
|
98
|
-
|
|
97
|
+
const gaps = requiredLocales === undefined || requiredLocales.length === 0
|
|
98
|
+
? []
|
|
99
|
+
: findRequiredLocaleGaps(features, requiredLocales);
|
|
100
|
+
if (requiredLocales !== undefined && gaps.length > 0) {
|
|
99
101
|
const lines = gaps.map((gap) => `[i18n] Feature "${gap.featureName}": translation key "${gap.key}" is missing required locale(s) ${gap.missingLocales.join(", ")}.`);
|
|
100
|
-
throw new Error(`${lines.join("\n")}\nThe app requires locales [${
|
|
102
|
+
throw new Error(`${lines.join("\n")}\nThe app requires locales [${requiredLocales.join(", ")}] for every translation key; load the matching locale package or add the missing translations.`);
|
|
101
103
|
}
|
|
102
104
|
}
|
|
@@ -9,14 +9,14 @@ export type ValidateBootOptions = {
|
|
|
9
9
|
/** Warn when an access role is used by exactly one handler/config-key/
|
|
10
10
|
* field across the whole boot scan — often a typo, but also the normal
|
|
11
11
|
* shape of a legitimate fine-grained role (one role scoped to one admin
|
|
12
|
-
* endpoint on purpose). Opt-in (default false
|
|
12
|
+
* endpoint on purpose). Opt-in (default false): a default-on
|
|
13
13
|
* prod warning that nobody can silence per-role isn't worth the noise
|
|
14
14
|
* it generates on every boot. */
|
|
15
15
|
readonly warnOnUniqueAccessRoles?: boolean;
|
|
16
16
|
/** QNs the app's sidebar allowlist admits. When set, every declared nav
|
|
17
17
|
* whose screen isn't reachable from any allowlisted nav entry gets a
|
|
18
18
|
* boot-time warning — that screen would otherwise be silently
|
|
19
|
-
* unreachable via the sidebar
|
|
19
|
+
* unreachable via the sidebar. When omitted and the app has
|
|
20
20
|
* workspaces, the allowlist is derived from `r.workspace({ nav })` and
|
|
21
21
|
* `r.nav({ workspaces })` assignments instead. */
|
|
22
22
|
readonly navAllowlist?: ReadonlySet<string>;
|
|
@@ -2,6 +2,7 @@ import { validateEntityFieldEncryptionAvailable } from "../../db/entity-field-en
|
|
|
2
2
|
import { dedupeFeatures } from "../dedupe-features.js";
|
|
3
3
|
import { FILE_STORAGE_PROVIDER_ENV } from "../extension-names.js";
|
|
4
4
|
import { QnTypes, qualifyEntityName } from "../qualified-name.js";
|
|
5
|
+
import { validateProjectionAllowlist } from "../validate-projection-allowlist.js";
|
|
5
6
|
import { validateAccessDeclarations } from "./access-declarations.js";
|
|
6
7
|
import { warnOnUniqueAccessRoles } from "./access-roles.js";
|
|
7
8
|
import { validateActionWiring, validateFieldWiring } from "./action-wiring.js";
|
|
@@ -30,9 +31,9 @@ import { validateExtensionSelectorPanels } from "./selector-owner-panels.js";
|
|
|
30
31
|
import { validateTransferGraph } from "./transfer-graph.js";
|
|
31
32
|
import { validateSubmitPrefilledHiddenFields } from "./url-prefill-fields.js";
|
|
32
33
|
import { collectWorkspaceQns, resolveNavAllowlist, validateDefaultWorkspaceUniqueness, validateWorkspaces, } from "./workspaces.js";
|
|
33
|
-
// Re-export:
|
|
34
|
-
//
|
|
35
|
-
//
|
|
34
|
+
// Re-export: run-dev-app.ts needs this to pass write-handler QNs
|
|
35
|
+
// to codegen. It is not part of validateBoot, but uses the same
|
|
36
|
+
// extraction logic.
|
|
36
37
|
export { collectCardMetaOverflow } from "./card-meta-overflow.js";
|
|
37
38
|
export { validateAppCustomScreenWriteQns } from "./custom-screen-write-qns.js";
|
|
38
39
|
export { collectWriteHandlerQns } from "./nav.js";
|
|
@@ -101,13 +102,13 @@ export function validateBoot(rawFeatures, options) {
|
|
|
101
102
|
}
|
|
102
103
|
// Collect all config keys across features (for cross-feature reference validation)
|
|
103
104
|
const allConfigKeys = new Set();
|
|
104
|
-
// Qualified config-key set
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// `…:config:branding-title`;
|
|
109
|
-
// multi-word
|
|
110
|
-
// `feature.short
|
|
105
|
+
// Qualified config-key set for ConfigEditScreen validation. MUST be
|
|
106
|
+
// canonicalized via qualifyEntityName (toKebab on feature + key), exactly
|
|
107
|
+
// as define-feature/registry builds the QN. The raw f.configKeys key is
|
|
108
|
+
// the camelCase object key (`brandingTitle`), but the actual QN is
|
|
109
|
+
// `…:config:branding-title`; without toKebab, every configEdit screen
|
|
110
|
+
// with a multi-word config key would fail incorrectly. allConfigKeys above
|
|
111
|
+
// uses the older `feature.short` format for validateConfigReads.
|
|
111
112
|
const configKeyDefsByQn = new Map();
|
|
112
113
|
for (const f of features) {
|
|
113
114
|
for (const [key, def] of Object.entries(f.configKeys)) {
|
|
@@ -138,15 +139,15 @@ export function validateBoot(rawFeatures, options) {
|
|
|
138
139
|
const screensByShortId = collectScreensByShortId(features);
|
|
139
140
|
validateScreenShortIdCollisions(screensByShortId);
|
|
140
141
|
// Cross-feature entity → detailFor-screen map — built (+ uniqueness-
|
|
141
|
-
// validated) up front so per-feature rowAction entity-targets
|
|
142
|
+
// validated) up front so per-feature rowAction entity-targets
|
|
142
143
|
// resolve against it inside the loop below, instead of at the very end
|
|
143
144
|
// where this used to run.
|
|
144
145
|
const detailForScreens = collectDetailForScreens(features, featureMap);
|
|
145
|
-
// Cross-feature API exposure
|
|
146
|
-
// r.exposesApi(name). Per-feature validateApiExposureMatching
|
|
147
|
-
// usedApis
|
|
148
|
-
//
|
|
149
|
-
//
|
|
146
|
+
// Cross-feature API exposure map — each feature declares markers via
|
|
147
|
+
// r.exposesApi(name). Per-feature validateApiExposureMatching walks the
|
|
148
|
+
// usedApis set and checks that every entry has a match here.
|
|
149
|
+
// This prevents typoed or removed QN calls from causing a runtime crash
|
|
150
|
+
// instead of a boot failure.
|
|
150
151
|
const allExposedApis = new Map(); // apiName → providerFeature
|
|
151
152
|
for (const f of features) {
|
|
152
153
|
for (const apiName of f.exposedApis) {
|
|
@@ -200,10 +201,10 @@ export function validateBoot(rawFeatures, options) {
|
|
|
200
201
|
validateParentRefs(feature, featureMap);
|
|
201
202
|
validateTransferGraph(feature, featureMap);
|
|
202
203
|
validateMultiStreamProjections(feature);
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
// Function
|
|
204
|
+
// Before validateScreens: its visible/entityId field-reference checks throw
|
|
205
|
+
// for a Function value first, with the confusing "unknown field undefined"
|
|
206
|
+
// message (because `typeof fn !== "boolean"` is true), before the clear
|
|
207
|
+
// Function check here can run.
|
|
207
208
|
validateActionWiring(feature);
|
|
208
209
|
validateFieldWiring(feature);
|
|
209
210
|
validateScreens(feature, featureMap, allWriteHandlerQns, allScreenQns, configKeyDefsByQn, screensByShortId, detailForScreens);
|
|
@@ -238,6 +239,7 @@ export function validateBoot(rawFeatures, options) {
|
|
|
238
239
|
validateGdprStoragePersistence(features);
|
|
239
240
|
validateGlobalTenancyEntities(features);
|
|
240
241
|
validateFeatureBootChecks(features);
|
|
242
|
+
validateProjectionAllowlist(features);
|
|
241
243
|
if (hasEncryptedFields) {
|
|
242
244
|
// Availability check, not env-presence: eagerly building the keyring
|
|
243
245
|
// catches malformed keys (wrong length, bad base64) at boot instead of
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Nav validation + collectKnownRoles (role synthesis for ownership
|
|
2
2
|
// validation in index.ts — thematically adjacent to ownership.ts, kept
|
|
3
3
|
// here since this is a structural move, not a semantic reorg).
|
|
4
|
+
import { createFallbackLogger } from "../../logging/utils.js";
|
|
4
5
|
import { qualifyEntityName } from "../qualified-name.js";
|
|
5
6
|
import { isOpenToAllGranted } from "../types/index.js";
|
|
7
|
+
const log = createFallbackLogger("kumiko:boot");
|
|
6
8
|
export function collectWriteHandlerQns(features) {
|
|
7
9
|
const set = new Set();
|
|
8
10
|
for (const f of features) {
|
|
@@ -110,7 +112,7 @@ function navViewerRoles(access) {
|
|
|
110
112
|
return isOpenToAllGranted(access) ? undefined : [];
|
|
111
113
|
return access.roles;
|
|
112
114
|
}
|
|
113
|
-
//
|
|
115
|
+
// Warn (never throw — this is a real UX bug, not a broken ref)
|
|
114
116
|
// when a nav leaf's role gate is disjoint from its parent section's — no
|
|
115
117
|
// user who can see the section could ever see this child, so the section
|
|
116
118
|
// renders with zero visible children for every user who reaches it.
|
|
@@ -133,8 +135,7 @@ export function warnOnNavAccessInversion(allNavQns) {
|
|
|
133
135
|
continue; // parent already visible to nobody — a different problem
|
|
134
136
|
if (leafRoles.some((role) => parentRoles.includes(role)))
|
|
135
137
|
continue;
|
|
136
|
-
|
|
137
|
-
console.warn(`[kumiko:boot] Nav entry "${qn}" requires roles [${leafRoles.join(", ")}] but its parent ` +
|
|
138
|
+
log.warn(`Nav entry "${qn}" requires roles [${leafRoles.join(", ")}] but its parent ` +
|
|
138
139
|
`"${navDef.parent}" only allows [${parentRoles.join(", ")}] — no user who can see the ` +
|
|
139
140
|
`parent section could ever see this entry. If this is intentional, ignore this warning; ` +
|
|
140
141
|
`otherwise align the role sets.`);
|
|
@@ -158,8 +159,7 @@ export function warnOnUnreachableNavScreens(allNavQns, allowedNavQns, navAllowli
|
|
|
158
159
|
const remedy = allowlistSource === "workspaces"
|
|
159
160
|
? "add it to a workspace's `nav` array or set `workspaces` on the nav"
|
|
160
161
|
: "add it to the allowlist";
|
|
161
|
-
|
|
162
|
-
console.warn(`[kumiko:boot] Nav entry "${qn}" declared by feature "${navDef.featureName}" points at ` +
|
|
162
|
+
log.warn(`Nav entry "${qn}" declared by feature "${navDef.featureName}" points at ` +
|
|
163
163
|
`screen "${screenQn}", which no allowlisted nav entry reaches — it is unreachable via ` +
|
|
164
164
|
`the sidebar. If this is intentional (e.g. reachable through a generated hub), add ` +
|
|
165
165
|
`"${qn}" to navAllowlistExempt; otherwise ${remedy}.`);
|
|
@@ -3,23 +3,23 @@ import { tableNameOf, } from "../ownership.js";
|
|
|
3
3
|
import { SYSTEM_USER_ID } from "../system-user.js";
|
|
4
4
|
import { SYSTEM_TENANT_ID } from "../types/identifiers.js";
|
|
5
5
|
import { assertQualifiedWhereFragment, tableColumnSqlNames } from "../where-rule-lint.js";
|
|
6
|
-
// --- Ownership rule validation
|
|
6
|
+
// --- Ownership rule validation ---
|
|
7
7
|
//
|
|
8
8
|
// Walks every entity.access and every field.access map, resolves each
|
|
9
9
|
// FromRule against the cross-feature claim registry, and confirms the
|
|
10
10
|
// referenced column exists on the entity. Catches typos, renames, and
|
|
11
11
|
// cross-feature-claim-removal scenarios at boot instead of at request time.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
13
|
+
// Also probes every `{ kind: "where" }` rule against the where-rule
|
|
14
14
|
// lint (see ../where-rule-lint.ts) so a fail-open unqualified-column bug
|
|
15
15
|
// fails the boot instead of waiting for a request from the right role.
|
|
16
16
|
//
|
|
17
17
|
// Field-level `access.read` rejects where-rules too: filterFieldsForRead
|
|
18
18
|
// evaluates it in memory and userCanReadFieldRow fails closed on a where-rule.
|
|
19
19
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
20
|
+
// `access.write` maps reject `{ kind: "where" }` outright. A where-rule is
|
|
21
|
+
// raw SQL and the write path never reaches SQL — it evaluates rules in memory
|
|
22
|
+
// against the concrete row (userCanCreateFieldRow /
|
|
23
23
|
// userCanWriteFieldRow), and a create has no row to run a predicate against
|
|
24
24
|
// at all. Such a rule can therefore only ever deny, so the boot fails instead
|
|
25
25
|
// of shipping an access map that silently locks the role out.
|
|
@@ -49,9 +49,8 @@ export function collectClaimKeys(features) {
|
|
|
49
49
|
// corpora go in as undefined — a three-feature stack genuinely does not know
|
|
50
50
|
// which roles and claims the whole app declares, and answering that question
|
|
51
51
|
// from a slice produces false "unknown role"/"unknown claim" errors. The
|
|
52
|
-
// subset-invariant checks (
|
|
53
|
-
//
|
|
54
|
-
// mounted.
|
|
52
|
+
// subset-invariant checks (where-lint, where-on-write, unknown column) run in
|
|
53
|
+
// full: those hold or fail per feature, whatever else is mounted.
|
|
55
54
|
export function validateOwnershipBoot(features) {
|
|
56
55
|
for (const feature of features) {
|
|
57
56
|
validateOwnershipRules(feature, undefined, undefined);
|
|
@@ -258,12 +257,16 @@ export function probeWhereRule(rule, roleName, probe, scope, featureName) {
|
|
|
258
257
|
return;
|
|
259
258
|
assertQualifiedWhereFragment(fragment.sqlText, probe.columns, `${scope} (role "${roleName}", feature: "${featureName}")`);
|
|
260
259
|
}
|
|
260
|
+
// Array.isArray does not narrow a readonly array out of a union.
|
|
261
|
+
function isRoleList(access) {
|
|
262
|
+
return Array.isArray(access);
|
|
263
|
+
}
|
|
261
264
|
// Embedded sub-fields are filtered in memory too (filterFieldsForRead), so a
|
|
262
265
|
// where-rule there is just as unevaluable as on a top-level field.
|
|
263
266
|
function rejectWhereInEmbeddedRead(schema, parentScope, featureName) {
|
|
264
267
|
for (const [subName, sub] of Object.entries(schema)) {
|
|
265
268
|
const map = sub.access?.read;
|
|
266
|
-
if (!map ||
|
|
269
|
+
if (!map || isRoleList(map))
|
|
267
270
|
continue;
|
|
268
271
|
for (const [roleName, rule] of Object.entries(map)) {
|
|
269
272
|
if (rule !== "all" && rule.kind === "where") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
-
// --- Parent-ref boot validation
|
|
2
|
+
// --- Parent-ref boot validation ---
|
|
3
3
|
//
|
|
4
4
|
// Join-row entities (EntityDefinition.parentRef) derive both the write-path
|
|
5
5
|
// deny and the read-path SQL gate from one declaration. This is the boot-time
|