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.
Files changed (690) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/package.json +4 -3
  3. package/src/async/jobs/abortable-delay.ts +18 -0
  4. package/src/async/jobs/claims.ts +52 -0
  5. package/src/async/jobs/cron.ts +179 -0
  6. package/src/async/jobs/errors.ts +56 -0
  7. package/src/async/jobs/exclusion.ts +117 -0
  8. package/src/async/jobs/execute.ts +219 -0
  9. package/src/async/jobs/index.ts +10 -0
  10. package/src/async/jobs/listener.ts +135 -0
  11. package/src/async/jobs/namespace.ts +20 -0
  12. package/src/async/jobs/queue-door.ts +44 -0
  13. package/src/async/jobs/queue.ts +1012 -0
  14. package/src/async/jobs/run-script.ts +67 -0
  15. package/src/async/jobs/scheduler.ts +218 -0
  16. package/src/async/jobs/schedules-table.ts +37 -0
  17. package/src/async/jobs/serialize-error.ts +48 -0
  18. package/src/async/jobs/table.ts +191 -0
  19. package/src/async/pipeline/define.ts +53 -0
  20. package/src/async/pipeline/errors.ts +35 -0
  21. package/src/async/pipeline/execute.ts +53 -0
  22. package/src/async/pipeline/index.ts +13 -0
  23. package/src/async/pipeline/namespace.ts +12 -0
  24. package/src/async/pipeline/resume.ts +67 -0
  25. package/src/async/pipeline/run-steps.ts +98 -0
  26. package/src/async/pipeline/types.ts +57 -0
  27. package/src/async/state-machine/define.ts +64 -0
  28. package/src/async/state-machine/errors.ts +29 -0
  29. package/src/async/state-machine/index.ts +3 -0
  30. package/src/async/state-machine/namespace.ts +12 -0
  31. package/src/async/state-machine/transition.ts +90 -0
  32. package/src/async/state-machine/types.ts +37 -0
  33. package/src/async/steps/errors.ts +31 -0
  34. package/src/async/steps/graph.ts +1214 -0
  35. package/src/async/steps/hash.ts +40 -0
  36. package/src/async/steps/index.ts +7 -0
  37. package/src/async/steps/namespace.ts +7 -0
  38. package/src/async/steps/table.ts +97 -0
  39. package/src/async/steps/types.ts +318 -0
  40. package/src/audit/chain/index.ts +9 -0
  41. package/src/audit/chain/install.ts +113 -0
  42. package/src/audit/chain/retire.ts +52 -0
  43. package/src/audit/chain/verify.ts +257 -0
  44. package/src/audit/query/cursor.ts +37 -0
  45. package/src/audit/query/index.ts +9 -0
  46. package/src/audit/query/query.ts +223 -0
  47. package/src/audit/schema/audit-record-fields.ts +68 -0
  48. package/src/audit/schema/audit-record.ts +23 -0
  49. package/src/auth/auth/audit.ts +31 -0
  50. package/src/auth/auth/chain.ts +84 -0
  51. package/src/auth/auth/cookie.ts +136 -0
  52. package/src/auth/auth/email-links.ts +311 -0
  53. package/src/auth/auth/guard.ts +128 -0
  54. package/src/auth/auth/index.ts +18 -0
  55. package/src/auth/auth/multi-org.ts +337 -0
  56. package/src/auth/auth/namespace.ts +34 -0
  57. package/src/auth/auth/parse-expiry.ts +13 -0
  58. package/src/auth/auth/passwords.ts +110 -0
  59. package/src/auth/auth/schema.ts +110 -0
  60. package/src/auth/auth/sessions.ts +565 -0
  61. package/src/auth/auth/sign-in.ts +156 -0
  62. package/src/auth/auth/signing-keys.ts +69 -0
  63. package/src/auth/auth/types.ts +26 -0
  64. package/src/auth/auth/uuid.ts +10 -0
  65. package/src/auth/rbac/cache.ts +55 -0
  66. package/src/auth/rbac/check.ts +30 -0
  67. package/src/auth/rbac/create.ts +35 -0
  68. package/src/auth/rbac/index.ts +5 -0
  69. package/src/auth/rbac/middleware.ts +20 -0
  70. package/src/auth/rbac/namespace.ts +15 -0
  71. package/src/auth/rbac/resolve.ts +31 -0
  72. package/src/auth/rbac/schema.sql +31 -0
  73. package/src/auth/rbac/seed.ts +67 -0
  74. package/src/auth/rbac/tables.ts +32 -0
  75. package/src/auth/rbac/types.ts +65 -0
  76. package/src/auth/tenant/extract.ts +33 -0
  77. package/src/auth/tenant/in-tenant.ts +49 -0
  78. package/src/auth/tenant/index.ts +7 -0
  79. package/src/auth/tenant/namespace.ts +23 -0
  80. package/src/auth/tenant/propagate.ts +20 -0
  81. package/src/auth/tenant/rls.ts +120 -0
  82. package/src/auth/tenant/scope.ts +33 -0
  83. package/src/auth/tenant/scoped-db.ts +581 -0
  84. package/src/auth/tenant/types.ts +14 -0
  85. package/src/auth/tenant/validate.ts +27 -0
  86. package/src/cli/commands/add.ts +158 -0
  87. package/src/cli/commands/alerts.ts +98 -0
  88. package/src/cli/commands/check.ts +1418 -0
  89. package/src/cli/commands/codemod.ts +78 -0
  90. package/src/cli/commands/db.ts +356 -0
  91. package/src/cli/commands/dev.ts +101 -0
  92. package/src/cli/commands/generate.ts +75 -0
  93. package/src/cli/commands/init.ts +36 -0
  94. package/src/cli/commands/install.ts +28 -0
  95. package/src/cli/commands/lint.ts +62 -0
  96. package/src/cli/commands/migrate.ts +132 -0
  97. package/src/cli/commands/openapi.ts +63 -0
  98. package/src/cli/commands/remove.ts +72 -0
  99. package/src/cli/commands/run.ts +50 -0
  100. package/src/cli/commands/scope.ts +54 -0
  101. package/src/cli/commands/serve.ts +68 -0
  102. package/src/cli/commands/shard.ts +31 -0
  103. package/src/cli/commands/shared/config-pipework-version.ts +28 -0
  104. package/src/cli/commands/shared/find-config.ts +22 -0
  105. package/src/cli/commands/shared/load-config.ts +35 -0
  106. package/src/cli/commands/shared/pnpm-install.ts +95 -0
  107. package/src/cli/commands/shared/report-removed.ts +8 -0
  108. package/src/cli/commands/sync.ts +32 -0
  109. package/src/cli/commands/test.ts +307 -0
  110. package/src/cli/commands/upgrade.ts +48 -0
  111. package/src/cli/dev-bootstrap.ts +24 -0
  112. package/src/cli/errors.ts +10 -0
  113. package/src/cli/index.ts +303 -0
  114. package/src/cli/output.ts +17 -0
  115. package/src/cli/port.ts +15 -0
  116. package/src/cli/surface-kinds.ts +20 -0
  117. package/src/client/index.ts +4 -0
  118. package/src/client/poll.ts +130 -0
  119. package/src/client/report-fault.ts +119 -0
  120. package/src/core/config/app-pool.ts +19 -0
  121. package/src/core/config/aws-env.ts +29 -0
  122. package/src/core/config/base-dir.ts +24 -0
  123. package/src/core/config/discover.ts +64 -0
  124. package/src/core/config/env-resolve.ts +87 -0
  125. package/src/core/config/env-types.ts +116 -0
  126. package/src/core/config/env.ts +55 -0
  127. package/src/core/config/errors.ts +9 -0
  128. package/src/core/config/import-config.ts +40 -0
  129. package/src/core/config/index.ts +8 -0
  130. package/src/core/config/load.ts +417 -0
  131. package/src/core/config/namespace.ts +14 -0
  132. package/src/core/config/resolve.ts +195 -0
  133. package/src/core/config/schema.ts +488 -0
  134. package/src/core/config/statement-observer.ts +48 -0
  135. package/src/core/config/ts-inline-hooks.ts +75 -0
  136. package/src/core/config/ts-register.ts +4 -0
  137. package/src/core/config/ts-resolve-hooks.ts +101 -0
  138. package/src/core/errors/base.ts +49 -0
  139. package/src/core/errors/http.ts +146 -0
  140. package/src/core/errors/index.ts +2 -0
  141. package/src/core/errors/namespace.ts +31 -0
  142. package/src/core/lifecycle/drain.ts +33 -0
  143. package/src/core/lifecycle/health.ts +72 -0
  144. package/src/core/lifecycle/index.ts +4 -0
  145. package/src/core/lifecycle/namespace.ts +7 -0
  146. package/src/core/lifecycle/startup.ts +102 -0
  147. package/src/core/lifecycle/types.ts +36 -0
  148. package/src/core/logging/context.ts +60 -0
  149. package/src/core/logging/error-chain.ts +68 -0
  150. package/src/core/logging/index.ts +5 -0
  151. package/src/core/logging/logger.ts +65 -0
  152. package/src/core/logging/namespace.ts +16 -0
  153. package/src/core/logging/proxy.ts +22 -0
  154. package/src/core/logging/redact-match.ts +24 -0
  155. package/src/core/logging/types.ts +47 -0
  156. package/src/core/pipework.ts +566 -0
  157. package/src/core/runtime/broker.ts +69 -0
  158. package/src/core/surface/http.ts +133 -0
  159. package/src/core/surface/index.ts +11 -0
  160. package/src/core/surface/job-timeout.ts +40 -0
  161. package/src/core/surface/namespace.ts +13 -0
  162. package/src/core/surface/script.ts +33 -0
  163. package/src/core/surface/start.ts +142 -0
  164. package/src/core/surface/types.ts +225 -0
  165. package/src/core/surface/validate.ts +88 -0
  166. package/src/core/surface/worker.ts +528 -0
  167. package/src/core/validation/connectivity.ts +49 -0
  168. package/src/core/validation/domain.ts +99 -0
  169. package/src/core/validation/index.ts +5 -0
  170. package/src/core/validation/queue-tables.ts +312 -0
  171. package/src/core/validation/rls-role.ts +138 -0
  172. package/src/core/validation/startup.ts +51 -0
  173. package/src/core/validation/surfaces.ts +65 -0
  174. package/src/core/validation/types.ts +25 -0
  175. package/src/data/db/advisory-lock.ts +55 -0
  176. package/src/data/db/aggregates.ts +172 -0
  177. package/src/data/db/array-elements.ts +111 -0
  178. package/src/data/db/bulk-set.ts +53 -0
  179. package/src/data/db/col/index.ts +14 -0
  180. package/src/data/db/composed.ts +53 -0
  181. package/src/data/db/db.ts +276 -0
  182. package/src/data/db/excluded.ts +16 -0
  183. package/src/data/db/expressions.ts +473 -0
  184. package/src/data/db/filter.ts +90 -0
  185. package/src/data/db/function-source.ts +114 -0
  186. package/src/data/db/identifiers.ts +16 -0
  187. package/src/data/db/idx/index.ts +9 -0
  188. package/src/data/db/in-list.ts +49 -0
  189. package/src/data/db/index.ts +9 -0
  190. package/src/data/db/insert-split.ts +137 -0
  191. package/src/data/db/instrumentation.ts +82 -0
  192. package/src/data/db/maintenance.ts +28 -0
  193. package/src/data/db/namespace.ts +107 -0
  194. package/src/data/db/owner-labels.ts +9 -0
  195. package/src/data/db/pool.ts +187 -0
  196. package/src/data/db/query-label.ts +13 -0
  197. package/src/data/db/query-watch.ts +116 -0
  198. package/src/data/db/raw-client-warning.ts +37 -0
  199. package/src/data/db/readonly.ts +23 -0
  200. package/src/data/db/reservation.ts +151 -0
  201. package/src/data/db/schema-features.ts +14 -0
  202. package/src/data/db/serializable.ts +83 -0
  203. package/src/data/db/sql.ts +1 -0
  204. package/src/data/db/timeouts.ts +76 -0
  205. package/src/data/db/types.ts +18 -0
  206. package/src/data/db/unbound.ts +33 -0
  207. package/src/data/db/values.ts +29 -0
  208. package/src/data/domain/brand.ts +15 -0
  209. package/src/data/domain/build-factory.ts +193 -0
  210. package/src/data/domain/build-schema.ts +161 -0
  211. package/src/data/domain/build-table.ts +355 -0
  212. package/src/data/domain/define.ts +239 -0
  213. package/src/data/domain/external-registry.ts +34 -0
  214. package/src/data/domain/external.ts +77 -0
  215. package/src/data/domain/field.ts +279 -0
  216. package/src/data/domain/index.ts +13 -0
  217. package/src/data/domain/naming.ts +6 -0
  218. package/src/data/domain/projections.ts +57 -0
  219. package/src/data/domain/trace-safe-projection.ts +31 -0
  220. package/src/data/domain/traits.ts +108 -0
  221. package/src/data/domain/types.ts +289 -0
  222. package/src/data/jsonb/composed.ts +18 -0
  223. package/src/data/jsonb/index.ts +1 -0
  224. package/src/data/jsonb/namespace.ts +30 -0
  225. package/src/data/jsonb/query.ts +86 -0
  226. package/src/data/migrate/append-only-generate.ts +47 -0
  227. package/src/data/migrate/applied.ts +47 -0
  228. package/src/data/migrate/apply.ts +309 -0
  229. package/src/data/migrate/check.ts +119 -0
  230. package/src/data/migrate/diff.ts +357 -0
  231. package/src/data/migrate/door-generate.ts +130 -0
  232. package/src/data/migrate/framework-rls.ts +58 -0
  233. package/src/data/migrate/generate.ts +201 -0
  234. package/src/data/migrate/history.ts +215 -0
  235. package/src/data/migrate/index.ts +9 -0
  236. package/src/data/migrate/internal-definitions.ts +175 -0
  237. package/src/data/migrate/post-process.ts +73 -0
  238. package/src/data/migrate/pragma.ts +73 -0
  239. package/src/data/migrate/provision-framework.ts +147 -0
  240. package/src/data/migrate/rls-generate.ts +61 -0
  241. package/src/data/migrate/run.ts +124 -0
  242. package/src/data/migrate/safety.ts +167 -0
  243. package/src/data/migrate/snapshot.ts +368 -0
  244. package/src/data/migrate/sql-emit.ts +392 -0
  245. package/src/data/migrate/statements.ts +312 -0
  246. package/src/data/query/alias.ts +126 -0
  247. package/src/data/query/batch.ts +8 -0
  248. package/src/data/query/cache/core/cache.ts +78 -0
  249. package/src/data/query/cache/core/index.ts +1 -0
  250. package/src/data/query/cache/core/types.ts +29 -0
  251. package/src/data/query/cache/readme.md +230 -0
  252. package/src/data/query/casing.ts +76 -0
  253. package/src/data/query/collation.ts +11 -0
  254. package/src/data/query/column-builder.ts +370 -0
  255. package/src/data/query/column.ts +150 -0
  256. package/src/data/query/entity.ts +42 -0
  257. package/src/data/query/errors.ts +33 -0
  258. package/src/data/query/index.ts +14 -0
  259. package/src/data/query/logger.ts +47 -0
  260. package/src/data/query/operations.ts +45 -0
  261. package/src/data/query/pg-core/alias.ts +12 -0
  262. package/src/data/query/pg-core/checks.ts +32 -0
  263. package/src/data/query/pg-core/columns/all.ts +71 -0
  264. package/src/data/query/pg-core/columns/bigint.ts +110 -0
  265. package/src/data/query/pg-core/columns/bigserial.ts +128 -0
  266. package/src/data/query/pg-core/columns/boolean.ts +43 -0
  267. package/src/data/query/pg-core/columns/char.ts +85 -0
  268. package/src/data/query/pg-core/columns/cidr.ts +43 -0
  269. package/src/data/query/pg-core/columns/common.ts +366 -0
  270. package/src/data/query/pg-core/columns/custom.ts +232 -0
  271. package/src/data/query/pg-core/columns/date.common.ts +15 -0
  272. package/src/data/query/pg-core/columns/date.ts +112 -0
  273. package/src/data/query/pg-core/columns/double-precision.ts +55 -0
  274. package/src/data/query/pg-core/columns/enum.ts +201 -0
  275. package/src/data/query/pg-core/columns/index.ts +35 -0
  276. package/src/data/query/pg-core/columns/inet.ts +43 -0
  277. package/src/data/query/pg-core/columns/int.common.ts +57 -0
  278. package/src/data/query/pg-core/columns/integer.ts +53 -0
  279. package/src/data/query/pg-core/columns/interval.ts +83 -0
  280. package/src/data/query/pg-core/columns/json.ts +58 -0
  281. package/src/data/query/pg-core/columns/jsonb.ts +56 -0
  282. package/src/data/query/pg-core/columns/line.ts +117 -0
  283. package/src/data/query/pg-core/columns/macaddr.ts +43 -0
  284. package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
  285. package/src/data/query/pg-core/columns/numeric.ts +227 -0
  286. package/src/data/query/pg-core/columns/point.ts +127 -0
  287. package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
  288. package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
  289. package/src/data/query/pg-core/columns/real.ts +58 -0
  290. package/src/data/query/pg-core/columns/serial.ts +55 -0
  291. package/src/data/query/pg-core/columns/smallint.ts +53 -0
  292. package/src/data/query/pg-core/columns/smallserial.ts +60 -0
  293. package/src/data/query/pg-core/columns/text.ts +67 -0
  294. package/src/data/query/pg-core/columns/time.ts +72 -0
  295. package/src/data/query/pg-core/columns/timestamp.ts +163 -0
  296. package/src/data/query/pg-core/columns/uuid.ts +51 -0
  297. package/src/data/query/pg-core/columns/varchar.ts +89 -0
  298. package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
  299. package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
  300. package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
  301. package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
  302. package/src/data/query/pg-core/db.ts +674 -0
  303. package/src/data/query/pg-core/dialect.ts +1404 -0
  304. package/src/data/query/pg-core/expressions.ts +25 -0
  305. package/src/data/query/pg-core/foreign-keys.ts +130 -0
  306. package/src/data/query/pg-core/index.ts +21 -0
  307. package/src/data/query/pg-core/indexes.ts +276 -0
  308. package/src/data/query/pg-core/policies.ts +56 -0
  309. package/src/data/query/pg-core/primary-keys.ts +62 -0
  310. package/src/data/query/pg-core/query-builders/count.ts +86 -0
  311. package/src/data/query/pg-core/query-builders/delete.ts +293 -0
  312. package/src/data/query/pg-core/query-builders/index.ts +7 -0
  313. package/src/data/query/pg-core/query-builders/insert.ts +452 -0
  314. package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
  315. package/src/data/query/pg-core/query-builders/query.ts +157 -0
  316. package/src/data/query/pg-core/query-builders/raw.ts +49 -0
  317. package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
  318. package/src/data/query/pg-core/query-builders/select.ts +1346 -0
  319. package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
  320. package/src/data/query/pg-core/query-builders/update.ts +626 -0
  321. package/src/data/query/pg-core/roles.ts +41 -0
  322. package/src/data/query/pg-core/schema.ts +72 -0
  323. package/src/data/query/pg-core/sequence.ts +41 -0
  324. package/src/data/query/pg-core/session.ts +292 -0
  325. package/src/data/query/pg-core/subquery.ts +46 -0
  326. package/src/data/query/pg-core/table.ts +252 -0
  327. package/src/data/query/pg-core/unique-constraint.ts +73 -0
  328. package/src/data/query/pg-core/utils/array.ts +95 -0
  329. package/src/data/query/pg-core/utils/index.ts +1 -0
  330. package/src/data/query/pg-core/utils.ts +104 -0
  331. package/src/data/query/pg-core/view-base.ts +14 -0
  332. package/src/data/query/pg-core/view-common.ts +1 -0
  333. package/src/data/query/pg-core/view.ts +435 -0
  334. package/src/data/query/pg-core/with-builder.ts +41 -0
  335. package/src/data/query/postgres-js/README.md +46 -0
  336. package/src/data/query/postgres-js/array-parser.ts +45 -0
  337. package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
  338. package/src/data/query/postgres-js/driver.ts +163 -0
  339. package/src/data/query/postgres-js/index.ts +2 -0
  340. package/src/data/query/postgres-js/session.ts +234 -0
  341. package/src/data/query/primary-key.ts +11 -0
  342. package/src/data/query/query-builders/query-builder.ts +19 -0
  343. package/src/data/query/query-builders/select.types.ts +175 -0
  344. package/src/data/query/query-promise.ts +35 -0
  345. package/src/data/query/relations.ts +725 -0
  346. package/src/data/query/runnable-query.ts +12 -0
  347. package/src/data/query/selection-proxy.ts +121 -0
  348. package/src/data/query/session.ts +8 -0
  349. package/src/data/query/sql/expressions/conditions.ts +741 -0
  350. package/src/data/query/sql/expressions/index.ts +2 -0
  351. package/src/data/query/sql/expressions/select.ts +45 -0
  352. package/src/data/query/sql/functions/aggregate.ts +129 -0
  353. package/src/data/query/sql/functions/index.ts +2 -0
  354. package/src/data/query/sql/functions/vector.ts +178 -0
  355. package/src/data/query/sql/index.ts +3 -0
  356. package/src/data/query/sql/sql.ts +794 -0
  357. package/src/data/query/subquery.ts +53 -0
  358. package/src/data/query/table.ts +208 -0
  359. package/src/data/query/table.utils.ts +2 -0
  360. package/src/data/query/tracing-utils.ts +3 -0
  361. package/src/data/query/tracing.ts +11 -0
  362. package/src/data/query/utils.ts +357 -0
  363. package/src/data/query/version.ts +4 -0
  364. package/src/data/query/view-common.ts +1 -0
  365. package/src/data/schema/branded.ts +13 -0
  366. package/src/data/schema/generated/coerce.ts +5 -0
  367. package/src/data/schema/generated/combinator.ts +6 -0
  368. package/src/data/schema/generated/encoding.ts +5 -0
  369. package/src/data/schema/generated/error.ts +6 -0
  370. package/src/data/schema/generated/format.ts +5 -0
  371. package/src/data/schema/generated/type-constructor.ts +10 -0
  372. package/src/data/schema/generated/type.ts +6 -0
  373. package/src/data/schema/index.ts +6 -0
  374. package/src/data/schema/json-schema.ts +33 -0
  375. package/src/data/schema/namespace.ts +55 -0
  376. package/src/data/schema/parse.ts +39 -0
  377. package/src/data/schema/types.ts +39 -0
  378. package/src/data/temporal/append-only.ts +134 -0
  379. package/src/data/temporal/close.ts +27 -0
  380. package/src/data/temporal/columns.ts +10 -0
  381. package/src/data/temporal/definition-queries.ts +70 -0
  382. package/src/data/temporal/errors.ts +10 -0
  383. package/src/data/temporal/get-current.ts +22 -0
  384. package/src/data/temporal/index.ts +9 -0
  385. package/src/data/temporal/namespace.ts +36 -0
  386. package/src/data/temporal/query.ts +25 -0
  387. package/src/data/temporal/resolve.ts +17 -0
  388. package/src/data/temporal/revise.ts +67 -0
  389. package/src/data/temporal/types.ts +22 -0
  390. package/src/data/valve/artifact.ts +33 -0
  391. package/src/data/valve/contract-diff.ts +21 -0
  392. package/src/data/valve/cross.ts +57 -0
  393. package/src/data/valve/crossing-ledger.ts +50 -0
  394. package/src/data/valve/decision.ts +44 -0
  395. package/src/data/valve/durable-crossing.ts +52 -0
  396. package/src/data/valve/execute-crossing.ts +77 -0
  397. package/src/data/valve/faucet.ts +56 -0
  398. package/src/data/valve/gate.ts +114 -0
  399. package/src/data/valve/graph.ts +77 -0
  400. package/src/data/valve/index.ts +53 -0
  401. package/src/data/valve/namespace.ts +57 -0
  402. package/src/data/valve/registry.ts +50 -0
  403. package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
  404. package/src/data/valve/schema/valve-crossing.ts +9 -0
  405. package/src/data/valve/supply.ts +154 -0
  406. package/src/data/valve/types.ts +143 -0
  407. package/src/data/vector/columns.ts +27 -0
  408. package/src/data/vector/index.ts +4 -0
  409. package/src/data/vector/namespace.ts +33 -0
  410. package/src/data/vector/query.ts +85 -0
  411. package/src/data/vector/validate.ts +16 -0
  412. package/src/data/write/audit/actor.ts +15 -0
  413. package/src/data/write/audit/audit-columns.ts +18 -0
  414. package/src/data/write/audit/audit-select.ts +111 -0
  415. package/src/data/write/audit/bound-transaction.ts +14 -0
  416. package/src/data/write/audit/cte-compile.ts +32 -0
  417. package/src/data/write/audit/decode-returning.ts +47 -0
  418. package/src/data/write/audit/final-select.ts +43 -0
  419. package/src/data/write/audit/infra-audit.ts +36 -0
  420. package/src/data/write/audit/primary-key.ts +65 -0
  421. package/src/data/write/audit/projection.ts +41 -0
  422. package/src/data/write/audit/read-touches.ts +86 -0
  423. package/src/data/write/audit/touch-match.ts +78 -0
  424. package/src/data/write/audit/verb-delete.ts +62 -0
  425. package/src/data/write/audit/verb-insert.ts +36 -0
  426. package/src/data/write/audit/verb-select.ts +47 -0
  427. package/src/data/write/audit/verb-update.ts +60 -0
  428. package/src/data/write/audit/verb-upsert.ts +104 -0
  429. package/src/data/write/conflict.ts +43 -0
  430. package/src/data/write/delete.ts +38 -0
  431. package/src/data/write/execute.ts +243 -0
  432. package/src/data/write/index.ts +11 -0
  433. package/src/data/write/insert.ts +63 -0
  434. package/src/data/write/state.ts +43 -0
  435. package/src/data/write/update.ts +54 -0
  436. package/src/data/write/upsert.ts +58 -0
  437. package/src/data/write/values.ts +19 -0
  438. package/src/index.ts +230 -0
  439. package/src/infra/access-links/code.ts +70 -0
  440. package/src/infra/access-links/handle.ts +892 -0
  441. package/src/infra/access-links/index.ts +9 -0
  442. package/src/infra/access-links/namespace.ts +30 -0
  443. package/src/infra/access-links/person-check.ts +177 -0
  444. package/src/infra/access-links/schema.ts +71 -0
  445. package/src/infra/alerts/async-sources.ts +231 -0
  446. package/src/infra/alerts/audit.ts +36 -0
  447. package/src/infra/alerts/beat.ts +123 -0
  448. package/src/infra/alerts/client-faults.ts +111 -0
  449. package/src/infra/alerts/faults.ts +156 -0
  450. package/src/infra/alerts/handle.ts +778 -0
  451. package/src/infra/alerts/http.ts +192 -0
  452. package/src/infra/alerts/index.ts +12 -0
  453. package/src/infra/alerts/infra-sources.ts +180 -0
  454. package/src/infra/alerts/namespace.ts +81 -0
  455. package/src/infra/alerts/redact.ts +21 -0
  456. package/src/infra/alerts/request-faults.ts +41 -0
  457. package/src/infra/alerts/schema.ts +124 -0
  458. package/src/infra/alerts/sinks.ts +125 -0
  459. package/src/infra/alerts/source-options.ts +26 -0
  460. package/src/infra/alerts/sources.ts +503 -0
  461. package/src/infra/alerts/subsystem-sources.ts +137 -0
  462. package/src/infra/audit/emit.ts +87 -0
  463. package/src/infra/audit/index.ts +2 -0
  464. package/src/infra/audit/namespace.ts +22 -0
  465. package/src/infra/audit/schema.sql +35 -0
  466. package/src/infra/audit/types.ts +18 -0
  467. package/src/infra/aws/credentials.ts +147 -0
  468. package/src/infra/aws/sigv4.ts +140 -0
  469. package/src/infra/cache/create.ts +58 -0
  470. package/src/infra/cache/entry.ts +81 -0
  471. package/src/infra/cache/index.ts +4 -0
  472. package/src/infra/cache/namespace.ts +10 -0
  473. package/src/infra/cache/tenant.ts +93 -0
  474. package/src/infra/cache/types.ts +24 -0
  475. package/src/infra/crawl/audit.ts +35 -0
  476. package/src/infra/crawl/errors.ts +25 -0
  477. package/src/infra/crawl/handle.ts +693 -0
  478. package/src/infra/crawl/index.ts +9 -0
  479. package/src/infra/crawl/links.ts +160 -0
  480. package/src/infra/crawl/namespace.ts +36 -0
  481. package/src/infra/crawl/robots.ts +100 -0
  482. package/src/infra/crawl/schema.ts +89 -0
  483. package/src/infra/credit-line/handle.ts +883 -0
  484. package/src/infra/credit-line/index.ts +6 -0
  485. package/src/infra/credit-line/namespace.ts +7 -0
  486. package/src/infra/credit-line/schema.ts +96 -0
  487. package/src/infra/credits/handle.ts +706 -0
  488. package/src/infra/credits/index.ts +5 -0
  489. package/src/infra/credits/namespace.ts +7 -0
  490. package/src/infra/credits/schema.ts +83 -0
  491. package/src/infra/email/audit.ts +36 -0
  492. package/src/infra/email/inbound.ts +622 -0
  493. package/src/infra/email/index.ts +11 -0
  494. package/src/infra/email/mail.ts +517 -0
  495. package/src/infra/email/mime.ts +447 -0
  496. package/src/infra/email/namespace.ts +22 -0
  497. package/src/infra/email/providers.ts +111 -0
  498. package/src/infra/email/schema.ts +96 -0
  499. package/src/infra/email/sns.ts +200 -0
  500. package/src/infra/email/unsubscribe.ts +44 -0
  501. package/src/infra/fetch/address.ts +88 -0
  502. package/src/infra/fetch/connect-retry.ts +87 -0
  503. package/src/infra/fetch/handle.ts +329 -0
  504. package/src/infra/fetch/index.ts +7 -0
  505. package/src/infra/fetch/namespace.ts +70 -0
  506. package/src/infra/idempotency/index.ts +3 -0
  507. package/src/infra/idempotency/middleware.ts +87 -0
  508. package/src/infra/idempotency/store.ts +92 -0
  509. package/src/infra/idempotency/types.ts +30 -0
  510. package/src/infra/join-requests/handle.ts +312 -0
  511. package/src/infra/join-requests/index.ts +2 -0
  512. package/src/infra/join-requests/namespace.ts +7 -0
  513. package/src/infra/join-requests/schema.ts +33 -0
  514. package/src/infra/outbox/core.ts +165 -0
  515. package/src/infra/quota/errors.ts +32 -0
  516. package/src/infra/quota/handle.ts +215 -0
  517. package/src/infra/quota/index.ts +5 -0
  518. package/src/infra/quota/namespace.ts +7 -0
  519. package/src/infra/quota/schema.ts +15 -0
  520. package/src/infra/storage/backend.ts +34 -0
  521. package/src/infra/storage/handle.ts +513 -0
  522. package/src/infra/storage/index.ts +9 -0
  523. package/src/infra/storage/local.ts +104 -0
  524. package/src/infra/storage/namespace.ts +13 -0
  525. package/src/infra/storage/s3.ts +257 -0
  526. package/src/infra/storage/schema.ts +24 -0
  527. package/src/infra/support/handle.ts +854 -0
  528. package/src/infra/support/http.ts +229 -0
  529. package/src/infra/support/index.ts +7 -0
  530. package/src/infra/support/namespace.ts +12 -0
  531. package/src/infra/support/schema.ts +96 -0
  532. package/src/infra/webhook/inbound.ts +45 -0
  533. package/src/infra/webhook/index.ts +5 -0
  534. package/src/infra/webhook/namespace.ts +24 -0
  535. package/src/infra/webhook/outbound.ts +315 -0
  536. package/src/infra/webhook/sign.ts +32 -0
  537. package/src/infra/webhook/verify.ts +82 -0
  538. package/src/invariants/assert.ts +26 -0
  539. package/src/invariants/index.ts +1 -0
  540. package/src/lint/config.ts +56 -0
  541. package/src/lint/doctrine.ts +163 -0
  542. package/src/lint/include.ts +97 -0
  543. package/src/lint/index.ts +3 -0
  544. package/src/lint/preset.ts +10 -0
  545. package/src/lint/resolver.ts +218 -0
  546. package/src/lint/rules/no-reexports.ts +46 -0
  547. package/src/request/behavior/audited.ts +85 -0
  548. package/src/request/behavior/cached.ts +62 -0
  549. package/src/request/behavior/compose.ts +28 -0
  550. package/src/request/behavior/index.ts +5 -0
  551. package/src/request/behavior/namespace.ts +16 -0
  552. package/src/request/behavior/types.ts +26 -0
  553. package/src/request/behavior/versioned.ts +54 -0
  554. package/src/request/context/create.ts +103 -0
  555. package/src/request/context/guard.ts +49 -0
  556. package/src/request/context/index.ts +4 -0
  557. package/src/request/context/run-in-transaction.ts +200 -0
  558. package/src/request/context/store.ts +28 -0
  559. package/src/request/context/types.ts +50 -0
  560. package/src/request/di/builder.ts +535 -0
  561. package/src/request/di/index.ts +8 -0
  562. package/src/request/di/resolve.ts +271 -0
  563. package/src/request/di/types.ts +262 -0
  564. package/src/request/fitting/namespace.ts +7 -0
  565. package/src/request/fixture/batch.ts +71 -0
  566. package/src/request/fixture/builder.ts +26 -0
  567. package/src/request/fixture/errors.ts +10 -0
  568. package/src/request/fixture/index.ts +8 -0
  569. package/src/request/fixture/namespace.ts +23 -0
  570. package/src/request/fixture/paginate.ts +44 -0
  571. package/src/request/fixture/to-handlers.ts +260 -0
  572. package/src/request/fixture/types.ts +79 -0
  573. package/src/request/flow/act-as.ts +78 -0
  574. package/src/request/flow/namespace.ts +17 -0
  575. package/src/request/http/error-step.ts +42 -0
  576. package/src/request/http/file.ts +141 -0
  577. package/src/request/http/index.ts +4 -0
  578. package/src/request/http/middleware.ts +141 -0
  579. package/src/request/http/namespace.ts +5 -0
  580. package/src/request/http/rate-limit-classes.ts +170 -0
  581. package/src/request/http/response.ts +82 -0
  582. package/src/request/http/server.ts +916 -0
  583. package/src/request/http/sniff.ts +261 -0
  584. package/src/request/http/types.ts +250 -0
  585. package/src/request/http/upload.ts +284 -0
  586. package/src/request/internal/text.ts +6 -0
  587. package/src/request/openapi/generate.ts +205 -0
  588. package/src/request/openapi/index.ts +2 -0
  589. package/src/request/openapi/namespace.ts +7 -0
  590. package/src/test/auth.ts +13 -0
  591. package/src/test/auto-setup.ts +45 -0
  592. package/src/test/context.ts +91 -0
  593. package/src/test/crawl-fixture.ts +157 -0
  594. package/src/test/discover-fork.ts +17 -0
  595. package/src/test/fingerprint.ts +135 -0
  596. package/src/test/globals.d.ts +7 -0
  597. package/src/test/index.ts +19 -0
  598. package/src/test/namespace.ts +87 -0
  599. package/src/test/plugin.ts +189 -0
  600. package/src/test/progress.ts +185 -0
  601. package/src/test/reap-after-run.ts +28 -0
  602. package/src/test/reap.ts +286 -0
  603. package/src/test/reset.ts +253 -0
  604. package/src/test/savepoints.ts +172 -0
  605. package/src/test/setup.ts +1422 -0
  606. package/src/test/system-channel.ts +50 -0
  607. package/src/test/vitest-plugin.ts +142 -0
  608. package/src/test/vitest-workspace.ts +507 -0
  609. package/src/test/vitest.ts +333 -0
  610. package/src/test/watch.ts +105 -0
  611. package/src/trace/capture/bounded-serializer.ts +146 -0
  612. package/src/trace/cardinality/analyze.ts +173 -0
  613. package/src/trace/cardinality/branch-context.ts +214 -0
  614. package/src/trace/cardinality/check.ts +33 -0
  615. package/src/trace/cardinality/classify-edge.ts +64 -0
  616. package/src/trace/cardinality/structural-max.ts +107 -0
  617. package/src/trace/check/coverage-check.ts +188 -0
  618. package/src/trace/check/flow-check.ts +103 -0
  619. package/src/trace/check/flow-step-site.ts +234 -0
  620. package/src/trace/check/io-pattern.ts +116 -0
  621. package/src/trace/codemod/add-cardinality.ts +152 -0
  622. package/src/trace/codemod/run.ts +80 -0
  623. package/src/trace/entry/child-trace.ts +21 -0
  624. package/src/trace/entry/close-trace.ts +87 -0
  625. package/src/trace/entry/finalize-trace.ts +28 -0
  626. package/src/trace/entry/open-trace.ts +138 -0
  627. package/src/trace/entry/parent-trace.ts +20 -0
  628. package/src/trace/entry/parse-traceparent.ts +39 -0
  629. package/src/trace/entry/process-start.ts +38 -0
  630. package/src/trace/entry/trace-context.ts +26 -0
  631. package/src/trace/entry/tracing-enabled.ts +10 -0
  632. package/src/trace/external/record.ts +195 -0
  633. package/src/trace/external/report.ts +66 -0
  634. package/src/trace/namespace.ts +19 -0
  635. package/src/trace/partition/day.ts +47 -0
  636. package/src/trace/partition/index.ts +14 -0
  637. package/src/trace/partition/partition-name.ts +23 -0
  638. package/src/trace/partition/plan.ts +82 -0
  639. package/src/trace/partition/schedule.ts +26 -0
  640. package/src/trace/partition/startup-hook.ts +39 -0
  641. package/src/trace/partition/sync.ts +154 -0
  642. package/src/trace/partition/tables.ts +53 -0
  643. package/src/trace/read/tree.ts +83 -0
  644. package/src/trace/retain/copy-on-error.ts +76 -0
  645. package/src/trace/runtime/expire-stale-traces.ts +54 -0
  646. package/src/trace/runtime/flow-step.ts +147 -0
  647. package/src/trace/runtime/flush-steps.ts +31 -0
  648. package/src/trace/runtime/pending-writes.ts +54 -0
  649. package/src/trace/runtime/step-buffer.ts +298 -0
  650. package/src/trace/runtime/step-error.ts +21 -0
  651. package/src/trace/runtime/step-meta.ts +55 -0
  652. package/src/trace/runtime/thenable.ts +12 -0
  653. package/src/trace/runtime/truncate-trace.ts +62 -0
  654. package/src/trace/schema/trace-fields.ts +52 -0
  655. package/src/trace/schema/trace-retained.ts +22 -0
  656. package/src/trace/schema/trace-step-fields.ts +44 -0
  657. package/src/trace/schema/trace-step-retained.ts +25 -0
  658. package/src/trace/schema/trace-step.ts +18 -0
  659. package/src/trace/schema/trace.ts +12 -0
  660. package/src/trace/transformer/apply.ts +79 -0
  661. package/src/trace/transformer/build-meta-literal.ts +30 -0
  662. package/src/trace/transformer/find-flow-step-sites.ts +85 -0
  663. package/src/trace/transformer/inject-flow-step-import.ts +39 -0
  664. package/src/trace/transformer/transform-error.ts +14 -0
  665. package/src/trace/transformer/transform-flow-steps.ts +123 -0
  666. package/src/trace/transformer/wrap-function-declaration.ts +62 -0
  667. package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
  668. package/src/workspace/affected.ts +110 -0
  669. package/src/workspace/changed-files.ts +163 -0
  670. package/src/workspace/check-deps.ts +213 -0
  671. package/src/workspace/checksum.ts +20 -0
  672. package/src/workspace/dependency-graph.ts +67 -0
  673. package/src/workspace/detect-pipework-copies.ts +93 -0
  674. package/src/workspace/discover.ts +206 -0
  675. package/src/workspace/generate.ts +401 -0
  676. package/src/workspace/index.ts +7 -0
  677. package/src/workspace/module-imports-pipework.ts +39 -0
  678. package/src/workspace/module-package-name.ts +12 -0
  679. package/src/workspace/scrub-git-env.ts +23 -0
  680. package/src/workspace/source-paths.ts +36 -0
  681. package/src/workspace/sync.ts +182 -0
  682. package/dist/REFERENCE.md +0 -1477
  683. package/dist/infra/access-links/rate-limit.d.ts +0 -10
  684. package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
  685. package/dist/infra/access-links/rate-limit.js +0 -28
  686. package/dist/infra/access-links/rate-limit.js.map +0 -1
  687. package/dist/infra/email/sigv4.d.ts +0 -24
  688. package/dist/infra/email/sigv4.d.ts.map +0 -1
  689. package/dist/infra/email/sigv4.js +0 -55
  690. package/dist/infra/email/sigv4.js.map +0 -1
