@cosmicdrift/kumiko-framework 0.387.0 → 0.389.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 +9 -1
- 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/golden-path.md +13 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +55 -2
- package/agents/examples/list-table-options.md +466 -0
- package/agents/examples/pdf-audit-export.md +4 -2
- 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/state-machine.md +3 -2
- package/agents/examples/tenant-isolation.md +73 -1
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/examples.md +1 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +2 -0
- package/dist/api/api-constants.d.ts +1 -0
- package/dist/api/api-constants.js +2 -0
- package/dist/api/auth-routes-shared.d.ts +2 -0
- package/dist/api/auth-routes-shared.js +5 -0
- package/dist/api/client-error-route.js +10 -5
- package/dist/api/entity-read-access.d.ts +2 -1
- package/dist/api/entity-read-access.js +14 -0
- package/dist/api/export/export-cells-reference.d.ts +9 -0
- package/dist/api/export/export-cells-reference.js +21 -0
- package/dist/api/export/export-cells.d.ts +11 -0
- package/dist/api/export/export-cells.js +136 -0
- package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
- package/dist/api/export/export-ciphertext-guard.js +27 -0
- package/dist/api/export/export-columns.d.ts +29 -0
- package/dist/api/export/export-columns.js +85 -0
- package/dist/api/export/export-errors.d.ts +4 -0
- package/dist/api/export/export-errors.js +14 -0
- package/dist/api/export/export-route.d.ts +4 -0
- package/dist/api/export/export-route.js +74 -0
- package/dist/api/export/export-rows.d.ts +13 -0
- package/dist/api/export/export-rows.js +62 -0
- package/dist/api/export/export-screen.d.ts +12 -0
- package/dist/api/export/export-screen.js +47 -0
- package/dist/api/export/export-xlsx-writer.d.ts +4 -0
- package/dist/api/export/export-xlsx-writer.js +21 -0
- package/dist/api/extra-route.d.ts +4 -0
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/pii-leak-guard.d.ts +3 -0
- package/dist/api/pii-leak-guard.js +3 -2
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.d.ts +16 -0
- 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 +206 -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 +299 -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 +238 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1251
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-route.d.ts +2 -1
- package/dist/api/sse-route.js +27 -13
- package/dist/config/index.d.ts +3 -1
- 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.d.ts +1 -0
- package/dist/crypto/pg-kms-adapter.js +12 -18
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +3 -1
- package/dist/db/aggregate-sql.d.ts +23 -0
- package/dist/db/aggregate-sql.js +165 -0
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +88 -0
- package/dist/db/connection.d.ts +4 -5
- package/dist/db/connection.js +29 -20
- package/dist/db/dialect.js +1 -1
- 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 +101 -0
- 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.js +103 -110
- package/dist/db/event-store-executor-context.d.ts +2 -1
- package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
- package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +143 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +206 -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 +48 -701
- package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
- package/dist/db/event-store-executor-write-atomic.js +41 -0
- package/dist/db/event-store-executor-write-steps.d.ts +17 -0
- package/dist/db/event-store-executor-write-steps.js +95 -0
- package/dist/db/event-store-executor-write.js +405 -509
- package/dist/db/event-store-executor.js +2 -1
- package/dist/db/executor-managed-tables.js +1 -1
- package/dist/db/index.d.ts +7 -3
- package/dist/db/index.js +6 -2
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/migrate-generator.js +71 -86
- package/dist/db/migrate-runner.js +7 -4
- package/dist/db/pg-error.js +26 -21
- package/dist/db/queries/backfill-pii.js +3 -6
- package/dist/db/queries/database-now.js +2 -1
- package/dist/db/queries/shadow-swap.js +2 -3
- package/dist/db/query.d.ts +16 -15
- package/dist/db/query.js +76 -804
- package/dist/db/reference-data.js +19 -2
- package/dist/db/statement-timeout.js +2 -1
- package/dist/db/table-builder.js +113 -166
- 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-upsert.d.ts +5 -0
- package/dist/db/tenant-db-upsert.js +91 -0
- package/dist/db/tenant-db.js +50 -10
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +2 -1
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
- package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
- package/dist/engine/boot-validator/detail-screens.js +2 -2
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +17 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/i18n-keys.js +5 -3
- package/dist/engine/boot-validator/index.js +2 -0
- package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
- package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
- package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
- package/dist/engine/boot-validator/list-column-layout.js +41 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
- package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-export-options.js +27 -0
- package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
- package/dist/engine/boot-validator/list-table-options.js +77 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
- package/dist/engine/boot-validator/pii-retention.js +233 -155
- package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
- package/dist/engine/boot-validator/projection-list-screens.js +3 -13
- package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
- package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +8 -1
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +2 -6
- 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 +235 -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 +56 -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 +24 -11
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +99 -860
- package/dist/engine/build-app-schema.d.ts +2 -1
- package/dist/engine/build-app-schema.js +65 -97
- package/dist/engine/build-config-feature-schema.js +6 -1
- package/dist/engine/define-feature.js +13 -2
- package/dist/engine/define-handler.js +1 -0
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +10 -21
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/feature-ast/extractors/handlers.js +1 -0
- package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
- package/dist/engine/feature-ast/extractors/index.js +1 -1
- package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
- package/dist/engine/feature-ast/extractors/round1.js +6 -0
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/index.d.ts +1 -1
- package/dist/engine/feature-ast/parse.js +3 -1
- package/dist/engine/feature-ast/patch.d.ts +2 -0
- package/dist/engine/feature-ast/patch.js +5 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -245
- package/dist/engine/feature-ast/pattern-change-schema.js +10 -163
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +211 -0
- package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
- package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
- package/dist/engine/feature-ast/patterns.d.ts +14 -1
- package/dist/engine/feature-ast/patterns.js +44 -51
- package/dist/engine/feature-ast/render.js +52 -87
- package/dist/engine/feature-builder-state.d.ts +1 -0
- package/dist/engine/feature-builder-state.js +1 -0
- package/dist/engine/feature-entity-handlers.js +3 -0
- package/dist/engine/feature-manifest.d.ts +1 -0
- package/dist/engine/feature-manifest.js +1 -0
- package/dist/engine/field-access.d.ts +1 -0
- package/dist/engine/field-access.js +12 -0
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +1 -1
- package/dist/engine/pattern-library/library.js +2 -1
- package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
- package/dist/engine/pattern-library/static-schemas.js +9 -0
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +6 -4
- package/dist/engine/registry-ingest.js +1 -1
- package/dist/engine/schema-builder.js +228 -212
- package/dist/engine/screen-filter-ops.js +2 -0
- package/dist/engine/screen-helpers.js +1 -1
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +58 -17
- package/dist/engine/types/feature.d.ts +1 -0
- package/dist/engine/types/index.d.ts +4 -3
- package/dist/engine/types/screen.d.ts +1 -1
- package/dist/engine/types/screen.js +1 -1
- package/dist/engine/validate-projection-allowlist.js +2 -1
- package/dist/entrypoint/index.d.ts +6 -1
- package/dist/entrypoint/index.js +10 -1
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/es-ops/context.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/event-store.d.ts +1 -0
- package/dist/event-store/event-store.js +17 -8
- package/dist/event-store/provenance-append.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +7 -1
- package/dist/files/create-file-ref.d.ts +3 -0
- package/dist/files/create-file-ref.js +11 -2
- package/dist/files/file-handle.js +2 -3
- package/dist/files/file-routes.d.ts +2 -0
- package/dist/files/file-routes.js +40 -10
- package/dist/files/provider-resolver.d.ts +1 -0
- package/dist/files/provider-resolver.js +1 -1
- package/dist/files/record-write-check.d.ts +6 -0
- package/dist/files/record-write-check.js +13 -0
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/upload-ledger.d.ts +2 -2
- package/dist/files/upload-ledger.js +11 -4
- package/dist/i18n/required-surface-keys.js +189 -208
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/internal/testing.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 +61 -2
- 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 +50 -657
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +21 -3
- package/dist/pipeline/after-rollback-sink.d.ts +6 -0
- package/dist/pipeline/after-rollback-sink.js +32 -0
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +8 -1
- package/dist/pipeline/dispatch-query.js +10 -3
- package/dist/pipeline/dispatch-shared.d.ts +13 -3
- package/dist/pipeline/dispatch-shared.js +62 -710
- package/dist/pipeline/dispatch-write.js +296 -189
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +6 -6
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +6 -9
- 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 +23 -0
- package/dist/pipeline/handler-context-dispatch.js +101 -0
- package/dist/pipeline/handler-context-events.d.ts +41 -0
- package/dist/pipeline/handler-context-events.js +239 -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/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 +2 -1
- package/dist/pipeline/system-hooks.d.ts +3 -1
- package/dist/pipeline/system-hooks.js +55 -6
- package/dist/pipeline/write-result-masking.js +13 -10
- 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 -604
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/search/reindex-entity.js +15 -5
- package/dist/ui-types/index.d.ts +2 -2
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-changelog.d.ts +2 -0
- package/dist/upgrade-changelog.js +14 -0
- package/dist/upgrade-cli.d.ts +3 -12
- package/dist/upgrade-cli.js +171 -197
- package/dist/upgrade-marker-parse.d.ts +32 -0
- package/dist/upgrade-marker-parse.js +73 -0
- package/package.json +9 -6
- package/src/changes.json +555 -0
- package/src/scripts/codemod/codemod-support.ts +1 -1
- package/src/scripts/codemod/db-factory.ts +1 -1
- package/src/scripts/codemod/feature-layout.ts +10 -2
- 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 +175 -145
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/dist/engine/provider-plugin-context.d.ts +0 -3
- package/dist/engine/provider-plugin-context.js +0 -28
|
@@ -7,6 +7,7 @@ export type EventConsumerBatchHandler = (events: readonly StoredEvent[], ctx: Ap
|
|
|
7
7
|
export type EventConsumerErrorPolicy = {
|
|
8
8
|
readonly skipApplyErrors?: boolean;
|
|
9
9
|
readonly maxAttempts?: number;
|
|
10
|
+
readonly isTransientError?: (error: unknown) => boolean;
|
|
10
11
|
};
|
|
11
12
|
export type EventConsumer = {
|
|
12
13
|
readonly name: string;
|
|
@@ -16,12 +16,10 @@ const PENDING_GAPS_WARN_THRESHOLD = 1000;
|
|
|
16
16
|
// Only these say the database socket itself is gone; timeouts, schema or pool errors do not
|
|
17
17
|
// imply the dedicated LISTEN connection dropped.
|
|
18
18
|
const CONNECTION_LOSS_CODES = new Set([
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"ECONNREFUSED",
|
|
24
|
-
"ECONNRESET",
|
|
19
|
+
"ERR_POSTGRES_CONNECTION_CLOSED",
|
|
20
|
+
"ERR_POSTGRES_CONNECTION_REFUSED",
|
|
21
|
+
"ERR_POSTGRES_CONNECTION_TIMEOUT",
|
|
22
|
+
"ERR_POSTGRES_CONNECTION_FAILED",
|
|
25
23
|
"57P01",
|
|
26
24
|
]);
|
|
27
25
|
function isConnectionLossError(e) {
|
|
@@ -111,7 +109,7 @@ export function createEventDispatcher(options) {
|
|
|
111
109
|
let pgSubscription = null;
|
|
112
110
|
// Bounds how many consumer turns hold a DB transaction at once. Each turn's
|
|
113
111
|
// db.begin() checks out one pool connection, plus any writes the handler
|
|
114
|
-
// itself makes via context.db — the app db pool defaults to
|
|
112
|
+
// itself makes via context.db — the app db pool defaults to Bun.SQL's
|
|
115
113
|
// max=10 (db/connection.ts, no DATABASE_POOL_MAX override) and is shared
|
|
116
114
|
// with HTTP request handlers, so turns can't be allowed to claim it all.
|
|
117
115
|
// A pool no larger than this limit can deadlock: every connection held by
|
|
@@ -493,8 +491,7 @@ export function createEventDispatcher(options) {
|
|
|
493
491
|
const sub = await options.pgClient.listen(EVENTS_PUBSUB_CHANNEL, runBackgroundPass, () => {
|
|
494
492
|
// Fires on initial connect and again when the driver's own
|
|
495
493
|
// re-LISTEN lands after a drop; during an outage that re-LISTEN
|
|
496
|
-
// waits in
|
|
497
|
-
// after the DB returns), polling covers
|
|
494
|
+
// waits in the driver's reconnect backoff, polling covers
|
|
498
495
|
// delivery meanwhile. Never unlisten()
|
|
499
496
|
// + listen() by hand: that leaves duplicate listeners.
|
|
500
497
|
// ponytail: no own LISTEN reconnect, the backoff gap is polled;
|
|
@@ -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,23 @@
|
|
|
1
|
+
import type { WriteResult } from "../engine/types/index.ts";
|
|
2
|
+
import { type AfterRollbackHook } from "./after-rollback-sink.ts";
|
|
3
|
+
import { type HandlerTransactionScope } from "./dispatch-shared.ts";
|
|
4
|
+
import { type AfterCommitHook } from "./dispatcher-utils.ts";
|
|
5
|
+
import type { HandlerContextScope } from "./handler-context-scope.ts";
|
|
6
|
+
export type CommitSinks = {
|
|
7
|
+
readonly bridgeSink: AfterCommitHook[];
|
|
8
|
+
readonly hasLiveCommitSink: boolean;
|
|
9
|
+
readonly notifyCommitSink: AfterCommitHook[] | undefined;
|
|
10
|
+
};
|
|
11
|
+
export declare function createCommitSinks(afterCommitHooks: AfterCommitHook[] | undefined, transactionScope: HandlerTransactionScope, outerNotifySink: AfterCommitHook[] | undefined): CommitSinks;
|
|
12
|
+
export declare function buildDispatchFacet(scope: HandlerContextScope, sinks: CommitSinks): {
|
|
13
|
+
bridge: {
|
|
14
|
+
query: (targetType: string, payload: unknown) => Promise<unknown>;
|
|
15
|
+
queryAs: import("./system-identity-switch.ts").QueryAsFn;
|
|
16
|
+
write: (targetType: string, payload: unknown) => Promise<WriteResult>;
|
|
17
|
+
writeAs: import("./system-identity-switch.ts").WriteAsFn;
|
|
18
|
+
};
|
|
19
|
+
scheduleFragment: {
|
|
20
|
+
scheduleAfterCommit?: ((hook: AfterCommitHook) => void) | undefined;
|
|
21
|
+
scheduleAfterRollback?: ((hook: AfterRollbackHook) => void) | undefined;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { runWithWriteOrigin } from "../api/request-context.js";
|
|
2
|
+
import { runInSavepoint } from "../db/query.js";
|
|
3
|
+
import { writeFailure } from "../errors/index.js";
|
|
4
|
+
import { afterRollbackHooksOf, carrySavepointRollbackHooks, } from "./after-rollback-sink.js";
|
|
5
|
+
import { runBatch, unwrapSingle } from "./dispatch-batch.js";
|
|
6
|
+
import { executeQuery } from "./dispatch-query.js";
|
|
7
|
+
import { perCommandNestedDenied } from "./dispatch-shared.js";
|
|
8
|
+
import { executeWrite } from "./dispatch-write.js";
|
|
9
|
+
import { isFailedWriteResult, NestedWriteRollback, } from "./dispatcher-utils.js";
|
|
10
|
+
import { createGatedIdentitySwitch } from "./system-identity-switch.js";
|
|
11
|
+
export function createCommitSinks(afterCommitHooks, transactionScope, outerNotifySink) {
|
|
12
|
+
// Cross-feature bridge: ctx.query/write share the current tx + afterCommitHooks sink.
|
|
13
|
+
// queryAs/writeAs to anyone but the caller itself needs r.systemScope() or { escapeHatch } (system-identity-switch.ts).
|
|
14
|
+
const bridgeSink = afterCommitHooks ?? [];
|
|
15
|
+
// Query bridges and post-commit contexts get a throwaway sink: hooks pushed there never run.
|
|
16
|
+
const hasLiveCommitSink = afterCommitHooks !== undefined && transactionScope !== "postCommit";
|
|
17
|
+
return {
|
|
18
|
+
bridgeSink,
|
|
19
|
+
hasLiveCommitSink,
|
|
20
|
+
notifyCommitSink: hasLiveCommitSink ? bridgeSink : outerNotifySink,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// A failed inner write has already appended its event and projections when a later
|
|
24
|
+
// step (e.g. an inTransaction hook) fails, and the outer handler may swallow the
|
|
25
|
+
// failure result. The savepoint makes the failure undo exactly the inner write; the
|
|
26
|
+
// inner afterCommit hooks only reach the outer sink if it succeeded.
|
|
27
|
+
async function runNestedWriteInSavepoint(scope, sinks, asUser, targetType, payload) {
|
|
28
|
+
const { ctx, registry, origin, tx } = scope;
|
|
29
|
+
const { bridgeSink } = sinks;
|
|
30
|
+
if (registry.getWriteHandler(targetType)?.transaction === "perCommand") {
|
|
31
|
+
return writeFailure(perCommandNestedDenied(targetType));
|
|
32
|
+
}
|
|
33
|
+
if (!tx)
|
|
34
|
+
return executeWrite(ctx, targetType, payload, asUser, origin, tx, bridgeSink);
|
|
35
|
+
const innerAfterCommitHooks = [];
|
|
36
|
+
try {
|
|
37
|
+
const result = await runInSavepoint(tx, async (savepointTx) => {
|
|
38
|
+
const innerResult = await executeWrite(ctx, targetType, payload, asUser, origin, savepointTx, // @cast-boundary db-runner savepoint handle is a transaction handle
|
|
39
|
+
innerAfterCommitHooks);
|
|
40
|
+
if (isFailedWriteResult(innerResult))
|
|
41
|
+
throw new NestedWriteRollback(innerResult);
|
|
42
|
+
return innerResult;
|
|
43
|
+
});
|
|
44
|
+
bridgeSink.push(...innerAfterCommitHooks);
|
|
45
|
+
carrySavepointRollbackHooks(innerAfterCommitHooks, bridgeSink, false);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
carrySavepointRollbackHooks(innerAfterCommitHooks, bridgeSink, true);
|
|
50
|
+
if (e instanceof NestedWriteRollback)
|
|
51
|
+
return e.failure;
|
|
52
|
+
throw e;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Savepoints on one connection are not serialized by the driver: two parallel nested writes would
|
|
56
|
+
// interleave their statements and a ROLLBACK TO of the first would also discard the second's rows.
|
|
57
|
+
// Writes nested one level deeper run on their own savepoint handle and therefore their own chain.
|
|
58
|
+
function createSerializedNestedWriter(scope, sinks) {
|
|
59
|
+
let nestedWriteChain = Promise.resolve();
|
|
60
|
+
return (asUser, targetType, payload) => {
|
|
61
|
+
const run = nestedWriteChain.then(() => runNestedWriteInSavepoint(scope, sinks, asUser, targetType, payload));
|
|
62
|
+
nestedWriteChain = run.catch(() => undefined);
|
|
63
|
+
return run;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function buildDispatchFacet(scope, sinks) {
|
|
67
|
+
const { ctx, type, user, origin, tx, transactionScope, hasIdentitySwitchGrant, identitySwitchAudit, } = scope;
|
|
68
|
+
const scheduleAfterCommit = (hook) => {
|
|
69
|
+
sinks.bridgeSink.push(hook);
|
|
70
|
+
};
|
|
71
|
+
const scheduleAfterRollback = (hook) => {
|
|
72
|
+
afterRollbackHooksOf(sinks.bridgeSink).push(() => runWithWriteOrigin(origin, hook));
|
|
73
|
+
};
|
|
74
|
+
const executeNestedWriteInSavepoint = createSerializedNestedWriter(scope, sinks);
|
|
75
|
+
// Without an open transaction (post-commit hooks, stream/query handlers, perCommand handlers) a bare
|
|
76
|
+
// executeWrite would auto-commit event append, projections and inTransaction hooks separately and its
|
|
77
|
+
// afterCommit hooks would land in a sink nobody flushes. A batch-of-one gives the write its own
|
|
78
|
+
// transaction and its own flush.
|
|
79
|
+
const writeAsUser = async (asUser, targetType, payload) => transactionScope === "inTransaction"
|
|
80
|
+
? executeNestedWriteInSavepoint(asUser, targetType, payload)
|
|
81
|
+
: unwrapSingle(await runBatch(ctx, [{ type: targetType, payload }], asUser, undefined, origin));
|
|
82
|
+
const identitySwitch = createGatedIdentitySwitch(`handler "${type}"`, user, hasIdentitySwitchGrant, {
|
|
83
|
+
// Inherits the caller's origin, so switching to SYSTEM cannot shed an anonymous root.
|
|
84
|
+
queryAs: (asUser, targetType, payload) => executeQuery(ctx, targetType, payload, asUser, origin, tx, sinks.notifyCommitSink), // @wrapper-known semantic-alias
|
|
85
|
+
writeAs: writeAsUser,
|
|
86
|
+
}, identitySwitchAudit);
|
|
87
|
+
return {
|
|
88
|
+
bridge: {
|
|
89
|
+
query: (targetType, payload) => executeQuery(ctx, targetType, payload, user, origin, tx, sinks.notifyCommitSink), // @wrapper-known semantic-alias
|
|
90
|
+
queryAs: identitySwitch.queryAs,
|
|
91
|
+
write: (targetType, payload) => writeAsUser(user, targetType, payload),
|
|
92
|
+
writeAs: identitySwitch.writeAs,
|
|
93
|
+
},
|
|
94
|
+
scheduleFragment: {
|
|
95
|
+
...(sinks.hasLiveCommitSink && { scheduleAfterCommit }),
|
|
96
|
+
// perCommand and no-tx paths have earlier commands already committed: nothing to compensate on rollback.
|
|
97
|
+
...(sinks.hasLiveCommitSink &&
|
|
98
|
+
transactionScope === "inTransaction" && { scheduleAfterRollback }),
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AggregateStreamHandle, AppendEventArgs, AppendEventFn, FetchForWritingArgs, HandlerContext } from "../engine/types/index.ts";
|
|
2
|
+
import { IdempotentAppendConflictError as EventStoreIdempotentAppendConflictError, VersionConflictError as EventStoreVersionConflictError } from "../event-store/errors.ts";
|
|
3
|
+
import { type StoredEvent } from "../event-store/event-store.ts";
|
|
4
|
+
import { type LoadAggregateWithSnapshotOptions, type LoadAggregateWithSnapshotResult, type SnapshotReducer } from "../event-store/snapshot.ts";
|
|
5
|
+
import type { HandlerContextScope } from "./handler-context-scope.ts";
|
|
6
|
+
export declare function buildEventStoreFacet(scope: HandlerContextScope): {
|
|
7
|
+
appendEvent: AppendEventFn;
|
|
8
|
+
unsafeAppendEvent: (args: AppendEventArgs) => Promise<void>;
|
|
9
|
+
tryAppendEvent: (args: AppendEventArgs) => Promise<{
|
|
10
|
+
ok: true;
|
|
11
|
+
event: StoredEvent;
|
|
12
|
+
conflict?: undefined;
|
|
13
|
+
} | {
|
|
14
|
+
event?: undefined;
|
|
15
|
+
ok: false;
|
|
16
|
+
conflict: EventStoreVersionConflictError;
|
|
17
|
+
} | {
|
|
18
|
+
event?: undefined;
|
|
19
|
+
ok: false;
|
|
20
|
+
conflict: EventStoreIdempotentAppendConflictError;
|
|
21
|
+
}>;
|
|
22
|
+
fetchForWriting: (args: FetchForWritingArgs) => Promise<AggregateStreamHandle>;
|
|
23
|
+
loadAggregate: (aggregateId: string, loadOptions?: Parameters<HandlerContext["loadAggregate"]>[1]) => Promise<readonly StoredEvent[]>;
|
|
24
|
+
archiveStream: (aggregateId: string, archiveArgs: {
|
|
25
|
+
readonly aggregateType: string;
|
|
26
|
+
readonly reason?: string;
|
|
27
|
+
}) => Promise<void>;
|
|
28
|
+
restoreStream: (aggregateId: string) => Promise<void>;
|
|
29
|
+
isStreamArchived: (aggregateId: string) => Promise<boolean>;
|
|
30
|
+
snapshotAggregate: (snapshotArgs: {
|
|
31
|
+
readonly aggregateId: string;
|
|
32
|
+
readonly aggregateType: string;
|
|
33
|
+
readonly version: number;
|
|
34
|
+
readonly state: Record<string, unknown>;
|
|
35
|
+
readonly snapshotVersion?: number;
|
|
36
|
+
}) => Promise<void>;
|
|
37
|
+
loadAggregateWithSnapshot: <TState extends Record<string, unknown>>(aggregateId: string, reducer: SnapshotReducer<TState>, initial: TState, loadOptions?: Omit<LoadAggregateWithSnapshotOptions, "upcastEvent" | "entityEventVersionOf">) => Promise<LoadAggregateWithSnapshotResult<TState>>;
|
|
38
|
+
queryProjection: <T = Record<string, unknown>>(qualifiedName: string, options?: {
|
|
39
|
+
readonly unsafeAllTenants?: boolean;
|
|
40
|
+
}) => Promise<readonly T[]>;
|
|
41
|
+
};
|