@cosmicdrift/kumiko-framework 0.386.0 → 0.388.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -1
- package/LICENSE +21 -57
- package/README.md +2 -2
- package/agents/examples/anonymous-access.md +1 -1
- package/agents/examples/apex-landing.md +204 -0
- package/agents/examples/api-worker-split.md +64 -0
- package/agents/examples/app-config.md +14 -0
- package/agents/examples/auth-mfa.md +115 -0
- package/agents/examples/env-composition.md +28 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/prod-boot-options.md +16 -0
- package/agents/examples/rate-limiting.md +48 -0
- package/agents/examples/record-detail-layout.md +692 -0
- package/agents/examples/seed-migration.md +212 -0
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +40 -39
- package/dist/api/api-constants.js +2 -2
- package/dist/api/auth-middleware.js +7 -7
- package/dist/api/auth-rate-limiters.js +7 -3
- package/dist/api/auth-routes-mfa.js +1 -1
- package/dist/api/auth-routes-session.js +1 -1
- package/dist/api/auth-routes-shared.js +7 -3
- package/dist/api/auth-routes-signup.js +1 -1
- package/dist/api/client-error-route.js +8 -2
- package/dist/api/client-ip.js +3 -1
- package/dist/api/extra-route.js +1 -1
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/pii-leak-guard.js +4 -2
- package/dist/api/readiness.js +5 -5
- package/dist/api/redis-sse-broker.js +8 -6
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.js +2 -2
- package/dist/api/server-boot-checks.d.ts +10 -0
- package/dist/api/server-boot-checks.js +157 -0
- package/dist/api/server-event-dispatcher.d.ts +26 -0
- package/dist/api/server-event-dispatcher.js +208 -0
- package/dist/api/server-extra-routes.d.ts +11 -0
- package/dist/api/server-extra-routes.js +203 -0
- package/dist/api/server-middleware.d.ts +20 -0
- package/dist/api/server-middleware.js +211 -0
- package/dist/api/server-routes.d.ts +19 -0
- package/dist/api/server-routes.js +293 -0
- package/dist/api/server-websocket-routes.d.ts +4 -0
- package/dist/api/server-websocket-routes.js +108 -0
- package/dist/api/server-wiring.d.ts +32 -0
- package/dist/api/server-wiring.js +236 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1250
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-broker.js +1 -1
- package/dist/api/sse-route.d.ts +13 -13
- package/dist/api/sse-route.js +31 -27
- package/dist/auth/index.js +4 -4
- package/dist/auth/roles.d.ts +4 -4
- package/dist/auth/roles.js +24 -24
- package/dist/compliance/duration-spec.js +18 -20
- package/dist/compliance/index.js +2 -3
- package/dist/compliance/override-schema.d.ts +4 -4
- package/dist/compliance/override-schema.js +19 -22
- package/dist/compliance/profiles.d.ts +26 -29
- package/dist/compliance/profiles.js +62 -69
- package/dist/compliance/sub-processors.d.ts +28 -30
- package/dist/compliance/sub-processors.js +17 -21
- package/dist/consumer-cli.js +4 -4
- package/dist/crypto/blind-index.js +18 -17
- package/dist/crypto/ciphertext-pattern.js +1 -1
- package/dist/crypto/event-pii.js +3 -3
- package/dist/crypto/kek-source.d.ts +1 -1
- package/dist/crypto/kek-source.js +3 -2
- package/dist/crypto/kms-adapter.d.ts +1 -1
- package/dist/crypto/kms-adapter.js +4 -6
- package/dist/crypto/kms-wiring.d.ts +1 -2
- package/dist/crypto/kms-wiring.js +2 -2
- package/dist/crypto/owner-transition-rekey.d.ts +5 -1
- package/dist/crypto/owner-transition-rekey.js +1 -1
- package/dist/crypto/pg-kms-adapter.js +2 -6
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +7 -5
- package/dist/crypto/subject-resolver.d.ts +1 -1
- package/dist/crypto/subject-resolver.js +2 -2
- package/dist/db/aggregate-sql.d.ts +21 -0
- package/dist/db/aggregate-sql.js +155 -0
- package/dist/db/apply-entity-event.d.ts +6 -6
- package/dist/db/apply-entity-event.js +55 -55
- package/dist/db/blind-index-cleanup.js +7 -7
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +95 -0
- package/dist/db/collect-table-metas.js +10 -11
- package/dist/db/compound-types.d.ts +1 -1
- package/dist/db/compound-types.js +12 -12
- package/dist/db/cursor.js +3 -4
- package/dist/db/dialect.d.ts +2 -2
- package/dist/db/dialect.js +3 -3
- package/dist/db/distinct-on.d.ts +1 -1
- package/dist/db/driver-row-coercion.d.ts +2 -0
- package/dist/db/driver-row-coercion.js +186 -0
- package/dist/db/driver-value-preparation.d.ts +16 -0
- package/dist/db/driver-value-preparation.js +99 -0
- package/dist/db/eagerload.d.ts +3 -3
- package/dist/db/eagerload.js +21 -12
- package/dist/db/entity-field-encryption.d.ts +1 -0
- package/dist/db/entity-field-encryption.js +1 -1
- package/dist/db/entity-table-meta.d.ts +2 -2
- package/dist/db/entity-table-meta.js +105 -112
- package/dist/db/event-store-executor-context.d.ts +3 -2
- package/dist/db/event-store-executor-context.js +13 -13
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +112 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +203 -0
- package/dist/db/event-store-executor-read-paging.d.ts +14 -0
- package/dist/db/event-store-executor-read-paging.js +99 -0
- package/dist/db/event-store-executor-read-rows.d.ts +6 -0
- package/dist/db/event-store-executor-read-rows.js +32 -0
- package/dist/db/event-store-executor-read-search.d.ts +28 -0
- package/dist/db/event-store-executor-read-search.js +221 -0
- package/dist/db/event-store-executor-read-sort.d.ts +41 -0
- package/dist/db/event-store-executor-read-sort.js +130 -0
- package/dist/db/event-store-executor-read-where.d.ts +24 -0
- package/dist/db/event-store-executor-read-where.js +81 -0
- package/dist/db/event-store-executor-read.d.ts +2 -9
- package/dist/db/event-store-executor-read.js +50 -704
- package/dist/db/event-store-executor-write-steps.d.ts +20 -0
- package/dist/db/event-store-executor-write-steps.js +100 -0
- package/dist/db/event-store-executor-write.js +401 -510
- package/dist/db/index.d.ts +6 -2
- package/dist/db/index.js +5 -1
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/located-timestamp.d.ts +11 -11
- package/dist/db/located-timestamp.js +29 -30
- package/dist/db/migrate-generator.d.ts +1 -1
- package/dist/db/migrate-generator.js +97 -105
- package/dist/db/migrate-runner.js +29 -26
- package/dist/db/money.d.ts +6 -6
- package/dist/db/money.js +29 -27
- package/dist/db/queries/backfill-pii.js +5 -5
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/es-ops.d.ts +1 -1
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +3 -3
- package/dist/db/queries/event-store.js +4 -2
- package/dist/db/queries/projection-rebuild.js +3 -3
- package/dist/db/queries/raw-sql.d.ts +2 -2
- package/dist/db/queries/raw-sql.js +2 -2
- package/dist/db/queries/shadow-swap.js +19 -19
- package/dist/db/query.d.ts +3 -16
- package/dist/db/query.js +8 -761
- package/dist/db/rebuild-marker.js +20 -21
- package/dist/db/reference-data.js +6 -1
- package/dist/db/replay-migration-sql.js +2 -2
- package/dist/db/schema-inspection.js +4 -5
- package/dist/db/table-builder.d.ts +1 -2
- package/dist/db/table-builder.js +137 -190
- package/dist/db/table-info.d.ts +10 -0
- package/dist/db/table-info.js +45 -0
- package/dist/db/temporal-value-guards.d.ts +6 -0
- package/dist/db/temporal-value-guards.js +15 -0
- package/dist/db/tenant-db-runner.d.ts +1 -0
- package/dist/db/tenant-db-runner.js +3 -0
- package/dist/db/tenant-db.d.ts +2 -2
- package/dist/db/tenant-db.js +48 -11
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +6 -2
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/access-roles.js +4 -3
- package/dist/engine/boot-validator/api-ext.js +14 -14
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +7 -6
- package/dist/engine/boot-validator/detail-screens.js +4 -4
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +77 -61
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/gdpr-storage.js +4 -4
- package/dist/engine/boot-validator/i18n-keys.js +6 -4
- package/dist/engine/boot-validator/index.d.ts +2 -2
- package/dist/engine/boot-validator/index.js +22 -20
- package/dist/engine/boot-validator/nav.js +5 -5
- package/dist/engine/boot-validator/ownership.js +12 -9
- package/dist/engine/boot-validator/parent-ref.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +258 -187
- package/dist/engine/boot-validator/projection-list-screens.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +1 -1
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +23 -17
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +12 -17
- package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
- package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
- package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
- package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
- package/dist/engine/boot-validator/screens-lists.js +26 -24
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +102 -869
- package/dist/engine/boot-validator/security-baseline.js +3 -2
- package/dist/engine/boot-validator/transfer-graph.js +2 -2
- package/dist/engine/boot-validator/workspaces.js +3 -3
- package/dist/engine/bridge-stub.js +3 -4
- package/dist/engine/build-app-schema.js +130 -170
- package/dist/engine/build-config-feature-schema.js +50 -50
- package/dist/engine/build-target.js +20 -23
- package/dist/engine/codemod/pipeline-codemod.js +20 -13
- package/dist/engine/config-helpers.js +4 -4
- package/dist/engine/constants.js +2 -2
- package/dist/engine/create-app.js +2 -2
- package/dist/engine/define-feature.js +21 -13
- package/dist/engine/define-handler.js +13 -11
- package/dist/engine/define-step.js +1 -1
- package/dist/engine/define-workflow.d.ts +5 -5
- package/dist/engine/define-workflow.js +6 -6
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +22 -34
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/event-def.js +2 -2
- package/dist/engine/extension-names.d.ts +16 -17
- package/dist/engine/extension-names.js +37 -38
- package/dist/engine/extensions/escape-hatch-usage.js +1 -1
- package/dist/engine/extensions/tenant-data.js +2 -2
- package/dist/engine/extensions/user-data.d.ts +23 -24
- package/dist/engine/factories.d.ts +14 -13
- package/dist/engine/factories.js +38 -37
- package/dist/engine/feature-ast/entity-field-types.js +1 -1
- package/dist/engine/feature-ast/extractors/handlers.js +2 -1
- package/dist/engine/feature-ast/extractors/hooks.js +1 -1
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
- package/dist/engine/feature-ast/extractors/shared.js +2 -3
- package/dist/engine/feature-ast/parse.js +13 -13
- package/dist/engine/feature-ast/patch.js +4 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +2 -1
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
- package/dist/engine/feature-ast/patterns.d.ts +11 -2
- package/dist/engine/feature-ast/patterns.js +43 -51
- package/dist/engine/feature-ast/render.js +50 -89
- package/dist/engine/feature-config-events-jobs.js +15 -12
- package/dist/engine/feature-entity-handlers.js +12 -5
- package/dist/engine/feature-manifest.d.ts +9 -9
- package/dist/engine/feature-manifest.js +6 -7
- package/dist/engine/feature-ui-extensions.d.ts +3 -3
- package/dist/engine/feature-ui-extensions.js +30 -22
- package/dist/engine/field-access.js +5 -6
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +4 -4
- package/dist/engine/object-form.d.ts +1 -0
- package/dist/engine/object-form.js +14 -4
- package/dist/engine/ownership.js +3 -3
- package/dist/engine/parse-ref-target.js +5 -7
- package/dist/engine/pattern-library/index.js +2 -2
- package/dist/engine/pattern-library/library.js +2 -2
- package/dist/engine/pattern-library/types.js +3 -3
- package/dist/engine/pipeline.js +2 -2
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +7 -5
- package/dist/engine/projection-helpers.js +9 -9
- package/dist/engine/qualified-name.js +2 -2
- package/dist/engine/registry-facade.js +1 -1
- package/dist/engine/registry-ingest.js +13 -13
- package/dist/engine/registry-state.js +17 -19
- package/dist/engine/registry-validate.js +12 -12
- package/dist/engine/run-in.js +2 -2
- package/dist/engine/run-pipeline.js +7 -0
- package/dist/engine/schema-builder.js +251 -233
- package/dist/engine/screen-filter-ops.js +17 -20
- package/dist/engine/screen-helpers.js +4 -3
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/state-machine.d.ts +12 -12
- package/dist/engine/state-machine.js +7 -7
- package/dist/engine/steps/_resolver-utils.js +4 -3
- package/dist/engine/steps/for-each.js +4 -4
- package/dist/engine/steps/unsafe-projection-delete.js +1 -1
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
- package/dist/engine/steps/wait-for-event.js +4 -4
- package/dist/engine/steps/workflow-system-events.js +3 -3
- package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
- package/dist/engine/tier-resolver-extension.d.ts +29 -27
- package/dist/engine/tier-resolver-extension.js +26 -26
- package/dist/engine/transfer-adjacency.js +2 -2
- package/dist/engine/validate-projection-allowlist.js +9 -28
- package/dist/engine/walk-step-instances.d.ts +2 -0
- package/dist/engine/walk-step-instances.js +38 -0
- package/dist/engine/where-rule-lint.js +1 -1
- package/dist/engine/write-handler-qn-extract.js +1 -1
- package/dist/entrypoint/index.d.ts +4 -1
- package/dist/entrypoint/index.js +10 -4
- package/dist/env/index.d.ts +7 -6
- package/dist/env/index.js +10 -9
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/errors/transition-details.js +7 -7
- package/dist/errors/write-error-info.d.ts +6 -6
- package/dist/errors/write-error-info.js +6 -6
- package/dist/es-ops/context.d.ts +9 -9
- package/dist/es-ops/context.js +29 -29
- package/dist/es-ops/index.js +7 -9
- package/dist/es-ops/operations-schema.js +4 -10
- package/dist/es-ops/runner.d.ts +11 -11
- package/dist/es-ops/runner.js +49 -53
- package/dist/es-ops/types.d.ts +55 -55
- package/dist/es-ops/types.js +11 -11
- package/dist/event-store/admin-api.js +7 -8
- package/dist/event-store/errors.js +1 -1
- package/dist/event-store/event-store.js +12 -6
- package/dist/event-store/rebuild-dead-letter.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +4 -1
- package/dist/files/feature.js +9 -10
- package/dist/files/file-handle.js +7 -9
- package/dist/files/file-ref-entity.js +15 -15
- package/dist/files/file-ref-table.js +7 -8
- package/dist/files/file-routes.d.ts +7 -0
- package/dist/files/file-routes.js +22 -13
- package/dist/files/in-memory-provider.js +8 -9
- package/dist/files/local-provider.js +18 -25
- package/dist/files/provider-resolver.d.ts +7 -7
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/storage-tracking.js +2 -2
- package/dist/files/upload-ledger.js +7 -0
- package/dist/files/zip-stream.d.ts +12 -19
- package/dist/files/zip-stream.js +44 -63
- package/dist/i18n/required-surface-keys.d.ts +1 -1
- package/dist/i18n/required-surface-keys.js +188 -212
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-context.d.ts +32 -0
- package/dist/jobs/job-runner-context.js +305 -0
- package/dist/jobs/job-runner-execution.d.ts +7 -0
- package/dist/jobs/job-runner-execution.js +160 -0
- package/dist/jobs/job-runner-gates.d.ts +19 -0
- package/dist/jobs/job-runner-gates.js +198 -0
- package/dist/jobs/job-runner-helpers.d.ts +17 -3
- package/dist/jobs/job-runner-helpers.js +69 -4
- package/dist/jobs/job-runner-schedulers.d.ts +1 -1
- package/dist/jobs/job-runner-schedulers.js +3 -4
- package/dist/jobs/job-runner-scope.d.ts +44 -0
- package/dist/jobs/job-runner-scope.js +102 -0
- package/dist/jobs/job-runner-types.d.ts +12 -0
- package/dist/jobs/job-runner.d.ts +0 -1
- package/dist/jobs/job-runner.js +57 -669
- package/dist/lifecycle/lifecycle.js +3 -5
- package/dist/lifecycle/signal-handlers.js +5 -4
- package/dist/logging/utils.d.ts +2 -1
- package/dist/logging/utils.js +38 -10
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/kumiko-drift.js +14 -13
- package/dist/migrations/pending-rebuilds.d.ts +21 -20
- package/dist/migrations/pending-rebuilds.js +27 -27
- package/dist/migrations/projection-table-index.d.ts +3 -3
- package/dist/migrations/projection-table-index.js +8 -8
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +12 -1
- package/dist/observability/metric-validator.js +1 -1
- package/dist/observability/metrics-handle.js +3 -3
- package/dist/pipeline/append-event-core.js +11 -2
- package/dist/pipeline/cascade-handler.d.ts +1 -1
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +2 -2
- package/dist/pipeline/dispatch-query.js +20 -13
- package/dist/pipeline/dispatch-shared.d.ts +14 -3
- package/dist/pipeline/dispatch-shared.js +67 -721
- package/dist/pipeline/dispatch-stream.js +1 -1
- package/dist/pipeline/dispatch-write.js +297 -190
- package/dist/pipeline/dispatcher-utils.d.ts +1 -1
- package/dist/pipeline/dispatcher-utils.js +1 -1
- package/dist/pipeline/event-consumer-state.js +2 -2
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +4 -6
- package/dist/pipeline/event-dispatcher.js +17 -28
- package/dist/pipeline/handler-context-auth.d.ts +9 -0
- package/dist/pipeline/handler-context-auth.js +54 -0
- package/dist/pipeline/handler-context-config.d.ts +14 -0
- package/dist/pipeline/handler-context-config.js +113 -0
- package/dist/pipeline/handler-context-db.d.ts +24 -0
- package/dist/pipeline/handler-context-db.js +149 -0
- package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
- package/dist/pipeline/handler-context-dispatch.js +89 -0
- package/dist/pipeline/handler-context-events.d.ts +43 -0
- package/dist/pipeline/handler-context-events.js +237 -0
- package/dist/pipeline/handler-context-notify.d.ts +12 -0
- package/dist/pipeline/handler-context-notify.js +55 -0
- package/dist/pipeline/handler-context-observability.d.ts +7 -0
- package/dist/pipeline/handler-context-observability.js +21 -0
- package/dist/pipeline/handler-context-scope.d.ts +48 -0
- package/dist/pipeline/handler-context-scope.js +88 -0
- package/dist/pipeline/irreversible-operation-gate.js +1 -1
- package/dist/pipeline/lifecycle-pipeline.js +9 -9
- package/dist/pipeline/member-read-only-transaction.js +1 -3
- package/dist/pipeline/msp-rebuild.js +1 -1
- package/dist/pipeline/notify-scope.js +12 -1
- package/dist/pipeline/pre-handler-gates.js +20 -4
- package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
- package/dist/pipeline/pre-transaction-reservations.js +9 -3
- package/dist/pipeline/projection-rebuild.js +15 -13
- package/dist/pipeline/system-hooks.js +39 -41
- package/dist/pipeline/system-identity-switch.js +1 -3
- package/dist/pipeline/tenant-timezone-cache.js +1 -1
- package/dist/random/generate.d.ts +29 -29
- package/dist/random/generate.js +30 -30
- package/dist/random/words.js +20 -20
- package/dist/rate-limit/middleware.js +9 -5
- package/dist/redis/client.js +2 -2
- package/dist/redis/pubsub-signal.js +14 -17
- package/dist/schema-cli/apply-command.d.ts +2 -0
- package/dist/schema-cli/apply-command.js +97 -0
- package/dist/schema-cli/baseline-command.d.ts +2 -0
- package/dist/schema-cli/baseline-command.js +33 -0
- package/dist/schema-cli/generate-command.d.ts +2 -0
- package/dist/schema-cli/generate-command.js +90 -0
- package/dist/schema-cli/shared.d.ts +39 -0
- package/dist/schema-cli/shared.js +95 -0
- package/dist/schema-cli/status-command.d.ts +2 -0
- package/dist/schema-cli/status-command.js +73 -0
- package/dist/schema-cli/validate-boot-input.d.ts +5 -0
- package/dist/schema-cli/validate-boot-input.js +130 -0
- package/dist/schema-cli/validate-command.d.ts +2 -0
- package/dist/schema-cli/validate-command.js +130 -0
- package/dist/schema-cli.d.ts +2 -18
- package/dist/schema-cli.js +47 -510
- package/dist/search/index.js +4 -4
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/search/purge-subject.js +4 -5
- package/dist/search/reindex-entity.js +6 -9
- package/dist/time/boot-tz-warning.d.ts +2 -2
- package/dist/time/boot-tz-warning.js +8 -8
- package/dist/time/iana.js +10 -10
- package/dist/time/index.js +9 -9
- package/dist/time/legacy-date.d.ts +2 -2
- package/dist/time/legacy-date.js +6 -8
- package/dist/time/polyfill.js +1 -1
- package/dist/time/tz-context.d.ts +1 -1
- package/dist/time/tz-context.js +1 -1
- package/dist/ui-types/app-schema.js +7 -7
- package/dist/ui-types/list-row-meta.js +4 -4
- package/dist/upgrade-cli.js +159 -130
- package/dist/utils/compare.js +1 -1
- package/package.json +5 -5
- package/src/changes.json +358 -0
- package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
- package/src/scripts/codemod/feature-layout.ts +10 -2
- package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
- package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
- package/src/scripts/codemod/object-form.ts +41 -4
- package/src/scripts/codemod/package-merge.ts +58 -8
- package/src/scripts/codemod/pii-personal-migration.ts +192 -153
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
|
@@ -1,38 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { createHandlerCrossTenantReads } from "../db/cross-tenant-reads.js";
|
|
4
|
-
import { runInSavepoint, selectMany } from "../db/query.js";
|
|
5
|
-
import { createTenantDb, createUncheckedSystemDb, hasTenantColumn, } from "../db/tenant-db.js";
|
|
6
|
-
import { createDerivativesContext } from "../derivatives/derivatives-context.js";
|
|
7
|
-
import { mayOverrideTenant } from "../engine/cross-tenant.js";
|
|
8
|
-
import { createSystemUser } from "../engine/system-user.js";
|
|
1
|
+
import { requestContext, runWithOrigin } from "../api/request-context.js";
|
|
2
|
+
import { createTenantDb } from "../db/tenant-db.js";
|
|
9
3
|
import { declaredRateLimitOptions, isRateLimitDisabled, resolveAgentExposure, } from "../engine/types/index.js";
|
|
10
|
-
import {
|
|
11
|
-
import { archiveStream as archiveStreamHelper, isStreamArchived, restoreStream as restoreStreamHelper, } from "../event-store/archive.js";
|
|
12
|
-
import { IdempotentAppendConflictError as EventStoreIdempotentAppendConflictError, VersionConflictError as EventStoreVersionConflictError, } from "../event-store/errors.js";
|
|
13
|
-
import { loadAggregate, loadAggregateAsOf, loadAggregateForWriting, } from "../event-store/event-store.js";
|
|
14
|
-
import { effectiveSnapshotGeneration, loadAggregateWithSnapshot, saveSnapshot, } from "../event-store/snapshot.js";
|
|
15
|
-
import { upcastStoredEvent, upcastStoredEvents } from "../event-store/upcaster.js";
|
|
16
|
-
import { createTenantFileContext } from "../files/file-handle.js";
|
|
17
|
-
import { DEFAULT_LOCALE, isValidLocaleTag } from "../i18n/request-locale.js";
|
|
4
|
+
import { FeatureDisabledError, InternalError, memberResolutionReadOnlyDenied, RateLimitError, RateLimitUnavailableError, } from "../errors/index.js";
|
|
18
5
|
import { createFallbackLogger } from "../logging/utils.js";
|
|
19
|
-
import { createEscapeHatchReporter,
|
|
6
|
+
import { createEscapeHatchReporter, emitDispatcherError, emitDispatcherHandler, observabilityContext, } from "../observability/index.js";
|
|
20
7
|
import { buildBucketKey, buildPayloadBucketKey, normalizePayloadBucketValue, } from "../rate-limit/index.js";
|
|
21
|
-
import { createTzContext, isValidIanaTimeZone } from "../time/index.js";
|
|
22
8
|
import { isPlainObject } from "../utils/is-plain-object.js";
|
|
23
|
-
import { INTERACTIVE_SIGN_IN_POLICY, resolveActiveMembershipFn } from "./active-membership.js";
|
|
24
|
-
import { appendDomainEventCore } from "./append-event-core.js";
|
|
25
9
|
import { resolveAuthClaims as runAuthClaimsResolver } from "./auth-claims-resolver.js";
|
|
26
10
|
import { configAccessorFactoryOf } from "./config-accessor-factory.js";
|
|
27
|
-
import { runBatch, unwrapSingle } from "./dispatch-batch.js";
|
|
28
11
|
import { executeQuery } from "./dispatch-query.js";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
12
|
+
import { dispatcherSpanAttributes, isFailedWriteResult, } from "./dispatcher-utils.js";
|
|
13
|
+
import { buildAuthFacet } from "./handler-context-auth.js";
|
|
14
|
+
import { buildConfigFacet, resolveHandlerLocale, resolveHandlerTz, } from "./handler-context-config.js";
|
|
15
|
+
import { buildFileFragment, buildForTenantFragment, buildHandlerDbFacet, } from "./handler-context-db.js";
|
|
16
|
+
import { buildDispatchFacet, createCommitSinks } from "./handler-context-dispatch.js";
|
|
17
|
+
import { buildEventStoreFacet } from "./handler-context-events.js";
|
|
18
|
+
import { buildNotifyFacet } from "./handler-context-notify.js";
|
|
19
|
+
import { buildObservabilityFacet } from "./handler-context-observability.js";
|
|
20
|
+
import { buildPreSaveFragment, buildRuntimeFragment, resolveHandlerContextScope, } from "./handler-context-scope.js";
|
|
21
|
+
import { createGatedIdentitySwitch, isSystemIdentity } from "./system-identity-switch.js";
|
|
34
22
|
import { TENANT_TIMEZONE_CONFIG_KEY } from "./tenant-timezone-config-key.js";
|
|
35
|
-
import {
|
|
23
|
+
import { rootWriteOrigin } from "./write-origin.js";
|
|
36
24
|
export { TENANT_TIMEZONE_CONFIG_KEY };
|
|
37
25
|
// Same bundled-features-free constraint as above — these name the config
|
|
38
26
|
// feature's write handlers so dispatch-write.ts can invalidate
|
|
@@ -42,39 +30,15 @@ export { TENANT_TIMEZONE_CONFIG_KEY };
|
|
|
42
30
|
export const CONFIG_WRITE_SET_TYPE = "config:write:set";
|
|
43
31
|
export const CONFIG_WRITE_RESET_TYPE = "config:write:reset";
|
|
44
32
|
export const DEFAULT_MEMBER_READ_TIMEOUT_MS = 10_000;
|
|
45
|
-
// Narrowing
|
|
46
|
-
// dispatch
|
|
47
|
-
// appendEvent/projection
|
|
33
|
+
// Narrowing helper: AppContext.db is DbConnection|TenantDb|undefined. The
|
|
34
|
+
// dispatch paths need DbConnection (or DbTx from the caller scope) for
|
|
35
|
+
// appendEvent/projection writes; the TenantDb branch is excluded here.
|
|
48
36
|
export function resolveDbSource(ctx, tx) {
|
|
49
37
|
return tx ?? resolveAppPoolDb(ctx);
|
|
50
38
|
}
|
|
51
|
-
function resolveAppPoolDb(ctx) {
|
|
39
|
+
export function resolveAppPoolDb(ctx) {
|
|
52
40
|
return ctx.appContext.db; // @cast-boundary db-operator
|
|
53
41
|
}
|
|
54
|
-
// ctx.appendEvent — append a domain event onto a specific aggregate stream
|
|
55
|
-
// in the current tx, then fire matching inline projections. Core logic
|
|
56
|
-
// lives in appendDomainEventCore; this wrapper just locates dbSource +
|
|
57
|
-
// stringifies the SessionUser id for the shared helper.
|
|
58
|
-
async function appendDomainEvent(ctx, args, user, tx, callerFeature) {
|
|
59
|
-
// Sink behind every append surface, so a resolved member stays read-only even via fetchForWriting handles.
|
|
60
|
-
if (isMemberResolutionPrincipal(user))
|
|
61
|
-
throw memberResolutionReadOnlyDenied();
|
|
62
|
-
const { registry } = ctx;
|
|
63
|
-
const dbSource = resolveDbSource(ctx, tx);
|
|
64
|
-
if (!dbSource) {
|
|
65
|
-
throw new InternalError({
|
|
66
|
-
message: `ctx.appendEvent("${args.type}") requires a database connection — none is configured.`,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
await appendDomainEventCore({
|
|
70
|
-
registry,
|
|
71
|
-
db: dbSource,
|
|
72
|
-
tenantId: user.tenantId,
|
|
73
|
-
userId: String(user.id),
|
|
74
|
-
callSiteLabel: "ctx.appendEvent",
|
|
75
|
-
callerFeature,
|
|
76
|
-
}, args);
|
|
77
|
-
}
|
|
78
42
|
// r.systemScope() handlers must go through ctx.systemDb's guarded methods
|
|
79
43
|
// (assertTenantMatch / acknowledgeCrossTenant) instead of plain ctx.db —
|
|
80
44
|
// "system" mode has no tenant filter at all, so silent ctx.db or
|
|
@@ -83,7 +47,7 @@ async function appendDomainEvent(ctx, args, user, tx, callerFeature) {
|
|
|
83
47
|
// runtime-only registry lookup, not a type-level discriminant), so instead
|
|
84
48
|
// of omitting the key we hand back a Proxy that throws on first touch,
|
|
85
49
|
// naming the handler and pointing at the matching ctx.systemDb escape hatch.
|
|
86
|
-
function createSystemScopedDbGuard(handlerType, fieldName, hint) {
|
|
50
|
+
export function createSystemScopedDbGuard(handlerType, fieldName, hint) {
|
|
87
51
|
return new Proxy({}, {
|
|
88
52
|
get(_target, prop) {
|
|
89
53
|
throw new InternalError({
|
|
@@ -97,14 +61,6 @@ function createSystemScopedDbGuard(handlerType, fieldName, hint) {
|
|
|
97
61
|
export function isMemberResolutionPrincipal(user) {
|
|
98
62
|
return user.origin === "member-resolution";
|
|
99
63
|
}
|
|
100
|
-
// DispatchContext.jobRunner is typed as the handleEvent-only JobRunnerRef but
|
|
101
|
-
// holds the concrete runner at boot; narrow instead of casting.
|
|
102
|
-
function isNotifyJobDispatcher(runner) {
|
|
103
|
-
return (typeof runner === "object" &&
|
|
104
|
-
runner !== null &&
|
|
105
|
-
"dispatch" in runner &&
|
|
106
|
-
typeof runner.dispatch === "function");
|
|
107
|
-
}
|
|
108
64
|
async function denyMemberResolutionWrite() {
|
|
109
65
|
throw memberResolutionReadOnlyDenied();
|
|
110
66
|
}
|
|
@@ -256,671 +212,60 @@ export function applyBeforeTransactionReadOnly(handlerContext) {
|
|
|
256
212
|
dbOutsideTransaction: undefined,
|
|
257
213
|
};
|
|
258
214
|
}
|
|
259
|
-
export async function buildHandlerContext(ctx, type, user, origin,
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
const writeEscapeHatch = writeHandler?.escapeHatch;
|
|
275
|
-
const isPerCommand = writeHandler?.transaction === "perCommand";
|
|
276
|
-
const denyDbWrites = transactionScope === "withoutTransaction";
|
|
277
|
-
const handlerEscapeHatch = writeEscapeHatch ??
|
|
278
|
-
registry.getQueryHandler(type)?.escapeHatch ??
|
|
279
|
-
registry.getStreamHandler(type)?.escapeHatch;
|
|
280
|
-
const identitySwitchEscapeHatch = escapeHatchFor(handlerEscapeHatch, "systemIdentity");
|
|
281
|
-
const hasIdentitySwitchGrant = isSystem || identitySwitchEscapeHatch !== undefined;
|
|
282
|
-
const featureName = registry.getHandlerFeature(type);
|
|
283
|
-
const reportEscapeHatch = createEscapeHatchReporter({
|
|
284
|
-
handler: type,
|
|
285
|
-
tenantId: user.tenantId,
|
|
286
|
-
actor: user.id,
|
|
287
|
-
sink: context._escapeHatchAuditSink,
|
|
288
|
-
log: context.log ?? createFallbackLogger("dispatcher"),
|
|
289
|
-
window: ctx.escapeHatchReportWindow,
|
|
290
|
-
});
|
|
291
|
-
const identitySwitchGrantReason = identitySwitchEscapeHatch?.reason ??
|
|
292
|
-
(isSystem ? `r.systemScope() feature "${featureName ?? "unknown"}"` : undefined);
|
|
293
|
-
const buildTenantScopedDb = (source, signal, foreignTenantId, denyWrites = false) => createTenantDb({
|
|
294
|
-
db: source,
|
|
295
|
-
tenantId: foreignTenantId ?? user.tenantId,
|
|
296
|
-
mode: isSystem && foreignTenantId === undefined ? "system" : "tenant",
|
|
297
|
-
tracer: context.tracer,
|
|
298
|
-
meter: context.meter,
|
|
299
|
-
signal,
|
|
300
|
-
grants: {
|
|
301
|
-
...(foreignTenantId === undefined && {
|
|
302
|
-
globalWrites: escapeHatchFor(writeEscapeHatch, "globalWrites"),
|
|
303
|
-
unsafeRaw: escapeHatchFor(handlerEscapeHatch, "unsafeRaw"),
|
|
304
|
-
}),
|
|
305
|
-
report: reportEscapeHatch,
|
|
306
|
-
memberReadOnly: isMemberResolutionPrincipal(user),
|
|
307
|
-
personalDataGate: buildPersonalDataGate(registry, origin),
|
|
308
|
-
projectionRegistry: registry,
|
|
309
|
-
...(denyWrites && { writesDeniedReason: "without an open transaction" }),
|
|
310
|
-
},
|
|
311
|
-
});
|
|
312
|
-
// Propagate the request's AbortSignal so every TenantDb query throws when
|
|
313
|
-
// the client has disconnected — handlers with many sequential queries skip
|
|
314
|
-
// the rest of the chain instead of burning DB-CPU for results no one reads.
|
|
315
|
-
const db = dbSource
|
|
316
|
-
? buildTenantScopedDb(dbSource, reqCtx?.signal, undefined, denyDbWrites)
|
|
317
|
-
: undefined;
|
|
318
|
-
// Unbound pool, tenant-scoped like `db` but never tx-bound — writes
|
|
319
|
-
// through it survive a rollback of the handler's own transaction. No
|
|
320
|
-
// AbortSignal here: a client disconnect must not abort a durability write
|
|
321
|
-
// that is meant to outlive the request. Computed before `systemDb` below
|
|
322
|
-
// so its guarded escape hatch (ctx.systemDb.outsideTransaction) can wrap
|
|
323
|
-
// this same TenantDb instead of a second, independently-built one.
|
|
324
|
-
const outsideTxSource = ctx.outsideTransactionDb ?? resolveAppPoolDb(ctx);
|
|
325
|
-
const rawDbOutsideTransaction = outsideTxSource
|
|
326
|
-
? buildTenantScopedDb(outsideTxSource, undefined)
|
|
327
|
-
: undefined;
|
|
328
|
-
const systemDb = isSystem && db
|
|
329
|
-
? createUncheckedSystemDb(db, rawDbOutsideTransaction, reportEscapeHatch)
|
|
330
|
-
: undefined;
|
|
331
|
-
// An agent tool call runs with the user's session but feeds the LLM context, so
|
|
332
|
-
// cross-tenant rows must never reach it, even when unsafeRaw implies the grant.
|
|
333
|
-
const crossTenantReadsReason = isAgentToolCallScope()
|
|
334
|
-
? undefined
|
|
335
|
-
: crossTenantReadsGrantFor(handlerEscapeHatch)?.reason;
|
|
336
|
-
const crossTenantReads = dbSource && crossTenantReadsReason !== undefined
|
|
337
|
-
? createHandlerCrossTenantReads({
|
|
338
|
-
db: dbSource,
|
|
339
|
-
tenantId: user.tenantId,
|
|
340
|
-
tracer: context.tracer,
|
|
341
|
-
meter: context.meter,
|
|
342
|
-
signal: reqCtx?.signal,
|
|
343
|
-
reason: crossTenantReadsReason,
|
|
344
|
-
report: reportEscapeHatch,
|
|
345
|
-
grants: {
|
|
346
|
-
report: reportEscapeHatch,
|
|
347
|
-
memberReadOnly: isMemberResolutionPrincipal(user),
|
|
348
|
-
personalDataGate: buildPersonalDataGate(registry, origin),
|
|
349
|
-
projectionRegistry: registry,
|
|
350
|
-
},
|
|
351
|
-
})
|
|
352
|
-
: undefined;
|
|
353
|
-
// Exposed as ctx.db below — the internal `db` above stays the real,
|
|
354
|
-
// working TenantDb for this function's own use (config/derivatives
|
|
355
|
-
// accessors, systemDb construction).
|
|
356
|
-
const exposedDb = isSystem && db
|
|
357
|
-
? createSystemScopedDbGuard(type, "db", "ctx.systemDb.assertTenantMatch(...) or ctx.systemDb.acknowledgeCrossTenant(...)")
|
|
358
|
-
: db;
|
|
359
|
-
// Same fail-closed treatment as `exposedDb` — a system handler reaching
|
|
360
|
-
// ctx.dbOutsideTransaction directly would be a second, unguarded door to
|
|
361
|
-
// the same cross-tenant access ctx.db closes off above.
|
|
362
|
-
const dbOutsideTransaction = isSystem && rawDbOutsideTransaction
|
|
363
|
-
? createSystemScopedDbGuard(type, "dbOutsideTransaction", "ctx.systemDb.outsideTransaction.assertTenantMatch(...) or " +
|
|
364
|
-
"ctx.systemDb.outsideTransaction.acknowledgeCrossTenant(...)")
|
|
365
|
-
: rawDbOutsideTransaction;
|
|
366
|
-
const log = context.log?.child({
|
|
367
|
-
handler: type,
|
|
368
|
-
tenantId: user.tenantId,
|
|
369
|
-
userId: user.id,
|
|
370
|
-
...(reqCtx && { requestId: reqCtx.requestId }),
|
|
371
|
-
});
|
|
372
|
-
// Cross-feature bridge: ctx.query/write share the current tx + afterCommitHooks sink.
|
|
373
|
-
// queryAs/writeAs to anyone but the caller itself needs r.systemScope() or { escapeHatch } (system-identity-switch.ts).
|
|
374
|
-
const bridgeSink = afterCommitHooks ?? [];
|
|
375
|
-
// Query bridges and post-commit contexts get a throwaway sink: hooks pushed there never run.
|
|
376
|
-
const hasLiveCommitSink = afterCommitHooks !== undefined && transactionScope !== "postCommit";
|
|
377
|
-
const scheduleAfterCommit = (hook) => {
|
|
378
|
-
bridgeSink.push(hook);
|
|
379
|
-
};
|
|
380
|
-
const notifyFactory = context._notifyFactory;
|
|
381
|
-
const notifyJobDispatcher = isNotifyJobDispatcher(jobRunner) ? jobRunner : undefined;
|
|
382
|
-
// Inside the write tx the app pool's slots can all be held by waiting write txs, so the
|
|
383
|
-
// delivery's own DB work (broadcast lookup, attempt log, channel sends) runs on the outside-tx pool.
|
|
384
|
-
const notifyInTransactionFactory = notifyFactory && tx !== undefined
|
|
385
|
-
? (notifyUser, notifyTenantId) => notifyFactory(notifyUser, notifyTenantId, notifyJobDispatcher, outsideTxSource, context._fileProviderResolver)
|
|
386
|
-
: undefined;
|
|
387
|
-
const notifyAfterCommitFactory = notifyFactory
|
|
388
|
-
? (notifyUser, notifyTenantId) => notifyFactory(notifyUser, notifyTenantId, notifyJobDispatcher, undefined, context._fileProviderResolver)
|
|
389
|
-
: undefined;
|
|
390
|
-
const notify = buildNotify();
|
|
391
|
-
function buildNotify() {
|
|
392
|
-
if (!notifyAfterCommitFactory)
|
|
393
|
-
return undefined;
|
|
394
|
-
const notifyAfterCommit = notifyAfterCommitFactory(user, user.tenantId);
|
|
395
|
-
if (!notifyInTransactionFactory)
|
|
396
|
-
return notifyAfterCommit;
|
|
397
|
-
const notifyInTransaction = notifyInTransactionFactory(user, user.tenantId);
|
|
398
|
-
// A rolled-back write must leave no notification behind. Without any commit to wait for (a
|
|
399
|
-
// post-commit context) delivery stays immediate.
|
|
400
|
-
const notifyCommitSink = hasLiveCommitSink ? bridgeSink : outerNotifySink;
|
|
401
|
-
if (notifyCommitSink === undefined)
|
|
402
|
-
return notifyInTransaction;
|
|
403
|
-
return createDeferringNotify({
|
|
404
|
-
registry,
|
|
405
|
-
tenantId: user.tenantId,
|
|
406
|
-
notifyAfterCommit,
|
|
407
|
-
notifyNow: notifyInTransaction,
|
|
408
|
-
scheduleAfterCommit: (hook) => {
|
|
409
|
-
notifyCommitSink.push(hook);
|
|
410
|
-
},
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
// Mirror notify: only built when the config feature wired its factory.
|
|
414
|
-
const configFactory = configAccessorFactoryOf(context);
|
|
415
|
-
const config = configFactory && db
|
|
416
|
-
? configFactory({
|
|
417
|
-
user: { id: user.id, tenantId: user.tenantId },
|
|
418
|
-
db,
|
|
419
|
-
secrets: context.secrets,
|
|
420
|
-
})
|
|
421
|
-
: undefined;
|
|
422
|
-
// Cross-tenant accessor for SystemAdmin/system callers. The
|
|
423
|
-
// identity is the target tenant's system user so the caller's own user-scope
|
|
424
|
-
// values never resolve into another tenant's config; the db is tenant-mode
|
|
425
|
-
// scoped to the target.
|
|
426
|
-
const configFor = configFactory && dbSource
|
|
427
|
-
? (targetTenantId) => {
|
|
428
|
-
if (targetTenantId === user.tenantId && config)
|
|
429
|
-
return config;
|
|
430
|
-
if (!isSystemIdentity(user) && !user.roles.includes("SystemAdmin")) {
|
|
431
|
-
throw new AccessDeniedError({
|
|
432
|
-
message: "ctx.configFor(otherTenant) requires the system role or SystemAdmin",
|
|
433
|
-
details: { reason: "config_for_requires_system_admin" },
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
const targetSystemUser = createSystemUser(targetTenantId);
|
|
437
|
-
return configFactory({
|
|
438
|
-
user: { id: targetSystemUser.id, tenantId: targetTenantId },
|
|
439
|
-
db: createTenantDb({
|
|
440
|
-
db: dbSource,
|
|
441
|
-
tenantId: targetTenantId,
|
|
442
|
-
mode: "tenant",
|
|
443
|
-
tracer: context.tracer,
|
|
444
|
-
meter: context.meter,
|
|
445
|
-
grants: { projectionRegistry: registry },
|
|
446
|
-
}),
|
|
447
|
-
secrets: context.secrets,
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
: undefined;
|
|
451
|
-
// Write handlers only, and not r.systemScope() ones (they have ctx.systemDb). The gate is the
|
|
452
|
-
// dispatcher's tenantIdOverride predicate, so no caller reaches more than it could via
|
|
453
|
-
// tenantIdOverride. The foreign db carries no unsafeRaw/globalWrites grant and is tenant mode
|
|
454
|
-
// bound to the target; the executor accepts streamTenantId only on a db bound to that tenant.
|
|
455
|
-
const forTenant = !isSystem && dbSource && registry.getWriteHandler(type) !== undefined
|
|
456
|
-
? (targetTenantId) => {
|
|
457
|
-
if (targetTenantId === user.tenantId && exposedDb) {
|
|
458
|
-
// Only perCommand handlers lose forTenant writes; reservations keep their pre-transaction contract.
|
|
459
|
-
const ownTenantDb = denyDbWrites && !isPerCommand
|
|
460
|
-
? buildTenantScopedDb(dbSource, reqCtx?.signal, undefined, readOnlyForTenant)
|
|
461
|
-
: exposedDb;
|
|
462
|
-
return { db: ownTenantDb, streamTenantId: undefined };
|
|
463
|
-
}
|
|
464
|
-
if (!mayOverrideTenant(user)) {
|
|
465
|
-
throw new AccessDeniedError({
|
|
466
|
-
details: { reason: FrameworkReasons.tenantOverrideRequiresSystemAdmin },
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
return {
|
|
470
|
-
db: buildTenantScopedDb(dbSource, reqCtx?.signal, targetTenantId, denyDbWrites && (isPerCommand || readOnlyForTenant)),
|
|
471
|
-
streamTenantId: targetTenantId,
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
: undefined;
|
|
475
|
-
// ctx.files resolved per-tenant through file-foundation (lazy — the
|
|
476
|
-
// provider is only resolved when a handle actually does I/O). Boot wires
|
|
477
|
-
// _fileProviderResolver when a file-provider plugin is mounted; falls back
|
|
478
|
-
// to a statically-injected context.files (tests).
|
|
479
|
-
const fileResolver = context._fileProviderResolver;
|
|
480
|
-
const files = fileResolver ? createTenantFileContext(fileResolver, user.tenantId) : context.files;
|
|
481
|
-
// ctx.derivatives builds on ctx.files (needs a FileContext to read the
|
|
482
|
-
// original + write the variant) plus db (to look up the FileRef row) — so
|
|
483
|
-
// it can only be constructed exactly when files+db both resolved; falls
|
|
484
|
-
// back to a statically-injected context.derivatives otherwise (tests).
|
|
485
|
-
const derivatives = files && db
|
|
486
|
-
? createDerivativesContext({ files, registry, db, tenantId: user.tenantId })
|
|
487
|
-
: context.derivatives;
|
|
488
|
-
// Observability — feature-bound metrics handle, so ctx.metrics.inc("foo")
|
|
489
|
-
// resolves to kumiko_<feature>_foo. Unknown feature falls back to noop
|
|
490
|
-
// so legacy internal handlers don't crash.
|
|
491
|
-
const tracer = context.tracer ?? getFallbackTracer();
|
|
492
|
-
const meter = context.meter;
|
|
493
|
-
const metrics = meter && featureName ? createMetricsHandle(meter, featureName) : createNoopMetricsHandle();
|
|
494
|
-
// ctx.metricsFor(featureName) — shared/library code binds to a feature
|
|
495
|
-
// name of its own choosing instead of the dispatching handler's
|
|
496
|
-
// (framework#1844). Unregistered names no-op rather than throw.
|
|
497
|
-
const metricsFor = createMetricsForFactory(meter);
|
|
498
|
-
// A failed inner write has already appended its event and projections when a later
|
|
499
|
-
// step (e.g. an inTransaction hook) fails, and the outer handler may swallow the
|
|
500
|
-
// failure result. The savepoint makes the failure undo exactly the inner write; the
|
|
501
|
-
// inner afterCommit hooks only reach the outer sink if it succeeded.
|
|
502
|
-
const runNestedWriteInSavepoint = async (asUser, targetType, payload) => {
|
|
503
|
-
if (registry.getWriteHandler(targetType)?.transaction === "perCommand") {
|
|
504
|
-
return writeFailure(perCommandNestedDenied(targetType));
|
|
505
|
-
}
|
|
506
|
-
if (!tx)
|
|
507
|
-
return executeWrite(ctx, targetType, payload, asUser, origin, tx, bridgeSink);
|
|
508
|
-
const innerAfterCommitHooks = [];
|
|
509
|
-
try {
|
|
510
|
-
const result = await runInSavepoint(tx, async (savepointTx) => {
|
|
511
|
-
const innerResult = await executeWrite(ctx, targetType, payload, asUser, origin, savepointTx, // @cast-boundary db-runner savepoint handle is a transaction handle
|
|
512
|
-
innerAfterCommitHooks);
|
|
513
|
-
if (isFailedWriteResult(innerResult))
|
|
514
|
-
throw new NestedWriteRollback(innerResult);
|
|
515
|
-
return innerResult;
|
|
516
|
-
});
|
|
517
|
-
bridgeSink.push(...innerAfterCommitHooks);
|
|
518
|
-
return result;
|
|
519
|
-
}
|
|
520
|
-
catch (e) {
|
|
521
|
-
if (e instanceof NestedWriteRollback)
|
|
522
|
-
return e.failure;
|
|
523
|
-
throw e;
|
|
524
|
-
}
|
|
525
|
-
};
|
|
526
|
-
// Savepoints on one connection are not serialized by the driver: two parallel nested writes would
|
|
527
|
-
// interleave their statements and a ROLLBACK TO of the first would also discard the second's rows.
|
|
528
|
-
// Writes nested one level deeper run on their own savepoint handle and therefore their own chain.
|
|
529
|
-
let nestedWriteChain = Promise.resolve();
|
|
530
|
-
const executeNestedWriteInSavepoint = (asUser, targetType, payload) => {
|
|
531
|
-
const run = nestedWriteChain.then(() => runNestedWriteInSavepoint(asUser, targetType, payload));
|
|
532
|
-
nestedWriteChain = run.catch(() => undefined);
|
|
533
|
-
return run;
|
|
534
|
-
};
|
|
535
|
-
// Without an open transaction (post-commit hooks, stream/query handlers, perCommand handlers) a bare
|
|
536
|
-
// executeWrite would auto-commit event append, projections and inTransaction hooks separately and its
|
|
537
|
-
// afterCommit hooks would land in a sink nobody flushes. A batch-of-one gives the write its own
|
|
538
|
-
// transaction and its own flush.
|
|
539
|
-
const writeAsUser = async (asUser, targetType, payload) => transactionScope === "inTransaction"
|
|
540
|
-
? executeNestedWriteInSavepoint(asUser, targetType, payload)
|
|
541
|
-
: unwrapSingle(await runBatch(ctx, [{ type: targetType, payload }], asUser, undefined, origin));
|
|
542
|
-
const identitySwitchAudit = { reason: identitySwitchGrantReason, report: reportEscapeHatch };
|
|
543
|
-
const identitySwitch = createGatedIdentitySwitch(`handler "${type}"`, user, hasIdentitySwitchGrant, {
|
|
544
|
-
// Inherits the caller's origin, so switching to SYSTEM cannot shed an anonymous root.
|
|
545
|
-
queryAs: (asUser, targetType, payload) => executeQuery(ctx, targetType, payload, asUser, origin, tx, hasLiveCommitSink ? bridgeSink : outerNotifySink), // @wrapper-known semantic-alias
|
|
546
|
-
writeAs: writeAsUser,
|
|
547
|
-
}, identitySwitchAudit);
|
|
548
|
-
// Lazy — creates the reader (which fails closed on SYSTEM_TENANT_ID) only
|
|
549
|
-
// on first actual use, not on every HandlerContext build.
|
|
550
|
-
let ungatedMemberReader;
|
|
551
|
-
const queryAsMember = createGatedMemberReader(`handler "${type}"`, hasIdentitySwitchGrant, (userId, qn, payload) => {
|
|
552
|
-
ungatedMemberReader ??= createMemberReaderFn(ctx, user.tenantId, tx);
|
|
553
|
-
return ungatedMemberReader(userId, qn, payload);
|
|
554
|
-
}, { ...identitySwitchAudit, tenantId: user.tenantId });
|
|
555
|
-
// queryProjection works against both single-stream and multi-stream
|
|
556
|
-
// projections. MSPs without a table cannot be queried — those are
|
|
557
|
-
// side-effect-only consumers (no state to read back).
|
|
558
|
-
const findProjectionTable = (qualifiedName) => registry.getAllProjections().get(qualifiedName)?.table ??
|
|
559
|
-
registry.getAllMultiStreamProjections().get(qualifiedName)?.table;
|
|
560
|
-
const ungatedQueryProjection = async (qualifiedName, queryOptions) => {
|
|
561
|
-
const projTable = findProjectionTable(qualifiedName);
|
|
562
|
-
if (!projTable) {
|
|
563
|
-
const singleNames = [...registry.getAllProjections().keys()];
|
|
564
|
-
const mspNames = [...registry.getAllMultiStreamProjections().keys()].filter((n) => registry.getAllMultiStreamProjections().get(n)?.table);
|
|
565
|
-
const all = [...singleNames, ...mspNames];
|
|
566
|
-
throw new InternalError({
|
|
567
|
-
message: `ctx.queryProjection("${qualifiedName}") — projection not registered, or it is a ` +
|
|
568
|
-
`table-less MSP (side-effect-only). Known queryable projections: ${all.join(", ") || "(none)"}`,
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
const dbSource = resolveDbSource(ctx, tx);
|
|
572
|
-
if (!dbSource) {
|
|
573
|
-
throw new InternalError({
|
|
574
|
-
message: `ctx.queryProjection("${qualifiedName}") requires a database connection — none is configured.`,
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
const where = hasTenantColumn(projTable) && queryOptions?.unsafeAllTenants !== true
|
|
578
|
-
? { tenantId: user.tenantId }
|
|
579
|
-
: undefined;
|
|
580
|
-
const rows = await selectMany(dbSource, projTable, where);
|
|
581
|
-
return rows; // @cast-boundary engine-payload
|
|
582
|
-
};
|
|
583
|
-
const queryProjection = createGatedProjectionReader(`handler "${type}"`, hasIdentitySwitchGrant, ungatedQueryProjection, identitySwitchAudit, (qualifiedName) => {
|
|
584
|
-
const projTable = findProjectionTable(qualifiedName);
|
|
585
|
-
// Unknown projection: keep the grant check first, the ungated reader reports the real error.
|
|
586
|
-
return projTable === undefined || hasTenantColumn(projTable);
|
|
587
|
-
});
|
|
588
|
-
const bridge = {
|
|
589
|
-
query: (targetType, payload) => executeQuery(ctx, targetType, payload, user, origin, tx, hasLiveCommitSink ? bridgeSink : outerNotifySink), // @wrapper-known semantic-alias
|
|
590
|
-
queryAs: identitySwitch.queryAs,
|
|
591
|
-
write: (targetType, payload) => writeAsUser(user, targetType, payload),
|
|
592
|
-
writeAs: identitySwitch.writeAs,
|
|
593
|
-
// Strict + unsafe share the same runtime — only the type-surface
|
|
594
|
-
// differs. The strict signature is what's exposed to typed callers;
|
|
595
|
-
// unsafe is the explicit escape-hatch for runtime-pluggable events.
|
|
596
|
-
appendEvent: (async (args) => {
|
|
597
|
-
await appendDomainEvent(ctx, args, user, tx, registry.getHandlerFeature(type));
|
|
598
|
-
}), // @cast-boundary engine-bridge
|
|
599
|
-
unsafeAppendEvent: async (args) => {
|
|
600
|
-
await appendDomainEvent(ctx, args, user, tx, registry.getHandlerFeature(type));
|
|
601
|
-
},
|
|
602
|
-
// Savepoint-scoped append: catches a losing writer's VersionConflictError
|
|
603
|
-
// without poisoning the rest of the handler's transaction. postgres.js
|
|
604
|
-
// aborts the WHOLE begin() on an uncaught statement error
|
|
605
|
-
// (SQLSTATE 25P02) even if the JS error is caught — runInSavepoint
|
|
606
|
-
// wraps the append in a real SAVEPOINT so only that nested scope rolls
|
|
607
|
-
// back on conflict, and subsequent statements in the outer tx (e.g. a
|
|
608
|
-
// dedup-anchor insert) still succeed. Use when a handler must react to
|
|
609
|
-
// losing a concurrent-append race instead of failing the whole write.
|
|
610
|
-
tryAppendEvent: async (args) => {
|
|
611
|
-
if (!tx) {
|
|
612
|
-
throw new InternalError({
|
|
613
|
-
message: `ctx.tryAppendEvent("${args.type}") requires an active transaction — no tx is threaded through this call.`,
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
try {
|
|
617
|
-
const event = await runInSavepoint(tx, (sp) => appendDomainEventCore({
|
|
618
|
-
registry,
|
|
619
|
-
db: sp,
|
|
620
|
-
tenantId: user.tenantId,
|
|
621
|
-
userId: String(user.id),
|
|
622
|
-
callSiteLabel: "ctx.tryAppendEvent",
|
|
623
|
-
callerFeature: registry.getHandlerFeature(type),
|
|
624
|
-
}, args));
|
|
625
|
-
return { ok: true, event };
|
|
626
|
-
}
|
|
627
|
-
catch (e) {
|
|
628
|
-
if (e instanceof EventStoreVersionConflictError) {
|
|
629
|
-
return { ok: false, conflict: e };
|
|
630
|
-
}
|
|
631
|
-
if (e instanceof EventStoreIdempotentAppendConflictError) {
|
|
632
|
-
return { ok: false, conflict: e };
|
|
633
|
-
}
|
|
634
|
-
throw e;
|
|
635
|
-
}
|
|
636
|
-
},
|
|
637
|
-
fetchForWriting: async (args) => {
|
|
638
|
-
const dbSource = resolveDbSource(ctx, tx);
|
|
639
|
-
if (!dbSource) {
|
|
640
|
-
throw new InternalError({
|
|
641
|
-
message: `ctx.fetchForWriting("${args.aggregateId}") requires a database connection — none is configured.`,
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
// Stream-version authoritative (same policy as CRUD executor + Block 0), read together
|
|
645
|
-
// with the events so the pinned version covers exactly what the handler's rules saw.
|
|
646
|
-
const { events: storedEvents, streamVersion: fetchedVersion } = await loadAggregateForWriting(dbSource, args.aggregateId, user.tenantId, { aggregateType: args.aggregateType });
|
|
647
|
-
const events = await upcastStoredEvents(storedEvents, registry.getEventUpcasters(), {
|
|
648
|
-
db: dbSource,
|
|
649
|
-
tenantId: user.tenantId,
|
|
650
|
-
});
|
|
651
|
-
// Optimistic concurrency: if the caller knows the version they
|
|
652
|
-
// worked against (e.g. from a prior read-model row) and the stream
|
|
653
|
-
// has moved on, fail fast before any downstream work.
|
|
654
|
-
if (args.expectedVersion !== undefined && args.expectedVersion !== fetchedVersion) {
|
|
655
|
-
throw new VersionConflictError({
|
|
656
|
-
entityId: args.aggregateId,
|
|
657
|
-
expectedVersion: args.expectedVersion,
|
|
658
|
-
currentVersion: fetchedVersion,
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
// Pinned into every append: a write landing between fetch and append
|
|
662
|
-
// fails with VersionConflictError instead of stacking on a state the
|
|
663
|
-
// caller's business rules never saw. Bumps per appendOne so consecutive
|
|
664
|
-
// appends stay in order without re-reading the DB.
|
|
665
|
-
let handleVersion = fetchedVersion;
|
|
666
|
-
const appendOne = async (appendArgs) => {
|
|
667
|
-
await appendDomainEvent(ctx, {
|
|
668
|
-
aggregateId: args.aggregateId,
|
|
669
|
-
aggregateType: args.aggregateType,
|
|
670
|
-
type: appendArgs.type,
|
|
671
|
-
payload: appendArgs.payload,
|
|
672
|
-
expectedVersion: handleVersion,
|
|
673
|
-
}, user, tx, registry.getHandlerFeature(type));
|
|
674
|
-
handleVersion += 1;
|
|
675
|
-
};
|
|
676
|
-
return {
|
|
677
|
-
events,
|
|
678
|
-
get version() {
|
|
679
|
-
return handleVersion;
|
|
680
|
-
},
|
|
681
|
-
appendOne,
|
|
682
|
-
hasEvent: (type) => events.some((e) => e.type === type),
|
|
683
|
-
};
|
|
684
|
-
},
|
|
685
|
-
loadAggregate: async (aggregateId, loadOptions) => {
|
|
686
|
-
const dbSource = resolveDbSource(ctx, tx);
|
|
687
|
-
if (!dbSource) {
|
|
688
|
-
throw new InternalError({
|
|
689
|
-
message: `ctx.loadAggregate("${aggregateId}") requires a database connection — none is configured.`,
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
const events = loadOptions?.asOf
|
|
693
|
-
? await loadAggregateAsOf(dbSource, aggregateId, user.tenantId, loadOptions.asOf)
|
|
694
|
-
: await loadAggregate(dbSource, aggregateId, user.tenantId);
|
|
695
|
-
return upcastStoredEvents(events, registry.getEventUpcasters(), {
|
|
696
|
-
db: dbSource,
|
|
697
|
-
tenantId: user.tenantId,
|
|
698
|
-
});
|
|
699
|
-
},
|
|
700
|
-
archiveStream: async (aggregateId, archiveArgs) => {
|
|
701
|
-
const dbSource = resolveDbSource(ctx, tx);
|
|
702
|
-
if (!dbSource) {
|
|
703
|
-
throw new InternalError({
|
|
704
|
-
message: `ctx.archiveStream("${aggregateId}") requires a database connection — none is configured.`,
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
await archiveStreamHelper(dbSource, {
|
|
708
|
-
tenantId: user.tenantId,
|
|
709
|
-
aggregateId,
|
|
710
|
-
aggregateType: archiveArgs.aggregateType,
|
|
711
|
-
archivedBy: user.id,
|
|
712
|
-
reason: archiveArgs.reason,
|
|
713
|
-
});
|
|
714
|
-
},
|
|
715
|
-
restoreStream: async (aggregateId) => {
|
|
716
|
-
const dbSource = resolveDbSource(ctx, tx);
|
|
717
|
-
if (!dbSource) {
|
|
718
|
-
throw new InternalError({
|
|
719
|
-
message: `ctx.restoreStream("${aggregateId}") requires a database connection — none is configured.`,
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
await restoreStreamHelper(dbSource, user.tenantId, aggregateId);
|
|
723
|
-
},
|
|
724
|
-
isStreamArchived: async (aggregateId) => {
|
|
725
|
-
const dbSource = resolveDbSource(ctx, tx);
|
|
726
|
-
if (!dbSource) {
|
|
727
|
-
throw new InternalError({
|
|
728
|
-
message: `ctx.isStreamArchived("${aggregateId}") requires a database connection — none is configured.`,
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
return isStreamArchived(dbSource, user.tenantId, aggregateId);
|
|
732
|
-
},
|
|
733
|
-
snapshotAggregate: async (snapshotArgs) => {
|
|
734
|
-
const dbSource = resolveDbSource(ctx, tx);
|
|
735
|
-
if (!dbSource) {
|
|
736
|
-
throw new InternalError({
|
|
737
|
-
message: `ctx.snapshotAggregate("${snapshotArgs.aggregateId}") requires a database connection — none is configured.`,
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
await saveSnapshot(dbSource, {
|
|
741
|
-
aggregateId: snapshotArgs.aggregateId,
|
|
742
|
-
tenantId: user.tenantId,
|
|
743
|
-
aggregateType: snapshotArgs.aggregateType,
|
|
744
|
-
version: snapshotArgs.version,
|
|
745
|
-
state: snapshotArgs.state,
|
|
746
|
-
snapshotVersion: effectiveSnapshotGeneration(snapshotArgs.snapshotVersion ?? 1, registry.getEntity(snapshotArgs.aggregateType)?.eventVersion),
|
|
747
|
-
});
|
|
748
|
-
},
|
|
749
|
-
loadAggregateWithSnapshot: async (aggregateId, reducer, initial, loadOptions) => {
|
|
750
|
-
const dbSource = resolveDbSource(ctx, tx);
|
|
751
|
-
if (!dbSource) {
|
|
752
|
-
throw new InternalError({
|
|
753
|
-
message: `ctx.loadAggregateWithSnapshot("${aggregateId}") requires a database connection — none is configured.`,
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
// Upcaster-aware: pass an upcastEvent callback so loadAggregateWithSnapshot
|
|
757
|
-
// walks every delta through the registered chain before invoking the
|
|
758
|
-
// user's (sync) reducer. Async upcasters (DB-enrichment) are awaited
|
|
759
|
-
// inside loadAggregateWithSnapshot — feature authors never see legacy
|
|
760
|
-
// payload shapes regardless of which load path they chose.
|
|
761
|
-
const upcasters = registry.getEventUpcasters();
|
|
762
|
-
const upcastCtx = { db: dbSource, tenantId: user.tenantId };
|
|
763
|
-
return loadAggregateWithSnapshot(dbSource, aggregateId, user.tenantId, reducer, initial, {
|
|
764
|
-
...loadOptions,
|
|
765
|
-
entityEventVersionOf: (aggregateType) => registry.getEntity(aggregateType)?.eventVersion,
|
|
766
|
-
upcastEvent: (event) => upcastStoredEvent(event, upcasters, upcastCtx), // @wrapper-known semantic-alias
|
|
767
|
-
});
|
|
768
|
-
},
|
|
769
|
-
queryProjection,
|
|
770
|
-
// Thin pass-through: one resolve impl lives on the dispatcher, the
|
|
771
|
-
// handler surface just forwards the call so both entry points (login
|
|
772
|
-
// handler via ctx.resolveAuthClaims, switch-tenant route via
|
|
773
|
-
// dispatcher.resolveAuthClaims) cannot drift.
|
|
774
|
-
resolveAuthClaims: (claimsUser) => resolveAuthClaimsFn(ctx, claimsUser), // @wrapper-known semantic-alias
|
|
775
|
-
// Thin pass-through, same reasoning as resolveAuthClaims above — one
|
|
776
|
-
// resolve impl lives on the dispatcher so callers can't drift apart.
|
|
777
|
-
// Internally queries memberships as SYSTEM, so it needs the same grant
|
|
778
|
-
// as a SYSTEM queryAs/writeAs (r.systemScope() or escapeHatch).
|
|
779
|
-
resolveActiveMembership: (userId, tenantId) => {
|
|
780
|
-
if (!hasIdentitySwitchGrant)
|
|
781
|
-
throw systemIdentitySwitchDenied(`handler "${type}"`);
|
|
782
|
-
return resolveActiveMembershipFn(ctx, userId, tenantId, INTERACTIVE_SIGN_IN_POLICY); // @wrapper-known semantic-alias
|
|
783
|
-
},
|
|
784
|
-
// Needs the same grant as a SYSTEM queryAs.
|
|
785
|
-
queryAsMember,
|
|
786
|
-
// Feature-effective check for in-handler opt-in logic. Scope:
|
|
787
|
-
// **current user's tenant** — for cross-tenant lookups (rare,
|
|
788
|
-
// SysAdmin operations) read effectiveFeatures(otherTenantId) directly.
|
|
789
|
-
// When the feature-toggles or tier-engine feature isn't wired (no
|
|
790
|
-
// effectiveFeatures callback), always returns true — apps without
|
|
791
|
-
// tier-cuts treat all features on.
|
|
792
|
-
//
|
|
793
|
-
// Falls back to the live trialGate when the sync set says the feature
|
|
794
|
-
// is off — the sync set never contains trial-tier features (time-
|
|
795
|
-
// derived, can't boot-cache), so without this a trial tenant checking
|
|
796
|
-
// a companion feature's toggle would silently read `false` even though
|
|
797
|
-
// the dispatch gate already lets trial-tier handlers run.
|
|
798
|
-
hasFeature: async (featureName) => {
|
|
799
|
-
if (!effectiveFeatures)
|
|
800
|
-
return true;
|
|
801
|
-
if (effectiveFeatures(user.tenantId).has(featureName))
|
|
802
|
-
return true;
|
|
803
|
-
if (!effectiveFeatures.trialGate)
|
|
804
|
-
return false;
|
|
805
|
-
return effectiveFeatures.trialGate(user.tenantId, featureName);
|
|
806
|
-
},
|
|
807
|
-
};
|
|
808
|
-
// Registry is always the dispatcher's registry — injecting it here lets
|
|
809
|
-
// tests/callers pass `context` without `registry` and still get a valid
|
|
810
|
-
// HandlerContext. The spread-then-assign order matters: anything in
|
|
811
|
-
// `context` can be overridden, but we want the authoritative registry
|
|
812
|
-
// from the dispatcher's own closure to win.
|
|
813
|
-
// ctx.tz is always present. tenant reads tenant:config:timezone through
|
|
814
|
-
// the already-built `config` accessor (raw qualified-key — no import of
|
|
815
|
-
// the tenant feature, framework/pipeline stays bundled-features-free);
|
|
816
|
-
// falls back to "UTC" when config is missing or the key is unset. user
|
|
817
|
-
// comes from SessionUser.timezone (set at login), else falls back to
|
|
818
|
-
// tenant (createTzContext's own default). An app-injected GeoTzProvider
|
|
819
|
-
// (context.geoTzProvider) feeds ctx.tz.fromCoordinates / fromAddress.
|
|
820
|
-
//
|
|
821
|
-
// Resolving this via `config()` is a config-table SELECT on every single
|
|
822
|
-
// dispatch (fw#2462) — ctx.tenantTimezoneCache memoizes it per tenant.
|
|
823
|
-
// dispatch-write.ts invalidates on config:write:set/reset for this key.
|
|
824
|
-
// Only populate from a tx-free read (tx === undefined): a write-path
|
|
825
|
-
// lookup runs inside the caller's own open transaction, and caching a
|
|
826
|
-
// value read there could memoize state from a write earlier in that same
|
|
827
|
-
// transaction before it's known to commit. Reads always consult the
|
|
828
|
-
// cache regardless of tx, since serving a slightly stale cached value on
|
|
829
|
-
// the write path is harmless (bounded by ttlMs) and avoids reintroducing
|
|
830
|
-
// the SELECT on the hot path this fix targets.
|
|
831
|
-
let tenantTz;
|
|
832
|
-
if (config === undefined) {
|
|
833
|
-
tenantTz = undefined;
|
|
834
|
-
}
|
|
835
|
-
else {
|
|
836
|
-
const cachedTz = ctx.tenantTimezoneCache.get(user.tenantId);
|
|
837
|
-
if (cachedTz) {
|
|
838
|
-
tenantTz = cachedTz.value;
|
|
839
|
-
}
|
|
840
|
-
else {
|
|
841
|
-
const generationAtRead = ctx.tenantTimezoneCache.generation();
|
|
842
|
-
tenantTz = await config(TENANT_TIMEZONE_CONFIG_KEY);
|
|
843
|
-
if (tx === undefined) {
|
|
844
|
-
ctx.tenantTimezoneCache.setIfFresh(user.tenantId, typeof tenantTz === "string" ? tenantTz : undefined, generationAtRead);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
// Guarded against garbage: an unvalidated string here (free-form config
|
|
849
|
-
// key, legacy JWT claim predating validation) blows up every ctx.tz call
|
|
850
|
-
// for the whole tenant with a RangeError. Fall back to UTC/tenant instead
|
|
851
|
-
// of trusting the raw value.
|
|
852
|
-
const safeTenantTz = typeof tenantTz === "string" && isValidIanaTimeZone(tenantTz) ? tenantTz : "UTC";
|
|
853
|
-
const safeUserTz = user.timezone !== undefined && isValidIanaTimeZone(user.timezone) ? user.timezone : undefined;
|
|
854
|
-
const tz = createTzContext({
|
|
855
|
-
...(context.geoTzProvider !== undefined ? { geoTz: context.geoTzProvider } : {}),
|
|
856
|
-
...(context.clock !== undefined ? { clock: context.clock } : {}),
|
|
857
|
-
tenant: safeTenantTz,
|
|
858
|
-
...(safeUserTz !== undefined && { user: safeUserTz }),
|
|
859
|
-
});
|
|
860
|
-
// ctx.locale — request-layer signal (X-Locale header → Accept-Language,
|
|
861
|
-
// resolved once at the HTTP boundary by request-id-middleware.ts) wins;
|
|
862
|
-
// then SessionUser.locale (set at login, see fw#2333 — stale until
|
|
863
|
-
// re-login, same tradeoff as ctx.tz.user); falls back to the app's
|
|
864
|
-
// boot-configured defaultLocale, then DEFAULT_LOCALE. Mirrors ctx.tz's
|
|
865
|
-
// Request → Session → Boot-Default chain above.
|
|
866
|
-
const safeUserLocale = user.locale !== undefined && isValidLocaleTag(user.locale) ? user.locale : undefined;
|
|
867
|
-
const locale = reqCtx?.locale ?? safeUserLocale ?? context.defaultLocale ?? DEFAULT_LOCALE;
|
|
215
|
+
export async function buildHandlerContext(ctx, type, user, origin, options = {}) {
|
|
216
|
+
const { tx, afterCommitHooks, includeDeleted, outerNotifySink, readOnlyForTenant = false, transactionScope = tx ? "inTransaction" : "withoutTransaction", } = options;
|
|
217
|
+
const scope = resolveHandlerContextScope({ ctx, type, user, origin, tx, transactionScope });
|
|
218
|
+
const dbFacet = buildHandlerDbFacet(scope);
|
|
219
|
+
const observability = buildObservabilityFacet(scope);
|
|
220
|
+
const sinks = createCommitSinks(afterCommitHooks, transactionScope, outerNotifySink);
|
|
221
|
+
const notifyFacet = buildNotifyFacet(scope, sinks, dbFacet.outsideTxSource);
|
|
222
|
+
const configFacet = buildConfigFacet(scope, dbFacet.db);
|
|
223
|
+
const forTenantFragment = buildForTenantFragment(scope, dbFacet.exposedDb, readOnlyForTenant);
|
|
224
|
+
const fileFragment = buildFileFragment(scope, dbFacet.db);
|
|
225
|
+
const dispatchFacet = buildDispatchFacet(scope, sinks);
|
|
226
|
+
const authFacet = buildAuthFacet(scope);
|
|
227
|
+
const eventStoreFacet = buildEventStoreFacet(scope);
|
|
228
|
+
const tz = await resolveHandlerTz(scope, configFacet.config);
|
|
229
|
+
const locale = resolveHandlerLocale(scope);
|
|
868
230
|
// The payload-bucket digest is dispatcher-internal; handlers must not be
|
|
869
231
|
// able to compute bucket keys.
|
|
870
232
|
const { _rateLimitPayloadDigest: payloadDigest,
|
|
871
233
|
// Bound to this call's tx: an outer context's factory must not leak into a tx-less one.
|
|
872
|
-
_notifyInTransactionFactory: _omitOuterNotifyInTransactionFactory, _notifyAfterCommitFactory: _omitOuterNotifyAfterCommitFactory, ...contextForHandler } = context;
|
|
234
|
+
_notifyInTransactionFactory: _omitOuterNotifyInTransactionFactory, _notifyAfterCommitFactory: _omitOuterNotifyAfterCommitFactory, ...contextForHandler } = scope.context;
|
|
873
235
|
const handlerContext = {
|
|
874
236
|
...contextForHandler,
|
|
875
237
|
...(payloadDigest && { rateLimitDigest: createHandlerBoundDigest(payloadDigest, type) }),
|
|
876
|
-
...
|
|
877
|
-
|
|
878
|
-
registry
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
...
|
|
887
|
-
...
|
|
888
|
-
...
|
|
889
|
-
...(
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
// (kumiko-framework#1672).
|
|
894
|
-
...(lifecycle && {
|
|
895
|
-
runPreSave: (handlerName, changes, previous, isNew) => lifecycle.runPreSave(handlerName, changes, previous, isNew, context),
|
|
896
|
-
}),
|
|
897
|
-
tracer,
|
|
898
|
-
metrics,
|
|
899
|
-
metricsFor,
|
|
238
|
+
...notifyFacet.factoryFragment,
|
|
239
|
+
// Registry is always the dispatcher's registry — injecting it here lets
|
|
240
|
+
// tests/callers pass `context` without `registry` and still get a valid
|
|
241
|
+
// HandlerContext. The spread-then-assign order matters: anything in
|
|
242
|
+
// `context` can be overridden, but we want the authoritative registry
|
|
243
|
+
// from the dispatcher's own closure to win.
|
|
244
|
+
registry: scope.registry,
|
|
245
|
+
...dbFacet.fragment,
|
|
246
|
+
log: observability.log,
|
|
247
|
+
notify: notifyFacet.notify,
|
|
248
|
+
...configFacet.fragment,
|
|
249
|
+
...forTenantFragment,
|
|
250
|
+
...fileFragment,
|
|
251
|
+
...buildPreSaveFragment(scope),
|
|
252
|
+
tracer: observability.tracer,
|
|
253
|
+
metrics: observability.metrics,
|
|
254
|
+
metricsFor: observability.metricsFor,
|
|
900
255
|
tz,
|
|
901
256
|
locale,
|
|
902
|
-
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
// Propagate the feature-toggle resolver so the lifecycle pipeline,
|
|
908
|
-
// MSP runner, and ctx.hasFeature all pull from the same source.
|
|
909
|
-
...(effectiveFeatures && { effectiveFeatures }),
|
|
910
|
-
...(ctx.cacheSync && { cacheSync: ctx.cacheSync }),
|
|
911
|
-
// Lets write handlers call ctx.jobRunner.dispatch(...) directly, same
|
|
912
|
-
// as a follow-up job would (test-stack.ts wires the matching runner).
|
|
913
|
-
...(jobRunner && { jobRunner }),
|
|
914
|
-
// ctx.user als Convenience-Alias auf event.user. Der typisch-
|
|
915
|
-
// intuitive Pfad „der Context kennt seinen User" — ohne den
|
|
916
|
-
// schreiben Handler `event.user.tenantId` und brechen sich die
|
|
917
|
-
// Finger an typo-resistenten ctx.user-Patterns. Identisch zum
|
|
918
|
-
// event.user-Wert; Identity-Switches nutzen weiterhin queryAs/writeAs.
|
|
257
|
+
...buildRuntimeFragment(scope),
|
|
258
|
+
// ctx.user as a convenience alias for event.user. The intuitive path—"the
|
|
259
|
+
// context knows its user"—lets handlers avoid writing `event.user.tenantId`
|
|
260
|
+
// and reduces errors in ctx.user access patterns. It is identical to
|
|
261
|
+
// event.user; identity switches still use queryAs/writeAs.
|
|
919
262
|
user,
|
|
920
263
|
_tenantId: user.tenantId,
|
|
921
|
-
...
|
|
264
|
+
...dispatchFacet.scheduleFragment,
|
|
922
265
|
...(includeDeleted && { includeDeleted: true }),
|
|
923
|
-
...bridge,
|
|
266
|
+
...dispatchFacet.bridge,
|
|
267
|
+
...eventStoreFacet,
|
|
268
|
+
...authFacet,
|
|
924
269
|
}; // @cast-boundary engine-bridge
|
|
925
270
|
const scopedContext = transactionScope === "withoutTransaction"
|
|
926
271
|
? denyEventStoreSurfaces(handlerContext, withoutTransactionDenied)
|
|
@@ -970,7 +315,7 @@ export async function runHandlerInstrumented(ctx, type, operation, user, inner)
|
|
|
970
315
|
attributes: dispatcherSpanAttributes(type, operation, user, registry.getHandlerFeature(type)),
|
|
971
316
|
}, async (span) => {
|
|
972
317
|
try {
|
|
973
|
-
//
|
|
318
|
+
// Everything the handler writes, including entity-executor
|
|
974
319
|
// writes below it, is attributed to this handler.
|
|
975
320
|
const result = await runWithOrigin({
|
|
976
321
|
handler: type,
|
|
@@ -1160,7 +505,7 @@ async function enforceBuckets(resolver, requests) {
|
|
|
1160
505
|
// pass through. ip-modes are commonly used at L1/L2 middleware
|
|
1161
506
|
// where the IP comes from Hono directly; falling back to "skip"
|
|
1162
507
|
// here keeps non-HTTP entry-points (jobs, MSPs) functional.
|
|
1163
|
-
export async function enforceRateLimit(ctx, rateLimit, handlerName, user, isSystemScope) {
|
|
508
|
+
export async function enforceRateLimit(ctx, rateLimit, handlerName, user, isSystemScope, costOverride) {
|
|
1164
509
|
const { appContext: context } = ctx;
|
|
1165
510
|
// skip: handler explicitly opts out via rateLimit: { disabled: true }
|
|
1166
511
|
if (isRateLimitDisabled(rateLimit))
|
|
@@ -1183,14 +528,15 @@ export async function enforceRateLimit(ctx, rateLimit, handlerName, user, isSyst
|
|
|
1183
528
|
// requiring a resolver; HTTP path always has an IP + L1/L2 middleware.
|
|
1184
529
|
if (bucket.kind === "skip")
|
|
1185
530
|
continue;
|
|
1186
|
-
const
|
|
531
|
+
const cost = costOverride ?? option.cost;
|
|
532
|
+
const chargedKey = `${bucket.key}|${option.limit}|${option.windowSeconds}|${cost}`;
|
|
1187
533
|
// skip: this exact bucket, limit and window was already charged by the entry dispatch
|
|
1188
534
|
if (charged?.has(chargedKey))
|
|
1189
535
|
continue;
|
|
1190
536
|
pending.push({
|
|
1191
537
|
request: {
|
|
1192
538
|
bucket: bucket.key,
|
|
1193
|
-
config: { limit: option.limit, windowSeconds: option.windowSeconds, cost
|
|
539
|
+
config: { limit: option.limit, windowSeconds: option.windowSeconds, cost },
|
|
1194
540
|
},
|
|
1195
541
|
chargedKey,
|
|
1196
542
|
});
|