pipework 0.251.0 → 0.252.1
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/CHANGELOG.md +11 -0
- package/dist/core/config/load.d.ts +2 -0
- package/dist/core/config/load.d.ts.map +1 -1
- package/dist/core/config/load.js +1 -0
- package/dist/core/config/load.js.map +1 -1
- package/dist/core/config/namespace.d.ts +1 -0
- package/dist/core/config/namespace.d.ts.map +1 -1
- package/dist/core/config/schema.d.ts +2 -0
- package/dist/core/config/schema.d.ts.map +1 -1
- package/dist/core/config/schema.js +1 -0
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/surface/types.d.ts +1 -0
- package/dist/core/surface/types.d.ts.map +1 -1
- package/dist/data/db/namespace.d.ts +3 -0
- package/dist/data/db/namespace.d.ts.map +1 -1
- package/dist/data/db/namespace.js +3 -0
- package/dist/data/db/namespace.js.map +1 -1
- package/dist/data/db/pool.d.ts.map +1 -1
- package/dist/data/db/pool.js +6 -1
- package/dist/data/db/pool.js.map +1 -1
- package/dist/data/db/query-label.d.ts +5 -0
- package/dist/data/db/query-label.d.ts.map +1 -0
- package/dist/data/db/query-label.js +11 -0
- package/dist/data/db/query-label.js.map +1 -0
- package/dist/data/db/query-watch.d.ts +21 -0
- package/dist/data/db/query-watch.d.ts.map +1 -0
- package/dist/data/db/query-watch.js +90 -0
- package/dist/data/db/query-watch.js.map +1 -0
- package/dist/data/query/postgres-js/driver.js +1 -1
- package/dist/data/query/postgres-js/driver.js.map +1 -1
- package/dist/data/query/postgres-js/session.d.ts +5 -1
- package/dist/data/query/postgres-js/session.d.ts.map +1 -1
- package/dist/data/query/postgres-js/session.js +16 -11
- package/dist/data/query/postgres-js/session.js.map +1 -1
- package/dist/data/query/utils.d.ts +2 -0
- package/dist/data/query/utils.d.ts.map +1 -1
- package/dist/data/query/utils.js.map +1 -1
- package/dist/infra/credits/schema.d.ts +1 -1
- package/dist/test/plugin.d.ts.map +1 -1
- package/dist/test/plugin.js +10 -4
- package/dist/test/plugin.js.map +1 -1
- package/package.json +4 -3
- package/src/async/jobs/abortable-delay.ts +18 -0
- package/src/async/jobs/claims.ts +52 -0
- package/src/async/jobs/cron.ts +179 -0
- package/src/async/jobs/errors.ts +56 -0
- package/src/async/jobs/exclusion.ts +117 -0
- package/src/async/jobs/execute.ts +219 -0
- package/src/async/jobs/index.ts +10 -0
- package/src/async/jobs/listener.ts +135 -0
- package/src/async/jobs/namespace.ts +20 -0
- package/src/async/jobs/queue-door.ts +44 -0
- package/src/async/jobs/queue.ts +1012 -0
- package/src/async/jobs/run-script.ts +67 -0
- package/src/async/jobs/scheduler.ts +218 -0
- package/src/async/jobs/schedules-table.ts +37 -0
- package/src/async/jobs/serialize-error.ts +48 -0
- package/src/async/jobs/table.ts +191 -0
- package/src/async/pipeline/define.ts +53 -0
- package/src/async/pipeline/errors.ts +35 -0
- package/src/async/pipeline/execute.ts +53 -0
- package/src/async/pipeline/index.ts +13 -0
- package/src/async/pipeline/namespace.ts +12 -0
- package/src/async/pipeline/resume.ts +67 -0
- package/src/async/pipeline/run-steps.ts +98 -0
- package/src/async/pipeline/types.ts +57 -0
- package/src/async/state-machine/define.ts +64 -0
- package/src/async/state-machine/errors.ts +29 -0
- package/src/async/state-machine/index.ts +3 -0
- package/src/async/state-machine/namespace.ts +12 -0
- package/src/async/state-machine/transition.ts +90 -0
- package/src/async/state-machine/types.ts +37 -0
- package/src/async/steps/errors.ts +31 -0
- package/src/async/steps/graph.ts +1214 -0
- package/src/async/steps/hash.ts +40 -0
- package/src/async/steps/index.ts +7 -0
- package/src/async/steps/namespace.ts +7 -0
- package/src/async/steps/table.ts +97 -0
- package/src/async/steps/types.ts +318 -0
- package/src/audit/chain/index.ts +9 -0
- package/src/audit/chain/install.ts +113 -0
- package/src/audit/chain/retire.ts +52 -0
- package/src/audit/chain/verify.ts +257 -0
- package/src/audit/query/cursor.ts +37 -0
- package/src/audit/query/index.ts +9 -0
- package/src/audit/query/query.ts +223 -0
- package/src/audit/schema/audit-record-fields.ts +68 -0
- package/src/audit/schema/audit-record.ts +23 -0
- package/src/auth/auth/audit.ts +31 -0
- package/src/auth/auth/chain.ts +84 -0
- package/src/auth/auth/cookie.ts +136 -0
- package/src/auth/auth/email-links.ts +311 -0
- package/src/auth/auth/guard.ts +128 -0
- package/src/auth/auth/index.ts +18 -0
- package/src/auth/auth/multi-org.ts +337 -0
- package/src/auth/auth/namespace.ts +34 -0
- package/src/auth/auth/parse-expiry.ts +13 -0
- package/src/auth/auth/passwords.ts +110 -0
- package/src/auth/auth/schema.ts +110 -0
- package/src/auth/auth/sessions.ts +565 -0
- package/src/auth/auth/sign-in.ts +156 -0
- package/src/auth/auth/signing-keys.ts +69 -0
- package/src/auth/auth/types.ts +26 -0
- package/src/auth/auth/uuid.ts +10 -0
- package/src/auth/rbac/cache.ts +55 -0
- package/src/auth/rbac/check.ts +30 -0
- package/src/auth/rbac/create.ts +35 -0
- package/src/auth/rbac/index.ts +5 -0
- package/src/auth/rbac/middleware.ts +20 -0
- package/src/auth/rbac/namespace.ts +15 -0
- package/src/auth/rbac/resolve.ts +31 -0
- package/src/auth/rbac/schema.sql +31 -0
- package/src/auth/rbac/seed.ts +67 -0
- package/src/auth/rbac/tables.ts +32 -0
- package/src/auth/rbac/types.ts +65 -0
- package/src/auth/tenant/extract.ts +33 -0
- package/src/auth/tenant/in-tenant.ts +49 -0
- package/src/auth/tenant/index.ts +7 -0
- package/src/auth/tenant/namespace.ts +23 -0
- package/src/auth/tenant/propagate.ts +20 -0
- package/src/auth/tenant/rls.ts +120 -0
- package/src/auth/tenant/scope.ts +33 -0
- package/src/auth/tenant/scoped-db.ts +581 -0
- package/src/auth/tenant/types.ts +14 -0
- package/src/auth/tenant/validate.ts +27 -0
- package/src/cli/commands/add.ts +158 -0
- package/src/cli/commands/alerts.ts +98 -0
- package/src/cli/commands/check.ts +1418 -0
- package/src/cli/commands/codemod.ts +78 -0
- package/src/cli/commands/db.ts +356 -0
- package/src/cli/commands/dev.ts +101 -0
- package/src/cli/commands/generate.ts +75 -0
- package/src/cli/commands/init.ts +36 -0
- package/src/cli/commands/install.ts +28 -0
- package/src/cli/commands/lint.ts +62 -0
- package/src/cli/commands/migrate.ts +132 -0
- package/src/cli/commands/openapi.ts +63 -0
- package/src/cli/commands/remove.ts +72 -0
- package/src/cli/commands/run.ts +50 -0
- package/src/cli/commands/scope.ts +54 -0
- package/src/cli/commands/serve.ts +68 -0
- package/src/cli/commands/shard.ts +31 -0
- package/src/cli/commands/shared/config-pipework-version.ts +28 -0
- package/src/cli/commands/shared/find-config.ts +22 -0
- package/src/cli/commands/shared/load-config.ts +35 -0
- package/src/cli/commands/shared/pnpm-install.ts +95 -0
- package/src/cli/commands/shared/report-removed.ts +8 -0
- package/src/cli/commands/sync.ts +32 -0
- package/src/cli/commands/test.ts +307 -0
- package/src/cli/commands/upgrade.ts +48 -0
- package/src/cli/dev-bootstrap.ts +24 -0
- package/src/cli/errors.ts +10 -0
- package/src/cli/index.ts +303 -0
- package/src/cli/output.ts +17 -0
- package/src/cli/port.ts +15 -0
- package/src/cli/surface-kinds.ts +20 -0
- package/src/client/index.ts +4 -0
- package/src/client/poll.ts +130 -0
- package/src/client/report-fault.ts +119 -0
- package/src/core/config/app-pool.ts +19 -0
- package/src/core/config/aws-env.ts +29 -0
- package/src/core/config/base-dir.ts +24 -0
- package/src/core/config/discover.ts +64 -0
- package/src/core/config/env-resolve.ts +87 -0
- package/src/core/config/env-types.ts +116 -0
- package/src/core/config/env.ts +55 -0
- package/src/core/config/errors.ts +9 -0
- package/src/core/config/import-config.ts +40 -0
- package/src/core/config/index.ts +8 -0
- package/src/core/config/load.ts +417 -0
- package/src/core/config/namespace.ts +14 -0
- package/src/core/config/resolve.ts +195 -0
- package/src/core/config/schema.ts +488 -0
- package/src/core/config/statement-observer.ts +48 -0
- package/src/core/config/ts-inline-hooks.ts +75 -0
- package/src/core/config/ts-register.ts +4 -0
- package/src/core/config/ts-resolve-hooks.ts +101 -0
- package/src/core/errors/base.ts +49 -0
- package/src/core/errors/http.ts +146 -0
- package/src/core/errors/index.ts +2 -0
- package/src/core/errors/namespace.ts +31 -0
- package/src/core/lifecycle/drain.ts +33 -0
- package/src/core/lifecycle/health.ts +72 -0
- package/src/core/lifecycle/index.ts +4 -0
- package/src/core/lifecycle/namespace.ts +7 -0
- package/src/core/lifecycle/startup.ts +102 -0
- package/src/core/lifecycle/types.ts +36 -0
- package/src/core/logging/context.ts +60 -0
- package/src/core/logging/error-chain.ts +68 -0
- package/src/core/logging/index.ts +5 -0
- package/src/core/logging/logger.ts +65 -0
- package/src/core/logging/namespace.ts +16 -0
- package/src/core/logging/proxy.ts +22 -0
- package/src/core/logging/redact-match.ts +24 -0
- package/src/core/logging/types.ts +47 -0
- package/src/core/pipework.ts +566 -0
- package/src/core/runtime/broker.ts +69 -0
- package/src/core/surface/http.ts +133 -0
- package/src/core/surface/index.ts +11 -0
- package/src/core/surface/job-timeout.ts +40 -0
- package/src/core/surface/namespace.ts +13 -0
- package/src/core/surface/script.ts +33 -0
- package/src/core/surface/start.ts +142 -0
- package/src/core/surface/types.ts +225 -0
- package/src/core/surface/validate.ts +88 -0
- package/src/core/surface/worker.ts +528 -0
- package/src/core/validation/connectivity.ts +49 -0
- package/src/core/validation/domain.ts +99 -0
- package/src/core/validation/index.ts +5 -0
- package/src/core/validation/queue-tables.ts +312 -0
- package/src/core/validation/rls-role.ts +138 -0
- package/src/core/validation/startup.ts +51 -0
- package/src/core/validation/surfaces.ts +65 -0
- package/src/core/validation/types.ts +25 -0
- package/src/data/db/advisory-lock.ts +55 -0
- package/src/data/db/aggregates.ts +172 -0
- package/src/data/db/array-elements.ts +111 -0
- package/src/data/db/bulk-set.ts +53 -0
- package/src/data/db/col/index.ts +14 -0
- package/src/data/db/composed.ts +53 -0
- package/src/data/db/db.ts +276 -0
- package/src/data/db/excluded.ts +16 -0
- package/src/data/db/expressions.ts +473 -0
- package/src/data/db/filter.ts +90 -0
- package/src/data/db/function-source.ts +114 -0
- package/src/data/db/identifiers.ts +16 -0
- package/src/data/db/idx/index.ts +9 -0
- package/src/data/db/in-list.ts +49 -0
- package/src/data/db/index.ts +9 -0
- package/src/data/db/insert-split.ts +137 -0
- package/src/data/db/instrumentation.ts +82 -0
- package/src/data/db/maintenance.ts +28 -0
- package/src/data/db/namespace.ts +107 -0
- package/src/data/db/owner-labels.ts +9 -0
- package/src/data/db/pool.ts +187 -0
- package/src/data/db/query-label.ts +13 -0
- package/src/data/db/query-watch.ts +116 -0
- package/src/data/db/raw-client-warning.ts +37 -0
- package/src/data/db/readonly.ts +23 -0
- package/src/data/db/reservation.ts +151 -0
- package/src/data/db/schema-features.ts +14 -0
- package/src/data/db/serializable.ts +83 -0
- package/src/data/db/sql.ts +1 -0
- package/src/data/db/timeouts.ts +76 -0
- package/src/data/db/types.ts +18 -0
- package/src/data/db/unbound.ts +33 -0
- package/src/data/db/values.ts +29 -0
- package/src/data/domain/brand.ts +15 -0
- package/src/data/domain/build-factory.ts +193 -0
- package/src/data/domain/build-schema.ts +161 -0
- package/src/data/domain/build-table.ts +355 -0
- package/src/data/domain/define.ts +239 -0
- package/src/data/domain/external-registry.ts +34 -0
- package/src/data/domain/external.ts +77 -0
- package/src/data/domain/field.ts +279 -0
- package/src/data/domain/index.ts +13 -0
- package/src/data/domain/naming.ts +6 -0
- package/src/data/domain/projections.ts +57 -0
- package/src/data/domain/trace-safe-projection.ts +31 -0
- package/src/data/domain/traits.ts +108 -0
- package/src/data/domain/types.ts +289 -0
- package/src/data/jsonb/composed.ts +18 -0
- package/src/data/jsonb/index.ts +1 -0
- package/src/data/jsonb/namespace.ts +30 -0
- package/src/data/jsonb/query.ts +86 -0
- package/src/data/migrate/append-only-generate.ts +47 -0
- package/src/data/migrate/applied.ts +47 -0
- package/src/data/migrate/apply.ts +309 -0
- package/src/data/migrate/check.ts +119 -0
- package/src/data/migrate/diff.ts +357 -0
- package/src/data/migrate/door-generate.ts +130 -0
- package/src/data/migrate/framework-rls.ts +58 -0
- package/src/data/migrate/generate.ts +201 -0
- package/src/data/migrate/history.ts +215 -0
- package/src/data/migrate/index.ts +9 -0
- package/src/data/migrate/internal-definitions.ts +175 -0
- package/src/data/migrate/post-process.ts +73 -0
- package/src/data/migrate/pragma.ts +73 -0
- package/src/data/migrate/provision-framework.ts +147 -0
- package/src/data/migrate/rls-generate.ts +61 -0
- package/src/data/migrate/run.ts +124 -0
- package/src/data/migrate/safety.ts +167 -0
- package/src/data/migrate/snapshot.ts +368 -0
- package/src/data/migrate/sql-emit.ts +392 -0
- package/src/data/migrate/statements.ts +312 -0
- package/src/data/query/alias.ts +126 -0
- package/src/data/query/batch.ts +8 -0
- package/src/data/query/cache/core/cache.ts +78 -0
- package/src/data/query/cache/core/index.ts +1 -0
- package/src/data/query/cache/core/types.ts +29 -0
- package/src/data/query/cache/readme.md +230 -0
- package/src/data/query/casing.ts +76 -0
- package/src/data/query/collation.ts +11 -0
- package/src/data/query/column-builder.ts +370 -0
- package/src/data/query/column.ts +150 -0
- package/src/data/query/entity.ts +42 -0
- package/src/data/query/errors.ts +33 -0
- package/src/data/query/index.ts +14 -0
- package/src/data/query/logger.ts +47 -0
- package/src/data/query/operations.ts +45 -0
- package/src/data/query/pg-core/alias.ts +12 -0
- package/src/data/query/pg-core/checks.ts +32 -0
- package/src/data/query/pg-core/columns/all.ts +71 -0
- package/src/data/query/pg-core/columns/bigint.ts +110 -0
- package/src/data/query/pg-core/columns/bigserial.ts +128 -0
- package/src/data/query/pg-core/columns/boolean.ts +43 -0
- package/src/data/query/pg-core/columns/char.ts +85 -0
- package/src/data/query/pg-core/columns/cidr.ts +43 -0
- package/src/data/query/pg-core/columns/common.ts +366 -0
- package/src/data/query/pg-core/columns/custom.ts +232 -0
- package/src/data/query/pg-core/columns/date.common.ts +15 -0
- package/src/data/query/pg-core/columns/date.ts +112 -0
- package/src/data/query/pg-core/columns/double-precision.ts +55 -0
- package/src/data/query/pg-core/columns/enum.ts +201 -0
- package/src/data/query/pg-core/columns/index.ts +35 -0
- package/src/data/query/pg-core/columns/inet.ts +43 -0
- package/src/data/query/pg-core/columns/int.common.ts +57 -0
- package/src/data/query/pg-core/columns/integer.ts +53 -0
- package/src/data/query/pg-core/columns/interval.ts +83 -0
- package/src/data/query/pg-core/columns/json.ts +58 -0
- package/src/data/query/pg-core/columns/jsonb.ts +56 -0
- package/src/data/query/pg-core/columns/line.ts +117 -0
- package/src/data/query/pg-core/columns/macaddr.ts +43 -0
- package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
- package/src/data/query/pg-core/columns/numeric.ts +227 -0
- package/src/data/query/pg-core/columns/point.ts +127 -0
- package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
- package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
- package/src/data/query/pg-core/columns/real.ts +58 -0
- package/src/data/query/pg-core/columns/serial.ts +55 -0
- package/src/data/query/pg-core/columns/smallint.ts +53 -0
- package/src/data/query/pg-core/columns/smallserial.ts +60 -0
- package/src/data/query/pg-core/columns/text.ts +67 -0
- package/src/data/query/pg-core/columns/time.ts +72 -0
- package/src/data/query/pg-core/columns/timestamp.ts +163 -0
- package/src/data/query/pg-core/columns/uuid.ts +51 -0
- package/src/data/query/pg-core/columns/varchar.ts +89 -0
- package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
- package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
- package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
- package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
- package/src/data/query/pg-core/db.ts +674 -0
- package/src/data/query/pg-core/dialect.ts +1404 -0
- package/src/data/query/pg-core/expressions.ts +25 -0
- package/src/data/query/pg-core/foreign-keys.ts +130 -0
- package/src/data/query/pg-core/index.ts +21 -0
- package/src/data/query/pg-core/indexes.ts +276 -0
- package/src/data/query/pg-core/policies.ts +56 -0
- package/src/data/query/pg-core/primary-keys.ts +62 -0
- package/src/data/query/pg-core/query-builders/count.ts +86 -0
- package/src/data/query/pg-core/query-builders/delete.ts +293 -0
- package/src/data/query/pg-core/query-builders/index.ts +7 -0
- package/src/data/query/pg-core/query-builders/insert.ts +452 -0
- package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
- package/src/data/query/pg-core/query-builders/query.ts +157 -0
- package/src/data/query/pg-core/query-builders/raw.ts +49 -0
- package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
- package/src/data/query/pg-core/query-builders/select.ts +1346 -0
- package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
- package/src/data/query/pg-core/query-builders/update.ts +626 -0
- package/src/data/query/pg-core/roles.ts +41 -0
- package/src/data/query/pg-core/schema.ts +72 -0
- package/src/data/query/pg-core/sequence.ts +41 -0
- package/src/data/query/pg-core/session.ts +292 -0
- package/src/data/query/pg-core/subquery.ts +46 -0
- package/src/data/query/pg-core/table.ts +252 -0
- package/src/data/query/pg-core/unique-constraint.ts +73 -0
- package/src/data/query/pg-core/utils/array.ts +95 -0
- package/src/data/query/pg-core/utils/index.ts +1 -0
- package/src/data/query/pg-core/utils.ts +104 -0
- package/src/data/query/pg-core/view-base.ts +14 -0
- package/src/data/query/pg-core/view-common.ts +1 -0
- package/src/data/query/pg-core/view.ts +435 -0
- package/src/data/query/pg-core/with-builder.ts +41 -0
- package/src/data/query/postgres-js/README.md +46 -0
- package/src/data/query/postgres-js/array-parser.ts +45 -0
- package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
- package/src/data/query/postgres-js/driver.ts +163 -0
- package/src/data/query/postgres-js/index.ts +2 -0
- package/src/data/query/postgres-js/session.ts +234 -0
- package/src/data/query/primary-key.ts +11 -0
- package/src/data/query/query-builders/query-builder.ts +19 -0
- package/src/data/query/query-builders/select.types.ts +175 -0
- package/src/data/query/query-promise.ts +35 -0
- package/src/data/query/relations.ts +725 -0
- package/src/data/query/runnable-query.ts +12 -0
- package/src/data/query/selection-proxy.ts +121 -0
- package/src/data/query/session.ts +8 -0
- package/src/data/query/sql/expressions/conditions.ts +741 -0
- package/src/data/query/sql/expressions/index.ts +2 -0
- package/src/data/query/sql/expressions/select.ts +45 -0
- package/src/data/query/sql/functions/aggregate.ts +129 -0
- package/src/data/query/sql/functions/index.ts +2 -0
- package/src/data/query/sql/functions/vector.ts +178 -0
- package/src/data/query/sql/index.ts +3 -0
- package/src/data/query/sql/sql.ts +794 -0
- package/src/data/query/subquery.ts +53 -0
- package/src/data/query/table.ts +208 -0
- package/src/data/query/table.utils.ts +2 -0
- package/src/data/query/tracing-utils.ts +3 -0
- package/src/data/query/tracing.ts +11 -0
- package/src/data/query/utils.ts +357 -0
- package/src/data/query/version.ts +4 -0
- package/src/data/query/view-common.ts +1 -0
- package/src/data/schema/branded.ts +13 -0
- package/src/data/schema/generated/coerce.ts +5 -0
- package/src/data/schema/generated/combinator.ts +6 -0
- package/src/data/schema/generated/encoding.ts +5 -0
- package/src/data/schema/generated/error.ts +6 -0
- package/src/data/schema/generated/format.ts +5 -0
- package/src/data/schema/generated/type-constructor.ts +10 -0
- package/src/data/schema/generated/type.ts +6 -0
- package/src/data/schema/index.ts +6 -0
- package/src/data/schema/json-schema.ts +33 -0
- package/src/data/schema/namespace.ts +55 -0
- package/src/data/schema/parse.ts +39 -0
- package/src/data/schema/types.ts +39 -0
- package/src/data/temporal/append-only.ts +134 -0
- package/src/data/temporal/close.ts +27 -0
- package/src/data/temporal/columns.ts +10 -0
- package/src/data/temporal/definition-queries.ts +70 -0
- package/src/data/temporal/errors.ts +10 -0
- package/src/data/temporal/get-current.ts +22 -0
- package/src/data/temporal/index.ts +9 -0
- package/src/data/temporal/namespace.ts +36 -0
- package/src/data/temporal/query.ts +25 -0
- package/src/data/temporal/resolve.ts +17 -0
- package/src/data/temporal/revise.ts +67 -0
- package/src/data/temporal/types.ts +22 -0
- package/src/data/valve/artifact.ts +33 -0
- package/src/data/valve/contract-diff.ts +21 -0
- package/src/data/valve/cross.ts +57 -0
- package/src/data/valve/crossing-ledger.ts +50 -0
- package/src/data/valve/decision.ts +44 -0
- package/src/data/valve/durable-crossing.ts +52 -0
- package/src/data/valve/execute-crossing.ts +77 -0
- package/src/data/valve/faucet.ts +56 -0
- package/src/data/valve/gate.ts +114 -0
- package/src/data/valve/graph.ts +77 -0
- package/src/data/valve/index.ts +53 -0
- package/src/data/valve/namespace.ts +57 -0
- package/src/data/valve/registry.ts +50 -0
- package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
- package/src/data/valve/schema/valve-crossing.ts +9 -0
- package/src/data/valve/supply.ts +154 -0
- package/src/data/valve/types.ts +143 -0
- package/src/data/vector/columns.ts +27 -0
- package/src/data/vector/index.ts +4 -0
- package/src/data/vector/namespace.ts +33 -0
- package/src/data/vector/query.ts +85 -0
- package/src/data/vector/validate.ts +16 -0
- package/src/data/write/audit/actor.ts +15 -0
- package/src/data/write/audit/audit-columns.ts +18 -0
- package/src/data/write/audit/audit-select.ts +111 -0
- package/src/data/write/audit/bound-transaction.ts +14 -0
- package/src/data/write/audit/cte-compile.ts +32 -0
- package/src/data/write/audit/decode-returning.ts +47 -0
- package/src/data/write/audit/final-select.ts +43 -0
- package/src/data/write/audit/infra-audit.ts +36 -0
- package/src/data/write/audit/primary-key.ts +65 -0
- package/src/data/write/audit/projection.ts +41 -0
- package/src/data/write/audit/read-touches.ts +86 -0
- package/src/data/write/audit/touch-match.ts +78 -0
- package/src/data/write/audit/verb-delete.ts +62 -0
- package/src/data/write/audit/verb-insert.ts +36 -0
- package/src/data/write/audit/verb-select.ts +47 -0
- package/src/data/write/audit/verb-update.ts +60 -0
- package/src/data/write/audit/verb-upsert.ts +104 -0
- package/src/data/write/conflict.ts +43 -0
- package/src/data/write/delete.ts +38 -0
- package/src/data/write/execute.ts +243 -0
- package/src/data/write/index.ts +11 -0
- package/src/data/write/insert.ts +63 -0
- package/src/data/write/state.ts +43 -0
- package/src/data/write/update.ts +54 -0
- package/src/data/write/upsert.ts +58 -0
- package/src/data/write/values.ts +19 -0
- package/src/index.ts +230 -0
- package/src/infra/access-links/code.ts +70 -0
- package/src/infra/access-links/handle.ts +892 -0
- package/src/infra/access-links/index.ts +9 -0
- package/src/infra/access-links/namespace.ts +30 -0
- package/src/infra/access-links/person-check.ts +177 -0
- package/src/infra/access-links/schema.ts +71 -0
- package/src/infra/alerts/async-sources.ts +231 -0
- package/src/infra/alerts/audit.ts +36 -0
- package/src/infra/alerts/beat.ts +123 -0
- package/src/infra/alerts/client-faults.ts +111 -0
- package/src/infra/alerts/faults.ts +156 -0
- package/src/infra/alerts/handle.ts +778 -0
- package/src/infra/alerts/http.ts +192 -0
- package/src/infra/alerts/index.ts +12 -0
- package/src/infra/alerts/infra-sources.ts +180 -0
- package/src/infra/alerts/namespace.ts +81 -0
- package/src/infra/alerts/redact.ts +21 -0
- package/src/infra/alerts/request-faults.ts +41 -0
- package/src/infra/alerts/schema.ts +124 -0
- package/src/infra/alerts/sinks.ts +125 -0
- package/src/infra/alerts/source-options.ts +26 -0
- package/src/infra/alerts/sources.ts +503 -0
- package/src/infra/alerts/subsystem-sources.ts +137 -0
- package/src/infra/audit/emit.ts +87 -0
- package/src/infra/audit/index.ts +2 -0
- package/src/infra/audit/namespace.ts +22 -0
- package/src/infra/audit/schema.sql +35 -0
- package/src/infra/audit/types.ts +18 -0
- package/src/infra/aws/credentials.ts +147 -0
- package/src/infra/aws/sigv4.ts +140 -0
- package/src/infra/cache/create.ts +58 -0
- package/src/infra/cache/entry.ts +81 -0
- package/src/infra/cache/index.ts +4 -0
- package/src/infra/cache/namespace.ts +10 -0
- package/src/infra/cache/tenant.ts +93 -0
- package/src/infra/cache/types.ts +24 -0
- package/src/infra/crawl/audit.ts +35 -0
- package/src/infra/crawl/errors.ts +25 -0
- package/src/infra/crawl/handle.ts +693 -0
- package/src/infra/crawl/index.ts +9 -0
- package/src/infra/crawl/links.ts +160 -0
- package/src/infra/crawl/namespace.ts +36 -0
- package/src/infra/crawl/robots.ts +100 -0
- package/src/infra/crawl/schema.ts +89 -0
- package/src/infra/credit-line/handle.ts +883 -0
- package/src/infra/credit-line/index.ts +6 -0
- package/src/infra/credit-line/namespace.ts +7 -0
- package/src/infra/credit-line/schema.ts +96 -0
- package/src/infra/credits/handle.ts +706 -0
- package/src/infra/credits/index.ts +5 -0
- package/src/infra/credits/namespace.ts +7 -0
- package/src/infra/credits/schema.ts +83 -0
- package/src/infra/email/audit.ts +36 -0
- package/src/infra/email/inbound.ts +622 -0
- package/src/infra/email/index.ts +11 -0
- package/src/infra/email/mail.ts +517 -0
- package/src/infra/email/mime.ts +447 -0
- package/src/infra/email/namespace.ts +22 -0
- package/src/infra/email/providers.ts +111 -0
- package/src/infra/email/schema.ts +96 -0
- package/src/infra/email/sns.ts +200 -0
- package/src/infra/email/unsubscribe.ts +44 -0
- package/src/infra/fetch/address.ts +88 -0
- package/src/infra/fetch/connect-retry.ts +87 -0
- package/src/infra/fetch/handle.ts +329 -0
- package/src/infra/fetch/index.ts +7 -0
- package/src/infra/fetch/namespace.ts +70 -0
- package/src/infra/idempotency/index.ts +3 -0
- package/src/infra/idempotency/middleware.ts +87 -0
- package/src/infra/idempotency/store.ts +92 -0
- package/src/infra/idempotency/types.ts +30 -0
- package/src/infra/join-requests/handle.ts +312 -0
- package/src/infra/join-requests/index.ts +2 -0
- package/src/infra/join-requests/namespace.ts +7 -0
- package/src/infra/join-requests/schema.ts +33 -0
- package/src/infra/outbox/core.ts +165 -0
- package/src/infra/quota/errors.ts +32 -0
- package/src/infra/quota/handle.ts +215 -0
- package/src/infra/quota/index.ts +5 -0
- package/src/infra/quota/namespace.ts +7 -0
- package/src/infra/quota/schema.ts +15 -0
- package/src/infra/storage/backend.ts +34 -0
- package/src/infra/storage/handle.ts +513 -0
- package/src/infra/storage/index.ts +9 -0
- package/src/infra/storage/local.ts +104 -0
- package/src/infra/storage/namespace.ts +13 -0
- package/src/infra/storage/s3.ts +257 -0
- package/src/infra/storage/schema.ts +24 -0
- package/src/infra/support/handle.ts +854 -0
- package/src/infra/support/http.ts +229 -0
- package/src/infra/support/index.ts +7 -0
- package/src/infra/support/namespace.ts +12 -0
- package/src/infra/support/schema.ts +96 -0
- package/src/infra/webhook/inbound.ts +45 -0
- package/src/infra/webhook/index.ts +5 -0
- package/src/infra/webhook/namespace.ts +24 -0
- package/src/infra/webhook/outbound.ts +315 -0
- package/src/infra/webhook/sign.ts +32 -0
- package/src/infra/webhook/verify.ts +82 -0
- package/src/invariants/assert.ts +26 -0
- package/src/invariants/index.ts +1 -0
- package/src/lint/config.ts +56 -0
- package/src/lint/doctrine.ts +163 -0
- package/src/lint/include.ts +97 -0
- package/src/lint/index.ts +3 -0
- package/src/lint/preset.ts +10 -0
- package/src/lint/resolver.ts +218 -0
- package/src/lint/rules/no-reexports.ts +46 -0
- package/src/request/behavior/audited.ts +85 -0
- package/src/request/behavior/cached.ts +62 -0
- package/src/request/behavior/compose.ts +28 -0
- package/src/request/behavior/index.ts +5 -0
- package/src/request/behavior/namespace.ts +16 -0
- package/src/request/behavior/types.ts +26 -0
- package/src/request/behavior/versioned.ts +54 -0
- package/src/request/context/create.ts +103 -0
- package/src/request/context/guard.ts +49 -0
- package/src/request/context/index.ts +4 -0
- package/src/request/context/run-in-transaction.ts +200 -0
- package/src/request/context/store.ts +28 -0
- package/src/request/context/types.ts +50 -0
- package/src/request/di/builder.ts +535 -0
- package/src/request/di/index.ts +8 -0
- package/src/request/di/resolve.ts +271 -0
- package/src/request/di/types.ts +262 -0
- package/src/request/fitting/namespace.ts +7 -0
- package/src/request/fixture/batch.ts +71 -0
- package/src/request/fixture/builder.ts +26 -0
- package/src/request/fixture/errors.ts +10 -0
- package/src/request/fixture/index.ts +8 -0
- package/src/request/fixture/namespace.ts +23 -0
- package/src/request/fixture/paginate.ts +44 -0
- package/src/request/fixture/to-handlers.ts +260 -0
- package/src/request/fixture/types.ts +79 -0
- package/src/request/flow/act-as.ts +78 -0
- package/src/request/flow/namespace.ts +17 -0
- package/src/request/http/error-step.ts +42 -0
- package/src/request/http/file.ts +141 -0
- package/src/request/http/index.ts +4 -0
- package/src/request/http/middleware.ts +141 -0
- package/src/request/http/namespace.ts +5 -0
- package/src/request/http/rate-limit-classes.ts +170 -0
- package/src/request/http/response.ts +82 -0
- package/src/request/http/server.ts +916 -0
- package/src/request/http/sniff.ts +261 -0
- package/src/request/http/types.ts +250 -0
- package/src/request/http/upload.ts +284 -0
- package/src/request/internal/text.ts +6 -0
- package/src/request/openapi/generate.ts +205 -0
- package/src/request/openapi/index.ts +2 -0
- package/src/request/openapi/namespace.ts +7 -0
- package/src/test/auth.ts +13 -0
- package/src/test/auto-setup.ts +45 -0
- package/src/test/context.ts +91 -0
- package/src/test/crawl-fixture.ts +157 -0
- package/src/test/discover-fork.ts +17 -0
- package/src/test/fingerprint.ts +135 -0
- package/src/test/globals.d.ts +7 -0
- package/src/test/index.ts +19 -0
- package/src/test/namespace.ts +87 -0
- package/src/test/plugin.ts +189 -0
- package/src/test/progress.ts +185 -0
- package/src/test/reap-after-run.ts +28 -0
- package/src/test/reap.ts +286 -0
- package/src/test/reset.ts +253 -0
- package/src/test/savepoints.ts +172 -0
- package/src/test/setup.ts +1422 -0
- package/src/test/system-channel.ts +50 -0
- package/src/test/vitest-plugin.ts +142 -0
- package/src/test/vitest-workspace.ts +507 -0
- package/src/test/vitest.ts +333 -0
- package/src/test/watch.ts +105 -0
- package/src/trace/capture/bounded-serializer.ts +146 -0
- package/src/trace/cardinality/analyze.ts +173 -0
- package/src/trace/cardinality/branch-context.ts +214 -0
- package/src/trace/cardinality/check.ts +33 -0
- package/src/trace/cardinality/classify-edge.ts +64 -0
- package/src/trace/cardinality/structural-max.ts +107 -0
- package/src/trace/check/coverage-check.ts +188 -0
- package/src/trace/check/flow-check.ts +103 -0
- package/src/trace/check/flow-step-site.ts +234 -0
- package/src/trace/check/io-pattern.ts +116 -0
- package/src/trace/codemod/add-cardinality.ts +152 -0
- package/src/trace/codemod/run.ts +80 -0
- package/src/trace/entry/child-trace.ts +21 -0
- package/src/trace/entry/close-trace.ts +87 -0
- package/src/trace/entry/finalize-trace.ts +28 -0
- package/src/trace/entry/open-trace.ts +138 -0
- package/src/trace/entry/parent-trace.ts +20 -0
- package/src/trace/entry/parse-traceparent.ts +39 -0
- package/src/trace/entry/process-start.ts +38 -0
- package/src/trace/entry/trace-context.ts +26 -0
- package/src/trace/entry/tracing-enabled.ts +10 -0
- package/src/trace/external/record.ts +195 -0
- package/src/trace/external/report.ts +66 -0
- package/src/trace/namespace.ts +19 -0
- package/src/trace/partition/day.ts +47 -0
- package/src/trace/partition/index.ts +14 -0
- package/src/trace/partition/partition-name.ts +23 -0
- package/src/trace/partition/plan.ts +82 -0
- package/src/trace/partition/schedule.ts +26 -0
- package/src/trace/partition/startup-hook.ts +39 -0
- package/src/trace/partition/sync.ts +154 -0
- package/src/trace/partition/tables.ts +53 -0
- package/src/trace/read/tree.ts +83 -0
- package/src/trace/retain/copy-on-error.ts +76 -0
- package/src/trace/runtime/expire-stale-traces.ts +54 -0
- package/src/trace/runtime/flow-step.ts +147 -0
- package/src/trace/runtime/flush-steps.ts +31 -0
- package/src/trace/runtime/pending-writes.ts +54 -0
- package/src/trace/runtime/step-buffer.ts +298 -0
- package/src/trace/runtime/step-error.ts +21 -0
- package/src/trace/runtime/step-meta.ts +55 -0
- package/src/trace/runtime/thenable.ts +12 -0
- package/src/trace/runtime/truncate-trace.ts +62 -0
- package/src/trace/schema/trace-fields.ts +52 -0
- package/src/trace/schema/trace-retained.ts +22 -0
- package/src/trace/schema/trace-step-fields.ts +44 -0
- package/src/trace/schema/trace-step-retained.ts +25 -0
- package/src/trace/schema/trace-step.ts +18 -0
- package/src/trace/schema/trace.ts +12 -0
- package/src/trace/transformer/apply.ts +79 -0
- package/src/trace/transformer/build-meta-literal.ts +30 -0
- package/src/trace/transformer/find-flow-step-sites.ts +85 -0
- package/src/trace/transformer/inject-flow-step-import.ts +39 -0
- package/src/trace/transformer/transform-error.ts +14 -0
- package/src/trace/transformer/transform-flow-steps.ts +123 -0
- package/src/trace/transformer/wrap-function-declaration.ts +62 -0
- package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
- package/src/workspace/affected.ts +110 -0
- package/src/workspace/changed-files.ts +163 -0
- package/src/workspace/check-deps.ts +213 -0
- package/src/workspace/checksum.ts +20 -0
- package/src/workspace/dependency-graph.ts +67 -0
- package/src/workspace/detect-pipework-copies.ts +93 -0
- package/src/workspace/discover.ts +206 -0
- package/src/workspace/generate.ts +401 -0
- package/src/workspace/index.ts +7 -0
- package/src/workspace/module-imports-pipework.ts +39 -0
- package/src/workspace/module-package-name.ts +12 -0
- package/src/workspace/scrub-git-env.ts +23 -0
- package/src/workspace/source-paths.ts +36 -0
- package/src/workspace/sync.ts +182 -0
- package/dist/REFERENCE.md +0 -1477
- package/dist/infra/access-links/rate-limit.d.ts +0 -10
- package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
- package/dist/infra/access-links/rate-limit.js +0 -28
- package/dist/infra/access-links/rate-limit.js.map +0 -1
- package/dist/infra/email/sigv4.d.ts +0 -24
- package/dist/infra/email/sigv4.d.ts.map +0 -1
- package/dist/infra/email/sigv4.js +0 -55
- package/dist/infra/email/sigv4.js.map +0 -1
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { Readable } from 'node:stream'
|
|
3
|
+
import Fastify, { type FastifyInstance, type FastifyRequest, type FastifyReply } from 'fastify'
|
|
4
|
+
import fastifyCors from '@fastify/cors'
|
|
5
|
+
import fastifyHelmet from '@fastify/helmet'
|
|
6
|
+
import fastifyRateLimit from '@fastify/rate-limit'
|
|
7
|
+
import fastifyMultipart from '@fastify/multipart'
|
|
8
|
+
import type { Manifold } from '../../core/pipework.js'
|
|
9
|
+
import type { Handler, HttpMethod } from '../di/types.js'
|
|
10
|
+
import type { CorsConfig, ServerConfig, RouteRegistration, HandlerResponse, InjectOptions, InjectResponse } from './types.js'
|
|
11
|
+
import { runInContext } from '../context/store.js'
|
|
12
|
+
import { createRequestContext } from '../context/index.js'
|
|
13
|
+
import { executeInTransaction } from '../context/run-in-transaction.js'
|
|
14
|
+
import { resolveAndExecute, parseRouteInputs, resolveHandlerDatabases, buildCheckpoint, type ResolveOptions } from '../di/resolve.js'
|
|
15
|
+
import { createRateLimitClasses } from './rate-limit-classes.js'
|
|
16
|
+
import { buildContext, type ContextOptions } from './middleware.js'
|
|
17
|
+
import { closeTrace } from '../../trace/entry/close-trace.js'
|
|
18
|
+
import { recordErrorStep } from './error-step.js'
|
|
19
|
+
import type { Flow } from '../context/index.js'
|
|
20
|
+
import type { ErrorResponse } from './types.js'
|
|
21
|
+
import { buildErrorResponse, buildInternalErrorResponse, buildClientErrorResponse, sendError } from './response.js'
|
|
22
|
+
import { isPipeworkError } from '../../core/errors/index.js'
|
|
23
|
+
import { checkUploadHeaders, uploadBodyLimit, startUpload, settleUpload, isUploadRefusal, newUploadTrack, type UploadTrack, MULTIPART_FIELD_LIMIT } from './upload.js'
|
|
24
|
+
import type { BinaryUpload, MultipartUpload, UploadConfig, UploadRefusal, RouteRefusal, RouteRefusedHook, HandlerMeta } from '../di/types.js'
|
|
25
|
+
import { __flowStep } from '../../trace/runtime/flow-step.js'
|
|
26
|
+
import { isFileResponse, fileResponseProblem, contentDisposition, primeStream, type FileResponse } from './file.js'
|
|
27
|
+
import type { FileConfig } from '../di/types.js'
|
|
28
|
+
import { toJsonSchema } from '../../data/schema/json-schema.js'
|
|
29
|
+
import { generateOpenAPI } from '../openapi/generate.js'
|
|
30
|
+
import { getBaseLogger, setLoggingConfig } from '../../core/logging/index.js'
|
|
31
|
+
import { settleTraceWrites } from '../../trace/runtime/pending-writes.js'
|
|
32
|
+
import { setupDrain } from '../../core/lifecycle/drain.js'
|
|
33
|
+
import { registerHealthRoutes } from '../../core/lifecycle/health.js'
|
|
34
|
+
import { validateSessionVarName } from '../../auth/tenant/index.js'
|
|
35
|
+
import type { Rbac } from '../../auth/rbac/types.js'
|
|
36
|
+
import { ForbiddenError } from '../../core/errors/index.js'
|
|
37
|
+
import { withTenantSetting } from '../../auth/tenant/in-tenant.js'
|
|
38
|
+
import type { Resource } from '../fixture/types.js'
|
|
39
|
+
import { resourceToHandlers } from '../fixture/to-handlers.js'
|
|
40
|
+
|
|
41
|
+
/** Fastify server wrapper with route registration, handler auto-wiring, and inject() for testing. */
|
|
42
|
+
export interface PipeworkServer {
|
|
43
|
+
route(method: HttpMethod, path: string, handler: Handler<Record<string, unknown>, unknown>): void
|
|
44
|
+
registerHandlers(handlers: Handler<Record<string, unknown>, unknown>[]): void
|
|
45
|
+
registerResource(resource: Resource<unknown>): void
|
|
46
|
+
ready(): Promise<void>
|
|
47
|
+
inject(opts: InjectOptions): Promise<InjectResponse>
|
|
48
|
+
listen(): Promise<string>
|
|
49
|
+
close(): Promise<void>
|
|
50
|
+
/**
|
|
51
|
+
* Stops the listener at once and drains in-flight requests, leaving the pools open. When `force` aborts, the
|
|
52
|
+
* remaining sockets are destroyed and this returns without waiting for their handlers.
|
|
53
|
+
*/
|
|
54
|
+
stop(force?: AbortSignal): Promise<void>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Creates a PipeworkServer — wraps Fastify with auto request IDs, CORS, rate limiting, and transaction-per-request. */
|
|
58
|
+
export function createServer(instance: Manifold, config: ServerConfig = {}): PipeworkServer {
|
|
59
|
+
if (instance.config.environment === 'production') {
|
|
60
|
+
validateProductionConfig(config)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const fastifyOpts: Record<string, unknown> = {
|
|
64
|
+
trustProxy: Array.isArray(config.trustProxy) ? [...config.trustProxy] : (config.trustProxy ?? false),
|
|
65
|
+
genReqId: () => randomUUID(),
|
|
66
|
+
requestIdHeader: false,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (config.bodyLimit !== undefined) {
|
|
70
|
+
fastifyOpts['bodyLimit'] = config.bodyLimit
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (config.logger === false) {
|
|
74
|
+
fastifyOpts['logger'] = false
|
|
75
|
+
} else {
|
|
76
|
+
fastifyOpts['loggerInstance'] = config.logger ?? getBaseLogger()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const app = Fastify(fastifyOpts)
|
|
80
|
+
|
|
81
|
+
// Fastify's own refusals (content type, bad JSON, body too large) answer in
|
|
82
|
+
// Pipework's error shape, never through a route's declared response schema.
|
|
83
|
+
// The Flow of a request that got one, for the internal-error hook.
|
|
84
|
+
const requestFlows = new WeakMap<object, Flow>()
|
|
85
|
+
const refusedRequests = new WeakSet<object>()
|
|
86
|
+
|
|
87
|
+
// Tells `onInternalError` about an error answered with a 5xx, in the request's
|
|
88
|
+
// Flow (no transaction is open: the request's has rolled back). Awaited; a
|
|
89
|
+
// failure is logged and the response is unchanged.
|
|
90
|
+
async function reportInternalError(req: FastifyRequest, error: unknown, handler: Handler<Record<string, unknown>, unknown> | null = null): Promise<void> {
|
|
91
|
+
const hook = config.onInternalError
|
|
92
|
+
if (hook === undefined) return
|
|
93
|
+
try {
|
|
94
|
+
const route = (req.routeOptions as { url?: string } | undefined)?.url ?? req.url.split('?')[0]!
|
|
95
|
+
const ctx = requestFlows.get(req) ?? createRequestContext({
|
|
96
|
+
instance,
|
|
97
|
+
requestId: String(req.id),
|
|
98
|
+
metadata: { method: req.method, path: req.url, startedAt: Date.now(), traceId: String(req.id), sessionId: null },
|
|
99
|
+
})
|
|
100
|
+
const inputs = parseRouteInputs(handler?.meta ?? null, req.query, req.params)
|
|
101
|
+
await runInContext(ctx, () => hook({
|
|
102
|
+
error,
|
|
103
|
+
params: inputs.params,
|
|
104
|
+
query: inputs.query,
|
|
105
|
+
method: req.method,
|
|
106
|
+
route,
|
|
107
|
+
requestId: String(req.id),
|
|
108
|
+
tenantId: ctx.tenant,
|
|
109
|
+
userId: ctx.user ?? ((ctx.auth as Record<string, unknown> | null)?.['userId'] ?? null) as string | null,
|
|
110
|
+
traceId: ctx.trace?.id ?? null,
|
|
111
|
+
}))
|
|
112
|
+
} catch (hookError) {
|
|
113
|
+
req.log.error(hookError, 'onInternalError failed')
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Runs a route's `onRefused` hook for a 4xx about to be sent, in the request's
|
|
118
|
+
// Flow (a minimal one where the request never got its own), as a trace step.
|
|
119
|
+
// What it returns is merged into `response.error` except pipework's own keys;
|
|
120
|
+
// a throw is logged and the response is left as it was.
|
|
121
|
+
async function runRefusedHook(
|
|
122
|
+
req: FastifyRequest,
|
|
123
|
+
meta: HandlerMeta | null,
|
|
124
|
+
ctx: Flow | undefined,
|
|
125
|
+
status: number,
|
|
126
|
+
response: ErrorResponse,
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const hook = meta?.onRefused
|
|
129
|
+
if (meta === null || hook === undefined || hook === null) return
|
|
130
|
+
if (refusedRequests.has(req)) return
|
|
131
|
+
refusedRequests.add(req)
|
|
132
|
+
try {
|
|
133
|
+
const flow = ctx ?? createRequestContext({
|
|
134
|
+
instance,
|
|
135
|
+
requestId: String(req.id),
|
|
136
|
+
metadata: { method: req.method, path: req.url, startedAt: Date.now(), traceId: String(req.id), sessionId: null },
|
|
137
|
+
})
|
|
138
|
+
const inputs = parseRouteInputs(meta, req.query, req.params)
|
|
139
|
+
const refusal: RouteRefusal = {
|
|
140
|
+
status,
|
|
141
|
+
code: response.error.code,
|
|
142
|
+
message: response.error.message,
|
|
143
|
+
fields: response.error.fields ?? null,
|
|
144
|
+
retryAfterSeconds: status === 429 ? response.error.retryAfterSeconds ?? null : null,
|
|
145
|
+
auth: ctx?.auth ?? null,
|
|
146
|
+
params: inputs.params,
|
|
147
|
+
query: inputs.query,
|
|
148
|
+
request: {
|
|
149
|
+
headers: req.headers as Record<string, string | undefined>,
|
|
150
|
+
ip: req.ip,
|
|
151
|
+
ips: [...(req.ips ?? [req.ip])],
|
|
152
|
+
},
|
|
153
|
+
requestId: String(req.id),
|
|
154
|
+
}
|
|
155
|
+
const extra: unknown = await runInContext(flow, () => refusedStep(hook, refusal))
|
|
156
|
+
if (typeof extra !== 'object' || extra === null || Array.isArray(extra)) return
|
|
157
|
+
const target = response.error as unknown as Record<string, unknown>
|
|
158
|
+
for (const [key, value] of Object.entries(extra as Record<string, unknown>)) {
|
|
159
|
+
if (REFUSAL_OWN_KEYS.has(key)) {
|
|
160
|
+
req.log.warn({ key, requestId: String(req.id) }, 'onRefused hook returned a key pipework owns; dropped')
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true })
|
|
164
|
+
}
|
|
165
|
+
} catch (hookError) {
|
|
166
|
+
req.log.error({ err: hookError, requestId: String(req.id) }, 'onRefused hook threw')
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
app.setErrorHandler(async (error, req, reply) => {
|
|
171
|
+
const status = (error as { statusCode?: unknown }).statusCode
|
|
172
|
+
const open = requestFlows.get(req)
|
|
173
|
+
const routeOf = (): string => (req.routeOptions as { url?: string } | undefined)?.url ?? req.url.split('?')[0]!
|
|
174
|
+
const record = async (code: string, httpStatus: number, refused?: () => Promise<void>): Promise<void> => {
|
|
175
|
+
if (open === undefined) {
|
|
176
|
+
await refused?.()
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
recordErrorStep(open, { method: req.method, route: routeOf(), status: httpStatus, code, error })
|
|
180
|
+
try {
|
|
181
|
+
await refused?.()
|
|
182
|
+
} finally {
|
|
183
|
+
void closeTrace(instance, open.trace, 'error')
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (typeof status === 'number' && status >= 400 && status < 500) {
|
|
187
|
+
const response = buildClientErrorResponse(status, error, reply.getHeader('retry-after'))
|
|
188
|
+
const owner = (req.routeOptions as { config?: { pipeworkHandler?: Handler<Record<string, unknown>, unknown> } } | undefined)?.config?.pipeworkHandler
|
|
189
|
+
await record(response.error.code, status, () => runRefusedHook(req, owner?.meta ?? null, open, status, response))
|
|
190
|
+
return sendError(reply, status, response)
|
|
191
|
+
}
|
|
192
|
+
req.log.error(error, 'Unhandled error')
|
|
193
|
+
await record('INTERNAL_ERROR', 500)
|
|
194
|
+
await reportInternalError(req, error)
|
|
195
|
+
return sendError(reply, 500, buildInternalErrorResponse(error, config.exposeErrorDetails))
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// Validation is handled by Pipework's DI layer (parse + ValidationError).
|
|
199
|
+
// Pass-through compiler lets us attach JSON Schema for OpenAPI without AJV double-validating.
|
|
200
|
+
app.setValidatorCompiler(() => (data) => ({ value: data }))
|
|
201
|
+
|
|
202
|
+
// CORS is closed unless granted: a route's own `.cors()` replaces the server's
|
|
203
|
+
// `cors` for that route, and a route with neither grants nothing. The plugin
|
|
204
|
+
// loads only if some grant exists, which is known once routes are registered.
|
|
205
|
+
// A preflight names no route of its own, so each path that has a grant gets
|
|
206
|
+
// one OPTIONS route carrying the grant per method; Fastify does the matching.
|
|
207
|
+
const corsPaths = new Map<string, { byMethod: Record<string, CorsConfig | false> }>()
|
|
208
|
+
let grantCount = 0
|
|
209
|
+
const serverGrant: CorsConfig | null = config.cors !== undefined && config.cors !== false ? config.cors : null
|
|
210
|
+
const corsPlugin = async (scope: FastifyInstance): Promise<void> => {
|
|
211
|
+
if (serverGrant === null && grantCount === 0) return
|
|
212
|
+
await scope.register(fastifyCors, {
|
|
213
|
+
delegator: (req: FastifyRequest, cb: (err: Error | null, options?: Record<string, unknown>) => void) => {
|
|
214
|
+
let grant: CorsConfig | null
|
|
215
|
+
const requested = req.headers['access-control-request-method']
|
|
216
|
+
const routeConfig = req.routeOptions.config as { cors?: CorsConfig | false; corsByMethod?: Record<string, CorsConfig | false> } | undefined
|
|
217
|
+
if (req.method === 'OPTIONS' && typeof requested === 'string') {
|
|
218
|
+
const g = routeConfig?.corsByMethod?.[requested.toUpperCase()]
|
|
219
|
+
grant = g === undefined || g === false ? null : g
|
|
220
|
+
} else {
|
|
221
|
+
const own = routeConfig?.cors
|
|
222
|
+
grant = own === undefined ? serverGrant : own === false ? null : own
|
|
223
|
+
}
|
|
224
|
+
cb(null, grant === null ? { origin: false } : (grant as Record<string, unknown>))
|
|
225
|
+
},
|
|
226
|
+
} as never)
|
|
227
|
+
for (const [path, entry] of corsPaths) {
|
|
228
|
+
if (!Object.values(entry.byMethod).some((g) => g !== false)) continue
|
|
229
|
+
scope.options(path, { config: { corsByMethod: entry.byMethod } }, (_req, reply) => reply.status(204).send())
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
;(corsPlugin as unknown as Record<symbol, boolean>)[Symbol.for('skip-override')] = true
|
|
233
|
+
void app.register(corsPlugin)
|
|
234
|
+
|
|
235
|
+
if (config.security !== false) {
|
|
236
|
+
void app.register(fastifyHelmet, (config.security ?? {}) as Record<string, unknown>)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (config.rateLimit !== false) {
|
|
240
|
+
void app.register(fastifyRateLimit, (config.rateLimit ?? {}) as Record<string, unknown>)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (config.logging) {
|
|
244
|
+
setLoggingConfig(config.logging)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
app.decorateRequest('rawBody', undefined)
|
|
248
|
+
app.decorateRequest('pipeworkTraceId', null)
|
|
249
|
+
|
|
250
|
+
app.addHook('onSend', (_req, reply, payload, done) => {
|
|
251
|
+
void reply.header('x-request-id', _req.id)
|
|
252
|
+
const traceId = (_req as FastifyRequest & { pipeworkTraceId: string | null }).pipeworkTraceId
|
|
253
|
+
if (traceId !== null) void reply.header('x-pipework-trace-id', traceId)
|
|
254
|
+
done(null, payload)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const routes: RouteRegistration[] = []
|
|
258
|
+
|
|
259
|
+
const authConfig = config.auth
|
|
260
|
+
const strategies = authConfig
|
|
261
|
+
? ('strategies' in authConfig ? authConfig.strategies : [authConfig.strategy])
|
|
262
|
+
: []
|
|
263
|
+
const enrichers = authConfig && 'enrichers' in authConfig ? (authConfig.enrichers ?? []) : []
|
|
264
|
+
const tenantResolver = authConfig && 'tenantResolver' in authConfig ? authConfig.tenantResolver : undefined
|
|
265
|
+
|
|
266
|
+
const tenantConfig = config.tenant ?? null
|
|
267
|
+
if (tenantConfig?.sessionVars) {
|
|
268
|
+
for (const name of Object.keys(tenantConfig.sessionVars)) {
|
|
269
|
+
validateSessionVarName(name)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const contextOptions: ContextOptions = {
|
|
274
|
+
instance,
|
|
275
|
+
strategies,
|
|
276
|
+
enrichers,
|
|
277
|
+
tenantResolver,
|
|
278
|
+
tenantConfig,
|
|
279
|
+
authOptional: authConfig?.optional ?? false,
|
|
280
|
+
logging: config.logging ?? null,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const gracefulShutdownMs = config.gracefulShutdownMs ?? 10000
|
|
284
|
+
const drain = setupDrain(app)
|
|
285
|
+
|
|
286
|
+
if (config.health !== false) {
|
|
287
|
+
// Registered on the app itself: probes are never rate limited.
|
|
288
|
+
registerHealthRoutes(app, instance, config.healthChecks ?? [], {
|
|
289
|
+
health: config.healthPath ?? '/health',
|
|
290
|
+
ready: config.readyPath ?? '/ready',
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const rbacInstance = config.rbac ?? null
|
|
295
|
+
const rateLimitClasses = config.rateLimits === undefined ? null : createRateLimitClasses(instance, config.rateLimits)
|
|
296
|
+
|
|
297
|
+
const txDatabases: readonly string[] | null = config.transactionPerRequest !== undefined && config.transactionPerRequest !== false
|
|
298
|
+
? (config.transactionPerRequest === true
|
|
299
|
+
? [...instance.config.databases().keys()]
|
|
300
|
+
: config.transactionPerRequest)
|
|
301
|
+
: null
|
|
302
|
+
const txTimeoutMs = config.transactionTimeoutMs ?? 5000
|
|
303
|
+
|
|
304
|
+
if (tenantConfig?.sessionVars !== undefined && txDatabases === null) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
'[pipework] Tenant session variables require transactionPerRequest.\n\n' +
|
|
307
|
+
' Your server config has tenant.sessionVars configured, but transactionPerRequest\n' +
|
|
308
|
+
' is not enabled. Session variables are propagated via SET LOCAL inside a\n' +
|
|
309
|
+
' transaction — without transactionPerRequest, they are silently skipped.\n\n' +
|
|
310
|
+
' Fix: Add transactionPerRequest: true (or a list of database names) to your\n' +
|
|
311
|
+
' createServer() config.\n',
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function route(
|
|
316
|
+
method: HttpMethod,
|
|
317
|
+
path: string,
|
|
318
|
+
handler: Handler<Record<string, unknown>, unknown>,
|
|
319
|
+
): void {
|
|
320
|
+
routes.push({ method, path, handler })
|
|
321
|
+
rateLimitClasses?.checkRoute(method, path, handler.meta)
|
|
322
|
+
|
|
323
|
+
const fastifyMethod = method.toLowerCase() as 'get' | 'post' | 'put' | 'patch' | 'delete'
|
|
324
|
+
|
|
325
|
+
const schema: Record<string, unknown> = {}
|
|
326
|
+
if (handler.meta.inputSchema) {
|
|
327
|
+
schema['body'] = toJsonSchema(handler.meta.inputSchema)
|
|
328
|
+
}
|
|
329
|
+
if (handler.meta.outputSchemas.size > 0) {
|
|
330
|
+
const response: Record<string, unknown> = {}
|
|
331
|
+
for (const [status, outputSchema] of handler.meta.outputSchemas) {
|
|
332
|
+
response[String(status)] = toJsonSchema(outputSchema)
|
|
333
|
+
}
|
|
334
|
+
schema['response'] = response
|
|
335
|
+
}
|
|
336
|
+
if (handler.meta.querySchema) {
|
|
337
|
+
schema['querystring'] = toJsonSchema(handler.meta.querySchema)
|
|
338
|
+
}
|
|
339
|
+
if (handler.meta.paramsSchema) {
|
|
340
|
+
schema['params'] = toJsonSchema(handler.meta.paramsSchema)
|
|
341
|
+
}
|
|
342
|
+
const opts: Record<string, unknown> = Object.keys(schema).length > 0 ? { schema } : {}
|
|
343
|
+
|
|
344
|
+
const ownCors = handler.meta.cors ?? undefined
|
|
345
|
+
const entry = corsPaths.get(path) ?? { byMethod: {} }
|
|
346
|
+
const effective = ownCors === undefined ? (serverGrant ?? false) : ownCors
|
|
347
|
+
entry.byMethod[method] = effective
|
|
348
|
+
corsPaths.set(path, entry)
|
|
349
|
+
if (ownCors !== undefined) {
|
|
350
|
+
grantCount++
|
|
351
|
+
opts['config'] = { ...(opts['config'] as object | undefined), cors: ownCors }
|
|
352
|
+
if (instance.config.environment === 'production') {
|
|
353
|
+
const problem = credentialedWildcard(ownCors)
|
|
354
|
+
if (problem !== null) throw new Error(`[pipework] ${method} ${path}: ${problem}`)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (handler.meta.onRefused) {
|
|
359
|
+
opts['config'] = { ...(opts['config'] as object | undefined), pipeworkHandler: handler }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const upload = handler.meta.upload ?? null
|
|
363
|
+
if (upload !== null) {
|
|
364
|
+
opts['bodyLimit'] = uploadBodyLimit(upload)
|
|
365
|
+
} else if (handler.meta.route?.bodyLimit !== undefined) {
|
|
366
|
+
opts['bodyLimit'] = handler.meta.route.bodyLimit
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const screen = handler.meta.route?.screen
|
|
370
|
+
if (screen !== undefined) {
|
|
371
|
+
// Runs before the body is parsed, so a route can refuse on headers alone
|
|
372
|
+
// (its own status + named reason) before the body limit turns it into 413.
|
|
373
|
+
opts['onRequest'] = async (req: FastifyRequest, reply: FastifyReply) => {
|
|
374
|
+
const raw = req.headers['content-length']
|
|
375
|
+
const declared = raw === undefined || raw === '' ? NaN : Number(raw)
|
|
376
|
+
let refusal
|
|
377
|
+
try {
|
|
378
|
+
// The screen runs inside its own minimal request context so `pipe.system()`
|
|
379
|
+
// works. No auth chain and no transaction: the screen is pre-parse and
|
|
380
|
+
// pre-auth, and its reads are committed-only, outside request atomicity.
|
|
381
|
+
const screenCtx = createRequestContext({
|
|
382
|
+
instance,
|
|
383
|
+
requestId: String(req.id),
|
|
384
|
+
metadata: { method: req.method, path: req.url, startedAt: Date.now(), traceId: String(req.id), sessionId: null },
|
|
385
|
+
})
|
|
386
|
+
refusal = await runInContext(screenCtx, () => screen({
|
|
387
|
+
method: req.method,
|
|
388
|
+
url: req.url,
|
|
389
|
+
headers: req.headers as Record<string, string | undefined>,
|
|
390
|
+
contentLength: Number.isFinite(declared) ? declared : null,
|
|
391
|
+
params: parseRouteInputs(handler.meta, req.query, req.params).params,
|
|
392
|
+
}))
|
|
393
|
+
} catch (error) {
|
|
394
|
+
req.log.error(error, 'Route screen threw')
|
|
395
|
+
await reportInternalError(req, error, handler)
|
|
396
|
+
return sendError(reply, 500, {
|
|
397
|
+
error: 'ScreenFailed',
|
|
398
|
+
message:
|
|
399
|
+
`[pipework] The pre-parse screen for ${method} ${path} threw: ${error instanceof Error ? error.message : String(error)}\n\n` +
|
|
400
|
+
' A screen must inspect headers and either return a refusal\n' +
|
|
401
|
+
' ({ status, reason }) or return nothing. Fix the screen, or catch\n' +
|
|
402
|
+
' inside it and return the refusal you want the caller to see.\n',
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
if (refusal !== undefined && refusal !== null) {
|
|
406
|
+
return sendError(reply, refusal.status, { ...refusal.body, reason: refusal.reason })
|
|
407
|
+
}
|
|
408
|
+
return undefined
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (upload !== null && upload.kind === 'multipart') {
|
|
413
|
+
// After the screen: a body that is not multipart/form-data is a protocol
|
|
414
|
+
// error, refused from the headers before anything is read.
|
|
415
|
+
const headerCheck = async (req: FastifyRequest, reply: FastifyReply) => {
|
|
416
|
+
const refusal = checkUploadHeaders(upload, req.headers)
|
|
417
|
+
if (refusal === null) return undefined
|
|
418
|
+
void reply.header('connection', 'close')
|
|
419
|
+
const response = buildErrorResponse(refusal)
|
|
420
|
+
await runRefusedHook(req, handler.meta, undefined, refusal.httpStatus, response)
|
|
421
|
+
return sendError(reply, refusal.httpStatus, response)
|
|
422
|
+
}
|
|
423
|
+
const existing = opts['onRequest'] as ((req: FastifyRequest, reply: FastifyReply) => Promise<unknown>) | undefined
|
|
424
|
+
opts['onRequest'] = existing === undefined ? [headerCheck] : [existing, headerCheck]
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (handler.meta.wantsRawBody) {
|
|
428
|
+
opts['preParsing'] = async (req: FastifyRequest, _reply: FastifyReply, payload: AsyncIterable<Buffer>) => {
|
|
429
|
+
const chunks: Buffer[] = []
|
|
430
|
+
for await (const chunk of payload) chunks.push(chunk)
|
|
431
|
+
const raw = Buffer.concat(chunks)
|
|
432
|
+
;(req as FastifyRequest & { rawBody: Buffer }).rawBody = raw
|
|
433
|
+
return Readable.from(raw)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const routeHandler = async (req: FastifyRequest, reply: FastifyReply) => {
|
|
438
|
+
let ctx: Flow | undefined
|
|
439
|
+
let uploaded: BinaryUpload | MultipartUpload | undefined
|
|
440
|
+
const track = newUploadTrack()
|
|
441
|
+
// Ends the upload stream and closes the connection if the body was left unread.
|
|
442
|
+
const finishUpload = (failed: boolean): void => {
|
|
443
|
+
if (settleUpload(uploaded?.stream) || (failed && upload !== null)) void reply.header('connection', 'close')
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
ctx = await buildContext(req, contextOptions, handler.meta.authRequirement)
|
|
447
|
+
;(req as FastifyRequest & { pipeworkTraceId: string | null }).pipeworkTraceId = ctx.trace?.id ?? null
|
|
448
|
+
requestFlows.set(req, ctx)
|
|
449
|
+
|
|
450
|
+
if (rateLimitClasses !== null) await rateLimitClasses.check(req, ctx, handler.meta, { method, url: path })
|
|
451
|
+
|
|
452
|
+
if (handler.meta.permission && rbacInstance) {
|
|
453
|
+
await enforceHandlerPermission(instance, ctx, handler.meta, rbacInstance)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const requestObj: import('./types.js').HttpRequest = {
|
|
457
|
+
headers: req.headers as Record<string, string | undefined>,
|
|
458
|
+
ip: req.ip,
|
|
459
|
+
ips: [...(req.ips ?? [req.ip])],
|
|
460
|
+
}
|
|
461
|
+
const rawBodyBuffer = handler.meta.wantsRawBody
|
|
462
|
+
? (req as FastifyRequest & { rawBody: Buffer }).rawBody
|
|
463
|
+
: undefined
|
|
464
|
+
if (rawBodyBuffer !== undefined) {
|
|
465
|
+
(requestObj as { rawBody: Buffer }).rawBody = rawBodyBuffer
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (upload !== null) {
|
|
469
|
+
uploaded = await startUpload(req, upload, track)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const httpOptions: ResolveOptions = {
|
|
473
|
+
request: requestObj,
|
|
474
|
+
response: {
|
|
475
|
+
header: (name: string, value: string | readonly string[]) => { void reply.header(name, typeof value === 'string' ? value : [...value]) },
|
|
476
|
+
onFinish: (cb) => watchResponse(reply, cb, req.log),
|
|
477
|
+
},
|
|
478
|
+
...(rawBodyBuffer !== undefined ? { rawBody: rawBodyBuffer } : {}),
|
|
479
|
+
...(uploaded !== undefined ? { upload: uploaded } : {}),
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Fastify hands a missing body over as null; the input sees undefined.
|
|
483
|
+
// A body that is literally JSON null names a content-type and stays null.
|
|
484
|
+
const body = req.body === null && req.headers['content-type'] === undefined ? undefined : req.body
|
|
485
|
+
|
|
486
|
+
let result: unknown
|
|
487
|
+
if (handler.meta.jobTransaction === 'manual') {
|
|
488
|
+
// The route owns its commit boundaries: no request-wide transaction
|
|
489
|
+
// is opened, and every checkpoint commits on its own. Between them
|
|
490
|
+
// the handler holds no connection, which is the point — a lease can
|
|
491
|
+
// be written and committed, a network call made, and the outcome
|
|
492
|
+
// written again, all under the request's own tenant with RLS
|
|
493
|
+
// enforced. What it costs is stated on `.transaction('manual')`.
|
|
494
|
+
const checkpoint = buildCheckpoint(
|
|
495
|
+
handler,
|
|
496
|
+
ctx,
|
|
497
|
+
resolveHandlerDatabases(handler, instance),
|
|
498
|
+
tenantConfig,
|
|
499
|
+
txTimeoutMs,
|
|
500
|
+
)
|
|
501
|
+
result = await runInContext(ctx, () =>
|
|
502
|
+
resolveAndExecute(handler, body, req.query, req.params, { ...httpOptions, checkpoint }),
|
|
503
|
+
)
|
|
504
|
+
} else if (txDatabases !== null && txDatabases.length > 0) {
|
|
505
|
+
result = await executeInTransaction(
|
|
506
|
+
ctx,
|
|
507
|
+
txDatabases,
|
|
508
|
+
tenantConfig,
|
|
509
|
+
txTimeoutMs,
|
|
510
|
+
() => resolveAndExecute(handler, body, req.query, req.params, httpOptions),
|
|
511
|
+
)
|
|
512
|
+
} else {
|
|
513
|
+
result = await runInContext(ctx, () => resolveAndExecute(handler, body, req.query, req.params, httpOptions))
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
finishUpload(false)
|
|
517
|
+
void closeTrace(instance, ctx.trace, 'closed')
|
|
518
|
+
return await sendResult(reply, req, result, handler.meta.file ?? null, `${method} ${path}`)
|
|
519
|
+
} catch (error) {
|
|
520
|
+
finishUpload(true)
|
|
521
|
+
// Records the error step, runs `refused` (the route's onRefused hook) while the
|
|
522
|
+
// trace is still open, then closes the trace.
|
|
523
|
+
const failWith = async (status: number, code: string, refused?: () => Promise<void>): Promise<void> => {
|
|
524
|
+
recordErrorStep(ctx, { method, route: path, status, code, error })
|
|
525
|
+
try {
|
|
526
|
+
await refused?.()
|
|
527
|
+
} finally {
|
|
528
|
+
if (ctx !== undefined) void closeTrace(instance, ctx.trace, 'error')
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (error instanceof FileResponseError) {
|
|
533
|
+
await failWith(500, 'FILE_RESPONSE_INVALID')
|
|
534
|
+
req.log.error({ route: `${method} ${path}`, requestId: String(req.id), err: error }, 'File response refused')
|
|
535
|
+
await reportInternalError(req, error, handler)
|
|
536
|
+
return sendError(reply, 500, { error: { code: 'FILE_RESPONSE_INVALID', message: error.message } })
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (isPipeworkError(error)) {
|
|
540
|
+
const response = buildErrorResponse(error)
|
|
541
|
+
const isClient = error.httpStatus >= 400 && error.httpStatus < 500
|
|
542
|
+
await failWith(error.httpStatus, error.code, async () => {
|
|
543
|
+
if (upload !== null && ctx !== undefined && isUploadRefusal(error)) {
|
|
544
|
+
const message = await notifyRefusal(req, ctx, handler, upload, track, error)
|
|
545
|
+
if (message !== undefined) response.error.message = message
|
|
546
|
+
}
|
|
547
|
+
if (isClient) await runRefusedHook(req, handler.meta, ctx, error.httpStatus, response)
|
|
548
|
+
})
|
|
549
|
+
const retryAfter = response.error.retryAfterSeconds
|
|
550
|
+
if (retryAfter !== undefined) void reply.header('Retry-After', String(retryAfter))
|
|
551
|
+
if (error.httpStatus >= 500) await reportInternalError(req, error, handler)
|
|
552
|
+
return sendError(reply, error.httpStatus, response)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// A malformed multipart body surfaces as a Fastify client error (4xx).
|
|
556
|
+
const clientStatus = upload !== null ? (error as { statusCode?: unknown } | null)?.statusCode : undefined
|
|
557
|
+
if (typeof clientStatus === 'number' && clientStatus >= 400 && clientStatus < 500) {
|
|
558
|
+
const response = buildClientErrorResponse(clientStatus, error)
|
|
559
|
+
await failWith(clientStatus, response.error.code, () => runRefusedHook(req, handler.meta, ctx, clientStatus, response))
|
|
560
|
+
return sendError(reply, clientStatus, response)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
await failWith(500, 'INTERNAL_ERROR')
|
|
564
|
+
req.log.error(error, 'Unhandled error in route handler')
|
|
565
|
+
await reportInternalError(req, error, handler)
|
|
566
|
+
return sendError(reply, 500, buildInternalErrorResponse(error, config.exposeErrorDetails))
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Its own plugin scope, so the route loads after @fastify/rate-limit and
|
|
571
|
+
// gets its limit (a route added to `app` directly would be registered
|
|
572
|
+
// before the plugin's onRoute hook exists). An upload's body parsers
|
|
573
|
+
// replace the JSON parser for this route only, so no other route's
|
|
574
|
+
// parsing changes.
|
|
575
|
+
void app.register(async (scope) => {
|
|
576
|
+
if (upload !== null) {
|
|
577
|
+
scope.removeAllContentTypeParsers()
|
|
578
|
+
if (upload.kind === 'binary') {
|
|
579
|
+
scope.addContentTypeParser('*', (_req, payload, done) => { done(null, payload) })
|
|
580
|
+
} else {
|
|
581
|
+
await scope.register(fastifyMultipart, {
|
|
582
|
+
throwFileSizeLimit: false,
|
|
583
|
+
limits: { fileSize: upload.maxBytes + 1024, fieldSize: MULTIPART_FIELD_LIMIT, files: 1, fields: 100 },
|
|
584
|
+
})
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (handler.meta.ignoresBody === true && upload === null) {
|
|
588
|
+
scope.removeAllContentTypeParsers()
|
|
589
|
+
scope.addContentTypeParser('*', (req, payload, done) => {
|
|
590
|
+
const declared = Number(req.headers['content-length'])
|
|
591
|
+
if (Number.isFinite(declared) && declared > IGNORED_BODY_LIMIT) {
|
|
592
|
+
done(Object.assign(new Error('Request body is too large'), { statusCode: 413 }))
|
|
593
|
+
return
|
|
594
|
+
}
|
|
595
|
+
payload.resume()
|
|
596
|
+
done(null, undefined)
|
|
597
|
+
})
|
|
598
|
+
}
|
|
599
|
+
scope[fastifyMethod](path, opts, routeHandler)
|
|
600
|
+
})
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Tells the route's `onRefuse` hook about a file pipework refused, in the
|
|
604
|
+
// request's context. Returns the hook's replacement message, if it gave one.
|
|
605
|
+
async function notifyRefusal(
|
|
606
|
+
req: FastifyRequest,
|
|
607
|
+
ctx: Flow,
|
|
608
|
+
handler: Handler<Record<string, unknown>, unknown>,
|
|
609
|
+
upload: UploadConfig,
|
|
610
|
+
track: UploadTrack,
|
|
611
|
+
error: { httpStatus: number; code: string },
|
|
612
|
+
): Promise<string | undefined> {
|
|
613
|
+
if (upload.onRefuse === undefined) return undefined
|
|
614
|
+
const declaredLength = Number(req.headers['content-length'])
|
|
615
|
+
const inputs = parseRouteInputs(handler.meta, req.query, req.params)
|
|
616
|
+
const refusal: UploadRefusal = {
|
|
617
|
+
status: error.httpStatus as 413 | 415,
|
|
618
|
+
code: error.code,
|
|
619
|
+
detectedType: track.detectedType,
|
|
620
|
+
declaredType: track.declaredType,
|
|
621
|
+
extension: track.extension,
|
|
622
|
+
filename: track.filename,
|
|
623
|
+
declaredLength: req.headers['content-length'] !== undefined && Number.isFinite(declaredLength) ? declaredLength : null,
|
|
624
|
+
bytesRead: track.bytesRead,
|
|
625
|
+
accepted: upload.mediaTypes,
|
|
626
|
+
auth: ctx.auth,
|
|
627
|
+
requestId: ctx.requestId,
|
|
628
|
+
params: inputs.params,
|
|
629
|
+
query: inputs.query,
|
|
630
|
+
}
|
|
631
|
+
const hook = upload.onRefuse
|
|
632
|
+
try {
|
|
633
|
+
const message = await runInContext(ctx, async () => hook(refusal))
|
|
634
|
+
return typeof message === 'string' && message !== '' ? message : undefined
|
|
635
|
+
} catch (hookError) {
|
|
636
|
+
req.log.error({ err: hookError, requestId: ctx.requestId }, 'Upload onRefuse hook threw')
|
|
637
|
+
return undefined
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function registerHandlers(handlers: Handler<Record<string, unknown>, unknown>[]): void {
|
|
642
|
+
for (const handler of handlers) {
|
|
643
|
+
if (handler.meta.route) {
|
|
644
|
+
route(handler.meta.route.method, handler.meta.route.path, handler)
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function registerResourceFn(resource: Resource<unknown>): void {
|
|
650
|
+
registerHandlers(resourceToHandlers(resource))
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (config.openapi) {
|
|
654
|
+
const openApiPath = config.openApiPath ?? '/openapi.json'
|
|
655
|
+
const openApiInfo = config.openapi
|
|
656
|
+
// Registered on the app itself: the document is not rate limited.
|
|
657
|
+
app.get(openApiPath, async (_req, reply) => {
|
|
658
|
+
return reply.send(generateOpenAPI(openApiInfo, routes, { rateLimited: config.rateLimit !== false || config.rateLimits !== undefined }))
|
|
659
|
+
})
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function listen(): Promise<string> {
|
|
663
|
+
const address = await app.listen({
|
|
664
|
+
port: config.port ?? 3000,
|
|
665
|
+
host: config.host ?? '0.0.0.0',
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
if (config.managed !== true) {
|
|
669
|
+
setupGracefulShutdown(app, instance, drain, gracefulShutdownMs)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return address
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function close(): Promise<void> {
|
|
676
|
+
await app.close()
|
|
677
|
+
await settleTraceWrites()
|
|
678
|
+
await instance.pool.closeAll()
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function stop(force?: AbortSignal): Promise<void> {
|
|
682
|
+
const node = app.server
|
|
683
|
+
const destroy = (): void => { node.closeAllConnections() }
|
|
684
|
+
if (node.listening) node.close()
|
|
685
|
+
if (force?.aborted === true) destroy()
|
|
686
|
+
else force?.addEventListener('abort', destroy, { once: true })
|
|
687
|
+
try {
|
|
688
|
+
await app.close()
|
|
689
|
+
} finally {
|
|
690
|
+
force?.removeEventListener('abort', destroy)
|
|
691
|
+
}
|
|
692
|
+
if (force?.aborted !== true) await settleTraceWrites()
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function ready(): Promise<void> {
|
|
696
|
+
await app.ready()
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function inject(opts: InjectOptions): Promise<InjectResponse> {
|
|
700
|
+
const response = await app.inject({
|
|
701
|
+
method: opts.method as 'GET',
|
|
702
|
+
url: opts.url,
|
|
703
|
+
headers: opts.headers as Record<string, string>,
|
|
704
|
+
payload: opts.payload as string,
|
|
705
|
+
query: opts.query as Record<string, string>,
|
|
706
|
+
...(opts.remoteAddress !== undefined ? { remoteAddress: opts.remoteAddress } : {}),
|
|
707
|
+
})
|
|
708
|
+
const r = response as unknown as { statusCode: number; headers: Record<string, string | number | string[] | undefined>; body: string; rawPayload: Buffer; json(): unknown }
|
|
709
|
+
return { statusCode: r.statusCode, headers: r.headers, body: r.body, rawPayload: r.rawPayload, json: () => r.json() }
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return { route, registerHandlers, registerResource: registerResourceFn, ready, inject, listen, close, stop }
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const REFUSAL_OWN_KEYS: ReadonlySet<string> = new Set(['code', 'message', 'fields', 'details', 'retryAfterSeconds'])
|
|
716
|
+
|
|
717
|
+
const refusedStep = __flowStep(
|
|
718
|
+
{ ref: 'pipework/http:on-refused' },
|
|
719
|
+
async (hook: RouteRefusedHook, refusal: RouteRefusal) => hook(refusal),
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
function setupGracefulShutdown(
|
|
723
|
+
app: FastifyInstance,
|
|
724
|
+
instance: Manifold,
|
|
725
|
+
drain: ReturnType<typeof setupDrain>,
|
|
726
|
+
timeoutMs: number,
|
|
727
|
+
): void {
|
|
728
|
+
const shutdown = async (signal: string) => {
|
|
729
|
+
app.log.info({ signal }, 'Graceful shutdown initiated')
|
|
730
|
+
|
|
731
|
+
const timer = setTimeout(() => {
|
|
732
|
+
process.exit(1)
|
|
733
|
+
}, timeoutMs)
|
|
734
|
+
|
|
735
|
+
let exitCode = 0
|
|
736
|
+
try {
|
|
737
|
+
await drain.waitForDrain(timeoutMs - 1000)
|
|
738
|
+
await app.close()
|
|
739
|
+
await settleTraceWrites()
|
|
740
|
+
await instance.pool.closeAll()
|
|
741
|
+
} catch {
|
|
742
|
+
exitCode = 1
|
|
743
|
+
} finally {
|
|
744
|
+
clearTimeout(timer)
|
|
745
|
+
process.exit(exitCode)
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
process.once('SIGTERM', () => { void shutdown('SIGTERM') })
|
|
750
|
+
process.once('SIGINT', () => { void shutdown('SIGINT') })
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Runs `cb` once with how the response ended. `finish` fires when the last byte
|
|
754
|
+
// is handed to the socket; `close` without `finish` means the connection died first.
|
|
755
|
+
function watchResponse(
|
|
756
|
+
reply: FastifyReply,
|
|
757
|
+
cb: (outcome: 'sent' | 'aborted') => void | Promise<void>,
|
|
758
|
+
log: { error(obj: unknown, msg?: string): void },
|
|
759
|
+
): void {
|
|
760
|
+
const raw = reply.raw
|
|
761
|
+
let called = false
|
|
762
|
+
const settle = (outcome: 'sent' | 'aborted'): void => {
|
|
763
|
+
if (called) return
|
|
764
|
+
called = true
|
|
765
|
+
raw.removeListener('finish', onFinish)
|
|
766
|
+
raw.removeListener('close', onClose)
|
|
767
|
+
void (async () => {
|
|
768
|
+
try {
|
|
769
|
+
await cb(outcome)
|
|
770
|
+
} catch (err) {
|
|
771
|
+
log.error(err, 'HttpResponse.onFinish callback threw')
|
|
772
|
+
}
|
|
773
|
+
})()
|
|
774
|
+
}
|
|
775
|
+
const onFinish = (): void => settle('sent')
|
|
776
|
+
const onClose = (): void => settle(raw.writableFinished ? 'sent' : 'aborted')
|
|
777
|
+
if (raw.writableFinished) return settle('sent')
|
|
778
|
+
if (raw.destroyed) return settle('aborted')
|
|
779
|
+
raw.once('finish', onFinish)
|
|
780
|
+
raw.once('close', onClose)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** A file response the route cannot send; becomes a 500 naming the reason. */
|
|
784
|
+
class FileResponseError extends Error {}
|
|
785
|
+
|
|
786
|
+
async function sendFile(reply: FastifyReply, req: FastifyRequest, file: FileResponse, declared: FileConfig | null, route: string): Promise<unknown> {
|
|
787
|
+
const problem = fileResponseProblem(declared, file)
|
|
788
|
+
if (problem !== null) {
|
|
789
|
+
if (!Buffer.isBuffer(file.body)) file.body.destroy()
|
|
790
|
+
throw new FileResponseError(`[pipework] ${route}: ${problem}`)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
let body: Buffer | Readable
|
|
794
|
+
let length: number | undefined
|
|
795
|
+
if (Buffer.isBuffer(file.body)) {
|
|
796
|
+
body = file.body
|
|
797
|
+
length = file.body.length
|
|
798
|
+
} else {
|
|
799
|
+
body = await primeStream(file.body, file.length)
|
|
800
|
+
length = file.length
|
|
801
|
+
const raw = reply.raw
|
|
802
|
+
body.on('error', (err) => {
|
|
803
|
+
// Headers are out: the only honest signal left is a dead connection.
|
|
804
|
+
req.log.error({ route, requestId: String(req.id), err }, 'File stream failed while sending')
|
|
805
|
+
raw.destroy()
|
|
806
|
+
})
|
|
807
|
+
raw.once('close', () => { if (!(body as Readable).destroyed) (body as Readable).destroy() })
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
void reply.status(declared?.status ?? 200)
|
|
811
|
+
void reply.header('content-type', file.mediaType)
|
|
812
|
+
void reply.header('content-disposition', contentDisposition(file.disposition, file.filename))
|
|
813
|
+
void reply.header('x-content-type-options', 'nosniff')
|
|
814
|
+
if (length !== undefined) void reply.header('content-length', String(length))
|
|
815
|
+
return reply.send(body)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** The largest declared body a route that ignores its body drains. */
|
|
819
|
+
const IGNORED_BODY_LIMIT = 64 * 1024
|
|
820
|
+
|
|
821
|
+
function isStatusResponse(value: unknown): value is HandlerResponse {
|
|
822
|
+
return value !== null && typeof value === 'object' && 'status' in value && typeof (value as Record<string, unknown>)['status'] === 'number'
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async function sendResult(reply: FastifyReply, req: FastifyRequest, result: unknown, fileConfig: FileConfig | null, route: string): Promise<unknown> {
|
|
826
|
+
if (isFileResponse(result)) return sendFile(reply, req, result, fileConfig, route)
|
|
827
|
+
if (fileConfig !== null) {
|
|
828
|
+
const other = isStatusResponse(result) && result.status !== fileConfig.status
|
|
829
|
+
if (!other) {
|
|
830
|
+
throw new FileResponseError(`[pipework] ${route} is declared with .file() and must return http.file({ body, mediaType, filename? }) at status ${fileConfig.status}; it returned ${result === null ? 'null' : typeof result}. Only a { status } answer at another status may stand in for the file.`)
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (isStatusResponse(result)) {
|
|
834
|
+
const response = result as HandlerResponse
|
|
835
|
+
if (response.body !== undefined) {
|
|
836
|
+
return reply.status(response.status).send(response.body)
|
|
837
|
+
}
|
|
838
|
+
return reply.status(response.status).send()
|
|
839
|
+
}
|
|
840
|
+
return reply.status(200).send(result)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function credentialedWildcard(grant: CorsConfig | false): string | null {
|
|
844
|
+
if (grant === false || grant.credentials !== true) return null
|
|
845
|
+
const origin = grant.origin
|
|
846
|
+
if (origin === true || origin === '*' || (Array.isArray(origin) && origin.includes('*'))) {
|
|
847
|
+
return 'a grant with credentials: true must name its origins; origin: true or "*" lets any site read credentialed responses.\n' +
|
|
848
|
+
' Fix: List the exact origins, or drop credentials.'
|
|
849
|
+
}
|
|
850
|
+
return null
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function validateProductionConfig(config: ServerConfig): void {
|
|
854
|
+
const problems: string[] = []
|
|
855
|
+
|
|
856
|
+
if (config.cors !== undefined && config.cors !== false) {
|
|
857
|
+
const problem = credentialedWildcard(config.cors)
|
|
858
|
+
if (problem !== null) problems.push(`CORS: ${problem}`)
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (config.rateLimit === undefined && config.rateLimits === undefined) {
|
|
862
|
+
problems.push(
|
|
863
|
+
'Rate limiting: Not configured. Production APIs should have rate limiting.\n' +
|
|
864
|
+
' Fix: Set rateLimit: { max: 100, timeWindow: "1 minute" }, set rateLimits: { classes: {...} }, or set rateLimit: false to explicitly opt out.',
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (config.auth !== undefined && config.tenant === undefined) {
|
|
869
|
+
problems.push(
|
|
870
|
+
'Tenant isolation: Auth is configured but no tenant config is set.\n' +
|
|
871
|
+
' Fix: Set tenant: { mode: "schema" | "rls" | "header", ... } to enforce tenant isolation,\n' +
|
|
872
|
+
' or acknowledge this by setting tenant: null in your server config.',
|
|
873
|
+
)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (problems.length > 0) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
`[pipework] Production server configuration is missing security settings:\n\n${problems.map((p, i) => ` ${i + 1}. ${p}`).join('\n\n')}\n\n` +
|
|
879
|
+
' Pipework refuses to start a production server with insecure defaults.\n' +
|
|
880
|
+
' Set each option explicitly to confirm your security posture.\n',
|
|
881
|
+
)
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async function enforceHandlerPermission(
|
|
886
|
+
instance: Manifold,
|
|
887
|
+
ctx: import('../context/types.js').Flow,
|
|
888
|
+
meta: import('../di/types.js').HandlerMeta,
|
|
889
|
+
rbac: Rbac,
|
|
890
|
+
): Promise<void> {
|
|
891
|
+
const permission = meta.permission!
|
|
892
|
+
|
|
893
|
+
if (ctx.auth === null || typeof ctx.auth !== 'object') {
|
|
894
|
+
throw new ForbiddenError(`Missing permission: ${permission.resource}:${permission.action}`)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const auth = ctx.auth as Record<string, unknown>
|
|
898
|
+
const userId = typeof auth['userId'] === 'string' ? auth['userId'] : null
|
|
899
|
+
const tenantId = ctx.tenant
|
|
900
|
+
const authVersion = typeof auth['authVersion'] === 'number' ? auth['authVersion'] : 1
|
|
901
|
+
|
|
902
|
+
if (userId === null || tenantId === null) {
|
|
903
|
+
throw new ForbiddenError(`Missing permission: ${permission.resource}:${permission.action}`)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const dbName = meta.databases[0] ?? [...instance.config.databases().keys()][0]
|
|
907
|
+
if (dbName === undefined) {
|
|
908
|
+
throw new ForbiddenError(`Missing permission: ${permission.resource}:${permission.action}`)
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const permissionSet = await withTenantSetting(instance, dbName, tenantId, db => rbac.resolve(db, userId, tenantId, authVersion))
|
|
912
|
+
const granted = rbac.check(permissionSet, permission.resource, permission.action, permission.scope)
|
|
913
|
+
if (!granted) {
|
|
914
|
+
throw new ForbiddenError(`Missing permission: ${permission.resource}:${permission.action}`)
|
|
915
|
+
}
|
|
916
|
+
}
|