pipework 0.252.0 → 0.252.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/package.json +4 -3
- package/src/async/jobs/abortable-delay.ts +18 -0
- package/src/async/jobs/claims.ts +52 -0
- package/src/async/jobs/cron.ts +179 -0
- package/src/async/jobs/errors.ts +56 -0
- package/src/async/jobs/exclusion.ts +117 -0
- package/src/async/jobs/execute.ts +219 -0
- package/src/async/jobs/index.ts +10 -0
- package/src/async/jobs/listener.ts +135 -0
- package/src/async/jobs/namespace.ts +20 -0
- package/src/async/jobs/queue-door.ts +44 -0
- package/src/async/jobs/queue.ts +1012 -0
- package/src/async/jobs/run-script.ts +67 -0
- package/src/async/jobs/scheduler.ts +218 -0
- package/src/async/jobs/schedules-table.ts +37 -0
- package/src/async/jobs/serialize-error.ts +48 -0
- package/src/async/jobs/table.ts +191 -0
- package/src/async/pipeline/define.ts +53 -0
- package/src/async/pipeline/errors.ts +35 -0
- package/src/async/pipeline/execute.ts +53 -0
- package/src/async/pipeline/index.ts +13 -0
- package/src/async/pipeline/namespace.ts +12 -0
- package/src/async/pipeline/resume.ts +67 -0
- package/src/async/pipeline/run-steps.ts +98 -0
- package/src/async/pipeline/types.ts +57 -0
- package/src/async/state-machine/define.ts +64 -0
- package/src/async/state-machine/errors.ts +29 -0
- package/src/async/state-machine/index.ts +3 -0
- package/src/async/state-machine/namespace.ts +12 -0
- package/src/async/state-machine/transition.ts +90 -0
- package/src/async/state-machine/types.ts +37 -0
- package/src/async/steps/errors.ts +31 -0
- package/src/async/steps/graph.ts +1214 -0
- package/src/async/steps/hash.ts +40 -0
- package/src/async/steps/index.ts +7 -0
- package/src/async/steps/namespace.ts +7 -0
- package/src/async/steps/table.ts +97 -0
- package/src/async/steps/types.ts +318 -0
- package/src/audit/chain/index.ts +9 -0
- package/src/audit/chain/install.ts +113 -0
- package/src/audit/chain/retire.ts +52 -0
- package/src/audit/chain/verify.ts +257 -0
- package/src/audit/query/cursor.ts +37 -0
- package/src/audit/query/index.ts +9 -0
- package/src/audit/query/query.ts +223 -0
- package/src/audit/schema/audit-record-fields.ts +68 -0
- package/src/audit/schema/audit-record.ts +23 -0
- package/src/auth/auth/audit.ts +31 -0
- package/src/auth/auth/chain.ts +84 -0
- package/src/auth/auth/cookie.ts +136 -0
- package/src/auth/auth/email-links.ts +311 -0
- package/src/auth/auth/guard.ts +128 -0
- package/src/auth/auth/index.ts +18 -0
- package/src/auth/auth/multi-org.ts +337 -0
- package/src/auth/auth/namespace.ts +34 -0
- package/src/auth/auth/parse-expiry.ts +13 -0
- package/src/auth/auth/passwords.ts +110 -0
- package/src/auth/auth/schema.ts +110 -0
- package/src/auth/auth/sessions.ts +565 -0
- package/src/auth/auth/sign-in.ts +156 -0
- package/src/auth/auth/signing-keys.ts +69 -0
- package/src/auth/auth/types.ts +26 -0
- package/src/auth/auth/uuid.ts +10 -0
- package/src/auth/rbac/cache.ts +55 -0
- package/src/auth/rbac/check.ts +30 -0
- package/src/auth/rbac/create.ts +35 -0
- package/src/auth/rbac/index.ts +5 -0
- package/src/auth/rbac/middleware.ts +20 -0
- package/src/auth/rbac/namespace.ts +15 -0
- package/src/auth/rbac/resolve.ts +31 -0
- package/src/auth/rbac/schema.sql +31 -0
- package/src/auth/rbac/seed.ts +67 -0
- package/src/auth/rbac/tables.ts +32 -0
- package/src/auth/rbac/types.ts +65 -0
- package/src/auth/tenant/extract.ts +33 -0
- package/src/auth/tenant/in-tenant.ts +49 -0
- package/src/auth/tenant/index.ts +7 -0
- package/src/auth/tenant/namespace.ts +23 -0
- package/src/auth/tenant/propagate.ts +20 -0
- package/src/auth/tenant/rls.ts +120 -0
- package/src/auth/tenant/scope.ts +33 -0
- package/src/auth/tenant/scoped-db.ts +581 -0
- package/src/auth/tenant/types.ts +14 -0
- package/src/auth/tenant/validate.ts +27 -0
- package/src/cli/commands/add.ts +158 -0
- package/src/cli/commands/alerts.ts +98 -0
- package/src/cli/commands/check.ts +1418 -0
- package/src/cli/commands/codemod.ts +78 -0
- package/src/cli/commands/db.ts +356 -0
- package/src/cli/commands/dev.ts +101 -0
- package/src/cli/commands/generate.ts +75 -0
- package/src/cli/commands/init.ts +36 -0
- package/src/cli/commands/install.ts +28 -0
- package/src/cli/commands/lint.ts +62 -0
- package/src/cli/commands/migrate.ts +132 -0
- package/src/cli/commands/openapi.ts +63 -0
- package/src/cli/commands/remove.ts +72 -0
- package/src/cli/commands/run.ts +50 -0
- package/src/cli/commands/scope.ts +54 -0
- package/src/cli/commands/serve.ts +68 -0
- package/src/cli/commands/shard.ts +31 -0
- package/src/cli/commands/shared/config-pipework-version.ts +28 -0
- package/src/cli/commands/shared/find-config.ts +22 -0
- package/src/cli/commands/shared/load-config.ts +35 -0
- package/src/cli/commands/shared/pnpm-install.ts +95 -0
- package/src/cli/commands/shared/report-removed.ts +8 -0
- package/src/cli/commands/sync.ts +32 -0
- package/src/cli/commands/test.ts +307 -0
- package/src/cli/commands/upgrade.ts +48 -0
- package/src/cli/dev-bootstrap.ts +24 -0
- package/src/cli/errors.ts +10 -0
- package/src/cli/index.ts +303 -0
- package/src/cli/output.ts +17 -0
- package/src/cli/port.ts +15 -0
- package/src/cli/surface-kinds.ts +20 -0
- package/src/client/index.ts +4 -0
- package/src/client/poll.ts +130 -0
- package/src/client/report-fault.ts +119 -0
- package/src/core/config/app-pool.ts +19 -0
- package/src/core/config/aws-env.ts +29 -0
- package/src/core/config/base-dir.ts +24 -0
- package/src/core/config/discover.ts +64 -0
- package/src/core/config/env-resolve.ts +87 -0
- package/src/core/config/env-types.ts +116 -0
- package/src/core/config/env.ts +55 -0
- package/src/core/config/errors.ts +9 -0
- package/src/core/config/import-config.ts +40 -0
- package/src/core/config/index.ts +8 -0
- package/src/core/config/load.ts +417 -0
- package/src/core/config/namespace.ts +14 -0
- package/src/core/config/resolve.ts +195 -0
- package/src/core/config/schema.ts +488 -0
- package/src/core/config/statement-observer.ts +48 -0
- package/src/core/config/ts-inline-hooks.ts +75 -0
- package/src/core/config/ts-register.ts +4 -0
- package/src/core/config/ts-resolve-hooks.ts +101 -0
- package/src/core/errors/base.ts +49 -0
- package/src/core/errors/http.ts +146 -0
- package/src/core/errors/index.ts +2 -0
- package/src/core/errors/namespace.ts +31 -0
- package/src/core/lifecycle/drain.ts +33 -0
- package/src/core/lifecycle/health.ts +72 -0
- package/src/core/lifecycle/index.ts +4 -0
- package/src/core/lifecycle/namespace.ts +7 -0
- package/src/core/lifecycle/startup.ts +102 -0
- package/src/core/lifecycle/types.ts +36 -0
- package/src/core/logging/context.ts +60 -0
- package/src/core/logging/error-chain.ts +68 -0
- package/src/core/logging/index.ts +5 -0
- package/src/core/logging/logger.ts +65 -0
- package/src/core/logging/namespace.ts +16 -0
- package/src/core/logging/proxy.ts +22 -0
- package/src/core/logging/redact-match.ts +24 -0
- package/src/core/logging/types.ts +47 -0
- package/src/core/pipework.ts +566 -0
- package/src/core/runtime/broker.ts +69 -0
- package/src/core/surface/http.ts +133 -0
- package/src/core/surface/index.ts +11 -0
- package/src/core/surface/job-timeout.ts +40 -0
- package/src/core/surface/namespace.ts +13 -0
- package/src/core/surface/script.ts +33 -0
- package/src/core/surface/start.ts +142 -0
- package/src/core/surface/types.ts +225 -0
- package/src/core/surface/validate.ts +88 -0
- package/src/core/surface/worker.ts +528 -0
- package/src/core/validation/connectivity.ts +49 -0
- package/src/core/validation/domain.ts +99 -0
- package/src/core/validation/index.ts +5 -0
- package/src/core/validation/queue-tables.ts +312 -0
- package/src/core/validation/rls-role.ts +138 -0
- package/src/core/validation/startup.ts +51 -0
- package/src/core/validation/surfaces.ts +65 -0
- package/src/core/validation/types.ts +25 -0
- package/src/data/db/advisory-lock.ts +55 -0
- package/src/data/db/aggregates.ts +172 -0
- package/src/data/db/array-elements.ts +111 -0
- package/src/data/db/bulk-set.ts +53 -0
- package/src/data/db/col/index.ts +14 -0
- package/src/data/db/composed.ts +53 -0
- package/src/data/db/db.ts +276 -0
- package/src/data/db/excluded.ts +16 -0
- package/src/data/db/expressions.ts +473 -0
- package/src/data/db/filter.ts +90 -0
- package/src/data/db/function-source.ts +114 -0
- package/src/data/db/identifiers.ts +16 -0
- package/src/data/db/idx/index.ts +9 -0
- package/src/data/db/in-list.ts +49 -0
- package/src/data/db/index.ts +9 -0
- package/src/data/db/insert-split.ts +137 -0
- package/src/data/db/instrumentation.ts +82 -0
- package/src/data/db/maintenance.ts +28 -0
- package/src/data/db/namespace.ts +107 -0
- package/src/data/db/owner-labels.ts +9 -0
- package/src/data/db/pool.ts +187 -0
- package/src/data/db/query-label.ts +13 -0
- package/src/data/db/query-watch.ts +116 -0
- package/src/data/db/raw-client-warning.ts +37 -0
- package/src/data/db/readonly.ts +23 -0
- package/src/data/db/reservation.ts +151 -0
- package/src/data/db/schema-features.ts +14 -0
- package/src/data/db/serializable.ts +83 -0
- package/src/data/db/sql.ts +1 -0
- package/src/data/db/timeouts.ts +76 -0
- package/src/data/db/types.ts +18 -0
- package/src/data/db/unbound.ts +33 -0
- package/src/data/db/values.ts +29 -0
- package/src/data/domain/brand.ts +15 -0
- package/src/data/domain/build-factory.ts +193 -0
- package/src/data/domain/build-schema.ts +161 -0
- package/src/data/domain/build-table.ts +355 -0
- package/src/data/domain/define.ts +239 -0
- package/src/data/domain/external-registry.ts +34 -0
- package/src/data/domain/external.ts +77 -0
- package/src/data/domain/field.ts +279 -0
- package/src/data/domain/index.ts +13 -0
- package/src/data/domain/naming.ts +6 -0
- package/src/data/domain/projections.ts +57 -0
- package/src/data/domain/trace-safe-projection.ts +31 -0
- package/src/data/domain/traits.ts +108 -0
- package/src/data/domain/types.ts +289 -0
- package/src/data/jsonb/composed.ts +18 -0
- package/src/data/jsonb/index.ts +1 -0
- package/src/data/jsonb/namespace.ts +30 -0
- package/src/data/jsonb/query.ts +86 -0
- package/src/data/migrate/append-only-generate.ts +47 -0
- package/src/data/migrate/applied.ts +47 -0
- package/src/data/migrate/apply.ts +309 -0
- package/src/data/migrate/check.ts +119 -0
- package/src/data/migrate/diff.ts +357 -0
- package/src/data/migrate/door-generate.ts +130 -0
- package/src/data/migrate/framework-rls.ts +58 -0
- package/src/data/migrate/generate.ts +201 -0
- package/src/data/migrate/history.ts +215 -0
- package/src/data/migrate/index.ts +9 -0
- package/src/data/migrate/internal-definitions.ts +175 -0
- package/src/data/migrate/post-process.ts +73 -0
- package/src/data/migrate/pragma.ts +73 -0
- package/src/data/migrate/provision-framework.ts +147 -0
- package/src/data/migrate/rls-generate.ts +61 -0
- package/src/data/migrate/run.ts +124 -0
- package/src/data/migrate/safety.ts +167 -0
- package/src/data/migrate/snapshot.ts +368 -0
- package/src/data/migrate/sql-emit.ts +392 -0
- package/src/data/migrate/statements.ts +312 -0
- package/src/data/query/alias.ts +126 -0
- package/src/data/query/batch.ts +8 -0
- package/src/data/query/cache/core/cache.ts +78 -0
- package/src/data/query/cache/core/index.ts +1 -0
- package/src/data/query/cache/core/types.ts +29 -0
- package/src/data/query/cache/readme.md +230 -0
- package/src/data/query/casing.ts +76 -0
- package/src/data/query/collation.ts +11 -0
- package/src/data/query/column-builder.ts +370 -0
- package/src/data/query/column.ts +150 -0
- package/src/data/query/entity.ts +42 -0
- package/src/data/query/errors.ts +33 -0
- package/src/data/query/index.ts +14 -0
- package/src/data/query/logger.ts +47 -0
- package/src/data/query/operations.ts +45 -0
- package/src/data/query/pg-core/alias.ts +12 -0
- package/src/data/query/pg-core/checks.ts +32 -0
- package/src/data/query/pg-core/columns/all.ts +71 -0
- package/src/data/query/pg-core/columns/bigint.ts +110 -0
- package/src/data/query/pg-core/columns/bigserial.ts +128 -0
- package/src/data/query/pg-core/columns/boolean.ts +43 -0
- package/src/data/query/pg-core/columns/char.ts +85 -0
- package/src/data/query/pg-core/columns/cidr.ts +43 -0
- package/src/data/query/pg-core/columns/common.ts +366 -0
- package/src/data/query/pg-core/columns/custom.ts +232 -0
- package/src/data/query/pg-core/columns/date.common.ts +15 -0
- package/src/data/query/pg-core/columns/date.ts +112 -0
- package/src/data/query/pg-core/columns/double-precision.ts +55 -0
- package/src/data/query/pg-core/columns/enum.ts +201 -0
- package/src/data/query/pg-core/columns/index.ts +35 -0
- package/src/data/query/pg-core/columns/inet.ts +43 -0
- package/src/data/query/pg-core/columns/int.common.ts +57 -0
- package/src/data/query/pg-core/columns/integer.ts +53 -0
- package/src/data/query/pg-core/columns/interval.ts +83 -0
- package/src/data/query/pg-core/columns/json.ts +58 -0
- package/src/data/query/pg-core/columns/jsonb.ts +56 -0
- package/src/data/query/pg-core/columns/line.ts +117 -0
- package/src/data/query/pg-core/columns/macaddr.ts +43 -0
- package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
- package/src/data/query/pg-core/columns/numeric.ts +227 -0
- package/src/data/query/pg-core/columns/point.ts +127 -0
- package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
- package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
- package/src/data/query/pg-core/columns/real.ts +58 -0
- package/src/data/query/pg-core/columns/serial.ts +55 -0
- package/src/data/query/pg-core/columns/smallint.ts +53 -0
- package/src/data/query/pg-core/columns/smallserial.ts +60 -0
- package/src/data/query/pg-core/columns/text.ts +67 -0
- package/src/data/query/pg-core/columns/time.ts +72 -0
- package/src/data/query/pg-core/columns/timestamp.ts +163 -0
- package/src/data/query/pg-core/columns/uuid.ts +51 -0
- package/src/data/query/pg-core/columns/varchar.ts +89 -0
- package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
- package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
- package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
- package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
- package/src/data/query/pg-core/db.ts +674 -0
- package/src/data/query/pg-core/dialect.ts +1404 -0
- package/src/data/query/pg-core/expressions.ts +25 -0
- package/src/data/query/pg-core/foreign-keys.ts +130 -0
- package/src/data/query/pg-core/index.ts +21 -0
- package/src/data/query/pg-core/indexes.ts +276 -0
- package/src/data/query/pg-core/policies.ts +56 -0
- package/src/data/query/pg-core/primary-keys.ts +62 -0
- package/src/data/query/pg-core/query-builders/count.ts +86 -0
- package/src/data/query/pg-core/query-builders/delete.ts +293 -0
- package/src/data/query/pg-core/query-builders/index.ts +7 -0
- package/src/data/query/pg-core/query-builders/insert.ts +452 -0
- package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
- package/src/data/query/pg-core/query-builders/query.ts +157 -0
- package/src/data/query/pg-core/query-builders/raw.ts +49 -0
- package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
- package/src/data/query/pg-core/query-builders/select.ts +1346 -0
- package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
- package/src/data/query/pg-core/query-builders/update.ts +626 -0
- package/src/data/query/pg-core/roles.ts +41 -0
- package/src/data/query/pg-core/schema.ts +72 -0
- package/src/data/query/pg-core/sequence.ts +41 -0
- package/src/data/query/pg-core/session.ts +292 -0
- package/src/data/query/pg-core/subquery.ts +46 -0
- package/src/data/query/pg-core/table.ts +252 -0
- package/src/data/query/pg-core/unique-constraint.ts +73 -0
- package/src/data/query/pg-core/utils/array.ts +95 -0
- package/src/data/query/pg-core/utils/index.ts +1 -0
- package/src/data/query/pg-core/utils.ts +104 -0
- package/src/data/query/pg-core/view-base.ts +14 -0
- package/src/data/query/pg-core/view-common.ts +1 -0
- package/src/data/query/pg-core/view.ts +435 -0
- package/src/data/query/pg-core/with-builder.ts +41 -0
- package/src/data/query/postgres-js/README.md +46 -0
- package/src/data/query/postgres-js/array-parser.ts +45 -0
- package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
- package/src/data/query/postgres-js/driver.ts +163 -0
- package/src/data/query/postgres-js/index.ts +2 -0
- package/src/data/query/postgres-js/session.ts +234 -0
- package/src/data/query/primary-key.ts +11 -0
- package/src/data/query/query-builders/query-builder.ts +19 -0
- package/src/data/query/query-builders/select.types.ts +175 -0
- package/src/data/query/query-promise.ts +35 -0
- package/src/data/query/relations.ts +725 -0
- package/src/data/query/runnable-query.ts +12 -0
- package/src/data/query/selection-proxy.ts +121 -0
- package/src/data/query/session.ts +8 -0
- package/src/data/query/sql/expressions/conditions.ts +741 -0
- package/src/data/query/sql/expressions/index.ts +2 -0
- package/src/data/query/sql/expressions/select.ts +45 -0
- package/src/data/query/sql/functions/aggregate.ts +129 -0
- package/src/data/query/sql/functions/index.ts +2 -0
- package/src/data/query/sql/functions/vector.ts +178 -0
- package/src/data/query/sql/index.ts +3 -0
- package/src/data/query/sql/sql.ts +794 -0
- package/src/data/query/subquery.ts +53 -0
- package/src/data/query/table.ts +208 -0
- package/src/data/query/table.utils.ts +2 -0
- package/src/data/query/tracing-utils.ts +3 -0
- package/src/data/query/tracing.ts +11 -0
- package/src/data/query/utils.ts +357 -0
- package/src/data/query/version.ts +4 -0
- package/src/data/query/view-common.ts +1 -0
- package/src/data/schema/branded.ts +13 -0
- package/src/data/schema/generated/coerce.ts +5 -0
- package/src/data/schema/generated/combinator.ts +6 -0
- package/src/data/schema/generated/encoding.ts +5 -0
- package/src/data/schema/generated/error.ts +6 -0
- package/src/data/schema/generated/format.ts +5 -0
- package/src/data/schema/generated/type-constructor.ts +10 -0
- package/src/data/schema/generated/type.ts +6 -0
- package/src/data/schema/index.ts +6 -0
- package/src/data/schema/json-schema.ts +33 -0
- package/src/data/schema/namespace.ts +55 -0
- package/src/data/schema/parse.ts +39 -0
- package/src/data/schema/types.ts +39 -0
- package/src/data/temporal/append-only.ts +134 -0
- package/src/data/temporal/close.ts +27 -0
- package/src/data/temporal/columns.ts +10 -0
- package/src/data/temporal/definition-queries.ts +70 -0
- package/src/data/temporal/errors.ts +10 -0
- package/src/data/temporal/get-current.ts +22 -0
- package/src/data/temporal/index.ts +9 -0
- package/src/data/temporal/namespace.ts +36 -0
- package/src/data/temporal/query.ts +25 -0
- package/src/data/temporal/resolve.ts +17 -0
- package/src/data/temporal/revise.ts +67 -0
- package/src/data/temporal/types.ts +22 -0
- package/src/data/valve/artifact.ts +33 -0
- package/src/data/valve/contract-diff.ts +21 -0
- package/src/data/valve/cross.ts +57 -0
- package/src/data/valve/crossing-ledger.ts +50 -0
- package/src/data/valve/decision.ts +44 -0
- package/src/data/valve/durable-crossing.ts +52 -0
- package/src/data/valve/execute-crossing.ts +77 -0
- package/src/data/valve/faucet.ts +56 -0
- package/src/data/valve/gate.ts +114 -0
- package/src/data/valve/graph.ts +77 -0
- package/src/data/valve/index.ts +53 -0
- package/src/data/valve/namespace.ts +57 -0
- package/src/data/valve/registry.ts +50 -0
- package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
- package/src/data/valve/schema/valve-crossing.ts +9 -0
- package/src/data/valve/supply.ts +154 -0
- package/src/data/valve/types.ts +143 -0
- package/src/data/vector/columns.ts +27 -0
- package/src/data/vector/index.ts +4 -0
- package/src/data/vector/namespace.ts +33 -0
- package/src/data/vector/query.ts +85 -0
- package/src/data/vector/validate.ts +16 -0
- package/src/data/write/audit/actor.ts +15 -0
- package/src/data/write/audit/audit-columns.ts +18 -0
- package/src/data/write/audit/audit-select.ts +111 -0
- package/src/data/write/audit/bound-transaction.ts +14 -0
- package/src/data/write/audit/cte-compile.ts +32 -0
- package/src/data/write/audit/decode-returning.ts +47 -0
- package/src/data/write/audit/final-select.ts +43 -0
- package/src/data/write/audit/infra-audit.ts +36 -0
- package/src/data/write/audit/primary-key.ts +65 -0
- package/src/data/write/audit/projection.ts +41 -0
- package/src/data/write/audit/read-touches.ts +86 -0
- package/src/data/write/audit/touch-match.ts +78 -0
- package/src/data/write/audit/verb-delete.ts +62 -0
- package/src/data/write/audit/verb-insert.ts +36 -0
- package/src/data/write/audit/verb-select.ts +47 -0
- package/src/data/write/audit/verb-update.ts +60 -0
- package/src/data/write/audit/verb-upsert.ts +104 -0
- package/src/data/write/conflict.ts +43 -0
- package/src/data/write/delete.ts +38 -0
- package/src/data/write/execute.ts +243 -0
- package/src/data/write/index.ts +11 -0
- package/src/data/write/insert.ts +63 -0
- package/src/data/write/state.ts +43 -0
- package/src/data/write/update.ts +54 -0
- package/src/data/write/upsert.ts +58 -0
- package/src/data/write/values.ts +19 -0
- package/src/index.ts +230 -0
- package/src/infra/access-links/code.ts +70 -0
- package/src/infra/access-links/handle.ts +892 -0
- package/src/infra/access-links/index.ts +9 -0
- package/src/infra/access-links/namespace.ts +30 -0
- package/src/infra/access-links/person-check.ts +177 -0
- package/src/infra/access-links/schema.ts +71 -0
- package/src/infra/alerts/async-sources.ts +231 -0
- package/src/infra/alerts/audit.ts +36 -0
- package/src/infra/alerts/beat.ts +123 -0
- package/src/infra/alerts/client-faults.ts +111 -0
- package/src/infra/alerts/faults.ts +156 -0
- package/src/infra/alerts/handle.ts +778 -0
- package/src/infra/alerts/http.ts +192 -0
- package/src/infra/alerts/index.ts +12 -0
- package/src/infra/alerts/infra-sources.ts +180 -0
- package/src/infra/alerts/namespace.ts +81 -0
- package/src/infra/alerts/redact.ts +21 -0
- package/src/infra/alerts/request-faults.ts +41 -0
- package/src/infra/alerts/schema.ts +124 -0
- package/src/infra/alerts/sinks.ts +125 -0
- package/src/infra/alerts/source-options.ts +26 -0
- package/src/infra/alerts/sources.ts +503 -0
- package/src/infra/alerts/subsystem-sources.ts +137 -0
- package/src/infra/audit/emit.ts +87 -0
- package/src/infra/audit/index.ts +2 -0
- package/src/infra/audit/namespace.ts +22 -0
- package/src/infra/audit/schema.sql +35 -0
- package/src/infra/audit/types.ts +18 -0
- package/src/infra/aws/credentials.ts +147 -0
- package/src/infra/aws/sigv4.ts +140 -0
- package/src/infra/cache/create.ts +58 -0
- package/src/infra/cache/entry.ts +81 -0
- package/src/infra/cache/index.ts +4 -0
- package/src/infra/cache/namespace.ts +10 -0
- package/src/infra/cache/tenant.ts +93 -0
- package/src/infra/cache/types.ts +24 -0
- package/src/infra/crawl/audit.ts +35 -0
- package/src/infra/crawl/errors.ts +25 -0
- package/src/infra/crawl/handle.ts +693 -0
- package/src/infra/crawl/index.ts +9 -0
- package/src/infra/crawl/links.ts +160 -0
- package/src/infra/crawl/namespace.ts +36 -0
- package/src/infra/crawl/robots.ts +100 -0
- package/src/infra/crawl/schema.ts +89 -0
- package/src/infra/credit-line/handle.ts +883 -0
- package/src/infra/credit-line/index.ts +6 -0
- package/src/infra/credit-line/namespace.ts +7 -0
- package/src/infra/credit-line/schema.ts +96 -0
- package/src/infra/credits/handle.ts +706 -0
- package/src/infra/credits/index.ts +5 -0
- package/src/infra/credits/namespace.ts +7 -0
- package/src/infra/credits/schema.ts +83 -0
- package/src/infra/email/audit.ts +36 -0
- package/src/infra/email/inbound.ts +622 -0
- package/src/infra/email/index.ts +11 -0
- package/src/infra/email/mail.ts +517 -0
- package/src/infra/email/mime.ts +447 -0
- package/src/infra/email/namespace.ts +22 -0
- package/src/infra/email/providers.ts +111 -0
- package/src/infra/email/schema.ts +96 -0
- package/src/infra/email/sns.ts +200 -0
- package/src/infra/email/unsubscribe.ts +44 -0
- package/src/infra/fetch/address.ts +88 -0
- package/src/infra/fetch/connect-retry.ts +87 -0
- package/src/infra/fetch/handle.ts +329 -0
- package/src/infra/fetch/index.ts +7 -0
- package/src/infra/fetch/namespace.ts +70 -0
- package/src/infra/idempotency/index.ts +3 -0
- package/src/infra/idempotency/middleware.ts +87 -0
- package/src/infra/idempotency/store.ts +92 -0
- package/src/infra/idempotency/types.ts +30 -0
- package/src/infra/join-requests/handle.ts +312 -0
- package/src/infra/join-requests/index.ts +2 -0
- package/src/infra/join-requests/namespace.ts +7 -0
- package/src/infra/join-requests/schema.ts +33 -0
- package/src/infra/outbox/core.ts +165 -0
- package/src/infra/quota/errors.ts +32 -0
- package/src/infra/quota/handle.ts +215 -0
- package/src/infra/quota/index.ts +5 -0
- package/src/infra/quota/namespace.ts +7 -0
- package/src/infra/quota/schema.ts +15 -0
- package/src/infra/storage/backend.ts +34 -0
- package/src/infra/storage/handle.ts +513 -0
- package/src/infra/storage/index.ts +9 -0
- package/src/infra/storage/local.ts +104 -0
- package/src/infra/storage/namespace.ts +13 -0
- package/src/infra/storage/s3.ts +257 -0
- package/src/infra/storage/schema.ts +24 -0
- package/src/infra/support/handle.ts +854 -0
- package/src/infra/support/http.ts +229 -0
- package/src/infra/support/index.ts +7 -0
- package/src/infra/support/namespace.ts +12 -0
- package/src/infra/support/schema.ts +96 -0
- package/src/infra/webhook/inbound.ts +45 -0
- package/src/infra/webhook/index.ts +5 -0
- package/src/infra/webhook/namespace.ts +24 -0
- package/src/infra/webhook/outbound.ts +315 -0
- package/src/infra/webhook/sign.ts +32 -0
- package/src/infra/webhook/verify.ts +82 -0
- package/src/invariants/assert.ts +26 -0
- package/src/invariants/index.ts +1 -0
- package/src/lint/config.ts +56 -0
- package/src/lint/doctrine.ts +163 -0
- package/src/lint/include.ts +97 -0
- package/src/lint/index.ts +3 -0
- package/src/lint/preset.ts +10 -0
- package/src/lint/resolver.ts +218 -0
- package/src/lint/rules/no-reexports.ts +46 -0
- package/src/request/behavior/audited.ts +85 -0
- package/src/request/behavior/cached.ts +62 -0
- package/src/request/behavior/compose.ts +28 -0
- package/src/request/behavior/index.ts +5 -0
- package/src/request/behavior/namespace.ts +16 -0
- package/src/request/behavior/types.ts +26 -0
- package/src/request/behavior/versioned.ts +54 -0
- package/src/request/context/create.ts +103 -0
- package/src/request/context/guard.ts +49 -0
- package/src/request/context/index.ts +4 -0
- package/src/request/context/run-in-transaction.ts +200 -0
- package/src/request/context/store.ts +28 -0
- package/src/request/context/types.ts +50 -0
- package/src/request/di/builder.ts +535 -0
- package/src/request/di/index.ts +8 -0
- package/src/request/di/resolve.ts +271 -0
- package/src/request/di/types.ts +262 -0
- package/src/request/fitting/namespace.ts +7 -0
- package/src/request/fixture/batch.ts +71 -0
- package/src/request/fixture/builder.ts +26 -0
- package/src/request/fixture/errors.ts +10 -0
- package/src/request/fixture/index.ts +8 -0
- package/src/request/fixture/namespace.ts +23 -0
- package/src/request/fixture/paginate.ts +44 -0
- package/src/request/fixture/to-handlers.ts +260 -0
- package/src/request/fixture/types.ts +79 -0
- package/src/request/flow/act-as.ts +78 -0
- package/src/request/flow/namespace.ts +17 -0
- package/src/request/http/error-step.ts +42 -0
- package/src/request/http/file.ts +141 -0
- package/src/request/http/index.ts +4 -0
- package/src/request/http/middleware.ts +141 -0
- package/src/request/http/namespace.ts +5 -0
- package/src/request/http/rate-limit-classes.ts +170 -0
- package/src/request/http/response.ts +82 -0
- package/src/request/http/server.ts +916 -0
- package/src/request/http/sniff.ts +261 -0
- package/src/request/http/types.ts +250 -0
- package/src/request/http/upload.ts +284 -0
- package/src/request/internal/text.ts +6 -0
- package/src/request/openapi/generate.ts +205 -0
- package/src/request/openapi/index.ts +2 -0
- package/src/request/openapi/namespace.ts +7 -0
- package/src/test/auth.ts +13 -0
- package/src/test/auto-setup.ts +45 -0
- package/src/test/context.ts +91 -0
- package/src/test/crawl-fixture.ts +157 -0
- package/src/test/discover-fork.ts +17 -0
- package/src/test/fingerprint.ts +135 -0
- package/src/test/globals.d.ts +7 -0
- package/src/test/index.ts +19 -0
- package/src/test/namespace.ts +87 -0
- package/src/test/plugin.ts +189 -0
- package/src/test/progress.ts +185 -0
- package/src/test/reap-after-run.ts +28 -0
- package/src/test/reap.ts +286 -0
- package/src/test/reset.ts +253 -0
- package/src/test/savepoints.ts +172 -0
- package/src/test/setup.ts +1422 -0
- package/src/test/system-channel.ts +50 -0
- package/src/test/vitest-plugin.ts +142 -0
- package/src/test/vitest-workspace.ts +507 -0
- package/src/test/vitest.ts +333 -0
- package/src/test/watch.ts +105 -0
- package/src/trace/capture/bounded-serializer.ts +146 -0
- package/src/trace/cardinality/analyze.ts +173 -0
- package/src/trace/cardinality/branch-context.ts +214 -0
- package/src/trace/cardinality/check.ts +33 -0
- package/src/trace/cardinality/classify-edge.ts +64 -0
- package/src/trace/cardinality/structural-max.ts +107 -0
- package/src/trace/check/coverage-check.ts +188 -0
- package/src/trace/check/flow-check.ts +103 -0
- package/src/trace/check/flow-step-site.ts +234 -0
- package/src/trace/check/io-pattern.ts +116 -0
- package/src/trace/codemod/add-cardinality.ts +152 -0
- package/src/trace/codemod/run.ts +80 -0
- package/src/trace/entry/child-trace.ts +21 -0
- package/src/trace/entry/close-trace.ts +87 -0
- package/src/trace/entry/finalize-trace.ts +28 -0
- package/src/trace/entry/open-trace.ts +138 -0
- package/src/trace/entry/parent-trace.ts +20 -0
- package/src/trace/entry/parse-traceparent.ts +39 -0
- package/src/trace/entry/process-start.ts +38 -0
- package/src/trace/entry/trace-context.ts +26 -0
- package/src/trace/entry/tracing-enabled.ts +10 -0
- package/src/trace/external/record.ts +195 -0
- package/src/trace/external/report.ts +66 -0
- package/src/trace/namespace.ts +19 -0
- package/src/trace/partition/day.ts +47 -0
- package/src/trace/partition/index.ts +14 -0
- package/src/trace/partition/partition-name.ts +23 -0
- package/src/trace/partition/plan.ts +82 -0
- package/src/trace/partition/schedule.ts +26 -0
- package/src/trace/partition/startup-hook.ts +39 -0
- package/src/trace/partition/sync.ts +154 -0
- package/src/trace/partition/tables.ts +53 -0
- package/src/trace/read/tree.ts +83 -0
- package/src/trace/retain/copy-on-error.ts +76 -0
- package/src/trace/runtime/expire-stale-traces.ts +54 -0
- package/src/trace/runtime/flow-step.ts +147 -0
- package/src/trace/runtime/flush-steps.ts +31 -0
- package/src/trace/runtime/pending-writes.ts +54 -0
- package/src/trace/runtime/step-buffer.ts +298 -0
- package/src/trace/runtime/step-error.ts +21 -0
- package/src/trace/runtime/step-meta.ts +55 -0
- package/src/trace/runtime/thenable.ts +12 -0
- package/src/trace/runtime/truncate-trace.ts +62 -0
- package/src/trace/schema/trace-fields.ts +52 -0
- package/src/trace/schema/trace-retained.ts +22 -0
- package/src/trace/schema/trace-step-fields.ts +44 -0
- package/src/trace/schema/trace-step-retained.ts +25 -0
- package/src/trace/schema/trace-step.ts +18 -0
- package/src/trace/schema/trace.ts +12 -0
- package/src/trace/transformer/apply.ts +79 -0
- package/src/trace/transformer/build-meta-literal.ts +30 -0
- package/src/trace/transformer/find-flow-step-sites.ts +85 -0
- package/src/trace/transformer/inject-flow-step-import.ts +39 -0
- package/src/trace/transformer/transform-error.ts +14 -0
- package/src/trace/transformer/transform-flow-steps.ts +123 -0
- package/src/trace/transformer/wrap-function-declaration.ts +62 -0
- package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
- package/src/workspace/affected.ts +110 -0
- package/src/workspace/changed-files.ts +163 -0
- package/src/workspace/check-deps.ts +213 -0
- package/src/workspace/checksum.ts +20 -0
- package/src/workspace/dependency-graph.ts +67 -0
- package/src/workspace/detect-pipework-copies.ts +93 -0
- package/src/workspace/discover.ts +206 -0
- package/src/workspace/generate.ts +401 -0
- package/src/workspace/index.ts +7 -0
- package/src/workspace/module-imports-pipework.ts +39 -0
- package/src/workspace/module-package-name.ts +12 -0
- package/src/workspace/scrub-git-env.ts +23 -0
- package/src/workspace/source-paths.ts +36 -0
- package/src/workspace/sync.ts +182 -0
- package/dist/REFERENCE.md +0 -1477
- package/dist/infra/access-links/rate-limit.d.ts +0 -10
- package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
- package/dist/infra/access-links/rate-limit.js +0 -28
- package/dist/infra/access-links/rate-limit.js.map +0 -1
- package/dist/infra/email/sigv4.d.ts +0 -24
- package/dist/infra/email/sigv4.d.ts.map +0 -1
- package/dist/infra/email/sigv4.js +0 -55
- package/dist/infra/email/sigv4.js.map +0 -1
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
// A hand-written MIME parser for received mail. Pure: bytes in, a plain object out.
|
|
2
|
+
// It never throws; malformed input degrades to the best reading. Internal to the
|
|
3
|
+
// inbound pipeline (not public API); exported for its tests.
|
|
4
|
+
//
|
|
5
|
+
// The message is held as a latin1 string (one char per byte) while it is split, so
|
|
6
|
+
// offsets are byte offsets and slices convert back losslessly; charsets are decoded
|
|
7
|
+
// only on the final text of a header or body.
|
|
8
|
+
|
|
9
|
+
export interface ParsedAddress {
|
|
10
|
+
readonly address: string
|
|
11
|
+
readonly name: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ParsedAttachment {
|
|
15
|
+
readonly filename: string | null
|
|
16
|
+
readonly mediaType: string
|
|
17
|
+
readonly size: number
|
|
18
|
+
readonly contentId: string | null
|
|
19
|
+
readonly inline: boolean
|
|
20
|
+
readonly content: Buffer
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ParsedMessage {
|
|
24
|
+
/** Message-ID without angle brackets, or null. */
|
|
25
|
+
readonly messageId: string | null
|
|
26
|
+
/** The first In-Reply-To id without angle brackets, or null. */
|
|
27
|
+
readonly inReplyTo: string | null
|
|
28
|
+
/** In-Reply-To ids (first) then References ids, without angle brackets, duplicates removed. */
|
|
29
|
+
readonly threadIds: readonly string[]
|
|
30
|
+
/** References ids without angle brackets, in order. */
|
|
31
|
+
readonly references: readonly string[]
|
|
32
|
+
/** The author, or null when there is no readable From or the author is ambiguous (see `fromProblem`). */
|
|
33
|
+
readonly from: ParsedAddress | null
|
|
34
|
+
/**
|
|
35
|
+
* Why `from` is null for a message that had a From: several From fields, or several mailboxes in one.
|
|
36
|
+
* Null otherwise. RFC 7489 section 6.6.1: verifiers differ in which one they judge, so none is named.
|
|
37
|
+
*/
|
|
38
|
+
readonly fromProblem: 'several-from-fields' | 'several-mailboxes' | null
|
|
39
|
+
readonly to: readonly ParsedAddress[]
|
|
40
|
+
readonly cc: readonly ParsedAddress[]
|
|
41
|
+
readonly replyTo: readonly ParsedAddress[]
|
|
42
|
+
readonly subject: string
|
|
43
|
+
readonly date: Date | null
|
|
44
|
+
readonly text: string | null
|
|
45
|
+
readonly html: string | null
|
|
46
|
+
readonly attachments: readonly ParsedAttachment[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MAX_DEPTH = 20
|
|
50
|
+
|
|
51
|
+
interface Header { readonly name: string; readonly value: string }
|
|
52
|
+
|
|
53
|
+
interface Entity {
|
|
54
|
+
readonly headers: readonly Header[]
|
|
55
|
+
/** Body, still transfer-encoded, as latin1. */
|
|
56
|
+
readonly body: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function header(entity: Entity, name: string): string | undefined {
|
|
60
|
+
return entity.headers.find(h => h.name === name)?.value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function headers(entity: Entity, name: string): string[] {
|
|
64
|
+
return entity.headers.filter(h => h.name === name).map(h => h.value)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function splitEntity(raw: string): Entity {
|
|
68
|
+
let at = -1
|
|
69
|
+
let bodyAt = 0
|
|
70
|
+
if (raw.startsWith('\r\n')) { at = 0; bodyAt = 2 } else if (raw.startsWith('\n')) { at = 0; bodyAt = 1 } else {
|
|
71
|
+
const m = /\r?\n\r?\n/.exec(raw)
|
|
72
|
+
if (m !== null) { at = m.index; bodyAt = m.index + m[0].length }
|
|
73
|
+
}
|
|
74
|
+
const head = at < 0 ? raw : raw.slice(0, at)
|
|
75
|
+
const body = at < 0 ? '' : raw.slice(bodyAt)
|
|
76
|
+
const headers: Header[] = []
|
|
77
|
+
let current: { name: string; value: string } | null = null
|
|
78
|
+
for (const line of head.split(/\r?\n/)) {
|
|
79
|
+
if (/^[ \t]/.test(line)) {
|
|
80
|
+
if (current !== null) current.value += ` ${line.trim()}`
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
const colon = line.indexOf(':')
|
|
84
|
+
if (colon <= 0 || /[\s\x00-\x1f]/.test(line.slice(0, colon))) { current = null; continue }
|
|
85
|
+
current = { name: line.slice(0, colon).toLowerCase(), value: line.slice(colon + 1).trim() }
|
|
86
|
+
headers.push(current)
|
|
87
|
+
}
|
|
88
|
+
// A header with 8-bit bytes is UTF-8 when it is valid UTF-8, otherwise latin1 as read.
|
|
89
|
+
return { headers: headers.map(h => ({ name: h.name, value: decodeRawHeader(h.value) })), body }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function decodeRawHeader(value: string): string {
|
|
93
|
+
if (!/[^\x00-\x7f]/.test(value)) return value
|
|
94
|
+
try {
|
|
95
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(Buffer.from(value, 'latin1'))
|
|
96
|
+
} catch {
|
|
97
|
+
return value
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function decodeCharset(bytes: Buffer, charset: string | undefined): string {
|
|
102
|
+
const label = (charset ?? 'utf-8').trim().replace(/^"|"$/g, '').toLowerCase()
|
|
103
|
+
try {
|
|
104
|
+
return new TextDecoder(label === '' ? 'utf-8' : label).decode(bytes)
|
|
105
|
+
} catch {
|
|
106
|
+
return new TextDecoder('utf-8').decode(bytes)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---- RFC 2047 encoded words ----
|
|
111
|
+
|
|
112
|
+
function decodeQ(text: string): Buffer {
|
|
113
|
+
const bytes: number[] = []
|
|
114
|
+
for (let i = 0; i < text.length; i++) {
|
|
115
|
+
const c = text[i]!
|
|
116
|
+
if (c === '_') bytes.push(0x20)
|
|
117
|
+
else if (c === '=' && /^[0-9a-fA-F]{2}$/.test(text.slice(i + 1, i + 3))) { bytes.push(parseInt(text.slice(i + 1, i + 3), 16)); i += 2 } else bytes.push(c.charCodeAt(0) & 0xff)
|
|
118
|
+
}
|
|
119
|
+
return Buffer.from(bytes)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ENCODED_WORD = /=\?([^?\s]+)\?([bBqQ])\?([^?\s]*)\?=/g
|
|
123
|
+
|
|
124
|
+
/** Decodes RFC 2047 encoded words in `text`. Adjacent words are joined without the whitespace between them, and words in the same charset are decoded together so a multibyte character split across words survives. */
|
|
125
|
+
export function decodeWords(text: string): string {
|
|
126
|
+
if (!text.includes('=?')) return text
|
|
127
|
+
const out: string[] = []
|
|
128
|
+
let run: { charset: string; bytes: Buffer[] } | null = null
|
|
129
|
+
const flush = (): void => {
|
|
130
|
+
if (run !== null) out.push(decodeCharset(Buffer.concat(run.bytes), run.charset))
|
|
131
|
+
run = null
|
|
132
|
+
}
|
|
133
|
+
let last = 0
|
|
134
|
+
for (const m of text.matchAll(ENCODED_WORD)) {
|
|
135
|
+
const between = text.slice(last, m.index)
|
|
136
|
+
if (run !== null && between.trim() === '') {
|
|
137
|
+
// whitespace between adjacent encoded words is dropped
|
|
138
|
+
} else {
|
|
139
|
+
flush()
|
|
140
|
+
out.push(between)
|
|
141
|
+
}
|
|
142
|
+
const charset = m[1]!.split('*')[0]!.toLowerCase()
|
|
143
|
+
const bytes = m[2]!.toLowerCase() === 'b' ? Buffer.from(m[3]!, 'base64') : decodeQ(m[3]!)
|
|
144
|
+
if (run !== null && (run as { charset: string }).charset !== charset) flush()
|
|
145
|
+
if (run === null) run = { charset, bytes: [] }
|
|
146
|
+
;(run as { bytes: Buffer[] }).bytes.push(bytes)
|
|
147
|
+
last = m.index + m[0].length
|
|
148
|
+
}
|
|
149
|
+
flush()
|
|
150
|
+
out.push(text.slice(last))
|
|
151
|
+
return out.join('')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---- structured header values ----
|
|
155
|
+
|
|
156
|
+
function unquote(text: string): string {
|
|
157
|
+
const t = text.trim()
|
|
158
|
+
if (t.length >= 2 && t.startsWith('"') && t.endsWith('"')) return t.slice(1, -1).replace(/\\(.)/g, '$1')
|
|
159
|
+
return t
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface Structured { readonly value: string; readonly params: ReadonlyMap<string, string> }
|
|
163
|
+
|
|
164
|
+
function splitTop(text: string, separator: string): string[] {
|
|
165
|
+
const parts: string[] = []
|
|
166
|
+
let cur = ''
|
|
167
|
+
let quoted = false
|
|
168
|
+
for (let i = 0; i < text.length; i++) {
|
|
169
|
+
const c = text[i]!
|
|
170
|
+
if (quoted) {
|
|
171
|
+
cur += c
|
|
172
|
+
if (c === '\\' && i + 1 < text.length) cur += text[++i]!
|
|
173
|
+
else if (c === '"') quoted = false
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
if (c === '"') { quoted = true; cur += c; continue }
|
|
177
|
+
if (c === separator) { parts.push(cur); cur = ''; continue }
|
|
178
|
+
cur += c
|
|
179
|
+
}
|
|
180
|
+
parts.push(cur)
|
|
181
|
+
return parts
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function percentBytes(text: string): Buffer {
|
|
185
|
+
const bytes: number[] = []
|
|
186
|
+
for (let i = 0; i < text.length; i++) {
|
|
187
|
+
if (text[i] === '%' && /^[0-9a-fA-F]{2}$/.test(text.slice(i + 1, i + 3))) { bytes.push(parseInt(text.slice(i + 1, i + 3), 16)); i += 2 } else bytes.push(text.charCodeAt(i) & 0xff)
|
|
188
|
+
}
|
|
189
|
+
return Buffer.from(bytes)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Parses `value; name=param; name*=utf-8''enc; name*0*=...` and resolves RFC 2231 extended and continued parameters. */
|
|
193
|
+
function parseStructured(text: string | undefined): Structured {
|
|
194
|
+
if (text === undefined) return { value: '', params: new Map() }
|
|
195
|
+
const [first, ...rest] = splitTop(text, ';')
|
|
196
|
+
const plain = new Map<string, string>()
|
|
197
|
+
const groups = new Map<string, { n: number; ext: boolean; value: string }[]>()
|
|
198
|
+
for (const part of rest) {
|
|
199
|
+
const eq = part.indexOf('=')
|
|
200
|
+
if (eq <= 0) continue
|
|
201
|
+
const key = part.slice(0, eq).trim().toLowerCase()
|
|
202
|
+
const value = unquote(part.slice(eq + 1))
|
|
203
|
+
const m = /^([^*]+)(?:\*(\d+))?(\*)?$/.exec(key)
|
|
204
|
+
if (m === null || (m[2] === undefined && m[3] === undefined)) { plain.set(key, value); continue }
|
|
205
|
+
const list = groups.get(m[1]!) ?? []
|
|
206
|
+
list.push({ n: m[2] === undefined ? 0 : Number(m[2]), ext: m[3] !== undefined, value })
|
|
207
|
+
groups.set(m[1]!, list)
|
|
208
|
+
}
|
|
209
|
+
const params = new Map(plain)
|
|
210
|
+
for (const [name, list] of groups) {
|
|
211
|
+
list.sort((a, b) => a.n - b.n)
|
|
212
|
+
let charset: string | undefined
|
|
213
|
+
const chunks: Buffer[] = []
|
|
214
|
+
list.forEach((seg, i) => {
|
|
215
|
+
let v = seg.value
|
|
216
|
+
if (i === 0 && seg.ext) {
|
|
217
|
+
const m = /^([^']*)'[^']*'(.*)$/s.exec(v)
|
|
218
|
+
if (m !== null) { charset = m[1] === '' ? undefined : m[1]; v = m[2]! }
|
|
219
|
+
}
|
|
220
|
+
chunks.push(seg.ext ? percentBytes(v) : Buffer.from(v, 'latin1'))
|
|
221
|
+
})
|
|
222
|
+
params.set(name, decodeCharset(Buffer.concat(chunks), charset))
|
|
223
|
+
}
|
|
224
|
+
return { value: (first ?? '').trim(), params }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---- addresses ----
|
|
228
|
+
|
|
229
|
+
/** Parses an address list (RFC 5322 mailboxes, groups, quoted names, comments, encoded words). */
|
|
230
|
+
export function parseAddresses(text: string | undefined): ParsedAddress[] {
|
|
231
|
+
if (text === undefined || text.trim() === '') return []
|
|
232
|
+
const mailboxes: string[] = []
|
|
233
|
+
let cur = ''
|
|
234
|
+
let quoted = false
|
|
235
|
+
let angle = false
|
|
236
|
+
let comment = 0
|
|
237
|
+
for (let i = 0; i < text.length; i++) {
|
|
238
|
+
const c = text[i]!
|
|
239
|
+
if (quoted) {
|
|
240
|
+
cur += c
|
|
241
|
+
if (c === '\\' && i + 1 < text.length) cur += text[++i]!
|
|
242
|
+
else if (c === '"') quoted = false
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
if (comment > 0) {
|
|
246
|
+
cur += c
|
|
247
|
+
if (c === '\\' && i + 1 < text.length) cur += text[++i]!
|
|
248
|
+
else if (c === '(') comment++
|
|
249
|
+
else if (c === ')') comment--
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
if (c === '"') { quoted = true; cur += c; continue }
|
|
253
|
+
if (c === '(') { comment = 1; cur += c; continue }
|
|
254
|
+
if (c === '<') angle = true
|
|
255
|
+
else if (c === '>') angle = false
|
|
256
|
+
if (!angle && c === ':') { cur = ''; continue } // group name
|
|
257
|
+
if (!angle && (c === ',' || c === ';')) { mailboxes.push(cur); cur = ''; continue }
|
|
258
|
+
cur += c
|
|
259
|
+
}
|
|
260
|
+
mailboxes.push(cur)
|
|
261
|
+
|
|
262
|
+
const result: ParsedAddress[] = []
|
|
263
|
+
for (const box of mailboxes) {
|
|
264
|
+
const parsed = parseMailbox(box)
|
|
265
|
+
if (parsed !== null) result.push(parsed)
|
|
266
|
+
}
|
|
267
|
+
return result
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function stripComments(text: string): { text: string; comments: string[] } {
|
|
271
|
+
const comments: string[] = []
|
|
272
|
+
let out = ''
|
|
273
|
+
let depth = 0
|
|
274
|
+
let quoted = false
|
|
275
|
+
let current = ''
|
|
276
|
+
for (let i = 0; i < text.length; i++) {
|
|
277
|
+
const c = text[i]!
|
|
278
|
+
if (quoted) {
|
|
279
|
+
out += c
|
|
280
|
+
if (c === '\\' && i + 1 < text.length) out += text[++i]!
|
|
281
|
+
else if (c === '"') quoted = false
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
if (depth > 0) {
|
|
285
|
+
if (c === '\\' && i + 1 < text.length) { current += text[++i]!; continue }
|
|
286
|
+
if (c === '(') depth++
|
|
287
|
+
else if (c === ')') { depth--; if (depth === 0) { comments.push(current); current = ''; continue } }
|
|
288
|
+
current += c
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
291
|
+
if (c === '"') { quoted = true; out += c; continue }
|
|
292
|
+
if (c === '(') { depth = 1; continue }
|
|
293
|
+
out += c
|
|
294
|
+
}
|
|
295
|
+
return { text: out, comments }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function parseMailbox(box: string): ParsedAddress | null {
|
|
299
|
+
const { text, comments } = stripComments(box)
|
|
300
|
+
const t = text.trim()
|
|
301
|
+
if (t === '') return null
|
|
302
|
+
const lt = t.lastIndexOf('<')
|
|
303
|
+
const gt = t.lastIndexOf('>')
|
|
304
|
+
if (lt >= 0 && gt > lt) {
|
|
305
|
+
const address = t.slice(lt + 1, gt).trim().replace(/^.*:/, '')
|
|
306
|
+
const name = decodeWords(unquote(t.slice(0, lt)))
|
|
307
|
+
return address === '' ? null : { address, name }
|
|
308
|
+
}
|
|
309
|
+
const address = unquote(t)
|
|
310
|
+
if (!address.includes('@')) return null
|
|
311
|
+
return { address, name: comments.length > 0 ? decodeWords(comments[0]!.trim()) : '' }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function idsOf(text: string | undefined): string[] {
|
|
315
|
+
if (text === undefined) return []
|
|
316
|
+
return [...text.matchAll(/<([^<>\s]+)>/g)].map(m => m[1]!)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ---- bodies ----
|
|
320
|
+
|
|
321
|
+
function decodeTransfer(body: string, encoding: string | undefined): Buffer {
|
|
322
|
+
const e = (encoding ?? '7bit').trim().toLowerCase()
|
|
323
|
+
if (e === 'base64') return Buffer.from(body.replace(/[^A-Za-z0-9+/=_-]/g, ''), 'base64')
|
|
324
|
+
if (e === 'quoted-printable') {
|
|
325
|
+
const joined = body.replace(/[ \t]+(?=\r?\n)/g, '').replace(/=\r?\n/g, '')
|
|
326
|
+
const bytes: number[] = []
|
|
327
|
+
for (let i = 0; i < joined.length; i++) {
|
|
328
|
+
const c = joined[i]!
|
|
329
|
+
if (c === '=' && /^[0-9a-fA-F]{2}$/.test(joined.slice(i + 1, i + 3))) { bytes.push(parseInt(joined.slice(i + 1, i + 3), 16)); i += 2 } else bytes.push(joined.charCodeAt(i) & 0xff)
|
|
330
|
+
}
|
|
331
|
+
return Buffer.from(bytes)
|
|
332
|
+
}
|
|
333
|
+
return Buffer.from(body, 'latin1')
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function splitMultipart(body: string, boundary: string): string[] {
|
|
337
|
+
const delim = `--${boundary}`
|
|
338
|
+
const parts: string[] = []
|
|
339
|
+
let start = -1
|
|
340
|
+
let pos = 0
|
|
341
|
+
for (;;) {
|
|
342
|
+
const idx = body.indexOf(delim, pos)
|
|
343
|
+
if (idx < 0) break
|
|
344
|
+
pos = idx + 1
|
|
345
|
+
if (idx > 0 && body[idx - 1] !== '\n') continue
|
|
346
|
+
const after = idx + delim.length
|
|
347
|
+
const eol = body.indexOf('\n', after)
|
|
348
|
+
const rest = body.slice(after, eol < 0 ? body.length : eol)
|
|
349
|
+
const closing = rest.startsWith('--')
|
|
350
|
+
if (!closing && !/^[ \t]*\r?$/.test(rest)) continue
|
|
351
|
+
if (start >= 0) {
|
|
352
|
+
let end = idx
|
|
353
|
+
if (body[end - 1] === '\n') end--
|
|
354
|
+
if (body[end - 1] === '\r') end--
|
|
355
|
+
parts.push(body.slice(start, Math.max(start, end)))
|
|
356
|
+
start = -1
|
|
357
|
+
}
|
|
358
|
+
if (closing || eol < 0) return parts
|
|
359
|
+
start = eol + 1
|
|
360
|
+
pos = start
|
|
361
|
+
}
|
|
362
|
+
if (start >= 0) parts.push(body.slice(start)) // no closing boundary
|
|
363
|
+
return parts
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
interface Collected {
|
|
367
|
+
text: string | null
|
|
368
|
+
html: string | null
|
|
369
|
+
attachments: ParsedAttachment[]
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function walk(entity: Entity, depth: number, out: Collected): void {
|
|
373
|
+
const type = parseStructured(header(entity, 'content-type'))
|
|
374
|
+
const mediaType = (type.value === '' ? 'text/plain' : type.value).toLowerCase()
|
|
375
|
+
const disposition = parseStructured(header(entity, 'content-disposition'))
|
|
376
|
+
const disp = disposition.value.toLowerCase()
|
|
377
|
+
const contentId = idsOf(header(entity, 'content-id'))[0] ?? null
|
|
378
|
+
|
|
379
|
+
if (mediaType.startsWith('multipart/') && depth < MAX_DEPTH) {
|
|
380
|
+
const boundary = type.params.get('boundary')
|
|
381
|
+
if (boundary !== undefined && boundary !== '') {
|
|
382
|
+
for (const part of splitMultipart(entity.body, boundary)) walk(splitEntity(part), depth + 1, out)
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const rawName = disposition.params.get('filename') ?? type.params.get('name')
|
|
388
|
+
const filename = rawName === undefined ? null : decodeWords(rawName)
|
|
389
|
+
const isText = mediaType === 'text/plain' || mediaType === 'text/html'
|
|
390
|
+
const attached = disp === 'attachment' || filename !== null || !isText || mediaType.startsWith('multipart/')
|
|
391
|
+
|
|
392
|
+
const content = decodeTransfer(entity.body, header(entity, 'content-transfer-encoding'))
|
|
393
|
+
if (!attached) {
|
|
394
|
+
const decoded = decodeCharset(content, type.params.get('charset'))
|
|
395
|
+
if (mediaType === 'text/html') { if (out.html === null) out.html = decoded } else if (out.text === null) out.text = decoded
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
out.attachments.push({
|
|
399
|
+
filename,
|
|
400
|
+
mediaType: mediaType.startsWith('multipart/') ? 'application/octet-stream' : mediaType,
|
|
401
|
+
size: content.length,
|
|
402
|
+
contentId,
|
|
403
|
+
inline: disp === 'inline' || (disp === '' && contentId !== null),
|
|
404
|
+
content,
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const EMPTY: ParsedMessage = {
|
|
409
|
+
messageId: null, inReplyTo: null, threadIds: [], references: [], from: null, fromProblem: null, to: [], cc: [], replyTo: [],
|
|
410
|
+
subject: '', date: null, text: null, html: null, attachments: [],
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Parses a raw RFC 5322 message. Never throws; unreadable input yields as much as could be read. */
|
|
414
|
+
export function parseMime(raw: Buffer): ParsedMessage {
|
|
415
|
+
try {
|
|
416
|
+
const entity = splitEntity(raw.toString('latin1'))
|
|
417
|
+
const out: Collected = { text: null, html: null, attachments: [] }
|
|
418
|
+
walk(entity, 0, out)
|
|
419
|
+
const inReplyToIds = idsOf(header(entity, 'in-reply-to'))
|
|
420
|
+
const references = idsOf(header(entity, 'references'))
|
|
421
|
+
const threadIds = [...new Set([...inReplyToIds, ...references])]
|
|
422
|
+
const date = header(entity, 'date')
|
|
423
|
+
const parsedDate = date === undefined ? null : new Date(date)
|
|
424
|
+
const fromFields = headers(entity, 'from')
|
|
425
|
+
const fromMailboxes = fromFields.length === 1 ? parseAddresses(fromFields[0]) : []
|
|
426
|
+
const fromProblem = fromFields.length > 1 ? 'several-from-fields' as const
|
|
427
|
+
: fromMailboxes.length > 1 ? 'several-mailboxes' as const : null
|
|
428
|
+
return {
|
|
429
|
+
messageId: idsOf(header(entity, 'message-id'))[0] ?? null,
|
|
430
|
+
inReplyTo: inReplyToIds[0] ?? null,
|
|
431
|
+
threadIds,
|
|
432
|
+
references,
|
|
433
|
+
from: fromProblem === null ? fromMailboxes[0] ?? null : null,
|
|
434
|
+
fromProblem,
|
|
435
|
+
to: parseAddresses(header(entity, 'to')),
|
|
436
|
+
cc: parseAddresses(header(entity, 'cc')),
|
|
437
|
+
replyTo: parseAddresses(header(entity, 'reply-to')),
|
|
438
|
+
subject: decodeWords(header(entity, 'subject') ?? ''),
|
|
439
|
+
date: parsedDate === null || Number.isNaN(parsedDate.getTime()) ? null : parsedDate,
|
|
440
|
+
text: out.text,
|
|
441
|
+
html: out.html,
|
|
442
|
+
attachments: out.attachments,
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
return EMPTY
|
|
446
|
+
}
|
|
447
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createMail } from './mail.js'
|
|
2
|
+
import { ses, devStandIn } from './providers.js'
|
|
3
|
+
import { snsWebhook } from './sns.js'
|
|
4
|
+
import { createInbound, sesReceipt, inboundFolder } from './inbound.js'
|
|
5
|
+
|
|
6
|
+
/** Mail namespace — email through a job queue, SES v2 or a dev stand-in, bounce and complaint suppression, and receiving. */
|
|
7
|
+
export const mail = {
|
|
8
|
+
/** Creates the mail interface: `send(db, { to, template, data, key })` in the caller's transaction, and the `jobs` that deliver. */
|
|
9
|
+
create: createMail,
|
|
10
|
+
/** Amazon SES v2 provider, SigV4-signed without the AWS SDK. */
|
|
11
|
+
ses,
|
|
12
|
+
/** Development provider that writes each message to a file instead of sending; refused in production. */
|
|
13
|
+
devStandIn,
|
|
14
|
+
/** Inbound handler for SES bounce and complaint events delivered through SNS; suppresses the address. */
|
|
15
|
+
snsWebhook,
|
|
16
|
+
/** Receiving email: `inbound({ source, queue, onMessage, ... })` returns `{ webhook, jobs, scan, sweep, get }`; messages arrive parsed, threaded into conversations, attachments stored as tenant files. */
|
|
17
|
+
inbound: createInbound,
|
|
18
|
+
/** Inbound source: SES receipt rule storing raw messages in S3 and notifying a signed SNS topic. */
|
|
19
|
+
sesReceipt,
|
|
20
|
+
/** Inbound development source: `.eml` files dropped in a folder, received by `scan()`; refused in production. */
|
|
21
|
+
inboundFolder,
|
|
22
|
+
} as const
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { mkdir, rename, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { getBaseLogger } from '../../core/logging/index.js'
|
|
4
|
+
import { signV4, type AwsCredentials } from '../aws/sigv4.js'
|
|
5
|
+
import { awsCredentials } from '../aws/credentials.js'
|
|
6
|
+
|
|
7
|
+
/** One rendered message handed to a provider. */
|
|
8
|
+
export interface OutgoingMessage {
|
|
9
|
+
/** The message row id; unique per message. */
|
|
10
|
+
readonly messageId: string
|
|
11
|
+
readonly from: string
|
|
12
|
+
readonly to: string
|
|
13
|
+
readonly subject: string
|
|
14
|
+
readonly text: string
|
|
15
|
+
readonly html?: string
|
|
16
|
+
readonly configurationSet?: string
|
|
17
|
+
/** Extra headers, e.g. `In-Reply-To` and `References` for a reply. */
|
|
18
|
+
readonly headers?: Readonly<Record<string, string>>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Sends one rendered message. A throw means it was not accepted and may be retried. */
|
|
22
|
+
export interface MailProvider {
|
|
23
|
+
readonly name: string
|
|
24
|
+
send(message: OutgoingMessage): Promise<{ providerMessageId: string }>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SesConfig {
|
|
28
|
+
readonly region: string
|
|
29
|
+
/** Called for every send. Default: `awsCredentials()` (environment, ECS task role, EC2 instance role). */
|
|
30
|
+
readonly credentials?: () => Promise<AwsCredentials>
|
|
31
|
+
readonly configurationSet?: string
|
|
32
|
+
/** Base URL instead of `https://email.<region>.amazonaws.com`; for tests against a local server. */
|
|
33
|
+
readonly endpoint?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Amazon SES v2 provider. Posts to `/v2/email/outbound-emails`, signed with SigV4
|
|
38
|
+
* (no AWS SDK). Credentials default to `awsCredentials()`.
|
|
39
|
+
* A non-2xx answer throws with the status and AWS's message.
|
|
40
|
+
*/
|
|
41
|
+
export function ses(config: SesConfig): MailProvider {
|
|
42
|
+
const getCredentials = config.credentials ?? awsCredentials()
|
|
43
|
+
const base = (config.endpoint ?? `https://email.${config.region}.amazonaws.com`).replace(/\/+$/, '')
|
|
44
|
+
return {
|
|
45
|
+
name: 'ses',
|
|
46
|
+
async send(message) {
|
|
47
|
+
const set = message.configurationSet ?? config.configurationSet
|
|
48
|
+
const headerList = Object.entries(message.headers ?? {}).map(([Name, Value]) => ({ Name, Value }))
|
|
49
|
+
const body = JSON.stringify({
|
|
50
|
+
FromEmailAddress: message.from,
|
|
51
|
+
Destination: { ToAddresses: [message.to] },
|
|
52
|
+
Content: {
|
|
53
|
+
Simple: {
|
|
54
|
+
Subject: { Data: message.subject, Charset: 'UTF-8' },
|
|
55
|
+
...(headerList.length > 0 ? { Headers: headerList } : {}),
|
|
56
|
+
Body: {
|
|
57
|
+
Text: { Data: message.text, Charset: 'UTF-8' },
|
|
58
|
+
...(message.html !== undefined ? { Html: { Data: message.html, Charset: 'UTF-8' } } : {}),
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
...(set !== undefined ? { ConfigurationSetName: set } : {}),
|
|
63
|
+
})
|
|
64
|
+
const url = `${base}/v2/email/outbound-emails`
|
|
65
|
+
const headers = signV4({
|
|
66
|
+
method: 'POST', url, headers: { 'content-type': 'application/json' }, body,
|
|
67
|
+
region: config.region, service: 'ses', credentials: await getCredentials(),
|
|
68
|
+
})
|
|
69
|
+
const res = await fetch(url, { method: 'POST', headers, body })
|
|
70
|
+
const raw = await res.text()
|
|
71
|
+
let parsed: Record<string, unknown> = {}
|
|
72
|
+
try { parsed = JSON.parse(raw) as Record<string, unknown> } catch { /* not JSON */ }
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
const detail = String(parsed['message'] ?? parsed['Message'] ?? raw.slice(0, 200))
|
|
75
|
+
throw new Error(`[pipework] SES refused the message: HTTP ${res.status} ${res.headers.get('x-amzn-errortype') ?? ''} ${detail}`.replace(/ +/g, ' '))
|
|
76
|
+
}
|
|
77
|
+
const id = parsed['MessageId']
|
|
78
|
+
if (typeof id !== 'string') throw new Error('[pipework] SES answered without a MessageId.')
|
|
79
|
+
return { providerMessageId: id }
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Development stand-in: writes each rendered message to `<dir>/<messageId>.json`
|
|
86
|
+
* (from, to, subject, text, html, and headers when there are any) and logs one line, sending nothing.
|
|
87
|
+
* The file appears only once it is complete. `mail.create` refuses it when the environment is production.
|
|
88
|
+
*/
|
|
89
|
+
export function devStandIn(config: { readonly dir: string }): MailProvider {
|
|
90
|
+
return {
|
|
91
|
+
name: 'dev-stand-in',
|
|
92
|
+
async send(message) {
|
|
93
|
+
await mkdir(config.dir, { recursive: true })
|
|
94
|
+
const file = join(config.dir, `${message.messageId}.json`)
|
|
95
|
+
const { from, to, subject, text, html, headers } = message
|
|
96
|
+
const tmp = join(config.dir, `.${message.messageId}.json.tmp`)
|
|
97
|
+
try {
|
|
98
|
+
await writeFile(tmp, JSON.stringify({
|
|
99
|
+
from, to, subject, text, ...(html !== undefined ? { html } : {}),
|
|
100
|
+
...(headers !== undefined && Object.keys(headers).length > 0 ? { headers } : {}),
|
|
101
|
+
}, null, 2))
|
|
102
|
+
await rename(tmp, file)
|
|
103
|
+
} catch (err) {
|
|
104
|
+
await rm(tmp, { force: true }).catch(() => {})
|
|
105
|
+
throw err
|
|
106
|
+
}
|
|
107
|
+
getBaseLogger().info({ event: 'email dev stand-in', to, file }, 'email written to file instead of sent')
|
|
108
|
+
return { providerMessageId: `dev-${message.messageId}` }
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// The tables behind `mail`. Enabled with `email: {}` in the blueprint;
|
|
2
|
+
// `pipework generate` then emits them into the default database's migrations,
|
|
3
|
+
// IF NOT EXISTS. Not tenant-scoped: a bounce notification arrives with no
|
|
4
|
+
// tenant, and a suppressed address is suppressed for every tenant. The same holds
|
|
5
|
+
// for `pipework_email_unsubscribes`: the one-click POST arrives from a mail
|
|
6
|
+
// provider with no tenant, and an opt-out names an address and a list, not a tenant.
|
|
7
|
+
|
|
8
|
+
import { define } from '../../data/domain/index.js'
|
|
9
|
+
import { field } from '../../data/domain/field.js'
|
|
10
|
+
import { schema } from '../../data/schema/namespace.js'
|
|
11
|
+
import { index, unique } from '../../data/db/idx/index.js'
|
|
12
|
+
|
|
13
|
+
export const EMAIL_TABLE_NAMES: ReadonlySet<string> = new Set([
|
|
14
|
+
'pipework_email_messages', 'pipework_email_suppressions', 'pipework_email_inbound', 'pipework_email_unsubscribes',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
export const EMAIL_STATUSES = ['queued', 'sending', 'sent', 'suppressed', 'unsubscribed', 'failed'] as const
|
|
18
|
+
export const INBOUND_STATUSES = ['received', 'processed', 'failed'] as const
|
|
19
|
+
export const INBOUND_SOURCES = ['ses', 'folder'] as const
|
|
20
|
+
export const SUPPRESSION_REASONS = ['bounce', 'complaint'] as const
|
|
21
|
+
|
|
22
|
+
export const emailMessages = define('pipework_email_messages', {
|
|
23
|
+
id: field.uuid().defaultRandom().primaryKey(),
|
|
24
|
+
key: field.text(),
|
|
25
|
+
toAddress: field.text(),
|
|
26
|
+
template: field.text(),
|
|
27
|
+
data: field.jsonb(schema.check.json()),
|
|
28
|
+
status: field.enum(EMAIL_STATUSES).default('queued'),
|
|
29
|
+
providerMessageId: field.text().nullable(),
|
|
30
|
+
error: field.text().nullable(),
|
|
31
|
+
traceId: field.uuid().nullable(),
|
|
32
|
+
createdAt: field.timestamp().default('now'),
|
|
33
|
+
sentAt: field.timestamp().nullable(),
|
|
34
|
+
sendingAt: field.timestamp().nullable(),
|
|
35
|
+
failedAt: field.timestamp().nullable(),
|
|
36
|
+
conversationId: field.uuid().nullable(),
|
|
37
|
+
inReplyToInbound: field.uuid().nullable(),
|
|
38
|
+
list: field.text().nullable(),
|
|
39
|
+
}, {
|
|
40
|
+
indexes: (t) => ({
|
|
41
|
+
byKey: unique('pipework_email_messages_key_key').on(t['key']),
|
|
42
|
+
byProviderId: index('idx_pipework_email_messages_provider_id').on(t['providerMessageId']),
|
|
43
|
+
byConversation: index('idx_pipework_email_messages_conversation').on(t['conversationId']),
|
|
44
|
+
}),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export const emailSuppressions = define('pipework_email_suppressions', {
|
|
48
|
+
address: field.text().primaryKey(),
|
|
49
|
+
reason: field.enum(SUPPRESSION_REASONS),
|
|
50
|
+
detail: field.jsonb(schema.check.json()).nullable(),
|
|
51
|
+
createdAt: field.timestamp().default('now'),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// One row per (address, list) that opted out of a list's mail. Address lowercased.
|
|
55
|
+
export const emailUnsubscribes = define('pipework_email_unsubscribes', {
|
|
56
|
+
address: field.text(),
|
|
57
|
+
list: field.text(),
|
|
58
|
+
createdAt: field.timestamp().default('now'),
|
|
59
|
+
}, {
|
|
60
|
+
primaryKey: ['address', 'list'],
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// One row per received message. Bodies are not stored here: the raw message is
|
|
64
|
+
// the copy (in the receipt bucket or the stand-in's `received/` folder) and is
|
|
65
|
+
// deleted after `raw_expires_at`. Not tenant-scoped: the row exists before a
|
|
66
|
+
// tenant is known; `tenant_id` is set once the app's resolver names one.
|
|
67
|
+
export const emailInbound = define('pipework_email_inbound', {
|
|
68
|
+
id: field.uuid().defaultRandom().primaryKey(),
|
|
69
|
+
sourceId: field.text(),
|
|
70
|
+
source: field.enum(INBOUND_SOURCES),
|
|
71
|
+
rawLocation: field.jsonb(schema.check.json()),
|
|
72
|
+
rawExpiresAt: field.timestamp(),
|
|
73
|
+
rawDeletedAt: field.timestamp().nullable(),
|
|
74
|
+
status: field.enum(INBOUND_STATUSES).default('received'),
|
|
75
|
+
error: field.text().nullable(),
|
|
76
|
+
messageIdHeader: field.text().nullable(),
|
|
77
|
+
inReplyTo: field.text().nullable(),
|
|
78
|
+
references: field.jsonb(schema.check.json()).nullable(),
|
|
79
|
+
fromAddress: field.text().nullable(),
|
|
80
|
+
subject: field.text().nullable(),
|
|
81
|
+
conversationId: field.uuid().nullable(),
|
|
82
|
+
tenantId: field.uuid().nullable(),
|
|
83
|
+
verdicts: field.jsonb(schema.check.json()).nullable(),
|
|
84
|
+
recipients: field.jsonb(schema.check.json()).nullable(),
|
|
85
|
+
attachmentFiles: field.jsonb(schema.check.json()).nullable(),
|
|
86
|
+
receivedAt: field.timestamp().default('now'),
|
|
87
|
+
processedAt: field.timestamp().nullable(),
|
|
88
|
+
failedAt: field.timestamp().nullable(),
|
|
89
|
+
traceId: field.uuid().nullable(),
|
|
90
|
+
}, {
|
|
91
|
+
indexes: (t) => ({
|
|
92
|
+
bySource: unique('pipework_email_inbound_source_id_key').on(t['sourceId']),
|
|
93
|
+
byMessageId: index('idx_pipework_email_inbound_message_id').on(t['messageIdHeader']),
|
|
94
|
+
byExpiry: index('idx_pipework_email_inbound_raw_expires').on(t['rawExpiresAt']),
|
|
95
|
+
}),
|
|
96
|
+
})
|