@@ -0,0 +1,622 @@
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import { copyFile, mkdir, readdir, readFile, stat, unlink } from 'node:fs/promises'
3
+ import { basename, join } from 'node:path'
4
+ import { Readable } from 'node:stream'
5
+ import { sql } from '../../data/db/sql.js'
6
+ import { pipeSystem } from '../../data/db/db.js'
7
+ import { schema } from '../../data/schema/namespace.js'
8
+ import { fitting } from '../../request/di/index.js'
9
+ import { getContext } from '../../request/context/store.js'
10
+ import { getBaseLogger } from '../../core/logging/index.js'
11
+ import { recordFault, faultMessage } from '../alerts/faults.js'
12
+ import type { Queue } from '../../async/jobs/queue.js'
13
+ import type { Handler } from '../../request/di/types.js'
14
+ import { __flowStep } from '../../trace/runtime/flow-step.js'
15
+ import type { StorageBackend } from '../storage/backend.js'
16
+ import type { Storage, StoredFile } from '../storage/handle.js'
17
+ import { auditEmail } from './audit.js'
18
+ import { extensionOf, resolveMediaType, SNIFF_BYTES } from '../../request/http/sniff.js'
19
+ import { parseMime, type ParsedAddress } from './mime.js'
20
+ import { snsHandler } from './sns.js'
21
+ import { describeError } from '../../core/logging/error-chain.js'
22
+
23
+ export const RECEIVE_JOB = 'pipework.email.receive'
24
+ const DEFAULT_RETENTION_DAYS = 30
25
+ const DEFAULT_MAX_RAW_BYTES = 41_943_040
26
+ const DEFAULT_SWEEP_LIMIT = 500
27
+
28
+ export interface EmailAddress {
29
+ readonly address: string
30
+ readonly name: string
31
+ }
32
+
33
+ export interface ReceivedAttachment {
34
+ readonly filename: string | null
35
+ readonly mediaType: string
36
+ readonly size: number
37
+ /** Lowercase hex SHA-256 of the decoded content. */
38
+ readonly sha256: string
39
+ /** The `Content-ID` without angle brackets, for `cid:` references from the HTML. */
40
+ readonly contentId: string | null
41
+ readonly inline: boolean
42
+ /** The stored file, or `null` when no tenant was resolved or no `storage` was given. */
43
+ readonly file: StoredFile | null
44
+ }
45
+
46
+ /** Why a message that had a From field names no author: several From fields, or several mailboxes in one. */
47
+ export type FromProblem = 'several-from-fields' | 'several-mailboxes'
48
+
49
+ /** What SES concluded about a received message: each of `PASS`, `FAIL`, `GRAY` or `PROCESSING_FAILED`. */
50
+ export interface EmailVerdicts {
51
+ readonly spam: string | null
52
+ readonly virus: string | null
53
+ readonly spf: string | null
54
+ readonly dkim: string | null
55
+ readonly dmarc: string | null
56
+ }
57
+
58
+ /** What is known about a message before it is threaded: everything but its conversation and tenant. Handed to `resolveTenant`. */
59
+ export type UnthreadedEmail = Omit<ReceivedEmail, 'conversationId' | 'isNewConversation' | 'tenantId'>
60
+
61
+ /** A received, parsed and threaded message, as handed to `onMessage`. */
62
+ export interface ReceivedEmail {
63
+ /** The `pipework_email_inbound` row id; pass it as `inReplyTo` to `mail.send` to answer. */
64
+ readonly id: string
65
+ readonly conversationId: string
66
+ /** True when no earlier received or sent message matched this one's `In-Reply-To`/`References`. */
67
+ readonly isNewConversation: boolean
68
+ readonly tenantId: string | null
69
+ /** The `Message-ID` without angle brackets, or `null`. */
70
+ readonly messageId: string | null
71
+ readonly inReplyTo: string | null
72
+ readonly references: readonly string[]
73
+ /** An empty address and name when there is no readable From, or when the author is ambiguous (see `fromProblem`). */
74
+ readonly from: EmailAddress
75
+ /**
76
+ * Why `from` is empty for a message that had a From: several From fields, or several mailboxes in one.
77
+ * RFC 7489 section 6.6.1: verifiers differ in which one they judge, so no author is named. Null otherwise.
78
+ */
79
+ readonly fromProblem: FromProblem | null
80
+ readonly to: readonly EmailAddress[]
81
+ readonly cc: readonly EmailAddress[]
82
+ readonly replyTo: readonly EmailAddress[]
83
+ readonly subject: string
84
+ readonly date: Date | null
85
+ readonly text: string | null
86
+ readonly html: string | null
87
+ readonly attachments: readonly ReceivedAttachment[]
88
+ /**
89
+ * `null` for a message from the folder stand-in. DMARC vouches for `from` only when `fromProblem` is null
90
+ * and `from.address` is non-empty; a PASS on a message with an ambiguous From says nothing about any one author.
91
+ */
92
+ readonly verdicts: EmailVerdicts | null
93
+ /** The envelope recipients SES accepted the message for; for the stand-in, the To and Cc addresses. */
94
+ readonly recipients: readonly string[]
95
+ }
96
+
97
+ /** A received message's row, without bodies. */
98
+ export interface InboundRecord {
99
+ readonly id: string
100
+ readonly source: 'ses' | 'folder'
101
+ readonly status: 'received' | 'processed' | 'failed'
102
+ readonly error: string | null
103
+ readonly messageId: string | null
104
+ readonly conversationId: string | null
105
+ readonly tenantId: string | null
106
+ readonly subject: string | null
107
+ readonly fromAddress: string | null
108
+ readonly verdicts: EmailVerdicts | null
109
+ readonly receivedAt: Date
110
+ readonly processedAt: Date | null
111
+ readonly rawExpiresAt: Date
112
+ readonly rawDeletedAt: Date | null
113
+ }
114
+
115
+ export interface InboundSweepResult {
116
+ /** Raw copies deleted. */
117
+ readonly deleted: number
118
+ /** Raw copies that could not be deleted (left for the next sweep). */
119
+ readonly failed: number
120
+ }
121
+
122
+ export interface InboundScanResult {
123
+ /** Files taken in. */
124
+ readonly received: number
125
+ /** Files already taken in earlier (same bytes); moved to `received/` without a new row. */
126
+ readonly duplicates: number
127
+ }
128
+
129
+ type RawLocation = Record<string, unknown>
130
+
131
+ /** Where received mail comes from. Made by `mail.sesReceipt` or `mail.inboundFolder`. */
132
+ export interface InboundSource {
133
+ readonly kind: 'ses' | 'folder'
134
+ /** Reads the raw message; throws `RawTooLargeError` past `maxBytes` and `RawNotFoundError` when it definitely does not exist. */
135
+ read(location: RawLocation, maxBytes: number): Promise<Buffer>
136
+ remove(location: RawLocation): Promise<void>
137
+ readonly ses?: SesReceiptConfig
138
+ readonly folder?: { readonly dir: string }
139
+ }
140
+
141
+ export class RawNotFoundError extends Error {
142
+ constructor(detail: string) {
143
+ super(`raw message not found: ${detail}`)
144
+ }
145
+ }
146
+
147
+ export class RawTooLargeError extends Error {
148
+ constructor(readonly maxBytes: number) {
149
+ super(`raw message is larger than ${maxBytes} bytes`)
150
+ }
151
+ }
152
+
153
+ export interface SesReceiptConfig {
154
+ /** A storage backend over the SES receipt bucket with no prefix (`storage.s3({ bucket, region, credentials })`); keys are the S3 action's object keys. */
155
+ readonly bucket: StorageBackend
156
+ /** The SNS topics SES publishes receipt notifications to. */
157
+ readonly topicArns: readonly string[]
158
+ readonly region?: string
159
+ readonly fetchCert?: (url: string) => Promise<string>
160
+ readonly confirm?: (url: string) => Promise<void>
161
+ }
162
+
163
+ async function collect(stream: AsyncIterable<Buffer | Uint8Array>, maxBytes: number): Promise<Buffer> {
164
+ const parts: Buffer[] = []
165
+ let size = 0
166
+ for await (const chunk of stream) {
167
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
168
+ size += buf.length
169
+ if (size > maxBytes) {
170
+ if (stream instanceof Readable) stream.destroy()
171
+ throw new RawTooLargeError(maxBytes)
172
+ }
173
+ parts.push(buf)
174
+ }
175
+ return Buffer.concat(parts)
176
+ }
177
+
178
+ /**
179
+ * The SES receipt source. SES stores each raw message in S3 (receipt rule action "S3" with an SNS topic)
180
+ * and notifies the topic; `inbound.webhook` verifies the notification and records it, and the receive job
181
+ * reads the object from `bucket`. A notification whose action is not S3 (for example the SNS action,
182
+ * which carries the message inline), or whose type is not `Received`, is answered 200 and ignored.
183
+ */
184
+ export function sesReceipt(config: SesReceiptConfig): InboundSource {
185
+ if (config.topicArns.length === 0) throw new Error('[pipework] mail.sesReceipt needs at least one topic ARN in topicArns.')
186
+ return {
187
+ kind: 'ses',
188
+ ses: config,
189
+ async read(location, maxBytes) {
190
+ let stream: Awaited<ReturnType<StorageBackend['read']>>
191
+ try {
192
+ stream = await config.bucket.read(String(location['key']))
193
+ } catch (err) {
194
+ if (/\bNoSuchKey\b/.test(describeError(err).message)) throw new RawNotFoundError(String(location['key']))
195
+ throw err
196
+ }
197
+ return collect(stream, maxBytes)
198
+ },
199
+ async remove(location) {
200
+ await config.bucket.remove(String(location['key']))
201
+ },
202
+ }
203
+ }
204
+
205
+ /**
206
+ * The development source: `.eml` files dropped in `dir` are received by `inbound.scan()`, which copies each
207
+ * to `<dir>/received/` (where the raw copy stays until `sweep`) and removes the original. Refused when the
208
+ * environment is `production`.
209
+ */
210
+ export function inboundFolder(config: { readonly dir: string }): InboundSource {
211
+ return {
212
+ kind: 'folder',
213
+ folder: config,
214
+ async read(location, maxBytes) {
215
+ const path = String(location['path'])
216
+ try {
217
+ if ((await stat(path)).size > maxBytes) throw new RawTooLargeError(maxBytes)
218
+ return await readFile(path)
219
+ } catch (err) {
220
+ if ((err as { code?: string }).code === 'ENOENT') throw new RawNotFoundError(path)
221
+ throw err
222
+ }
223
+ },
224
+ async remove(location) {
225
+ try { await unlink(String(location['path'])) } catch (err) { if ((err as { code?: string }).code !== 'ENOENT') throw err }
226
+ },
227
+ }
228
+ }
229
+
230
+ export interface InboundConfig {
231
+ readonly source: InboundSource
232
+ /** The app's job queue; its worker surface must run `inbound.jobs`. */
233
+ readonly queue: Queue
234
+ /**
235
+ * Called in the receive job with each parsed, threaded message. A throw makes the queue retry the job.
236
+ * It is called at least once per message: a retry after a handler error, or a job re-run after a crash,
237
+ * calls it again with the same `id`. Make it idempotent on `id`.
238
+ */
239
+ readonly onMessage: (email: ReceivedEmail) => Promise<void> | void
240
+ /** Stores attachments (as the resolved tenant's files). Without it, or without a tenant, attachments have `file: null`. */
241
+ readonly storage?: Storage
242
+ /**
243
+ * Names the tenant a message belongs to (`null` for none), e.g. by its recipient address. It runs before
244
+ * threading, so it gets the message without a conversation: a message is only joined to a conversation of
245
+ * the same tenant (or of a message with no tenant).
246
+ */
247
+ readonly resolveTenant?: (email: UnthreadedEmail) => Promise<string | null>
248
+ /** Days the raw copy is kept before `sweep` deletes it. Default 30. */
249
+ readonly rawRetentionDays?: number
250
+ /** Days each stored attachment is kept, from receipt, before the storage sweep deletes it. Unset: no expiry. */
251
+ readonly attachmentRetentionDays?: number
252
+ /** Raw messages larger than this are marked `failed` and not handled. Default 40 MiB. */
253
+ readonly maxRawBytes?: number
254
+ /** The app's environment; `inboundFolder` is refused when this is `production`. */
255
+ readonly environment?: string
256
+ }
257
+
258
+ export interface Inbound {
259
+ /** The public POST endpoint SNS delivers SES receipt notifications to (SES source only). */
260
+ webhook(config: { readonly path: string }): Handler<Record<string, unknown>, unknown>
261
+ /** Job handlers for the receive job; spread into the worker surface's `jobs`. */
262
+ readonly jobs: readonly Handler<Record<string, unknown>, unknown>[]
263
+ /** Folder source only: receives every `.eml` file in the folder. Needs a Flow (a job or schedule run). */
264
+ scan(): Promise<InboundScanResult>
265
+ /** Deletes raw copies past their retention. Needs a Flow (a job or schedule run). */
266
+ sweep(options?: { readonly limit?: number }): Promise<InboundSweepResult>
267
+ /** The row of a received message, or `null`. Needs a Flow (a job, request or test context). */
268
+ get(id: string): Promise<InboundRecord | null>
269
+ }
270
+
271
+ function toDate(value: unknown): Date {
272
+ if (value instanceof Date) return value
273
+ const text = String(value)
274
+ return new Date(/(Z|[+-]\d\d(:?\d\d)?)$/.test(text) ? text : `${text.replace(' ', 'T')}Z`)
275
+ }
276
+
277
+ /** A Postgres text-array literal, sent as one parameter and cast with `::text[]`. */
278
+ function textArray(values: readonly string[]): string {
279
+ return `{${values.map(v => `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`).join(',')}}`
280
+ }
281
+
282
+ function verdictsOf(receipt: Record<string, unknown>): EmailVerdicts {
283
+ const status = (key: string): string | null => {
284
+ const v = receipt[key] as { status?: unknown } | undefined
285
+ return typeof v?.status === 'string' ? v.status : null
286
+ }
287
+ return {
288
+ spam: status('spamVerdict'), virus: status('virusVerdict'), spf: status('spfVerdict'),
289
+ dkim: status('dkimVerdict'), dmarc: status('dmarcVerdict'),
290
+ }
291
+ }
292
+
293
+ function addressOf(a: ParsedAddress | null): EmailAddress {
294
+ return a === null ? { address: '', name: '' } : { address: a.address, name: a.name }
295
+ }
296
+
297
+ /**
298
+ * Receiving email. `inbound.webhook` (SES) or `inbound.scan` (folder) records each message in
299
+ * `pipework_email_inbound` and enqueues the `pipework.email.receive` job, once per message. The job
300
+ * reads the raw message, parses it, assigns a conversation, resolves the tenant, stores the attachments
301
+ * as that tenant's files and calls `onMessage` with a `ReceivedEmail`.
302
+ *
303
+ * Needs blueprint `email: {}` (table `pipework_email_inbound`; `pipework_email_messages` gains `conversation_id`).
304
+ * The row is not tenant-scoped (it exists before a tenant is known) and holds no bodies: the raw message
305
+ * is the copy and `sweep` deletes it after `rawRetentionDays`.
306
+ *
307
+ * Threading: the ids in In-Reply-To then References are matched to an earlier received message's
308
+ * Message-ID, or, for an id `<X@region.amazonses.com>`, to a sent message whose `provider_message_id` is `X`
309
+ * (set the sent message's conversation with `mail.send({ conversationId })` or `inReplyTo`). The first match
310
+ * decides the conversation; with none the message starts one (`isNewConversation: true`). Ids are matched exactly.
311
+ * A received message is matched only when its tenant equals the tenant `resolveTenant` named for this message or
312
+ * either is null; a sent message's conversation is refused when it holds a received message of another tenant.
313
+ *
314
+ * Threading is not authentication: Message-IDs are not secret and From is forgeable. Before attaching a message to
315
+ * a conversation's participants, the app checks `from` against them and the DKIM and DMARC verdicts.
316
+ *
317
+ * The receive job is a trace step `pipework/email:receive` with an `EmailInbound:received` audit record on success.
318
+ * A handler error (or a failure reading the raw message) rethrows so the queue retries; attachments stored by
319
+ * an earlier attempt are reused. Set `maxAttempts` on the queue. A raw message over `maxRawBytes` is marked
320
+ * `failed` and not retried. Message bodies are never logged.
321
+ */
322
+ export function createInbound(config: InboundConfig): Inbound {
323
+ if (config.source.kind === 'folder' && config.environment === 'production') {
324
+ throw new Error(
325
+ '[pipework] mail.inboundFolder is refused in production.\n\n' +
326
+ ' The stand-in reads .eml files from a folder.\n' +
327
+ ' Configure a real source, e.g. mail.sesReceipt({ bucket, topicArns }).\n',
328
+ )
329
+ }
330
+ const retentionDays = Math.floor(config.rawRetentionDays ?? DEFAULT_RETENTION_DAYS)
331
+ const maxRawBytes = config.maxRawBytes ?? DEFAULT_MAX_RAW_BYTES
332
+ if (!(retentionDays >= 0)) throw new Error('[pipework] inbound rawRetentionDays must be zero or more.')
333
+ const attachmentDays = config.attachmentRetentionDays === undefined ? undefined : Math.floor(config.attachmentRetentionDays)
334
+ if (attachmentDays !== undefined && !(attachmentDays >= 1)) throw new Error('[pipework] inbound attachmentRetentionDays must be one or more.')
335
+ if (!(maxRawBytes > 0)) throw new Error('[pipework] inbound maxRawBytes must be positive.')
336
+
337
+ async function record(
338
+ source: 'ses' | 'folder', sourceId: string, location: RawLocation,
339
+ extra: { verdicts: EmailVerdicts | null; recipients: readonly string[] | null; from: string | null },
340
+ ): Promise<string | null> {
341
+ const traceId = getContext()?.trace?.id ?? null
342
+ return pipeSystem().transaction(async (tx) => {
343
+ const inserted = await tx.execute(sql`
344
+ INSERT INTO pipework_email_inbound
345
+ (source_id, source, raw_location, raw_expires_at, verdicts, recipients, from_address, trace_id)
346
+ VALUES (${sourceId}, ${source}, ${JSON.stringify(location)}::jsonb, now() + make_interval(days => ${retentionDays}::int),
347
+ ${extra.verdicts === null ? null : JSON.stringify(extra.verdicts)}::jsonb,
348
+ ${extra.recipients === null ? null : JSON.stringify(extra.recipients)}::jsonb, ${extra.from}, ${traceId}::uuid)
349
+ ON CONFLICT (source_id) DO NOTHING
350
+ RETURNING id`)
351
+ if (inserted.length === 0) return null
352
+ const inboundId = inserted[0]!['id'] as string
353
+ await config.queue.enqueue(tx, { type: RECEIVE_JOB, payload: { inboundId }, continueFlow: true })
354
+ return inboundId
355
+ })
356
+ }
357
+
358
+ async function ingestNotification(message: string): Promise<Record<string, unknown>> {
359
+ let parsed: unknown
360
+ try {
361
+ parsed = JSON.parse(message)
362
+ } catch {
363
+ return { ignored: true }
364
+ }
365
+ if (typeof parsed !== 'object' || parsed === null) return { ignored: true }
366
+ const event = parsed as Record<string, unknown>
367
+ if (event['notificationType'] !== 'Received') return { ignored: true }
368
+ const mailPart = event['mail'] as { messageId?: unknown; source?: unknown; destination?: unknown } | undefined
369
+ const receipt = event['receipt'] as Record<string, unknown> | undefined
370
+ const action = receipt?.['action'] as { type?: unknown; bucketName?: unknown; objectKey?: unknown } | undefined
371
+ if (typeof mailPart?.messageId !== 'string' || receipt === undefined) return { ignored: true }
372
+ if (action?.type !== 'S3' || typeof action.bucketName !== 'string' || typeof action.objectKey !== 'string') {
373
+ getBaseLogger().info({ event: 'email receipt ignored', actionType: action?.type ?? null }, 'SES receipt ignored: its action is not S3')
374
+ return { ignored: true }
375
+ }
376
+ const recipientList = Array.isArray(receipt['recipients']) ? receipt['recipients'] : mailPart.destination
377
+ const recipients = Array.isArray(recipientList) ? recipientList.filter((r): r is string => typeof r === 'string') : []
378
+ const id = await record('ses', mailPart.messageId, { bucket: action.bucketName, key: action.objectKey }, {
379
+ verdicts: verdictsOf(receipt), recipients, from: typeof mailPart.source === 'string' ? mailPart.source : null,
380
+ })
381
+ if (id === null) getBaseLogger().info({ event: 'email receipt duplicate', sourceId: mailPart.messageId }, 'SES receipt already recorded')
382
+ return {}
383
+ }
384
+
385
+ interface Threaded { readonly conversationId: string; readonly isNew: boolean }
386
+
387
+ async function thread(ids: readonly string[], recorded: string | null, tenantId: string | null): Promise<Threaded> {
388
+ const db = pipeSystem()
389
+ if (ids.length > 0) {
390
+ const sesLocals = ids.flatMap(id => {
391
+ const m = /^(.+)@[^@]*amazonses\.com$/i.exec(id)
392
+ return m === null ? [] : [m[1]!]
393
+ })
394
+ const received = await db.execute(sql`
395
+ SELECT message_id_header AS id, conversation_id FROM pipework_email_inbound
396
+ WHERE message_id_header = ANY(${textArray(ids)}::text[]) AND conversation_id IS NOT NULL
397
+ AND (${tenantId}::uuid IS NULL OR tenant_id IS NULL OR tenant_id = ${tenantId}::uuid)`)
398
+ const sent = sesLocals.length === 0 ? [] : await db.execute(sql`
399
+ SELECT provider_message_id AS id, conversation_id FROM pipework_email_messages
400
+ WHERE provider_message_id = ANY(${textArray(sesLocals)}::text[]) AND conversation_id IS NOT NULL`)
401
+ const byId = new Map<string, string>()
402
+ for (const r of received) byId.set(r['id'] as string, r['conversation_id'] as string)
403
+ const bySes = new Map<string, string>()
404
+ for (const r of sent) {
405
+ const conversation = r['conversation_id'] as string
406
+ const foreign = tenantId === null ? [] : await db.execute(sql`
407
+ SELECT 1 FROM pipework_email_inbound
408
+ WHERE conversation_id = ${conversation}::uuid AND tenant_id IS NOT NULL AND tenant_id <> ${tenantId}::uuid LIMIT 1`)
409
+ if (foreign.length === 0) bySes.set(r['id'] as string, conversation)
410
+ }
411
+ for (const id of ids) {
412
+ const hit = byId.get(id) ?? bySes.get(/^(.+)@[^@]*amazonses\.com$/i.exec(id)?.[1] ?? '\u0000')
413
+ if (hit !== undefined) return { conversationId: hit, isNew: false }
414
+ }
415
+ }
416
+ // No match: this message's own conversation, kept from an earlier attempt of the job.
417
+ if (recorded !== null) return { conversationId: recorded, isNew: true }
418
+ return { conversationId: randomUUID(), isNew: true }
419
+ }
420
+
421
+ async function fail(id: string, error: string): Promise<{ status: string }> {
422
+ await pipeSystem().execute(sql`UPDATE pipework_email_inbound SET status = 'failed', failed_at = now(), error = ${error} WHERE id = ${id}::uuid`)
423
+ await auditEmail('failed', id, { error }, 'EmailInbound')
424
+ getBaseLogger().error({ event: 'email receive failed', inboundId: id }, `received email not handled: ${error}`)
425
+ return { status: 'failed' }
426
+ }
427
+
428
+ const receive = __flowStep({ ref: 'pipework/email:receive' }, async (inboundId: string): Promise<{ status: string }> => {
429
+ const db = pipeSystem()
430
+ const row = (await db.execute(sql`SELECT * FROM pipework_email_inbound WHERE id = ${inboundId}::uuid`))[0]
431
+ if (row === undefined) throw new Error(`[pipework] Received email ${inboundId} does not exist.`)
432
+ const status = row['status'] as string
433
+ if (status === 'processed' || status === 'failed') return { status }
434
+ if (config.source.kind === 'folder' && getContext()?.instance.config.environment === 'production') {
435
+ throw new Error('[pipework] mail.inboundFolder is refused in production.')
436
+ }
437
+ if (row['raw_deleted_at'] !== null) return fail(inboundId, 'the raw message was deleted before it was handled')
438
+
439
+ let raw: Buffer
440
+ try {
441
+ raw = await config.source.read(row['raw_location'] as RawLocation, maxRawBytes)
442
+ } catch (err) {
443
+ if (err instanceof RawTooLargeError || err instanceof RawNotFoundError) return fail(inboundId, err.message)
444
+ const message = describeError(err).message
445
+ await db.execute(sql`UPDATE pipework_email_inbound SET error = ${message} WHERE id = ${inboundId}::uuid`)
446
+ throw err
447
+ }
448
+
449
+ const parsed = parseMime(raw)
450
+ const recipients = (row['recipients'] as string[] | null) ?? [...parsed.to, ...parsed.cc].map(a => a.address)
451
+ const verdicts = (row['verdicts'] as EmailVerdicts | null) ?? null
452
+ // A declared type the bytes contradict resolves to '': store octet-stream rather than refuse the mail.
453
+ const resolved = parsed.attachments.map(a => {
454
+ const declared = a.mediaType.split(';')[0]!.trim().toLowerCase()
455
+ const { detectedType, mediaType } = resolveMediaType({ head: a.content.subarray(0, SNIFF_BYTES), declared, extension: extensionOf(a.filename) })
456
+ return { mediaType: mediaType === '' ? 'application/octet-stream' : mediaType, detectedType, declaredType: declared }
457
+ })
458
+ const unthreaded: UnthreadedEmail = {
459
+ id: inboundId,
460
+ messageId: parsed.messageId, inReplyTo: parsed.inReplyTo, references: parsed.references,
461
+ from: addressOf(parsed.from), fromProblem: parsed.fromProblem,
462
+ to: parsed.to.map(addressOf), cc: parsed.cc.map(addressOf), replyTo: parsed.replyTo.map(addressOf),
463
+ subject: parsed.subject, date: parsed.date, text: parsed.text, html: parsed.html,
464
+ attachments: parsed.attachments.map((a, idx) => ({
465
+ filename: a.filename, mediaType: resolved[idx]!.mediaType, size: a.size, sha256: createHash('sha256').update(a.content).digest('hex'),
466
+ contentId: a.contentId, inline: a.inline, file: null,
467
+ })),
468
+ verdicts, recipients,
469
+ }
470
+
471
+ let tenantId = (row['tenant_id'] as string | null) ?? null
472
+ if (tenantId === null && config.resolveTenant !== undefined) tenantId = await config.resolveTenant(unthreaded)
473
+ const { conversationId, isNew } = await thread(parsed.threadIds, (row['conversation_id'] as string | null) ?? null, tenantId)
474
+ const base: ReceivedEmail = { ...unthreaded, conversationId, isNewConversation: isNew, tenantId }
475
+
476
+ await db.execute(sql`
477
+ UPDATE pipework_email_inbound SET
478
+ message_id_header = ${parsed.messageId}, in_reply_to = ${parsed.inReplyTo}, "references" = ${JSON.stringify(parsed.references)}::jsonb,
479
+ from_address = ${base.from.address}, subject = ${parsed.subject}, conversation_id = ${conversationId}::uuid,
480
+ tenant_id = ${tenantId}::uuid, recipients = ${JSON.stringify(recipients)}::jsonb, error = NULL
481
+ WHERE id = ${inboundId}::uuid`)
482
+
483
+ let attachments = base.attachments
484
+ if (config.storage !== undefined && tenantId !== null && parsed.attachments.length > 0) {
485
+ const storage = config.storage
486
+ const stored: (string | null)[] = Array.isArray(row['attachment_files']) ? [...(row['attachment_files'] as (string | null)[])] : []
487
+ const files: (StoredFile | null)[] = []
488
+ for (let i = 0; i < parsed.attachments.length; i++) {
489
+ const part = parsed.attachments[i]!
490
+ let file: StoredFile | null = null
491
+ const earlier = stored[i]
492
+ if (typeof earlier === 'string') file = await storage.getFor(tenantId, earlier)
493
+ if (file === null) {
494
+ file = await storage.putFor(tenantId, {
495
+ body: Readable.from([part.content]), mediaType: attachments[i]!.mediaType,
496
+ metadata: { detectedType: resolved[i]!.detectedType, declaredType: resolved[i]!.declaredType },
497
+ ...(part.filename !== null ? { filename: part.filename } : {}),
498
+ expectedSha256: attachments[i]!.sha256,
499
+ ...(attachmentDays !== undefined ? { expiresAt: new Date(toDate(row['received_at']).getTime() + attachmentDays * 86_400_000) } : {}),
500
+ })
501
+ stored[i] = file.id
502
+ await db.execute(sql`UPDATE pipework_email_inbound SET attachment_files = ${JSON.stringify(stored)}::jsonb WHERE id = ${inboundId}::uuid`)
503
+ }
504
+ files.push(file)
505
+ }
506
+ attachments = attachments.map((a, i) => ({ ...a, file: files[i] ?? null }))
507
+ }
508
+
509
+ try {
510
+ await config.onMessage({ ...base, tenantId, attachments })
511
+ } catch (err) {
512
+ const message = describeError(err).message
513
+ await db.execute(sql`UPDATE pipework_email_inbound SET error = ${message} WHERE id = ${inboundId}::uuid`)
514
+ throw err
515
+ }
516
+ const traceId = getContext()?.trace?.id ?? null
517
+ await db.execute(sql`
518
+ UPDATE pipework_email_inbound SET status = 'processed', processed_at = now(), error = NULL, trace_id = COALESCE(${traceId}::uuid, trace_id)
519
+ WHERE id = ${inboundId}::uuid`)
520
+ await auditEmail('received', inboundId, {
521
+ conversationId, isNewConversation: isNew, tenantId, attachments: attachments.length,
522
+ }, 'EmailInbound')
523
+ return { status: 'processed' }
524
+ })
525
+
526
+ const receiveJob = fitting.use().job(RECEIVE_JOB)
527
+ .input(schema.check.object({ inboundId: schema.check.string() }))
528
+ .fit(async ({ input }: { input: { inboundId: string } }) => receive(input.inboundId))
529
+
530
+ return {
531
+ webhook(webhook) {
532
+ const ses = config.source.ses
533
+ if (config.source.kind !== 'ses' || ses === undefined) {
534
+ throw new Error('[pipework] inbound.webhook needs a mail.sesReceipt source; this inbound reads a folder.')
535
+ }
536
+ return snsHandler({
537
+ path: webhook.path, topicArns: ses.topicArns,
538
+ ...(ses.region !== undefined ? { region: ses.region } : {}),
539
+ ...(ses.fetchCert !== undefined ? { fetchCert: ses.fetchCert } : {}),
540
+ ...(ses.confirm !== undefined ? { confirm: ses.confirm } : {}),
541
+ }, ingestNotification)
542
+ },
543
+ jobs: [receiveJob as unknown as Handler<Record<string, unknown>, unknown>],
544
+
545
+ async scan() {
546
+ const folder = config.source.folder
547
+ if (config.source.kind !== 'folder' || folder === undefined) {
548
+ throw new Error('[pipework] inbound.scan needs a mail.inboundFolder source; this inbound receives through SES.')
549
+ }
550
+ let names: string[]
551
+ try {
552
+ names = (await readdir(folder.dir)).filter(n => n.toLowerCase().endsWith('.eml')).sort()
553
+ } catch (err) {
554
+ if ((err as { code?: string }).code === 'ENOENT') return { received: 0, duplicates: 0 }
555
+ throw err
556
+ }
557
+ const done = join(folder.dir, 'received')
558
+ let received = 0
559
+ let duplicates = 0
560
+ for (const name of names) {
561
+ const from = join(folder.dir, name)
562
+ if (!(await stat(from)).isFile()) continue
563
+ const bytes = await readFile(from)
564
+ const sha = createHash('sha256').update(bytes).digest('hex')
565
+ await mkdir(done, { recursive: true })
566
+ const to = join(done, `${sha.slice(0, 12)}-${basename(name)}`)
567
+ await copyFile(from, to)
568
+ const id = await record('folder', `folder:${sha}`, { path: to }, { verdicts: null, recipients: null, from: null })
569
+ await unlink(from)
570
+ if (id === null) duplicates++
571
+ else received++
572
+ }
573
+ return { received, duplicates }
574
+ },
575
+
576
+ async sweep(options) {
577
+ const limit = options?.limit ?? DEFAULT_SWEEP_LIMIT
578
+ const db = pipeSystem()
579
+ const due = await db.execute(sql`
580
+ SELECT id, raw_location FROM pipework_email_inbound
581
+ WHERE raw_expires_at <= now() AND raw_deleted_at IS NULL
582
+ ORDER BY raw_expires_at LIMIT ${limit}`)
583
+ let deleted = 0
584
+ let failed = 0
585
+ for (const row of due) {
586
+ try {
587
+ await config.source.remove(row['raw_location'] as RawLocation)
588
+ await db.execute(sql`UPDATE pipework_email_inbound SET raw_deleted_at = now() WHERE id = ${row['id'] as string}::uuid`)
589
+ deleted++
590
+ } catch (err) {
591
+ failed++
592
+ getBaseLogger().error({ err, inboundId: row['id'] }, 'inbound sweep could not delete a raw message')
593
+ void recordFault('inbound-raw-delete', faultMessage(err), { inboundId: String(row['id']) })
594
+ }
595
+ }
596
+ return { deleted, failed }
597
+ },
598
+
599
+ async get(id) {
600
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) return null
601
+ const row = (await pipeSystem().execute(sql`SELECT * FROM pipework_email_inbound WHERE id = ${id}::uuid`))[0]
602
+ if (row === undefined) return null
603
+ const date = (v: unknown): Date | null => (v === null || v === undefined ? null : toDate(v))
604
+ return {
605
+ id: row['id'] as string,
606
+ source: row['source'] as 'ses' | 'folder',
607
+ status: row['status'] as 'received' | 'processed' | 'failed',
608
+ error: (row['error'] as string | null) ?? null,
609
+ messageId: (row['message_id_header'] as string | null) ?? null,
610
+ conversationId: (row['conversation_id'] as string | null) ?? null,
611
+ tenantId: (row['tenant_id'] as string | null) ?? null,
612
+ subject: (row['subject'] as string | null) ?? null,
613
+ fromAddress: (row['from_address'] as string | null) ?? null,
614
+ verdicts: (row['verdicts'] as EmailVerdicts | null) ?? null,
615
+ receivedAt: toDate(row['received_at']),
616
+ processedAt: date(row['processed_at']),
617
+ rawExpiresAt: toDate(row['raw_expires_at']),
618
+ rawDeletedAt: date(row['raw_deleted_at']),
619
+ }
620
+ },
621
+ }
622
+ }
@@ -0,0 +1,11 @@
1
+ export { createMail, normalizeAddress, SEND_JOB, type Mail, type MailConfig, type SendInput, type SendResult,
2
+ type RenderedEmail, type EmailTemplates, type EmailRenderMeta } from './mail.js'
3
+ export { ses, devStandIn, type MailProvider, type OutgoingMessage, type SesConfig } from './providers.js'
4
+ export {
5
+ createInbound, sesReceipt, inboundFolder, RECEIVE_JOB, type Inbound, type InboundConfig, type InboundSource, type InboundRecord,
6
+ type InboundScanResult, type InboundSweepResult, type ReceivedEmail, type ReceivedAttachment, type EmailAddress,
7
+ type EmailVerdicts, type FromProblem, type UnthreadedEmail, type SesReceiptConfig,
8
+ } from './inbound.js'
9
+ export { snsWebhook, type SnsWebhookConfig } from './sns.js'
10
+ export { signV4, type SigV4Request, type AwsCredentials } from '../aws/sigv4.js'
11
+ export { awsCredentials, type AwsCredentialsOptions } from '../aws/credentials.js'