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,892 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { openChildTrace } from '../../trace/entry/child-trace.js'
|
|
3
|
+
import { finalizeTrace } from '../../trace/entry/finalize-trace.js'
|
|
4
|
+
import { sql } from '../../data/db/sql.js'
|
|
5
|
+
import { pipe, pipeRaw, pipeSystem } from '../../data/db/db.js'
|
|
6
|
+
import type { DB, SQL } from '../../data/db/index.js'
|
|
7
|
+
import { requireContext, runInContext } from '../../request/context/store.js'
|
|
8
|
+
import { bindTransaction } from '../../request/context/guard.js'
|
|
9
|
+
import { propagateLocals } from '../../request/context/run-in-transaction.js'
|
|
10
|
+
import type { Flow } from '../../request/context/types.js'
|
|
11
|
+
import type { HttpResponse } from '../../request/http/types.js'
|
|
12
|
+
import { writeInfraAudit } from '../../data/write/audit/infra-audit.js'
|
|
13
|
+
import { resolveActorId } from '../../data/write/audit/actor.js'
|
|
14
|
+
import { getBaseLogger } from '../../core/logging/index.js'
|
|
15
|
+
import { recordFault, faultMessage } from '../alerts/faults.js'
|
|
16
|
+
import { ValidationError } from '../../core/errors/index.js'
|
|
17
|
+
import { inTenant as runAsTenant } from '../../auth/tenant/in-tenant.js'
|
|
18
|
+
import { encodeCursor, decodeCursor } from '../../request/fixture/paginate.js'
|
|
19
|
+
import { createCodeGenerator, type CodeConfig } from './code.js'
|
|
20
|
+
|
|
21
|
+
export interface RateLimitConfig {
|
|
22
|
+
readonly max: number
|
|
23
|
+
readonly windowMs: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AccessLinkTarget {
|
|
27
|
+
readonly type: string
|
|
28
|
+
readonly id: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A link as its tenant sees it — includes the code and the app's `checkData`. */
|
|
32
|
+
export interface AccessLink {
|
|
33
|
+
readonly id: string
|
|
34
|
+
readonly tenantId: string
|
|
35
|
+
readonly code: string
|
|
36
|
+
readonly target: AccessLinkTarget
|
|
37
|
+
readonly permissions: readonly string[]
|
|
38
|
+
readonly note: string | null
|
|
39
|
+
readonly createdBy: string | null
|
|
40
|
+
readonly createdAt: Date
|
|
41
|
+
readonly expiresAt: Date | null
|
|
42
|
+
readonly maxUses: number | null
|
|
43
|
+
readonly requireChecks: readonly string[]
|
|
44
|
+
readonly checkData: unknown
|
|
45
|
+
readonly revokedAt: Date | null
|
|
46
|
+
readonly revokedBy: string | null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A link as a public route sees it — never `checkData`, never the code. */
|
|
50
|
+
export interface PublicLink {
|
|
51
|
+
readonly id: string
|
|
52
|
+
readonly target: AccessLinkTarget
|
|
53
|
+
readonly permissions: readonly string[]
|
|
54
|
+
readonly note: string | null
|
|
55
|
+
readonly expiresAt: Date | null
|
|
56
|
+
readonly maxUses: number | null
|
|
57
|
+
readonly usesCompleted: number
|
|
58
|
+
readonly requireChecks: readonly string[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface AccessLinkUse {
|
|
62
|
+
readonly id: string
|
|
63
|
+
readonly linkId: string
|
|
64
|
+
readonly kind: string
|
|
65
|
+
readonly status: 'pending' | 'completed' | 'abandoned'
|
|
66
|
+
readonly startedAt: Date
|
|
67
|
+
readonly reservedUntil: Date
|
|
68
|
+
readonly completedAt: Date | null
|
|
69
|
+
readonly client: string | null
|
|
70
|
+
readonly location: string | null
|
|
71
|
+
/** App-defined detail given to `beginUse` / `completeUse` (shallow-merged); `null` when none was given. */
|
|
72
|
+
readonly detail: Readonly<Record<string, unknown>> | null
|
|
73
|
+
/** When `claimUse` took this use; `null` while unclaimed. */
|
|
74
|
+
readonly claimedAt: Date | null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface AccessLinkCheckArgs {
|
|
78
|
+
readonly link: AccessLink
|
|
79
|
+
readonly proof: unknown
|
|
80
|
+
readonly client: string | null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type AccessLinkCheck = (args: AccessLinkCheckArgs) => Promise<boolean>
|
|
84
|
+
|
|
85
|
+
export interface AccessLinksConfig {
|
|
86
|
+
/** The app's permission names, e.g. `['view', 'download']`. */
|
|
87
|
+
readonly permissions: readonly string[]
|
|
88
|
+
readonly code?: CodeConfig
|
|
89
|
+
/** Pre-use verifiers by name; a link lists the ones it requires in `requireChecks`. */
|
|
90
|
+
readonly checks?: Readonly<Record<string, AccessLinkCheck>>
|
|
91
|
+
/** How long a begun use holds its slot before it frees. Default 15 minutes. */
|
|
92
|
+
readonly reservationMs?: number
|
|
93
|
+
/** Per-client limit on codes that match no link, counted in Postgres across processes. Default 30 misses per 60 s; a client over it is refused for every code. `false` turns it off. */
|
|
94
|
+
readonly resolveRateLimit?: RateLimitConfig | false
|
|
95
|
+
/** Called for every code that is not found — the app's guessing signal. */
|
|
96
|
+
readonly onMiss?: (info: { client: string | null; code: string }) => void
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface LinkOptions {
|
|
100
|
+
readonly target: AccessLinkTarget
|
|
101
|
+
readonly permissions: readonly string[]
|
|
102
|
+
readonly note?: string
|
|
103
|
+
readonly expiresAt?: Date
|
|
104
|
+
readonly maxUses?: number
|
|
105
|
+
readonly requireChecks?: readonly string[]
|
|
106
|
+
readonly checkData?: unknown
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type LinkState = 'not-found' | 'revoked' | 'expired' | 'exhausted'
|
|
110
|
+
|
|
111
|
+
/** A link in a listing: the link, its completed use count and its state. */
|
|
112
|
+
export type AccessLinkListed = AccessLink & {
|
|
113
|
+
readonly usesCompleted: number
|
|
114
|
+
readonly state: 'active' | 'revoked' | 'expired' | 'exhausted'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface AccessLinkPage {
|
|
118
|
+
readonly items: AccessLinkListed[]
|
|
119
|
+
/** Pass as `cursor` for the next (older) page; `null` at the end. */
|
|
120
|
+
readonly nextCursor: string | null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type ResolveResult =
|
|
124
|
+
| { readonly status: 'active'; readonly link: PublicLink; readonly tenantId: string }
|
|
125
|
+
| { readonly status: LinkState | 'rate-limited' }
|
|
126
|
+
|
|
127
|
+
export type BeginUseResult =
|
|
128
|
+
| { readonly ok: true; readonly useToken: string; readonly link: PublicLink }
|
|
129
|
+
| { readonly ok: false; readonly reason: LinkState | 'kind-not-permitted' | 'check-failed' | 'rate-limited' }
|
|
130
|
+
|
|
131
|
+
export type CompleteUseResult =
|
|
132
|
+
| { readonly ok: true }
|
|
133
|
+
| { readonly ok: false; readonly reason: 'not-found' | 'expired-reservation' | 'already-settled' }
|
|
134
|
+
|
|
135
|
+
export type ClaimUseResult =
|
|
136
|
+
| { readonly ok: true; readonly link: PublicLink }
|
|
137
|
+
| { readonly ok: false; readonly reason: 'not-found' | 'already-claimed' | 'expired-reservation' | 'already-settled' }
|
|
138
|
+
|
|
139
|
+
export type AbandonUseResult =
|
|
140
|
+
| { readonly ok: true }
|
|
141
|
+
| { readonly ok: false; readonly reason: 'not-found' | 'already-settled' }
|
|
142
|
+
|
|
143
|
+
export interface AccessLinks {
|
|
144
|
+
/** Creates a link. When the Flow has a trace with an open step, an `audit_record` (`AccessLink`, verb `create`, the link without its code or `checkData`) is written in the same transaction. */
|
|
145
|
+
link(options: LinkOptions): Promise<AccessLink>
|
|
146
|
+
get(id: string): Promise<AccessLink | null>
|
|
147
|
+
list(options?: { target?: AccessLinkTarget }): Promise<AccessLink[]>
|
|
148
|
+
/** One page of the tenant's links, newest first, with completed use counts and state; `nextCursor` is `null` on the last page. `limit` defaults to 50, at most 500. */
|
|
149
|
+
listPage(options?: {
|
|
150
|
+
target?: AccessLinkTarget
|
|
151
|
+
createdBy?: string
|
|
152
|
+
state?: AccessLinkListed['state']
|
|
153
|
+
limit?: number
|
|
154
|
+
cursor?: string
|
|
155
|
+
}): Promise<AccessLinkPage>
|
|
156
|
+
/** Revokes a link; when this call revoked it and the Flow has an open trace step, an `AccessLink:revoke` audit record is written in the same transaction. */
|
|
157
|
+
revoke(id: string): Promise<AccessLink | null>
|
|
158
|
+
/**
|
|
159
|
+
* Deletes one link of the tenant with its `access_link_code` row and all its
|
|
160
|
+
* `access_link_use` rows, in one transaction (the caller's when there is one).
|
|
161
|
+
* Returns true when this call deleted it, false when the tenant has no such link.
|
|
162
|
+
* Writes an `AccessLink:delete` audit record (`{ id, target, usesDeleted }`, no
|
|
163
|
+
* personal fields) under the same open-step rule as `revoke`; the link's earlier
|
|
164
|
+
* audit records are left as they are. Afterwards `get`/`list`/`listPage` omit the
|
|
165
|
+
* link and `resolve`/`beginUse`/`completeUse`/`abandonUse`/`claimUse` with its code
|
|
166
|
+
* or tokens return `not-found`; a miss on the deleted code counts toward the
|
|
167
|
+
* guessing limit, as an unknown code does.
|
|
168
|
+
*/
|
|
169
|
+
delete(id: string): Promise<boolean>
|
|
170
|
+
/**
|
|
171
|
+
* Deletes every link of the tenant whose target is `{ type, id }`, revoked and
|
|
172
|
+
* expired ones included, as `delete` does for each, in one transaction.
|
|
173
|
+
* Returns how many links it deleted.
|
|
174
|
+
*/
|
|
175
|
+
deleteForTarget(target: AccessLinkTarget): Promise<number>
|
|
176
|
+
uses(linkId: string): Promise<AccessLinkUse[]>
|
|
177
|
+
resolve(code: string, options?: { client?: string | null }): Promise<ResolveResult>
|
|
178
|
+
beginUse(code: string, options: {
|
|
179
|
+
kind: string
|
|
180
|
+
client?: string | null
|
|
181
|
+
location?: string | null
|
|
182
|
+
proof?: Readonly<Record<string, unknown>>
|
|
183
|
+
/** App-defined detail stored on the use and shown by `uses()` (a JSON object). */
|
|
184
|
+
detail?: Readonly<Record<string, unknown>>
|
|
185
|
+
}): Promise<BeginUseResult>
|
|
186
|
+
/** `detail` is shallow-merged over the detail given to `beginUse` (new keys win). */
|
|
187
|
+
completeUse(useToken: string, options?: { detail?: Readonly<Record<string, unknown>> }): Promise<CompleteUseResult>
|
|
188
|
+
abandonUse(useToken: string): Promise<AbandonUseResult>
|
|
189
|
+
/**
|
|
190
|
+
* Takes a pending use exactly once, across processes: a single
|
|
191
|
+
* `UPDATE ... SET claimed_at = now() WHERE claimed_at IS NULL AND status = 'pending'
|
|
192
|
+
* AND reserved_until > clock_timestamp()`, so of any number of concurrent calls for
|
|
193
|
+
* one token exactly one gets `{ ok: true, link }`; the rest get `already-claimed`.
|
|
194
|
+
* Claiming does not count the use: `completeUse` / `abandonUse` still do. `link`
|
|
195
|
+
* is accepted by `asLink` like a `beginUse` result.
|
|
196
|
+
*
|
|
197
|
+
* Pattern for a single-use URL: `beginUse` when the URL is issued, hand out a URL
|
|
198
|
+
* carrying `useToken`, and on the follow `claimUse`, serve, then `completeUseWhenSent`.
|
|
199
|
+
* Other reasons: `not-found`, `expired-reservation` (reservation or link expiry
|
|
200
|
+
* passed), `already-settled` (completed or abandoned).
|
|
201
|
+
*/
|
|
202
|
+
claimUse(useToken: string): Promise<ClaimUseResult>
|
|
203
|
+
/**
|
|
204
|
+
* Counts the use when the response has been sent, and frees it when the connection
|
|
205
|
+
* closed first: registers `response.onFinish`, then `completeUse(useToken, options)`
|
|
206
|
+
* on `'sent'` and `abandonUse(useToken)` on `'aborted'`. Call it before returning
|
|
207
|
+
* the handler's streamed body. The callback runs after the request's transaction
|
|
208
|
+
* has ended, with the Flow captured here and no transaction of its own, so it
|
|
209
|
+
* opens its own. The trace is closed by then, so the completion writes no
|
|
210
|
+
* `AccessLink:use` audit row; the use log row is the record. A failure is logged,
|
|
211
|
+
* never thrown into the response.
|
|
212
|
+
*/
|
|
213
|
+
completeUseWhenSent(
|
|
214
|
+
response: HttpResponse,
|
|
215
|
+
useToken: string,
|
|
216
|
+
options?: { detail?: Readonly<Record<string, unknown>> },
|
|
217
|
+
): void
|
|
218
|
+
/**
|
|
219
|
+
* Runs `fn` as the tenant an access link resolved to, from a `.public()` route.
|
|
220
|
+
* `grant` must be the object `resolve` returned with `status: 'active'` or
|
|
221
|
+
* `beginUse` or `claimUse` returned with `ok: true`; anything else (hand-built, copied,
|
|
222
|
+
* refused) throws, so a tenant cannot be named, only earned.
|
|
223
|
+
*
|
|
224
|
+
* Inside `fn` the Flow is a child of the current one — same request id — with `tenant` set to the link's tenant and
|
|
225
|
+
* `auth = { userId: 'link:<linkId>', accessLinkId }`, so `pipe()` is scoped to
|
|
226
|
+
* the tenant and an audited write records `actor_id = 'link:<linkId>'`. When
|
|
227
|
+
* the current Flow is traced, the work inside `fn` is recorded in a child trace
|
|
228
|
+
* (a child of the request's trace, opened at the step open on entry) that
|
|
229
|
+
* carries the link's tenant, so the tenant can read it; the request's own trace
|
|
230
|
+
* stays tenantless. The child trace closes when `fn`'s transaction settles. `fn`
|
|
231
|
+
* runs in its own transaction on the default database as the app role
|
|
232
|
+
* (row-level security applies), opened with the tenant's session variables.
|
|
233
|
+
* It commits when `fn` resolves and rolls back when `fn` throws; under the
|
|
234
|
+
* vitest harness it is a savepoint of the test's transaction. Nested
|
|
235
|
+
* `pipe.transaction` calls inside `fn` are savepoints of it.
|
|
236
|
+
*
|
|
237
|
+
* Only the default database is carried into `fn`: other databases bound in the
|
|
238
|
+
* outer Flow are not, so `pipe('<other>')` inside `fn` opens no bound
|
|
239
|
+
* transaction. Outside `fn` the Flow is unchanged: no tenant, no auth.
|
|
240
|
+
*/
|
|
241
|
+
asLink<T>(grant: ResolveResult | BeginUseResult | ClaimUseResult, fn: () => Promise<T>): Promise<T>
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const DEFAULT_RESERVATION_MS = 15 * 60 * 1000
|
|
245
|
+
const MISS_CLEANUP_EVERY_MS = 60_000
|
|
246
|
+
const DEFAULT_RATE_LIMIT: RateLimitConfig = { max: 30, windowMs: 60_000 }
|
|
247
|
+
const MAX_CODE_ATTEMPTS = 5
|
|
248
|
+
const DEFAULT_LIST_LIMIT = 50
|
|
249
|
+
const MAX_LIST_LIMIT = 500
|
|
250
|
+
const LIST_STATES: readonly string[] = ['active', 'revoked', 'expired', 'exhausted']
|
|
251
|
+
|
|
252
|
+
type Row = Record<string, unknown>
|
|
253
|
+
|
|
254
|
+
function asStringArray(value: unknown): string[] {
|
|
255
|
+
return Array.isArray(value) ? value.map(String) : []
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Raw `execute` rows carry timestamps as driver strings.
|
|
259
|
+
function toDate(value: unknown): Date {
|
|
260
|
+
return value instanceof Date ? value : new Date(String(value))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function toDateOrNull(value: unknown): Date | null {
|
|
264
|
+
return value === null || value === undefined ? null : toDate(value)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function rowToLink(row: Row): AccessLink {
|
|
268
|
+
return {
|
|
269
|
+
id: row['id'] as string,
|
|
270
|
+
tenantId: row['tenant_id'] as string,
|
|
271
|
+
code: row['code'] as string,
|
|
272
|
+
target: { type: row['target_type'] as string, id: row['target_id'] as string },
|
|
273
|
+
permissions: asStringArray(row['permissions']),
|
|
274
|
+
note: (row['note'] as string | null) ?? null,
|
|
275
|
+
createdBy: (row['created_by'] as string | null) ?? null,
|
|
276
|
+
createdAt: toDate(row['created_at']),
|
|
277
|
+
expiresAt: toDateOrNull(row['expires_at']),
|
|
278
|
+
maxUses: (row['max_uses'] as number | null) ?? null,
|
|
279
|
+
requireChecks: asStringArray(row['require_checks']),
|
|
280
|
+
checkData: row['check_data'] ?? null,
|
|
281
|
+
revokedAt: toDateOrNull(row['revoked_at']),
|
|
282
|
+
revokedBy: (row['revoked_by'] as string | null) ?? null,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function rowToPublic(row: Row): PublicLink {
|
|
287
|
+
const link = rowToLink(row)
|
|
288
|
+
return {
|
|
289
|
+
id: link.id,
|
|
290
|
+
target: link.target,
|
|
291
|
+
permissions: link.permissions,
|
|
292
|
+
note: link.note,
|
|
293
|
+
expiresAt: link.expiresAt,
|
|
294
|
+
maxUses: link.maxUses,
|
|
295
|
+
usesCompleted: Number(row['uses_completed'] ?? 0),
|
|
296
|
+
requireChecks: link.requireChecks,
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function rowToUse(row: Row): AccessLinkUse {
|
|
301
|
+
return {
|
|
302
|
+
id: row['id'] as string,
|
|
303
|
+
linkId: row['link_id'] as string,
|
|
304
|
+
kind: row['kind'] as string,
|
|
305
|
+
status: row['status'] as AccessLinkUse['status'],
|
|
306
|
+
startedAt: toDate(row['started_at']),
|
|
307
|
+
reservedUntil: toDate(row['reserved_until']),
|
|
308
|
+
completedAt: toDateOrNull(row['completed_at']),
|
|
309
|
+
client: (row['client'] as string | null) ?? null,
|
|
310
|
+
location: (row['location'] as string | null) ?? null,
|
|
311
|
+
detail: (row['detail'] as Record<string, unknown> | null) ?? null,
|
|
312
|
+
claimedAt: toDateOrNull(row['claimed_at']),
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isUniqueViolation(err: unknown): boolean {
|
|
317
|
+
let current: unknown = err
|
|
318
|
+
for (let depth = 0; depth < 4 && typeof current === 'object' && current !== null; depth++) {
|
|
319
|
+
if ((current as { code?: unknown }).code === '23505') return true
|
|
320
|
+
current = (current as { cause?: unknown }).cause
|
|
321
|
+
}
|
|
322
|
+
return false
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function splitToken(useToken: string): { code: string; useId: string } | null {
|
|
326
|
+
const dot = useToken.lastIndexOf('.')
|
|
327
|
+
if (dot <= 0 || dot === useToken.length - 1) return null
|
|
328
|
+
const useId = useToken.slice(dot + 1)
|
|
329
|
+
if (!/^[0-9a-f-]{36}$/i.test(useId)) return null
|
|
330
|
+
return { code: useToken.slice(0, dot), useId }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// A link row plus the state a use decision needs, all read with the database
|
|
334
|
+
// clock. The caller that must serialize locks the row in its own statement first.
|
|
335
|
+
function linkWithState(where: ReturnType<typeof sql>) {
|
|
336
|
+
return sql`
|
|
337
|
+
SELECT l.*,
|
|
338
|
+
(l.revoked_at IS NOT NULL) AS is_revoked,
|
|
339
|
+
(l.expires_at IS NOT NULL AND l.expires_at <= now()) AS is_expired,
|
|
340
|
+
(SELECT count(*)::int FROM access_link_use u WHERE u.link_id = l.id AND u.status = 'completed') AS uses_completed,
|
|
341
|
+
(SELECT count(*)::int FROM access_link_use u WHERE u.link_id = l.id
|
|
342
|
+
AND (u.status = 'completed' OR (u.status = 'pending' AND u.reserved_until > clock_timestamp()))) AS uses_held
|
|
343
|
+
FROM access_link l
|
|
344
|
+
WHERE ${where}
|
|
345
|
+
`
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function stateOf(row: Row, mode: 'completed' | 'held'): LinkState | null {
|
|
349
|
+
if (row['is_revoked'] === true) return 'revoked'
|
|
350
|
+
if (row['is_expired'] === true) return 'expired'
|
|
351
|
+
const max = row['max_uses'] as number | null
|
|
352
|
+
const used = Number(mode === 'completed' ? row['uses_completed'] : row['uses_held'])
|
|
353
|
+
if (max !== null && used >= max) return 'exhausted'
|
|
354
|
+
return null
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
interface Grant {
|
|
358
|
+
readonly tenantId: string
|
|
359
|
+
readonly linkId: string
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// The results `resolve` and `beginUse` handed out, by identity. `asLink` accepts
|
|
363
|
+
// only these, so no caller can name a tenant by building an object.
|
|
364
|
+
const grants = new WeakMap<object, Grant>()
|
|
365
|
+
|
|
366
|
+
function issue<R extends object>(result: R, tenantId: string, linkId: string): R {
|
|
367
|
+
grants.set(result, { tenantId, linkId })
|
|
368
|
+
return result
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function createAccessLinks(config: AccessLinksConfig): AccessLinks {
|
|
372
|
+
const permissionSet = new Set(config.permissions)
|
|
373
|
+
if (permissionSet.size === 0) {
|
|
374
|
+
throw new Error('[pipework] accessLinks.create needs a non-empty `permissions` list.\n')
|
|
375
|
+
}
|
|
376
|
+
const generator = createCodeGenerator(config.code)
|
|
377
|
+
const checks = config.checks ?? {}
|
|
378
|
+
const reservationMs = config.reservationMs ?? DEFAULT_RESERVATION_MS
|
|
379
|
+
if (!Number.isFinite(reservationMs) || reservationMs <= 0) {
|
|
380
|
+
throw new Error('[pipework] accessLinks reservationMs must be a positive number of milliseconds.\n')
|
|
381
|
+
}
|
|
382
|
+
const rateConfig = config.resolveRateLimit === undefined ? DEFAULT_RATE_LIMIT : config.resolveRateLimit
|
|
383
|
+
if (rateConfig !== false) {
|
|
384
|
+
if (!Number.isInteger(rateConfig.max) || rateConfig.max <= 0) {
|
|
385
|
+
throw new Error('[pipework] accessLinks resolveRateLimit.max must be a positive integer.\n')
|
|
386
|
+
}
|
|
387
|
+
if (!Number.isFinite(rateConfig.windowMs) || rateConfig.windowMs <= 0) {
|
|
388
|
+
throw new Error('[pipework] accessLinks resolveRateLimit.windowMs must be a positive number of milliseconds.\n')
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
let lastMissCleanup = 0
|
|
392
|
+
|
|
393
|
+
function requireTenant(): { flow: Flow; tenantId: string } {
|
|
394
|
+
const flow = requireContext()
|
|
395
|
+
if (flow.tenant === null) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
'[pipework] This accessLinks method needs a tenant in the Flow.\n\n' +
|
|
398
|
+
' link/get/list/revoke/delete/uses manage one tenant\'s links. Call them from an authenticated\n' +
|
|
399
|
+
' tenant route; resolve/beginUse/completeUse/abandonUse are the ones for public routes.\n',
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
return { flow, tenantId: flow.tenant }
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function inTenant<T>(tenantId: string, fn: (tx: DB) => Promise<T>): Promise<T> {
|
|
406
|
+
return runAsTenant(requireContext(), tenantId, fn)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function findTenant(code: string): Promise<string | null> {
|
|
410
|
+
const rows = await pipeSystem().execute(sql`SELECT tenant_id FROM access_link_code WHERE code = ${code}`)
|
|
411
|
+
return rows.length === 0 ? null : (rows[0]!['tenant_id'] as string)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// True when the client has used up its miss budget in the current window.
|
|
415
|
+
async function overBudget(client: string | null): Promise<boolean> {
|
|
416
|
+
if (rateConfig === false || client === null) return false
|
|
417
|
+
const rows = await pipeSystem().execute(sql`
|
|
418
|
+
SELECT count FROM access_link_miss
|
|
419
|
+
WHERE client = ${client} AND window_start > now() - ${rateConfig.windowMs}::float8 * interval '1 millisecond'`)
|
|
420
|
+
return rows.length > 0 && Number(rows[0]!['count']) >= rateConfig.max
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Counts one miss against the client; atomic per client, shared by every process.
|
|
424
|
+
async function recordMiss(client: string | null): Promise<void> {
|
|
425
|
+
if (rateConfig === false || client === null) return
|
|
426
|
+
const db = pipeSystem()
|
|
427
|
+
await db.execute(sql`
|
|
428
|
+
INSERT INTO access_link_miss AS m (client, count, window_start)
|
|
429
|
+
VALUES (${client}, 1, now())
|
|
430
|
+
ON CONFLICT (client) DO UPDATE SET
|
|
431
|
+
count = CASE WHEN m.window_start <= now() - ${rateConfig.windowMs}::float8 * interval '1 millisecond' THEN 1 ELSE m.count + 1 END,
|
|
432
|
+
window_start = CASE WHEN m.window_start <= now() - ${rateConfig.windowMs}::float8 * interval '1 millisecond' THEN now() ELSE m.window_start END`)
|
|
433
|
+
const now = Date.now()
|
|
434
|
+
if (now - lastMissCleanup < MISS_CLEANUP_EVERY_MS) return
|
|
435
|
+
lastMissCleanup = now
|
|
436
|
+
void Promise.resolve(db.execute(sql`
|
|
437
|
+
DELETE FROM access_link_miss WHERE window_start <= now() - ${rateConfig.windowMs}::float8 * interval '1 millisecond'`))
|
|
438
|
+
.catch((err: unknown) => getBaseLogger().error({ err }, 'accessLinks miss cleanup failed'))
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function missed(code: string, client: string | null): Promise<void> {
|
|
442
|
+
await recordMiss(client)
|
|
443
|
+
signalMiss(code, client)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function signalMiss(code: string, client: string | null): void {
|
|
447
|
+
getBaseLogger().warn({ event: 'access link resolve miss', client, codeLength: code.length }, 'access link resolve miss')
|
|
448
|
+
try {
|
|
449
|
+
config.onMiss?.({ client, code })
|
|
450
|
+
} catch (err) {
|
|
451
|
+
getBaseLogger().error({ err }, 'accessLinks onMiss callback threw')
|
|
452
|
+
void recordFault('access-link-on-miss', faultMessage(err))
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function audit(flow: Flow, tx: DB, tenantId: string, verb: string, linkId: string, details: Record<string, unknown>): Promise<void> {
|
|
457
|
+
return writeInfraAudit(flow, tx, { tenantId, entity: 'AccessLink', verb, entityId: linkId, after: details })
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function auditProjection(link: AccessLink): Record<string, unknown> {
|
|
461
|
+
return {
|
|
462
|
+
id: link.id, target: link.target, permissions: link.permissions, note: link.note,
|
|
463
|
+
createdBy: link.createdBy, expiresAt: link.expiresAt, maxUses: link.maxUses,
|
|
464
|
+
requireChecks: link.requireChecks, revokedAt: link.revokedAt, revokedBy: link.revokedBy,
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function newLink(options: LinkOptions): Promise<AccessLink> {
|
|
469
|
+
const { flow, tenantId } = requireTenant()
|
|
470
|
+
if (options.permissions.length === 0) {
|
|
471
|
+
throw new ValidationError({ permissions: ['must name at least one permission'] }, 'Access link needs at least one permission.')
|
|
472
|
+
}
|
|
473
|
+
const unknownPermissions = options.permissions.filter(p => !permissionSet.has(p))
|
|
474
|
+
if (unknownPermissions.length > 0) {
|
|
475
|
+
const detail = `not in the configured set (${[...permissionSet].join(', ')}): ${unknownPermissions.join(', ')}`
|
|
476
|
+
throw new ValidationError({ permissions: [detail] }, `Access link permissions ${detail}`)
|
|
477
|
+
}
|
|
478
|
+
const requireChecks = options.requireChecks ?? []
|
|
479
|
+
const unknownChecks = requireChecks.filter(c => checks[c] === undefined)
|
|
480
|
+
if (unknownChecks.length > 0) {
|
|
481
|
+
const detail = `no such check in accessLinks.create({ checks }): ${unknownChecks.join(', ')}`
|
|
482
|
+
throw new ValidationError({ requireChecks: [detail] }, `Access link requireChecks ${detail}`)
|
|
483
|
+
}
|
|
484
|
+
if (options.maxUses !== undefined && (!Number.isInteger(options.maxUses) || options.maxUses < 1)) {
|
|
485
|
+
throw new ValidationError({ maxUses: ['must be a positive integer'] }, 'Access link maxUses must be a positive integer.')
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const id = randomUUID()
|
|
489
|
+
const createdBy = resolveActorId(flow)
|
|
490
|
+
const permissions = JSON.stringify([...new Set(options.permissions)])
|
|
491
|
+
const checkData = options.checkData === undefined ? null : JSON.stringify(options.checkData)
|
|
492
|
+
|
|
493
|
+
return pipe.transaction(async (tx: DB) => {
|
|
494
|
+
for (let attempt = 1; attempt <= MAX_CODE_ATTEMPTS; attempt++) {
|
|
495
|
+
const code = generator.next()
|
|
496
|
+
try {
|
|
497
|
+
// A savepoint per attempt: a collision aborts only the attempt.
|
|
498
|
+
return await tx.transaction(async (sp: DB) => {
|
|
499
|
+
await sp.execute(sql`
|
|
500
|
+
INSERT INTO access_link_code (code, tenant_id, link_id) VALUES (${code}, ${tenantId}, ${id})
|
|
501
|
+
`)
|
|
502
|
+
const rows = await sp.execute(sql`
|
|
503
|
+
INSERT INTO access_link
|
|
504
|
+
(id, tenant_id, code, target_type, target_id, permissions, note, created_by,
|
|
505
|
+
expires_at, max_uses, require_checks, check_data)
|
|
506
|
+
VALUES (${id}, ${tenantId}, ${code}, ${options.target.type}, ${options.target.id},
|
|
507
|
+
${permissions}::jsonb, ${options.note ?? null}, ${createdBy},
|
|
508
|
+
${options.expiresAt?.toISOString() ?? null}, ${options.maxUses ?? null},
|
|
509
|
+
${JSON.stringify(requireChecks)}::jsonb,
|
|
510
|
+
${checkData === null ? sql`NULL` : sql`${checkData}::jsonb`})
|
|
511
|
+
RETURNING *
|
|
512
|
+
`)
|
|
513
|
+
const created = rowToLink(rows[0]!)
|
|
514
|
+
await audit(flow, sp, tenantId, 'create', created.id, auditProjection(created))
|
|
515
|
+
return created
|
|
516
|
+
})
|
|
517
|
+
} catch (err) {
|
|
518
|
+
if (!isUniqueViolation(err)) throw err
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
throw new Error(
|
|
522
|
+
`[pipework] accessLinks could not find a free code in ${MAX_CODE_ATTEMPTS} attempts.\n\n` +
|
|
523
|
+
` Codes are ${generator.length} characters from an alphabet of ${generator.alphabet.length}; the space is\n` +
|
|
524
|
+
' nearly full. Raise `code.length` or widen `code.alphabet`.\n',
|
|
525
|
+
)
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Locks the matching links first (serialising with beginUse), then removes uses, code and links.
|
|
530
|
+
async function deleteLinks(match: SQL): Promise<number> {
|
|
531
|
+
const { flow, tenantId } = requireTenant()
|
|
532
|
+
return pipe.transaction(async (tx: DB) => {
|
|
533
|
+
const found = await tx.execute(sql`
|
|
534
|
+
SELECT id FROM access_link WHERE tenant_id = ${tenantId} AND ${match} ORDER BY id FOR UPDATE
|
|
535
|
+
`)
|
|
536
|
+
if (found.length === 0) return 0
|
|
537
|
+
const ids = sql.inList(found.map(r => r['id'] as string), 'uuid')
|
|
538
|
+
const usesGone = await tx.execute(sql`
|
|
539
|
+
DELETE FROM access_link_use WHERE tenant_id = ${tenantId} AND link_id ${ids} RETURNING link_id
|
|
540
|
+
`)
|
|
541
|
+
const usesByLink = new Map<string, number>()
|
|
542
|
+
for (const u of usesGone) {
|
|
543
|
+
const k = u['link_id'] as string
|
|
544
|
+
usesByLink.set(k, (usesByLink.get(k) ?? 0) + 1)
|
|
545
|
+
}
|
|
546
|
+
await tx.execute(sql`DELETE FROM access_link_code WHERE tenant_id = ${tenantId} AND link_id ${ids}`)
|
|
547
|
+
const gone = await tx.execute(sql`
|
|
548
|
+
DELETE FROM access_link WHERE tenant_id = ${tenantId} AND id ${ids} RETURNING id, target_type, target_id
|
|
549
|
+
`)
|
|
550
|
+
for (const g of gone) {
|
|
551
|
+
const id = g['id'] as string
|
|
552
|
+
await audit(flow, tx, tenantId, 'delete', id, {
|
|
553
|
+
id,
|
|
554
|
+
target: { type: g['target_type'] as string, id: g['target_id'] as string },
|
|
555
|
+
usesDeleted: usesByLink.get(id) ?? 0,
|
|
556
|
+
})
|
|
557
|
+
}
|
|
558
|
+
return gone.length
|
|
559
|
+
})
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const handle: AccessLinks = {
|
|
563
|
+
link: newLink,
|
|
564
|
+
|
|
565
|
+
async delete(id) {
|
|
566
|
+
return (await deleteLinks(sql`id = ${id}`)) > 0
|
|
567
|
+
},
|
|
568
|
+
|
|
569
|
+
async deleteForTarget(target) {
|
|
570
|
+
return deleteLinks(sql`target_type = ${target.type} AND target_id = ${target.id}`)
|
|
571
|
+
},
|
|
572
|
+
|
|
573
|
+
async get(id) {
|
|
574
|
+
const { tenantId } = requireTenant()
|
|
575
|
+
const rows = await pipeRaw().execute(sql`
|
|
576
|
+
SELECT * FROM access_link WHERE id = ${id} AND tenant_id = ${tenantId}
|
|
577
|
+
`)
|
|
578
|
+
return rows.length === 0 ? null : rowToLink(rows[0]!)
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
async list(options) {
|
|
582
|
+
const { tenantId } = requireTenant()
|
|
583
|
+
const target = options?.target
|
|
584
|
+
const rows = await pipeRaw().execute(sql`
|
|
585
|
+
SELECT * FROM access_link
|
|
586
|
+
WHERE tenant_id = ${tenantId}
|
|
587
|
+
${target === undefined ? sql`` : sql`AND target_type = ${target.type} AND target_id = ${target.id}`}
|
|
588
|
+
ORDER BY created_at DESC, id
|
|
589
|
+
`)
|
|
590
|
+
return rows.map(rowToLink)
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
async listPage(options = {}) {
|
|
594
|
+
const { tenantId } = requireTenant()
|
|
595
|
+
const limit = options.limit ?? DEFAULT_LIST_LIMIT
|
|
596
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT) {
|
|
597
|
+
throw new ValidationError({ limit: [`must be an integer from 1 to ${MAX_LIST_LIMIT}`] }, 'Access link list limit is out of range.')
|
|
598
|
+
}
|
|
599
|
+
if (options.state !== undefined && !LIST_STATES.includes(options.state)) {
|
|
600
|
+
throw new ValidationError({ state: [`must be one of ${LIST_STATES.join(', ')}`] }, 'Access link list state is not valid.')
|
|
601
|
+
}
|
|
602
|
+
let cursor: { at: string; id: string } | null = null
|
|
603
|
+
if (options.cursor !== undefined) {
|
|
604
|
+
let parts: unknown[] = []
|
|
605
|
+
try { parts = decodeCursor(options.cursor) } catch { /* reported below */ }
|
|
606
|
+
if (parts.length !== 2 || typeof parts[0] !== 'string' || typeof parts[1] !== 'string'
|
|
607
|
+
|| !/^[0-9a-f-]{36}$/i.test(parts[1]) || Number.isNaN(new Date(parts[0]).getTime())) {
|
|
608
|
+
throw new ValidationError('Invalid access link cursor: expected [createdAt, id]. Cursors are opaque — pass the value returned by listPage unchanged.')
|
|
609
|
+
}
|
|
610
|
+
cursor = { at: parts[0], id: parts[1] }
|
|
611
|
+
}
|
|
612
|
+
const { target, createdBy, state } = options
|
|
613
|
+
const rows = await pipeRaw().execute(sql`
|
|
614
|
+
SELECT * FROM (
|
|
615
|
+
SELECT s.*, s.created_at::text AS created_at_text,
|
|
616
|
+
CASE WHEN s.is_revoked THEN 'revoked'
|
|
617
|
+
WHEN s.is_expired THEN 'expired'
|
|
618
|
+
WHEN s.max_uses IS NOT NULL AND s.uses_completed >= s.max_uses THEN 'exhausted'
|
|
619
|
+
ELSE 'active' END AS state
|
|
620
|
+
FROM (${linkWithState(sql`l.tenant_id = ${tenantId}`)}) s
|
|
621
|
+
) p
|
|
622
|
+
WHERE true
|
|
623
|
+
${target === undefined ? sql`` : sql`AND p.target_type = ${target.type} AND p.target_id = ${target.id}`}
|
|
624
|
+
${createdBy === undefined ? sql`` : sql`AND p.created_by = ${createdBy}`}
|
|
625
|
+
${state === undefined ? sql`` : sql`AND p.state = ${state}`}
|
|
626
|
+
${cursor === null ? sql`` : sql`AND (p.created_at, p.id) < (${cursor.at}::timestamptz, ${cursor.id}::uuid)`}
|
|
627
|
+
ORDER BY p.created_at DESC, p.id DESC
|
|
628
|
+
LIMIT ${limit + 1}
|
|
629
|
+
`)
|
|
630
|
+
const page = rows.slice(0, limit)
|
|
631
|
+
const last = page[page.length - 1]
|
|
632
|
+
return {
|
|
633
|
+
items: page.map(row => ({
|
|
634
|
+
...rowToLink(row),
|
|
635
|
+
usesCompleted: Number(row['uses_completed']),
|
|
636
|
+
state: row['state'] as AccessLinkListed['state'],
|
|
637
|
+
})),
|
|
638
|
+
nextCursor: rows.length > limit && last !== undefined
|
|
639
|
+
? encodeCursor([last['created_at_text'] as string, last['id'] as string])
|
|
640
|
+
: null,
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
async revoke(id) {
|
|
645
|
+
const { flow, tenantId } = requireTenant()
|
|
646
|
+
const db = pipeRaw()
|
|
647
|
+
const rows = await db.execute(sql`
|
|
648
|
+
UPDATE access_link SET revoked_at = now(), revoked_by = ${resolveActorId(flow)}
|
|
649
|
+
WHERE id = ${id} AND tenant_id = ${tenantId} AND revoked_at IS NULL
|
|
650
|
+
RETURNING *
|
|
651
|
+
`)
|
|
652
|
+
if (rows.length > 0) {
|
|
653
|
+
const revoked = rowToLink(rows[0]!)
|
|
654
|
+
await audit(flow, db, tenantId, 'revoke', revoked.id, auditProjection(revoked))
|
|
655
|
+
return revoked
|
|
656
|
+
}
|
|
657
|
+
return handle.get(id)
|
|
658
|
+
},
|
|
659
|
+
|
|
660
|
+
async uses(linkId) {
|
|
661
|
+
const { tenantId } = requireTenant()
|
|
662
|
+
const rows = await pipeRaw().execute(sql`
|
|
663
|
+
SELECT * FROM access_link_use WHERE link_id = ${linkId} AND tenant_id = ${tenantId}
|
|
664
|
+
ORDER BY started_at, id
|
|
665
|
+
`)
|
|
666
|
+
return rows.map(rowToUse)
|
|
667
|
+
},
|
|
668
|
+
|
|
669
|
+
async resolve(code, options) {
|
|
670
|
+
const client = options?.client ?? null
|
|
671
|
+
if (await overBudget(client)) return { status: 'rate-limited' }
|
|
672
|
+
|
|
673
|
+
const tenantId = await findTenant(code)
|
|
674
|
+
const row = tenantId === null ? undefined : (await inTenant(tenantId, tx => tx.execute(
|
|
675
|
+
linkWithState(sql`l.code = ${code} AND l.tenant_id = ${tenantId}`),
|
|
676
|
+
)))[0]
|
|
677
|
+
if (tenantId === null || row === undefined) {
|
|
678
|
+
await missed(code, client)
|
|
679
|
+
return { status: 'not-found' }
|
|
680
|
+
}
|
|
681
|
+
const state = stateOf(row, 'completed')
|
|
682
|
+
if (state !== null) return { status: state }
|
|
683
|
+
const link = rowToPublic(row)
|
|
684
|
+
return issue({ status: 'active' as const, link, tenantId }, tenantId, link.id)
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
async beginUse(code, options) {
|
|
688
|
+
const client = options.client ?? null
|
|
689
|
+
// Same budget as resolve: beginUse takes a raw code too, so it is a guessing path.
|
|
690
|
+
if (await overBudget(client)) return { ok: false, reason: 'rate-limited' }
|
|
691
|
+
const tenantId = await findTenant(code)
|
|
692
|
+
if (tenantId === null) {
|
|
693
|
+
await missed(code, client)
|
|
694
|
+
return { ok: false, reason: 'not-found' }
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Read first, without a lock: refuse what is already decided and run the
|
|
698
|
+
// verifiers (which may be slow) before any row is held.
|
|
699
|
+
const first = (await inTenant(tenantId, tx => tx.execute(
|
|
700
|
+
linkWithState(sql`l.code = ${code} AND l.tenant_id = ${tenantId}`),
|
|
701
|
+
)))[0]
|
|
702
|
+
if (first === undefined) {
|
|
703
|
+
await missed(code, client)
|
|
704
|
+
return { ok: false, reason: 'not-found' }
|
|
705
|
+
}
|
|
706
|
+
const early = stateOf(first, 'held')
|
|
707
|
+
if (early !== null) return { ok: false, reason: early }
|
|
708
|
+
const link = rowToLink(first)
|
|
709
|
+
if (!link.permissions.includes(options.kind)) return { ok: false, reason: 'kind-not-permitted' }
|
|
710
|
+
|
|
711
|
+
for (const name of link.requireChecks) {
|
|
712
|
+
const verify = checks[name]
|
|
713
|
+
let passed = false
|
|
714
|
+
try {
|
|
715
|
+
passed = verify !== undefined && await verify({ link, proof: options.proof?.[name], client }) === true
|
|
716
|
+
} catch (err) {
|
|
717
|
+
getBaseLogger().warn({ err, check: name, linkId: link.id }, 'access link check threw; treated as failed')
|
|
718
|
+
}
|
|
719
|
+
if (!passed) return { ok: false, reason: 'check-failed' }
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Then decide under the link's row lock: concurrent begins queue here,
|
|
723
|
+
// and each counts the uses the one before it left. The lock and the count
|
|
724
|
+
// are separate statements: a count in the locking statement reads the
|
|
725
|
+
// snapshot taken before the wait, missing the use the holder committed.
|
|
726
|
+
return inTenant(tenantId, async (tx): Promise<BeginUseResult> => {
|
|
727
|
+
await tx.execute(sql`SELECT 1 FROM access_link WHERE id = ${link.id} AND tenant_id = ${tenantId} FOR UPDATE`)
|
|
728
|
+
const locked = (await tx.execute(
|
|
729
|
+
linkWithState(sql`l.id = ${link.id} AND l.tenant_id = ${tenantId}`),
|
|
730
|
+
))[0]
|
|
731
|
+
if (locked === undefined) return { ok: false, reason: 'not-found' }
|
|
732
|
+
const state = stateOf(locked, 'held')
|
|
733
|
+
if (state !== null) return { ok: false, reason: state }
|
|
734
|
+
|
|
735
|
+
const useId = randomUUID()
|
|
736
|
+
await tx.execute(sql`
|
|
737
|
+
INSERT INTO access_link_use (id, tenant_id, link_id, kind, status, reserved_until, client, location, detail)
|
|
738
|
+
VALUES (${useId}, ${tenantId}, ${link.id}, ${options.kind}, 'pending',
|
|
739
|
+
clock_timestamp() + ${reservationMs} * interval '1 millisecond',
|
|
740
|
+
${client}, ${options.location ?? null},
|
|
741
|
+
${options.detail === undefined ? sql`NULL` : sql`${JSON.stringify(options.detail)}::jsonb`})
|
|
742
|
+
`)
|
|
743
|
+
return issue({ ok: true as const, useToken: `${code}.${useId}`, link: rowToPublic(locked) }, tenantId, link.id)
|
|
744
|
+
})
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
async completeUse(useToken, options) {
|
|
748
|
+
const flow = requireContext()
|
|
749
|
+
const parts = splitToken(useToken)
|
|
750
|
+
if (parts === null) return { ok: false, reason: 'not-found' }
|
|
751
|
+
const tenantId = await findTenant(parts.code)
|
|
752
|
+
if (tenantId === null) return { ok: false, reason: 'not-found' }
|
|
753
|
+
|
|
754
|
+
return inTenant(tenantId, async (tx): Promise<CompleteUseResult> => {
|
|
755
|
+
const rows = await tx.execute(sql`
|
|
756
|
+
SELECT u.id, u.link_id, u.kind, u.status,
|
|
757
|
+
(u.reserved_until > clock_timestamp()) AS reservation_live,
|
|
758
|
+
(l.expires_at IS NOT NULL AND l.expires_at <= now()) AS link_expired,
|
|
759
|
+
l.target_type, l.target_id
|
|
760
|
+
FROM access_link_use u JOIN access_link l ON l.id = u.link_id
|
|
761
|
+
WHERE u.id = ${parts.useId} AND l.code = ${parts.code} AND u.tenant_id = ${tenantId}
|
|
762
|
+
FOR UPDATE OF u
|
|
763
|
+
`)
|
|
764
|
+
const row = rows[0]
|
|
765
|
+
if (row === undefined) return { ok: false, reason: 'not-found' }
|
|
766
|
+
if (row['status'] !== 'pending') return { ok: false, reason: 'already-settled' }
|
|
767
|
+
if (row['reservation_live'] !== true || row['link_expired'] === true) {
|
|
768
|
+
return { ok: false, reason: 'expired-reservation' }
|
|
769
|
+
}
|
|
770
|
+
const detail = options?.detail
|
|
771
|
+
const settled = await tx.execute(sql`
|
|
772
|
+
UPDATE access_link_use SET status = 'completed', completed_at = now()
|
|
773
|
+
${detail === undefined ? sql`` : sql`, detail = COALESCE(detail, '{}'::jsonb) || ${JSON.stringify(detail)}::jsonb`}
|
|
774
|
+
WHERE id = ${parts.useId}
|
|
775
|
+
RETURNING detail
|
|
776
|
+
`)
|
|
777
|
+
await audit(flow, tx, tenantId, 'use', String(row['link_id']), {
|
|
778
|
+
linkId: row['link_id'], useId: parts.useId, kind: row['kind'],
|
|
779
|
+
targetType: row['target_type'], targetId: row['target_id'],
|
|
780
|
+
...(settled[0]?.['detail'] != null ? { detail: settled[0]['detail'] } : {}),
|
|
781
|
+
})
|
|
782
|
+
return { ok: true }
|
|
783
|
+
})
|
|
784
|
+
},
|
|
785
|
+
|
|
786
|
+
async abandonUse(useToken) {
|
|
787
|
+
const parts = splitToken(useToken)
|
|
788
|
+
if (parts === null) return { ok: false, reason: 'not-found' }
|
|
789
|
+
const tenantId = await findTenant(parts.code)
|
|
790
|
+
if (tenantId === null) return { ok: false, reason: 'not-found' }
|
|
791
|
+
|
|
792
|
+
return inTenant(tenantId, async (tx): Promise<AbandonUseResult> => {
|
|
793
|
+
const updated = await tx.execute(sql`
|
|
794
|
+
UPDATE access_link_use u SET status = 'abandoned'
|
|
795
|
+
FROM access_link l
|
|
796
|
+
WHERE u.id = ${parts.useId} AND l.id = u.link_id AND l.code = ${parts.code}
|
|
797
|
+
AND u.tenant_id = ${tenantId} AND u.status = 'pending'
|
|
798
|
+
RETURNING u.id
|
|
799
|
+
`)
|
|
800
|
+
if (updated.length > 0) return { ok: true }
|
|
801
|
+
const exists = await tx.execute(sql`
|
|
802
|
+
SELECT 1 FROM access_link_use u JOIN access_link l ON l.id = u.link_id
|
|
803
|
+
WHERE u.id = ${parts.useId} AND l.code = ${parts.code} AND u.tenant_id = ${tenantId}
|
|
804
|
+
`)
|
|
805
|
+
return { ok: false, reason: exists.length === 0 ? 'not-found' : 'already-settled' }
|
|
806
|
+
})
|
|
807
|
+
},
|
|
808
|
+
|
|
809
|
+
async claimUse(useToken) {
|
|
810
|
+
const parts = splitToken(useToken)
|
|
811
|
+
if (parts === null) return { ok: false, reason: 'not-found' }
|
|
812
|
+
const tenantId = await findTenant(parts.code)
|
|
813
|
+
if (tenantId === null) return { ok: false, reason: 'not-found' }
|
|
814
|
+
|
|
815
|
+
return inTenant(tenantId, async (tx): Promise<ClaimUseResult> => {
|
|
816
|
+
const claimed = await tx.execute(sql`
|
|
817
|
+
UPDATE access_link_use u SET claimed_at = now()
|
|
818
|
+
FROM access_link l
|
|
819
|
+
WHERE u.id = ${parts.useId} AND l.id = u.link_id AND l.code = ${parts.code}
|
|
820
|
+
AND u.tenant_id = ${tenantId} AND u.claimed_at IS NULL AND u.status = 'pending'
|
|
821
|
+
AND u.reserved_until > clock_timestamp()
|
|
822
|
+
AND NOT (l.expires_at IS NOT NULL AND l.expires_at <= now())
|
|
823
|
+
RETURNING l.id AS link_id
|
|
824
|
+
`)
|
|
825
|
+
if (claimed.length > 0) {
|
|
826
|
+
const row = (await tx.execute(
|
|
827
|
+
linkWithState(sql`l.id = ${claimed[0]!['link_id'] as string} AND l.tenant_id = ${tenantId}`),
|
|
828
|
+
))[0]!
|
|
829
|
+
const link = rowToPublic(row)
|
|
830
|
+
return issue({ ok: true as const, link }, tenantId, link.id)
|
|
831
|
+
}
|
|
832
|
+
const rows = await tx.execute(sql`
|
|
833
|
+
SELECT u.status, (u.claimed_at IS NOT NULL) AS claimed
|
|
834
|
+
FROM access_link_use u JOIN access_link l ON l.id = u.link_id
|
|
835
|
+
WHERE u.id = ${parts.useId} AND l.code = ${parts.code} AND u.tenant_id = ${tenantId}
|
|
836
|
+
`)
|
|
837
|
+
const row = rows[0]
|
|
838
|
+
if (row === undefined) return { ok: false, reason: 'not-found' }
|
|
839
|
+
if (row['status'] !== 'pending') return { ok: false, reason: 'already-settled' }
|
|
840
|
+
if (row['claimed'] === true) return { ok: false, reason: 'already-claimed' }
|
|
841
|
+
return { ok: false, reason: 'expired-reservation' }
|
|
842
|
+
})
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
completeUseWhenSent(response, useToken, options) {
|
|
846
|
+
const flow = requireContext()
|
|
847
|
+
// The request's transaction is over when this runs: keep the instance and
|
|
848
|
+
// the trace, drop the bound transactions.
|
|
849
|
+
const detached: Flow = { ...flow, _nonce: randomUUID(), transactions: new Map() }
|
|
850
|
+
response.onFinish(async (outcome) => {
|
|
851
|
+
await runInContext(detached, async () => {
|
|
852
|
+
if (outcome === 'sent') await handle.completeUse(useToken, options)
|
|
853
|
+
else await handle.abandonUse(useToken)
|
|
854
|
+
})
|
|
855
|
+
})
|
|
856
|
+
},
|
|
857
|
+
|
|
858
|
+
async asLink(grant, fn) {
|
|
859
|
+
const issued = grants.get(grant)
|
|
860
|
+
if (issued === undefined) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
'[pipework] accessLinks.asLink needs the result of resolve (status "active") or beginUse (ok) ' +
|
|
863
|
+
'exactly as this handle returned it.\n\n' +
|
|
864
|
+
' A copied, rebuilt or refused result is not a grant. Check `status === "active"` / `ok` first.\n',
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
const flow = requireContext()
|
|
868
|
+
const dbName = flow.instance.config.defaultDatabaseName()
|
|
869
|
+
const auth = { userId: `link:${issued.linkId}`, accessLinkId: issued.linkId }
|
|
870
|
+
const childTrace = openChildTrace(flow, issued.tenantId, auth)
|
|
871
|
+
const child: Flow = {
|
|
872
|
+
...flow,
|
|
873
|
+
_nonce: randomUUID(),
|
|
874
|
+
tenant: issued.tenantId,
|
|
875
|
+
user: null,
|
|
876
|
+
auth,
|
|
877
|
+
transactions: new Map(),
|
|
878
|
+
trace: childTrace,
|
|
879
|
+
}
|
|
880
|
+
// A NEW connection as the app role, with the link's tenant as its locals —
|
|
881
|
+
// the same transaction `pipe.transaction` opens, for the child's tenant.
|
|
882
|
+
const conn = flow.instance.pool.getOrCreate(flow.instance.config.database(dbName), 'app')
|
|
883
|
+
return finalizeTrace(flow.instance, childTrace, conn.drizzle.transaction(async (tx) => {
|
|
884
|
+
child.transactions.set(dbName, bindTransaction(child, dbName, tx))
|
|
885
|
+
await propagateLocals(tx, child, child.tenantConfig)
|
|
886
|
+
return runInContext(child, fn)
|
|
887
|
+
}))
|
|
888
|
+
},
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return handle
|
|
892
|
+
}
|