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,137 @@
|
|
|
1
|
+
// Sources over the failures pipework's own subsystems already record: outboxes that gave up, webhook deliveries,
|
|
2
|
+
// credit settlements. Each is a composition of `querySource` and `newRowsSource`.
|
|
3
|
+
|
|
4
|
+
import { sql } from '../../data/db/sql.js'
|
|
5
|
+
import { assertValidIdentifier, quoteIdentifier } from '../../data/db/identifiers.js'
|
|
6
|
+
import { DEFAULT_WEBHOOK_DELIVERY_TABLE } from '../webhook/outbound.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
|
+
/**
|
|
11
|
+
* Raises `delivery-failed:<table>:<sink>` for outbox rows that gave up (`failed_at`) since the last tick, and
|
|
12
|
+
* `delivery-stuck:<table>:<sink>` while rows are still pending more than `stuckAfterMs` (default 15 min) past
|
|
13
|
+
* `next_attempt_at`. `tables` defaults to `['pipework_alert_deliveries']`; name `support_event_delivery` to add the support outbox.
|
|
14
|
+
* The failed condition clears by the handle's quiet period; the stuck one resolves when the rows drain.
|
|
15
|
+
*/
|
|
16
|
+
export function deliveryFailedSource(options: Basics & { tables?: readonly string[]; stuckAfterMs?: number; settleMs?: number } = {}): AlertSource {
|
|
17
|
+
const tables = [...new Set(options.tables ?? ['pipework_alert_deliveries'])]
|
|
18
|
+
if (tables.length === 0) throw new Error('[pipework] alerts.source.deliveryFailed tables must not be empty; omit it to watch the alert outbox.')
|
|
19
|
+
for (const t of tables) assertValidIdentifier(t, 'deliveryFailed table')
|
|
20
|
+
const stuckAfter = positive(options.stuckAfterMs ?? FIFTEEN_MIN, 'stuckAfterMs', 'alerts.source.deliveryFailed')
|
|
21
|
+
const failed = tables.map(table => newRowsSource({
|
|
22
|
+
...common(options),
|
|
23
|
+
name: 'delivery-failed',
|
|
24
|
+
stateKey: `delivery-failed:${table}`,
|
|
25
|
+
table,
|
|
26
|
+
at: 'failed_at',
|
|
27
|
+
groupBy: sql.raw(`'${table}:' || t."sink"`),
|
|
28
|
+
...(options.settleMs !== undefined ? { settleMs: options.settleMs } : {}),
|
|
29
|
+
message: g => `${String(g.count)} delivery(ies) to ${String(g.last['sink'])} gave up; last error: ${text(g.last['last_error'])}`,
|
|
30
|
+
subject: g => ({ entity: 'sink', entityId: String(g.last['sink']) }),
|
|
31
|
+
context: g => ({ table, sink: String(g.last['sink']), count: g.count, lastError: g.last['last_error'] === null ? null : String(g.last['last_error']) }),
|
|
32
|
+
}))
|
|
33
|
+
const pending = tables.map(table => `
|
|
34
|
+
SELECT '${table}' AS tbl, sink, count(*)::int AS n,
|
|
35
|
+
(extract(epoch FROM clock_timestamp() - min(next_attempt_at)) * 1000)::bigint AS oldest_ms
|
|
36
|
+
FROM ${quoteIdentifier(table, 'deliveryFailed table')}
|
|
37
|
+
WHERE delivered_at IS NULL AND failed_at IS NULL
|
|
38
|
+
AND next_attempt_at < clock_timestamp() - ${String(stuckAfter)} * interval '1 millisecond'
|
|
39
|
+
GROUP BY sink`).join(' UNION ALL ')
|
|
40
|
+
const stuck = querySource({
|
|
41
|
+
...common(options),
|
|
42
|
+
name: 'delivery-stuck',
|
|
43
|
+
query: sql`SELECT * FROM (${sql.raw(pending)}) p ORDER BY tbl, sink`,
|
|
44
|
+
id: r => `${String(r['tbl'])}:${String(r['sink'])}`,
|
|
45
|
+
message: r => `${String(r['n'])} delivery(ies) to ${String(r['sink'])} pending more than ${String(stuckAfter)} ms past their retry time (oldest ${String(r['oldest_ms'])} ms)`,
|
|
46
|
+
subject: r => ({ entity: 'sink', entityId: String(r['sink']) }),
|
|
47
|
+
context: r => ({ table: String(r['tbl']), sink: String(r['sink']), count: Number(r['n']), oldestMs: Number(r['oldest_ms']) }),
|
|
48
|
+
})
|
|
49
|
+
return composeSources('delivery-failed', [...failed, stuck])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Raises `webhook-failed:<endpointId>` for outbound webhook deliveries that gave up (completed with no 2xx status)
|
|
54
|
+
* since the last tick, and `webhook-stalled:<endpointId>` while deliveries are more than `stalledAfterMs`
|
|
55
|
+
* (default 15 min) past their `next_retry_at`. `table` is the delivery table (default the outbound webhook's).
|
|
56
|
+
*/
|
|
57
|
+
export function webhookFailedSource(options: Basics & { table?: string; stalledAfterMs?: number; settleMs?: number } = {}): AlertSource {
|
|
58
|
+
const table = options.table ?? DEFAULT_WEBHOOK_DELIVERY_TABLE
|
|
59
|
+
assertValidIdentifier(table, 'webhookFailed table')
|
|
60
|
+
const stalledAfter = positive(options.stalledAfterMs ?? FIFTEEN_MIN, 'stalledAfterMs', 'alerts.source.webhookFailed')
|
|
61
|
+
const failed = newRowsSource({
|
|
62
|
+
...common(options),
|
|
63
|
+
name: 'webhook-failed',
|
|
64
|
+
stateKey: `webhook-failed:${table}`,
|
|
65
|
+
table,
|
|
66
|
+
at: 'completed_at',
|
|
67
|
+
where: sql`(t.status_code IS NULL OR t.status_code < 200 OR t.status_code >= 300)`,
|
|
68
|
+
groupBy: 'endpoint_id',
|
|
69
|
+
...(options.settleMs !== undefined ? { settleMs: options.settleMs } : {}),
|
|
70
|
+
message: g => `${String(g.count)} webhook delivery(ies) to endpoint ${g.id} gave up; last status ${text(g.last['status_code'])}`,
|
|
71
|
+
subject: g => ({ tenantId: String(g.last['tenant_id']), entity: 'webhook-endpoint', entityId: g.id }),
|
|
72
|
+
context: g => ({ endpointId: g.id, count: g.count, lastStatus: g.last['status_code'] === null ? null : Number(g.last['status_code']) }),
|
|
73
|
+
})
|
|
74
|
+
const stalled = querySource({
|
|
75
|
+
...common(options),
|
|
76
|
+
name: 'webhook-stalled',
|
|
77
|
+
query: sql`
|
|
78
|
+
SELECT endpoint_id, count(*)::int AS n, (extract(epoch FROM clock_timestamp() - min(next_retry_at)) * 1000)::bigint AS oldest_ms
|
|
79
|
+
FROM ${sql.raw(quoteIdentifier(table, 'webhookFailed table'))}
|
|
80
|
+
WHERE completed_at IS NULL AND next_retry_at < clock_timestamp() - ${stalledAfter} * interval '1 millisecond'
|
|
81
|
+
GROUP BY endpoint_id ORDER BY endpoint_id`,
|
|
82
|
+
id: r => String(r['endpoint_id']),
|
|
83
|
+
message: r => `${String(r['n'])} webhook delivery(ies) to endpoint ${String(r['endpoint_id'])} more than ${String(stalledAfter)} ms past their retry time (oldest ${String(r['oldest_ms'])} ms)`,
|
|
84
|
+
subject: r => ({ entity: 'webhook-endpoint', entityId: String(r['endpoint_id']) }),
|
|
85
|
+
context: r => ({ endpointId: String(r['endpoint_id']), count: Number(r['n']), oldestMs: Number(r['oldest_ms']) }),
|
|
86
|
+
})
|
|
87
|
+
return composeSources('webhook-failed', [failed, stalled])
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Raises `credit-overdraft:<tenantId>` for settlements that overdrew a tenant since the last tick,
|
|
92
|
+
* `credit-held:<tenantId>` while a reservation has been held longer than `heldOlderThanMs` (default 1 h), and
|
|
93
|
+
* `credit-expiry-undrained:all` while grants expired more than `expiryGraceMs` (default 1 h) ago still have credit left,
|
|
94
|
+
* which means nothing is running the credits expiry job.
|
|
95
|
+
*/
|
|
96
|
+
export function creditsSource(options: Basics & { heldOlderThanMs?: number; expiryGraceMs?: number; settleMs?: number } = {}): AlertSource {
|
|
97
|
+
const held = positive(options.heldOlderThanMs ?? HOUR, 'heldOlderThanMs', 'alerts.source.credits')
|
|
98
|
+
const grace = positive(options.expiryGraceMs ?? HOUR, 'expiryGraceMs', 'alerts.source.credits')
|
|
99
|
+
const overdraft = newRowsSource({
|
|
100
|
+
...common(options),
|
|
101
|
+
name: 'credit-overdraft',
|
|
102
|
+
table: 'credit_reservation',
|
|
103
|
+
at: 'settled_at',
|
|
104
|
+
where: sql`t.overdraft > 0`,
|
|
105
|
+
groupBy: 'tenant_id',
|
|
106
|
+
sum: 'overdraft',
|
|
107
|
+
...(options.settleMs !== undefined ? { settleMs: options.settleMs } : {}),
|
|
108
|
+
message: g => `${String(g.count)} settlement(s) overdrew ${g.id} by ${String(g.sum)}`,
|
|
109
|
+
subject: g => ({ tenantId: g.id }),
|
|
110
|
+
context: g => ({ count: g.count, overdraft: g.sum }),
|
|
111
|
+
})
|
|
112
|
+
const stale = querySource({
|
|
113
|
+
...common(options),
|
|
114
|
+
name: 'credit-held',
|
|
115
|
+
query: sql`
|
|
116
|
+
SELECT tenant_id, count(*)::int AS n, (extract(epoch FROM clock_timestamp() - min(created_at)) * 1000)::bigint AS oldest_ms
|
|
117
|
+
FROM credit_reservation
|
|
118
|
+
WHERE status = 'held' AND created_at < clock_timestamp() - ${held} * interval '1 millisecond'
|
|
119
|
+
GROUP BY tenant_id ORDER BY tenant_id`,
|
|
120
|
+
id: r => String(r['tenant_id']),
|
|
121
|
+
message: r => `${String(r['n'])} credit hold(s) for ${String(r['tenant_id'])} older than ${String(held)} ms (oldest ${String(r['oldest_ms'])} ms)`,
|
|
122
|
+
subject: r => ({ tenantId: String(r['tenant_id']) }),
|
|
123
|
+
context: r => ({ count: Number(r['n']), oldestMs: Number(r['oldest_ms']) }),
|
|
124
|
+
})
|
|
125
|
+
const expiry = querySource({
|
|
126
|
+
...common(options),
|
|
127
|
+
name: 'credit-expiry-undrained',
|
|
128
|
+
query: sql`
|
|
129
|
+
SELECT count(*)::int AS n FROM credit_grant
|
|
130
|
+
WHERE remaining > 0 AND expires_at <= clock_timestamp() - ${grace} * interval '1 millisecond'
|
|
131
|
+
HAVING count(*) > 0`,
|
|
132
|
+
id: () => 'all',
|
|
133
|
+
message: r => `${String(r['n'])} expired grant(s) not drained; is the credits expiry job scheduled?`,
|
|
134
|
+
context: r => ({ count: Number(r['n']) }),
|
|
135
|
+
})
|
|
136
|
+
return composeSources('credits', [overdraft, stale, expiry])
|
|
137
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { sql } from '../../data/db/sql.js'
|
|
2
|
+
import type { AuditConfig, AuditEmitOptions, Audit } from './types.js'
|
|
3
|
+
import { getContext } from '../../request/context/store.js'
|
|
4
|
+
import { assertValidIdentifier, quoteIdentifier } from '../../data/db/identifiers.js'
|
|
5
|
+
|
|
6
|
+
export function createAudit(config: AuditConfig): Audit {
|
|
7
|
+
const table = config.table ?? 'pipework_audit_events'
|
|
8
|
+
assertValidIdentifier(table, 'audit table')
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
async emit(options: AuditEmitOptions): Promise<string> {
|
|
12
|
+
const ctx = getContext()
|
|
13
|
+
if (!ctx) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'[pipework] audit.emit() called outside a managed context. ' +
|
|
16
|
+
'It must run inside a request handler or job.',
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (ctx.tenant === null) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'[pipework] audit.emit() requires tenant context. ' +
|
|
23
|
+
'Configure tenant extraction in createServer().',
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (ctx.auth === null) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'[pipework] audit.emit() requires authenticated context. ' +
|
|
30
|
+
'Ensure the handler uses .auth() or the route is not .public().',
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const userId = extractUserId(ctx.auth)
|
|
35
|
+
if (userId === null) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
'[pipework] audit.emit() requires auth.userId. ' +
|
|
38
|
+
'Ensure the auth object has a userId field.',
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const bound = ctx.transactions.get(config.database)
|
|
43
|
+
if (!bound) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`[pipework] audit.emit() requires an active transaction on database "${config.database}". ` +
|
|
46
|
+
`Configure transactionPerRequest: ['${config.database}'] in createServer().`,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
const tx = bound.drizzle
|
|
50
|
+
|
|
51
|
+
const sessionId = config.sessionIdField !== undefined && typeof ctx.auth === 'object'
|
|
52
|
+
? (ctx.auth as Record<string, unknown>)[config.sessionIdField] as string | null ?? null
|
|
53
|
+
: null
|
|
54
|
+
|
|
55
|
+
const beforeJson = options.before !== undefined ? JSON.stringify(options.before) : null
|
|
56
|
+
const afterJson = options.after !== undefined ? JSON.stringify(options.after) : null
|
|
57
|
+
const metadataJson = options.metadata !== undefined ? JSON.stringify(options.metadata) : null
|
|
58
|
+
|
|
59
|
+
const result = await tx.execute(sql`
|
|
60
|
+
INSERT INTO ${sql.raw(quoteIdentifier(table, 'audit table'))} (tenant_id, request_id, user_id, session_id, action_type, entity_type, entity_id, before_state, after_state, metadata)
|
|
61
|
+
VALUES (
|
|
62
|
+
${ctx.tenant},
|
|
63
|
+
${ctx.requestId},
|
|
64
|
+
${userId},
|
|
65
|
+
${sessionId ?? null},
|
|
66
|
+
${options.actionType},
|
|
67
|
+
${options.entityType ?? null},
|
|
68
|
+
${options.entityId ?? null},
|
|
69
|
+
${beforeJson !== null ? sql`${beforeJson}::jsonb` : sql`NULL`},
|
|
70
|
+
${afterJson !== null ? sql`${afterJson}::jsonb` : sql`NULL`},
|
|
71
|
+
${metadataJson !== null ? sql`${metadataJson}::jsonb` : sql`NULL`}
|
|
72
|
+
)
|
|
73
|
+
RETURNING id::text
|
|
74
|
+
`)
|
|
75
|
+
|
|
76
|
+
return (result[0] as { id: string }).id
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extractUserId(auth: unknown): string | null {
|
|
82
|
+
if (auth !== null && auth !== undefined && typeof auth === 'object' && 'userId' in auth) {
|
|
83
|
+
const val = (auth as { userId: unknown }).userId
|
|
84
|
+
return typeof val === 'string' ? val : null
|
|
85
|
+
}
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createAudit } from './index.js'
|
|
2
|
+
import { query } from '../../audit/query/index.js'
|
|
3
|
+
import { auditEntityId } from '../../data/write/audit/primary-key.js'
|
|
4
|
+
import { chainHead, verify, removeTenant } from '../../audit/chain/index.js'
|
|
5
|
+
|
|
6
|
+
/** Audit logging namespace — structured audit trail for data changes. */
|
|
7
|
+
export const audit = {
|
|
8
|
+
/** Creates an Audit instance that writes audit records to a Postgres table. */
|
|
9
|
+
create: createAudit,
|
|
10
|
+
/** Queries the framework-managed `audit_record` table. Tenant-scoped via the active Flow. `{ entity, entityId }` returns one row's history from an index; build a composite key's `entityId` with `audit.entityId`. */
|
|
11
|
+
query,
|
|
12
|
+
/**
|
|
13
|
+
* The `entityId` filter text for a written table's row: the primary key's value as text, or for a composite key a compact JSON array of the key values as strings in key order (`["a","7"]`). Pass the table and a row (or just its key fields). Dates become ISO strings.
|
|
14
|
+
*/
|
|
15
|
+
entityId: auditEntityId,
|
|
16
|
+
/** The newest record's `{ seq, hash }` in the tenant's audit hash chain (blueprint `audit.chain`), to store outside the database; `null` when empty. */
|
|
17
|
+
chainHead,
|
|
18
|
+
/** Walks the tenant's audit hash chain and reports the first break (hash-mismatch, prev-mismatch, missing, head-mismatch, anchor-mismatch), or `{ ok: true }`. */
|
|
19
|
+
verify,
|
|
20
|
+
/** Deletes every audit record of one tenant and its hash chain, for the closure of that tenant's account. Irreversible; returns the number of records removed. Other tenants' records and chains are untouched. */
|
|
21
|
+
removeTenant,
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS pipework_audit_events (
|
|
2
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3
|
+
tenant_id text NOT NULL,
|
|
4
|
+
request_id text NOT NULL,
|
|
5
|
+
user_id text NOT NULL,
|
|
6
|
+
session_id text,
|
|
7
|
+
action_type text NOT NULL,
|
|
8
|
+
entity_type text,
|
|
9
|
+
entity_id text,
|
|
10
|
+
before_state jsonb,
|
|
11
|
+
after_state jsonb,
|
|
12
|
+
metadata jsonb,
|
|
13
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_audit_tenant_action ON pipework_audit_events (tenant_id, action_type, created_at);
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_audit_tenant_entity ON pipework_audit_events (tenant_id, entity_type, entity_id, created_at);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_audit_tenant_user ON pipework_audit_events (tenant_id, user_id, created_at);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_audit_tenant_request ON pipework_audit_events (tenant_id, request_id);
|
|
20
|
+
|
|
21
|
+
CREATE OR REPLACE FUNCTION pipework_audit_immutable() RETURNS trigger AS $$
|
|
22
|
+
BEGIN
|
|
23
|
+
RAISE EXCEPTION 'pipework_audit_events is append-only: UPDATE and DELETE are not permitted';
|
|
24
|
+
END;
|
|
25
|
+
$$ LANGUAGE plpgsql;
|
|
26
|
+
|
|
27
|
+
DO $$ BEGIN
|
|
28
|
+
IF NOT EXISTS (
|
|
29
|
+
SELECT 1 FROM pg_trigger WHERE tgname = 'pipework_audit_no_modify'
|
|
30
|
+
) THEN
|
|
31
|
+
CREATE TRIGGER pipework_audit_no_modify
|
|
32
|
+
BEFORE UPDATE OR DELETE ON pipework_audit_events
|
|
33
|
+
FOR EACH ROW EXECUTE FUNCTION pipework_audit_immutable();
|
|
34
|
+
END IF;
|
|
35
|
+
END $$;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface AuditConfig {
|
|
2
|
+
readonly database: string
|
|
3
|
+
readonly table?: string
|
|
4
|
+
readonly sessionIdField?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface AuditEmitOptions {
|
|
8
|
+
readonly actionType: string
|
|
9
|
+
readonly entityType?: string
|
|
10
|
+
readonly entityId?: string
|
|
11
|
+
readonly before?: unknown
|
|
12
|
+
readonly after?: unknown
|
|
13
|
+
readonly metadata?: Record<string, unknown>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Audit {
|
|
17
|
+
emit(options: AuditEmitOptions): Promise<string>
|
|
18
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { readFile as fsReadFile } from 'node:fs/promises'
|
|
2
|
+
import { readAwsEnv, type AwsEnv } from '../../core/config/aws-env.js'
|
|
3
|
+
import type { AwsCredentials } from './sigv4.js'
|
|
4
|
+
|
|
5
|
+
export interface AwsCredentialsOptions {
|
|
6
|
+
/** For tests: where the AWS variables come from. Default: the process environment. */
|
|
7
|
+
readonly env?: () => AwsEnv
|
|
8
|
+
/** For tests: replaces global `fetch`. */
|
|
9
|
+
readonly fetch?: typeof fetch
|
|
10
|
+
/** For tests: the clock, in epoch milliseconds. */
|
|
11
|
+
readonly now?: () => number
|
|
12
|
+
/** For tests: reads AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE. */
|
|
13
|
+
readonly readFile?: (path: string) => Promise<string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const REFRESH_BEFORE_MS = 5 * 60 * 1000
|
|
17
|
+
const ECS_HOST = 'http://169.254.170.2'
|
|
18
|
+
const IMDS_HOST = 'http://169.254.169.254'
|
|
19
|
+
const IMDS_TIMEOUT_MS = 1000
|
|
20
|
+
const CONTAINER_TIMEOUT_MS = 2000
|
|
21
|
+
|
|
22
|
+
interface Cached { readonly credentials: AwsCredentials; readonly expiresAt: number }
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default AWS credential provider: environment keys, then the ECS container
|
|
26
|
+
* endpoint, then EC2 instance metadata (IMDSv2). Container and IMDS credentials
|
|
27
|
+
* are cached until five minutes before `Expiration`. Profiles and SSO are not read.
|
|
28
|
+
* `storage.s3` and `mail.ses` use it when `credentials` is omitted.
|
|
29
|
+
*/
|
|
30
|
+
export function awsCredentials(options: AwsCredentialsOptions = {}): () => Promise<AwsCredentials> {
|
|
31
|
+
const env = options.env ?? readAwsEnv
|
|
32
|
+
const doFetch = options.fetch ?? fetch
|
|
33
|
+
const now = options.now ?? Date.now
|
|
34
|
+
const readFile = options.readFile ?? ((path: string) => fsReadFile(path, 'utf8'))
|
|
35
|
+
let cached: Cached | undefined
|
|
36
|
+
let inflight: Promise<Cached> | undefined
|
|
37
|
+
|
|
38
|
+
async function fetchJson(url: string, init: RequestInit, timeoutMs: number, what: string): Promise<Record<string, unknown>> {
|
|
39
|
+
let res: Response
|
|
40
|
+
try {
|
|
41
|
+
res = await doFetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) })
|
|
42
|
+
} catch (err) {
|
|
43
|
+
throw new Error(`[pipework] ${what} at ${url} could not be reached: ${err instanceof Error ? err.message : String(err)}`)
|
|
44
|
+
}
|
|
45
|
+
if (!res.ok) throw new Error(`[pipework] ${what} at ${url} answered HTTP ${res.status}`)
|
|
46
|
+
const raw = await res.text()
|
|
47
|
+
try {
|
|
48
|
+
const parsed: unknown = JSON.parse(raw)
|
|
49
|
+
if (typeof parsed !== 'object' || parsed === null) throw new Error('not an object')
|
|
50
|
+
return parsed as Record<string, unknown>
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error(`[pipework] ${what} at ${url} answered HTTP ${res.status} with malformed JSON`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parse(body: Record<string, unknown>, url: string, what: string): Cached {
|
|
57
|
+
const id = body['AccessKeyId']
|
|
58
|
+
const secret = body['SecretAccessKey']
|
|
59
|
+
const token = body['Token']
|
|
60
|
+
const expiration = body['Expiration']
|
|
61
|
+
const expiresAt = typeof expiration === 'string' ? Date.parse(expiration) : NaN
|
|
62
|
+
if (typeof id !== 'string' || typeof secret !== 'string' || Number.isNaN(expiresAt)) {
|
|
63
|
+
throw new Error(`[pipework] ${what} at ${url} answered HTTP 200 without AccessKeyId, SecretAccessKey and Expiration`)
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
credentials: { accessKeyId: id, secretAccessKey: secret, ...(typeof token === 'string' ? { sessionToken: token } : {}) },
|
|
67
|
+
expiresAt,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function fromContainer(e: AwsEnv): Promise<Cached> {
|
|
72
|
+
const rel = e.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
|
|
73
|
+
const url = rel !== undefined ? `${ECS_HOST}${rel}` : e.AWS_CONTAINER_CREDENTIALS_FULL_URI!
|
|
74
|
+
let token = e.AWS_CONTAINER_AUTHORIZATION_TOKEN
|
|
75
|
+
if (e.AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE !== undefined) {
|
|
76
|
+
token = (await readFile(e.AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE)).trim()
|
|
77
|
+
}
|
|
78
|
+
const headers: Record<string, string> = token !== undefined ? { Authorization: token } : {}
|
|
79
|
+
const what = 'the ECS container credentials endpoint'
|
|
80
|
+
return parse(await fetchJson(url, { method: 'GET', headers }, CONTAINER_TIMEOUT_MS, what), url, what)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function fromImds(): Promise<Cached> {
|
|
84
|
+
const tokenRes = await doFetch(`${IMDS_HOST}/latest/api/token`, {
|
|
85
|
+
method: 'PUT',
|
|
86
|
+
headers: { 'X-aws-ec2-metadata-token-ttl-seconds': '21600' },
|
|
87
|
+
signal: AbortSignal.timeout(IMDS_TIMEOUT_MS),
|
|
88
|
+
})
|
|
89
|
+
if (!tokenRes.ok) throw new Error(`token request answered HTTP ${tokenRes.status}`)
|
|
90
|
+
const token = (await tokenRes.text()).trim()
|
|
91
|
+
const headers = { 'X-aws-ec2-metadata-token': token }
|
|
92
|
+
const listUrl = `${IMDS_HOST}/latest/meta-data/iam/security-credentials/`
|
|
93
|
+
const listRes = await doFetch(listUrl, { method: 'GET', headers, signal: AbortSignal.timeout(IMDS_TIMEOUT_MS) })
|
|
94
|
+
if (!listRes.ok) throw new Error(`role lookup at ${listUrl} answered HTTP ${listRes.status}`)
|
|
95
|
+
const role = (await listRes.text()).split(/\r?\n/)[0]!.trim()
|
|
96
|
+
if (role === '') throw new Error(`no instance role attached (${listUrl} was empty)`)
|
|
97
|
+
const url = `${listUrl}${role}`
|
|
98
|
+
return parse(await fetchJson(url, { method: 'GET', headers }, IMDS_TIMEOUT_MS, 'EC2 instance metadata'), url, 'EC2 instance metadata')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function resolve(): Promise<Cached> {
|
|
102
|
+
const e = env()
|
|
103
|
+
if (e.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI !== undefined || e.AWS_CONTAINER_CREDENTIALS_FULL_URI !== undefined) {
|
|
104
|
+
return fromContainer(e)
|
|
105
|
+
}
|
|
106
|
+
let imds: string
|
|
107
|
+
if (e.AWS_EC2_METADATA_DISABLED?.toLowerCase() === 'true') {
|
|
108
|
+
imds = 'disabled (AWS_EC2_METADATA_DISABLED=true)'
|
|
109
|
+
} else {
|
|
110
|
+
try {
|
|
111
|
+
return await fromImds()
|
|
112
|
+
} catch (err) {
|
|
113
|
+
imds = `failed (${err instanceof Error ? err.message : String(err)})`
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw new Error(
|
|
117
|
+
'[pipework] No AWS credentials found. Looked at:\n' +
|
|
118
|
+
' - environment: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (AWS_SESSION_TOKEN optional), not set\n' +
|
|
119
|
+
' - container endpoint: AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, AWS_CONTAINER_CREDENTIALS_FULL_URI, not set\n' +
|
|
120
|
+
` - EC2 instance metadata (IMDSv2): ${imds}`,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return async () => {
|
|
125
|
+
const e = env()
|
|
126
|
+
const id = e.AWS_ACCESS_KEY_ID
|
|
127
|
+
const secret = e.AWS_SECRET_ACCESS_KEY
|
|
128
|
+
if (id !== undefined || secret !== undefined) {
|
|
129
|
+
if (id === undefined) throw new Error('[pipework] AWS_SECRET_ACCESS_KEY is set but AWS_ACCESS_KEY_ID is missing.')
|
|
130
|
+
if (secret === undefined) throw new Error('[pipework] AWS_ACCESS_KEY_ID is set but AWS_SECRET_ACCESS_KEY is missing.')
|
|
131
|
+
return { accessKeyId: id, secretAccessKey: secret, ...(e.AWS_SESSION_TOKEN !== undefined ? { sessionToken: e.AWS_SESSION_TOKEN } : {}) }
|
|
132
|
+
}
|
|
133
|
+
if (cached !== undefined && now() < cached.expiresAt - REFRESH_BEFORE_MS) return cached.credentials
|
|
134
|
+
if (inflight === undefined) {
|
|
135
|
+
inflight = resolve().then(
|
|
136
|
+
(c) => { cached = c; inflight = undefined; return c },
|
|
137
|
+
(err: unknown) => { inflight = undefined; throw err },
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return (await inflight).credentials
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (cached !== undefined && now() < cached.expiresAt) return cached.credentials
|
|
144
|
+
throw err
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createHash, createHmac } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
export interface AwsCredentials {
|
|
4
|
+
readonly accessKeyId: string
|
|
5
|
+
readonly secretAccessKey: string
|
|
6
|
+
readonly sessionToken?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SigV4Request {
|
|
10
|
+
readonly method: string
|
|
11
|
+
readonly url: string
|
|
12
|
+
/** Headers to sign and send, other than host and x-amz-date, which are added. */
|
|
13
|
+
readonly headers: Readonly<Record<string, string>>
|
|
14
|
+
readonly body: string | Buffer
|
|
15
|
+
/**
|
|
16
|
+
* The hex SHA-256 of the body, or `UNSIGNED-PAYLOAD`, used in place of hashing `body`.
|
|
17
|
+
* For a streamed S3 body; the caller also sends and signs `x-amz-content-sha256`.
|
|
18
|
+
*/
|
|
19
|
+
readonly payloadHash?: string
|
|
20
|
+
readonly region: string
|
|
21
|
+
readonly service: string
|
|
22
|
+
readonly credentials: AwsCredentials
|
|
23
|
+
readonly now?: Date
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sha256Hex(data: string | Buffer): string {
|
|
27
|
+
return createHash('sha256').update(data).digest('hex')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hmac(key: string | Buffer, data: string): Buffer {
|
|
31
|
+
return createHmac('sha256', key).update(data).digest()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function uriEncode(s: string): string {
|
|
35
|
+
return encodeURIComponent(s).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function canonicalPathOf(url: URL, service: string): string {
|
|
39
|
+
// S3 encodes each path segment once; every other service encodes the already-encoded path again.
|
|
40
|
+
if (service === 's3') return url.pathname.split('/').map(seg => uriEncode(decodeURIComponent(seg))).join('/')
|
|
41
|
+
return url.pathname.split('/').map(uriEncode).join('/')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function canonicalQueryOf(params: Iterable<readonly [string, string]>): string {
|
|
45
|
+
return [...params]
|
|
46
|
+
.map(([k, v]) => [uriEncode(k), uriEncode(v)] as const)
|
|
47
|
+
.sort((a, b) => (a[0] === b[0] ? (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) : (a[0] < b[0] ? -1 : 1)))
|
|
48
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
49
|
+
.join('&')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function signingKey(secret: string, day: string, region: string, service: string): Buffer {
|
|
53
|
+
return hmac(hmac(hmac(hmac(`AWS4${secret}`, day), region), service), 'aws4_request')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function amzDate(d: Date): string {
|
|
57
|
+
return d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Signs a request with AWS Signature Version 4 and returns the headers to send:
|
|
62
|
+
* the given ones, `x-amz-date`, `x-amz-security-token` when the credentials carry
|
|
63
|
+
* a session token, and `authorization`. `host` is signed but not returned; the
|
|
64
|
+
* HTTP client sets it from the URL.
|
|
65
|
+
*/
|
|
66
|
+
export function signV4(req: SigV4Request): Record<string, string> {
|
|
67
|
+
const url = new URL(req.url)
|
|
68
|
+
const stamp = amzDate(req.now ?? new Date())
|
|
69
|
+
const day = stamp.slice(0, 8)
|
|
70
|
+
|
|
71
|
+
const headers: Record<string, string> = {}
|
|
72
|
+
for (const [name, value] of Object.entries(req.headers)) headers[name.toLowerCase()] = value.trim().replace(/\s+/g, ' ')
|
|
73
|
+
headers['x-amz-date'] = stamp
|
|
74
|
+
if (req.credentials.sessionToken !== undefined) headers['x-amz-security-token'] = req.credentials.sessionToken
|
|
75
|
+
|
|
76
|
+
const signed: Record<string, string> = { ...headers, host: url.host }
|
|
77
|
+
const names = Object.keys(signed).sort()
|
|
78
|
+
const canonicalHeaders = names.map(n => `${n}:${signed[n]!}\n`).join('')
|
|
79
|
+
const signedHeaders = names.join(';')
|
|
80
|
+
|
|
81
|
+
const canonicalRequest = [
|
|
82
|
+
req.method.toUpperCase(), canonicalPathOf(url, req.service), canonicalQueryOf(url.searchParams.entries()),
|
|
83
|
+
canonicalHeaders, signedHeaders, req.payloadHash ?? sha256Hex(req.body),
|
|
84
|
+
].join('\n')
|
|
85
|
+
|
|
86
|
+
const scope = `${day}/${req.region}/${req.service}/aws4_request`
|
|
87
|
+
const stringToSign = ['AWS4-HMAC-SHA256', stamp, scope, sha256Hex(canonicalRequest)].join('\n')
|
|
88
|
+
const signature = createHmac('sha256', signingKey(req.credentials.secretAccessKey, day, req.region, req.service))
|
|
89
|
+
.update(stringToSign).digest('hex')
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
...headers,
|
|
93
|
+
authorization:
|
|
94
|
+
`AWS4-HMAC-SHA256 Credential=${req.credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface PresignRequest {
|
|
99
|
+
readonly method: string
|
|
100
|
+
/** The full URL, query included (e.g. `response-content-disposition`); it is signed with the rest. */
|
|
101
|
+
readonly url: string
|
|
102
|
+
readonly region: string
|
|
103
|
+
readonly service: string
|
|
104
|
+
readonly credentials: AwsCredentials
|
|
105
|
+
/** 1 to 604800 (seven days). */
|
|
106
|
+
readonly expiresInSeconds: number
|
|
107
|
+
readonly now?: Date
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Returns `url` with SigV4 query-string signing added (`X-Amz-Algorithm`, `-Credential`, `-Date`,
|
|
112
|
+
* `-Expires`, `-SignedHeaders=host`, `-Security-Token` for a session token, `-Signature`), so a client
|
|
113
|
+
* with no credentials can make the one request until it expires. The payload is `UNSIGNED-PAYLOAD`.
|
|
114
|
+
*/
|
|
115
|
+
export function presignV4(req: PresignRequest): string {
|
|
116
|
+
if (!Number.isInteger(req.expiresInSeconds) || req.expiresInSeconds < 1 || req.expiresInSeconds > 604800) {
|
|
117
|
+
throw new Error('[pipework] presigned URL expiry must be an integer from 1 to 604800 seconds.')
|
|
118
|
+
}
|
|
119
|
+
const url = new URL(req.url)
|
|
120
|
+
const stamp = amzDate(req.now ?? new Date())
|
|
121
|
+
const day = stamp.slice(0, 8)
|
|
122
|
+
const scope = `${day}/${req.region}/${req.service}/aws4_request`
|
|
123
|
+
url.searchParams.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256')
|
|
124
|
+
url.searchParams.set('X-Amz-Credential', `${req.credentials.accessKeyId}/${scope}`)
|
|
125
|
+
url.searchParams.set('X-Amz-Date', stamp)
|
|
126
|
+
url.searchParams.set('X-Amz-Expires', String(req.expiresInSeconds))
|
|
127
|
+
url.searchParams.set('X-Amz-SignedHeaders', 'host')
|
|
128
|
+
if (req.credentials.sessionToken !== undefined) url.searchParams.set('X-Amz-Security-Token', req.credentials.sessionToken)
|
|
129
|
+
|
|
130
|
+
const canonicalRequest = [
|
|
131
|
+
req.method.toUpperCase(), canonicalPathOf(url, req.service), canonicalQueryOf(url.searchParams.entries()),
|
|
132
|
+
`host:${url.host}\n`, 'host', 'UNSIGNED-PAYLOAD',
|
|
133
|
+
].join('\n')
|
|
134
|
+
const stringToSign = ['AWS4-HMAC-SHA256', stamp, scope, sha256Hex(canonicalRequest)].join('\n')
|
|
135
|
+
const signature = createHmac('sha256', signingKey(req.credentials.secretAccessKey, day, req.region, req.service))
|
|
136
|
+
.update(stringToSign).digest('hex')
|
|
137
|
+
// Keep the query exactly as signed: rebuild it with the same encoding the canonical form used.
|
|
138
|
+
const query = canonicalQueryOf(url.searchParams.entries())
|
|
139
|
+
return `${url.origin}${url.pathname}?${query}&X-Amz-Signature=${signature}`
|
|
140
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getContext } from '../../request/context/store.js'
|
|
2
|
+
import type { CacheConfig, Cache, CacheStats } from './types.js'
|
|
3
|
+
import { loadWithDb, createEmptyEntry, isExpired, getOrLoad, preloadEntry, type CacheEntryState } from './entry.js'
|
|
4
|
+
|
|
5
|
+
export function createCache<T>(config: CacheConfig<T>): Cache<T> {
|
|
6
|
+
const entry = createEmptyEntry<T>()
|
|
7
|
+
let lastLoadAt: number | null = null
|
|
8
|
+
let lastLoadDurationMs: number | null = null
|
|
9
|
+
|
|
10
|
+
async function doLoad(e: CacheEntryState<T>): Promise<T> {
|
|
11
|
+
const start = Date.now()
|
|
12
|
+
const result = await loadWithDb(config.database, config.name, getContext()?.tenant ?? null, db => config.load(db))
|
|
13
|
+
lastLoadDurationMs = Date.now() - start
|
|
14
|
+
lastLoadAt = Date.now()
|
|
15
|
+
e.loadedAt = Date.now()
|
|
16
|
+
e.data = result
|
|
17
|
+
e.loadPromise = null
|
|
18
|
+
return result
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
async get(): Promise<T> {
|
|
23
|
+
return getOrLoad(entry, config.ttlMs, doLoad)
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
invalidate(): void {
|
|
27
|
+
entry.data = undefined
|
|
28
|
+
entry.loadedAt = null
|
|
29
|
+
entry.loadPromise = null
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
stats(): CacheStats {
|
|
33
|
+
let status: CacheStats['status']
|
|
34
|
+
if (entry.loadPromise !== null) {
|
|
35
|
+
status = 'loading'
|
|
36
|
+
} else if (entry.data === undefined) {
|
|
37
|
+
status = 'empty'
|
|
38
|
+
} else if (isExpired(entry, config.ttlMs)) {
|
|
39
|
+
status = 'stale'
|
|
40
|
+
} else {
|
|
41
|
+
status = 'loaded'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
name: config.name,
|
|
46
|
+
status,
|
|
47
|
+
hits: entry.hits,
|
|
48
|
+
misses: entry.misses,
|
|
49
|
+
lastLoadAt,
|
|
50
|
+
lastLoadDurationMs,
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async preload(): Promise<void> {
|
|
55
|
+
return preloadEntry(entry, config.ttlMs, doLoad)
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|