pipework 0.252.0 → 0.253.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/infra/support/handle.d.ts +8 -1
- package/dist/infra/support/handle.d.ts.map +1 -1
- package/dist/infra/support/handle.js +9 -2
- package/dist/infra/support/handle.js.map +1 -1
- package/dist/infra/support/namespace.d.ts.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 +869 -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,192 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from 'node:crypto'
|
|
2
|
+
import { Builder } from '../../request/di/builder.js'
|
|
3
|
+
import type { Handler } from '../../request/di/types.js'
|
|
4
|
+
import { schema } from '../../data/schema/namespace.js'
|
|
5
|
+
import { PipeworkError, NotFoundError, UnauthorizedError } from '../../core/errors/index.js'
|
|
6
|
+
import type { Alerts, AlertSeverity, AlertStatus } from './handle.js'
|
|
7
|
+
|
|
8
|
+
export interface AlertsHttpOptions {
|
|
9
|
+
/** Route prefix, e.g. '/ops/alerts'. No trailing slash. */
|
|
10
|
+
readonly path: string
|
|
11
|
+
/** Service credentials, name -> secret. A request sends `Authorization: Bearer <secret>`; the matching name is the default `by`. At least one; secrets at least 32 chars. */
|
|
12
|
+
readonly credentials: Readonly<Record<string, string>>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class BadRequestError extends PipeworkError {
|
|
16
|
+
readonly code = 'BAD_REQUEST' as const
|
|
17
|
+
readonly httpStatus = 400 as const
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const STATUSES: readonly AlertStatus[] = ['open', 'acknowledged', 'resolved']
|
|
21
|
+
const SEVERITIES: readonly AlertSeverity[] = ['info', 'warning', 'error', 'critical']
|
|
22
|
+
|
|
23
|
+
const sha = (s: string): Buffer => createHash('sha256').update(s).digest()
|
|
24
|
+
|
|
25
|
+
function authenticate(credentials: readonly (readonly [string, Buffer])[], header: string | undefined): string {
|
|
26
|
+
const match = header !== undefined ? /^Bearer (.+)$/.exec(header) : null
|
|
27
|
+
const given = sha(match?.[1] ?? '')
|
|
28
|
+
let name: string | null = null
|
|
29
|
+
for (const [credName, digest] of credentials) {
|
|
30
|
+
if (timingSafeEqual(given, digest) && match !== null && name === null) name = credName
|
|
31
|
+
}
|
|
32
|
+
if (name === null) throw new UnauthorizedError('A valid service credential is required: send Authorization: Bearer <secret>.')
|
|
33
|
+
return name
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function bad(field: string, problem: string): never {
|
|
37
|
+
throw new BadRequestError(`Invalid ${field}: ${problem}.`, { field })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function optString(source: unknown, field: string): string | undefined {
|
|
41
|
+
const value = (source as Record<string, unknown> | null | undefined)?.[field]
|
|
42
|
+
if (value === undefined) return undefined
|
|
43
|
+
if (typeof value !== 'string') bad(field, 'must be a string')
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function body(raw: unknown): Record<string, unknown> {
|
|
48
|
+
if (raw === undefined || raw === null) return {}
|
|
49
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) bad('body', 'must be a JSON object')
|
|
50
|
+
return raw as Record<string, unknown>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function csv<T extends string>(raw: string | undefined, field: string, allowed: readonly T[]): T[] | undefined {
|
|
54
|
+
if (raw === undefined || raw === '') return undefined
|
|
55
|
+
const out: T[] = []
|
|
56
|
+
for (const part of raw.split(',')) {
|
|
57
|
+
if (!(allowed as readonly string[]).includes(part)) bad(field, `"${part}" is not one of ${allowed.join(', ')}`)
|
|
58
|
+
out.push(part as T)
|
|
59
|
+
}
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ownerList(raw: string | undefined): string[] | undefined {
|
|
64
|
+
if (raw === undefined || raw === '') return undefined
|
|
65
|
+
const parts = raw.split(',').map(p => p.trim())
|
|
66
|
+
if (parts.some(p => p === '')) bad('owner', 'must be a comma list of non-empty names')
|
|
67
|
+
return parts
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function limitOf(raw: string | undefined): number | undefined {
|
|
71
|
+
if (raw === undefined) return undefined
|
|
72
|
+
if (!/^[1-9][0-9]*$/.test(raw)) bad('limit', 'must be a positive integer')
|
|
73
|
+
return Number(raw)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function pageQuery(query: unknown): { limit?: number; cursor?: string } {
|
|
77
|
+
const limit = optString(query, 'limit')
|
|
78
|
+
const cursor = optString(query, 'cursor')
|
|
79
|
+
const out: { limit?: number; cursor?: string } = {}
|
|
80
|
+
const n = limitOf(limit)
|
|
81
|
+
if (n !== undefined) out.limit = n
|
|
82
|
+
if (cursor !== undefined) out.cursor = cursor
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateOptions(options: AlertsHttpOptions): void {
|
|
87
|
+
if (!options.path.startsWith('/') || options.path.length < 2 || options.path.endsWith('/')) {
|
|
88
|
+
throw new Error(`alerts.http: path "${options.path}" must start with "/" and have no trailing slash, e.g. "/ops/alerts".`)
|
|
89
|
+
}
|
|
90
|
+
const entries = Object.entries(options.credentials)
|
|
91
|
+
if (entries.length === 0) {
|
|
92
|
+
throw new Error('alerts.http: credentials needs at least one entry, name -> secret (the secret is at least 32 characters).')
|
|
93
|
+
}
|
|
94
|
+
for (const [name, secret] of entries) {
|
|
95
|
+
if (typeof secret !== 'string' || secret.length < 32) {
|
|
96
|
+
throw new Error(`alerts.http: the secret for credential "${name}" is under 32 characters; use a long random value.`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Routes for an operations agent, under `options.path` (all JSON, no session, no tenant):
|
|
103
|
+
* GET list (`status`, `severity`, `owner` comma lists, `limit`, `cursor`; `owner` filters on the current owner), GET `/totals`, GET `/health`,
|
|
104
|
+
* GET `/:key` (an alert keyed `totals` or `health` is not reachable here), GET `/:key/occurrences`, and POST `/:key/acknowledge` (optional `owner` in the body replaces the alert's owner and is kept in its `ownerHistory`), `/:key/resolve`, `/:key/link`.
|
|
105
|
+
* `:key` is URL-encoded. Every request needs `Authorization: Bearer <secret>` matching one of
|
|
106
|
+
* `credentials`; the credential's name is the default `by` for acknowledge and resolve.
|
|
107
|
+
* Reads cross tenants (the handle reads through `pipe.system()`). Resolve never notifies:
|
|
108
|
+
* the agent does not hear its own change. Dates are ISO strings. A bad value is 400 naming the field.
|
|
109
|
+
*/
|
|
110
|
+
export function alertsHttp(handle: Alerts, options: AlertsHttpOptions): Handler<Record<string, unknown>, unknown>[] {
|
|
111
|
+
validateOptions(options)
|
|
112
|
+
const credentials = Object.entries(options.credentials).map(([name, secret]) => [name, sha(secret)] as const)
|
|
113
|
+
const base = options.path
|
|
114
|
+
const any = schema.check.unknown()
|
|
115
|
+
|
|
116
|
+
const get = (path: string) => new Builder().public().request().query(any).params(any).route('GET', path)
|
|
117
|
+
const post = (path: string) => new Builder().public().request().query(any).params(any).input(any).route('POST', path)
|
|
118
|
+
|
|
119
|
+
// Dates inside the results serialize through JSON.stringify as ISO strings.
|
|
120
|
+
const keyOf = (params: unknown): string => String((params as { key: string }).key)
|
|
121
|
+
const known = async (key: string): Promise<void> => {
|
|
122
|
+
if ((await handle.get(key)) === null) throw new NotFoundError(`Alert "${key}"`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [
|
|
126
|
+
get(base).fit(async ({ request, query }) => {
|
|
127
|
+
authenticate(credentials, request.headers['authorization'])
|
|
128
|
+
const q: { status?: AlertStatus[]; severity?: AlertSeverity[]; owner?: string[]; limit?: number; cursor?: string } = pageQuery(query)
|
|
129
|
+
const status = csv(optString(query, 'status'), 'status', STATUSES)
|
|
130
|
+
const severity = csv(optString(query, 'severity'), 'severity', SEVERITIES)
|
|
131
|
+
if (status !== undefined) q.status = status
|
|
132
|
+
if (severity !== undefined) q.severity = severity
|
|
133
|
+
const owner = ownerList(optString(query, 'owner'))
|
|
134
|
+
if (owner !== undefined) q.owner = owner
|
|
135
|
+
return handle.list(q)
|
|
136
|
+
}),
|
|
137
|
+
get(`${base}/totals`).fit(async ({ request }) => {
|
|
138
|
+
authenticate(credentials, request.headers['authorization'])
|
|
139
|
+
return handle.totals()
|
|
140
|
+
}),
|
|
141
|
+
get(`${base}/health`).fit(async ({ request }) => {
|
|
142
|
+
authenticate(credentials, request.headers['authorization'])
|
|
143
|
+
return handle.health()
|
|
144
|
+
}),
|
|
145
|
+
get(`${base}/:key`).fit(async ({ request, params }) => {
|
|
146
|
+
authenticate(credentials, request.headers['authorization'])
|
|
147
|
+
const found = await handle.get(keyOf(params))
|
|
148
|
+
if (found === null) throw new NotFoundError(`Alert "${keyOf(params)}"`)
|
|
149
|
+
return found
|
|
150
|
+
}),
|
|
151
|
+
get(`${base}/:key/occurrences`).fit(async ({ request, params, query }) => {
|
|
152
|
+
authenticate(credentials, request.headers['authorization'])
|
|
153
|
+
const page = pageQuery(query)
|
|
154
|
+
await known(keyOf(params))
|
|
155
|
+
return handle.occurrences(keyOf(params), page)
|
|
156
|
+
}),
|
|
157
|
+
post(`${base}/:key/acknowledge`).fit(async ({ request, params, input }) => {
|
|
158
|
+
const name = authenticate(credentials, request.headers['authorization'])
|
|
159
|
+
const b = body(input)
|
|
160
|
+
const by = optString(b, 'by') ?? name
|
|
161
|
+
const note = optString(b, 'note')
|
|
162
|
+
const owner = optString(b, 'owner')
|
|
163
|
+
if (owner !== undefined && owner.trim() === '') bad('owner', 'must not be empty')
|
|
164
|
+
return {
|
|
165
|
+
acknowledged: await handle.acknowledge(keyOf(params), {
|
|
166
|
+
by, ...(note === undefined ? {} : { note }), ...(owner === undefined ? {} : { owner }),
|
|
167
|
+
}),
|
|
168
|
+
}
|
|
169
|
+
}),
|
|
170
|
+
post(`${base}/:key/resolve`).fit(async ({ request, params, input }) => {
|
|
171
|
+
const name = authenticate(credentials, request.headers['authorization'])
|
|
172
|
+
const b = body(input)
|
|
173
|
+
const by = optString(b, 'by') ?? name
|
|
174
|
+
const note = optString(b, 'note')
|
|
175
|
+
const release = optString(b, 'release')
|
|
176
|
+
return {
|
|
177
|
+
resolved: await handle.resolve(keyOf(params), {
|
|
178
|
+
by, notify: false,
|
|
179
|
+
...(note === undefined ? {} : { note }),
|
|
180
|
+
...(release === undefined ? {} : { release }),
|
|
181
|
+
}),
|
|
182
|
+
}
|
|
183
|
+
}),
|
|
184
|
+
post(`${base}/:key/link`).fit(async ({ request, params, input }) => {
|
|
185
|
+
authenticate(credentials, request.headers['authorization'])
|
|
186
|
+
const url = optString(body(input), 'url')
|
|
187
|
+
if (url === undefined) bad('url', 'is required')
|
|
188
|
+
if (!/^https?:\/\/[^\s]+$/i.test(url)) bad('url', 'must be an http or https URL')
|
|
189
|
+
return { linked: await handle.link(keyOf(params), url) }
|
|
190
|
+
}),
|
|
191
|
+
]
|
|
192
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { createAlerts } from './handle.js'
|
|
2
|
+
export type {
|
|
3
|
+
Alerts, AlertsConfig, Alert, OwnerChange, AlertSeverity, AlertSubject, RaiseOptions, DeliveryRow, TickResult,
|
|
4
|
+
AlertStatus, JsonValue, AlertOccurrence, Page, AlertTotals, DeliveryHealth, ResolveOptions,
|
|
5
|
+
} from './handle.js'
|
|
6
|
+
export type { AlertSink, AlertDelivery, AlertPayload, AlertEventPayload, AlertDigestPayload, AlertWebhookConfig, MemoryAlertSink } from './sinks.js'
|
|
7
|
+
export type { AlertSource, AlertCondition } from './sources.js'
|
|
8
|
+
export { alertsHttp } from './http.js'
|
|
9
|
+
export type { AlertsHttpOptions } from './http.js'
|
|
10
|
+
export { clientFaults } from './client-faults.js'
|
|
11
|
+
export type { ClientFaultsOptions, ClientFaultReport } from './client-faults.js'
|
|
12
|
+
export type { RequestFaultsOptions } from './request-faults.js'
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Sources over the failures that mail, storage, access links and sign-in locks record but nothing used to read.
|
|
2
|
+
// Each is a composition of `querySource` and `newRowsSource`; a source is opt-in, so its tables exist only
|
|
3
|
+
// when the blueprint key named in its JSDoc is set.
|
|
4
|
+
|
|
5
|
+
import { sql } from '../../data/db/sql.js'
|
|
6
|
+
import { pipeSystem } from '../../data/db/db.js'
|
|
7
|
+
import { common, positive, text, FIFTEEN_MIN, HOUR, type Basics } from './source-options.js'
|
|
8
|
+
import { composeSources, newRowsSource, querySource, type AlertSource } from './sources.js'
|
|
9
|
+
|
|
10
|
+
const nullable = (v: unknown): string | null => v === null || v === undefined ? null : String(v)
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Needs blueprint `email: {}`. Raises `email-failed:outbound` and `email-failed:inbound` for messages marked
|
|
14
|
+
* `failed` (`failed_at`) since the last tick (clears by the handle's quiet period); `email-stuck:sending` while
|
|
15
|
+
* a message has been `sending` more than `stuckAfterMs` (default 15 min); `email-stuck:queued` while one has been
|
|
16
|
+
* `queued` more than `queuedAfterMs` (default 1 h), which means nothing drains the send queue;
|
|
17
|
+
* `email-inbound-stuck:received` while an inbound message has been `received` more than `receivedAfterMs`
|
|
18
|
+
* (default 15 min) without being handled; and `email-raw-undrained:all` while raw inbound messages are more than
|
|
19
|
+
* `sweepGraceMs` (default 1 h) past `raw_expires_at` and not deleted, which means the raw sweep is not running.
|
|
20
|
+
* Addresses and subjects are never put in a key, message or context.
|
|
21
|
+
*/
|
|
22
|
+
export function emailSource(options: Basics & { stuckAfterMs?: number; queuedAfterMs?: number; receivedAfterMs?: number; sweepGraceMs?: number; settleMs?: number } = {}): AlertSource {
|
|
23
|
+
const who = 'alerts.source.email'
|
|
24
|
+
const stuckAfter = positive(options.stuckAfterMs ?? FIFTEEN_MIN, 'stuckAfterMs', who)
|
|
25
|
+
const queuedAfter = positive(options.queuedAfterMs ?? HOUR, 'queuedAfterMs', who)
|
|
26
|
+
const receivedAfter = positive(options.receivedAfterMs ?? FIFTEEN_MIN, 'receivedAfterMs', who)
|
|
27
|
+
const grace = positive(options.sweepGraceMs ?? HOUR, 'sweepGraceMs', who)
|
|
28
|
+
const failedRows = (direction: 'outbound' | 'inbound', table: string): AlertSource => newRowsSource({
|
|
29
|
+
...common(options),
|
|
30
|
+
name: 'email-failed',
|
|
31
|
+
stateKey: `email-failed:${direction}`,
|
|
32
|
+
table,
|
|
33
|
+
at: 'failed_at',
|
|
34
|
+
groupBy: sql.raw(`'${direction}'`),
|
|
35
|
+
...(options.settleMs !== undefined ? { settleMs: options.settleMs } : {}),
|
|
36
|
+
message: g => `${String(g.count)} ${direction} email(s) failed; last error: ${text(g.last['error'])}`,
|
|
37
|
+
context: g => ({ direction, count: g.count, lastError: nullable(g.last['error']) }),
|
|
38
|
+
})
|
|
39
|
+
const stuck = querySource({
|
|
40
|
+
...common(options),
|
|
41
|
+
name: 'email-stuck',
|
|
42
|
+
query: sql`
|
|
43
|
+
SELECT 'sending' AS kind, count(*)::int AS n, (extract(epoch FROM clock_timestamp() - min(sending_at)) * 1000)::bigint AS oldest_ms
|
|
44
|
+
FROM pipework_email_messages
|
|
45
|
+
WHERE status = 'sending' AND sending_at < clock_timestamp() - ${stuckAfter} * interval '1 millisecond'
|
|
46
|
+
HAVING count(*) > 0
|
|
47
|
+
UNION ALL
|
|
48
|
+
SELECT 'queued', count(*)::int, (extract(epoch FROM clock_timestamp() - min(created_at)) * 1000)::bigint
|
|
49
|
+
FROM pipework_email_messages
|
|
50
|
+
WHERE status = 'queued' AND created_at < clock_timestamp() - ${queuedAfter} * interval '1 millisecond'
|
|
51
|
+
HAVING count(*) > 0`,
|
|
52
|
+
id: r => String(r['kind']),
|
|
53
|
+
message: r => String(r['kind']) === 'sending'
|
|
54
|
+
? `${String(r['n'])} email(s) stuck sending for more than ${String(stuckAfter)} ms (oldest ${String(r['oldest_ms'])} ms)`
|
|
55
|
+
: `${String(r['n'])} email(s) queued for more than ${String(queuedAfter)} ms (oldest ${String(r['oldest_ms'])} ms); is the send queue being drained?`,
|
|
56
|
+
context: r => ({ count: Number(r['n']), oldestMs: Number(r['oldest_ms']) }),
|
|
57
|
+
})
|
|
58
|
+
const received = querySource({
|
|
59
|
+
...common(options),
|
|
60
|
+
name: 'email-inbound-stuck',
|
|
61
|
+
query: sql`
|
|
62
|
+
SELECT count(*)::int AS n, (extract(epoch FROM clock_timestamp() - min(received_at)) * 1000)::bigint AS oldest_ms
|
|
63
|
+
FROM pipework_email_inbound
|
|
64
|
+
WHERE status = 'received' AND received_at < clock_timestamp() - ${receivedAfter} * interval '1 millisecond'
|
|
65
|
+
HAVING count(*) > 0`,
|
|
66
|
+
id: () => 'received',
|
|
67
|
+
message: r => `${String(r['n'])} inbound email(s) received more than ${String(receivedAfter)} ms ago and not handled (oldest ${String(r['oldest_ms'])} ms)`,
|
|
68
|
+
context: r => ({ count: Number(r['n']), oldestMs: Number(r['oldest_ms']) }),
|
|
69
|
+
})
|
|
70
|
+
const raw = querySource({
|
|
71
|
+
...common(options),
|
|
72
|
+
name: 'email-raw-undrained',
|
|
73
|
+
query: sql`
|
|
74
|
+
SELECT count(*)::int AS n FROM pipework_email_inbound
|
|
75
|
+
WHERE raw_deleted_at IS NULL AND raw_expires_at < clock_timestamp() - ${grace} * interval '1 millisecond'
|
|
76
|
+
HAVING count(*) > 0`,
|
|
77
|
+
id: () => 'all',
|
|
78
|
+
message: r => `${String(r['n'])} raw inbound message(s) past their retention and not deleted; is the inbound sweep scheduled?`,
|
|
79
|
+
context: r => ({ count: Number(r['n']) }),
|
|
80
|
+
})
|
|
81
|
+
return composeSources('email', [
|
|
82
|
+
failedRows('outbound', 'pipework_email_messages'), failedRows('inbound', 'pipework_email_inbound'), stuck, received, raw,
|
|
83
|
+
])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Needs blueprint `storage: {}`. Raises `storage-expiry-undrained:all` while `storage_file` rows are more than
|
|
88
|
+
* `sweepGraceMs` (default 1 h) past `expires_at` and still exist (`sweep()` deletes them), which means the
|
|
89
|
+
* storage sweep is not scheduled.
|
|
90
|
+
*/
|
|
91
|
+
export function storageSource(options: Basics & { sweepGraceMs?: number } = {}): AlertSource {
|
|
92
|
+
const grace = positive(options.sweepGraceMs ?? HOUR, 'sweepGraceMs', 'alerts.source.storage')
|
|
93
|
+
return querySource({
|
|
94
|
+
...common(options),
|
|
95
|
+
name: 'storage-expiry-undrained',
|
|
96
|
+
query: sql`
|
|
97
|
+
SELECT count(*)::int AS n, (extract(epoch FROM clock_timestamp() - min(expires_at)) * 1000)::bigint AS oldest_ms
|
|
98
|
+
FROM storage_file
|
|
99
|
+
WHERE expires_at IS NOT NULL AND expires_at < clock_timestamp() - ${grace} * interval '1 millisecond'
|
|
100
|
+
HAVING count(*) > 0`,
|
|
101
|
+
id: () => 'all',
|
|
102
|
+
message: r => `${String(r['n'])} expired file(s) not swept (oldest ${String(r['oldest_ms'])} ms past expiry); is the storage sweep scheduled?`,
|
|
103
|
+
context: r => ({ count: Number(r['n']), oldestMs: Number(r['oldest_ms']) }),
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Needs blueprint `accessLinks: {}`. Raises `access-link-abandoned:<tenantId>` while a tenant has uses still
|
|
109
|
+
* `pending` more than `graceMs` (default 15 min) after `reserved_until`: the reservation was taken and never
|
|
110
|
+
* completed or released.
|
|
111
|
+
*/
|
|
112
|
+
export function accessLinksSource(options: Basics & { graceMs?: number } = {}): AlertSource {
|
|
113
|
+
const grace = positive(options.graceMs ?? FIFTEEN_MIN, 'graceMs', 'alerts.source.accessLinks')
|
|
114
|
+
return querySource({
|
|
115
|
+
...common(options),
|
|
116
|
+
name: 'access-link-abandoned',
|
|
117
|
+
query: sql`
|
|
118
|
+
SELECT tenant_id, count(*)::int AS n, (extract(epoch FROM clock_timestamp() - min(reserved_until)) * 1000)::bigint AS oldest_ms
|
|
119
|
+
FROM access_link_use
|
|
120
|
+
WHERE status = 'pending' AND reserved_until < clock_timestamp() - ${grace} * interval '1 millisecond'
|
|
121
|
+
GROUP BY tenant_id ORDER BY tenant_id`,
|
|
122
|
+
id: r => String(r['tenant_id']),
|
|
123
|
+
message: r => `${String(r['n'])} access link use(s) for ${String(r['tenant_id'])} reserved and never finished (oldest ${String(r['oldest_ms'])} ms past reservation)`,
|
|
124
|
+
subject: r => ({ tenantId: String(r['tenant_id']) }),
|
|
125
|
+
context: r => ({ count: Number(r['n']), oldestMs: Number(r['oldest_ms']) }),
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const LOCKOUT_TABLES = ['pipework_signin_attempts', 'pipework_guard_attempts'] as const
|
|
130
|
+
type LockoutTable = typeof LOCKOUT_TABLES[number]
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Raises `auth-lockout:<table>` (default severity `warning`) while rows of that table have `locked_until` in the
|
|
134
|
+
* future, with the count in the message and context. One condition per table, never per account: no account,
|
|
135
|
+
* address or key is named. `pipework_signin_attempts` needs blueprint `passwords: {}`, `pipework_guard_attempts`
|
|
136
|
+
* needs `guards: {}`. `tables` overrides; when omitted, the tables that exist are resolved once, on the first
|
|
137
|
+
* evaluate, and it throws if neither does.
|
|
138
|
+
*/
|
|
139
|
+
export function lockoutsSource(options: Basics & { tables?: readonly LockoutTable[] } = {}): AlertSource {
|
|
140
|
+
if (options.tables !== undefined) {
|
|
141
|
+
if (options.tables.length === 0) throw new Error('[pipework] alerts.source.lockouts tables must not be empty; omit it to watch the lockout tables that exist.')
|
|
142
|
+
for (const t of options.tables) {
|
|
143
|
+
if (!LOCKOUT_TABLES.includes(t)) throw new Error(`[pipework] alerts.source.lockouts table must be one of ${LOCKOUT_TABLES.join(', ')}, got ${String(t)}.`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const build = (tables: readonly LockoutTable[]): AlertSource => {
|
|
147
|
+
const selects = [...new Set(tables)].map(t => `SELECT '${t}' AS tbl, count(*)::int AS n FROM ${t} WHERE locked_until > clock_timestamp() HAVING count(*) > 0`)
|
|
148
|
+
return querySource({
|
|
149
|
+
...common(options),
|
|
150
|
+
severity: options.severity ?? 'warning',
|
|
151
|
+
name: 'auth-lockout',
|
|
152
|
+
query: sql.raw(selects.join(' UNION ALL ')),
|
|
153
|
+
id: r => String(r['tbl']),
|
|
154
|
+
message: r => `${String(r['n'])} lockout(s) active on ${String(r['tbl'])}`,
|
|
155
|
+
context: r => ({ table: String(r['tbl']), count: Number(r['n']) }),
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
if (options.tables !== undefined) return build(options.tables)
|
|
159
|
+
let resolved: Promise<AlertSource> | undefined
|
|
160
|
+
const resolve = async (): Promise<AlertSource> => {
|
|
161
|
+
const rows = await pipeSystem(options.database).execute(sql`
|
|
162
|
+
SELECT to_regclass('pipework_signin_attempts') IS NOT NULL AS a, to_regclass('pipework_guard_attempts') IS NOT NULL AS b`)
|
|
163
|
+
const found = LOCKOUT_TABLES.filter((_, i) => rows[0]?.[i === 0 ? 'a' : 'b'] === true)
|
|
164
|
+
if (found.length === 0) {
|
|
165
|
+
throw new Error('[pipework] alerts.source.lockouts found neither pipework_signin_attempts nor pipework_guard_attempts; enable `passwords: {}` and/or `guards: {}` in the blueprint, or drop this source.')
|
|
166
|
+
}
|
|
167
|
+
return build(found)
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
name: 'auth-lockout',
|
|
171
|
+
owns: key => key.startsWith('auth-lockout:'),
|
|
172
|
+
async evaluate() {
|
|
173
|
+
if (resolved === undefined) {
|
|
174
|
+
resolved = resolve()
|
|
175
|
+
resolved.catch(() => { resolved = undefined })
|
|
176
|
+
}
|
|
177
|
+
return (await resolved).evaluate()
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createAlerts } from './handle.js'
|
|
2
|
+
import { alertsHttp } from './http.js'
|
|
3
|
+
import { clientFaults } from './client-faults.js'
|
|
4
|
+
import { webhookSink, memorySink } from './sinks.js'
|
|
5
|
+
import { requestFaultsHook } from './request-faults.js'
|
|
6
|
+
import { deadLetterSource, longRunningSource, backlogSource, healthSource, quotaExhaustedSource, unitFaultSource, loopStalledSource, loopCheck, querySource, newRowsSource } from './sources.js'
|
|
7
|
+
import { recordFault, recordFaultFor, useFaultTarget, faultsSource } from './faults.js'
|
|
8
|
+
import { recordBeatFor } from './beat.js'
|
|
9
|
+
import { deliveryFailedSource, webhookFailedSource, creditsSource } from './subsystem-sources.js'
|
|
10
|
+
import { emailSource, storageSource, accessLinksSource, lockoutsSource } from './infra-sources.js'
|
|
11
|
+
import { schedulesSource, stepsSource, pipelinesSource, tracesSource } from './async-sources.js'
|
|
12
|
+
|
|
13
|
+
/** Alerts namespace — raise and resolve keyed conditions, grouped, delivered to sinks through an outbox, traced. */
|
|
14
|
+
export const alerts = {
|
|
15
|
+
/** Creates an alerts handle. */
|
|
16
|
+
create: createAlerts,
|
|
17
|
+
/** Routes for an operations agent: list, get, totals, delivery health, acknowledge, resolve, link; bearer credentials. */
|
|
18
|
+
http: alertsHttp,
|
|
19
|
+
/** One POST route that records failures reported by browser code as a trace step and an alert; answers 204, no session needed. Send with `reportClientFault` from `pipework/client`. */
|
|
20
|
+
clientFaults,
|
|
21
|
+
/** An `onInternalError` hook for the HTTP server: raises `request-fault:<METHOD> <route>` for each internal 500. */
|
|
22
|
+
requestFaults: requestFaultsHook,
|
|
23
|
+
/** A sink that POSTs the alert as signed JSON to a URL. */
|
|
24
|
+
webhook: webhookSink,
|
|
25
|
+
/** A sink that keeps deliveries in `.delivered`, for tests. */
|
|
26
|
+
memory: memorySink,
|
|
27
|
+
/** A `/ready` health check named `loops`: unhealthy while a background loop (the tick included) has not run or succeeded within its allowance. */
|
|
28
|
+
loopCheck,
|
|
29
|
+
/** Records a failure your code swallowed (logged and carried on) so `alerts.source.faults` can raise it; `void alerts.recordFault('my-kind', message, context?)`. Best effort, never throws, writes outside the caller's transaction. Does nothing without `alerts` in the blueprint. */
|
|
30
|
+
recordFault,
|
|
31
|
+
/** `recordFault` for code that holds the manifold and may run outside any Flow: `void alerts.recordFaultFor(manifold, kind, message, context?)`. */
|
|
32
|
+
recordFaultFor,
|
|
33
|
+
/** Registers the manifold (or a `DB`, or a function returning one) that `alerts.recordFault` writes on outside any Flow; call once at boot in a process that is not a pipework surface, and keep the release for shutdown. A manifold without `alerts` registers nothing. */
|
|
34
|
+
useFaultTarget,
|
|
35
|
+
/** Records one iteration of a background loop of your own for `alerts.source.loopStalled`: `void alerts.recordBeat(manifold, 'my-loop', { ok, error?, everyMs })`. Works outside any Flow; does nothing without `alerts`. */
|
|
36
|
+
recordBeat: recordBeatFor,
|
|
37
|
+
source: {
|
|
38
|
+
/** Raises `job-dead-letter:<jobType>` for jobs that ran out of attempts. */
|
|
39
|
+
deadLetter: deadLetterSource,
|
|
40
|
+
/** Raises `job-long-running:<jobType>` while a job has been claimed too long. */
|
|
41
|
+
longRunning: longRunningSource,
|
|
42
|
+
/** Raises `queue-backlog:<jobType>` while too many jobs wait or the oldest has waited too long. */
|
|
43
|
+
backlog: backlogSource,
|
|
44
|
+
/** Raises `quota-exhausted:<key>` while an upstream quota is at or under its floor or refused a call. */
|
|
45
|
+
quotaExhausted: quotaExhaustedSource,
|
|
46
|
+
/** Raises `health:<name>` while a health check fails. */
|
|
47
|
+
health: healthSource,
|
|
48
|
+
/** Raises `unit-fault:<unit>` while a systemd unit has stopped abnormally within `window` (rows written by `pipework alerts unit-fault`). */
|
|
49
|
+
unitFault: unitFaultSource,
|
|
50
|
+
/** Raises `${name}:${id}` for each row a query returns while the condition holds, and resolves what it stops returning. */
|
|
51
|
+
query: querySource,
|
|
52
|
+
/** Raises `${name}:${id}` per group of rows recorded in a table since the last tick (a cursor, settled against late commits). */
|
|
53
|
+
newRows: newRowsSource,
|
|
54
|
+
/** Raises `delivery-failed:<table>:<sink>` for outbox rows that gave up and `delivery-stuck:<table>:<sink>` for rows pending too long. */
|
|
55
|
+
deliveryFailed: deliveryFailedSource,
|
|
56
|
+
/** Raises `webhook-failed:<endpointId>` for outbound webhook deliveries that gave up and `webhook-stalled:<endpointId>` for ones overdue for a retry. */
|
|
57
|
+
webhookFailed: webhookFailedSource,
|
|
58
|
+
/** Raises `credit-overdraft:<tenantId>`, `credit-held:<tenantId>` and `credit-expiry-undrained:all` over the credits tables. */
|
|
59
|
+
credits: creditsSource,
|
|
60
|
+
/** Raises `email-failed:<direction>`, `email-stuck:<sending|queued>`, `email-inbound-stuck:received` and `email-raw-undrained:all` over the mail tables. */
|
|
61
|
+
email: emailSource,
|
|
62
|
+
/** Raises `storage-expiry-undrained:all` while expired files are not swept. */
|
|
63
|
+
storage: storageSource,
|
|
64
|
+
/** Raises `access-link-abandoned:<tenantId>` while link uses stay reserved and never finish. */
|
|
65
|
+
accessLinks: accessLinksSource,
|
|
66
|
+
/** Raises `auth-lockout:<table>` (warning) while sign-in or guard lockouts are active; counts only, no accounts. */
|
|
67
|
+
lockouts: lockoutsSource,
|
|
68
|
+
/** Raises `schedule-overdue:<name>` while a cron schedule's `next_run_at` is long past, which means no scheduler is firing it. */
|
|
69
|
+
schedules: schedulesSource,
|
|
70
|
+
/** Raises `step-run-failed:<graph>`, `step-run-unnotified:<graph>` and `step-task-waiting:<graph>` and `step-run-stalled:<graph>` over the step graph tables. */
|
|
71
|
+
steps: stepsSource,
|
|
72
|
+
/** Raises `pipeline-failed:<pipeline>` for failed runs `pipeline-waiting:<pipeline>` for runs waiting too long and `pipeline-stalled:<pipeline>` for runs still running too long. */
|
|
73
|
+
pipelines: pipelinesSource,
|
|
74
|
+
/** Raises `trace-expired:<entryKind>` for traces force-closed by the expiry sweep and `trace-expiry-undrained:all` while expired open traces are not being swept. */
|
|
75
|
+
traces: tracesSource,
|
|
76
|
+
/** Raises `fault:<kind>` per kind of swallowed failure recorded by `recordFault` since the last tick (the pipework subsystems record audit-write, quota-record, credits-callback, trace-steps-lost, queue-* and scheduler faults). `kinds` limits it. */
|
|
77
|
+
faults: faultsSource,
|
|
78
|
+
/** Raises `loop-stalled:<name>` while a background loop (scheduler, reaper, claim loop, support drain) has not run or succeeded within its allowance; the tick itself is covered by `pipework alerts stale`. */
|
|
79
|
+
loopStalled: loopStalledSource,
|
|
80
|
+
},
|
|
81
|
+
} as const
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// An email address: local part, "@", and a domain with at least one dot.
|
|
2
|
+
const EMAIL = /[A-Za-z0-9._%+-]+@(?:[A-Za-z0-9-]+\.)+[A-Za-z0-9-]+/gi
|
|
3
|
+
|
|
4
|
+
const global = (p: RegExp): RegExp => (p.flags.includes('g') ? p : new RegExp(p.source, `${p.flags}g`))
|
|
5
|
+
|
|
6
|
+
/** Email addresses become `[email]`, then each extra pattern becomes `[redacted]`. The rest of the text stays. */
|
|
7
|
+
export function redactText(text: string, extra: readonly RegExp[] = []): string {
|
|
8
|
+
let out = text.replace(EMAIL, '[email]')
|
|
9
|
+
for (const p of extra) out = out.replace(global(p), '[redacted]')
|
|
10
|
+
return out
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** `redactText` on every string inside a JSON value, at any depth. */
|
|
14
|
+
export function redactJson<T>(value: T, extra: readonly RegExp[] = []): T {
|
|
15
|
+
if (typeof value === 'string') return redactText(value, extra) as T
|
|
16
|
+
if (Array.isArray(value)) return value.map(v => redactJson(v, extra)) as T
|
|
17
|
+
if (value !== null && typeof value === 'object') {
|
|
18
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactJson(v, extra)])) as T
|
|
19
|
+
}
|
|
20
|
+
return value
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Alerts, AlertSeverity } from './handle.js'
|
|
2
|
+
import { describeError } from '../../core/logging/error-chain.js'
|
|
3
|
+
import type { InternalErrorFault } from '../../request/http/types.js'
|
|
4
|
+
|
|
5
|
+
export interface RequestFaultsOptions {
|
|
6
|
+
readonly owner?: string
|
|
7
|
+
readonly runbook?: string
|
|
8
|
+
/** The owner and runbook for one fault, over `owner` and `runbook`: an undefined field or result keeps them. */
|
|
9
|
+
readonly assign?: (fault: InternalErrorFault) => { readonly owner?: string; readonly runbook?: string } | undefined
|
|
10
|
+
/** Default `'error'`. */
|
|
11
|
+
readonly severity?: AlertSeverity
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* An `onInternalError` hook for the HTTP server: raises `request-fault:<METHOD> <route pattern>`
|
|
16
|
+
* with the error message (its causes' text folded in) and a context of method, route, requestId and traceId, and the cause chain when there is one; the subject is the
|
|
17
|
+
* request's tenant when it has one. Repeats of one route group into one alert.
|
|
18
|
+
*/
|
|
19
|
+
export function requestFaultsHook(handle: Alerts, options: RequestFaultsOptions = {}): (fault: InternalErrorFault) => Promise<void> {
|
|
20
|
+
return async (fault) => {
|
|
21
|
+
const { message: detail, chain } = describeError(fault.error)
|
|
22
|
+
const assigned = options.assign?.(fault)
|
|
23
|
+
const owner = assigned?.owner ?? options.owner
|
|
24
|
+
const runbook = assigned?.runbook ?? options.runbook
|
|
25
|
+
await handle.raise(`request-fault:${fault.method} ${fault.route}`, {
|
|
26
|
+
severity: options.severity ?? 'error',
|
|
27
|
+
message: `${fault.method} ${fault.route}: ${detail}`,
|
|
28
|
+
...(fault.tenantId !== null ? { subject: { tenantId: fault.tenantId } } : {}),
|
|
29
|
+
...(owner !== undefined ? { owner } : {}),
|
|
30
|
+
...(runbook !== undefined ? { runbook } : {}),
|
|
31
|
+
context: {
|
|
32
|
+
method: fault.method,
|
|
33
|
+
route: fault.route,
|
|
34
|
+
requestId: fault.requestId,
|
|
35
|
+
traceId: fault.traceId,
|
|
36
|
+
message: detail,
|
|
37
|
+
...(chain.length > 0 ? { cause: chain.map((l) => ({ ...l })) } : {}),
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// The tables behind alerts: one row per alert key, one per raise (occurrences), the outbox of deliveries (one row per
|
|
2
|
+
// event and sink), and the cursor a source keeps between evaluations. Not tenant-scoped.
|
|
3
|
+
|
|
4
|
+
import { define } from '../../data/domain/index.js'
|
|
5
|
+
import { field } from '../../data/domain/field.js'
|
|
6
|
+
import { schema } from '../../data/schema/namespace.js'
|
|
7
|
+
import { index } from '../../data/db/idx/index.js'
|
|
8
|
+
import { sql } from '../../data/db/sql.js'
|
|
9
|
+
|
|
10
|
+
export const ALERT_STATUSES = ['open', 'acknowledged', 'resolved'] as const
|
|
11
|
+
export const ALERT_SEVERITIES = ['info', 'warning', 'error', 'critical'] as const
|
|
12
|
+
export const ALERT_DELIVERY_KINDS = ['raise', 'repeat', 'resolve', 'reopen', 'digest'] as const
|
|
13
|
+
|
|
14
|
+
export const alerts = define('pipework_alerts', {
|
|
15
|
+
key: field.text().primaryKey(),
|
|
16
|
+
status: field.enum(ALERT_STATUSES).default('open'),
|
|
17
|
+
severity: field.enum(ALERT_SEVERITIES),
|
|
18
|
+
message: field.text(),
|
|
19
|
+
subject: field.jsonb(schema.check.json()).nullable(),
|
|
20
|
+
owner: field.text().nullable(),
|
|
21
|
+
ownerHistory: field.jsonb(schema.check.json()).default([]),
|
|
22
|
+
runbook: field.text().nullable(),
|
|
23
|
+
context: field.jsonb(schema.check.json()).nullable(),
|
|
24
|
+
count: field.integer().default(1),
|
|
25
|
+
firstSeenAt: field.timestamp().default('now'),
|
|
26
|
+
lastSeenAt: field.timestamp().default('now'),
|
|
27
|
+
lastSentAt: field.timestamp().nullable(),
|
|
28
|
+
resolvedAt: field.timestamp().nullable(),
|
|
29
|
+
reopenCount: field.integer().default(0),
|
|
30
|
+
reopenedAt: field.timestamp().nullable(),
|
|
31
|
+
acknowledgedAt: field.timestamp().nullable(),
|
|
32
|
+
acknowledgedBy: field.text().nullable(),
|
|
33
|
+
resolvedBy: field.text().nullable(),
|
|
34
|
+
resolveNote: field.text().nullable(),
|
|
35
|
+
resolvedRelease: field.text().nullable(),
|
|
36
|
+
issueUrl: field.text().nullable(),
|
|
37
|
+
traceIds: field.custom(schema.col.text('trace_ids').array(), schema.check.array(schema.check.string())).default('{}'),
|
|
38
|
+
resendAfterMs: field.integer().nullable(),
|
|
39
|
+
resolveAfterQuietMs: field.integer().nullable(),
|
|
40
|
+
updatedAt: field.timestamp().default('now'),
|
|
41
|
+
}, {
|
|
42
|
+
indexes: (t) => ({
|
|
43
|
+
byStatus: index('idx_pipework_alerts_status').on(t['status']),
|
|
44
|
+
}),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// One row per raise, kept for `occurrenceRetentionMs`.
|
|
48
|
+
export const alertOccurrences = define('pipework_alert_occurrences', {
|
|
49
|
+
id: field.uuid().defaultRandom().primaryKey(),
|
|
50
|
+
alertKey: field.text(),
|
|
51
|
+
at: field.timestamp().default('now'),
|
|
52
|
+
severity: field.enum(ALERT_SEVERITIES),
|
|
53
|
+
message: field.text(),
|
|
54
|
+
context: field.jsonb(schema.check.json()).nullable(),
|
|
55
|
+
traceId: field.text().nullable(),
|
|
56
|
+
}, {
|
|
57
|
+
indexes: (t) => ({
|
|
58
|
+
byKey: index('idx_pipework_alert_occurrences_key').on(t['alertKey'], t['at'], t['id']),
|
|
59
|
+
byAt: index('idx_pipework_alert_occurrences_at').on(t['at']),
|
|
60
|
+
}),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
export const alertDeliveries = define('pipework_alert_deliveries', {
|
|
64
|
+
id: field.uuid().defaultRandom().primaryKey(),
|
|
65
|
+
alertKey: field.text(),
|
|
66
|
+
kind: field.enum(ALERT_DELIVERY_KINDS),
|
|
67
|
+
sink: field.text(),
|
|
68
|
+
payload: field.jsonb(schema.check.json()),
|
|
69
|
+
attempts: field.integer().default(0),
|
|
70
|
+
nextAttemptAt: field.timestamp().default('now'),
|
|
71
|
+
deliveredAt: field.timestamp().nullable(),
|
|
72
|
+
failedAt: field.timestamp().nullable(),
|
|
73
|
+
lastError: field.text().nullable(),
|
|
74
|
+
createdAt: field.timestamp().default('now'),
|
|
75
|
+
}, {
|
|
76
|
+
indexes: (t) => ({
|
|
77
|
+
due: index('idx_pipework_alert_deliveries_due')
|
|
78
|
+
.on(t['nextAttemptAt'])
|
|
79
|
+
.where(sql.raw('delivered_at IS NULL AND failed_at IS NULL')),
|
|
80
|
+
byKey: index('idx_pipework_alert_deliveries_key').on(t['alertKey']),
|
|
81
|
+
}),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
export const alertSourceState = define('pipework_alert_source_state', {
|
|
85
|
+
name: field.text().primaryKey(),
|
|
86
|
+
cursorAt: field.timestamp(),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// One row per abnormal stop of a systemd unit, written by `pipework alerts unit-fault` from the unit's ExecStopPost.
|
|
90
|
+
export const unitFaults = define('pipework_unit_faults', {
|
|
91
|
+
id: field.uuid().defaultRandom().primaryKey(),
|
|
92
|
+
unit: field.text(),
|
|
93
|
+
result: field.text(),
|
|
94
|
+
exitCode: field.text().nullable(),
|
|
95
|
+
exitStatus: field.text().nullable(),
|
|
96
|
+
at: field.timestamp().default('now'),
|
|
97
|
+
}, {
|
|
98
|
+
indexes: (t) => ({
|
|
99
|
+
byUnit: index('idx_pipework_unit_faults_unit_at').on(t['unit'], t['at']),
|
|
100
|
+
}),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// One row per background loop, upserted at the end of each iteration; `pipework alerts stale`, the loop-stalled source and the loops check read it.
|
|
104
|
+
export const loopBeats = define('pipework_loop_beats', {
|
|
105
|
+
name: field.text().primaryKey(),
|
|
106
|
+
lastAt: field.timestamp(),
|
|
107
|
+
lastOkAt: field.timestamp().nullable(),
|
|
108
|
+
lastError: field.text().nullable(),
|
|
109
|
+
expectEveryMs: field.integer(),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// One row per failure that was swallowed and only logged, written by `recordFault`; the faults source reads it. Not tenant-scoped.
|
|
113
|
+
export const faults = define('pipework_faults', {
|
|
114
|
+
id: field.uuid().defaultRandom().primaryKey(),
|
|
115
|
+
kind: field.text(),
|
|
116
|
+
message: field.text(),
|
|
117
|
+
context: field.jsonb(schema.check.json()).nullable(),
|
|
118
|
+
tenantId: field.text().nullable(),
|
|
119
|
+
recordedAt: field.timestamp().default('now'),
|
|
120
|
+
}, {
|
|
121
|
+
indexes: (t) => ({
|
|
122
|
+
byRecordedAt: index('idx_pipework_faults_recorded_at').on(t['recordedAt']),
|
|
123
|
+
}),
|
|
124
|
+
})
|