@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
|
@@ -4,7 +4,7 @@ import { tableExists } from "../db/schema-inspection.js";
|
|
|
4
4
|
import { unsafePushTables } from "../db/table-push.js";
|
|
5
5
|
// Reserved sentinel used in the instance_id column for consumers whose
|
|
6
6
|
// delivery is "shared" — i.e. one cursor across all dispatcher instances
|
|
7
|
-
// (the default
|
|
7
|
+
// (the default behaviour). Per-instance consumers store
|
|
8
8
|
// the concrete instanceId. Postgres PK columns can't be NULL and
|
|
9
9
|
// `NULL = NULL` is UNKNOWN in SQL — a nullable instance_id would break
|
|
10
10
|
// both uniqueness and PK constraints. The boot-validator refuses to
|
|
@@ -62,7 +62,7 @@ export async function createEventConsumerStateTable(db) {
|
|
|
62
62
|
// no need to pre-check columnNamesOf first. Two instances booting
|
|
63
63
|
// concurrently against the same DB (this table is explicitly
|
|
64
64
|
// multi-instance) both ALTER, neither crashes on "column already
|
|
65
|
-
// exists"
|
|
65
|
+
// exists".
|
|
66
66
|
await alterTableAddColumn(db, "kumiko_event_consumers", "rearm_count", "integer", " DEFAULT 0", " NOT NULL",
|
|
67
67
|
/* ifNotExists */ true);
|
|
68
68
|
await alterTableAddColumn(db, "kumiko_event_consumers", "pending_gaps", "jsonb", " DEFAULT '[]'::jsonb", " NOT NULL",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { advanceConsumerPastEventReturning, removePendingGapReturning, selectConsumerForUpdate, selectSnapshotXmax, updateConsumerStatusReturning, } from "../db/queries/event-consumer.js";
|
|
2
2
|
import { selectNextEventIdAfter, selectSmallestVisibleIdInRanges, } from "../db/queries/event-store.js";
|
|
3
|
-
import { coerceRow,
|
|
3
|
+
import { coerceRow, runInNewTransaction, runInSavepointIfSupported, runnerCanBeginTransaction, selectMany, } from "../db/query.js";
|
|
4
|
+
import { extractTableInfo } from "../db/table-info.js";
|
|
4
5
|
import { getEventsHighWaterMark } from "../event-store/index.js";
|
|
5
6
|
import { eventConsumerStateTable, SHARED_INSTANCE_SENTINEL, } from "./event-consumer-state.js";
|
|
6
7
|
import { mergeContiguousGapRanges, rangeContainsId, splitRangeExcludingIds, toIdRanges, } from "./pending-gap-ranges.js";
|
|
@@ -3,7 +3,8 @@ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
|
3
3
|
import { requestContext } from "../api/request-context.js";
|
|
4
4
|
import { insertConsumerIfAbsent, markConsumerProcessing, rearmDeadConsumer, selectConsumerForUpdate, selectConsumerForUpdateSkipLocked, selectProvablyIdleConsumerPairs, updateConsumerDeliveryOutcome, } from "../db/queries/event-consumer.js";
|
|
5
5
|
import { selectEventsHeadId, selectPendingAndNewEventRows, } from "../db/queries/event-store.js";
|
|
6
|
-
import { coerceRow
|
|
6
|
+
import { coerceRow } from "../db/query.js";
|
|
7
|
+
import { extractTableInfo } from "../db/table-info.js";
|
|
7
8
|
import { qnScope } from "../engine/qualified-name.js";
|
|
8
9
|
import { eventsTable, toStoredEvent as rowToStoredEvent, } from "../event-store/index.js";
|
|
9
10
|
import { emitDispatcherError, emitEventConsumerLag, emitEventConsumerPendingGaps, getFallbackMeter, } from "../observability/index.js";
|
|
@@ -30,10 +31,6 @@ const UNPARSEABLE_STORED_WRITE_ORIGIN = {
|
|
|
30
31
|
export async function acquireConsumerState(tx, name, instanceId, rearmCooldownMs, maxRearmCount) {
|
|
31
32
|
const rawState = await selectConsumerForUpdateSkipLocked(tx, name, instanceId);
|
|
32
33
|
if (!rawState) {
|
|
33
|
-
return { state: null, skip: "not_registered" };
|
|
34
|
-
}
|
|
35
|
-
const state = coerceRow(rawState, extractTableInfo(eventConsumerStateTable));
|
|
36
|
-
if (!state) {
|
|
37
34
|
// Either the row never existed (no pre-reg, no ensureRegistered) or
|
|
38
35
|
// another instance currently holds the lock with SKIP LOCKED filtering
|
|
39
36
|
// us out. We can't distinguish here in a single query, so return
|
|
@@ -42,6 +39,7 @@ export async function acquireConsumerState(tx, name, instanceId, rearmCooldownMs
|
|
|
42
39
|
// this path is never taken.
|
|
43
40
|
return { state: null, skip: "not_registered" };
|
|
44
41
|
}
|
|
42
|
+
const state = coerceRow(rawState, extractTableInfo(eventConsumerStateTable));
|
|
45
43
|
if (state.status === ConsumerStatuses.disabled)
|
|
46
44
|
return { state: null, skip: "disabled" };
|
|
47
45
|
if (state.status === ConsumerStatuses.dead) {
|
|
@@ -239,7 +237,7 @@ export async function deliverEvents(consumer, events, context, maxAttempts, stat
|
|
|
239
237
|
// turn (see applyBatch for why). requestId falls back to a fresh id
|
|
240
238
|
// because the dispatcher runs outside any HTTP request (background poll),
|
|
241
239
|
// and a stable log-correlation handle is still useful for debugging.
|
|
242
|
-
//
|
|
240
|
+
// An event this apply writes is attributed to the consumer, not
|
|
243
241
|
// to whatever wrote the triggering event; causationId already links back.
|
|
244
242
|
function buildConsumerRequestScope(consumer, causationSource) {
|
|
245
243
|
const correlationId = causationSource.metadata.correlationId ?? requestContext.generateId();
|
|
@@ -2,6 +2,7 @@ import { extractPgError } from "../db/pg-error.js";
|
|
|
2
2
|
import { recordConsumerPassFailure, selectSnapshotXmax, selectSnapshotXmin, } from "../db/queries/event-consumer.js";
|
|
3
3
|
import { SYSTEM_TENANT_ID } from "../engine/types/identifiers.js";
|
|
4
4
|
import { EVENTS_PUBSUB_CHANNEL } from "../event-store/index.js";
|
|
5
|
+
import { createFallbackLogger } from "../logging/utils.js";
|
|
5
6
|
import { emitEventConsumerPassOutcome, emitEventConsumerPassSkipped, emitEventConsumerRearmExhausted, emitEventDispatcherListenConnected, getFallbackMeter, getFallbackTracer, } from "../observability/index.js";
|
|
6
7
|
import { SHARED_INSTANCE_SENTINEL } from "./event-consumer-state.js";
|
|
7
8
|
import { acquireConsumerState, consumerInstanceId, deliverEvents, emitLagFromTx, fetchPendingEvents, markProcessing, persistConsumerOutcome, preRegisterConsumers, selectIdleConsumerKeys, } from "./event-dispatcher-delivery.js";
|
|
@@ -52,15 +53,9 @@ export function createEventDispatcher(options) {
|
|
|
52
53
|
}
|
|
53
54
|
const tracer = options.tracer ?? getFallbackTracer();
|
|
54
55
|
const meter = options.meter ?? getFallbackMeter();
|
|
55
|
-
// A standalone dispatcher has no logger on its context;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
context.log.error(message);
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
console.error(message);
|
|
62
|
-
}
|
|
63
|
-
};
|
|
56
|
+
// A standalone dispatcher has no logger on its context; the fallback keeps the failure in pod logs.
|
|
57
|
+
const dispatcherLog = createFallbackLogger("event-dispatcher", context.log);
|
|
58
|
+
const logDispatcherError = (message) => dispatcherLog.error(message);
|
|
64
59
|
// Tracks which (consumer, instanceId) pairs already fired
|
|
65
60
|
// kumiko_event_consumer_rearm_exhausted_total, so a consumer stuck dead
|
|
66
61
|
// across many poll passes emits the ops-signal once, not every pass
|
|
@@ -79,7 +74,7 @@ export function createEventDispatcher(options) {
|
|
|
79
74
|
if (count <= PENDING_GAPS_WARN_THRESHOLD || reportedLargeGapSets.has(key))
|
|
80
75
|
return;
|
|
81
76
|
reportedLargeGapSets.add(key);
|
|
82
|
-
logDispatcherError(
|
|
77
|
+
logDispatcherError(`${consumerName} tracks ${count} pending gap ranges (> ${PENDING_GAPS_WARN_THRESHOLD}); ` +
|
|
83
78
|
`a long-running write transaction is probably holding back the snapshot xmin`);
|
|
84
79
|
};
|
|
85
80
|
// Per-(consumer, instanceId) backoff after a thrown pass (see the catch in
|
|
@@ -245,13 +240,13 @@ export function createEventDispatcher(options) {
|
|
|
245
240
|
// consumers see the same disabled-set even if an operator flips a
|
|
246
241
|
// toggle mid-pass, so "this event batch" decisions stay consistent.
|
|
247
242
|
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
// Tier
|
|
253
|
-
//
|
|
254
|
-
// SYSTEM_TENANT_ID
|
|
243
|
+
// Tier composition: the per-tenant resolver is consulted once per pass
|
|
244
|
+
// with SYSTEM_TENANT_ID. Async events are tier-agnostic — if a tenant is
|
|
245
|
+
// downgraded, its queued events are still processed (events are immutable,
|
|
246
|
+
// and the projection is eventually consistent).
|
|
247
|
+
// Tier cuts apply at request time in the sync dispatcher and lifecycle
|
|
248
|
+
// pipeline, not during async replay. The app-level resolver decides what
|
|
249
|
+
// it returns for SYSTEM_TENANT_ID (typically: the union of all tier features).
|
|
255
250
|
const effective = context.effectiveFeatures?.(SYSTEM_TENANT_ID);
|
|
256
251
|
// Lock-free pre-check, once per pass: which consumers are provably idle
|
|
257
252
|
// (no locking, no xid, no WAL — see selectIdleConsumerKeys). Read
|
|
@@ -275,13 +270,7 @@ export function createEventDispatcher(options) {
|
|
|
275
270
|
if (idlePreCheckErrorLogged) {
|
|
276
271
|
idlePreCheckErrorLogged = false;
|
|
277
272
|
emitEventDispatcherListenConnected(meter, listenSubscribed);
|
|
278
|
-
|
|
279
|
-
if (context.log) {
|
|
280
|
-
context.log.info(recoveredMsg);
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
console.log(recoveredMsg);
|
|
284
|
-
}
|
|
273
|
+
dispatcherLog.info("idle pre-check recovered, database reachable again");
|
|
285
274
|
}
|
|
286
275
|
}
|
|
287
276
|
catch (e) {
|
|
@@ -297,7 +286,7 @@ export function createEventDispatcher(options) {
|
|
|
297
286
|
if (isConnectionLossError(e))
|
|
298
287
|
emitEventDispatcherListenConnected(meter, false);
|
|
299
288
|
const msg = e instanceof Error ? e.message : String(e);
|
|
300
|
-
const logMsg = `
|
|
289
|
+
const logMsg = `idle pre-check failed, falling back to per-consumer locking every tick until it recovers: ${msg}`;
|
|
301
290
|
logDispatcherError(logMsg);
|
|
302
291
|
}
|
|
303
292
|
}
|
|
@@ -438,7 +427,7 @@ export function createEventDispatcher(options) {
|
|
|
438
427
|
// callers and at-least-once-with-duplicate-delivery on the next pass;
|
|
439
428
|
// neither is what we want, so ops needs to see it.
|
|
440
429
|
const msg = e instanceof Error ? e.message : String(e);
|
|
441
|
-
logDispatcherError(
|
|
430
|
+
logDispatcherError(`${consumer.name} pass failed: ${msg}`);
|
|
442
431
|
span.setStatus("error", msg);
|
|
443
432
|
// The whole pass rolled back, so markProcessing/persistConsumerOutcome
|
|
444
433
|
// never committed — attempts/last_error are stuck at their pre-pass
|
|
@@ -452,7 +441,7 @@ export function createEventDispatcher(options) {
|
|
|
452
441
|
}
|
|
453
442
|
catch (persistErr) {
|
|
454
443
|
const persistMsg = persistErr instanceof Error ? persistErr.message : String(persistErr);
|
|
455
|
-
const failureMsg =
|
|
444
|
+
const failureMsg = `${consumer.name} failed to persist pass-failure state: ${persistMsg}`;
|
|
456
445
|
logDispatcherError(failureMsg);
|
|
457
446
|
}
|
|
458
447
|
// Escalate the backoff so this consumer isn't retried on the very
|
|
@@ -519,7 +508,7 @@ export function createEventDispatcher(options) {
|
|
|
519
508
|
catch (e) {
|
|
520
509
|
emitEventDispatcherListenConnected(meter, false);
|
|
521
510
|
const msg = e instanceof Error ? e.message : String(e);
|
|
522
|
-
|
|
511
|
+
dispatcherLog.error("pg LISTEN failed", { err: msg });
|
|
523
512
|
}
|
|
524
513
|
}
|
|
525
514
|
},
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TenantId } from "../engine/types/identifiers.ts";
|
|
2
|
+
import type { MemberReader, SessionUser } from "../engine/types/index.ts";
|
|
3
|
+
import type { HandlerContextScope } from "./handler-context-scope.ts";
|
|
4
|
+
export declare function buildAuthFacet(scope: HandlerContextScope): {
|
|
5
|
+
resolveAuthClaims: (claimsUser: SessionUser) => Promise<Record<string, unknown>>;
|
|
6
|
+
resolveActiveMembership: (userId: string, tenantId: TenantId) => Promise<import("@cosmicdrift/kumiko-types/handlers").ActiveMembershipResult>;
|
|
7
|
+
queryAsMember: MemberReader;
|
|
8
|
+
hasFeature: (featureName: string) => Promise<boolean>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { INTERACTIVE_SIGN_IN_POLICY, resolveActiveMembershipFn } from "./active-membership.js";
|
|
2
|
+
import { resolveAuthClaimsFn } from "./dispatch-shared.js";
|
|
3
|
+
import { createMemberReaderFn } from "./member-reader.js";
|
|
4
|
+
import { createGatedMemberReader, systemIdentitySwitchDenied } from "./system-identity-switch.js";
|
|
5
|
+
export function buildAuthFacet(scope) {
|
|
6
|
+
const { ctx, type, user, tx, hasIdentitySwitchGrant, identitySwitchAudit } = scope;
|
|
7
|
+
const { effectiveFeatures } = ctx;
|
|
8
|
+
// Lazy — creates the reader (which fails closed on SYSTEM_TENANT_ID) only
|
|
9
|
+
// on first actual use, not on every HandlerContext build.
|
|
10
|
+
let ungatedMemberReader;
|
|
11
|
+
const queryAsMember = createGatedMemberReader(`handler "${type}"`, hasIdentitySwitchGrant, (userId, qn, payload) => {
|
|
12
|
+
ungatedMemberReader ??= createMemberReaderFn(ctx, user.tenantId, tx);
|
|
13
|
+
return ungatedMemberReader(userId, qn, payload);
|
|
14
|
+
}, { ...identitySwitchAudit, tenantId: user.tenantId });
|
|
15
|
+
return {
|
|
16
|
+
// Thin pass-through: one resolve impl lives on the dispatcher, the
|
|
17
|
+
// handler surface just forwards the call so both entry points (login
|
|
18
|
+
// handler via ctx.resolveAuthClaims, switch-tenant route via
|
|
19
|
+
// dispatcher.resolveAuthClaims) cannot drift.
|
|
20
|
+
resolveAuthClaims: (claimsUser) => resolveAuthClaimsFn(ctx, claimsUser), // @wrapper-known semantic-alias
|
|
21
|
+
// Thin pass-through, same reasoning as resolveAuthClaims above — one
|
|
22
|
+
// resolve impl lives on the dispatcher so callers can't drift apart.
|
|
23
|
+
// Internally queries memberships as SYSTEM, so it needs the same grant
|
|
24
|
+
// as a SYSTEM queryAs/writeAs (r.systemScope() or escapeHatch).
|
|
25
|
+
resolveActiveMembership: (userId, tenantId) => {
|
|
26
|
+
if (!hasIdentitySwitchGrant)
|
|
27
|
+
throw systemIdentitySwitchDenied(`handler "${type}"`);
|
|
28
|
+
return resolveActiveMembershipFn(ctx, userId, tenantId, INTERACTIVE_SIGN_IN_POLICY); // @wrapper-known semantic-alias
|
|
29
|
+
},
|
|
30
|
+
// Needs the same grant as a SYSTEM queryAs.
|
|
31
|
+
queryAsMember,
|
|
32
|
+
// Feature-effective check for in-handler opt-in logic. Scope:
|
|
33
|
+
// **current user's tenant** — for cross-tenant lookups (rare,
|
|
34
|
+
// SysAdmin operations) read effectiveFeatures(otherTenantId) directly.
|
|
35
|
+
// When the feature-toggles or tier-engine feature isn't wired (no
|
|
36
|
+
// effectiveFeatures callback), always returns true — apps without
|
|
37
|
+
// tier-cuts treat all features on.
|
|
38
|
+
//
|
|
39
|
+
// Falls back to the live trialGate when the sync set says the feature
|
|
40
|
+
// is off — the sync set never contains trial-tier features (time-
|
|
41
|
+
// derived, can't boot-cache), so without this a trial tenant checking
|
|
42
|
+
// a companion feature's toggle would silently read `false` even though
|
|
43
|
+
// the dispatch gate already lets trial-tier handlers run.
|
|
44
|
+
hasFeature: async (featureName) => {
|
|
45
|
+
if (!effectiveFeatures)
|
|
46
|
+
return true;
|
|
47
|
+
if (effectiveFeatures(user.tenantId).has(featureName))
|
|
48
|
+
return true;
|
|
49
|
+
if (!effectiveFeatures.trialGate)
|
|
50
|
+
return false;
|
|
51
|
+
return effectiveFeatures.trialGate(user.tenantId, featureName);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type TenantDb } from "../db/tenant-db.ts";
|
|
2
|
+
import type { TenantId } from "../engine/types/identifiers.ts";
|
|
3
|
+
import type { ConfigAccessor } from "../engine/types/index.ts";
|
|
4
|
+
import { createTzContext } from "../time/index.ts";
|
|
5
|
+
import type { HandlerContextScope } from "./handler-context-scope.ts";
|
|
6
|
+
export declare function buildConfigFacet(scope: HandlerContextScope, db: TenantDb | undefined): {
|
|
7
|
+
config: ConfigAccessor | undefined;
|
|
8
|
+
fragment: {
|
|
9
|
+
config?: ConfigAccessor | undefined;
|
|
10
|
+
configFor?: ((targetTenantId: TenantId) => ConfigAccessor) | undefined;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export declare function resolveHandlerTz(scope: HandlerContextScope, config: ConfigAccessor | undefined): Promise<ReturnType<typeof createTzContext>>;
|
|
14
|
+
export declare function resolveHandlerLocale(scope: HandlerContextScope): string;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createTenantDb } from "../db/tenant-db.js";
|
|
2
|
+
import { createSystemUser } from "../engine/system-user.js";
|
|
3
|
+
import { AccessDeniedError } from "../errors/index.js";
|
|
4
|
+
import { DEFAULT_LOCALE, isValidLocaleTag } from "../i18n/request-locale.js";
|
|
5
|
+
import { createTzContext, isValidIanaTimeZone } from "../time/index.js";
|
|
6
|
+
import { configAccessorFactoryOf } from "./config-accessor-factory.js";
|
|
7
|
+
import { isSystemIdentity } from "./system-identity-switch.js";
|
|
8
|
+
import { TENANT_TIMEZONE_CONFIG_KEY } from "./tenant-timezone-config-key.js";
|
|
9
|
+
// Cross-tenant accessor for SystemAdmin/system callers. The
|
|
10
|
+
// identity is the target tenant's system user so the caller's own user-scope
|
|
11
|
+
// values never resolve into another tenant's config; the db is tenant-mode
|
|
12
|
+
// scoped to the target.
|
|
13
|
+
function buildConfigFor(scope, configFactory, config) {
|
|
14
|
+
const { user, context, registry, dbSource } = scope;
|
|
15
|
+
return configFactory && dbSource
|
|
16
|
+
? (targetTenantId) => {
|
|
17
|
+
if (targetTenantId === user.tenantId && config)
|
|
18
|
+
return config;
|
|
19
|
+
if (!isSystemIdentity(user) && !user.roles.includes("SystemAdmin")) {
|
|
20
|
+
throw new AccessDeniedError({
|
|
21
|
+
message: "ctx.configFor(otherTenant) requires the system role or SystemAdmin",
|
|
22
|
+
details: { reason: "config_for_requires_system_admin" },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const targetSystemUser = createSystemUser(targetTenantId);
|
|
26
|
+
return configFactory({
|
|
27
|
+
user: { id: targetSystemUser.id, tenantId: targetTenantId },
|
|
28
|
+
db: createTenantDb({
|
|
29
|
+
db: dbSource,
|
|
30
|
+
tenantId: targetTenantId,
|
|
31
|
+
mode: "tenant",
|
|
32
|
+
tracer: context.tracer,
|
|
33
|
+
meter: context.meter,
|
|
34
|
+
grants: { projectionRegistry: registry },
|
|
35
|
+
}),
|
|
36
|
+
secrets: context.secrets,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
export function buildConfigFacet(scope, db) {
|
|
42
|
+
const { user, context } = scope;
|
|
43
|
+
// Mirror notify: only built when the config feature wired its factory.
|
|
44
|
+
const configFactory = configAccessorFactoryOf(context);
|
|
45
|
+
const config = configFactory && db
|
|
46
|
+
? configFactory({
|
|
47
|
+
user: { id: user.id, tenantId: user.tenantId },
|
|
48
|
+
db,
|
|
49
|
+
secrets: context.secrets,
|
|
50
|
+
})
|
|
51
|
+
: undefined;
|
|
52
|
+
const configFor = buildConfigFor(scope, configFactory, config);
|
|
53
|
+
return { config, fragment: { ...(config && { config }), ...(configFor && { configFor }) } };
|
|
54
|
+
}
|
|
55
|
+
async function readTenantTimezone(scope, config) {
|
|
56
|
+
const { ctx, user, tx } = scope;
|
|
57
|
+
if (config === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
const cachedTz = ctx.tenantTimezoneCache.get(user.tenantId);
|
|
60
|
+
if (cachedTz)
|
|
61
|
+
return cachedTz.value;
|
|
62
|
+
const generationAtRead = ctx.tenantTimezoneCache.generation();
|
|
63
|
+
const tenantTz = await config(TENANT_TIMEZONE_CONFIG_KEY);
|
|
64
|
+
if (tx === undefined) {
|
|
65
|
+
ctx.tenantTimezoneCache.setIfFresh(user.tenantId, typeof tenantTz === "string" ? tenantTz : undefined, generationAtRead);
|
|
66
|
+
}
|
|
67
|
+
return tenantTz;
|
|
68
|
+
}
|
|
69
|
+
// ctx.tz is always present. tenant reads tenant:config:timezone through
|
|
70
|
+
// the already-built `config` accessor (raw qualified-key — no import of
|
|
71
|
+
// the tenant feature, framework/pipeline stays bundled-features-free);
|
|
72
|
+
// falls back to "UTC" when config is missing or the key is unset. user
|
|
73
|
+
// comes from SessionUser.timezone (set at login), else falls back to
|
|
74
|
+
// tenant (createTzContext's own default). An app-injected GeoTzProvider
|
|
75
|
+
// (context.geoTzProvider) feeds ctx.tz.fromCoordinates / fromAddress.
|
|
76
|
+
//
|
|
77
|
+
// Resolving this via `config()` is a config-table SELECT on every single
|
|
78
|
+
// dispatch — ctx.tenantTimezoneCache memoizes it per tenant.
|
|
79
|
+
// dispatch-write.ts invalidates on config:write:set/reset for this key.
|
|
80
|
+
// Only populate from a tx-free read (tx === undefined): a write-path
|
|
81
|
+
// lookup runs inside the caller's own open transaction, and caching a
|
|
82
|
+
// value read there could memoize state from a write earlier in that same
|
|
83
|
+
// transaction before it's known to commit. Reads always consult the
|
|
84
|
+
// cache regardless of tx, since serving a slightly stale cached value on
|
|
85
|
+
// the write path is harmless (bounded by ttlMs) and avoids reintroducing
|
|
86
|
+
// the SELECT on the hot path this fix targets.
|
|
87
|
+
export async function resolveHandlerTz(scope, config) {
|
|
88
|
+
const { user, context } = scope;
|
|
89
|
+
const tenantTz = await readTenantTimezone(scope, config);
|
|
90
|
+
// Guarded against garbage: an unvalidated string here (free-form config
|
|
91
|
+
// key, legacy JWT claim predating validation) blows up every ctx.tz call
|
|
92
|
+
// for the whole tenant with a RangeError. Fall back to UTC/tenant instead
|
|
93
|
+
// of trusting the raw value.
|
|
94
|
+
const safeTenantTz = typeof tenantTz === "string" && isValidIanaTimeZone(tenantTz) ? tenantTz : "UTC";
|
|
95
|
+
const safeUserTz = user.timezone !== undefined && isValidIanaTimeZone(user.timezone) ? user.timezone : undefined;
|
|
96
|
+
return createTzContext({
|
|
97
|
+
...(context.geoTzProvider !== undefined ? { geoTz: context.geoTzProvider } : {}),
|
|
98
|
+
...(context.clock !== undefined ? { clock: context.clock } : {}),
|
|
99
|
+
tenant: safeTenantTz,
|
|
100
|
+
...(safeUserTz !== undefined && { user: safeUserTz }),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// ctx.locale — request-layer signal (X-Locale header → Accept-Language,
|
|
104
|
+
// resolved once at the HTTP boundary by request-id-middleware.ts) wins;
|
|
105
|
+
// then SessionUser.locale (set at login — stale until re-login, same
|
|
106
|
+
// tradeoff as ctx.tz.user); falls back to the app's
|
|
107
|
+
// boot-configured defaultLocale, then DEFAULT_LOCALE. Mirrors ctx.tz's
|
|
108
|
+
// Request → Session → Boot-Default chain.
|
|
109
|
+
export function resolveHandlerLocale(scope) {
|
|
110
|
+
const { user, reqCtx, context } = scope;
|
|
111
|
+
const safeUserLocale = user.locale !== undefined && isValidLocaleTag(user.locale) ? user.locale : undefined;
|
|
112
|
+
return reqCtx?.locale ?? safeUserLocale ?? context.defaultLocale ?? DEFAULT_LOCALE;
|
|
113
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DbConnection, DbTx } from "../db/connection.ts";
|
|
2
|
+
import { type TenantDb } from "../db/tenant-db.ts";
|
|
3
|
+
import type { TenantId } from "../engine/types/identifiers.ts";
|
|
4
|
+
import type { TenantWriteTarget } from "../engine/types/index.ts";
|
|
5
|
+
import type { HandlerContextScope } from "./handler-context-scope.ts";
|
|
6
|
+
export declare function buildTenantScopedDb(scope: HandlerContextScope, source: DbConnection | DbTx, signal: AbortSignal | undefined, foreignTenantId?: TenantId, denyWrites?: boolean): TenantDb;
|
|
7
|
+
export declare function buildHandlerDbFacet(scope: HandlerContextScope): {
|
|
8
|
+
db: TenantDb | undefined;
|
|
9
|
+
exposedDb: TenantDb | undefined;
|
|
10
|
+
outsideTxSource: DbConnection | undefined;
|
|
11
|
+
fragment: {
|
|
12
|
+
db: TenantDb | undefined;
|
|
13
|
+
dbOutsideTransaction: TenantDb | undefined;
|
|
14
|
+
systemDb?: import("@cosmicdrift/kumiko-types/tenant-db-types").UncheckedSystemDb | undefined;
|
|
15
|
+
crossTenantReads?: import("@cosmicdrift/kumiko-types/tenant-db-types").CrossTenantReads | undefined;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export declare function buildForTenantFragment(scope: HandlerContextScope, exposedDb: TenantDb | undefined, readOnlyForTenant: boolean): {
|
|
19
|
+
forTenant?: ((targetTenantId: TenantId) => TenantWriteTarget) | undefined;
|
|
20
|
+
};
|
|
21
|
+
export declare function buildFileFragment(scope: HandlerContextScope, db: TenantDb | undefined): {
|
|
22
|
+
files?: import("@cosmicdrift/kumiko-types/file-handle-types").FileContext | undefined;
|
|
23
|
+
derivatives?: import("@cosmicdrift/kumiko-types/derivatives-types").DerivativesContext | undefined;
|
|
24
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { crossTenantReadsGrantFor, escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
+
import { isAgentToolCallScope } from "../api/request-context.js";
|
|
3
|
+
import { createHandlerCrossTenantReads } from "../db/cross-tenant-reads.js";
|
|
4
|
+
import { createTenantDb, createUncheckedSystemDb } from "../db/tenant-db.js";
|
|
5
|
+
import { createDerivativesContext } from "../derivatives/derivatives-context.js";
|
|
6
|
+
import { mayOverrideTenant } from "../engine/cross-tenant.js";
|
|
7
|
+
import { AccessDeniedError, FrameworkReasons } from "../errors/index.js";
|
|
8
|
+
import { createTenantFileContext } from "../files/file-handle.js";
|
|
9
|
+
import { createSystemScopedDbGuard, isMemberResolutionPrincipal, resolveAppPoolDb, } from "./dispatch-shared.js";
|
|
10
|
+
import { buildPersonalDataGate } from "./write-origin.js";
|
|
11
|
+
export function buildTenantScopedDb(scope, source, signal, foreignTenantId, denyWrites = false) {
|
|
12
|
+
const { user, context, registry, origin, isSystem, writeEscapeHatch, handlerEscapeHatch } = scope;
|
|
13
|
+
return createTenantDb({
|
|
14
|
+
db: source,
|
|
15
|
+
tenantId: foreignTenantId ?? user.tenantId,
|
|
16
|
+
mode: isSystem && foreignTenantId === undefined ? "system" : "tenant",
|
|
17
|
+
tracer: context.tracer,
|
|
18
|
+
meter: context.meter,
|
|
19
|
+
signal,
|
|
20
|
+
grants: {
|
|
21
|
+
...(foreignTenantId === undefined && {
|
|
22
|
+
globalWrites: escapeHatchFor(writeEscapeHatch, "globalWrites"),
|
|
23
|
+
unsafeRaw: escapeHatchFor(handlerEscapeHatch, "unsafeRaw"),
|
|
24
|
+
}),
|
|
25
|
+
report: scope.reportEscapeHatch,
|
|
26
|
+
memberReadOnly: isMemberResolutionPrincipal(user),
|
|
27
|
+
personalDataGate: buildPersonalDataGate(registry, origin),
|
|
28
|
+
projectionRegistry: registry,
|
|
29
|
+
...(denyWrites && { writesDeniedReason: "without an open transaction" }),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function buildHandlerCrossTenantReads(scope) {
|
|
34
|
+
const { user, context, registry, origin, dbSource, reqCtx, handlerEscapeHatch } = scope;
|
|
35
|
+
// An agent tool call runs with the user's session but feeds the LLM context, so
|
|
36
|
+
// cross-tenant rows must never reach it, even when unsafeRaw implies the grant.
|
|
37
|
+
const crossTenantReadsReason = isAgentToolCallScope()
|
|
38
|
+
? undefined
|
|
39
|
+
: crossTenantReadsGrantFor(handlerEscapeHatch)?.reason;
|
|
40
|
+
return dbSource && crossTenantReadsReason !== undefined
|
|
41
|
+
? createHandlerCrossTenantReads({
|
|
42
|
+
db: dbSource,
|
|
43
|
+
tenantId: user.tenantId,
|
|
44
|
+
tracer: context.tracer,
|
|
45
|
+
meter: context.meter,
|
|
46
|
+
signal: reqCtx?.signal,
|
|
47
|
+
reason: crossTenantReadsReason,
|
|
48
|
+
report: scope.reportEscapeHatch,
|
|
49
|
+
grants: {
|
|
50
|
+
report: scope.reportEscapeHatch,
|
|
51
|
+
memberReadOnly: isMemberResolutionPrincipal(user),
|
|
52
|
+
personalDataGate: buildPersonalDataGate(registry, origin),
|
|
53
|
+
projectionRegistry: registry,
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
: undefined;
|
|
57
|
+
}
|
|
58
|
+
export function buildHandlerDbFacet(scope) {
|
|
59
|
+
const { ctx, type, isSystem, dbSource, reqCtx, denyDbWrites } = scope;
|
|
60
|
+
// Propagate the request's AbortSignal so every TenantDb query throws when
|
|
61
|
+
// the client has disconnected — handlers with many sequential queries skip
|
|
62
|
+
// the rest of the chain instead of burning DB-CPU for results no one reads.
|
|
63
|
+
const db = dbSource
|
|
64
|
+
? buildTenantScopedDb(scope, dbSource, reqCtx?.signal, undefined, denyDbWrites)
|
|
65
|
+
: undefined;
|
|
66
|
+
// Unbound pool, tenant-scoped like `db` but never tx-bound — writes
|
|
67
|
+
// through it survive a rollback of the handler's own transaction. No
|
|
68
|
+
// AbortSignal here: a client disconnect must not abort a durability write
|
|
69
|
+
// that is meant to outlive the request. Computed before `systemDb` below
|
|
70
|
+
// so its guarded escape hatch (ctx.systemDb.outsideTransaction) can wrap
|
|
71
|
+
// this same TenantDb instead of a second, independently-built one.
|
|
72
|
+
const outsideTxSource = ctx.outsideTransactionDb ?? resolveAppPoolDb(ctx);
|
|
73
|
+
const rawDbOutsideTransaction = outsideTxSource
|
|
74
|
+
? buildTenantScopedDb(scope, outsideTxSource, undefined)
|
|
75
|
+
: undefined;
|
|
76
|
+
const systemDb = isSystem && db
|
|
77
|
+
? createUncheckedSystemDb(db, rawDbOutsideTransaction, scope.reportEscapeHatch)
|
|
78
|
+
: undefined;
|
|
79
|
+
const crossTenantReads = buildHandlerCrossTenantReads(scope);
|
|
80
|
+
// Exposed as ctx.db below — the internal `db` above stays the real,
|
|
81
|
+
// working TenantDb for config/derivatives accessors and systemDb
|
|
82
|
+
// construction.
|
|
83
|
+
const exposedDb = isSystem && db
|
|
84
|
+
? createSystemScopedDbGuard(type, "db", "ctx.systemDb.assertTenantMatch(...) or ctx.systemDb.acknowledgeCrossTenant(...)")
|
|
85
|
+
: db;
|
|
86
|
+
// Same fail-closed treatment as `exposedDb` — a system handler reaching
|
|
87
|
+
// ctx.dbOutsideTransaction directly would be a second, unguarded door to
|
|
88
|
+
// the same cross-tenant access ctx.db closes off above.
|
|
89
|
+
const dbOutsideTransaction = isSystem && rawDbOutsideTransaction
|
|
90
|
+
? createSystemScopedDbGuard(type, "dbOutsideTransaction", "ctx.systemDb.outsideTransaction.assertTenantMatch(...) or " +
|
|
91
|
+
"ctx.systemDb.outsideTransaction.acknowledgeCrossTenant(...)")
|
|
92
|
+
: rawDbOutsideTransaction;
|
|
93
|
+
return {
|
|
94
|
+
db,
|
|
95
|
+
exposedDb,
|
|
96
|
+
outsideTxSource,
|
|
97
|
+
fragment: {
|
|
98
|
+
db: exposedDb,
|
|
99
|
+
dbOutsideTransaction,
|
|
100
|
+
...(systemDb && { systemDb }),
|
|
101
|
+
...(crossTenantReads && { crossTenantReads }),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Write handlers only, and not r.systemScope() ones (they have ctx.systemDb). The gate is the
|
|
106
|
+
// dispatcher's tenantIdOverride predicate, so no caller reaches more than it could via
|
|
107
|
+
// tenantIdOverride. The foreign db carries no unsafeRaw/globalWrites grant and is tenant mode
|
|
108
|
+
// bound to the target; the executor accepts streamTenantId only on a db bound to that tenant.
|
|
109
|
+
export function buildForTenantFragment(scope, exposedDb, readOnlyForTenant) {
|
|
110
|
+
const { user, isSystem, dbSource, isWriteHandler, reqCtx, denyDbWrites, isPerCommand } = scope;
|
|
111
|
+
const forTenant = !isSystem && dbSource && isWriteHandler
|
|
112
|
+
? (targetTenantId) => {
|
|
113
|
+
if (targetTenantId === user.tenantId && exposedDb) {
|
|
114
|
+
// Only perCommand handlers lose forTenant writes; reservations keep their pre-transaction contract.
|
|
115
|
+
const ownTenantDb = denyDbWrites && !isPerCommand
|
|
116
|
+
? buildTenantScopedDb(scope, dbSource, reqCtx?.signal, undefined, readOnlyForTenant)
|
|
117
|
+
: exposedDb;
|
|
118
|
+
return { db: ownTenantDb, streamTenantId: undefined };
|
|
119
|
+
}
|
|
120
|
+
if (!mayOverrideTenant(user)) {
|
|
121
|
+
throw new AccessDeniedError({
|
|
122
|
+
details: { reason: FrameworkReasons.tenantOverrideRequiresSystemAdmin },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
db: buildTenantScopedDb(scope, dbSource, reqCtx?.signal, targetTenantId, denyDbWrites && (isPerCommand || readOnlyForTenant)),
|
|
127
|
+
streamTenantId: targetTenantId,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
: undefined;
|
|
131
|
+
return { ...(forTenant && { forTenant }) };
|
|
132
|
+
}
|
|
133
|
+
// ctx.files resolved per-tenant through file-foundation (lazy — the
|
|
134
|
+
// provider is only resolved when a handle actually does I/O). Boot wires
|
|
135
|
+
// _fileProviderResolver when a file-provider plugin is mounted; falls back
|
|
136
|
+
// to a statically-injected context.files (tests).
|
|
137
|
+
export function buildFileFragment(scope, db) {
|
|
138
|
+
const { context, registry, user } = scope;
|
|
139
|
+
const fileResolver = context._fileProviderResolver;
|
|
140
|
+
const files = fileResolver ? createTenantFileContext(fileResolver, user.tenantId) : context.files;
|
|
141
|
+
// ctx.derivatives builds on ctx.files (needs a FileContext to read the
|
|
142
|
+
// original + write the variant) plus db (to look up the FileRef row) — so
|
|
143
|
+
// it can only be constructed exactly when files+db both resolved; falls
|
|
144
|
+
// back to a statically-injected context.derivatives otherwise (tests).
|
|
145
|
+
const derivatives = files && db
|
|
146
|
+
? createDerivativesContext({ files, registry, db, tenantId: user.tenantId })
|
|
147
|
+
: context.derivatives;
|
|
148
|
+
return { ...(files && { files }), ...(derivatives && { derivatives }) };
|
|
149
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { WriteResult } from "../engine/types/index.ts";
|
|
2
|
+
import { type HandlerTransactionScope } from "./dispatch-shared.ts";
|
|
3
|
+
import { type AfterCommitHook } from "./dispatcher-utils.ts";
|
|
4
|
+
import type { HandlerContextScope } from "./handler-context-scope.ts";
|
|
5
|
+
export type CommitSinks = {
|
|
6
|
+
readonly bridgeSink: AfterCommitHook[];
|
|
7
|
+
readonly hasLiveCommitSink: boolean;
|
|
8
|
+
readonly notifyCommitSink: AfterCommitHook[] | undefined;
|
|
9
|
+
};
|
|
10
|
+
export declare function createCommitSinks(afterCommitHooks: AfterCommitHook[] | undefined, transactionScope: HandlerTransactionScope, outerNotifySink: AfterCommitHook[] | undefined): CommitSinks;
|
|
11
|
+
export declare function buildDispatchFacet(scope: HandlerContextScope, sinks: CommitSinks): {
|
|
12
|
+
bridge: {
|
|
13
|
+
query: (targetType: string, payload: unknown) => Promise<unknown>;
|
|
14
|
+
queryAs: import("./system-identity-switch.ts").QueryAsFn;
|
|
15
|
+
write: (targetType: string, payload: unknown) => Promise<WriteResult>;
|
|
16
|
+
writeAs: import("./system-identity-switch.ts").WriteAsFn;
|
|
17
|
+
};
|
|
18
|
+
scheduleFragment: {
|
|
19
|
+
scheduleAfterCommit?: ((hook: AfterCommitHook) => void) | undefined;
|
|
20
|
+
};
|
|
21
|
+
};
|