pipework 0.252.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 +5 -0
- 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,315 @@
|
|
|
1
|
+
import { sql } from '../../data/db/sql.js'
|
|
2
|
+
import type { DB } from '../../data/db/index.js'
|
|
3
|
+
import { signPayload, type WebhookSignatureOptions } from './sign.js'
|
|
4
|
+
import { untrustedPost } from '../fetch/handle.js'
|
|
5
|
+
import type { FetchTarget } from '../fetch/handle.js'
|
|
6
|
+
import type { Socket } from 'node:net'
|
|
7
|
+
import { unscopedExecutor, scopedExecutor, requireUnscopedExecutor } from '../../auth/tenant/scoped-db.js'
|
|
8
|
+
|
|
9
|
+
export interface OutboundWebhookConfig {
|
|
10
|
+
table?: string
|
|
11
|
+
deliveryTable?: string
|
|
12
|
+
signing?: WebhookSignatureOptions
|
|
13
|
+
retry?: {
|
|
14
|
+
maxAttempts?: number
|
|
15
|
+
initialDelayMs?: number
|
|
16
|
+
maxDelayMs?: number
|
|
17
|
+
}
|
|
18
|
+
timeoutMs?: number
|
|
19
|
+
/**
|
|
20
|
+
* Where deliveries may go. `'public'` (default): only public addresses; a private, loopback or link-local address, a name
|
|
21
|
+
* resolving to one, or a redirect is refused and recorded on the delivery row's `error` column. `'any'`: for an app that
|
|
22
|
+
* sends webhooks to its own internal services; redirects are still not followed and the kept response is still capped.
|
|
23
|
+
* `'any'` throws at creation when `environment` is `production`.
|
|
24
|
+
*/
|
|
25
|
+
addresses?: 'public' | 'any'
|
|
26
|
+
/** The app's environment (`config.environment`); `addresses: 'any'` is refused when it is `production`. */
|
|
27
|
+
environment?: string
|
|
28
|
+
/** For tests: replaces DNS and the socket of the `'public'` path, as `untrustedFetch.get` takes them. The address check still runs. */
|
|
29
|
+
network?: {
|
|
30
|
+
resolve: (hostname: string) => Promise<string[]>
|
|
31
|
+
connect: (target: FetchTarget) => Socket
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WebhookEndpoint {
|
|
36
|
+
id: string
|
|
37
|
+
tenantId: string
|
|
38
|
+
url: string
|
|
39
|
+
secret: string
|
|
40
|
+
events: string[]
|
|
41
|
+
active: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DeliveryAttempt {
|
|
45
|
+
id: string
|
|
46
|
+
endpointId: string
|
|
47
|
+
tenantId: string
|
|
48
|
+
eventType: string
|
|
49
|
+
payload: unknown
|
|
50
|
+
statusCode: number | null
|
|
51
|
+
responseBody: string | null
|
|
52
|
+
attempt: number
|
|
53
|
+
maxAttempts: number
|
|
54
|
+
nextRetryAt: Date | null
|
|
55
|
+
createdAt: Date
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface OutboundWebhook {
|
|
59
|
+
ensureTables(db: DB): Promise<void>
|
|
60
|
+
send(db: DB, tenantId: string, eventType: string, payload: unknown): Promise<string[]>
|
|
61
|
+
deliver(db: DB, deliveryId: string): Promise<{ success: boolean; statusCode: number | null }>
|
|
62
|
+
processPending(db: DB): Promise<number>
|
|
63
|
+
registerEndpoint(db: DB, endpoint: Omit<WebhookEndpoint, 'id' | 'events'> & { events: readonly string[] }): Promise<string>
|
|
64
|
+
listEndpoints(db: DB, tenantId: string): Promise<WebhookEndpoint[]>
|
|
65
|
+
removeEndpoint(db: DB, tenantId: string, endpointId: string): Promise<boolean>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The delivery table `createOutboundWebhook` uses unless `deliveryTable` is set. */
|
|
69
|
+
export const DEFAULT_WEBHOOK_DELIVERY_TABLE = '__pipework_webhook_deliveries'
|
|
70
|
+
|
|
71
|
+
const DEFAULTS = {
|
|
72
|
+
table: '__pipework_webhook_endpoints',
|
|
73
|
+
deliveryTable: DEFAULT_WEBHOOK_DELIVERY_TABLE,
|
|
74
|
+
maxAttempts: 5,
|
|
75
|
+
initialDelayMs: 1000,
|
|
76
|
+
maxDelayMs: 3_600_000,
|
|
77
|
+
timeoutMs: 30_000,
|
|
78
|
+
} as const
|
|
79
|
+
|
|
80
|
+
/** The most of an endpoint's response kept on the delivery row. */
|
|
81
|
+
const RESPONSE_BODY_CAP = 64 * 1024
|
|
82
|
+
|
|
83
|
+
/** What a refused or failed attempt is recorded as on the delivery row's `error` column. */
|
|
84
|
+
type AttemptError = 'address' | 'scheme' | 'redirects' | 'network' | 'timeout' | 'too_large' | 'status' | 'rejected'
|
|
85
|
+
|
|
86
|
+
interface Attempt { statusCode: number | null; responseBody: string | null; success: boolean; error: AttemptError | null }
|
|
87
|
+
|
|
88
|
+
/** Plain fetch, for `addresses: 'any'`: no redirect followed, the kept body capped, one time limit. */
|
|
89
|
+
async function sendAnywhere(url: string, headers: Record<string, string>, body: string, timeoutMs: number): Promise<Attempt> {
|
|
90
|
+
const controller = new AbortController()
|
|
91
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(url, { method: 'POST', headers, body, signal: controller.signal, redirect: 'manual' })
|
|
94
|
+
const statusCode = response.status
|
|
95
|
+
if (statusCode >= 300 && statusCode < 400) {
|
|
96
|
+
await response.body?.cancel().catch(() => undefined)
|
|
97
|
+
return { statusCode, responseBody: null, success: false, error: 'redirects' }
|
|
98
|
+
}
|
|
99
|
+
const success = statusCode >= 200 && statusCode < 300
|
|
100
|
+
const chunks: Buffer[] = []
|
|
101
|
+
let total = 0
|
|
102
|
+
let over = false
|
|
103
|
+
const reader = response.body?.getReader()
|
|
104
|
+
try {
|
|
105
|
+
while (reader !== undefined) {
|
|
106
|
+
const { done, value } = await reader.read()
|
|
107
|
+
if (done) break
|
|
108
|
+
total += value.length
|
|
109
|
+
if (total > RESPONSE_BODY_CAP) { over = true; break }
|
|
110
|
+
chunks.push(Buffer.from(value))
|
|
111
|
+
}
|
|
112
|
+
} catch { /* a body that breaks is not kept; the status decides */ }
|
|
113
|
+
await reader?.cancel().catch(() => undefined)
|
|
114
|
+
return { statusCode, responseBody: over ? null : Buffer.concat(chunks).toString('utf8'), success, error: null }
|
|
115
|
+
} catch {
|
|
116
|
+
return { statusCode: null, responseBody: null, success: false, error: controller.signal.aborted ? 'timeout' : 'network' }
|
|
117
|
+
} finally {
|
|
118
|
+
clearTimeout(timer)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createOutboundWebhook(config: OutboundWebhookConfig = {}): OutboundWebhook {
|
|
123
|
+
const endpointTable = config.table ?? DEFAULTS.table
|
|
124
|
+
const deliveryTable = config.deliveryTable ?? DEFAULTS.deliveryTable
|
|
125
|
+
const maxAttempts = config.retry?.maxAttempts ?? DEFAULTS.maxAttempts
|
|
126
|
+
const initialDelayMs = config.retry?.initialDelayMs ?? DEFAULTS.initialDelayMs
|
|
127
|
+
const maxDelayMs = config.retry?.maxDelayMs ?? DEFAULTS.maxDelayMs
|
|
128
|
+
const timeoutMs = config.timeoutMs ?? DEFAULTS.timeoutMs
|
|
129
|
+
const signingOptions = config.signing ?? {}
|
|
130
|
+
const addresses = config.addresses ?? 'public'
|
|
131
|
+
if (addresses === 'any' && config.environment === 'production') {
|
|
132
|
+
throw new Error(
|
|
133
|
+
'[pipework] webhook.createOutbound: `addresses: \'any\'` is refused when the environment is production, because a tenant-registered URL could then reach the app\'s private network (cloud metadata, internal services). '
|
|
134
|
+
+ 'Fix: remove `addresses` so deliveries go to public addresses only, or set it only outside production.\n',
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
async ensureTables(handle) {
|
|
140
|
+
const db = unscopedExecutor(handle).db
|
|
141
|
+
await db.execute(sql.raw(`
|
|
142
|
+
CREATE TABLE IF NOT EXISTS ${endpointTable} (
|
|
143
|
+
id TEXT NOT NULL PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
144
|
+
tenant_id TEXT NOT NULL,
|
|
145
|
+
url TEXT NOT NULL,
|
|
146
|
+
secret TEXT NOT NULL,
|
|
147
|
+
events TEXT[] NOT NULL DEFAULT '{}',
|
|
148
|
+
active BOOLEAN NOT NULL DEFAULT true,
|
|
149
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
150
|
+
)
|
|
151
|
+
`))
|
|
152
|
+
|
|
153
|
+
await db.execute(sql.raw(`
|
|
154
|
+
CREATE TABLE IF NOT EXISTS ${deliveryTable} (
|
|
155
|
+
id TEXT NOT NULL PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
156
|
+
endpoint_id TEXT NOT NULL,
|
|
157
|
+
tenant_id TEXT NOT NULL,
|
|
158
|
+
event_type TEXT NOT NULL,
|
|
159
|
+
payload JSONB NOT NULL,
|
|
160
|
+
status_code INTEGER,
|
|
161
|
+
response_body TEXT,
|
|
162
|
+
error TEXT,
|
|
163
|
+
attempt INTEGER NOT NULL DEFAULT 0,
|
|
164
|
+
max_attempts INTEGER NOT NULL,
|
|
165
|
+
next_retry_at TIMESTAMPTZ,
|
|
166
|
+
completed_at TIMESTAMPTZ,
|
|
167
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
168
|
+
)
|
|
169
|
+
`))
|
|
170
|
+
await db.execute(sql.raw(`ALTER TABLE ${deliveryTable} ADD COLUMN IF NOT EXISTS error TEXT`))
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
async send(handle, tenantId, eventType, payload) {
|
|
174
|
+
const db = scopedExecutor(handle, tenantId, 'webhook.send()').db
|
|
175
|
+
const endpoints: unknown[] = await db.execute(
|
|
176
|
+
sql`SELECT id, url, secret FROM ${sql.raw(endpointTable)}
|
|
177
|
+
WHERE tenant_id = ${tenantId}
|
|
178
|
+
AND active = true
|
|
179
|
+
AND (events = '{}' OR ${eventType} = ANY(events))`,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
const deliveryIds: string[] = []
|
|
183
|
+
for (const row of endpoints as { id: string }[]) {
|
|
184
|
+
const result: unknown[] = await db.execute(
|
|
185
|
+
sql`INSERT INTO ${sql.raw(deliveryTable)} (endpoint_id, tenant_id, event_type, payload, max_attempts, next_retry_at)
|
|
186
|
+
VALUES (${row.id}, ${tenantId}, ${eventType}, ${JSON.stringify(payload)}, ${maxAttempts}, now())
|
|
187
|
+
RETURNING id`,
|
|
188
|
+
)
|
|
189
|
+
const inserted = (result as { id: string }[])[0]
|
|
190
|
+
if (inserted !== undefined) deliveryIds.push(inserted.id)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return deliveryIds
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
async deliver(handle, deliveryId) {
|
|
197
|
+
const db = requireUnscopedExecutor(handle, 'webhook.deliver()', 'a delivery is looked up by id across tenants')
|
|
198
|
+
const rows: unknown[] = await db.execute(
|
|
199
|
+
sql`SELECT d.id, d.endpoint_id, d.event_type, d.payload, d.attempt, d.max_attempts,
|
|
200
|
+
e.url, e.secret
|
|
201
|
+
FROM ${sql.raw(deliveryTable)} d
|
|
202
|
+
JOIN ${sql.raw(endpointTable)} e ON e.id = d.endpoint_id
|
|
203
|
+
WHERE d.id = ${deliveryId} AND d.completed_at IS NULL`,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const delivery = (rows as Record<string, unknown>[])[0]
|
|
207
|
+
if (delivery === undefined) return { success: false, statusCode: null }
|
|
208
|
+
|
|
209
|
+
const attempt = (delivery['attempt'] as number) + 1
|
|
210
|
+
const signed = signPayload(delivery['payload'], delivery['secret'] as string, signingOptions)
|
|
211
|
+
|
|
212
|
+
const headers = {
|
|
213
|
+
'Content-Type': 'application/json',
|
|
214
|
+
'X-Webhook-Id': signed.messageId,
|
|
215
|
+
'X-Webhook-Timestamp': signed.timestamp.toString(),
|
|
216
|
+
'X-Webhook-Signature': signed.signature,
|
|
217
|
+
}
|
|
218
|
+
const url = delivery['url'] as string
|
|
219
|
+
let outcome: Attempt
|
|
220
|
+
if (addresses === 'any') {
|
|
221
|
+
outcome = await sendAnywhere(url, headers, signed.body, timeoutMs)
|
|
222
|
+
} else {
|
|
223
|
+
const sent = await untrustedPost(url, {
|
|
224
|
+
body: signed.body, headers, maxBytes: RESPONSE_BODY_CAP, timeoutMs, maxRedirects: 0,
|
|
225
|
+
...(config.network ?? {}),
|
|
226
|
+
})
|
|
227
|
+
if (sent.ok) {
|
|
228
|
+
outcome = { statusCode: sent.status, responseBody: Buffer.from(sent.body).toString('utf8'), success: true, error: null }
|
|
229
|
+
} else if (sent.status !== undefined) {
|
|
230
|
+
// The answer arrived; the body is not kept (non-2xx, or a 2xx body over the cap).
|
|
231
|
+
outcome = { statusCode: sent.status, responseBody: null, success: sent.status >= 200 && sent.status < 300, error: null }
|
|
232
|
+
} else {
|
|
233
|
+
outcome = { statusCode: null, responseBody: null, success: false, error: sent.reason }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const { statusCode, responseBody, success, error } = outcome
|
|
237
|
+
|
|
238
|
+
if (success || attempt >= (delivery['max_attempts'] as number)) {
|
|
239
|
+
await db.execute(
|
|
240
|
+
sql`UPDATE ${sql.raw(deliveryTable)}
|
|
241
|
+
SET attempt = ${attempt}, status_code = ${statusCode},
|
|
242
|
+
response_body = ${responseBody}, error = ${error}, completed_at = now(), next_retry_at = NULL
|
|
243
|
+
WHERE id = ${deliveryId}`,
|
|
244
|
+
)
|
|
245
|
+
} else {
|
|
246
|
+
const delayMs = Math.min(initialDelayMs * Math.pow(2, attempt - 1), maxDelayMs)
|
|
247
|
+
const nextRetry = new Date(Date.now() + delayMs)
|
|
248
|
+
await db.execute(
|
|
249
|
+
sql`UPDATE ${sql.raw(deliveryTable)}
|
|
250
|
+
SET attempt = ${attempt}, status_code = ${statusCode},
|
|
251
|
+
response_body = ${responseBody}, error = ${error}, next_retry_at = ${nextRetry.toISOString()}
|
|
252
|
+
WHERE id = ${deliveryId}`,
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { success, statusCode }
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
async processPending(handle) {
|
|
260
|
+
const db = requireUnscopedExecutor(handle, 'webhook.processPending()', 'it delivers every tenant\'s pending webhooks')
|
|
261
|
+
const rows: unknown[] = await db.execute(
|
|
262
|
+
sql`SELECT id FROM ${sql.raw(deliveryTable)}
|
|
263
|
+
WHERE completed_at IS NULL AND next_retry_at <= now()
|
|
264
|
+
ORDER BY next_retry_at ASC
|
|
265
|
+
LIMIT 100`,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
let processed = 0
|
|
269
|
+
for (const row of rows as { id: string }[]) {
|
|
270
|
+
await this.deliver(db, row.id)
|
|
271
|
+
processed++
|
|
272
|
+
}
|
|
273
|
+
return processed
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
async registerEndpoint(handle, endpoint) {
|
|
277
|
+
const db = scopedExecutor(handle, endpoint.tenantId, 'webhook.registerEndpoint()').db
|
|
278
|
+
const rows: unknown[] = await db.execute(
|
|
279
|
+
sql`INSERT INTO ${sql.raw(endpointTable)} (tenant_id, url, secret, events, active)
|
|
280
|
+
VALUES (${endpoint.tenantId}, ${endpoint.url}, ${endpoint.secret},
|
|
281
|
+
${`{${endpoint.events.join(',')}}`}, ${endpoint.active})
|
|
282
|
+
RETURNING id`,
|
|
283
|
+
)
|
|
284
|
+
return ((rows as { id: string }[])[0])!.id
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
async listEndpoints(handle, tenantId) {
|
|
288
|
+
const db = scopedExecutor(handle, tenantId, 'webhook.listEndpoints()').db
|
|
289
|
+
const rows: unknown[] = await db.execute(
|
|
290
|
+
sql`SELECT id, tenant_id, url, secret, events, active
|
|
291
|
+
FROM ${sql.raw(endpointTable)}
|
|
292
|
+
WHERE tenant_id = ${tenantId}
|
|
293
|
+
ORDER BY created_at ASC`,
|
|
294
|
+
)
|
|
295
|
+
return (rows as Record<string, unknown>[]).map(row => ({
|
|
296
|
+
id: row['id'] as string,
|
|
297
|
+
tenantId: row['tenant_id'] as string,
|
|
298
|
+
url: row['url'] as string,
|
|
299
|
+
secret: row['secret'] as string,
|
|
300
|
+
events: row['events'] as string[],
|
|
301
|
+
active: row['active'] as boolean,
|
|
302
|
+
}))
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
async removeEndpoint(handle, tenantId, endpointId) {
|
|
306
|
+
const db = scopedExecutor(handle, tenantId, 'webhook.removeEndpoint()').db
|
|
307
|
+
const rows: unknown[] = await db.execute(
|
|
308
|
+
sql`DELETE FROM ${sql.raw(endpointTable)}
|
|
309
|
+
WHERE id = ${endpointId} AND tenant_id = ${tenantId}
|
|
310
|
+
RETURNING id`,
|
|
311
|
+
)
|
|
312
|
+
return (rows as unknown[]).length > 0
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHmac, randomUUID } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
export interface WebhookSignatureOptions {
|
|
4
|
+
algorithm?: string
|
|
5
|
+
encoding?: 'hex' | 'base64'
|
|
6
|
+
/** Message id to sign and return; default a fresh UUID. Pass a stable id so a retry is recognisable as the same message. */
|
|
7
|
+
messageId?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SignedPayload {
|
|
11
|
+
body: string
|
|
12
|
+
signature: string
|
|
13
|
+
timestamp: number
|
|
14
|
+
messageId: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function signPayload(
|
|
18
|
+
payload: unknown,
|
|
19
|
+
secret: string,
|
|
20
|
+
options: WebhookSignatureOptions = {},
|
|
21
|
+
): SignedPayload {
|
|
22
|
+
const algorithm = options.algorithm ?? 'sha256'
|
|
23
|
+
const encoding = options.encoding ?? 'hex'
|
|
24
|
+
|
|
25
|
+
const messageId = options.messageId ?? randomUUID()
|
|
26
|
+
const timestamp = Math.floor(Date.now() / 1000)
|
|
27
|
+
const body = typeof payload === 'string' ? payload : JSON.stringify(payload)
|
|
28
|
+
const toSign = `${messageId}.${timestamp}.${body}`
|
|
29
|
+
const signature = createHmac(algorithm, secret).update(toSign).digest(encoding)
|
|
30
|
+
|
|
31
|
+
return { body, signature, timestamp, messageId }
|
|
32
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
export interface SignatureVerifier {
|
|
4
|
+
verify(rawBody: Buffer, headers: Readonly<Record<string, string | undefined>>): boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type LazySecret = string | (() => string)
|
|
8
|
+
|
|
9
|
+
function resolveSecret(secret: LazySecret): string {
|
|
10
|
+
return typeof secret === 'function' ? secret() : secret
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function hmacVerifier(options: {
|
|
14
|
+
secret: LazySecret
|
|
15
|
+
algorithm?: string
|
|
16
|
+
header: string
|
|
17
|
+
prefix?: string
|
|
18
|
+
encoding?: 'hex' | 'base64'
|
|
19
|
+
computePayload?: (rawBody: Buffer, headers: Readonly<Record<string, string | undefined>>) => Buffer
|
|
20
|
+
}): SignatureVerifier {
|
|
21
|
+
const algorithm = options.algorithm ?? 'sha256'
|
|
22
|
+
const encoding = options.encoding ?? 'hex'
|
|
23
|
+
const prefix = options.prefix ?? ''
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
verify(rawBody, headers) {
|
|
27
|
+
const headerValue = headers[options.header.toLowerCase()]
|
|
28
|
+
if (headerValue === undefined || headerValue === '') return false
|
|
29
|
+
|
|
30
|
+
const payload = options.computePayload !== undefined
|
|
31
|
+
? options.computePayload(rawBody, headers)
|
|
32
|
+
: rawBody
|
|
33
|
+
|
|
34
|
+
const expected = prefix + createHmac(algorithm, resolveSecret(options.secret)).update(payload).digest(encoding)
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return timingSafeEqual(Buffer.from(headerValue), Buffer.from(expected))
|
|
38
|
+
} catch {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function stripeVerifier(secret: LazySecret): SignatureVerifier {
|
|
46
|
+
return {
|
|
47
|
+
verify(rawBody, headers) {
|
|
48
|
+
const sigHeader = headers['stripe-signature']
|
|
49
|
+
if (sigHeader === undefined) return false
|
|
50
|
+
|
|
51
|
+
const parts = new Map<string, string>()
|
|
52
|
+
for (const pair of sigHeader.split(',')) {
|
|
53
|
+
const eqIdx = pair.indexOf('=')
|
|
54
|
+
if (eqIdx === -1) continue
|
|
55
|
+
parts.set(pair.slice(0, eqIdx).trim(), pair.slice(eqIdx + 1).trim())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const timestamp = parts.get('t')
|
|
59
|
+
const v1Sig = parts.get('v1')
|
|
60
|
+
if (timestamp === undefined || v1Sig === undefined) return false
|
|
61
|
+
|
|
62
|
+
const signedPayload = Buffer.from(`${timestamp}.${rawBody.toString('utf-8')}`)
|
|
63
|
+
const expected = createHmac('sha256', resolveSecret(secret)).update(signedPayload).digest('hex')
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
return timingSafeEqual(Buffer.from(v1Sig), Buffer.from(expected))
|
|
67
|
+
} catch {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function githubVerifier(secret: LazySecret): SignatureVerifier {
|
|
75
|
+
return hmacVerifier({
|
|
76
|
+
secret,
|
|
77
|
+
algorithm: 'sha256',
|
|
78
|
+
header: 'x-hub-signature-256',
|
|
79
|
+
prefix: 'sha256=',
|
|
80
|
+
encoding: 'hex',
|
|
81
|
+
})
|
|
82
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class InvariantViolation extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
public readonly invariant: string,
|
|
4
|
+
public readonly context: Record<string, unknown>,
|
|
5
|
+
) {
|
|
6
|
+
const details = Object.entries(context)
|
|
7
|
+
.map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`)
|
|
8
|
+
.join('\n')
|
|
9
|
+
super(
|
|
10
|
+
`[pipework] Invariant violation: ${invariant}\n\n` +
|
|
11
|
+
` Context:\n${details}\n\n` +
|
|
12
|
+
` This is a framework bug. Please report it.\n`
|
|
13
|
+
)
|
|
14
|
+
this.name = 'TrunkInvariantViolation'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function invariant(
|
|
19
|
+
condition: boolean,
|
|
20
|
+
name: string,
|
|
21
|
+
context: Record<string, unknown> = {},
|
|
22
|
+
): asserts condition {
|
|
23
|
+
if (!condition) {
|
|
24
|
+
throw new InvariantViolation(name, context)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { invariant, InvariantViolation } from './assert.js'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import tseslint from '@typescript-eslint/eslint-plugin'
|
|
2
|
+
import tsparser from '@typescript-eslint/parser'
|
|
3
|
+
import { noReexports } from './rules/no-reexports.js'
|
|
4
|
+
|
|
5
|
+
export const rules = {
|
|
6
|
+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
7
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
8
|
+
'@typescript-eslint/no-floating-promises': 'error',
|
|
9
|
+
'@typescript-eslint/no-misused-promises': 'error',
|
|
10
|
+
'@typescript-eslint/require-await': 'error',
|
|
11
|
+
'@typescript-eslint/no-unnecessary-condition': 'error',
|
|
12
|
+
'@typescript-eslint/strict-boolean-expressions': 'error',
|
|
13
|
+
'pipework/no-reexports': 'error',
|
|
14
|
+
} as const
|
|
15
|
+
|
|
16
|
+
export interface LintOptions {
|
|
17
|
+
readonly tsconfigRootDir?: string
|
|
18
|
+
readonly include?: readonly string[]
|
|
19
|
+
readonly ignores?: readonly string[]
|
|
20
|
+
readonly extraRules?: Readonly<Record<string, unknown>>
|
|
21
|
+
readonly extraPlugins?: Readonly<Record<string, unknown>>
|
|
22
|
+
readonly extraConfigs?: readonly unknown[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createLintConfig(config?: LintOptions): unknown[] {
|
|
26
|
+
const files = config?.include ?? ['src/**/*.ts', 'src/**/*.tsx']
|
|
27
|
+
const parserOptions: Record<string, unknown> = { projectService: true }
|
|
28
|
+
if (config?.tsconfigRootDir !== undefined) {
|
|
29
|
+
parserOptions['tsconfigRootDir'] = config.tsconfigRootDir
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const appliedRules = { ...rules, ...config?.extraRules }
|
|
33
|
+
const appliedPlugins: Record<string, unknown> = {
|
|
34
|
+
'@typescript-eslint': tseslint,
|
|
35
|
+
'pipework': { rules: { 'no-reexports': noReexports } },
|
|
36
|
+
...config?.extraPlugins,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const blocks: unknown[] = [
|
|
40
|
+
{
|
|
41
|
+
files: [...files],
|
|
42
|
+
languageOptions: {
|
|
43
|
+
parser: tsparser,
|
|
44
|
+
parserOptions,
|
|
45
|
+
},
|
|
46
|
+
plugins: appliedPlugins,
|
|
47
|
+
rules: appliedRules,
|
|
48
|
+
},
|
|
49
|
+
...(config?.extraConfigs ?? []),
|
|
50
|
+
{
|
|
51
|
+
ignores: ['dist/', 'node_modules/', ...(config?.ignores ?? [])],
|
|
52
|
+
},
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
return blocks
|
|
56
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { join, relative } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export interface DoctrineViolation {
|
|
5
|
+
readonly rule: string
|
|
6
|
+
readonly file: string
|
|
7
|
+
readonly line: number
|
|
8
|
+
readonly message: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DoctrineResult {
|
|
12
|
+
readonly violations: readonly DoctrineViolation[]
|
|
13
|
+
readonly scanned: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validateDoctrine(cwd: string, sourceFiles: readonly string[]): DoctrineResult {
|
|
17
|
+
const violations: DoctrineViolation[] = []
|
|
18
|
+
let scanned = 0
|
|
19
|
+
|
|
20
|
+
for (const file of sourceFiles) {
|
|
21
|
+
const abs = join(cwd, file)
|
|
22
|
+
if (!existsSync(abs)) continue
|
|
23
|
+
|
|
24
|
+
const content = readFileSync(abs, 'utf-8')
|
|
25
|
+
const lines = content.split('\n')
|
|
26
|
+
scanned++
|
|
27
|
+
|
|
28
|
+
checkSchemaTableUsage(file, lines, violations)
|
|
29
|
+
checkRawPgTableImport(file, lines, violations)
|
|
30
|
+
checkUnbrandedPrimaryKeys(file, lines, violations)
|
|
31
|
+
checkHandWrittenTraitFields(file, lines, violations)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { violations, scanned }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SCHEMA_TABLE_PATTERN = /\bschema\.table\s*\(/
|
|
38
|
+
const PIPE_DEFINE_FILE_PATTERN = /\bpipe\.define\s*\(/
|
|
39
|
+
|
|
40
|
+
function checkSchemaTableUsage(
|
|
41
|
+
file: string,
|
|
42
|
+
lines: readonly string[],
|
|
43
|
+
violations: DoctrineViolation[],
|
|
44
|
+
): void {
|
|
45
|
+
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
const line = lines[i]!
|
|
47
|
+
if (SCHEMA_TABLE_PATTERN.test(line)) {
|
|
48
|
+
violations.push({
|
|
49
|
+
rule: 'define/prefer-pipe-define',
|
|
50
|
+
file,
|
|
51
|
+
line: i + 1,
|
|
52
|
+
message: 'Use pipe.define() instead of schema.table(). pipe.define() provides branded types, shape validators, test factories, and tenant isolation.',
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const RAW_PG_TABLE_IMPORT_LINE = /import\s+.*\bpgTable\b.*from\s+/
|
|
59
|
+
|
|
60
|
+
function checkRawPgTableImport(
|
|
61
|
+
file: string,
|
|
62
|
+
lines: readonly string[],
|
|
63
|
+
violations: DoctrineViolation[],
|
|
64
|
+
): void {
|
|
65
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66
|
+
const line = lines[i]!
|
|
67
|
+
if (RAW_PG_TABLE_IMPORT_LINE.test(line)) {
|
|
68
|
+
violations.push({
|
|
69
|
+
rule: 'define/no-raw-pgTable',
|
|
70
|
+
file,
|
|
71
|
+
line: i + 1,
|
|
72
|
+
message: 'Raw pgTable() import — use pipe.define() instead. pipe.define() returns a DefinedTable that IS a PgTable, with branded types that flow through select().from() and eq().',
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const PRIMARY_KEY_PATTERN = /\.primaryKey\(\)/
|
|
79
|
+
const BRAND_PATTERN = /\.brand\(/
|
|
80
|
+
const PIPE_FIELD_UUID_PATTERN = /pipe\.field\.(uuid|serial)\(\)/
|
|
81
|
+
|
|
82
|
+
function checkUnbrandedPrimaryKeys(
|
|
83
|
+
file: string,
|
|
84
|
+
lines: readonly string[],
|
|
85
|
+
violations: DoctrineViolation[],
|
|
86
|
+
): void {
|
|
87
|
+
if (!lines.some(l => PIPE_DEFINE_FILE_PATTERN.test(l))) return
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < lines.length; i++) {
|
|
90
|
+
const line = lines[i]!
|
|
91
|
+
if (PIPE_FIELD_UUID_PATTERN.test(line) && PRIMARY_KEY_PATTERN.test(line) && !BRAND_PATTERN.test(line)) {
|
|
92
|
+
violations.push({
|
|
93
|
+
rule: 'define/brand-primary-key',
|
|
94
|
+
file,
|
|
95
|
+
line: i + 1,
|
|
96
|
+
message: 'Primary key field missing .brand(). Add .brand(\'EntityNameId\') for nominal type safety.',
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const TRAIT_FIELD_PATTERNS: ReadonlyArray<{ pattern: RegExp; trait: string; field: string }> = [
|
|
103
|
+
{ pattern: /\bcreatedAt\s*:\s*pipe\.field\.timestamp\(\)/, trait: '.audited()', field: 'createdAt' },
|
|
104
|
+
{ pattern: /\bupdatedAt\s*:\s*pipe\.field\.timestamp\(\)/, trait: '.audited()', field: 'updatedAt' },
|
|
105
|
+
{ pattern: /\beffectiveFrom\s*:\s*pipe\.field\.timestamp\(\)/, trait: '.effectiveDated()', field: 'effectiveFrom' },
|
|
106
|
+
{ pattern: /\beffectiveTo\s*:\s*pipe\.field\.timestamp\(\)/, trait: '.effectiveDated()', field: 'effectiveTo' },
|
|
107
|
+
{ pattern: /\bdeletedAt\s*:\s*pipe\.field\.timestamp\(\)/, trait: '.softDeleted()', field: 'deletedAt' },
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
function checkHandWrittenTraitFields(
|
|
111
|
+
file: string,
|
|
112
|
+
lines: readonly string[],
|
|
113
|
+
violations: DoctrineViolation[],
|
|
114
|
+
): void {
|
|
115
|
+
if (!lines.some(l => PIPE_DEFINE_FILE_PATTERN.test(l))) return
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < lines.length; i++) {
|
|
118
|
+
const line = lines[i]!
|
|
119
|
+
for (const { pattern, trait, field } of TRAIT_FIELD_PATTERNS) {
|
|
120
|
+
if (pattern.test(line)) {
|
|
121
|
+
violations.push({
|
|
122
|
+
rule: 'define/use-traits',
|
|
123
|
+
file,
|
|
124
|
+
line: i + 1,
|
|
125
|
+
message: `Hand-written '${field}' field — use ${trait} trait instead. Traits are auto-managed and excluded from insert/update shapes.`,
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function findSourceFiles(cwd: string, modulePaths: readonly string[]): string[] {
|
|
133
|
+
const files: string[] = []
|
|
134
|
+
|
|
135
|
+
for (const modPath of modulePaths) {
|
|
136
|
+
const srcDir = join(cwd, modPath, 'src')
|
|
137
|
+
if (!existsSync(srcDir)) continue
|
|
138
|
+
collectTsFiles(srcDir, cwd, files)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return files.sort()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A `.ts` or `.tsx` source: not a test file, not a declaration file. */
|
|
145
|
+
export function isSourceFileName(name: string): boolean {
|
|
146
|
+
if (name.endsWith('.d.ts')) return false
|
|
147
|
+
if (name.endsWith('.test.ts') || name.endsWith('.test.tsx')) return false
|
|
148
|
+
return name.endsWith('.ts') || name.endsWith('.tsx')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function collectTsFiles(dir: string, cwd: string, out: string[]): void {
|
|
152
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
153
|
+
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
const full = join(dir, entry.name)
|
|
156
|
+
if (entry.isDirectory()) {
|
|
157
|
+
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '__tests__') continue
|
|
158
|
+
collectTsFiles(full, cwd, out)
|
|
159
|
+
} else if (isSourceFileName(entry.name)) {
|
|
160
|
+
out.push(relative(cwd, full))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|