pipework 0.251.0 → 0.252.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/core/config/load.d.ts +2 -0
- package/dist/core/config/load.d.ts.map +1 -1
- package/dist/core/config/load.js +1 -0
- package/dist/core/config/load.js.map +1 -1
- package/dist/core/config/namespace.d.ts +1 -0
- package/dist/core/config/namespace.d.ts.map +1 -1
- package/dist/core/config/schema.d.ts +2 -0
- package/dist/core/config/schema.d.ts.map +1 -1
- package/dist/core/config/schema.js +1 -0
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/surface/types.d.ts +1 -0
- package/dist/core/surface/types.d.ts.map +1 -1
- package/dist/data/db/namespace.d.ts +3 -0
- package/dist/data/db/namespace.d.ts.map +1 -1
- package/dist/data/db/namespace.js +3 -0
- package/dist/data/db/namespace.js.map +1 -1
- package/dist/data/db/pool.d.ts.map +1 -1
- package/dist/data/db/pool.js +6 -1
- package/dist/data/db/pool.js.map +1 -1
- package/dist/data/db/query-label.d.ts +5 -0
- package/dist/data/db/query-label.d.ts.map +1 -0
- package/dist/data/db/query-label.js +11 -0
- package/dist/data/db/query-label.js.map +1 -0
- package/dist/data/db/query-watch.d.ts +21 -0
- package/dist/data/db/query-watch.d.ts.map +1 -0
- package/dist/data/db/query-watch.js +90 -0
- package/dist/data/db/query-watch.js.map +1 -0
- package/dist/data/query/postgres-js/driver.js +1 -1
- package/dist/data/query/postgres-js/driver.js.map +1 -1
- package/dist/data/query/postgres-js/session.d.ts +5 -1
- package/dist/data/query/postgres-js/session.d.ts.map +1 -1
- package/dist/data/query/postgres-js/session.js +16 -11
- package/dist/data/query/postgres-js/session.js.map +1 -1
- package/dist/data/query/utils.d.ts +2 -0
- package/dist/data/query/utils.d.ts.map +1 -1
- package/dist/data/query/utils.js.map +1 -1
- package/dist/infra/credits/schema.d.ts +1 -1
- package/dist/test/plugin.d.ts.map +1 -1
- package/dist/test/plugin.js +10 -4
- package/dist/test/plugin.js.map +1 -1
- package/package.json +4 -3
- package/src/async/jobs/abortable-delay.ts +18 -0
- package/src/async/jobs/claims.ts +52 -0
- package/src/async/jobs/cron.ts +179 -0
- package/src/async/jobs/errors.ts +56 -0
- package/src/async/jobs/exclusion.ts +117 -0
- package/src/async/jobs/execute.ts +219 -0
- package/src/async/jobs/index.ts +10 -0
- package/src/async/jobs/listener.ts +135 -0
- package/src/async/jobs/namespace.ts +20 -0
- package/src/async/jobs/queue-door.ts +44 -0
- package/src/async/jobs/queue.ts +1012 -0
- package/src/async/jobs/run-script.ts +67 -0
- package/src/async/jobs/scheduler.ts +218 -0
- package/src/async/jobs/schedules-table.ts +37 -0
- package/src/async/jobs/serialize-error.ts +48 -0
- package/src/async/jobs/table.ts +191 -0
- package/src/async/pipeline/define.ts +53 -0
- package/src/async/pipeline/errors.ts +35 -0
- package/src/async/pipeline/execute.ts +53 -0
- package/src/async/pipeline/index.ts +13 -0
- package/src/async/pipeline/namespace.ts +12 -0
- package/src/async/pipeline/resume.ts +67 -0
- package/src/async/pipeline/run-steps.ts +98 -0
- package/src/async/pipeline/types.ts +57 -0
- package/src/async/state-machine/define.ts +64 -0
- package/src/async/state-machine/errors.ts +29 -0
- package/src/async/state-machine/index.ts +3 -0
- package/src/async/state-machine/namespace.ts +12 -0
- package/src/async/state-machine/transition.ts +90 -0
- package/src/async/state-machine/types.ts +37 -0
- package/src/async/steps/errors.ts +31 -0
- package/src/async/steps/graph.ts +1214 -0
- package/src/async/steps/hash.ts +40 -0
- package/src/async/steps/index.ts +7 -0
- package/src/async/steps/namespace.ts +7 -0
- package/src/async/steps/table.ts +97 -0
- package/src/async/steps/types.ts +318 -0
- package/src/audit/chain/index.ts +9 -0
- package/src/audit/chain/install.ts +113 -0
- package/src/audit/chain/retire.ts +52 -0
- package/src/audit/chain/verify.ts +257 -0
- package/src/audit/query/cursor.ts +37 -0
- package/src/audit/query/index.ts +9 -0
- package/src/audit/query/query.ts +223 -0
- package/src/audit/schema/audit-record-fields.ts +68 -0
- package/src/audit/schema/audit-record.ts +23 -0
- package/src/auth/auth/audit.ts +31 -0
- package/src/auth/auth/chain.ts +84 -0
- package/src/auth/auth/cookie.ts +136 -0
- package/src/auth/auth/email-links.ts +311 -0
- package/src/auth/auth/guard.ts +128 -0
- package/src/auth/auth/index.ts +18 -0
- package/src/auth/auth/multi-org.ts +337 -0
- package/src/auth/auth/namespace.ts +34 -0
- package/src/auth/auth/parse-expiry.ts +13 -0
- package/src/auth/auth/passwords.ts +110 -0
- package/src/auth/auth/schema.ts +110 -0
- package/src/auth/auth/sessions.ts +565 -0
- package/src/auth/auth/sign-in.ts +156 -0
- package/src/auth/auth/signing-keys.ts +69 -0
- package/src/auth/auth/types.ts +26 -0
- package/src/auth/auth/uuid.ts +10 -0
- package/src/auth/rbac/cache.ts +55 -0
- package/src/auth/rbac/check.ts +30 -0
- package/src/auth/rbac/create.ts +35 -0
- package/src/auth/rbac/index.ts +5 -0
- package/src/auth/rbac/middleware.ts +20 -0
- package/src/auth/rbac/namespace.ts +15 -0
- package/src/auth/rbac/resolve.ts +31 -0
- package/src/auth/rbac/schema.sql +31 -0
- package/src/auth/rbac/seed.ts +67 -0
- package/src/auth/rbac/tables.ts +32 -0
- package/src/auth/rbac/types.ts +65 -0
- package/src/auth/tenant/extract.ts +33 -0
- package/src/auth/tenant/in-tenant.ts +49 -0
- package/src/auth/tenant/index.ts +7 -0
- package/src/auth/tenant/namespace.ts +23 -0
- package/src/auth/tenant/propagate.ts +20 -0
- package/src/auth/tenant/rls.ts +120 -0
- package/src/auth/tenant/scope.ts +33 -0
- package/src/auth/tenant/scoped-db.ts +581 -0
- package/src/auth/tenant/types.ts +14 -0
- package/src/auth/tenant/validate.ts +27 -0
- package/src/cli/commands/add.ts +158 -0
- package/src/cli/commands/alerts.ts +98 -0
- package/src/cli/commands/check.ts +1418 -0
- package/src/cli/commands/codemod.ts +78 -0
- package/src/cli/commands/db.ts +356 -0
- package/src/cli/commands/dev.ts +101 -0
- package/src/cli/commands/generate.ts +75 -0
- package/src/cli/commands/init.ts +36 -0
- package/src/cli/commands/install.ts +28 -0
- package/src/cli/commands/lint.ts +62 -0
- package/src/cli/commands/migrate.ts +132 -0
- package/src/cli/commands/openapi.ts +63 -0
- package/src/cli/commands/remove.ts +72 -0
- package/src/cli/commands/run.ts +50 -0
- package/src/cli/commands/scope.ts +54 -0
- package/src/cli/commands/serve.ts +68 -0
- package/src/cli/commands/shard.ts +31 -0
- package/src/cli/commands/shared/config-pipework-version.ts +28 -0
- package/src/cli/commands/shared/find-config.ts +22 -0
- package/src/cli/commands/shared/load-config.ts +35 -0
- package/src/cli/commands/shared/pnpm-install.ts +95 -0
- package/src/cli/commands/shared/report-removed.ts +8 -0
- package/src/cli/commands/sync.ts +32 -0
- package/src/cli/commands/test.ts +307 -0
- package/src/cli/commands/upgrade.ts +48 -0
- package/src/cli/dev-bootstrap.ts +24 -0
- package/src/cli/errors.ts +10 -0
- package/src/cli/index.ts +303 -0
- package/src/cli/output.ts +17 -0
- package/src/cli/port.ts +15 -0
- package/src/cli/surface-kinds.ts +20 -0
- package/src/client/index.ts +4 -0
- package/src/client/poll.ts +130 -0
- package/src/client/report-fault.ts +119 -0
- package/src/core/config/app-pool.ts +19 -0
- package/src/core/config/aws-env.ts +29 -0
- package/src/core/config/base-dir.ts +24 -0
- package/src/core/config/discover.ts +64 -0
- package/src/core/config/env-resolve.ts +87 -0
- package/src/core/config/env-types.ts +116 -0
- package/src/core/config/env.ts +55 -0
- package/src/core/config/errors.ts +9 -0
- package/src/core/config/import-config.ts +40 -0
- package/src/core/config/index.ts +8 -0
- package/src/core/config/load.ts +417 -0
- package/src/core/config/namespace.ts +14 -0
- package/src/core/config/resolve.ts +195 -0
- package/src/core/config/schema.ts +488 -0
- package/src/core/config/statement-observer.ts +48 -0
- package/src/core/config/ts-inline-hooks.ts +75 -0
- package/src/core/config/ts-register.ts +4 -0
- package/src/core/config/ts-resolve-hooks.ts +101 -0
- package/src/core/errors/base.ts +49 -0
- package/src/core/errors/http.ts +146 -0
- package/src/core/errors/index.ts +2 -0
- package/src/core/errors/namespace.ts +31 -0
- package/src/core/lifecycle/drain.ts +33 -0
- package/src/core/lifecycle/health.ts +72 -0
- package/src/core/lifecycle/index.ts +4 -0
- package/src/core/lifecycle/namespace.ts +7 -0
- package/src/core/lifecycle/startup.ts +102 -0
- package/src/core/lifecycle/types.ts +36 -0
- package/src/core/logging/context.ts +60 -0
- package/src/core/logging/error-chain.ts +68 -0
- package/src/core/logging/index.ts +5 -0
- package/src/core/logging/logger.ts +65 -0
- package/src/core/logging/namespace.ts +16 -0
- package/src/core/logging/proxy.ts +22 -0
- package/src/core/logging/redact-match.ts +24 -0
- package/src/core/logging/types.ts +47 -0
- package/src/core/pipework.ts +566 -0
- package/src/core/runtime/broker.ts +69 -0
- package/src/core/surface/http.ts +133 -0
- package/src/core/surface/index.ts +11 -0
- package/src/core/surface/job-timeout.ts +40 -0
- package/src/core/surface/namespace.ts +13 -0
- package/src/core/surface/script.ts +33 -0
- package/src/core/surface/start.ts +142 -0
- package/src/core/surface/types.ts +225 -0
- package/src/core/surface/validate.ts +88 -0
- package/src/core/surface/worker.ts +528 -0
- package/src/core/validation/connectivity.ts +49 -0
- package/src/core/validation/domain.ts +99 -0
- package/src/core/validation/index.ts +5 -0
- package/src/core/validation/queue-tables.ts +312 -0
- package/src/core/validation/rls-role.ts +138 -0
- package/src/core/validation/startup.ts +51 -0
- package/src/core/validation/surfaces.ts +65 -0
- package/src/core/validation/types.ts +25 -0
- package/src/data/db/advisory-lock.ts +55 -0
- package/src/data/db/aggregates.ts +172 -0
- package/src/data/db/array-elements.ts +111 -0
- package/src/data/db/bulk-set.ts +53 -0
- package/src/data/db/col/index.ts +14 -0
- package/src/data/db/composed.ts +53 -0
- package/src/data/db/db.ts +276 -0
- package/src/data/db/excluded.ts +16 -0
- package/src/data/db/expressions.ts +473 -0
- package/src/data/db/filter.ts +90 -0
- package/src/data/db/function-source.ts +114 -0
- package/src/data/db/identifiers.ts +16 -0
- package/src/data/db/idx/index.ts +9 -0
- package/src/data/db/in-list.ts +49 -0
- package/src/data/db/index.ts +9 -0
- package/src/data/db/insert-split.ts +137 -0
- package/src/data/db/instrumentation.ts +82 -0
- package/src/data/db/maintenance.ts +28 -0
- package/src/data/db/namespace.ts +107 -0
- package/src/data/db/owner-labels.ts +9 -0
- package/src/data/db/pool.ts +187 -0
- package/src/data/db/query-label.ts +13 -0
- package/src/data/db/query-watch.ts +116 -0
- package/src/data/db/raw-client-warning.ts +37 -0
- package/src/data/db/readonly.ts +23 -0
- package/src/data/db/reservation.ts +151 -0
- package/src/data/db/schema-features.ts +14 -0
- package/src/data/db/serializable.ts +83 -0
- package/src/data/db/sql.ts +1 -0
- package/src/data/db/timeouts.ts +76 -0
- package/src/data/db/types.ts +18 -0
- package/src/data/db/unbound.ts +33 -0
- package/src/data/db/values.ts +29 -0
- package/src/data/domain/brand.ts +15 -0
- package/src/data/domain/build-factory.ts +193 -0
- package/src/data/domain/build-schema.ts +161 -0
- package/src/data/domain/build-table.ts +355 -0
- package/src/data/domain/define.ts +239 -0
- package/src/data/domain/external-registry.ts +34 -0
- package/src/data/domain/external.ts +77 -0
- package/src/data/domain/field.ts +279 -0
- package/src/data/domain/index.ts +13 -0
- package/src/data/domain/naming.ts +6 -0
- package/src/data/domain/projections.ts +57 -0
- package/src/data/domain/trace-safe-projection.ts +31 -0
- package/src/data/domain/traits.ts +108 -0
- package/src/data/domain/types.ts +289 -0
- package/src/data/jsonb/composed.ts +18 -0
- package/src/data/jsonb/index.ts +1 -0
- package/src/data/jsonb/namespace.ts +30 -0
- package/src/data/jsonb/query.ts +86 -0
- package/src/data/migrate/append-only-generate.ts +47 -0
- package/src/data/migrate/applied.ts +47 -0
- package/src/data/migrate/apply.ts +309 -0
- package/src/data/migrate/check.ts +119 -0
- package/src/data/migrate/diff.ts +357 -0
- package/src/data/migrate/door-generate.ts +130 -0
- package/src/data/migrate/framework-rls.ts +58 -0
- package/src/data/migrate/generate.ts +201 -0
- package/src/data/migrate/history.ts +215 -0
- package/src/data/migrate/index.ts +9 -0
- package/src/data/migrate/internal-definitions.ts +175 -0
- package/src/data/migrate/post-process.ts +73 -0
- package/src/data/migrate/pragma.ts +73 -0
- package/src/data/migrate/provision-framework.ts +147 -0
- package/src/data/migrate/rls-generate.ts +61 -0
- package/src/data/migrate/run.ts +124 -0
- package/src/data/migrate/safety.ts +167 -0
- package/src/data/migrate/snapshot.ts +368 -0
- package/src/data/migrate/sql-emit.ts +392 -0
- package/src/data/migrate/statements.ts +312 -0
- package/src/data/query/alias.ts +126 -0
- package/src/data/query/batch.ts +8 -0
- package/src/data/query/cache/core/cache.ts +78 -0
- package/src/data/query/cache/core/index.ts +1 -0
- package/src/data/query/cache/core/types.ts +29 -0
- package/src/data/query/cache/readme.md +230 -0
- package/src/data/query/casing.ts +76 -0
- package/src/data/query/collation.ts +11 -0
- package/src/data/query/column-builder.ts +370 -0
- package/src/data/query/column.ts +150 -0
- package/src/data/query/entity.ts +42 -0
- package/src/data/query/errors.ts +33 -0
- package/src/data/query/index.ts +14 -0
- package/src/data/query/logger.ts +47 -0
- package/src/data/query/operations.ts +45 -0
- package/src/data/query/pg-core/alias.ts +12 -0
- package/src/data/query/pg-core/checks.ts +32 -0
- package/src/data/query/pg-core/columns/all.ts +71 -0
- package/src/data/query/pg-core/columns/bigint.ts +110 -0
- package/src/data/query/pg-core/columns/bigserial.ts +128 -0
- package/src/data/query/pg-core/columns/boolean.ts +43 -0
- package/src/data/query/pg-core/columns/char.ts +85 -0
- package/src/data/query/pg-core/columns/cidr.ts +43 -0
- package/src/data/query/pg-core/columns/common.ts +366 -0
- package/src/data/query/pg-core/columns/custom.ts +232 -0
- package/src/data/query/pg-core/columns/date.common.ts +15 -0
- package/src/data/query/pg-core/columns/date.ts +112 -0
- package/src/data/query/pg-core/columns/double-precision.ts +55 -0
- package/src/data/query/pg-core/columns/enum.ts +201 -0
- package/src/data/query/pg-core/columns/index.ts +35 -0
- package/src/data/query/pg-core/columns/inet.ts +43 -0
- package/src/data/query/pg-core/columns/int.common.ts +57 -0
- package/src/data/query/pg-core/columns/integer.ts +53 -0
- package/src/data/query/pg-core/columns/interval.ts +83 -0
- package/src/data/query/pg-core/columns/json.ts +58 -0
- package/src/data/query/pg-core/columns/jsonb.ts +56 -0
- package/src/data/query/pg-core/columns/line.ts +117 -0
- package/src/data/query/pg-core/columns/macaddr.ts +43 -0
- package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
- package/src/data/query/pg-core/columns/numeric.ts +227 -0
- package/src/data/query/pg-core/columns/point.ts +127 -0
- package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
- package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
- package/src/data/query/pg-core/columns/real.ts +58 -0
- package/src/data/query/pg-core/columns/serial.ts +55 -0
- package/src/data/query/pg-core/columns/smallint.ts +53 -0
- package/src/data/query/pg-core/columns/smallserial.ts +60 -0
- package/src/data/query/pg-core/columns/text.ts +67 -0
- package/src/data/query/pg-core/columns/time.ts +72 -0
- package/src/data/query/pg-core/columns/timestamp.ts +163 -0
- package/src/data/query/pg-core/columns/uuid.ts +51 -0
- package/src/data/query/pg-core/columns/varchar.ts +89 -0
- package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
- package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
- package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
- package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
- package/src/data/query/pg-core/db.ts +674 -0
- package/src/data/query/pg-core/dialect.ts +1404 -0
- package/src/data/query/pg-core/expressions.ts +25 -0
- package/src/data/query/pg-core/foreign-keys.ts +130 -0
- package/src/data/query/pg-core/index.ts +21 -0
- package/src/data/query/pg-core/indexes.ts +276 -0
- package/src/data/query/pg-core/policies.ts +56 -0
- package/src/data/query/pg-core/primary-keys.ts +62 -0
- package/src/data/query/pg-core/query-builders/count.ts +86 -0
- package/src/data/query/pg-core/query-builders/delete.ts +293 -0
- package/src/data/query/pg-core/query-builders/index.ts +7 -0
- package/src/data/query/pg-core/query-builders/insert.ts +452 -0
- package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
- package/src/data/query/pg-core/query-builders/query.ts +157 -0
- package/src/data/query/pg-core/query-builders/raw.ts +49 -0
- package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
- package/src/data/query/pg-core/query-builders/select.ts +1346 -0
- package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
- package/src/data/query/pg-core/query-builders/update.ts +626 -0
- package/src/data/query/pg-core/roles.ts +41 -0
- package/src/data/query/pg-core/schema.ts +72 -0
- package/src/data/query/pg-core/sequence.ts +41 -0
- package/src/data/query/pg-core/session.ts +292 -0
- package/src/data/query/pg-core/subquery.ts +46 -0
- package/src/data/query/pg-core/table.ts +252 -0
- package/src/data/query/pg-core/unique-constraint.ts +73 -0
- package/src/data/query/pg-core/utils/array.ts +95 -0
- package/src/data/query/pg-core/utils/index.ts +1 -0
- package/src/data/query/pg-core/utils.ts +104 -0
- package/src/data/query/pg-core/view-base.ts +14 -0
- package/src/data/query/pg-core/view-common.ts +1 -0
- package/src/data/query/pg-core/view.ts +435 -0
- package/src/data/query/pg-core/with-builder.ts +41 -0
- package/src/data/query/postgres-js/README.md +46 -0
- package/src/data/query/postgres-js/array-parser.ts +45 -0
- package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
- package/src/data/query/postgres-js/driver.ts +163 -0
- package/src/data/query/postgres-js/index.ts +2 -0
- package/src/data/query/postgres-js/session.ts +234 -0
- package/src/data/query/primary-key.ts +11 -0
- package/src/data/query/query-builders/query-builder.ts +19 -0
- package/src/data/query/query-builders/select.types.ts +175 -0
- package/src/data/query/query-promise.ts +35 -0
- package/src/data/query/relations.ts +725 -0
- package/src/data/query/runnable-query.ts +12 -0
- package/src/data/query/selection-proxy.ts +121 -0
- package/src/data/query/session.ts +8 -0
- package/src/data/query/sql/expressions/conditions.ts +741 -0
- package/src/data/query/sql/expressions/index.ts +2 -0
- package/src/data/query/sql/expressions/select.ts +45 -0
- package/src/data/query/sql/functions/aggregate.ts +129 -0
- package/src/data/query/sql/functions/index.ts +2 -0
- package/src/data/query/sql/functions/vector.ts +178 -0
- package/src/data/query/sql/index.ts +3 -0
- package/src/data/query/sql/sql.ts +794 -0
- package/src/data/query/subquery.ts +53 -0
- package/src/data/query/table.ts +208 -0
- package/src/data/query/table.utils.ts +2 -0
- package/src/data/query/tracing-utils.ts +3 -0
- package/src/data/query/tracing.ts +11 -0
- package/src/data/query/utils.ts +357 -0
- package/src/data/query/version.ts +4 -0
- package/src/data/query/view-common.ts +1 -0
- package/src/data/schema/branded.ts +13 -0
- package/src/data/schema/generated/coerce.ts +5 -0
- package/src/data/schema/generated/combinator.ts +6 -0
- package/src/data/schema/generated/encoding.ts +5 -0
- package/src/data/schema/generated/error.ts +6 -0
- package/src/data/schema/generated/format.ts +5 -0
- package/src/data/schema/generated/type-constructor.ts +10 -0
- package/src/data/schema/generated/type.ts +6 -0
- package/src/data/schema/index.ts +6 -0
- package/src/data/schema/json-schema.ts +33 -0
- package/src/data/schema/namespace.ts +55 -0
- package/src/data/schema/parse.ts +39 -0
- package/src/data/schema/types.ts +39 -0
- package/src/data/temporal/append-only.ts +134 -0
- package/src/data/temporal/close.ts +27 -0
- package/src/data/temporal/columns.ts +10 -0
- package/src/data/temporal/definition-queries.ts +70 -0
- package/src/data/temporal/errors.ts +10 -0
- package/src/data/temporal/get-current.ts +22 -0
- package/src/data/temporal/index.ts +9 -0
- package/src/data/temporal/namespace.ts +36 -0
- package/src/data/temporal/query.ts +25 -0
- package/src/data/temporal/resolve.ts +17 -0
- package/src/data/temporal/revise.ts +67 -0
- package/src/data/temporal/types.ts +22 -0
- package/src/data/valve/artifact.ts +33 -0
- package/src/data/valve/contract-diff.ts +21 -0
- package/src/data/valve/cross.ts +57 -0
- package/src/data/valve/crossing-ledger.ts +50 -0
- package/src/data/valve/decision.ts +44 -0
- package/src/data/valve/durable-crossing.ts +52 -0
- package/src/data/valve/execute-crossing.ts +77 -0
- package/src/data/valve/faucet.ts +56 -0
- package/src/data/valve/gate.ts +114 -0
- package/src/data/valve/graph.ts +77 -0
- package/src/data/valve/index.ts +53 -0
- package/src/data/valve/namespace.ts +57 -0
- package/src/data/valve/registry.ts +50 -0
- package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
- package/src/data/valve/schema/valve-crossing.ts +9 -0
- package/src/data/valve/supply.ts +154 -0
- package/src/data/valve/types.ts +143 -0
- package/src/data/vector/columns.ts +27 -0
- package/src/data/vector/index.ts +4 -0
- package/src/data/vector/namespace.ts +33 -0
- package/src/data/vector/query.ts +85 -0
- package/src/data/vector/validate.ts +16 -0
- package/src/data/write/audit/actor.ts +15 -0
- package/src/data/write/audit/audit-columns.ts +18 -0
- package/src/data/write/audit/audit-select.ts +111 -0
- package/src/data/write/audit/bound-transaction.ts +14 -0
- package/src/data/write/audit/cte-compile.ts +32 -0
- package/src/data/write/audit/decode-returning.ts +47 -0
- package/src/data/write/audit/final-select.ts +43 -0
- package/src/data/write/audit/infra-audit.ts +36 -0
- package/src/data/write/audit/primary-key.ts +65 -0
- package/src/data/write/audit/projection.ts +41 -0
- package/src/data/write/audit/read-touches.ts +86 -0
- package/src/data/write/audit/touch-match.ts +78 -0
- package/src/data/write/audit/verb-delete.ts +62 -0
- package/src/data/write/audit/verb-insert.ts +36 -0
- package/src/data/write/audit/verb-select.ts +47 -0
- package/src/data/write/audit/verb-update.ts +60 -0
- package/src/data/write/audit/verb-upsert.ts +104 -0
- package/src/data/write/conflict.ts +43 -0
- package/src/data/write/delete.ts +38 -0
- package/src/data/write/execute.ts +243 -0
- package/src/data/write/index.ts +11 -0
- package/src/data/write/insert.ts +63 -0
- package/src/data/write/state.ts +43 -0
- package/src/data/write/update.ts +54 -0
- package/src/data/write/upsert.ts +58 -0
- package/src/data/write/values.ts +19 -0
- package/src/index.ts +230 -0
- package/src/infra/access-links/code.ts +70 -0
- package/src/infra/access-links/handle.ts +892 -0
- package/src/infra/access-links/index.ts +9 -0
- package/src/infra/access-links/namespace.ts +30 -0
- package/src/infra/access-links/person-check.ts +177 -0
- package/src/infra/access-links/schema.ts +71 -0
- package/src/infra/alerts/async-sources.ts +231 -0
- package/src/infra/alerts/audit.ts +36 -0
- package/src/infra/alerts/beat.ts +123 -0
- package/src/infra/alerts/client-faults.ts +111 -0
- package/src/infra/alerts/faults.ts +156 -0
- package/src/infra/alerts/handle.ts +778 -0
- package/src/infra/alerts/http.ts +192 -0
- package/src/infra/alerts/index.ts +12 -0
- package/src/infra/alerts/infra-sources.ts +180 -0
- package/src/infra/alerts/namespace.ts +81 -0
- package/src/infra/alerts/redact.ts +21 -0
- package/src/infra/alerts/request-faults.ts +41 -0
- package/src/infra/alerts/schema.ts +124 -0
- package/src/infra/alerts/sinks.ts +125 -0
- package/src/infra/alerts/source-options.ts +26 -0
- package/src/infra/alerts/sources.ts +503 -0
- package/src/infra/alerts/subsystem-sources.ts +137 -0
- package/src/infra/audit/emit.ts +87 -0
- package/src/infra/audit/index.ts +2 -0
- package/src/infra/audit/namespace.ts +22 -0
- package/src/infra/audit/schema.sql +35 -0
- package/src/infra/audit/types.ts +18 -0
- package/src/infra/aws/credentials.ts +147 -0
- package/src/infra/aws/sigv4.ts +140 -0
- package/src/infra/cache/create.ts +58 -0
- package/src/infra/cache/entry.ts +81 -0
- package/src/infra/cache/index.ts +4 -0
- package/src/infra/cache/namespace.ts +10 -0
- package/src/infra/cache/tenant.ts +93 -0
- package/src/infra/cache/types.ts +24 -0
- package/src/infra/crawl/audit.ts +35 -0
- package/src/infra/crawl/errors.ts +25 -0
- package/src/infra/crawl/handle.ts +693 -0
- package/src/infra/crawl/index.ts +9 -0
- package/src/infra/crawl/links.ts +160 -0
- package/src/infra/crawl/namespace.ts +36 -0
- package/src/infra/crawl/robots.ts +100 -0
- package/src/infra/crawl/schema.ts +89 -0
- package/src/infra/credit-line/handle.ts +883 -0
- package/src/infra/credit-line/index.ts +6 -0
- package/src/infra/credit-line/namespace.ts +7 -0
- package/src/infra/credit-line/schema.ts +96 -0
- package/src/infra/credits/handle.ts +706 -0
- package/src/infra/credits/index.ts +5 -0
- package/src/infra/credits/namespace.ts +7 -0
- package/src/infra/credits/schema.ts +83 -0
- package/src/infra/email/audit.ts +36 -0
- package/src/infra/email/inbound.ts +622 -0
- package/src/infra/email/index.ts +11 -0
- package/src/infra/email/mail.ts +517 -0
- package/src/infra/email/mime.ts +447 -0
- package/src/infra/email/namespace.ts +22 -0
- package/src/infra/email/providers.ts +111 -0
- package/src/infra/email/schema.ts +96 -0
- package/src/infra/email/sns.ts +200 -0
- package/src/infra/email/unsubscribe.ts +44 -0
- package/src/infra/fetch/address.ts +88 -0
- package/src/infra/fetch/connect-retry.ts +87 -0
- package/src/infra/fetch/handle.ts +329 -0
- package/src/infra/fetch/index.ts +7 -0
- package/src/infra/fetch/namespace.ts +70 -0
- package/src/infra/idempotency/index.ts +3 -0
- package/src/infra/idempotency/middleware.ts +87 -0
- package/src/infra/idempotency/store.ts +92 -0
- package/src/infra/idempotency/types.ts +30 -0
- package/src/infra/join-requests/handle.ts +312 -0
- package/src/infra/join-requests/index.ts +2 -0
- package/src/infra/join-requests/namespace.ts +7 -0
- package/src/infra/join-requests/schema.ts +33 -0
- package/src/infra/outbox/core.ts +165 -0
- package/src/infra/quota/errors.ts +32 -0
- package/src/infra/quota/handle.ts +215 -0
- package/src/infra/quota/index.ts +5 -0
- package/src/infra/quota/namespace.ts +7 -0
- package/src/infra/quota/schema.ts +15 -0
- package/src/infra/storage/backend.ts +34 -0
- package/src/infra/storage/handle.ts +513 -0
- package/src/infra/storage/index.ts +9 -0
- package/src/infra/storage/local.ts +104 -0
- package/src/infra/storage/namespace.ts +13 -0
- package/src/infra/storage/s3.ts +257 -0
- package/src/infra/storage/schema.ts +24 -0
- package/src/infra/support/handle.ts +854 -0
- package/src/infra/support/http.ts +229 -0
- package/src/infra/support/index.ts +7 -0
- package/src/infra/support/namespace.ts +12 -0
- package/src/infra/support/schema.ts +96 -0
- package/src/infra/webhook/inbound.ts +45 -0
- package/src/infra/webhook/index.ts +5 -0
- package/src/infra/webhook/namespace.ts +24 -0
- package/src/infra/webhook/outbound.ts +315 -0
- package/src/infra/webhook/sign.ts +32 -0
- package/src/infra/webhook/verify.ts +82 -0
- package/src/invariants/assert.ts +26 -0
- package/src/invariants/index.ts +1 -0
- package/src/lint/config.ts +56 -0
- package/src/lint/doctrine.ts +163 -0
- package/src/lint/include.ts +97 -0
- package/src/lint/index.ts +3 -0
- package/src/lint/preset.ts +10 -0
- package/src/lint/resolver.ts +218 -0
- package/src/lint/rules/no-reexports.ts +46 -0
- package/src/request/behavior/audited.ts +85 -0
- package/src/request/behavior/cached.ts +62 -0
- package/src/request/behavior/compose.ts +28 -0
- package/src/request/behavior/index.ts +5 -0
- package/src/request/behavior/namespace.ts +16 -0
- package/src/request/behavior/types.ts +26 -0
- package/src/request/behavior/versioned.ts +54 -0
- package/src/request/context/create.ts +103 -0
- package/src/request/context/guard.ts +49 -0
- package/src/request/context/index.ts +4 -0
- package/src/request/context/run-in-transaction.ts +200 -0
- package/src/request/context/store.ts +28 -0
- package/src/request/context/types.ts +50 -0
- package/src/request/di/builder.ts +535 -0
- package/src/request/di/index.ts +8 -0
- package/src/request/di/resolve.ts +271 -0
- package/src/request/di/types.ts +262 -0
- package/src/request/fitting/namespace.ts +7 -0
- package/src/request/fixture/batch.ts +71 -0
- package/src/request/fixture/builder.ts +26 -0
- package/src/request/fixture/errors.ts +10 -0
- package/src/request/fixture/index.ts +8 -0
- package/src/request/fixture/namespace.ts +23 -0
- package/src/request/fixture/paginate.ts +44 -0
- package/src/request/fixture/to-handlers.ts +260 -0
- package/src/request/fixture/types.ts +79 -0
- package/src/request/flow/act-as.ts +78 -0
- package/src/request/flow/namespace.ts +17 -0
- package/src/request/http/error-step.ts +42 -0
- package/src/request/http/file.ts +141 -0
- package/src/request/http/index.ts +4 -0
- package/src/request/http/middleware.ts +141 -0
- package/src/request/http/namespace.ts +5 -0
- package/src/request/http/rate-limit-classes.ts +170 -0
- package/src/request/http/response.ts +82 -0
- package/src/request/http/server.ts +916 -0
- package/src/request/http/sniff.ts +261 -0
- package/src/request/http/types.ts +250 -0
- package/src/request/http/upload.ts +284 -0
- package/src/request/internal/text.ts +6 -0
- package/src/request/openapi/generate.ts +205 -0
- package/src/request/openapi/index.ts +2 -0
- package/src/request/openapi/namespace.ts +7 -0
- package/src/test/auth.ts +13 -0
- package/src/test/auto-setup.ts +45 -0
- package/src/test/context.ts +91 -0
- package/src/test/crawl-fixture.ts +157 -0
- package/src/test/discover-fork.ts +17 -0
- package/src/test/fingerprint.ts +135 -0
- package/src/test/globals.d.ts +7 -0
- package/src/test/index.ts +19 -0
- package/src/test/namespace.ts +87 -0
- package/src/test/plugin.ts +189 -0
- package/src/test/progress.ts +185 -0
- package/src/test/reap-after-run.ts +28 -0
- package/src/test/reap.ts +286 -0
- package/src/test/reset.ts +253 -0
- package/src/test/savepoints.ts +172 -0
- package/src/test/setup.ts +1422 -0
- package/src/test/system-channel.ts +50 -0
- package/src/test/vitest-plugin.ts +142 -0
- package/src/test/vitest-workspace.ts +507 -0
- package/src/test/vitest.ts +333 -0
- package/src/test/watch.ts +105 -0
- package/src/trace/capture/bounded-serializer.ts +146 -0
- package/src/trace/cardinality/analyze.ts +173 -0
- package/src/trace/cardinality/branch-context.ts +214 -0
- package/src/trace/cardinality/check.ts +33 -0
- package/src/trace/cardinality/classify-edge.ts +64 -0
- package/src/trace/cardinality/structural-max.ts +107 -0
- package/src/trace/check/coverage-check.ts +188 -0
- package/src/trace/check/flow-check.ts +103 -0
- package/src/trace/check/flow-step-site.ts +234 -0
- package/src/trace/check/io-pattern.ts +116 -0
- package/src/trace/codemod/add-cardinality.ts +152 -0
- package/src/trace/codemod/run.ts +80 -0
- package/src/trace/entry/child-trace.ts +21 -0
- package/src/trace/entry/close-trace.ts +87 -0
- package/src/trace/entry/finalize-trace.ts +28 -0
- package/src/trace/entry/open-trace.ts +138 -0
- package/src/trace/entry/parent-trace.ts +20 -0
- package/src/trace/entry/parse-traceparent.ts +39 -0
- package/src/trace/entry/process-start.ts +38 -0
- package/src/trace/entry/trace-context.ts +26 -0
- package/src/trace/entry/tracing-enabled.ts +10 -0
- package/src/trace/external/record.ts +195 -0
- package/src/trace/external/report.ts +66 -0
- package/src/trace/namespace.ts +19 -0
- package/src/trace/partition/day.ts +47 -0
- package/src/trace/partition/index.ts +14 -0
- package/src/trace/partition/partition-name.ts +23 -0
- package/src/trace/partition/plan.ts +82 -0
- package/src/trace/partition/schedule.ts +26 -0
- package/src/trace/partition/startup-hook.ts +39 -0
- package/src/trace/partition/sync.ts +154 -0
- package/src/trace/partition/tables.ts +53 -0
- package/src/trace/read/tree.ts +83 -0
- package/src/trace/retain/copy-on-error.ts +76 -0
- package/src/trace/runtime/expire-stale-traces.ts +54 -0
- package/src/trace/runtime/flow-step.ts +147 -0
- package/src/trace/runtime/flush-steps.ts +31 -0
- package/src/trace/runtime/pending-writes.ts +54 -0
- package/src/trace/runtime/step-buffer.ts +298 -0
- package/src/trace/runtime/step-error.ts +21 -0
- package/src/trace/runtime/step-meta.ts +55 -0
- package/src/trace/runtime/thenable.ts +12 -0
- package/src/trace/runtime/truncate-trace.ts +62 -0
- package/src/trace/schema/trace-fields.ts +52 -0
- package/src/trace/schema/trace-retained.ts +22 -0
- package/src/trace/schema/trace-step-fields.ts +44 -0
- package/src/trace/schema/trace-step-retained.ts +25 -0
- package/src/trace/schema/trace-step.ts +18 -0
- package/src/trace/schema/trace.ts +12 -0
- package/src/trace/transformer/apply.ts +79 -0
- package/src/trace/transformer/build-meta-literal.ts +30 -0
- package/src/trace/transformer/find-flow-step-sites.ts +85 -0
- package/src/trace/transformer/inject-flow-step-import.ts +39 -0
- package/src/trace/transformer/transform-error.ts +14 -0
- package/src/trace/transformer/transform-flow-steps.ts +123 -0
- package/src/trace/transformer/wrap-function-declaration.ts +62 -0
- package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
- package/src/workspace/affected.ts +110 -0
- package/src/workspace/changed-files.ts +163 -0
- package/src/workspace/check-deps.ts +213 -0
- package/src/workspace/checksum.ts +20 -0
- package/src/workspace/dependency-graph.ts +67 -0
- package/src/workspace/detect-pipework-copies.ts +93 -0
- package/src/workspace/discover.ts +206 -0
- package/src/workspace/generate.ts +401 -0
- package/src/workspace/index.ts +7 -0
- package/src/workspace/module-imports-pipework.ts +39 -0
- package/src/workspace/module-package-name.ts +12 -0
- package/src/workspace/scrub-git-env.ts +23 -0
- package/src/workspace/source-paths.ts +36 -0
- package/src/workspace/sync.ts +182 -0
- package/dist/REFERENCE.md +0 -1477
- package/dist/infra/access-links/rate-limit.d.ts +0 -10
- package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
- package/dist/infra/access-links/rate-limit.js +0 -28
- package/dist/infra/access-links/rate-limit.js.map +0 -1
- package/dist/infra/email/sigv4.d.ts +0 -24
- package/dist/infra/email/sigv4.d.ts.map +0 -1
- package/dist/infra/email/sigv4.js +0 -55
- package/dist/infra/email/sigv4.js.map +0 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// `audit.chainHead` and `audit.verify` — the read side of the audit hash chain.
|
|
2
|
+
//
|
|
3
|
+
// Both act on the active Flow's tenant chain; outside a tenant scope they act
|
|
4
|
+
// on the chain of rows with no tenant. The recompute runs in SQL
|
|
5
|
+
// (`pipework_audit_record_canonical`), so what is checked is exactly what the
|
|
6
|
+
// trigger hashed.
|
|
7
|
+
|
|
8
|
+
import { auditRecord } from '../schema/audit-record.js'
|
|
9
|
+
import { pipeRaw } from '../../data/db/db.js'
|
|
10
|
+
import { sql } from '../../data/db/sql.js'
|
|
11
|
+
import type { DB } from '../../data/db/index.js'
|
|
12
|
+
import { withMaintenance } from '../../data/db/maintenance.js'
|
|
13
|
+
import { getContext } from '../../request/context/store.js'
|
|
14
|
+
import { ValidationError } from '../../core/errors/index.js'
|
|
15
|
+
import {
|
|
16
|
+
CHAIN_HEAD_TABLE,
|
|
17
|
+
CHAIN_RETIRED_TABLE,
|
|
18
|
+
CHAIN_KEY_SQL,
|
|
19
|
+
NULL_TENANT_CHAIN_KEY,
|
|
20
|
+
AUDIT_ERASE_SETTING,
|
|
21
|
+
} from './install.js'
|
|
22
|
+
import { auditChainInstalled, removeTenantChain } from './retire.js'
|
|
23
|
+
|
|
24
|
+
/** A chain position: the sequence number and the record's lowercase-hex SHA-256. */
|
|
25
|
+
export interface AuditChainHead {
|
|
26
|
+
readonly seq: number
|
|
27
|
+
readonly hash: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type AuditChainBreakReason =
|
|
31
|
+
| 'hash-mismatch'
|
|
32
|
+
| 'prev-mismatch'
|
|
33
|
+
| 'missing'
|
|
34
|
+
| 'head-mismatch'
|
|
35
|
+
| 'anchor-mismatch'
|
|
36
|
+
|
|
37
|
+
export interface AuditChainBreak {
|
|
38
|
+
readonly seq: number
|
|
39
|
+
/** The record at `seq`, or null when none exists. */
|
|
40
|
+
readonly recordId: string | null
|
|
41
|
+
readonly reason: AuditChainBreakReason
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AuditVerifyOptions {
|
|
45
|
+
/** Inclusive lower bound on the chain sequence. */
|
|
46
|
+
readonly fromSeq?: number
|
|
47
|
+
/** Inclusive upper bound. With any upper bound, the chain head is not compared. */
|
|
48
|
+
readonly toSeq?: number
|
|
49
|
+
/** Start at the first record with `occurredAt >= from`. */
|
|
50
|
+
readonly from?: Date
|
|
51
|
+
/** End at the last record with `occurredAt < to`. */
|
|
52
|
+
readonly to?: Date
|
|
53
|
+
/** A (seq, hash) pair the caller stored earlier; the chain must still hold it. */
|
|
54
|
+
readonly anchor?: AuditChainHead
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type AuditVerifyResult =
|
|
58
|
+
| { readonly ok: true; readonly checked: number; readonly head: AuditChainHead | null }
|
|
59
|
+
| { readonly ok: false; readonly checked: number; readonly break: AuditChainBreak }
|
|
60
|
+
|
|
61
|
+
const BATCH_SIZE = 1000
|
|
62
|
+
const GENESIS_HASH = '0'.repeat(64)
|
|
63
|
+
|
|
64
|
+
function chainKey(): string {
|
|
65
|
+
return getContext()?.tenant ?? NULL_TENANT_CHAIN_KEY
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toSeq(value: unknown): number {
|
|
69
|
+
return Number(value)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readHead(db: DB, key: string): Promise<AuditChainHead | null> {
|
|
73
|
+
const rows = await db.execute(sql`
|
|
74
|
+
SELECT seq, encode(hash, 'hex') AS hash FROM ${sql.raw(CHAIN_HEAD_TABLE)}
|
|
75
|
+
WHERE chain_key = ${key}::uuid AND seq > 0
|
|
76
|
+
`)
|
|
77
|
+
const row = rows[0]
|
|
78
|
+
return row === undefined ? null : { seq: toSeq(row['seq']), hash: row['hash'] as string }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The newest record's position in the tenant's chain, for the app to store
|
|
83
|
+
* outside the database. `null` when the chain has no records yet.
|
|
84
|
+
*/
|
|
85
|
+
export async function chainHead(): Promise<AuditChainHead | null> {
|
|
86
|
+
return readHead(pipeRaw(auditRecord.$options.database), chainKey())
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Walks the tenant's chain and reports the first break. Every seq in the
|
|
91
|
+
* checked range is examined: a record is recomputed from its fields, linked to
|
|
92
|
+
* the one before it, and a missing seq is a break.
|
|
93
|
+
*/
|
|
94
|
+
export async function verify(options: AuditVerifyOptions = {}): Promise<AuditVerifyResult> {
|
|
95
|
+
for (const [name, value] of [['fromSeq', options.fromSeq], ['toSeq', options.toSeq]] as const) {
|
|
96
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 1)) {
|
|
97
|
+
throw new ValidationError(`audit.verify: ${name} must be a positive integer (got ${String(value)}).`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const db = pipeRaw(auditRecord.$options.database)
|
|
101
|
+
const key = chainKey()
|
|
102
|
+
// Rows with no tenant are visible to no tenant setting, so the tenantless chain is read as a maintenance pass.
|
|
103
|
+
if (key === NULL_TENANT_CHAIN_KEY) return withMaintenance(db, tx => verifyChain(tx, key, options))
|
|
104
|
+
return verifyChain(db, key, options)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function verifyChain(db: DB, key: string, options: AuditVerifyOptions): Promise<AuditVerifyResult> {
|
|
108
|
+
const keyMatch = sql`${sql.raw(CHAIN_KEY_SQL)} = ${key}::uuid`
|
|
109
|
+
const bounded = options.toSeq !== undefined || options.to !== undefined
|
|
110
|
+
|
|
111
|
+
const head = await readHead(db, key)
|
|
112
|
+
|
|
113
|
+
// Resolve the range [lo, hi].
|
|
114
|
+
let lo = options.fromSeq ?? 1
|
|
115
|
+
if (options.fromSeq === undefined && options.from === undefined) {
|
|
116
|
+
const r = await db.execute(sql`
|
|
117
|
+
SELECT min(seq) AS seq FROM ${sql.raw(CHAIN_RETIRED_TABLE)} WHERE chain_key = ${key}::uuid
|
|
118
|
+
`)
|
|
119
|
+
if (r[0]?.['seq'] !== null && r[0]?.['seq'] !== undefined) lo = toSeq(r[0]['seq'])
|
|
120
|
+
}
|
|
121
|
+
const maxRow = await db.execute(sql`
|
|
122
|
+
SELECT max(chain_seq) AS seq FROM audit_record
|
|
123
|
+
WHERE ${keyMatch} AND chain_seq IS NOT NULL
|
|
124
|
+
`)
|
|
125
|
+
// Stop at the head read above: a row committed since then must not read as a head mismatch.
|
|
126
|
+
let hi = head !== null
|
|
127
|
+
? head.seq
|
|
128
|
+
: maxRow[0]?.['seq'] === null || maxRow[0]?.['seq'] === undefined ? 0 : toSeq(maxRow[0]['seq'])
|
|
129
|
+
if (options.from !== undefined) {
|
|
130
|
+
const r = await db.execute(sql`
|
|
131
|
+
SELECT min(chain_seq) AS seq FROM audit_record
|
|
132
|
+
WHERE ${keyMatch} AND chain_seq IS NOT NULL AND occurred_at >= ${options.from.toISOString()}::timestamptz
|
|
133
|
+
`)
|
|
134
|
+
if (r[0]?.['seq'] === null || r[0]?.['seq'] === undefined) return { ok: true, checked: 0, head }
|
|
135
|
+
lo = Math.max(lo, toSeq(r[0]['seq']))
|
|
136
|
+
}
|
|
137
|
+
if (options.to !== undefined) {
|
|
138
|
+
const r = await db.execute(sql`
|
|
139
|
+
SELECT max(chain_seq) AS seq FROM audit_record
|
|
140
|
+
WHERE ${keyMatch} AND chain_seq IS NOT NULL AND occurred_at < ${options.to.toISOString()}::timestamptz
|
|
141
|
+
`)
|
|
142
|
+
if (r[0]?.['seq'] === null || r[0]?.['seq'] === undefined) return { ok: true, checked: 0, head }
|
|
143
|
+
hi = Math.min(hi, toSeq(r[0]['seq']))
|
|
144
|
+
}
|
|
145
|
+
if (options.toSeq !== undefined) hi = options.toSeq
|
|
146
|
+
|
|
147
|
+
// Retired entries inside the range count as present; the one just below it is the first link.
|
|
148
|
+
const retiredRows = await db.execute(sql`
|
|
149
|
+
SELECT seq, encode(hash, 'hex') AS hash FROM ${sql.raw(CHAIN_RETIRED_TABLE)}
|
|
150
|
+
WHERE chain_key = ${key}::uuid AND seq >= ${lo - 1} AND seq <= ${hi}
|
|
151
|
+
`)
|
|
152
|
+
const retired = new Map<number, string>(retiredRows.map(r => [toSeq(r['seq']), r['hash'] as string]))
|
|
153
|
+
|
|
154
|
+
let last: string | undefined
|
|
155
|
+
if (lo === 1) last = GENESIS_HASH
|
|
156
|
+
else {
|
|
157
|
+
last = retired.get(lo - 1)
|
|
158
|
+
if (last === undefined) {
|
|
159
|
+
const r = await db.execute(sql`
|
|
160
|
+
SELECT hash FROM audit_record WHERE ${keyMatch} AND chain_seq = ${lo - 1}
|
|
161
|
+
`)
|
|
162
|
+
last = r[0]?.['hash'] as string | undefined
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let checked = 0
|
|
167
|
+
let expected = lo
|
|
168
|
+
let tail: AuditChainHead | null = null
|
|
169
|
+
const fail = (seq: number, recordId: string | null, reason: AuditChainBreakReason): AuditVerifyResult =>
|
|
170
|
+
({ ok: false, checked, break: { seq, recordId, reason } })
|
|
171
|
+
|
|
172
|
+
while (expected <= hi) {
|
|
173
|
+
const batch = await db.execute(sql`
|
|
174
|
+
SELECT a.chain_seq AS seq, a.id, a.prev_hash, a.hash,
|
|
175
|
+
encode(sha256(decode(a.prev_hash, 'hex') || convert_to(pipework_audit_record_canonical(a), 'UTF8')), 'hex') = a.hash AS intact
|
|
176
|
+
FROM audit_record a
|
|
177
|
+
WHERE ${keyMatch} AND a.chain_seq >= ${expected} AND a.chain_seq <= ${hi}
|
|
178
|
+
ORDER BY a.chain_seq, a.id
|
|
179
|
+
LIMIT ${BATCH_SIZE}
|
|
180
|
+
`)
|
|
181
|
+
if (batch.length === 0) {
|
|
182
|
+
for (; expected <= hi; expected++) {
|
|
183
|
+
const hash = retired.get(expected)
|
|
184
|
+
if (hash === undefined) return fail(expected, null, 'missing')
|
|
185
|
+
last = hash
|
|
186
|
+
}
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
for (const row of batch) {
|
|
190
|
+
const seq = toSeq(row['seq'])
|
|
191
|
+
const id = row['id'] as string
|
|
192
|
+
for (; expected < seq; expected++) {
|
|
193
|
+
const hash = retired.get(expected)
|
|
194
|
+
if (hash === undefined) return fail(expected, null, 'missing')
|
|
195
|
+
last = hash
|
|
196
|
+
}
|
|
197
|
+
if (seq < expected) return fail(seq, id, 'hash-mismatch')
|
|
198
|
+
if (row['intact'] !== true) return fail(seq, id, 'hash-mismatch')
|
|
199
|
+
if (last === undefined) return fail(seq - 1, null, 'missing')
|
|
200
|
+
if (row['prev_hash'] !== last) return fail(seq, id, 'prev-mismatch')
|
|
201
|
+
last = row['hash'] as string
|
|
202
|
+
tail = { seq, hash: last }
|
|
203
|
+
checked++
|
|
204
|
+
expected = seq + 1
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!bounded) {
|
|
209
|
+
const top = tail ?? await highestRetired(db, key)
|
|
210
|
+
const expectedHead = head
|
|
211
|
+
if ((top === null) !== (expectedHead === null) || (top !== null && expectedHead !== null &&
|
|
212
|
+
(top.seq !== expectedHead.seq || top.hash !== expectedHead.hash))) {
|
|
213
|
+
return fail(expectedHead?.seq ?? top?.seq ?? 0, null, 'head-mismatch')
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (options.anchor !== undefined) {
|
|
218
|
+
const { seq, hash } = options.anchor
|
|
219
|
+
const r = await db.execute(sql`
|
|
220
|
+
SELECT hash FROM audit_record WHERE ${keyMatch} AND chain_seq = ${seq}
|
|
221
|
+
UNION ALL
|
|
222
|
+
SELECT encode(hash, 'hex') FROM ${sql.raw(CHAIN_RETIRED_TABLE)} WHERE chain_key = ${key}::uuid AND seq = ${seq}
|
|
223
|
+
`)
|
|
224
|
+
if (!r.some(row => row['hash'] === hash)) return fail(seq, null, 'anchor-mismatch')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { ok: true, checked, head }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function highestRetired(db: DB, key: string): Promise<AuditChainHead | null> {
|
|
231
|
+
const rows = await db.execute(sql`
|
|
232
|
+
SELECT seq, encode(hash, 'hex') AS hash FROM ${sql.raw(CHAIN_RETIRED_TABLE)}
|
|
233
|
+
WHERE chain_key = ${key}::uuid ORDER BY seq DESC LIMIT 1
|
|
234
|
+
`)
|
|
235
|
+
const row = rows[0]
|
|
236
|
+
return row === undefined ? null : { seq: toSeq(row['seq']), hash: row['hash'] as string }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Deletes every audit record of one tenant, and its hash chain, for the closure
|
|
241
|
+
* of that tenant's account. Irreversible. Other tenants' records and chains are
|
|
242
|
+
* untouched and still verify. Returns the number of records removed; a second
|
|
243
|
+
* call returns 0. Runs as a maintenance pass naming this tenant, so the app
|
|
244
|
+
* role still cannot delete audit rows.
|
|
245
|
+
*/
|
|
246
|
+
export async function removeTenant(tenantId: string): Promise<number> {
|
|
247
|
+
if (typeof tenantId !== 'string' || tenantId.trim() === '') {
|
|
248
|
+
throw new ValidationError('audit.removeTenant: tenantId must be a non-empty string.')
|
|
249
|
+
}
|
|
250
|
+
const db = pipeRaw(auditRecord.$options.database)
|
|
251
|
+
return withMaintenance(db, async tx => {
|
|
252
|
+
await tx.execute(sql`SELECT set_config(${AUDIT_ERASE_SETTING}, ${tenantId}, true)`)
|
|
253
|
+
const removed = await tx.execute(sql`DELETE FROM audit_record WHERE tenant_id = ${tenantId}::uuid RETURNING 1`)
|
|
254
|
+
if (await auditChainInstalled(tx)) await removeTenantChain(tx, tenantId)
|
|
255
|
+
return removed.length
|
|
256
|
+
})
|
|
257
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Opaque cursor for audit-trail keyset pagination. Encodes `(occurredAt, id)`
|
|
2
|
+
// as a base64url JSON tuple — the same encoding the rest of pipework's
|
|
3
|
+
// cursor-paginated surfaces use. `occurredAt` is the row's time as text,
|
|
4
|
+
// UTC to the microsecond, because a JS Date holds only milliseconds and rows
|
|
5
|
+
// within one millisecond would be lost or repeated. `id` breaks ties between
|
|
6
|
+
// rows with the same time (a single CTE can emit N audit rows for fan-out
|
|
7
|
+
// targets), so the (occurredAt, id) pair is unique and totally ordered.
|
|
8
|
+
|
|
9
|
+
import { encodeCursor, decodeCursor } from '../../request/fixture/paginate.js'
|
|
10
|
+
import { ValidationError } from '../../core/errors/index.js'
|
|
11
|
+
|
|
12
|
+
export interface AuditCursor {
|
|
13
|
+
/** The row's time in UTC to the microsecond: `YYYY-MM-DDTHH:MM:SS.ffffffZ`. Older cursors carry milliseconds; both decode. */
|
|
14
|
+
readonly occurredAt: string
|
|
15
|
+
readonly id: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function encodeAuditCursor(cursor: AuditCursor): string {
|
|
19
|
+
return encodeCursor([cursor.occurredAt, cursor.id])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function decodeAuditCursor(raw: string): AuditCursor {
|
|
23
|
+
const parts = decodeCursor(raw)
|
|
24
|
+
if (parts.length !== 2 || typeof parts[0] !== 'string' || typeof parts[1] !== 'string') {
|
|
25
|
+
throw new ValidationError(
|
|
26
|
+
'Invalid audit cursor: expected [occurredAt, id] tuple. ' +
|
|
27
|
+
'Cursors are opaque — pass the value returned by audit.query unchanged.',
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
if (Number.isNaN(new Date(parts[0]).getTime())) {
|
|
31
|
+
throw new ValidationError(
|
|
32
|
+
`Invalid audit cursor: "${parts[0]}" is not a valid ISO timestamp. ` +
|
|
33
|
+
'Cursors are opaque — pass the value returned by audit.query unchanged.',
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
return { occurredAt: parts[0], id: parts[1] }
|
|
37
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { query } from './query.js'
|
|
2
|
+
export type {
|
|
3
|
+
AuditQueryFilters,
|
|
4
|
+
AuditQueryOptions,
|
|
5
|
+
AuditQueryResult,
|
|
6
|
+
AuditRecordRow,
|
|
7
|
+
} from './query.js'
|
|
8
|
+
export { encodeAuditCursor, decodeAuditCursor } from './cursor.js'
|
|
9
|
+
export type { AuditCursor } from './cursor.js'
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// `audit.query` — the read side of the audit-trail substrate. Plumb (and any
|
|
2
|
+
// other consumer) builds its audit UI against this contract; the column
|
|
3
|
+
// shape of `audit_record` is the schema-versioned commitment, and `query`
|
|
4
|
+
// is the surface for filtering and paging over it.
|
|
5
|
+
//
|
|
6
|
+
// Tenant scoping is implicit — the function uses `pipe()`, which applies the
|
|
7
|
+
// active Flow's tenant filter at the proxy layer. Run outside a tenant scope
|
|
8
|
+
// for cross-tenant visibility (admin / system contexts).
|
|
9
|
+
//
|
|
10
|
+
// Pagination is keyset on `(occurredAt, id)` — stable under inserts, no
|
|
11
|
+
// `OFFSET` cost. Default order is descending (newest first); `asc: true`
|
|
12
|
+
// inverts both the ORDER BY and the cursor inequality.
|
|
13
|
+
|
|
14
|
+
import { auditRecord } from '../schema/audit-record.js'
|
|
15
|
+
import { pipeRaw } from '../../data/db/db.js'
|
|
16
|
+
import { getContext } from '../../request/context/store.js'
|
|
17
|
+
import {
|
|
18
|
+
and,
|
|
19
|
+
eq,
|
|
20
|
+
gt,
|
|
21
|
+
gte,
|
|
22
|
+
lt,
|
|
23
|
+
lte,
|
|
24
|
+
inArray,
|
|
25
|
+
desc,
|
|
26
|
+
asc,
|
|
27
|
+
or,
|
|
28
|
+
getTableColumns,
|
|
29
|
+
} from '../../data/query/index.js'
|
|
30
|
+
import { sql } from '../../data/db/sql.js'
|
|
31
|
+
import type { SQL, Column } from '../../data/db/types.js'
|
|
32
|
+
import { ValidationError } from '../../core/errors/index.js'
|
|
33
|
+
import {
|
|
34
|
+
encodeAuditCursor,
|
|
35
|
+
decodeAuditCursor,
|
|
36
|
+
type AuditCursor,
|
|
37
|
+
} from './cursor.js'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Inferred row shape from the `audit_record` definition. The query result is
|
|
41
|
+
* typed against this — re-exported so consumers don't have to reach into
|
|
42
|
+
* `src/audit/schema/`.
|
|
43
|
+
*/
|
|
44
|
+
export type AuditRecordRow = typeof auditRecord.Select
|
|
45
|
+
|
|
46
|
+
export interface AuditQueryFilters {
|
|
47
|
+
/** Match audit rows whose `entity` is in this set. Single string accepted as shorthand. */
|
|
48
|
+
readonly entity?: string | readonly string[]
|
|
49
|
+
/**
|
|
50
|
+
* Match audit rows written for the row with this primary key — one row's history, served by an index on `(tenantId, entity, entityId, occurredAt, id)`. Requires `entity`. The text is the key's value, or for a composite key `audit.entityId(table, row)`; access-link rows carry the link id. Rows written before `entityId` existed, and read-touches rows, have none and never match.
|
|
51
|
+
*/
|
|
52
|
+
readonly entityId?: string
|
|
53
|
+
/** Match audit rows whose `verb` is in this set. Single string accepted as shorthand. */
|
|
54
|
+
readonly verb?: string | readonly string[]
|
|
55
|
+
/** Match audit rows whose `actorId` equals this. `null` matches system-actor rows. */
|
|
56
|
+
readonly actorId?: string | null
|
|
57
|
+
/** Match audit rows emitted under this trace. */
|
|
58
|
+
readonly traceId?: string
|
|
59
|
+
/** Match audit rows emitted under this specific flow step. */
|
|
60
|
+
readonly traceStepId?: string
|
|
61
|
+
/** Inclusive lower bound on `traceDay`. The partition-pruning hint Postgres uses to skip irrelevant partitions. */
|
|
62
|
+
readonly traceDayFrom?: string
|
|
63
|
+
/** Inclusive upper bound on `traceDay`. */
|
|
64
|
+
readonly traceDayTo?: string
|
|
65
|
+
/** Inclusive lower bound on `occurredAt`. */
|
|
66
|
+
readonly occurredFrom?: Date
|
|
67
|
+
/** Exclusive upper bound on `occurredAt`. */
|
|
68
|
+
readonly occurredTo?: Date
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface AuditQueryOptions {
|
|
72
|
+
/** Max rows to return. Default 50, hard cap 500 — exceeding it throws. */
|
|
73
|
+
readonly limit?: number
|
|
74
|
+
/** Opaque cursor from a previous page's result. */
|
|
75
|
+
readonly cursor?: string
|
|
76
|
+
/** `false` (default) returns newest-first; `true` returns oldest-first. */
|
|
77
|
+
readonly asc?: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface AuditQueryResult {
|
|
81
|
+
readonly rows: readonly AuditRecordRow[]
|
|
82
|
+
/** Opaque cursor for the next page; `null` when this page is the last. */
|
|
83
|
+
readonly cursor: string | null
|
|
84
|
+
/** `true` when more rows exist past this page. */
|
|
85
|
+
readonly hasMore: boolean
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const DEFAULT_LIMIT = 50
|
|
89
|
+
const MAX_LIMIT = 500
|
|
90
|
+
/** Selected alongside the table's columns: the row's time to the microsecond, which a JS Date cannot hold. */
|
|
91
|
+
const EXACT_TIME = '__occurredAtExact'
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Returns a page of `audit_record` rows matching `filters`, ordered by
|
|
95
|
+
* `(occurredAt, id)`. Pass `result.cursor` back as `options.cursor` to get
|
|
96
|
+
* the next page; stop when `cursor` is `null`.
|
|
97
|
+
*/
|
|
98
|
+
export async function query(
|
|
99
|
+
filters: AuditQueryFilters = {},
|
|
100
|
+
options: AuditQueryOptions = {},
|
|
101
|
+
): Promise<AuditQueryResult> {
|
|
102
|
+
const limit = resolveLimit(options.limit)
|
|
103
|
+
const ascending = options.asc === true
|
|
104
|
+
|
|
105
|
+
const table = auditRecord.table()
|
|
106
|
+
const cols = table as unknown as Record<string, Column>
|
|
107
|
+
|
|
108
|
+
if (filters.entityId !== undefined && (filters.entity === undefined || (Array.isArray(filters.entity) && filters.entity.length === 0))) {
|
|
109
|
+
throw new ValidationError(
|
|
110
|
+
'audit.query: entityId requires entity. A row id is only meaningful within one entity — ' +
|
|
111
|
+
'pass { entity: "Item", entityId } (use audit.entityId(table, row) for a composite key).',
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const conditions: SQL[] = []
|
|
116
|
+
appendFilter(conditions, filters.entity, v => buildInOrEq(cols['entity']!, v))
|
|
117
|
+
appendFilter(conditions, filters.verb, v => buildInOrEq(cols['verb']!, v))
|
|
118
|
+
if (filters.entityId !== undefined) conditions.push(eq(cols['entityId']!, filters.entityId))
|
|
119
|
+
if (filters.actorId !== undefined) {
|
|
120
|
+
conditions.push(eq(cols['actorId']!, filters.actorId))
|
|
121
|
+
}
|
|
122
|
+
if (filters.traceId !== undefined) conditions.push(eq(cols['traceId']!, filters.traceId))
|
|
123
|
+
if (filters.traceStepId !== undefined) conditions.push(eq(cols['traceStepId']!, filters.traceStepId))
|
|
124
|
+
if (filters.traceDayFrom !== undefined) conditions.push(gte(cols['traceDay']!, filters.traceDayFrom))
|
|
125
|
+
if (filters.traceDayTo !== undefined) conditions.push(lte(cols['traceDay']!, filters.traceDayTo))
|
|
126
|
+
if (filters.occurredFrom !== undefined) conditions.push(gte(cols['occurredAt']!, filters.occurredFrom))
|
|
127
|
+
if (filters.occurredTo !== undefined) conditions.push(lt(cols['occurredAt']!, filters.occurredTo))
|
|
128
|
+
|
|
129
|
+
if (options.cursor !== undefined) {
|
|
130
|
+
conditions.push(buildCursorClause(decodeAuditCursor(options.cursor), cols, ascending))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const tenant = getContext()?.tenant ?? null
|
|
134
|
+
if (tenant !== null) {
|
|
135
|
+
conditions.push(eq(cols['tenantId']!, tenant))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const exactTime = sql<string>`to_char(${cols['occurredAt']!} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`
|
|
139
|
+
const orderBy = ascending
|
|
140
|
+
? [asc(cols['occurredAt']!), asc(cols['id']!)]
|
|
141
|
+
: [desc(cols['occurredAt']!), desc(cols['id']!)]
|
|
142
|
+
|
|
143
|
+
const db = pipeRaw(auditRecord.$options.database)
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Drizzle's select/from/where chain is deeply generic; the column lookup is validated against the audit_record table at call time
|
|
145
|
+
let q: any = (db as any).select({ ...getTableColumns(table), [EXACT_TIME]: exactTime }).from(table)
|
|
146
|
+
if (conditions.length > 0) {
|
|
147
|
+
q = q.where(conditions.length === 1 ? conditions[0]! : and(...conditions))
|
|
148
|
+
}
|
|
149
|
+
q = q.orderBy(...orderBy).limit(limit + 1)
|
|
150
|
+
|
|
151
|
+
const fetched = (await q) as Array<AuditRecordRow & { [EXACT_TIME]: string }>
|
|
152
|
+
const hasMore = fetched.length > limit
|
|
153
|
+
const page = hasMore ? fetched.slice(0, limit) : fetched
|
|
154
|
+
const cursor = buildNextCursor(page, hasMore)
|
|
155
|
+
const items = page.map(row => {
|
|
156
|
+
const rest: Record<string, unknown> = { ...row }
|
|
157
|
+
delete rest[EXACT_TIME]
|
|
158
|
+
return rest as unknown as AuditRecordRow
|
|
159
|
+
})
|
|
160
|
+
return { rows: items, cursor, hasMore }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveLimit(requested: number | undefined): number {
|
|
164
|
+
if (requested === undefined) return DEFAULT_LIMIT
|
|
165
|
+
if (!Number.isInteger(requested) || requested < 1) {
|
|
166
|
+
throw new ValidationError(
|
|
167
|
+
`audit.query: limit must be a positive integer (got ${String(requested)}). ` +
|
|
168
|
+
`Default is ${DEFAULT_LIMIT}; max is ${MAX_LIMIT}.`,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
if (requested > MAX_LIMIT) {
|
|
172
|
+
throw new ValidationError(
|
|
173
|
+
`audit.query: limit ${requested} exceeds max ${MAX_LIMIT}. ` +
|
|
174
|
+
`Page through results instead of requesting a larger window.`,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
return requested
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function appendFilter<T>(
|
|
181
|
+
conditions: SQL[],
|
|
182
|
+
value: T | readonly T[] | undefined,
|
|
183
|
+
build: (value: T | readonly T[]) => SQL,
|
|
184
|
+
): void {
|
|
185
|
+
if (value === undefined) return
|
|
186
|
+
if (Array.isArray(value) && value.length === 0) return
|
|
187
|
+
conditions.push(build(value))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildInOrEq(column: Column, value: string | readonly string[]): SQL {
|
|
191
|
+
if (typeof value === 'string') return eq(column, value)
|
|
192
|
+
if (value.length === 1) return eq(column, value[0])
|
|
193
|
+
return inArray(column, [...value])
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Encodes the keyset lookahead `(occurredAt, id) < cursor` (or `>` when
|
|
198
|
+
* ascending). Postgres supports tuple comparison, but drizzle's operator set
|
|
199
|
+
* doesn't expose it directly, so we expand to the lexicographic form:
|
|
200
|
+
* occurredAt < $0 OR (occurredAt = $0 AND id < $1)
|
|
201
|
+
* The time is bound as text and cast in SQL so it keeps its microseconds.
|
|
202
|
+
*/
|
|
203
|
+
function buildCursorClause(cursor: AuditCursor, cols: Record<string, Column>, ascending: boolean): SQL {
|
|
204
|
+
const occurredAt = cols['occurredAt']!
|
|
205
|
+
const id = cols['id']!
|
|
206
|
+
const at = sql`${cursor.occurredAt}::timestamptz`
|
|
207
|
+
if (ascending) {
|
|
208
|
+
return or(
|
|
209
|
+
sql`${occurredAt} > ${at}`,
|
|
210
|
+
and(sql`${occurredAt} = ${at}`, gt(id, cursor.id))!,
|
|
211
|
+
)!
|
|
212
|
+
}
|
|
213
|
+
return or(
|
|
214
|
+
sql`${occurredAt} < ${at}`,
|
|
215
|
+
and(sql`${occurredAt} = ${at}`, lt(id, cursor.id))!,
|
|
216
|
+
)!
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildNextCursor(items: ReadonlyArray<{ readonly id: string; readonly [EXACT_TIME]: string }>, hasMore: boolean): string | null {
|
|
220
|
+
if (!hasMore || items.length === 0) return null
|
|
221
|
+
const last = items[items.length - 1]!
|
|
222
|
+
return encodeAuditCursor({ occurredAt: last[EXACT_TIME], id: last.id })
|
|
223
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The column shape for `audit_record` — the durable per-(entity × affected
|
|
2
|
+
// row) projection of `@touches`-annotated flow steps. The audit substrate
|
|
3
|
+
// shares the trace's day-partition key (`traceDay`) so audit rows file under
|
|
4
|
+
// the same calendar day as the trace step that emitted them, but the audit
|
|
5
|
+
// table has independent retention: long / indefinite, configured separately
|
|
6
|
+
// from the hot trace tables. There is no `_retained` cousin — audit_record
|
|
7
|
+
// IS the durable store.
|
|
8
|
+
//
|
|
9
|
+
// Each row is one (entity, affected db row) tuple from a write-touches CTE
|
|
10
|
+
// (write path) or a single emission at step completion (read-touches). The
|
|
11
|
+
// `before` / `after` jsonb columns carry server-side `.traceSafe()`
|
|
12
|
+
// projections — never raw rows. `capturedInputs` is a convenience copy of
|
|
13
|
+
// the originating trace_step's captured inputs so audit queries don't have
|
|
14
|
+
// to join. `occurredAt` records when the operation completed.
|
|
15
|
+
|
|
16
|
+
import { field } from '../../data/domain/index.js'
|
|
17
|
+
import { schema } from '../../data/schema/namespace.js'
|
|
18
|
+
import { trace } from '../../trace/schema/trace.js'
|
|
19
|
+
import { traceStep } from '../../trace/schema/trace-step.js'
|
|
20
|
+
|
|
21
|
+
export const auditRecordFields = {
|
|
22
|
+
id: field.uuid().brand('AuditRecordId').defaultRandom(),
|
|
23
|
+
// Partition key — the parent trace's `openedDay`. The audit row files under
|
|
24
|
+
// the same calendar day as its trace step; retention is independent of
|
|
25
|
+
// trace storage so the partition can outlive the trace it came from.
|
|
26
|
+
traceDay: field.date(),
|
|
27
|
+
// Logical reference to the originating trace. `enforced: false` means the
|
|
28
|
+
// brand is inherited (TraceId) but no FK constraint is emitted — trace's
|
|
29
|
+
// partition can retire while the audit row is still in retention.
|
|
30
|
+
traceId: field.uuid().references(trace, 'id', { enforced: false }),
|
|
31
|
+
// Logical reference to the originating trace step. Same retention argument
|
|
32
|
+
// as `traceId` — branded TraceStepId without an enforced FK.
|
|
33
|
+
traceStepId: field.uuid().references(traceStep, 'id', { enforced: false }),
|
|
34
|
+
tenantId: field.uuid().tenant().nullable(),
|
|
35
|
+
// The user who performed the action. Null for system-actor traces (cron,
|
|
36
|
+
// worker-claimed jobs without an auth principal). Plain text, not branded —
|
|
37
|
+
// audit lives downstream of auth, no need to enforce a userId brand here.
|
|
38
|
+
actorId: field.text().nullable(),
|
|
39
|
+
// The `@touches Entity:verb` annotation, split into entity and verb. Both
|
|
40
|
+
// non-null — a `@touches` annotation requires both halves.
|
|
41
|
+
entity: field.text(),
|
|
42
|
+
verb: field.text(),
|
|
43
|
+
// The audited row's primary key as text: the single key's value, or for a
|
|
44
|
+
// composite key a JSON array of the key values (as text, in key order). Null
|
|
45
|
+
// on read-touches rows and on rows written before the column existed.
|
|
46
|
+
// Indexed with the tenant and entity so one row's history is an index range.
|
|
47
|
+
entityId: field.text().nullable(),
|
|
48
|
+
// Server-side `.traceSafe()` projection of the row before/after the write.
|
|
49
|
+
// Both nullable: create-verb has no `before`, delete-verb has no `after`,
|
|
50
|
+
// read-touches has neither.
|
|
51
|
+
before: field.jsonb(schema.check.json()).nullable(),
|
|
52
|
+
after: field.jsonb(schema.check.json()).nullable(),
|
|
53
|
+
// Convenience copy of the emitting trace_step's `captured_inputs`, so audit
|
|
54
|
+
// queries don't have to join into the (possibly already-retired) trace_step
|
|
55
|
+
// partition. Null when the step had no Tier-2 capture (no `meta.capture`,
|
|
56
|
+
// or `captureInputs: false`).
|
|
57
|
+
capturedInputs: field.jsonb(schema.check.json()).nullable(),
|
|
58
|
+
occurredAt: field.timestamp(),
|
|
59
|
+
// Hash chain (opt-in, blueprint `audit.chain`). Written only by the
|
|
60
|
+
// `audit_record` insert trigger; null on rows written before chaining was
|
|
61
|
+
// enabled. `prevHash` / `hash` are lowercase hex SHA-256.
|
|
62
|
+
chainSeq: field.bigint().nullable(),
|
|
63
|
+
prevHash: field.text().nullable(),
|
|
64
|
+
hash: field.text().nullable(),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Fields the chain trigger owns — never part of an application INSERT's column list. */
|
|
68
|
+
export const AUDIT_CHAIN_FIELD_KEYS: ReadonlySet<string> = new Set(['chainSeq', 'prevHash', 'hash'])
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The `audit_record` table — durable per-(entity × affected row) audit
|
|
2
|
+
// projection. RANGE-partitioned by `traceDay`; retention is independent of
|
|
3
|
+
// the trace tables (long / indefinite, configured separately). The `traceId`
|
|
4
|
+
// and `traceStepId` columns declare a logical reference (`enforced: false`)
|
|
5
|
+
// to inherit the TraceId/TraceStepId brands without emitting an FK — the
|
|
6
|
+
// audit partition is free to outlive its source trace partition.
|
|
7
|
+
|
|
8
|
+
import { define } from '../../data/domain/index.js'
|
|
9
|
+
import { index } from '../../data/db/idx/index.js'
|
|
10
|
+
import { auditRecordFields } from './audit-record-fields.js'
|
|
11
|
+
|
|
12
|
+
/** One row's history: `audit.query({ entity, entityId })` is a range scan of this index. */
|
|
13
|
+
export const AUDIT_ENTITY_INDEX = 'audit_record_entity_idx'
|
|
14
|
+
export const AUDIT_ENTITY_INDEX_SQL =
|
|
15
|
+
`CREATE INDEX IF NOT EXISTS ${AUDIT_ENTITY_INDEX} ON audit_record (tenant_id, entity, entity_id, occurred_at, id)`
|
|
16
|
+
|
|
17
|
+
export const auditRecord = define('audit_record', auditRecordFields, {
|
|
18
|
+
primaryKey: ['id', 'traceDay'],
|
|
19
|
+
partitionBy: { strategy: 'range', column: 'traceDay' },
|
|
20
|
+
indexes: (t) => ({
|
|
21
|
+
entity: index(AUDIT_ENTITY_INDEX).on(t['tenantId'], t['entity'], t['entityId'], t['occurredAt'], t['id']),
|
|
22
|
+
}),
|
|
23
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { sql } from '../../data/db/sql.js'
|
|
2
|
+
import type { DB } from '../../data/db/index.js'
|
|
3
|
+
import { getContext } from '../../request/context/store.js'
|
|
4
|
+
import { getBaseLogger } from '../../core/logging/index.js'
|
|
5
|
+
import { recordFault, faultMessage } from '../../infra/alerts/faults.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Writes an audit record with no tenant and no actor (the person is not yet signed in)
|
|
9
|
+
* under the Flow's open trace step; does nothing when no step is open. A failure is logged, not thrown.
|
|
10
|
+
*/
|
|
11
|
+
export async function auditUnauthenticated(
|
|
12
|
+
db: DB, entity: string, verb: string, entityId: string | null, details: Record<string, unknown>,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const flow = getContext()
|
|
15
|
+
const trace = flow?.trace
|
|
16
|
+
const stepId = trace?.buffer.openStepId ?? null
|
|
17
|
+
if (trace === undefined || trace === null || stepId === null) return
|
|
18
|
+
try {
|
|
19
|
+
await db.transaction(async (sp: DB) => {
|
|
20
|
+
await sp.execute(sql`
|
|
21
|
+
INSERT INTO audit_record
|
|
22
|
+
(trace_day, trace_id, trace_step_id, tenant_id, actor_id, entity, verb, entity_id, after, occurred_at)
|
|
23
|
+
VALUES (${trace.openedDay}, ${trace.id}, ${stepId}, NULL, NULL,
|
|
24
|
+
${entity}, ${verb}, ${entityId}, ${JSON.stringify(details)}::jsonb, now())
|
|
25
|
+
`)
|
|
26
|
+
})
|
|
27
|
+
} catch (err) {
|
|
28
|
+
getBaseLogger().error({ err, traceId: trace.id }, `Failed to write the ${entity} ${verb} audit record`)
|
|
29
|
+
void recordFault('audit-write', faultMessage(err), { entity, verb })
|
|
30
|
+
}
|
|
31
|
+
}
|