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,156 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { sql } from '../../data/db/sql.js'
|
|
3
|
+
import type { DB } from '../../data/db/index.js'
|
|
4
|
+
import { pipeSystem } from '../../data/db/db.js'
|
|
5
|
+
import { auditUnauthenticated } from './audit.js'
|
|
6
|
+
import { getBaseLogger } from '../../core/logging/index.js'
|
|
7
|
+
import type { Passwords } from './passwords.js'
|
|
8
|
+
|
|
9
|
+
export interface SignInConfig {
|
|
10
|
+
readonly passwords: Passwords
|
|
11
|
+
/** Failures per account within the window before it is locked. Default 5. */
|
|
12
|
+
readonly accountThreshold?: number
|
|
13
|
+
/** Failures per client address within the window before it is locked. Default 20. */
|
|
14
|
+
readonly clientThreshold?: number
|
|
15
|
+
/** Window since the first failure after which the count restarts. Default 900 seconds. */
|
|
16
|
+
readonly windowSeconds?: number
|
|
17
|
+
/** Longest single lock. Default 900 seconds. */
|
|
18
|
+
readonly maxLockSeconds?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SignInAttempt {
|
|
22
|
+
readonly account: string
|
|
23
|
+
/** The client address; without it only the account is guarded. */
|
|
24
|
+
readonly client?: string
|
|
25
|
+
readonly password: string
|
|
26
|
+
/** Finds the account's user id and stored password hash, or null if there is no such account. */
|
|
27
|
+
readonly lookup: (db: DB, account: string) => Promise<{ userId: string; passwordHash: string } | null>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SignInResult =
|
|
31
|
+
| { readonly ok: true; readonly userId: string; readonly needsRehash: boolean }
|
|
32
|
+
| { readonly ok: false; readonly reason: 'invalid' | 'locked'; readonly retryAfterSeconds?: number }
|
|
33
|
+
|
|
34
|
+
export interface SignIn {
|
|
35
|
+
attempt(input: SignInAttempt): Promise<SignInResult>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Key { readonly key: string; readonly threshold: number }
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a guarded password sign-in. `attempt` needs an active Flow (a request,
|
|
42
|
+
* job or test context) and runs every statement, `lookup` included, on
|
|
43
|
+
* `pipe.system()`: outside the request transaction, committed on its own, so a
|
|
44
|
+
* route that answers a bad password by throwing `UnauthorizedError` (rolling the
|
|
45
|
+
* request back) still keeps the count. Failed attempts are counted per account and
|
|
46
|
+
* per client address in `pipework_signin_attempts` (blueprint `passwords: {}`), so
|
|
47
|
+
* counts survive restarts and are shared across processes. At the threshold each
|
|
48
|
+
* further failure locks the key for 2^(failures - threshold) seconds, capped at
|
|
49
|
+
* `maxLockSeconds`. Every attempt is counted, atomically per key, before the
|
|
50
|
+
* password is checked, so a burst of parallel guesses is locked out instead of
|
|
51
|
+
* all being verified. While any key is locked an attempt is refused with
|
|
52
|
+
* `reason: 'locked'` and `retryAfterSeconds`, without checking the password (a
|
|
53
|
+
* locked key is not counted further). A success deletes the account's key and
|
|
54
|
+
* gives the client key its count back. An unknown account runs a dummy verify
|
|
55
|
+
* and counts as a failure, reported as `'invalid'` like a wrong password.
|
|
56
|
+
*
|
|
57
|
+
* Tradeoff: locking is per account, so anyone who knows an account name can lock
|
|
58
|
+
* it out by failing on purpose. Each lock is capped (default 15 minutes) and the
|
|
59
|
+
* client key bounds how fast one address can do it.
|
|
60
|
+
*
|
|
61
|
+
* Every success, failure and refusal is logged, and audited (entity `SignIn`,
|
|
62
|
+
* verbs `success`, `fail`, `locked`; never the account string or password) when
|
|
63
|
+
* the Flow has an open trace step. Route a locked result to a 429 with
|
|
64
|
+
* `throw new TooManyRequestsError(result.retryAfterSeconds)`.
|
|
65
|
+
*/
|
|
66
|
+
export function createSignIn(config: SignInConfig): SignIn {
|
|
67
|
+
const accountThreshold = config.accountThreshold ?? 5
|
|
68
|
+
const clientThreshold = config.clientThreshold ?? 20
|
|
69
|
+
const windowSeconds = config.windowSeconds ?? 900
|
|
70
|
+
const maxLockSeconds = config.maxLockSeconds ?? 900
|
|
71
|
+
const { passwords } = config
|
|
72
|
+
|
|
73
|
+
function keysFor(account: string, client: string | undefined): { accountKey: Key; all: Key[] } {
|
|
74
|
+
const normalized = account.normalize('NFC').trim().toLowerCase()
|
|
75
|
+
const accountKey = { key: 'account:' + createHash('sha256').update(normalized).digest('hex'), threshold: accountThreshold }
|
|
76
|
+
const all = [accountKey]
|
|
77
|
+
if (client !== undefined && client !== '') all.push({ key: 'client:' + client, threshold: clientThreshold })
|
|
78
|
+
return { accountKey, all }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Counts the attempt before any password is checked, one statement per key, so
|
|
82
|
+
// a burst of parallel guesses is counted (and locked) before it is verified.
|
|
83
|
+
// The update applies only to a key that is not currently locked; a locked key
|
|
84
|
+
// returns no row and is read separately.
|
|
85
|
+
async function countAttempt(db: DB, { key, threshold }: Key): Promise<{ locked: boolean; failures: number; retryAfterSeconds: number }> {
|
|
86
|
+
const rows = await db.execute(sql`
|
|
87
|
+
INSERT INTO pipework_signin_attempts AS a (key, failures, first_failure_at, last_failure_at, locked_until)
|
|
88
|
+
VALUES (${key}, 1, now(), now(),
|
|
89
|
+
CASE WHEN 1 >= ${threshold}::int
|
|
90
|
+
THEN now() + least(power(2, 0), ${maxLockSeconds}::int) * interval '1 second' END)
|
|
91
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
92
|
+
failures = CASE WHEN a.first_failure_at < now() - ${windowSeconds}::int * interval '1 second' THEN 1 ELSE a.failures + 1 END,
|
|
93
|
+
first_failure_at = CASE WHEN a.first_failure_at < now() - ${windowSeconds}::int * interval '1 second' THEN now() ELSE a.first_failure_at END,
|
|
94
|
+
last_failure_at = now(),
|
|
95
|
+
locked_until = CASE
|
|
96
|
+
WHEN (CASE WHEN a.first_failure_at < now() - ${windowSeconds}::int * interval '1 second' THEN 1 ELSE a.failures + 1 END) >= ${threshold}::int
|
|
97
|
+
THEN now() + least(
|
|
98
|
+
power(2, least((CASE WHEN a.first_failure_at < now() - ${windowSeconds}::int * interval '1 second' THEN 1 ELSE a.failures + 1 END) - ${threshold}::int, 30)),
|
|
99
|
+
${maxLockSeconds}::int) * interval '1 second'
|
|
100
|
+
END
|
|
101
|
+
WHERE a.locked_until IS NULL OR a.locked_until <= now()
|
|
102
|
+
RETURNING failures, coalesce(ceil(extract(epoch FROM (locked_until - now()))), 0)::int AS remaining`)
|
|
103
|
+
if (rows.length > 0) {
|
|
104
|
+
return { locked: false, failures: Number(rows[0]!['failures']), retryAfterSeconds: Math.max(0, Number(rows[0]!['remaining'])) }
|
|
105
|
+
}
|
|
106
|
+
const held = await db.execute(sql`
|
|
107
|
+
SELECT failures, greatest(ceil(extract(epoch FROM (locked_until - now())))::int, 1) AS remaining
|
|
108
|
+
FROM pipework_signin_attempts WHERE key = ${key}`)
|
|
109
|
+
return { locked: true, failures: Number(held[0]?.['failures'] ?? 0), retryAfterSeconds: Number(held[0]?.['remaining'] ?? 1) }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const audit = (db: DB, verb: string, userId: string | null, details: Record<string, unknown>): Promise<void> =>
|
|
113
|
+
auditUnauthenticated(db, 'SignIn', verb, userId, details)
|
|
114
|
+
|
|
115
|
+
async function attempt(input: SignInAttempt): Promise<SignInResult> {
|
|
116
|
+
const db = pipeSystem()
|
|
117
|
+
const { accountKey, all } = keysFor(input.account, input.client)
|
|
118
|
+
const client = input.client ?? null
|
|
119
|
+
const base = { accountKey: accountKey.key, client }
|
|
120
|
+
|
|
121
|
+
const counted: Array<{ k: Key; r: { locked: boolean; failures: number; retryAfterSeconds: number } }> = []
|
|
122
|
+
for (const k of all) counted.push({ k, r: await countAttempt(db, k) })
|
|
123
|
+
const heldFor = Math.max(0, ...counted.filter(c => c.r.locked).map(c => c.r.retryAfterSeconds))
|
|
124
|
+
if (counted.some(c => c.r.locked)) {
|
|
125
|
+
getBaseLogger().warn({ event: 'sign-in refused', ...base, retryAfterSeconds: heldFor }, 'sign-in refused: locked')
|
|
126
|
+
await audit(db, 'locked', null, { ...base, failures: counted[0]!.r.failures, retryAfterSeconds: heldFor })
|
|
127
|
+
return { ok: false, reason: 'locked', retryAfterSeconds: heldFor }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const found = await input.lookup(db, input.account)
|
|
131
|
+
if (found === null) await passwords.verifyNone(input.password)
|
|
132
|
+
const verified = found === null ? null : await passwords.verify(found.passwordHash, input.password)
|
|
133
|
+
|
|
134
|
+
if (found !== null && verified !== null && verified.ok) {
|
|
135
|
+
await db.execute(sql`DELETE FROM pipework_signin_attempts WHERE key = ${accountKey.key}`)
|
|
136
|
+
for (const { k } of counted) {
|
|
137
|
+
if (k !== accountKey) {
|
|
138
|
+
await db.execute(sql`UPDATE pipework_signin_attempts SET failures = greatest(failures - 1, 0) WHERE key = ${k.key}`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
getBaseLogger().info({ event: 'sign-in success', ...base }, 'sign-in succeeded')
|
|
142
|
+
await audit(db, 'success', found.userId, { ...base, failures: 0, retryAfterSeconds: null })
|
|
143
|
+
return { ok: true, userId: found.userId, needsRehash: verified.needsRehash }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const retryAfterSeconds = Math.max(0, ...counted.map(c => c.r.retryAfterSeconds))
|
|
147
|
+
const details = { ...base, failures: counted[0]!.r.failures, retryAfterSeconds: retryAfterSeconds > 0 ? retryAfterSeconds : null }
|
|
148
|
+
getBaseLogger().warn({ event: 'sign-in failed', ...details }, 'sign-in failed')
|
|
149
|
+
await audit(db, 'fail', found?.userId ?? null, details)
|
|
150
|
+
return retryAfterSeconds > 0
|
|
151
|
+
? { ok: false, reason: 'invalid', retryAfterSeconds }
|
|
152
|
+
: { ok: false, reason: 'invalid' }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { attempt }
|
|
156
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createPublicKey, createPrivateKey, type KeyObject } from 'node:crypto'
|
|
2
|
+
import { importPKCS8, importSPKI } from 'jose'
|
|
3
|
+
import { AuthConfigurationError } from '../../core/errors/index.js'
|
|
4
|
+
|
|
5
|
+
interface Signing { readonly algorithm: 'ES256'; readonly privateKey: string; readonly publicKey: string }
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parses both configured signing keys now, so a key that is not valid PEM stops the server at start.
|
|
9
|
+
* Returns the public key for synchronous verification. `owner` names the factory in the message.
|
|
10
|
+
*/
|
|
11
|
+
export function checkSigningKeys(signing: Signing, owner: string): KeyObject {
|
|
12
|
+
let publicKey: KeyObject
|
|
13
|
+
try {
|
|
14
|
+
publicKey = createPublicKey(signing.publicKey)
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw keyFault('signing.publicKey', 'a valid SPKI PEM public key', owner, error)
|
|
17
|
+
}
|
|
18
|
+
let privateKey: KeyObject
|
|
19
|
+
try {
|
|
20
|
+
privateKey = createPrivateKey(signing.privateKey)
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw keyFault('signing.privateKey', 'a valid PKCS#8 PEM private key', owner, error)
|
|
23
|
+
}
|
|
24
|
+
assertFitsAlgorithm(publicKey, 'signing.publicKey', signing.algorithm, owner)
|
|
25
|
+
assertFitsAlgorithm(privateKey, 'signing.privateKey', signing.algorithm, owner)
|
|
26
|
+
return publicKey
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Imports a configured key for jose; a failure is a configuration fault (500), never a refusal of a token. */
|
|
30
|
+
export async function importSigningKey(kind: 'public' | 'private', signing: Signing, owner: string): Promise<CryptoKey> {
|
|
31
|
+
try {
|
|
32
|
+
return kind === 'public'
|
|
33
|
+
? await importSPKI(signing.publicKey, signing.algorithm)
|
|
34
|
+
: await importPKCS8(signing.privateKey, signing.algorithm)
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw keyFault(
|
|
37
|
+
kind === 'public' ? 'signing.publicKey' : 'signing.privateKey',
|
|
38
|
+
`a key usable with ${signing.algorithm}`, owner, error,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function keyFault(field: string, expected: string, owner: string, error: unknown): AuthConfigurationError {
|
|
44
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
45
|
+
return new AuthConfigurationError(
|
|
46
|
+
`[pipework] ${owner}: ${field} is not ${expected}: ${reason}`,
|
|
47
|
+
{ field },
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const CURVES: Record<string, string> = { ES256: 'prime256v1', ES384: 'secp384r1', ES512: 'secp521r1' }
|
|
52
|
+
|
|
53
|
+
function assertFitsAlgorithm(key: KeyObject, field: string, algorithm: string, owner: string): void {
|
|
54
|
+
const type = key.asymmetricKeyType
|
|
55
|
+
let fits: boolean | null = null
|
|
56
|
+
if (/^(RS|PS)\d+$/.test(algorithm)) {
|
|
57
|
+
fits = type === 'rsa' || (algorithm.startsWith('PS') && type === 'rsa-pss')
|
|
58
|
+
} else if (algorithm in CURVES) {
|
|
59
|
+
fits = type === 'ec' && key.asymmetricKeyDetails?.namedCurve === CURVES[algorithm]
|
|
60
|
+
} else if (algorithm === 'EdDSA') {
|
|
61
|
+
fits = type === 'ed25519' || type === 'ed448'
|
|
62
|
+
}
|
|
63
|
+
if (fits === null || fits) return
|
|
64
|
+
const curve = type === 'ec' ? ` (${key.asymmetricKeyDetails?.namedCurve ?? 'unknown curve'})` : ''
|
|
65
|
+
throw new AuthConfigurationError(
|
|
66
|
+
`[pipework] ${owner}: ${field} is a ${type ?? 'unknown'}${curve} key, which does not fit algorithm ${algorithm}`,
|
|
67
|
+
{ field },
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DB } from '../../data/db/types.js'
|
|
2
|
+
|
|
3
|
+
/** Runtime auth context passed to strategies — carries the resolved DB and request metadata. */
|
|
4
|
+
export interface AuthContext {
|
|
5
|
+
readonly db: (name?: string) => DB
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Pluggable auth strategy — implement authenticate() to extract auth from a request. */
|
|
9
|
+
export interface AuthStrategy<TAuth> {
|
|
10
|
+
readonly name: string
|
|
11
|
+
extract(request: AuthRequest, context: AuthContext): Promise<TAuth | null>
|
|
12
|
+
verify(extracted: TAuth, context: AuthContext): Promise<TAuth>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Normalized request shape passed to auth strategies — headers, the parsed `Cookie` header, and the upper-case HTTP method. */
|
|
16
|
+
export interface AuthRequest {
|
|
17
|
+
readonly headers: Readonly<Record<string, string | undefined>>
|
|
18
|
+
readonly cookies: Readonly<Record<string, string | undefined>>
|
|
19
|
+
readonly method?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Minimum auth shape — userId and tenantId. Extend for app-specific auth fields. */
|
|
23
|
+
export interface BaseAuth {
|
|
24
|
+
readonly userId: string
|
|
25
|
+
readonly tenantId: string
|
|
26
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ValidationError } from '../../core/errors/index.js'
|
|
2
|
+
|
|
3
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
4
|
+
|
|
5
|
+
/** Throws a ValidationError unless `value` is a uuid; the session and org tables type tenant and org ids uuid. */
|
|
6
|
+
export function assertUuid(value: string, what: string): void {
|
|
7
|
+
if (!UUID_RE.test(value)) {
|
|
8
|
+
throw new ValidationError(`${what} must be a uuid, got "${value}". The pipework_refresh_tokens and pipework_user_orgs tables type it uuid.`)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { PermissionSet } from './types.js'
|
|
2
|
+
|
|
3
|
+
interface CacheEntry {
|
|
4
|
+
readonly set: PermissionSet
|
|
5
|
+
readonly expiresAt: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class PermissionCache {
|
|
9
|
+
private readonly store = new Map<string, CacheEntry>()
|
|
10
|
+
private readonly ttlMs: number
|
|
11
|
+
|
|
12
|
+
constructor(ttlMs: number) {
|
|
13
|
+
this.ttlMs = ttlMs
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get(userId: string, tenantId: string, authVersion: number): PermissionSet | null {
|
|
17
|
+
const key = this.key(userId, tenantId, authVersion)
|
|
18
|
+
const entry = this.store.get(key)
|
|
19
|
+
if (!entry) return null
|
|
20
|
+
if (Date.now() > entry.expiresAt) {
|
|
21
|
+
this.store.delete(key)
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
return entry.set
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
set(permissionSet: PermissionSet): void {
|
|
28
|
+
const key = this.key(permissionSet.userId, permissionSet.tenantId, permissionSet.authVersion)
|
|
29
|
+
this.store.set(key, {
|
|
30
|
+
set: permissionSet,
|
|
31
|
+
expiresAt: Date.now() + this.ttlMs,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
invalidate(userId: string, tenantId: string): void {
|
|
36
|
+
const prefix = `${userId}:${tenantId}:`
|
|
37
|
+
for (const key of this.store.keys()) {
|
|
38
|
+
if (key.startsWith(prefix)) {
|
|
39
|
+
this.store.delete(key)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
clear(): void {
|
|
45
|
+
this.store.clear()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get size(): number {
|
|
49
|
+
return this.store.size
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private key(userId: string, tenantId: string, authVersion: number): string {
|
|
53
|
+
return `${userId}:${tenantId}:${authVersion}`
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ResolvedPermission } from './types.js'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_HIERARCHY: readonly string[] = ['own', 'team', 'territory', 'tenant']
|
|
4
|
+
|
|
5
|
+
export function scopeSatisfies(
|
|
6
|
+
granted: string,
|
|
7
|
+
required: string,
|
|
8
|
+
hierarchy: readonly string[] = DEFAULT_HIERARCHY,
|
|
9
|
+
): boolean {
|
|
10
|
+
const grantedIdx = hierarchy.indexOf(granted)
|
|
11
|
+
const requiredIdx = hierarchy.indexOf(required)
|
|
12
|
+
if (grantedIdx === -1 || requiredIdx === -1) return false
|
|
13
|
+
return grantedIdx >= requiredIdx
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function hasPermission(
|
|
17
|
+
permissions: readonly ResolvedPermission[],
|
|
18
|
+
resource: string,
|
|
19
|
+
action: string,
|
|
20
|
+
requiredScope?: string,
|
|
21
|
+
hierarchy: readonly string[] = DEFAULT_HIERARCHY,
|
|
22
|
+
): boolean {
|
|
23
|
+
for (const perm of permissions) {
|
|
24
|
+
if (perm.resource === resource && perm.action === action) {
|
|
25
|
+
if (requiredScope === undefined) return true
|
|
26
|
+
if (scopeSatisfies(perm.scope, requiredScope, hierarchy)) return true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DB } from '../../data/db/index.js'
|
|
2
|
+
import type { RbacConfig, Rbac, PermissionSet, SeedData } from './types.js'
|
|
3
|
+
import { PermissionCache } from './cache.js'
|
|
4
|
+
import { resolveUserPermissions } from './resolve.js'
|
|
5
|
+
import { hasPermission } from './check.js'
|
|
6
|
+
import { seedRbac } from './seed.js'
|
|
7
|
+
|
|
8
|
+
/** Creates an Rbac instance from role/permission config — provides check, enforce, and seed operations. */
|
|
9
|
+
export function createRbac(config: RbacConfig): Rbac {
|
|
10
|
+
const cache = new PermissionCache(config.cacheTtlMs ?? 60_000)
|
|
11
|
+
const scopes = config.scopes ?? ['own', 'team', 'territory', 'tenant']
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
async resolve(db: DB, userId: string, tenantId: string, authVersion: number): Promise<PermissionSet> {
|
|
15
|
+
const cached = cache.get(userId, tenantId, authVersion)
|
|
16
|
+
if (cached) return cached
|
|
17
|
+
|
|
18
|
+
const resolved = await resolveUserPermissions(db, userId, tenantId, authVersion, config)
|
|
19
|
+
cache.set(resolved)
|
|
20
|
+
return resolved
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
check(permissions: PermissionSet, resource: string, action: string, requiredScope?: string): boolean {
|
|
24
|
+
return hasPermission(permissions.permissions, resource, action, requiredScope, scopes)
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
invalidate(userId: string, tenantId: string): void {
|
|
28
|
+
cache.invalidate(userId, tenantId)
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
async seed(db: DB, data: SeedData): Promise<void> {
|
|
32
|
+
await seedRbac(db, data, config)
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { RbacConfig, Rbac, PermissionSet, ResolvedPermission, Scope, SeedData, SeedPermission, SeedRole, SeedRolePermission, SeedUserRole } from './types.js'
|
|
2
|
+
export { hasPermission, scopeSatisfies } from './check.js'
|
|
3
|
+
export { PermissionCache } from './cache.js'
|
|
4
|
+
export { enforcePermission } from './middleware.js'
|
|
5
|
+
export { createRbac } from './create.js'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DB } from '../../data/db/index.js'
|
|
2
|
+
import type { Rbac } from './types.js'
|
|
3
|
+
import { ForbiddenError } from '../../core/errors/index.js'
|
|
4
|
+
|
|
5
|
+
export async function enforcePermission(
|
|
6
|
+
rbac: Rbac,
|
|
7
|
+
db: DB,
|
|
8
|
+
userId: string,
|
|
9
|
+
tenantId: string,
|
|
10
|
+
authVersion: number,
|
|
11
|
+
resource: string,
|
|
12
|
+
action: string,
|
|
13
|
+
requiredScope?: string,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const permissions = await rbac.resolve(db, userId, tenantId, authVersion)
|
|
16
|
+
const granted = rbac.check(permissions, resource, action, requiredScope)
|
|
17
|
+
if (!granted) {
|
|
18
|
+
throw new ForbiddenError(`Missing permission: ${resource}:${action}`)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRbac, hasPermission, enforcePermission, scopeSatisfies, PermissionCache } from './index.js'
|
|
2
|
+
|
|
3
|
+
/** Role-based access control namespace — permission checking, enforcement, scope hierarchy. */
|
|
4
|
+
export const rbac = {
|
|
5
|
+
/** Creates an Rbac instance from config (roles, permissions, hierarchy). */
|
|
6
|
+
create: createRbac,
|
|
7
|
+
/** Checks if a permission set includes the required resource/action/scope. Pure function, no DB. */
|
|
8
|
+
check: hasPermission,
|
|
9
|
+
/** Checks permission and throws ForbiddenError if denied. Loads permissions from DB with caching. */
|
|
10
|
+
enforce: enforcePermission,
|
|
11
|
+
/** Checks if a granted scope satisfies a required scope given a hierarchy (e.g. org > team > self). */
|
|
12
|
+
satisfies: scopeSatisfies,
|
|
13
|
+
/** LRU cache for resolved permissions — avoids repeated DB lookups within a request. */
|
|
14
|
+
PermissionCache,
|
|
15
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { sql } from '../../data/db/sql.js'
|
|
2
|
+
import type { DB } from '../../data/db/index.js'
|
|
3
|
+
import type { RbacConfig, PermissionSet, ResolvedPermission } from './types.js'
|
|
4
|
+
import { quoteIdentifier } from '../../data/db/identifiers.js'
|
|
5
|
+
import { resolveRbacTables } from './tables.js'
|
|
6
|
+
|
|
7
|
+
export async function resolveUserPermissions(
|
|
8
|
+
db: DB,
|
|
9
|
+
userId: string,
|
|
10
|
+
tenantId: string,
|
|
11
|
+
authVersion: number,
|
|
12
|
+
config: RbacConfig,
|
|
13
|
+
): Promise<PermissionSet> {
|
|
14
|
+
const { userRoles: userRolesTable, rolePermissions: rolePermissionsTable, permissions: permissionsTable } = resolveRbacTables(config)
|
|
15
|
+
|
|
16
|
+
const rows = await db.execute(sql`
|
|
17
|
+
SELECT p.resource, p.action, rp.scope
|
|
18
|
+
FROM ${sql.raw(quoteIdentifier(userRolesTable, 'userRoles table'))} ur
|
|
19
|
+
JOIN ${sql.raw(quoteIdentifier(rolePermissionsTable, 'rolePermissions table'))} rp ON rp.role_id = ur.role_id
|
|
20
|
+
JOIN ${sql.raw(quoteIdentifier(permissionsTable, 'permissions table'))} p ON p.id = rp.permission_id
|
|
21
|
+
WHERE ur.user_id = ${userId} AND ur.tenant_id = ${tenantId}
|
|
22
|
+
`)
|
|
23
|
+
|
|
24
|
+
const permissions: ResolvedPermission[] = rows.map((row) => ({
|
|
25
|
+
resource: row['resource'] as string,
|
|
26
|
+
action: row['action'] as string,
|
|
27
|
+
scope: row['scope'] as string,
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
return { userId, tenantId, authVersion, permissions }
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS pipework_permissions (
|
|
2
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3
|
+
resource text NOT NULL,
|
|
4
|
+
action text NOT NULL,
|
|
5
|
+
UNIQUE (resource, action)
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS pipework_roles (
|
|
9
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
10
|
+
name text NOT NULL,
|
|
11
|
+
tenant_id text NOT NULL,
|
|
12
|
+
description text,
|
|
13
|
+
UNIQUE (tenant_id, name)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE TABLE IF NOT EXISTS pipework_role_permissions (
|
|
17
|
+
role_id uuid NOT NULL REFERENCES pipework_roles(id) ON DELETE CASCADE,
|
|
18
|
+
permission_id uuid NOT NULL REFERENCES pipework_permissions(id) ON DELETE CASCADE,
|
|
19
|
+
scope text NOT NULL DEFAULT 'own',
|
|
20
|
+
PRIMARY KEY (role_id, permission_id),
|
|
21
|
+
CONSTRAINT valid_scope CHECK (scope = ANY(ARRAY['own', 'team', 'territory', 'tenant']))
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE TABLE IF NOT EXISTS pipework_user_roles (
|
|
25
|
+
user_id text NOT NULL,
|
|
26
|
+
role_id uuid NOT NULL REFERENCES pipework_roles(id) ON DELETE CASCADE,
|
|
27
|
+
tenant_id text NOT NULL,
|
|
28
|
+
PRIMARY KEY (user_id, role_id, tenant_id)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_user_roles_lookup ON pipework_user_roles (user_id, tenant_id);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { sql } from '../../data/db/sql.js'
|
|
2
|
+
import type { DB } from '../../data/db/index.js'
|
|
3
|
+
import type { RbacConfig, SeedData } from './types.js'
|
|
4
|
+
import { quoteIdentifier } from '../../data/db/identifiers.js'
|
|
5
|
+
import { resolveRbacTables } from './tables.js'
|
|
6
|
+
|
|
7
|
+
export async function seedRbac(
|
|
8
|
+
db: DB,
|
|
9
|
+
data: SeedData,
|
|
10
|
+
config: RbacConfig,
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
const tables = resolveRbacTables(config)
|
|
13
|
+
|
|
14
|
+
const permQ = quoteIdentifier(tables.permissions, 'permissions table')
|
|
15
|
+
const rolesQ = quoteIdentifier(tables.roles, 'roles table')
|
|
16
|
+
const rolePermQ = quoteIdentifier(tables.rolePermissions, 'rolePermissions table')
|
|
17
|
+
const userRolesQ = quoteIdentifier(tables.userRoles, 'userRoles table')
|
|
18
|
+
|
|
19
|
+
for (const perm of data.permissions) {
|
|
20
|
+
await db.execute(sql`
|
|
21
|
+
INSERT INTO ${sql.raw(permQ)} (resource, action)
|
|
22
|
+
VALUES (${perm.resource}, ${perm.action})
|
|
23
|
+
ON CONFLICT (resource, action) DO NOTHING
|
|
24
|
+
`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const role of data.roles) {
|
|
28
|
+
if (role.description !== undefined) {
|
|
29
|
+
await db.execute(sql`
|
|
30
|
+
INSERT INTO ${sql.raw(rolesQ)} (name, tenant_id, description)
|
|
31
|
+
VALUES (${role.name}, ${role.tenantId}, ${role.description})
|
|
32
|
+
ON CONFLICT (tenant_id, name) DO UPDATE SET
|
|
33
|
+
description = EXCLUDED.description
|
|
34
|
+
`)
|
|
35
|
+
} else {
|
|
36
|
+
await db.execute(sql`
|
|
37
|
+
INSERT INTO ${sql.raw(rolesQ)} (name, tenant_id)
|
|
38
|
+
VALUES (${role.name}, ${role.tenantId})
|
|
39
|
+
ON CONFLICT (tenant_id, name) DO UPDATE SET
|
|
40
|
+
description = EXCLUDED.description
|
|
41
|
+
`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const rp of role.permissions) {
|
|
45
|
+
await db.execute(sql`
|
|
46
|
+
INSERT INTO ${sql.raw(rolePermQ)} (role_id, permission_id, scope)
|
|
47
|
+
SELECT r.id, p.id, ${rp.scope}
|
|
48
|
+
FROM ${sql.raw(rolesQ)} r, ${sql.raw(permQ)} p
|
|
49
|
+
WHERE r.name = ${role.name} AND r.tenant_id = ${role.tenantId}
|
|
50
|
+
AND p.resource = ${rp.resource} AND p.action = ${rp.action}
|
|
51
|
+
ON CONFLICT (role_id, permission_id) DO UPDATE SET scope = EXCLUDED.scope
|
|
52
|
+
`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (data.userRoles) {
|
|
57
|
+
for (const ur of data.userRoles) {
|
|
58
|
+
await db.execute(sql`
|
|
59
|
+
INSERT INTO ${sql.raw(userRolesQ)} (user_id, role_id, tenant_id)
|
|
60
|
+
SELECT ${ur.userId}, r.id, ${ur.tenantId}
|
|
61
|
+
FROM ${sql.raw(rolesQ)} r
|
|
62
|
+
WHERE r.name = ${ur.roleName} AND r.tenant_id = ${ur.tenantId}
|
|
63
|
+
ON CONFLICT (user_id, role_id, tenant_id) DO NOTHING
|
|
64
|
+
`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RbacConfig } from './types.js'
|
|
2
|
+
import { assertValidIdentifier } from '../../data/db/identifiers.js'
|
|
3
|
+
|
|
4
|
+
export interface RbacTableNames {
|
|
5
|
+
readonly permissions: string
|
|
6
|
+
readonly roles: string
|
|
7
|
+
readonly rolePermissions: string
|
|
8
|
+
readonly userRoles: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const RBAC_TABLE_DEFAULTS: RbacTableNames = {
|
|
12
|
+
permissions: 'pipework_permissions',
|
|
13
|
+
roles: 'pipework_roles',
|
|
14
|
+
rolePermissions: 'pipework_role_permissions',
|
|
15
|
+
userRoles: 'pipework_user_roles',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveRbacTables(config: RbacConfig): RbacTableNames {
|
|
19
|
+
const tables: RbacTableNames = {
|
|
20
|
+
permissions: config.table?.permissions ?? RBAC_TABLE_DEFAULTS.permissions,
|
|
21
|
+
roles: config.table?.roles ?? RBAC_TABLE_DEFAULTS.roles,
|
|
22
|
+
rolePermissions: config.table?.rolePermissions ?? RBAC_TABLE_DEFAULTS.rolePermissions,
|
|
23
|
+
userRoles: config.table?.userRoles ?? RBAC_TABLE_DEFAULTS.userRoles,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
assertValidIdentifier(tables.permissions, 'permissions table')
|
|
27
|
+
assertValidIdentifier(tables.roles, 'roles table')
|
|
28
|
+
assertValidIdentifier(tables.rolePermissions, 'rolePermissions table')
|
|
29
|
+
assertValidIdentifier(tables.userRoles, 'userRoles table')
|
|
30
|
+
|
|
31
|
+
return tables
|
|
32
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { DB } from '../../data/db/index.js'
|
|
2
|
+
|
|
3
|
+
export type Scope = 'own' | 'team' | 'territory' | 'tenant'
|
|
4
|
+
|
|
5
|
+
export interface RbacConfig {
|
|
6
|
+
readonly database: string
|
|
7
|
+
readonly scopes?: readonly string[]
|
|
8
|
+
readonly cacheTtlMs?: number
|
|
9
|
+
readonly table?: {
|
|
10
|
+
readonly permissions?: string
|
|
11
|
+
readonly roles?: string
|
|
12
|
+
readonly rolePermissions?: string
|
|
13
|
+
readonly userRoles?: string
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ResolvedPermission {
|
|
18
|
+
readonly resource: string
|
|
19
|
+
readonly action: string
|
|
20
|
+
readonly scope: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PermissionSet {
|
|
24
|
+
readonly userId: string
|
|
25
|
+
readonly tenantId: string
|
|
26
|
+
readonly authVersion: number
|
|
27
|
+
readonly permissions: readonly ResolvedPermission[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SeedPermission {
|
|
31
|
+
readonly resource: string
|
|
32
|
+
readonly action: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SeedRolePermission {
|
|
36
|
+
readonly resource: string
|
|
37
|
+
readonly action: string
|
|
38
|
+
readonly scope: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SeedRole {
|
|
42
|
+
readonly name: string
|
|
43
|
+
readonly tenantId: string
|
|
44
|
+
readonly description?: string
|
|
45
|
+
readonly permissions: readonly SeedRolePermission[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SeedUserRole {
|
|
49
|
+
readonly userId: string
|
|
50
|
+
readonly roleName: string
|
|
51
|
+
readonly tenantId: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SeedData {
|
|
55
|
+
readonly permissions: readonly SeedPermission[]
|
|
56
|
+
readonly roles: readonly SeedRole[]
|
|
57
|
+
readonly userRoles?: readonly SeedUserRole[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Rbac {
|
|
61
|
+
resolve(db: DB, userId: string, tenantId: string, authVersion: number): Promise<PermissionSet>
|
|
62
|
+
check(permissions: PermissionSet, resource: string, action: string, requiredScope?: string): boolean
|
|
63
|
+
invalidate(userId: string, tenantId: string): void
|
|
64
|
+
seed(db: DB, data: SeedData): Promise<void>
|
|
65
|
+
}
|