@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
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { createFallbackLogger } from "../../logging/utils.js";
|
|
1
2
|
import { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./entity-handler.js";
|
|
2
|
-
|
|
3
|
-
//
|
|
3
|
+
const log = createFallbackLogger("kumiko:boot");
|
|
4
|
+
// Framework-managed timestamp fields may be used as retention.reference
|
|
5
|
+
// even when they are not declared in entity.fields.
|
|
4
6
|
const FRAMEWORK_TIMESTAMP_FIELDS = new Set([
|
|
5
7
|
"createdAt",
|
|
6
8
|
"updatedAt",
|
|
7
9
|
"lastSeenAt",
|
|
8
10
|
"deletedAt",
|
|
9
11
|
]);
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// werden statt erst beim ersten Cleanup-Run.
|
|
12
|
+
// Allowed format for retention.keepFor: a number followed by a suffix (h/d/w/m/y).
|
|
13
|
+
// The cleanup job performs the actual parsing; the boot validator only checks
|
|
14
|
+
// the format so typos ("30days") become visible before the first cleanup run.
|
|
14
15
|
const KEEP_FOR_PATTERN = /^\d+[hdwmy]$/;
|
|
15
16
|
const FILE_FIELD_TYPES = new Set([
|
|
16
17
|
"file",
|
|
@@ -19,7 +20,7 @@ const FILE_FIELD_TYPES = new Set([
|
|
|
19
20
|
"images",
|
|
20
21
|
]);
|
|
21
22
|
// Excludes subjectRef: its `personal: "ref"` union member structurally
|
|
22
|
-
// forbids `anonymize` (packages/types/src/fields.ts)
|
|
23
|
+
// forbids `anonymize` (packages/types/src/fields.ts).
|
|
23
24
|
function hasAnonymizableSubjectField(annot) {
|
|
24
25
|
return Boolean(annot.pii || annot.userOwned || annot.tenantOwned || annot.recordOwned);
|
|
25
26
|
}
|
|
@@ -42,8 +43,9 @@ const WRITE_ONLY_FORBIDDEN_FLAGS = [
|
|
|
42
43
|
"filterable",
|
|
43
44
|
"lookupable",
|
|
44
45
|
];
|
|
45
|
-
function validateWriteOnlyField(
|
|
46
|
-
const
|
|
46
|
+
function validateWriteOnlyField(input) {
|
|
47
|
+
const { field } = input;
|
|
48
|
+
const where = fieldLocation(input);
|
|
47
49
|
if (field.type === "embedded") {
|
|
48
50
|
for (const [subName, subField] of Object.entries(field.schema)) {
|
|
49
51
|
if ("writeOnly" in subField && subField.writeOnly === true) {
|
|
@@ -81,8 +83,12 @@ const JSONB_PERSONAL_FORBIDDEN_FLAGS = [
|
|
|
81
83
|
"lookupable",
|
|
82
84
|
];
|
|
83
85
|
const CUSTOM_FIELDS_COLUMN = "customFields";
|
|
84
|
-
function validatePersonalJsonbField(
|
|
85
|
-
const
|
|
86
|
+
function validatePersonalJsonbField(input) {
|
|
87
|
+
const { entity, fieldName, field } = input;
|
|
88
|
+
// skip: only personal jsonb fields are stored as a ciphertext scalar
|
|
89
|
+
if (field.type !== "jsonb" || input.subjectAnnotationCount === 0)
|
|
90
|
+
return;
|
|
91
|
+
const where = fieldLocation(input);
|
|
86
92
|
const reason = "a personal jsonb field is stored as a ciphertext string, so nothing inside it can be indexed, filtered, sorted, searched or merged";
|
|
87
93
|
const flags = field; // @cast-boundary schema-walk
|
|
88
94
|
for (const flag of JSONB_PERSONAL_FORBIDDEN_FLAGS) {
|
|
@@ -100,188 +106,253 @@ function validatePersonalJsonbField(featureName, entityName, entity, fieldName,
|
|
|
100
106
|
}
|
|
101
107
|
// --- PII / Subject-Key Annotations + Retention validation ---
|
|
102
108
|
//
|
|
103
|
-
//
|
|
109
|
+
// Four classes of checks:
|
|
104
110
|
//
|
|
105
|
-
// 1. Mutual exclusion:
|
|
106
|
-
// (pii / userOwned / tenantOwned).
|
|
107
|
-
//
|
|
111
|
+
// 1. Mutual exclusion: each field may have only ONE subject annotation
|
|
112
|
+
// (pii / userOwned / tenantOwned). Multiple annotations are semantically
|
|
113
|
+
// contradictory because each field belongs to exactly one subject key.
|
|
108
114
|
//
|
|
109
|
-
// 2. Reference
|
|
110
|
-
// reference
|
|
111
|
-
//
|
|
112
|
-
// Encrypt-Aufruf.
|
|
115
|
+
// 2. Reference integrity: userOwned.ownerField must refer to an existing
|
|
116
|
+
// reference field that should target the user entity. This catches typos
|
|
117
|
+
// and refactors that removed a field during boot instead of at encryption.
|
|
113
118
|
//
|
|
114
|
-
// 3.
|
|
115
|
-
// (email, name, phone, body, etc.)
|
|
116
|
-
//
|
|
119
|
+
// 3. Heuristic warnings: field names that typically contain PII
|
|
120
|
+
// (email, name, phone, body, etc.) without an annotation produce a boot
|
|
121
|
+
// warning. `{ personal: false, reason: "<reason>" }` suppresses it and
|
|
122
|
+
// records the reason in the audit.
|
|
117
123
|
//
|
|
118
|
-
// 4. Retention
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
124
|
+
// 4. Retention integrity: retention.reference, when set, must refer to an
|
|
125
|
+
// existing field or a framework timestamp. retention.strategy="blockDelete"
|
|
126
|
+
// without anonymizable fields is ineffective because user forget cannot
|
|
127
|
+
// remove anything, so it produces a warning.
|
|
122
128
|
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
129
|
+
// Encryption and decryption are handled elsewhere; this validation runs early
|
|
130
|
+
// so schema drift is detected during boot.
|
|
125
131
|
export function validatePiiAndRetention(feature) {
|
|
126
132
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
133
|
+
validateFieldAnnotations(feature.name, entityName, entity);
|
|
134
|
+
validateEntityRetention(feature.name, entityName, entity);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Order matters: the first failing rule decides the boot error, and warnings
|
|
138
|
+
// are emitted in this order.
|
|
139
|
+
const FIELD_RULES = [
|
|
140
|
+
rejectMultipleSubjectAnnotations,
|
|
141
|
+
rejectTextWithoutPersonalStance,
|
|
142
|
+
rejectInvalidLookupable,
|
|
143
|
+
rejectSensitiveWithoutCiphertextAtRest,
|
|
144
|
+
validateWriteOnlyField,
|
|
145
|
+
rejectCiphertextSortOrSensitiveSearch,
|
|
146
|
+
validatePersonalJsonbField,
|
|
147
|
+
validatePersonalOfOwner,
|
|
148
|
+
warnOnUnannotatedPersonalName,
|
|
149
|
+
];
|
|
150
|
+
function validateFieldAnnotations(featureName, entityName, entity) {
|
|
151
|
+
const personalOfOwnerFieldNames = ownerFieldNamesReferencedByPersonalOf(entity.fields);
|
|
152
|
+
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
153
|
+
const flags = field; // @cast-boundary schema-walk
|
|
154
|
+
const input = {
|
|
155
|
+
featureName,
|
|
156
|
+
entityName,
|
|
157
|
+
entity,
|
|
158
|
+
fieldName,
|
|
159
|
+
field,
|
|
160
|
+
flags,
|
|
161
|
+
subjectAnnotationCount: countSubjectAnnotations(flags),
|
|
162
|
+
personalOfOwnerFieldNames,
|
|
163
|
+
};
|
|
164
|
+
for (const rule of FIELD_RULES)
|
|
165
|
+
rule(input);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function countSubjectAnnotations(flags) {
|
|
169
|
+
return [flags.pii, flags.userOwned, flags.tenantOwned, flags.recordOwned].filter(Boolean).length;
|
|
170
|
+
}
|
|
171
|
+
function fieldLocation(input) {
|
|
172
|
+
return `[Feature ${input.featureName}] Field "${input.fieldName}" on entity "${input.entityName}"`;
|
|
173
|
+
}
|
|
174
|
+
function rejectMultipleSubjectAnnotations(input) {
|
|
175
|
+
if (input.subjectAnnotationCount > 1) {
|
|
176
|
+
throw new Error(`${fieldLocation(input)} has multiple subject-key annotations (personal: "self" / "tenant" / { of: "<field>" }). Pick one — each field belongs to exactly one subject.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function rejectTextWithoutPersonalStance(input) {
|
|
180
|
+
const { field, flags } = input;
|
|
181
|
+
if ((field.type === "text" || field.type === "longText") &&
|
|
182
|
+
input.subjectAnnotationCount === 0 &&
|
|
183
|
+
!flags.allowPlaintext &&
|
|
184
|
+
!flags.subjectRef) {
|
|
185
|
+
throw new Error(`${fieldLocation(input)} (type "${field.type}") declares no personal stance. Build it with createTextField / createLongTextField and { personal: "self" | "tenant" | "ref" | { of: "<ownerField>" } | false }; false additionally needs { reason: "..." } stating why the value is not personal data.`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// lookupable: only text fields with a subject annotation are supported. Without
|
|
189
|
+
// a subject, the value remains plaintext, so a blind index would be a
|
|
190
|
+
// second deterministic copy without any benefit.
|
|
191
|
+
function rejectInvalidLookupable(input) {
|
|
192
|
+
// skip: only lookupable fields carry a blind index to validate
|
|
193
|
+
if (input.flags.lookupable !== true)
|
|
194
|
+
return;
|
|
195
|
+
if (input.field.type !== "text") {
|
|
196
|
+
throw new Error(`${fieldLocation(input)} declares find: "exact" (or "fuzzy") but has type "${input.field.type}" — blind-index equality lookups only apply to text fields. Use a text field or set find: "none".`);
|
|
197
|
+
}
|
|
198
|
+
if (input.subjectAnnotationCount === 0) {
|
|
199
|
+
throw new Error(`${fieldLocation(input)} declares find: "exact" (or "fuzzy") without a subject annotation (personal: "self" / "tenant" / { of: "<field>" }). Plaintext fields don't need a blind index — add personal or set find: "none".`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Sensitive fields are stored as table ciphertext in the event log; without
|
|
203
|
+
// ciphertext at rest, appending the event would write plaintext to the
|
|
204
|
+
// immutable history.
|
|
205
|
+
function rejectSensitiveWithoutCiphertextAtRest(input) {
|
|
206
|
+
const { flags } = input;
|
|
207
|
+
if (flags.sensitive === true && input.subjectAnnotationCount === 0 && flags.encrypted !== true) {
|
|
208
|
+
throw new Error(`${fieldLocation(input)} declares { sensitive: true } without ciphertext-at-rest. Since #967 the event log stores sensitive fields as table ciphertext — add a subject annotation (personal: "self" / "tenant" / { of: "<field>" }) or { encrypted: true }.`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Sorting reads the projection column — that stays ciphertext, so
|
|
212
|
+
// sortable + subject annotation stays a boot-fail. searchable is allowed:
|
|
213
|
+
// the search consumer decrypts into the derived index and forget purges
|
|
214
|
+
// those docs (see createSearchEventConsumer). sensitive + searchable stays
|
|
215
|
+
// forbidden (never searchable).
|
|
216
|
+
function rejectCiphertextSortOrSensitiveSearch(input) {
|
|
217
|
+
const { flags } = input;
|
|
218
|
+
if (input.subjectAnnotationCount > 0 && flags.sortable === true) {
|
|
219
|
+
throw new Error(`${fieldLocation(input)} combines a subject-key annotation with { sortable: true } — sorting reads the projection column, which is ciphertext at rest. For equality lookups use find: "exact"; drop sortable or keep the field plaintext (personal: false).`);
|
|
220
|
+
}
|
|
221
|
+
if (flags.sensitive === true && flags.searchable === true) {
|
|
222
|
+
throw new Error(`${fieldLocation(input)} combines { sensitive: true } with { searchable: true } — sensitive means ciphertext at rest, never searchable, stripped from event echoes — authorized readers can still read it back (passwords, tokens, tax IDs). Subject-annotated identity fields may be searchable (#1610); sensitive fields may not. To never return the value at all, add { writeOnly: true }.`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function validatePersonalOfOwner(input) {
|
|
226
|
+
const personalOf = input.flags.userOwned;
|
|
227
|
+
// skip: only personal: { of } fields reference an owner field
|
|
228
|
+
if (!personalOf)
|
|
229
|
+
return;
|
|
230
|
+
const ownerName = personalOf.ownerField;
|
|
231
|
+
if (!ownerName || typeof ownerName !== "string") {
|
|
232
|
+
throw new Error(`${fieldLocation(input)} has personal: { of: ... } without an owner field name. Pass the name of the owner field: personal: { of: "<ownerField>" }.`);
|
|
233
|
+
}
|
|
234
|
+
const ownerField = requireOwnerField(input, ownerName);
|
|
235
|
+
rejectOwnerWithoutRowFallback(input, ownerName, ownerField, personalOf);
|
|
236
|
+
warnOnNonUserOwnerReference(input, ownerName, ownerField);
|
|
237
|
+
}
|
|
238
|
+
function requireOwnerField(input, ownerName) {
|
|
239
|
+
const fieldsByName = input.entity.fields;
|
|
240
|
+
const ownerField = fieldsByName[ownerName];
|
|
241
|
+
if (!ownerField) {
|
|
242
|
+
const known = Object.keys(fieldsByName).sort().join(", ");
|
|
243
|
+
throw new Error(`${fieldLocation(input)} references personal.of "${ownerName}" but no such field exists. Known fields: ${known}`);
|
|
244
|
+
}
|
|
245
|
+
// Text is accepted alongside reference: the ES-framework carries
|
|
246
|
+
// user ids as plain text columns throughout the bundled features
|
|
247
|
+
// (user-session.userId, invitation.invitedBy) — there is no
|
|
248
|
+
// relational FK to point a reference at. Self-referencing
|
|
249
|
+
// ownerField (the field's own value IS the owner id) rides on this.
|
|
250
|
+
if (ownerField.type !== "reference" && ownerField.type !== "text") {
|
|
251
|
+
throw new Error(`[Feature ${input.featureName}] personal.of "${ownerName}" on entity "${input.entityName}" must be a reference or text (userId) field, got type "${ownerField.type}"`);
|
|
252
|
+
}
|
|
253
|
+
return ownerField;
|
|
254
|
+
}
|
|
255
|
+
function rejectOwnerWithoutRowFallback(input, ownerName, ownerField, personalOf) {
|
|
256
|
+
const { whenAbsent } = personalOf;
|
|
257
|
+
if (whenAbsent !== undefined && whenAbsent !== "tenant" && whenAbsent !== "self") {
|
|
258
|
+
throw new Error(`${fieldLocation(input)} declares personal.whenAbsent "${String(whenAbsent)}" — entity fields only support whenAbsent: "tenant" or "self" (no plaintext fallback for stored PII). Remove whenAbsent or set it to "tenant" (tenant key) or "self" (the row's own record key).`);
|
|
259
|
+
}
|
|
260
|
+
if (ownerField.required !== true && whenAbsent === undefined) {
|
|
261
|
+
throw new Error(`${fieldLocation(input)} is personal: { of: "${ownerName}" }, but "${ownerName}" is not required — rows without an owner would fail to encrypt (SubjectResolutionError, HTTP 500) on update, delete and forget. Declare { personal: { of: "${ownerName}", whenAbsent: "tenant" } } to encrypt those rows under the tenant key, whenAbsent: "self" to encrypt them under the row's own record key (shreddable per record), or make "${ownerName}" required: true. Entity fields have no plaintext fallback (fw#3654).`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Custom subject entities (such as HR employees or patients) are allowed but
|
|
265
|
+
// should be an intentional choice.
|
|
266
|
+
function warnOnNonUserOwnerReference(input, ownerName, ownerField) {
|
|
267
|
+
// skip: text owner fields carry a user id by convention, there is no target to check
|
|
268
|
+
if (ownerField.type !== "reference")
|
|
269
|
+
return;
|
|
270
|
+
const refTarget = ownerField.entity;
|
|
271
|
+
const targetEntity = refTarget.includes(":") ? refTarget.split(":")[1] : refTarget;
|
|
272
|
+
if (targetEntity !== "user") {
|
|
273
|
+
log.warn(`[Feature ${input.featureName}] personal.of "${ownerName}" on entity "${input.entityName}" targets reference "${refTarget}" — typically should be a user reference. If intentional (custom subject-entity like employee/patient), ignore.`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// First match wins. Business data with person-like names (for example,
|
|
277
|
+
// company.legalName) can produce false positives; authors can suppress them
|
|
278
|
+
// with { personal: false, reason: "is_business_data" }.
|
|
279
|
+
const UNANNOTATED_NAME_WARNINGS = [
|
|
280
|
+
{
|
|
281
|
+
applies: (input) => FILE_FIELD_TYPES.has(input.field.type),
|
|
282
|
+
message: (input) => `[Feature ${input.featureName}] File field "${input.fieldName}" on entity "${input.entityName}" has no personal annotation. A forget with strategy "delete" then only severs the uploader link and keeps the binary. If the file holds personal data, mark it { personal: "self" } or { personal: { of: "<ownerField>" } }. If it is business data, set { personal: false, reason: "is_business_data" } to silence.`,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
applies: (_input, lowerName) => PII_DIRECT_NAME_HINTS.has(lowerName),
|
|
286
|
+
message: (input) => `${fieldLocation(input)} has a PII-typical name but no personal annotation. If this is PII, mark it { personal: "self", find: ... }. If business data, set { personal: false, reason: "is_business_data" } to silence.`,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
applies: (_input, lowerName) => PII_USER_OWNED_NAME_HINTS.has(lowerName),
|
|
290
|
+
message: (input) => `${fieldLocation(input)} has a user-content-typical name but no personal annotation. If this contains user-generated content, mark it { personal: { of: "<authorIdField>" }, find: ... }. If business data, set { personal: false, reason: "..." } to silence.`,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
applies: (input, lowerName) => PII_USER_REFERENCE_NAME_HINTS.has(lowerName) &&
|
|
294
|
+
!input.flags.subjectRef &&
|
|
295
|
+
!input.personalOfOwnerFieldNames.has(input.fieldName),
|
|
296
|
+
message: (input) => `${fieldLocation(input)} has a user-reference-typical name but no personal annotation — a foreign key into \`user\` carries Art.17 obligations even with no annotated content on the entity. Mark it { personal: "ref" } AND register r.useExtension(EXT_USER_DATA, "${input.entityName}", …) for Art.17 coverage — this warning is the only boot-time check for that, registering the hook is not enforced. Or { personal: { of: "${input.fieldName}" } } on the field it owns. If business data, set { personal: false, reason: "..." } to silence.`,
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
function warnOnUnannotatedPersonalName(input) {
|
|
300
|
+
// skip: an annotation or an explicit plaintext reason already states the field's stance
|
|
301
|
+
if (input.subjectAnnotationCount > 0 || input.flags.allowPlaintext)
|
|
302
|
+
return;
|
|
303
|
+
const lowerName = input.fieldName.toLowerCase();
|
|
304
|
+
const warning = UNANNOTATED_NAME_WARNINGS.find((candidate) => candidate.applies(input, lowerName));
|
|
305
|
+
if (warning)
|
|
306
|
+
log.warn(warning.message(input));
|
|
307
|
+
}
|
|
308
|
+
const RETENTION_RULES = [
|
|
309
|
+
warnOnInvalidKeepFor,
|
|
310
|
+
rejectUnknownRetentionReference,
|
|
311
|
+
warnOnBlockDeleteWithoutAnonymize,
|
|
312
|
+
];
|
|
313
|
+
function validateEntityRetention(featureName, entityName, entity) {
|
|
314
|
+
const { retention } = entity;
|
|
315
|
+
// skip: entities without a retention policy have nothing to validate
|
|
316
|
+
if (!retention)
|
|
317
|
+
return;
|
|
318
|
+
const input = {
|
|
319
|
+
featureName,
|
|
320
|
+
entityName,
|
|
321
|
+
fieldsByName: entity.fields,
|
|
322
|
+
retention,
|
|
323
|
+
};
|
|
324
|
+
for (const rule of RETENTION_RULES)
|
|
325
|
+
rule(input);
|
|
326
|
+
}
|
|
327
|
+
function warnOnInvalidKeepFor({ featureName, entityName, retention }) {
|
|
328
|
+
if (!KEEP_FOR_PATTERN.test(retention.keepFor)) {
|
|
329
|
+
log.warn(`[Feature ${featureName}] Entity "${entityName}" retention.keepFor="${retention.keepFor}" has an invalid format. Expected: <number><h|d|w|m|y> (e.g. "30d", "10y", "6m"). The cleanup job cannot parse anything else; fix keepFor.`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function rejectUnknownRetentionReference(input) {
|
|
333
|
+
const { featureName, entityName, fieldsByName } = input;
|
|
334
|
+
const refName = input.retention.reference;
|
|
335
|
+
// skip: no explicit retention.reference to resolve
|
|
336
|
+
if (refName === undefined)
|
|
337
|
+
return;
|
|
338
|
+
if (!fieldsByName[refName] && !FRAMEWORK_TIMESTAMP_FIELDS.has(refName)) {
|
|
339
|
+
const known = Object.keys(fieldsByName).sort().join(", ");
|
|
340
|
+
const framework = [...FRAMEWORK_TIMESTAMP_FIELDS].sort().join(", ");
|
|
341
|
+
throw new Error(`[Feature ${featureName}] Entity "${entityName}" retention.reference "${refName}" does not exist. Known fields: ${known} — framework-managed timestamps also accepted: ${framework}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// blockDelete with no field that can carry `anonymize` is fine;
|
|
345
|
+
// subjectRef-only entities are covered by the V3 EXT_USER_DATA guard instead.
|
|
346
|
+
function warnOnBlockDeleteWithoutAnonymize(input) {
|
|
347
|
+
const { featureName, entityName, fieldsByName } = input;
|
|
348
|
+
// skip: only blockDelete relies on anonymize once keepFor expires
|
|
349
|
+
if (input.retention.strategy !== "blockDelete")
|
|
350
|
+
return;
|
|
351
|
+
const hasAnonymize = Object.values(fieldsByName).some((f) => {
|
|
352
|
+
const a = f; // @cast-boundary schema-walk
|
|
353
|
+
return Boolean(a.anonymize);
|
|
354
|
+
});
|
|
355
|
+
if (entityHasAnonymizableSubjectField(fieldsByName) && !hasAnonymize) {
|
|
356
|
+
log.warn(`[Feature ${featureName}] Entity "${entityName}" retention.strategy="blockDelete" but no field has an anonymize-function. Once keepFor expires, the data-retention cron anonymizes nothing and the row keeps its PII. Add { anonymize: () => null } or () => "[ANONYMIZED]" to PII fields.`);
|
|
286
357
|
}
|
|
287
358
|
}
|
|
@@ -10,7 +10,7 @@ import { getZodObjectShape } from "./zod-shape.js";
|
|
|
10
10
|
// reaches into feature.entities[screen.entity] — a projectionList has no
|
|
11
11
|
// entity, so sharing the function would mean threading a discriminated
|
|
12
12
|
// union through every entity-bound helper it calls.
|
|
13
|
-
// Exported for query-refs.ts
|
|
13
|
+
// Exported for query-refs.ts — one source for the QN-derivation
|
|
14
14
|
// logic instead of duplicating it in both boot-validators.
|
|
15
15
|
export function buildQueryHandlerMap(features) {
|
|
16
16
|
const out = new Map();
|
|
@@ -29,7 +29,7 @@ function schemaAccepts(schema, key) {
|
|
|
29
29
|
return shape !== undefined && key in shape;
|
|
30
30
|
}
|
|
31
31
|
// sortable/paginated are derived by buildAppSchema from the query's Zod
|
|
32
|
-
// schema
|
|
32
|
+
// schema — there is no separate wire type from the author-facing
|
|
33
33
|
// ProjectionListScreenDefinition, so hand-authoring them would otherwise be
|
|
34
34
|
// silently overwritten with no signal to the author. Reject outright.
|
|
35
35
|
function rejectHandAuthoredDerivedFlags(prefix, screen) {
|
|
@@ -67,7 +67,7 @@ function validateOneProjectionListScreen(feature, screen, queryHandlers, feature
|
|
|
67
67
|
validateProjectionListFilterSchemaAcceptance(prefix, screen, schema);
|
|
68
68
|
validateProjectionListFacetsSchemaAcceptance(prefix, screen, schema);
|
|
69
69
|
}
|
|
70
|
-
// filter
|
|
70
|
+
// filter sends `payload.filter` — same 422 footgun as facets below
|
|
71
71
|
// if the handler's schema doesn't accept it.
|
|
72
72
|
function validateProjectionListFilterSchemaAcceptance(prefix, screen, schema) {
|
|
73
73
|
// skip: no filter declared, or the schema already accepts it — nothing to reject.
|
|
@@ -77,11 +77,11 @@ function validateProjectionListFilterSchemaAcceptance(prefix, screen, schema) {
|
|
|
77
77
|
`schema — add filter: z.object({ field: z.string(), op: z.enum(["eq","ne","lt","gt","lte","gte","in"]), ` +
|
|
78
78
|
`value: z.unknown() }).optional() (or reuse entityListSchema's shape) to the handler's schema.`);
|
|
79
79
|
}
|
|
80
|
-
// facets
|
|
81
|
-
//
|
|
80
|
+
// facets send `payload.filters` — same failure mode as search/sort:
|
|
81
|
+
// definePagedQueryHandler doesn't auto-merge params into the
|
|
82
82
|
// handler's own Zod schema, so a declared facet would 422 on every query
|
|
83
83
|
// unless the author added `filters` themselves.
|
|
84
|
-
// A dateRange facet
|
|
84
|
+
// A dateRange facet sends its two bounds as the top-level payload
|
|
85
85
|
// keys it names in `params`, not as a `filters` entry — so the query has to
|
|
86
86
|
// accept exactly those keys. Catches the facet pointed at a field the query
|
|
87
87
|
// can't narrow by, which would otherwise 422 on the first date the user picks.
|
|
@@ -203,7 +203,7 @@ function validateRelatedListGroupBy(prefix, section) {
|
|
|
203
203
|
}
|
|
204
204
|
// relatedList sections declare search/facets the same way a projectionList
|
|
205
205
|
// screen does, and hit the same 422 footgun: definePagedQueryHandler doesn't
|
|
206
|
-
// auto-merge params into the handler's own Zod schema
|
|
206
|
+
// auto-merge params into the handler's own Zod schema, so an
|
|
207
207
|
// undeclared param would fail every query instead of failing at boot.
|
|
208
208
|
// Reachable on projectionDetail only (screens.ts rejects relatedList
|
|
209
209
|
// elsewhere), but the layout walk stays uniform so lifting that restriction
|
|
@@ -25,7 +25,7 @@ function checkVisibleWhenQueryRef(queryHandlers, query, buildPrefix) {
|
|
|
25
25
|
"Make the query's input fields optional or use a query without required input.");
|
|
26
26
|
}
|
|
27
27
|
// relatedList is currently only reachable on projectionDetail — screens.ts
|
|
28
|
-
// rejects it outright on entityEdit/actionForm/configEdit
|
|
28
|
+
// rejects it outright on entityEdit/actionForm/configEdit. Walking
|
|
29
29
|
// the shared EditLayout uniformly here (instead of excluding those three
|
|
30
30
|
// types) keeps this collector correct if that restriction is ever lifted.
|
|
31
31
|
function checkEditLayoutQueryRefs(queryHandlers, featureName, screenId, screenType, layout) {
|
|
@@ -64,4 +64,5 @@ export type NavigateTargetLookups = {
|
|
|
64
64
|
}>;
|
|
65
65
|
};
|
|
66
66
|
export declare function validateNavigateActionTab(featureName: string, screenId: string, screenType: NavigateSourceScreenType, actionLabel: string, action: RowAction, lookups: NavigateTargetLookups): void;
|
|
67
|
+
export declare function assertWriteHandlerRegistered(featureName: string, subject: string, handler: string, allWriteHandlerQns: ReadonlySet<string>): void;
|
|
67
68
|
export {};
|