@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
|
@@ -26,6 +26,9 @@ const writesDeniedReasons = new WeakMap();
|
|
|
26
26
|
export function denyTenantDbWrites(tenantDb, reason) {
|
|
27
27
|
writesDeniedReasons.set(tenantDb, reason);
|
|
28
28
|
}
|
|
29
|
+
export function tenantDbWritesDeniedReason(tenantDb) {
|
|
30
|
+
return writesDeniedReasons.get(tenantDb);
|
|
31
|
+
}
|
|
29
32
|
export function tenantDbWritesDeniedError(operation, reason) {
|
|
30
33
|
return new InternalError({
|
|
31
34
|
message: `${operation}: writes are not available ${reason}; run the write through ctx.write so it gets its own transaction`,
|
package/dist/db/tenant-db.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare function withExecutorManagedWrites(runner: DbRunner): ExecutorMan
|
|
|
16
16
|
export declare function withoutExecutorManagedWrites(runner: DbRunner): DbRunner;
|
|
17
17
|
export declare function assertPersonalDataWrite(db: TenantDb, tableName: string, keys: readonly string[], entity: EntityDefinition): void;
|
|
18
18
|
export declare function unsafeRawForDeclaredStep(holder: TenantDb | UncheckedSystemDb, reason: string): DbRunner;
|
|
19
|
-
export declare function withSystemDbUnsafeRawGrant(systemDb:
|
|
19
|
+
export declare function withSystemDbUnsafeRawGrant<T extends object>(systemDb: T, grant: EscapeHatchDeclaration | undefined, deniedCallerLabel: string): T | UncheckedSystemDb;
|
|
20
20
|
export declare function createUncheckedSystemDb(db: TenantDb, dbOutsideTransaction?: TenantDb, report?: EscapeHatchReporter): UncheckedSystemDb;
|
|
21
21
|
export declare function createSystemDbView(db: TenantDb, dbOutsideTransaction?: TenantDb, report?: EscapeHatchReporter): UncheckedSystemDb;
|
|
22
22
|
export declare function scopeTenantDbToTenant(db: TenantDb, tenantId: TenantId): TenantDb;
|
|
@@ -34,7 +34,7 @@ export type TenantDbGrants = {
|
|
|
34
34
|
readonly writesDeniedReason?: string;
|
|
35
35
|
};
|
|
36
36
|
export type PersonalDataGate = (tableName: string, keys: readonly string[], entity?: EntityDefinition) => void;
|
|
37
|
-
export declare function withUnsafeRawGrant(tenantDb:
|
|
37
|
+
export declare function withUnsafeRawGrant<T extends object>(tenantDb: T, grant: EscapeHatchDeclaration | undefined): T | TenantDb;
|
|
38
38
|
export declare function acknowledgeConventionCrossTenant(tenantDb: TenantDb, reason: string, options?: {
|
|
39
39
|
readonly deferReport?: boolean;
|
|
40
40
|
}): TenantDb;
|
package/dist/db/tenant-db.js
CHANGED
|
@@ -10,7 +10,7 @@ import { bindProjectionRegistry, projectionRegistryOf } from "./projection-regis
|
|
|
10
10
|
import { acquireNamespacedAdvisoryLock } from "./queries/advisory-lock.js";
|
|
11
11
|
import { queryDatabaseNow } from "./queries/database-now.js";
|
|
12
12
|
import { hasFieldProperty, tableNameOf } from "./table-introspection.js";
|
|
13
|
-
import { bindTenantDbRunner, denyTenantDbWrites, tenantDbRunner, tenantDbWritesDeniedError, } from "./tenant-db-runner.js";
|
|
13
|
+
import { bindTenantDbRunner, denyTenantDbWrites, tenantDbRunner, tenantDbWritesDeniedError, tenantDbWritesDeniedReason, } from "./tenant-db-runner.js";
|
|
14
14
|
export { SYSTEM_SCOPE_CHECK_BRAND, } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
15
15
|
const declaredUnsafeRawRunners = new WeakMap();
|
|
16
16
|
// The CRUD executor writes through tenantDbRunner, not insertOne, so it asks the
|
|
@@ -26,6 +26,9 @@ function boundRunner(runner, gate, projectionRegistry, executorManagedWrites) {
|
|
|
26
26
|
const proxy = new Proxy(runner, {
|
|
27
27
|
// Tagged-template calls need the real driver object as `this`.
|
|
28
28
|
apply(target, _thisArg, args) {
|
|
29
|
+
if (typeof target !== "function") {
|
|
30
|
+
throw new TypeError("tenant-db runner proxy applied on a non-callable runner");
|
|
31
|
+
}
|
|
29
32
|
return Reflect.apply(target, target, args);
|
|
30
33
|
},
|
|
31
34
|
get(target, prop, _receiver) {
|
|
@@ -51,7 +54,11 @@ function boundRunner(runner, gate, projectionRegistry, executorManagedWrites) {
|
|
|
51
54
|
}
|
|
52
55
|
const gatedArgs = [
|
|
53
56
|
...args.slice(0, -1),
|
|
54
|
-
(tx) =>
|
|
57
|
+
(tx) => {
|
|
58
|
+
// @cast-boundary db-runner — driver callback hands over the tx handle untyped
|
|
59
|
+
const txRunner = tx;
|
|
60
|
+
return callback(boundRunner(txRunner, gate, projectionRegistry, executorManagedWrites));
|
|
61
|
+
},
|
|
55
62
|
];
|
|
56
63
|
return Reflect.apply(value, target, gatedArgs);
|
|
57
64
|
};
|
|
@@ -98,9 +105,7 @@ export function assertPersonalDataWrite(db, tableName, keys, entity) {
|
|
|
98
105
|
}
|
|
99
106
|
// Framework-private (not re-exported from db/index.ts): same grant check + audit as unsafeRaw, for engine forwarding.
|
|
100
107
|
export function unsafeRawForDeclaredStep(holder, reason) {
|
|
101
|
-
|
|
102
|
-
throw new Error("unsafeRawForDeclaredStep requires a non-empty reason");
|
|
103
|
-
}
|
|
108
|
+
assertReason("unsafeRawForDeclaredStep", reason);
|
|
104
109
|
const runner = declaredUnsafeRawRunners.get(holder);
|
|
105
110
|
if (!runner) {
|
|
106
111
|
throw new InternalError({
|
|
@@ -179,7 +184,7 @@ function buildUncheckedSystemDb(db, dbOutsideTransaction, report, gate) {
|
|
|
179
184
|
// Reference rows (tenantId === SYSTEM_TENANT_ID) are allowed, mirroring
|
|
180
185
|
// "tenant"-mode readWhere's own [tenantId, SYSTEM_TENANT_ID] allowlist.
|
|
181
186
|
assertRowsTenant(rows, tenantField) {
|
|
182
|
-
const hasOffender = rows.some((row) => !allowedTenantIds.
|
|
187
|
+
const hasOffender = rows.some((row) => !allowedTenantIds.some((allowed) => allowed === row[tenantField]));
|
|
183
188
|
if (hasOffender) {
|
|
184
189
|
throw new AccessDeniedError({
|
|
185
190
|
message: `systemScope() row tenant self-check failed on field "${String(tenantField)}"`,
|
|
@@ -250,7 +255,13 @@ export function createSystemDbView(db, dbOutsideTransaction, report = fallbackEs
|
|
|
250
255
|
// same connection/transaction. For code that must run caller-supplied callbacks (e.g. config
|
|
251
256
|
// validators) without handing them a cross-tenant view.
|
|
252
257
|
export function scopeTenantDbToTenant(db, tenantId) {
|
|
253
|
-
|
|
258
|
+
const writesDeniedReason = tenantDbWritesDeniedReason(db);
|
|
259
|
+
return createTenantDb({
|
|
260
|
+
db: tenantDbRunner(db),
|
|
261
|
+
tenantId,
|
|
262
|
+
mode: "tenant",
|
|
263
|
+
...(writesDeniedReason !== undefined && { grants: { writesDeniedReason } }),
|
|
264
|
+
});
|
|
254
265
|
}
|
|
255
266
|
// @cast-boundary tenant-db-row
|
|
256
267
|
export function castTenantRows(rows) {
|
|
@@ -261,6 +272,18 @@ export { tableNameOf };
|
|
|
261
272
|
function hasGrant(decl) {
|
|
262
273
|
return decl !== undefined && decl.reason.trim().length > 0;
|
|
263
274
|
}
|
|
275
|
+
// Tenant ids left over when the caller excludes some via ne/notIn; undefined for other operator forms.
|
|
276
|
+
function tenantIdsAfterExclusion(requested, allowed) {
|
|
277
|
+
if (!isPlainObject(requested))
|
|
278
|
+
return undefined;
|
|
279
|
+
if ("ne" in requested)
|
|
280
|
+
return allowed.filter((t) => t !== requested["ne"]);
|
|
281
|
+
if (Array.isArray(requested["notIn"])) {
|
|
282
|
+
const excluded = requested["notIn"];
|
|
283
|
+
return allowed.filter((t) => !excluded.includes(t));
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
264
287
|
// Tenant ids a where.tenantId filter asks for; undefined for operator forms
|
|
265
288
|
// that cannot narrow to a tenant list (ne, like, ...).
|
|
266
289
|
export function requestedTenantIds(requested) {
|
|
@@ -422,7 +445,13 @@ function buildTenantDb(options) {
|
|
|
422
445
|
const allowed = [tenantId, SYSTEM_TENANT_ID];
|
|
423
446
|
const requested = where?.["tenantId"];
|
|
424
447
|
if (requested !== undefined) {
|
|
425
|
-
const requestedList = requestedTenantIds(requested)
|
|
448
|
+
const requestedList = requestedTenantIds(requested);
|
|
449
|
+
// An explicit exclusion or empty list must keep excluding, never widen to the full scope.
|
|
450
|
+
if (requestedList === undefined) {
|
|
451
|
+
return { ...where, tenantId: tenantIdsAfterExclusion(requested, allowed) ?? allowed };
|
|
452
|
+
}
|
|
453
|
+
if (requestedList.length === 0)
|
|
454
|
+
return { ...where, tenantId: [] };
|
|
426
455
|
const narrowed = requestedList.filter((t) => typeof t === "string" && allowed.includes(t));
|
|
427
456
|
return { ...where, tenantId: narrowed.length > 0 ? narrowed : allowed };
|
|
428
457
|
}
|
|
@@ -457,6 +486,14 @@ function buildTenantDb(options) {
|
|
|
457
486
|
function globalWriteReason() {
|
|
458
487
|
return grants?.globalWrites?.reason ?? "";
|
|
459
488
|
}
|
|
489
|
+
// The table is the audit target: process-level dedup would otherwise collapse a cron's
|
|
490
|
+
// writes to several global tables into one audit entry.
|
|
491
|
+
function reportGlobalWrite(table) {
|
|
492
|
+
report("global-write", globalWriteReason(), {
|
|
493
|
+
id: tableNameOf(table),
|
|
494
|
+
tenantId: tenantId ?? SYSTEM_TENANT_ID,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
460
497
|
function personalDataDenied(table, keys) {
|
|
461
498
|
if (!personalDataGate)
|
|
462
499
|
return undefined;
|
|
@@ -528,7 +565,7 @@ function buildTenantDb(options) {
|
|
|
528
565
|
personalDataDenied(table, Object.keys(values));
|
|
529
566
|
if (denied)
|
|
530
567
|
return Promise.reject(denied);
|
|
531
|
-
|
|
568
|
+
reportGlobalWrite(table);
|
|
532
569
|
return withDbSpan("insert", table, async () => insertRow(table, values));
|
|
533
570
|
},
|
|
534
571
|
updateMany(set, where) {
|
|
@@ -542,7 +579,7 @@ function buildTenantDb(options) {
|
|
|
542
579
|
if (!where || Object.keys(where).length === 0) {
|
|
543
580
|
return Promise.reject(new Error("db.global().updateMany without where would mass-update every tenant's rows. Pass at least one where condition."));
|
|
544
581
|
}
|
|
545
|
-
|
|
582
|
+
reportGlobalWrite(table);
|
|
546
583
|
return withDbSpan("update", table, async () => updateRows(table, set, where));
|
|
547
584
|
},
|
|
548
585
|
deleteMany(where) {
|
|
@@ -554,7 +591,7 @@ function buildTenantDb(options) {
|
|
|
554
591
|
if (!where || Object.keys(where).length === 0) {
|
|
555
592
|
return Promise.reject(new Error("db.global().deleteMany without where would mass-delete every tenant's rows. Pass at least one where condition."));
|
|
556
593
|
}
|
|
557
|
-
|
|
594
|
+
reportGlobalWrite(table);
|
|
558
595
|
return withDbSpan("delete", table, async () => {
|
|
559
596
|
await deleteRows(table, where);
|
|
560
597
|
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { WhereObject } from "@cosmicdrift/kumiko-types/where-clause-types";
|
|
2
|
+
import type { TableInfo } from "./table-info.ts";
|
|
3
|
+
export declare function buildWhereClause(info: TableInfo, where: WhereObject, startIndex: number, qualifier?: string): {
|
|
4
|
+
sqlText: string;
|
|
5
|
+
values: unknown[];
|
|
6
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import { computeBlindIndex, configuredBlindIndexKey } from "../crypto/blind-index.js";
|
|
3
|
+
import { quoteIdent } from "../crypto/ciphertext-pattern.js";
|
|
4
|
+
import { prepareJsonbValue, prepareValue } from "./driver-value-preparation.js";
|
|
5
|
+
function isJsonTextMatchList(value) {
|
|
6
|
+
return Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function isWhereOperator(v) {
|
|
9
|
+
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
10
|
+
return false;
|
|
11
|
+
// Don't false-positive on Date / Temporal / other domain-objects.
|
|
12
|
+
// WhereOperator is plain object literal with at most these keys.
|
|
13
|
+
const keys = Object.keys(v);
|
|
14
|
+
if (keys.length === 0)
|
|
15
|
+
return false;
|
|
16
|
+
return keys.every((key) => Object.hasOwn(OPERATOR_EMITTERS, key));
|
|
17
|
+
}
|
|
18
|
+
function isJsonbScalar(value) {
|
|
19
|
+
return value !== null && typeof value !== "object";
|
|
20
|
+
}
|
|
21
|
+
function valueList(target, values) {
|
|
22
|
+
return values
|
|
23
|
+
.map((v) => {
|
|
24
|
+
const p = prepareValue(v, target.pgType);
|
|
25
|
+
return p.kind === "literal" ? p.literal : `${target.bind(p.bound)}${p.sql}`;
|
|
26
|
+
})
|
|
27
|
+
.join(", ");
|
|
28
|
+
}
|
|
29
|
+
// multiSelect (and any other jsonb-array-of-scalars column) stores an
|
|
30
|
+
// array — a filter value is one option, not the whole array, so eq/ne/in
|
|
31
|
+
// must check array containment (`@>`) instead of scalar `=`/`<>`/`IN`
|
|
32
|
+
// against the jsonb column, which Postgres rejects outright.
|
|
33
|
+
// Mirrors applyMultiSelectFilter in event-store-executor-read-filter.ts — keep in lock-step.
|
|
34
|
+
function jsonbContainsAny(target, candidates) {
|
|
35
|
+
const { column, ref, bind } = target;
|
|
36
|
+
if (candidates.length === 0)
|
|
37
|
+
return "FALSE";
|
|
38
|
+
const parts = [];
|
|
39
|
+
for (const v of candidates) {
|
|
40
|
+
const p = prepareJsonbValue([v]);
|
|
41
|
+
// prepareJsonbValue never returns kind:"literal" (only prepareValue's
|
|
42
|
+
// isSqlExpression branch does) — narrow anyway, PreparedValue is a union.
|
|
43
|
+
if (p && p.kind === "param") {
|
|
44
|
+
parts.push(`${ref(column)} @> ${bind(p.bound)}${p.sql}`);
|
|
45
|
+
}
|
|
46
|
+
// A jsonb column holding the scalar itself is never `@>` an array of it.
|
|
47
|
+
if (isJsonbScalar(v)) {
|
|
48
|
+
parts.push(`${ref(column)} = ${bind(JSON.stringify(v))}::text::jsonb`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return parts.length > 0 ? `(${parts.join(" OR ")})` : "FALSE";
|
|
52
|
+
}
|
|
53
|
+
function isValidJsonTextMatch(match) {
|
|
54
|
+
if (match === null || typeof match !== "object")
|
|
55
|
+
return false;
|
|
56
|
+
const keys = propertyOf(match, "keys");
|
|
57
|
+
return (Array.isArray(keys) &&
|
|
58
|
+
keys.length > 0 &&
|
|
59
|
+
keys.every((k) => typeof k === "string") &&
|
|
60
|
+
typeof propertyOf(match, "eq") === "string");
|
|
61
|
+
}
|
|
62
|
+
function jsonTextMatchCondition(target, match) {
|
|
63
|
+
const { column, ref, bind } = target;
|
|
64
|
+
if (!isValidJsonTextMatch(match)) {
|
|
65
|
+
throw new Error(`where: jsonText on column "${column}" needs non-empty string keys and a string eq`);
|
|
66
|
+
}
|
|
67
|
+
const lookups = match.keys.map((key) => `${ref(column)}->>${bind(key)}::text`);
|
|
68
|
+
const lhs = lookups.length === 1 ? lookups[0] : `COALESCE(${lookups.join(", ")})`;
|
|
69
|
+
return `(${lhs} = ${bind(match.eq)})`;
|
|
70
|
+
}
|
|
71
|
+
function emitJsonText(target, operator) {
|
|
72
|
+
const { column, pgType } = target;
|
|
73
|
+
const operand = operator.jsonText;
|
|
74
|
+
if (operand === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
if (Object.keys(operator).length > 1) {
|
|
77
|
+
throw new Error(`where: jsonText on column "${column}" cannot be combined with other operators`);
|
|
78
|
+
}
|
|
79
|
+
if (pgType !== "jsonb") {
|
|
80
|
+
throw new Error(`where: jsonText requires a jsonb column, "${column}" is ${pgType}`);
|
|
81
|
+
}
|
|
82
|
+
const matches = isJsonTextMatchList(operand) ? operand : [operand];
|
|
83
|
+
if (matches.length === 0)
|
|
84
|
+
return "FALSE";
|
|
85
|
+
return matches.map((match) => jsonTextMatchCondition(target, match)).join(" AND ");
|
|
86
|
+
}
|
|
87
|
+
function comparisonCondition(target, symbol, operand) {
|
|
88
|
+
const { column, pgType, ref, bind } = target;
|
|
89
|
+
const p = prepareValue(operand, pgType);
|
|
90
|
+
return p.kind === "literal"
|
|
91
|
+
? `${ref(column)} ${symbol} ${p.literal}`
|
|
92
|
+
: `${ref(column)} ${symbol} ${bind(p.bound)}${p.sql}`;
|
|
93
|
+
}
|
|
94
|
+
function comparisonEmitter(key, symbol) {
|
|
95
|
+
return (target, operator) => {
|
|
96
|
+
const operand = operator[key];
|
|
97
|
+
if (operand === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
// Mirrors applyMultiSelectFilter: ordering/pattern ops have no meaning on a jsonb
|
|
100
|
+
// option array (`like` would crash as `jsonb ~~ jsonb`), so match nothing.
|
|
101
|
+
if (target.pgType === "jsonb")
|
|
102
|
+
return "FALSE";
|
|
103
|
+
return comparisonCondition(target, symbol, operand);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Mirrors applyMultiSelectFilter: an array `ne` means "does not contain all of these".
|
|
107
|
+
function jsonbNotEqualCondition(target, operand) {
|
|
108
|
+
const { column, ref, bind } = target;
|
|
109
|
+
if (Array.isArray(operand)) {
|
|
110
|
+
if (operand.length === 0)
|
|
111
|
+
return "FALSE";
|
|
112
|
+
const p = prepareJsonbValue(operand);
|
|
113
|
+
if (p && p.kind === "param") {
|
|
114
|
+
return `(${ref(column)} IS NULL OR NOT (${ref(column)} @> ${bind(p.bound)}${p.sql}))`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (isJsonbScalar(operand)) {
|
|
118
|
+
const p = prepareJsonbValue([operand]);
|
|
119
|
+
if (p && p.kind === "param") {
|
|
120
|
+
return `(${ref(column)} IS NULL OR NOT (${ref(column)} @> ${bind(p.bound)}${p.sql} OR ${ref(column)} = ${bind(JSON.stringify(operand))}::text::jsonb))`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
function emitNotEqual(target, operator) {
|
|
126
|
+
const operand = operator.ne;
|
|
127
|
+
if (operand === undefined)
|
|
128
|
+
return undefined;
|
|
129
|
+
if (operand === null && Object.keys(operator).length === 1) {
|
|
130
|
+
return `${target.ref(target.column)} IS NOT NULL`;
|
|
131
|
+
}
|
|
132
|
+
if (target.pgType === "jsonb") {
|
|
133
|
+
const jsonbCondition = jsonbNotEqualCondition(target, operand);
|
|
134
|
+
if (jsonbCondition !== undefined)
|
|
135
|
+
return jsonbCondition;
|
|
136
|
+
}
|
|
137
|
+
return comparisonCondition(target, "<>", operand);
|
|
138
|
+
}
|
|
139
|
+
function emitIn(target, operator) {
|
|
140
|
+
const operand = operator.in;
|
|
141
|
+
if (!Array.isArray(operand))
|
|
142
|
+
return undefined;
|
|
143
|
+
if (target.pgType === "jsonb")
|
|
144
|
+
return jsonbContainsAny(target, operand);
|
|
145
|
+
if (operand.length === 0)
|
|
146
|
+
return "FALSE";
|
|
147
|
+
return `${target.ref(target.column)} IN (${valueList(target, operand)})`;
|
|
148
|
+
}
|
|
149
|
+
function emitNotIn(target, operator) {
|
|
150
|
+
const operand = operator.notIn;
|
|
151
|
+
if (!Array.isArray(operand))
|
|
152
|
+
return undefined;
|
|
153
|
+
if (target.pgType === "jsonb") {
|
|
154
|
+
throw new Error(`where: notIn is not supported on jsonb column "${target.column}"`);
|
|
155
|
+
}
|
|
156
|
+
if (operand.length === 0)
|
|
157
|
+
return "TRUE";
|
|
158
|
+
return `${target.ref(target.column)} NOT IN (${valueList(target, operand)})`;
|
|
159
|
+
}
|
|
160
|
+
// Insertion order is the SQL emission order. jsonText comes first so its "cannot be combined" error wins over any other
|
|
161
|
+
// operator's error; the rest keep the historical order gt, gte, lt, lte, ne, like, in, notIn.
|
|
162
|
+
const OPERATOR_EMITTERS = {
|
|
163
|
+
jsonText: emitJsonText,
|
|
164
|
+
gt: comparisonEmitter("gt", ">"),
|
|
165
|
+
gte: comparisonEmitter("gte", ">="),
|
|
166
|
+
lt: comparisonEmitter("lt", "<"),
|
|
167
|
+
lte: comparisonEmitter("lte", "<="),
|
|
168
|
+
ne: emitNotEqual,
|
|
169
|
+
like: comparisonEmitter("like", "LIKE"),
|
|
170
|
+
in: emitIn,
|
|
171
|
+
notIn: emitNotIn,
|
|
172
|
+
};
|
|
173
|
+
function operatorConditions(target, operator) {
|
|
174
|
+
const conditions = [];
|
|
175
|
+
for (const emit of Object.values(OPERATOR_EMITTERS)) {
|
|
176
|
+
const condition = emit(target, operator);
|
|
177
|
+
if (condition !== undefined)
|
|
178
|
+
conditions.push(condition);
|
|
179
|
+
}
|
|
180
|
+
return conditions;
|
|
181
|
+
}
|
|
182
|
+
// Blind-index OR rewrite: if the column has a bidx
|
|
183
|
+
// counterpart (suffix convention `<col>_bidx`, framework-reserved)
|
|
184
|
+
// and a key is configured, equality matches both arms —
|
|
185
|
+
// plaintext legacy rows via the plaintext arm, encrypted rows
|
|
186
|
+
// via the HMAC. The `kumiko-pii:` prefix on ciphertexts makes
|
|
187
|
+
// a random match on the plaintext arm practically impossible.
|
|
188
|
+
function equalityCondition(target, value) {
|
|
189
|
+
const { info, column, pgType, ref, bind } = target;
|
|
190
|
+
const p = prepareValue(value, pgType);
|
|
191
|
+
if (p.kind === "literal")
|
|
192
|
+
return `${ref(column)} = ${p.literal}`;
|
|
193
|
+
const bidxKey = configuredBlindIndexKey();
|
|
194
|
+
const bidxCol = `${column}_bidx`;
|
|
195
|
+
if (bidxKey !== undefined &&
|
|
196
|
+
typeof value === "string" &&
|
|
197
|
+
!column.endsWith("_bidx") &&
|
|
198
|
+
info.hasColumn(bidxCol)) {
|
|
199
|
+
return `(${ref(column)} = ${bind(p.bound)} OR ${ref(bidxCol)} = ${bind(computeBlindIndex(bidxKey, value))})`;
|
|
200
|
+
}
|
|
201
|
+
return `${ref(column)} = ${bind(p.bound)}${p.sql}`;
|
|
202
|
+
}
|
|
203
|
+
function arrayCondition(target, values) {
|
|
204
|
+
if (values.length === 0)
|
|
205
|
+
return "FALSE";
|
|
206
|
+
if (target.pgType === "jsonb")
|
|
207
|
+
return jsonbContainsAny(target, values);
|
|
208
|
+
return `${target.ref(target.column)} IN (${valueList(target, values)})`;
|
|
209
|
+
}
|
|
210
|
+
function conditionsForValue(target, value) {
|
|
211
|
+
if (value === null)
|
|
212
|
+
return [`${target.ref(target.column)} IS NULL`];
|
|
213
|
+
if (Array.isArray(value))
|
|
214
|
+
return [arrayCondition(target, value)];
|
|
215
|
+
if (isWhereOperator(value))
|
|
216
|
+
return operatorConditions(target, value);
|
|
217
|
+
if (target.pgType === "jsonb" && isJsonbScalar(value)) {
|
|
218
|
+
return [jsonbContainsAny(target, [value])];
|
|
219
|
+
}
|
|
220
|
+
return [equalityCondition(target, value)];
|
|
221
|
+
}
|
|
222
|
+
export function buildWhereClause(info, where, startIndex, qualifier) {
|
|
223
|
+
const values = [];
|
|
224
|
+
// Single place that renders a column reference, so a join qualifier reaches
|
|
225
|
+
// every arm (blind index, jsonb containment, IS NULL, jsonText) alike.
|
|
226
|
+
const ref = (column) => qualifier === undefined ? quoteIdent(column) : `${quoteIdent(qualifier)}.${quoteIdent(column)}`;
|
|
227
|
+
const bind = (value) => {
|
|
228
|
+
values.push(value);
|
|
229
|
+
return `$${startIndex + values.length - 1}`;
|
|
230
|
+
};
|
|
231
|
+
const conditions = Object.entries(where).flatMap(([field, value]) => {
|
|
232
|
+
const column = info.columnOf(field);
|
|
233
|
+
return conditionsForValue({ info, column, pgType: info.pgTypeOf(column), ref, bind }, value);
|
|
234
|
+
});
|
|
235
|
+
return { sqlText: conditions.join(" AND "), values };
|
|
236
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// Used by both the user-data-rights-defaults forget hook and the
|
|
13
13
|
// data-retention hardDelete cleanup — moved here so neither copies the logic.
|
|
14
14
|
import { assertSafeStorageKey } from "../files/types.js";
|
|
15
|
+
import { createFallbackLogger } from "../logging/utils.js";
|
|
15
16
|
import { derivativeListPrefix, isDerivativeKeyOf } from "./variant-key.js";
|
|
16
17
|
// Wrap rather than let a raw provider error (e.g. S3 AccessDenied) surface
|
|
17
18
|
// unexplained — list() is a requirement on top of delete(); a bucket policy
|
|
@@ -55,6 +56,7 @@ export async function deleteStoredFileAndDerivatives(originalKey, store, errorCo
|
|
|
55
56
|
...(await listDerivativeKeys(originalKey, store, errorContext)),
|
|
56
57
|
];
|
|
57
58
|
const failedKeys = [];
|
|
59
|
+
const log = createFallbackLogger(errorContext);
|
|
58
60
|
for (const key of keysToDelete) {
|
|
59
61
|
try {
|
|
60
62
|
assertSafeStorageKey(key);
|
|
@@ -62,8 +64,10 @@ export async function deleteStoredFileAndDerivatives(originalKey, store, errorCo
|
|
|
62
64
|
}
|
|
63
65
|
catch (err) {
|
|
64
66
|
failedKeys.push(key);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
log.warn("storage delete failed", {
|
|
68
|
+
key,
|
|
69
|
+
error: err instanceof Error ? err.message : String(err),
|
|
70
|
+
});
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
return failedKeys;
|
|
@@ -116,7 +116,7 @@ async function resolveOverlaySpec(spec, row, registry, tenantId) {
|
|
|
116
116
|
// allowlisted before it becomes a response Content-Type or storage-provider
|
|
117
117
|
// metadata. Otherwise an app whose renderer wildcard is broad enough (e.g.
|
|
118
118
|
// `image/*`) would let a client-declared `image/svg+xml` ride straight
|
|
119
|
-
// through as an honestly-declared (non-sniffed) active-content type
|
|
119
|
+
// through as an honestly-declared (non-sniffed) active-content type.
|
|
120
120
|
const KNOWN_SAFE_VARIANT_MIME_TYPES = new Set([
|
|
121
121
|
"image/jpeg",
|
|
122
122
|
"image/png",
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createFallbackLogger } from "../../logging/utils.js";
|
|
1
2
|
import { normalizeAccessEntry } from "../ownership.js";
|
|
3
|
+
const log = createFallbackLogger("kumiko:boot");
|
|
2
4
|
const BUILTIN_ROLES = new Set(["all", "system"]);
|
|
3
5
|
function addRole(roleHandlers, role, identifier) {
|
|
4
6
|
let handlers = roleHandlers.get(role);
|
|
@@ -50,7 +52,7 @@ function addEntityFieldRoles(roleHandlers, f) {
|
|
|
50
52
|
// purpose) are the normal case, not a typo — and until every access
|
|
51
53
|
// surface is scanned, a role can look unique here while it's really used
|
|
52
54
|
// elsewhere (configKeys / entity+field access), a false positive in the
|
|
53
|
-
// other direction.
|
|
55
|
+
// other direction. This is why this stays opt-in rather than a
|
|
54
56
|
// default-on prod warning.
|
|
55
57
|
export function warnOnUniqueAccessRoles(features) {
|
|
56
58
|
// role → set of distinct identifiers using it, across every access
|
|
@@ -68,8 +70,7 @@ export function warnOnUniqueAccessRoles(features) {
|
|
|
68
70
|
if (handlers.size !== 1)
|
|
69
71
|
continue;
|
|
70
72
|
const [identifier] = handlers;
|
|
71
|
-
|
|
72
|
-
console.warn(`[kumiko:boot] Access role "${role}" is only used by one handler (${identifier}). ` +
|
|
73
|
+
log.warn(`Access role "${role}" is only used by one handler (${identifier}). ` +
|
|
73
74
|
`This is often a typo — the role is unknown to every other handler in the boot scan. ` +
|
|
74
75
|
`If this is intentional, ignore this warning.`);
|
|
75
76
|
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { createFallbackLogger } from "../../logging/utils.js";
|
|
2
|
+
const log = createFallbackLogger("kumiko:boot");
|
|
1
3
|
// --- Cross-feature API exposure / usage matching ---
|
|
2
4
|
//
|
|
3
5
|
// `r.exposesApi(name, impl)` registers a callable; `r.usesApi(name)`
|
|
4
|
-
// declares a caller. Boot
|
|
5
|
-
// 1.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// mit Warning markiert weil es typisch ein Refactor-Restbestand ist.
|
|
6
|
+
// declares a caller. Boot validation checks three invariants:
|
|
7
|
+
// 1. Every usesApi(name) finds an exposesApi(name) in some feature.
|
|
8
|
+
// 2. The exposing feature is listed in the caller's
|
|
9
|
+
// requires/optionalRequires so cross-feature call ordering works.
|
|
10
|
+
// 3. Self-exposure is allowed (a feature calling its own API), but is
|
|
11
|
+
// warned about because it is typically a refactoring leftover.
|
|
11
12
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
13
|
+
// apiNames must be globally unique across features; validateBoot() checks
|
|
14
|
+
// this before the per-feature walk.
|
|
14
15
|
export function validateApiExposureMatching(feature, allExposedApis, featureMap) {
|
|
15
16
|
for (const apiName of feature.usedApis) {
|
|
16
17
|
const providerFeature = allExposedApis.get(apiName);
|
|
@@ -19,17 +20,16 @@ export function validateApiExposureMatching(feature, allExposedApis, featureMap)
|
|
|
19
20
|
throw new Error(`[Feature ${feature.name}] r.usesApi("${apiName}") but no feature exposes that API. Known exposed APIs: ${known}`);
|
|
20
21
|
}
|
|
21
22
|
if (providerFeature === feature.name) {
|
|
22
|
-
|
|
23
|
-
console.warn(`[kumiko:boot] [Feature ${feature.name}] r.usesApi("${apiName}") on its own r.exposesApi — typically a refactor leftover. Call the impl directly instead.`);
|
|
23
|
+
log.warn(`[Feature ${feature.name}] r.usesApi("${apiName}") on its own r.exposesApi — typically a refactor leftover. Call the impl directly instead.`);
|
|
24
24
|
continue;
|
|
25
25
|
}
|
|
26
26
|
const allDeps = [...feature.requires, ...feature.optionalRequires];
|
|
27
27
|
if (!allDeps.includes(providerFeature)) {
|
|
28
28
|
throw new Error(`[Feature ${feature.name}] r.usesApi("${apiName}") is exposed by "${providerFeature}" but feature is not in requires/optionalRequires. Add r.requires({ features: ["${providerFeature}"] }).`);
|
|
29
29
|
}
|
|
30
|
-
// Sanity: provider feature
|
|
31
|
-
//
|
|
32
|
-
//
|
|
30
|
+
// Sanity: the provider feature must exist in this app's feature set.
|
|
31
|
+
// This should always hold when allExposedApis was built from `features`,
|
|
32
|
+
// but remains defensive for unclear constructor paths.
|
|
33
33
|
if (!featureMap.has(providerFeature)) {
|
|
34
34
|
throw new Error(`[Feature ${feature.name}] internal: r.usesApi("${apiName}") points to provider "${providerFeature}" which is not in feature map. Register the provider feature in the app's feature list.`);
|
|
35
35
|
}
|
|
@@ -13,7 +13,7 @@ export function validateFeatureBootChecks(features) {
|
|
|
13
13
|
}
|
|
14
14
|
catch (err) {
|
|
15
15
|
const message = err instanceof Error ? err.message : String(err);
|
|
16
|
-
throw new Error(`[Feature ${feature.name}] r.bootCheck failed: ${message}
|
|
16
|
+
throw new Error(`[Feature ${feature.name}] r.bootCheck failed: ${message}`);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { optionsQueryFieldRefs } from "@cosmicdrift/kumiko-types/fields";
|
|
2
|
+
import { createFallbackLogger } from "../../logging/utils.js";
|
|
2
3
|
import { isEncryptedAtRest } from "../config-helpers.js";
|
|
4
|
+
const log = createFallbackLogger("kumiko:boot");
|
|
3
5
|
// --- Toggleable-dependency warnings ---
|
|
4
6
|
//
|
|
5
7
|
// When feature A declares r.requires({ features: ["B"] }) and B is toggleable with
|
|
@@ -15,8 +17,7 @@ export function warnOnToggleableDependencies(features, featureMap) {
|
|
|
15
17
|
if (!depFeature)
|
|
16
18
|
continue; // requires-target-missing is handled elsewhere
|
|
17
19
|
if (depFeature.toggleableDefault === false) {
|
|
18
|
-
|
|
19
|
-
console.warn(`[kumiko:boot] Feature "${f.name}" requires "${dep}", which is toggleable(default=false). ` +
|
|
20
|
+
log.warn(`Feature "${f.name}" requires "${dep}", which is toggleable(default=false). ` +
|
|
20
21
|
`"${f.name}" will be effectively disabled until "${dep}" is enabled globally via the feature-toggles feature. ` +
|
|
21
22
|
`If this is intentional, ignore this warning; otherwise consider r.optionalRequires() or default=true.`);
|
|
22
23
|
}
|
|
@@ -70,9 +71,9 @@ export function validateConfigKeyRequired(feature) {
|
|
|
70
71
|
for (const [keyName, keyDef] of Object.entries(feature.configKeys ?? {})) {
|
|
71
72
|
if (keyDef.required !== true)
|
|
72
73
|
continue;
|
|
73
|
-
// required
|
|
74
|
-
//
|
|
75
|
-
//
|
|
74
|
+
// `required` means the tenant must configure the key. A non-empty default
|
|
75
|
+
// or computed resolver means the key can never be unset, so readiness could
|
|
76
|
+
// never report the omission and the flag would be misleading.
|
|
76
77
|
if (keyDef.computed !== undefined) {
|
|
77
78
|
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" has required=true AND a computed resolver — a computed key can never be missing; drop one of the two`);
|
|
78
79
|
}
|
|
@@ -160,7 +161,7 @@ export function validateConfigKeyBacking(feature) {
|
|
|
160
161
|
// read/write without it throws loud at request time, not silently.
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
|
-
// --- Config key piiEncrypted compatibility
|
|
164
|
+
// --- Config key piiEncrypted compatibility ---
|
|
164
165
|
export function validateConfigKeyPiiEncrypted(feature) {
|
|
165
166
|
for (const [keyName, keyDef] of Object.entries(feature.configKeys)) {
|
|
166
167
|
if (!keyDef.piiEncrypted)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { qualifyEntityName } from "../qualified-name.js";
|
|
2
|
-
import { findEntityFeature } from "./screens.js";
|
|
2
|
+
import { findEntityFeature } from "./screens-kind-shared.js";
|
|
3
3
|
// Entity name → the one screen that declares detailFor: "<entity>". Built
|
|
4
4
|
// once, up front (before the per-feature validateScreens loop) so both this
|
|
5
|
-
// module's own uniqueness check AND rowAction entity-targets (
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// module's own uniqueness check AND rowAction entity-targets (in screens-actions.ts)
|
|
6
|
+
// resolve against the same map instead of re-walking every feature's
|
|
7
|
+
// screens twice.
|
|
8
8
|
export function collectDetailForScreens(features, featureMap) {
|
|
9
9
|
const screenQnByEntity = new Map();
|
|
10
10
|
const result = new Map();
|
|
@@ -9,6 +9,7 @@ export declare function validateHandlerAccess(feature: FeatureDefinition): void;
|
|
|
9
9
|
export declare function validateAdditionalRateLimits(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
|
|
10
10
|
export declare function validatePerCommandTransaction(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
|
|
11
11
|
export declare function validateTenantlessAnonymous(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
|
|
12
|
+
export declare function validateRateLimitCost(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
|
|
12
13
|
export declare function validateRateLimitDisabledReason(featureName: string, kind: "write" | "query" | "stream", handlerName: string, rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"]): void;
|
|
13
14
|
export declare function validateRateLimitBucketList(featureName: string, kind: "write" | "query" | "stream", handlerName: string, rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"]): void;
|
|
14
15
|
export declare function validateNoAllRoleInHandlerAccess(featureName: string, kind: "write" | "query" | "stream", handlerName: string, access: NonNullable<FeatureDefinition["writeHandlers"][string]["access"]>): void;
|