pipework 0.251.0 → 0.252.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (730) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/core/config/load.d.ts +2 -0
  3. package/dist/core/config/load.d.ts.map +1 -1
  4. package/dist/core/config/load.js +1 -0
  5. package/dist/core/config/load.js.map +1 -1
  6. package/dist/core/config/namespace.d.ts +1 -0
  7. package/dist/core/config/namespace.d.ts.map +1 -1
  8. package/dist/core/config/schema.d.ts +2 -0
  9. package/dist/core/config/schema.d.ts.map +1 -1
  10. package/dist/core/config/schema.js +1 -0
  11. package/dist/core/config/schema.js.map +1 -1
  12. package/dist/core/surface/types.d.ts +1 -0
  13. package/dist/core/surface/types.d.ts.map +1 -1
  14. package/dist/data/db/namespace.d.ts +3 -0
  15. package/dist/data/db/namespace.d.ts.map +1 -1
  16. package/dist/data/db/namespace.js +3 -0
  17. package/dist/data/db/namespace.js.map +1 -1
  18. package/dist/data/db/pool.d.ts.map +1 -1
  19. package/dist/data/db/pool.js +6 -1
  20. package/dist/data/db/pool.js.map +1 -1
  21. package/dist/data/db/query-label.d.ts +5 -0
  22. package/dist/data/db/query-label.d.ts.map +1 -0
  23. package/dist/data/db/query-label.js +11 -0
  24. package/dist/data/db/query-label.js.map +1 -0
  25. package/dist/data/db/query-watch.d.ts +21 -0
  26. package/dist/data/db/query-watch.d.ts.map +1 -0
  27. package/dist/data/db/query-watch.js +90 -0
  28. package/dist/data/db/query-watch.js.map +1 -0
  29. package/dist/data/query/postgres-js/driver.js +1 -1
  30. package/dist/data/query/postgres-js/driver.js.map +1 -1
  31. package/dist/data/query/postgres-js/session.d.ts +5 -1
  32. package/dist/data/query/postgres-js/session.d.ts.map +1 -1
  33. package/dist/data/query/postgres-js/session.js +16 -11
  34. package/dist/data/query/postgres-js/session.js.map +1 -1
  35. package/dist/data/query/utils.d.ts +2 -0
  36. package/dist/data/query/utils.d.ts.map +1 -1
  37. package/dist/data/query/utils.js.map +1 -1
  38. package/dist/infra/credits/schema.d.ts +1 -1
  39. package/dist/test/plugin.d.ts.map +1 -1
  40. package/dist/test/plugin.js +10 -4
  41. package/dist/test/plugin.js.map +1 -1
  42. package/package.json +4 -3
  43. package/src/async/jobs/abortable-delay.ts +18 -0
  44. package/src/async/jobs/claims.ts +52 -0
  45. package/src/async/jobs/cron.ts +179 -0
  46. package/src/async/jobs/errors.ts +56 -0
  47. package/src/async/jobs/exclusion.ts +117 -0
  48. package/src/async/jobs/execute.ts +219 -0
  49. package/src/async/jobs/index.ts +10 -0
  50. package/src/async/jobs/listener.ts +135 -0
  51. package/src/async/jobs/namespace.ts +20 -0
  52. package/src/async/jobs/queue-door.ts +44 -0
  53. package/src/async/jobs/queue.ts +1012 -0
  54. package/src/async/jobs/run-script.ts +67 -0
  55. package/src/async/jobs/scheduler.ts +218 -0
  56. package/src/async/jobs/schedules-table.ts +37 -0
  57. package/src/async/jobs/serialize-error.ts +48 -0
  58. package/src/async/jobs/table.ts +191 -0
  59. package/src/async/pipeline/define.ts +53 -0
  60. package/src/async/pipeline/errors.ts +35 -0
  61. package/src/async/pipeline/execute.ts +53 -0
  62. package/src/async/pipeline/index.ts +13 -0
  63. package/src/async/pipeline/namespace.ts +12 -0
  64. package/src/async/pipeline/resume.ts +67 -0
  65. package/src/async/pipeline/run-steps.ts +98 -0
  66. package/src/async/pipeline/types.ts +57 -0
  67. package/src/async/state-machine/define.ts +64 -0
  68. package/src/async/state-machine/errors.ts +29 -0
  69. package/src/async/state-machine/index.ts +3 -0
  70. package/src/async/state-machine/namespace.ts +12 -0
  71. package/src/async/state-machine/transition.ts +90 -0
  72. package/src/async/state-machine/types.ts +37 -0
  73. package/src/async/steps/errors.ts +31 -0
  74. package/src/async/steps/graph.ts +1214 -0
  75. package/src/async/steps/hash.ts +40 -0
  76. package/src/async/steps/index.ts +7 -0
  77. package/src/async/steps/namespace.ts +7 -0
  78. package/src/async/steps/table.ts +97 -0
  79. package/src/async/steps/types.ts +318 -0
  80. package/src/audit/chain/index.ts +9 -0
  81. package/src/audit/chain/install.ts +113 -0
  82. package/src/audit/chain/retire.ts +52 -0
  83. package/src/audit/chain/verify.ts +257 -0
  84. package/src/audit/query/cursor.ts +37 -0
  85. package/src/audit/query/index.ts +9 -0
  86. package/src/audit/query/query.ts +223 -0
  87. package/src/audit/schema/audit-record-fields.ts +68 -0
  88. package/src/audit/schema/audit-record.ts +23 -0
  89. package/src/auth/auth/audit.ts +31 -0
  90. package/src/auth/auth/chain.ts +84 -0
  91. package/src/auth/auth/cookie.ts +136 -0
  92. package/src/auth/auth/email-links.ts +311 -0
  93. package/src/auth/auth/guard.ts +128 -0
  94. package/src/auth/auth/index.ts +18 -0
  95. package/src/auth/auth/multi-org.ts +337 -0
  96. package/src/auth/auth/namespace.ts +34 -0
  97. package/src/auth/auth/parse-expiry.ts +13 -0
  98. package/src/auth/auth/passwords.ts +110 -0
  99. package/src/auth/auth/schema.ts +110 -0
  100. package/src/auth/auth/sessions.ts +565 -0
  101. package/src/auth/auth/sign-in.ts +156 -0
  102. package/src/auth/auth/signing-keys.ts +69 -0
  103. package/src/auth/auth/types.ts +26 -0
  104. package/src/auth/auth/uuid.ts +10 -0
  105. package/src/auth/rbac/cache.ts +55 -0
  106. package/src/auth/rbac/check.ts +30 -0
  107. package/src/auth/rbac/create.ts +35 -0
  108. package/src/auth/rbac/index.ts +5 -0
  109. package/src/auth/rbac/middleware.ts +20 -0
  110. package/src/auth/rbac/namespace.ts +15 -0
  111. package/src/auth/rbac/resolve.ts +31 -0
  112. package/src/auth/rbac/schema.sql +31 -0
  113. package/src/auth/rbac/seed.ts +67 -0
  114. package/src/auth/rbac/tables.ts +32 -0
  115. package/src/auth/rbac/types.ts +65 -0
  116. package/src/auth/tenant/extract.ts +33 -0
  117. package/src/auth/tenant/in-tenant.ts +49 -0
  118. package/src/auth/tenant/index.ts +7 -0
  119. package/src/auth/tenant/namespace.ts +23 -0
  120. package/src/auth/tenant/propagate.ts +20 -0
  121. package/src/auth/tenant/rls.ts +120 -0
  122. package/src/auth/tenant/scope.ts +33 -0
  123. package/src/auth/tenant/scoped-db.ts +581 -0
  124. package/src/auth/tenant/types.ts +14 -0
  125. package/src/auth/tenant/validate.ts +27 -0
  126. package/src/cli/commands/add.ts +158 -0
  127. package/src/cli/commands/alerts.ts +98 -0
  128. package/src/cli/commands/check.ts +1418 -0
  129. package/src/cli/commands/codemod.ts +78 -0
  130. package/src/cli/commands/db.ts +356 -0
  131. package/src/cli/commands/dev.ts +101 -0
  132. package/src/cli/commands/generate.ts +75 -0
  133. package/src/cli/commands/init.ts +36 -0
  134. package/src/cli/commands/install.ts +28 -0
  135. package/src/cli/commands/lint.ts +62 -0
  136. package/src/cli/commands/migrate.ts +132 -0
  137. package/src/cli/commands/openapi.ts +63 -0
  138. package/src/cli/commands/remove.ts +72 -0
  139. package/src/cli/commands/run.ts +50 -0
  140. package/src/cli/commands/scope.ts +54 -0
  141. package/src/cli/commands/serve.ts +68 -0
  142. package/src/cli/commands/shard.ts +31 -0
  143. package/src/cli/commands/shared/config-pipework-version.ts +28 -0
  144. package/src/cli/commands/shared/find-config.ts +22 -0
  145. package/src/cli/commands/shared/load-config.ts +35 -0
  146. package/src/cli/commands/shared/pnpm-install.ts +95 -0
  147. package/src/cli/commands/shared/report-removed.ts +8 -0
  148. package/src/cli/commands/sync.ts +32 -0
  149. package/src/cli/commands/test.ts +307 -0
  150. package/src/cli/commands/upgrade.ts +48 -0
  151. package/src/cli/dev-bootstrap.ts +24 -0
  152. package/src/cli/errors.ts +10 -0
  153. package/src/cli/index.ts +303 -0
  154. package/src/cli/output.ts +17 -0
  155. package/src/cli/port.ts +15 -0
  156. package/src/cli/surface-kinds.ts +20 -0
  157. package/src/client/index.ts +4 -0
  158. package/src/client/poll.ts +130 -0
  159. package/src/client/report-fault.ts +119 -0
  160. package/src/core/config/app-pool.ts +19 -0
  161. package/src/core/config/aws-env.ts +29 -0
  162. package/src/core/config/base-dir.ts +24 -0
  163. package/src/core/config/discover.ts +64 -0
  164. package/src/core/config/env-resolve.ts +87 -0
  165. package/src/core/config/env-types.ts +116 -0
  166. package/src/core/config/env.ts +55 -0
  167. package/src/core/config/errors.ts +9 -0
  168. package/src/core/config/import-config.ts +40 -0
  169. package/src/core/config/index.ts +8 -0
  170. package/src/core/config/load.ts +417 -0
  171. package/src/core/config/namespace.ts +14 -0
  172. package/src/core/config/resolve.ts +195 -0
  173. package/src/core/config/schema.ts +488 -0
  174. package/src/core/config/statement-observer.ts +48 -0
  175. package/src/core/config/ts-inline-hooks.ts +75 -0
  176. package/src/core/config/ts-register.ts +4 -0
  177. package/src/core/config/ts-resolve-hooks.ts +101 -0
  178. package/src/core/errors/base.ts +49 -0
  179. package/src/core/errors/http.ts +146 -0
  180. package/src/core/errors/index.ts +2 -0
  181. package/src/core/errors/namespace.ts +31 -0
  182. package/src/core/lifecycle/drain.ts +33 -0
  183. package/src/core/lifecycle/health.ts +72 -0
  184. package/src/core/lifecycle/index.ts +4 -0
  185. package/src/core/lifecycle/namespace.ts +7 -0
  186. package/src/core/lifecycle/startup.ts +102 -0
  187. package/src/core/lifecycle/types.ts +36 -0
  188. package/src/core/logging/context.ts +60 -0
  189. package/src/core/logging/error-chain.ts +68 -0
  190. package/src/core/logging/index.ts +5 -0
  191. package/src/core/logging/logger.ts +65 -0
  192. package/src/core/logging/namespace.ts +16 -0
  193. package/src/core/logging/proxy.ts +22 -0
  194. package/src/core/logging/redact-match.ts +24 -0
  195. package/src/core/logging/types.ts +47 -0
  196. package/src/core/pipework.ts +566 -0
  197. package/src/core/runtime/broker.ts +69 -0
  198. package/src/core/surface/http.ts +133 -0
  199. package/src/core/surface/index.ts +11 -0
  200. package/src/core/surface/job-timeout.ts +40 -0
  201. package/src/core/surface/namespace.ts +13 -0
  202. package/src/core/surface/script.ts +33 -0
  203. package/src/core/surface/start.ts +142 -0
  204. package/src/core/surface/types.ts +225 -0
  205. package/src/core/surface/validate.ts +88 -0
  206. package/src/core/surface/worker.ts +528 -0
  207. package/src/core/validation/connectivity.ts +49 -0
  208. package/src/core/validation/domain.ts +99 -0
  209. package/src/core/validation/index.ts +5 -0
  210. package/src/core/validation/queue-tables.ts +312 -0
  211. package/src/core/validation/rls-role.ts +138 -0
  212. package/src/core/validation/startup.ts +51 -0
  213. package/src/core/validation/surfaces.ts +65 -0
  214. package/src/core/validation/types.ts +25 -0
  215. package/src/data/db/advisory-lock.ts +55 -0
  216. package/src/data/db/aggregates.ts +172 -0
  217. package/src/data/db/array-elements.ts +111 -0
  218. package/src/data/db/bulk-set.ts +53 -0
  219. package/src/data/db/col/index.ts +14 -0
  220. package/src/data/db/composed.ts +53 -0
  221. package/src/data/db/db.ts +276 -0
  222. package/src/data/db/excluded.ts +16 -0
  223. package/src/data/db/expressions.ts +473 -0
  224. package/src/data/db/filter.ts +90 -0
  225. package/src/data/db/function-source.ts +114 -0
  226. package/src/data/db/identifiers.ts +16 -0
  227. package/src/data/db/idx/index.ts +9 -0
  228. package/src/data/db/in-list.ts +49 -0
  229. package/src/data/db/index.ts +9 -0
  230. package/src/data/db/insert-split.ts +137 -0
  231. package/src/data/db/instrumentation.ts +82 -0
  232. package/src/data/db/maintenance.ts +28 -0
  233. package/src/data/db/namespace.ts +107 -0
  234. package/src/data/db/owner-labels.ts +9 -0
  235. package/src/data/db/pool.ts +187 -0
  236. package/src/data/db/query-label.ts +13 -0
  237. package/src/data/db/query-watch.ts +116 -0
  238. package/src/data/db/raw-client-warning.ts +37 -0
  239. package/src/data/db/readonly.ts +23 -0
  240. package/src/data/db/reservation.ts +151 -0
  241. package/src/data/db/schema-features.ts +14 -0
  242. package/src/data/db/serializable.ts +83 -0
  243. package/src/data/db/sql.ts +1 -0
  244. package/src/data/db/timeouts.ts +76 -0
  245. package/src/data/db/types.ts +18 -0
  246. package/src/data/db/unbound.ts +33 -0
  247. package/src/data/db/values.ts +29 -0
  248. package/src/data/domain/brand.ts +15 -0
  249. package/src/data/domain/build-factory.ts +193 -0
  250. package/src/data/domain/build-schema.ts +161 -0
  251. package/src/data/domain/build-table.ts +355 -0
  252. package/src/data/domain/define.ts +239 -0
  253. package/src/data/domain/external-registry.ts +34 -0
  254. package/src/data/domain/external.ts +77 -0
  255. package/src/data/domain/field.ts +279 -0
  256. package/src/data/domain/index.ts +13 -0
  257. package/src/data/domain/naming.ts +6 -0
  258. package/src/data/domain/projections.ts +57 -0
  259. package/src/data/domain/trace-safe-projection.ts +31 -0
  260. package/src/data/domain/traits.ts +108 -0
  261. package/src/data/domain/types.ts +289 -0
  262. package/src/data/jsonb/composed.ts +18 -0
  263. package/src/data/jsonb/index.ts +1 -0
  264. package/src/data/jsonb/namespace.ts +30 -0
  265. package/src/data/jsonb/query.ts +86 -0
  266. package/src/data/migrate/append-only-generate.ts +47 -0
  267. package/src/data/migrate/applied.ts +47 -0
  268. package/src/data/migrate/apply.ts +309 -0
  269. package/src/data/migrate/check.ts +119 -0
  270. package/src/data/migrate/diff.ts +357 -0
  271. package/src/data/migrate/door-generate.ts +130 -0
  272. package/src/data/migrate/framework-rls.ts +58 -0
  273. package/src/data/migrate/generate.ts +201 -0
  274. package/src/data/migrate/history.ts +215 -0
  275. package/src/data/migrate/index.ts +9 -0
  276. package/src/data/migrate/internal-definitions.ts +175 -0
  277. package/src/data/migrate/post-process.ts +73 -0
  278. package/src/data/migrate/pragma.ts +73 -0
  279. package/src/data/migrate/provision-framework.ts +147 -0
  280. package/src/data/migrate/rls-generate.ts +61 -0
  281. package/src/data/migrate/run.ts +124 -0
  282. package/src/data/migrate/safety.ts +167 -0
  283. package/src/data/migrate/snapshot.ts +368 -0
  284. package/src/data/migrate/sql-emit.ts +392 -0
  285. package/src/data/migrate/statements.ts +312 -0
  286. package/src/data/query/alias.ts +126 -0
  287. package/src/data/query/batch.ts +8 -0
  288. package/src/data/query/cache/core/cache.ts +78 -0
  289. package/src/data/query/cache/core/index.ts +1 -0
  290. package/src/data/query/cache/core/types.ts +29 -0
  291. package/src/data/query/cache/readme.md +230 -0
  292. package/src/data/query/casing.ts +76 -0
  293. package/src/data/query/collation.ts +11 -0
  294. package/src/data/query/column-builder.ts +370 -0
  295. package/src/data/query/column.ts +150 -0
  296. package/src/data/query/entity.ts +42 -0
  297. package/src/data/query/errors.ts +33 -0
  298. package/src/data/query/index.ts +14 -0
  299. package/src/data/query/logger.ts +47 -0
  300. package/src/data/query/operations.ts +45 -0
  301. package/src/data/query/pg-core/alias.ts +12 -0
  302. package/src/data/query/pg-core/checks.ts +32 -0
  303. package/src/data/query/pg-core/columns/all.ts +71 -0
  304. package/src/data/query/pg-core/columns/bigint.ts +110 -0
  305. package/src/data/query/pg-core/columns/bigserial.ts +128 -0
  306. package/src/data/query/pg-core/columns/boolean.ts +43 -0
  307. package/src/data/query/pg-core/columns/char.ts +85 -0
  308. package/src/data/query/pg-core/columns/cidr.ts +43 -0
  309. package/src/data/query/pg-core/columns/common.ts +366 -0
  310. package/src/data/query/pg-core/columns/custom.ts +232 -0
  311. package/src/data/query/pg-core/columns/date.common.ts +15 -0
  312. package/src/data/query/pg-core/columns/date.ts +112 -0
  313. package/src/data/query/pg-core/columns/double-precision.ts +55 -0
  314. package/src/data/query/pg-core/columns/enum.ts +201 -0
  315. package/src/data/query/pg-core/columns/index.ts +35 -0
  316. package/src/data/query/pg-core/columns/inet.ts +43 -0
  317. package/src/data/query/pg-core/columns/int.common.ts +57 -0
  318. package/src/data/query/pg-core/columns/integer.ts +53 -0
  319. package/src/data/query/pg-core/columns/interval.ts +83 -0
  320. package/src/data/query/pg-core/columns/json.ts +58 -0
  321. package/src/data/query/pg-core/columns/jsonb.ts +56 -0
  322. package/src/data/query/pg-core/columns/line.ts +117 -0
  323. package/src/data/query/pg-core/columns/macaddr.ts +43 -0
  324. package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
  325. package/src/data/query/pg-core/columns/numeric.ts +227 -0
  326. package/src/data/query/pg-core/columns/point.ts +127 -0
  327. package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
  328. package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
  329. package/src/data/query/pg-core/columns/real.ts +58 -0
  330. package/src/data/query/pg-core/columns/serial.ts +55 -0
  331. package/src/data/query/pg-core/columns/smallint.ts +53 -0
  332. package/src/data/query/pg-core/columns/smallserial.ts +60 -0
  333. package/src/data/query/pg-core/columns/text.ts +67 -0
  334. package/src/data/query/pg-core/columns/time.ts +72 -0
  335. package/src/data/query/pg-core/columns/timestamp.ts +163 -0
  336. package/src/data/query/pg-core/columns/uuid.ts +51 -0
  337. package/src/data/query/pg-core/columns/varchar.ts +89 -0
  338. package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
  339. package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
  340. package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
  341. package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
  342. package/src/data/query/pg-core/db.ts +674 -0
  343. package/src/data/query/pg-core/dialect.ts +1404 -0
  344. package/src/data/query/pg-core/expressions.ts +25 -0
  345. package/src/data/query/pg-core/foreign-keys.ts +130 -0
  346. package/src/data/query/pg-core/index.ts +21 -0
  347. package/src/data/query/pg-core/indexes.ts +276 -0
  348. package/src/data/query/pg-core/policies.ts +56 -0
  349. package/src/data/query/pg-core/primary-keys.ts +62 -0
  350. package/src/data/query/pg-core/query-builders/count.ts +86 -0
  351. package/src/data/query/pg-core/query-builders/delete.ts +293 -0
  352. package/src/data/query/pg-core/query-builders/index.ts +7 -0
  353. package/src/data/query/pg-core/query-builders/insert.ts +452 -0
  354. package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
  355. package/src/data/query/pg-core/query-builders/query.ts +157 -0
  356. package/src/data/query/pg-core/query-builders/raw.ts +49 -0
  357. package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
  358. package/src/data/query/pg-core/query-builders/select.ts +1346 -0
  359. package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
  360. package/src/data/query/pg-core/query-builders/update.ts +626 -0
  361. package/src/data/query/pg-core/roles.ts +41 -0
  362. package/src/data/query/pg-core/schema.ts +72 -0
  363. package/src/data/query/pg-core/sequence.ts +41 -0
  364. package/src/data/query/pg-core/session.ts +292 -0
  365. package/src/data/query/pg-core/subquery.ts +46 -0
  366. package/src/data/query/pg-core/table.ts +252 -0
  367. package/src/data/query/pg-core/unique-constraint.ts +73 -0
  368. package/src/data/query/pg-core/utils/array.ts +95 -0
  369. package/src/data/query/pg-core/utils/index.ts +1 -0
  370. package/src/data/query/pg-core/utils.ts +104 -0
  371. package/src/data/query/pg-core/view-base.ts +14 -0
  372. package/src/data/query/pg-core/view-common.ts +1 -0
  373. package/src/data/query/pg-core/view.ts +435 -0
  374. package/src/data/query/pg-core/with-builder.ts +41 -0
  375. package/src/data/query/postgres-js/README.md +46 -0
  376. package/src/data/query/postgres-js/array-parser.ts +45 -0
  377. package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
  378. package/src/data/query/postgres-js/driver.ts +163 -0
  379. package/src/data/query/postgres-js/index.ts +2 -0
  380. package/src/data/query/postgres-js/session.ts +234 -0
  381. package/src/data/query/primary-key.ts +11 -0
  382. package/src/data/query/query-builders/query-builder.ts +19 -0
  383. package/src/data/query/query-builders/select.types.ts +175 -0
  384. package/src/data/query/query-promise.ts +35 -0
  385. package/src/data/query/relations.ts +725 -0
  386. package/src/data/query/runnable-query.ts +12 -0
  387. package/src/data/query/selection-proxy.ts +121 -0
  388. package/src/data/query/session.ts +8 -0
  389. package/src/data/query/sql/expressions/conditions.ts +741 -0
  390. package/src/data/query/sql/expressions/index.ts +2 -0
  391. package/src/data/query/sql/expressions/select.ts +45 -0
  392. package/src/data/query/sql/functions/aggregate.ts +129 -0
  393. package/src/data/query/sql/functions/index.ts +2 -0
  394. package/src/data/query/sql/functions/vector.ts +178 -0
  395. package/src/data/query/sql/index.ts +3 -0
  396. package/src/data/query/sql/sql.ts +794 -0
  397. package/src/data/query/subquery.ts +53 -0
  398. package/src/data/query/table.ts +208 -0
  399. package/src/data/query/table.utils.ts +2 -0
  400. package/src/data/query/tracing-utils.ts +3 -0
  401. package/src/data/query/tracing.ts +11 -0
  402. package/src/data/query/utils.ts +357 -0
  403. package/src/data/query/version.ts +4 -0
  404. package/src/data/query/view-common.ts +1 -0
  405. package/src/data/schema/branded.ts +13 -0
  406. package/src/data/schema/generated/coerce.ts +5 -0
  407. package/src/data/schema/generated/combinator.ts +6 -0
  408. package/src/data/schema/generated/encoding.ts +5 -0
  409. package/src/data/schema/generated/error.ts +6 -0
  410. package/src/data/schema/generated/format.ts +5 -0
  411. package/src/data/schema/generated/type-constructor.ts +10 -0
  412. package/src/data/schema/generated/type.ts +6 -0
  413. package/src/data/schema/index.ts +6 -0
  414. package/src/data/schema/json-schema.ts +33 -0
  415. package/src/data/schema/namespace.ts +55 -0
  416. package/src/data/schema/parse.ts +39 -0
  417. package/src/data/schema/types.ts +39 -0
  418. package/src/data/temporal/append-only.ts +134 -0
  419. package/src/data/temporal/close.ts +27 -0
  420. package/src/data/temporal/columns.ts +10 -0
  421. package/src/data/temporal/definition-queries.ts +70 -0
  422. package/src/data/temporal/errors.ts +10 -0
  423. package/src/data/temporal/get-current.ts +22 -0
  424. package/src/data/temporal/index.ts +9 -0
  425. package/src/data/temporal/namespace.ts +36 -0
  426. package/src/data/temporal/query.ts +25 -0
  427. package/src/data/temporal/resolve.ts +17 -0
  428. package/src/data/temporal/revise.ts +67 -0
  429. package/src/data/temporal/types.ts +22 -0
  430. package/src/data/valve/artifact.ts +33 -0
  431. package/src/data/valve/contract-diff.ts +21 -0
  432. package/src/data/valve/cross.ts +57 -0
  433. package/src/data/valve/crossing-ledger.ts +50 -0
  434. package/src/data/valve/decision.ts +44 -0
  435. package/src/data/valve/durable-crossing.ts +52 -0
  436. package/src/data/valve/execute-crossing.ts +77 -0
  437. package/src/data/valve/faucet.ts +56 -0
  438. package/src/data/valve/gate.ts +114 -0
  439. package/src/data/valve/graph.ts +77 -0
  440. package/src/data/valve/index.ts +53 -0
  441. package/src/data/valve/namespace.ts +57 -0
  442. package/src/data/valve/registry.ts +50 -0
  443. package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
  444. package/src/data/valve/schema/valve-crossing.ts +9 -0
  445. package/src/data/valve/supply.ts +154 -0
  446. package/src/data/valve/types.ts +143 -0
  447. package/src/data/vector/columns.ts +27 -0
  448. package/src/data/vector/index.ts +4 -0
  449. package/src/data/vector/namespace.ts +33 -0
  450. package/src/data/vector/query.ts +85 -0
  451. package/src/data/vector/validate.ts +16 -0
  452. package/src/data/write/audit/actor.ts +15 -0
  453. package/src/data/write/audit/audit-columns.ts +18 -0
  454. package/src/data/write/audit/audit-select.ts +111 -0
  455. package/src/data/write/audit/bound-transaction.ts +14 -0
  456. package/src/data/write/audit/cte-compile.ts +32 -0
  457. package/src/data/write/audit/decode-returning.ts +47 -0
  458. package/src/data/write/audit/final-select.ts +43 -0
  459. package/src/data/write/audit/infra-audit.ts +36 -0
  460. package/src/data/write/audit/primary-key.ts +65 -0
  461. package/src/data/write/audit/projection.ts +41 -0
  462. package/src/data/write/audit/read-touches.ts +86 -0
  463. package/src/data/write/audit/touch-match.ts +78 -0
  464. package/src/data/write/audit/verb-delete.ts +62 -0
  465. package/src/data/write/audit/verb-insert.ts +36 -0
  466. package/src/data/write/audit/verb-select.ts +47 -0
  467. package/src/data/write/audit/verb-update.ts +60 -0
  468. package/src/data/write/audit/verb-upsert.ts +104 -0
  469. package/src/data/write/conflict.ts +43 -0
  470. package/src/data/write/delete.ts +38 -0
  471. package/src/data/write/execute.ts +243 -0
  472. package/src/data/write/index.ts +11 -0
  473. package/src/data/write/insert.ts +63 -0
  474. package/src/data/write/state.ts +43 -0
  475. package/src/data/write/update.ts +54 -0
  476. package/src/data/write/upsert.ts +58 -0
  477. package/src/data/write/values.ts +19 -0
  478. package/src/index.ts +230 -0
  479. package/src/infra/access-links/code.ts +70 -0
  480. package/src/infra/access-links/handle.ts +892 -0
  481. package/src/infra/access-links/index.ts +9 -0
  482. package/src/infra/access-links/namespace.ts +30 -0
  483. package/src/infra/access-links/person-check.ts +177 -0
  484. package/src/infra/access-links/schema.ts +71 -0
  485. package/src/infra/alerts/async-sources.ts +231 -0
  486. package/src/infra/alerts/audit.ts +36 -0
  487. package/src/infra/alerts/beat.ts +123 -0
  488. package/src/infra/alerts/client-faults.ts +111 -0
  489. package/src/infra/alerts/faults.ts +156 -0
  490. package/src/infra/alerts/handle.ts +778 -0
  491. package/src/infra/alerts/http.ts +192 -0
  492. package/src/infra/alerts/index.ts +12 -0
  493. package/src/infra/alerts/infra-sources.ts +180 -0
  494. package/src/infra/alerts/namespace.ts +81 -0
  495. package/src/infra/alerts/redact.ts +21 -0
  496. package/src/infra/alerts/request-faults.ts +41 -0
  497. package/src/infra/alerts/schema.ts +124 -0
  498. package/src/infra/alerts/sinks.ts +125 -0
  499. package/src/infra/alerts/source-options.ts +26 -0
  500. package/src/infra/alerts/sources.ts +503 -0
  501. package/src/infra/alerts/subsystem-sources.ts +137 -0
  502. package/src/infra/audit/emit.ts +87 -0
  503. package/src/infra/audit/index.ts +2 -0
  504. package/src/infra/audit/namespace.ts +22 -0
  505. package/src/infra/audit/schema.sql +35 -0
  506. package/src/infra/audit/types.ts +18 -0
  507. package/src/infra/aws/credentials.ts +147 -0
  508. package/src/infra/aws/sigv4.ts +140 -0
  509. package/src/infra/cache/create.ts +58 -0
  510. package/src/infra/cache/entry.ts +81 -0
  511. package/src/infra/cache/index.ts +4 -0
  512. package/src/infra/cache/namespace.ts +10 -0
  513. package/src/infra/cache/tenant.ts +93 -0
  514. package/src/infra/cache/types.ts +24 -0
  515. package/src/infra/crawl/audit.ts +35 -0
  516. package/src/infra/crawl/errors.ts +25 -0
  517. package/src/infra/crawl/handle.ts +693 -0
  518. package/src/infra/crawl/index.ts +9 -0
  519. package/src/infra/crawl/links.ts +160 -0
  520. package/src/infra/crawl/namespace.ts +36 -0
  521. package/src/infra/crawl/robots.ts +100 -0
  522. package/src/infra/crawl/schema.ts +89 -0
  523. package/src/infra/credit-line/handle.ts +883 -0
  524. package/src/infra/credit-line/index.ts +6 -0
  525. package/src/infra/credit-line/namespace.ts +7 -0
  526. package/src/infra/credit-line/schema.ts +96 -0
  527. package/src/infra/credits/handle.ts +706 -0
  528. package/src/infra/credits/index.ts +5 -0
  529. package/src/infra/credits/namespace.ts +7 -0
  530. package/src/infra/credits/schema.ts +83 -0
  531. package/src/infra/email/audit.ts +36 -0
  532. package/src/infra/email/inbound.ts +622 -0
  533. package/src/infra/email/index.ts +11 -0
  534. package/src/infra/email/mail.ts +517 -0
  535. package/src/infra/email/mime.ts +447 -0
  536. package/src/infra/email/namespace.ts +22 -0
  537. package/src/infra/email/providers.ts +111 -0
  538. package/src/infra/email/schema.ts +96 -0
  539. package/src/infra/email/sns.ts +200 -0
  540. package/src/infra/email/unsubscribe.ts +44 -0
  541. package/src/infra/fetch/address.ts +88 -0
  542. package/src/infra/fetch/connect-retry.ts +87 -0
  543. package/src/infra/fetch/handle.ts +329 -0
  544. package/src/infra/fetch/index.ts +7 -0
  545. package/src/infra/fetch/namespace.ts +70 -0
  546. package/src/infra/idempotency/index.ts +3 -0
  547. package/src/infra/idempotency/middleware.ts +87 -0
  548. package/src/infra/idempotency/store.ts +92 -0
  549. package/src/infra/idempotency/types.ts +30 -0
  550. package/src/infra/join-requests/handle.ts +312 -0
  551. package/src/infra/join-requests/index.ts +2 -0
  552. package/src/infra/join-requests/namespace.ts +7 -0
  553. package/src/infra/join-requests/schema.ts +33 -0
  554. package/src/infra/outbox/core.ts +165 -0
  555. package/src/infra/quota/errors.ts +32 -0
  556. package/src/infra/quota/handle.ts +215 -0
  557. package/src/infra/quota/index.ts +5 -0
  558. package/src/infra/quota/namespace.ts +7 -0
  559. package/src/infra/quota/schema.ts +15 -0
  560. package/src/infra/storage/backend.ts +34 -0
  561. package/src/infra/storage/handle.ts +513 -0
  562. package/src/infra/storage/index.ts +9 -0
  563. package/src/infra/storage/local.ts +104 -0
  564. package/src/infra/storage/namespace.ts +13 -0
  565. package/src/infra/storage/s3.ts +257 -0
  566. package/src/infra/storage/schema.ts +24 -0
  567. package/src/infra/support/handle.ts +854 -0
  568. package/src/infra/support/http.ts +229 -0
  569. package/src/infra/support/index.ts +7 -0
  570. package/src/infra/support/namespace.ts +12 -0
  571. package/src/infra/support/schema.ts +96 -0
  572. package/src/infra/webhook/inbound.ts +45 -0
  573. package/src/infra/webhook/index.ts +5 -0
  574. package/src/infra/webhook/namespace.ts +24 -0
  575. package/src/infra/webhook/outbound.ts +315 -0
  576. package/src/infra/webhook/sign.ts +32 -0
  577. package/src/infra/webhook/verify.ts +82 -0
  578. package/src/invariants/assert.ts +26 -0
  579. package/src/invariants/index.ts +1 -0
  580. package/src/lint/config.ts +56 -0
  581. package/src/lint/doctrine.ts +163 -0
  582. package/src/lint/include.ts +97 -0
  583. package/src/lint/index.ts +3 -0
  584. package/src/lint/preset.ts +10 -0
  585. package/src/lint/resolver.ts +218 -0
  586. package/src/lint/rules/no-reexports.ts +46 -0
  587. package/src/request/behavior/audited.ts +85 -0
  588. package/src/request/behavior/cached.ts +62 -0
  589. package/src/request/behavior/compose.ts +28 -0
  590. package/src/request/behavior/index.ts +5 -0
  591. package/src/request/behavior/namespace.ts +16 -0
  592. package/src/request/behavior/types.ts +26 -0
  593. package/src/request/behavior/versioned.ts +54 -0
  594. package/src/request/context/create.ts +103 -0
  595. package/src/request/context/guard.ts +49 -0
  596. package/src/request/context/index.ts +4 -0
  597. package/src/request/context/run-in-transaction.ts +200 -0
  598. package/src/request/context/store.ts +28 -0
  599. package/src/request/context/types.ts +50 -0
  600. package/src/request/di/builder.ts +535 -0
  601. package/src/request/di/index.ts +8 -0
  602. package/src/request/di/resolve.ts +271 -0
  603. package/src/request/di/types.ts +262 -0
  604. package/src/request/fitting/namespace.ts +7 -0
  605. package/src/request/fixture/batch.ts +71 -0
  606. package/src/request/fixture/builder.ts +26 -0
  607. package/src/request/fixture/errors.ts +10 -0
  608. package/src/request/fixture/index.ts +8 -0
  609. package/src/request/fixture/namespace.ts +23 -0
  610. package/src/request/fixture/paginate.ts +44 -0
  611. package/src/request/fixture/to-handlers.ts +260 -0
  612. package/src/request/fixture/types.ts +79 -0
  613. package/src/request/flow/act-as.ts +78 -0
  614. package/src/request/flow/namespace.ts +17 -0
  615. package/src/request/http/error-step.ts +42 -0
  616. package/src/request/http/file.ts +141 -0
  617. package/src/request/http/index.ts +4 -0
  618. package/src/request/http/middleware.ts +141 -0
  619. package/src/request/http/namespace.ts +5 -0
  620. package/src/request/http/rate-limit-classes.ts +170 -0
  621. package/src/request/http/response.ts +82 -0
  622. package/src/request/http/server.ts +916 -0
  623. package/src/request/http/sniff.ts +261 -0
  624. package/src/request/http/types.ts +250 -0
  625. package/src/request/http/upload.ts +284 -0
  626. package/src/request/internal/text.ts +6 -0
  627. package/src/request/openapi/generate.ts +205 -0
  628. package/src/request/openapi/index.ts +2 -0
  629. package/src/request/openapi/namespace.ts +7 -0
  630. package/src/test/auth.ts +13 -0
  631. package/src/test/auto-setup.ts +45 -0
  632. package/src/test/context.ts +91 -0
  633. package/src/test/crawl-fixture.ts +157 -0
  634. package/src/test/discover-fork.ts +17 -0
  635. package/src/test/fingerprint.ts +135 -0
  636. package/src/test/globals.d.ts +7 -0
  637. package/src/test/index.ts +19 -0
  638. package/src/test/namespace.ts +87 -0
  639. package/src/test/plugin.ts +189 -0
  640. package/src/test/progress.ts +185 -0
  641. package/src/test/reap-after-run.ts +28 -0
  642. package/src/test/reap.ts +286 -0
  643. package/src/test/reset.ts +253 -0
  644. package/src/test/savepoints.ts +172 -0
  645. package/src/test/setup.ts +1422 -0
  646. package/src/test/system-channel.ts +50 -0
  647. package/src/test/vitest-plugin.ts +142 -0
  648. package/src/test/vitest-workspace.ts +507 -0
  649. package/src/test/vitest.ts +333 -0
  650. package/src/test/watch.ts +105 -0
  651. package/src/trace/capture/bounded-serializer.ts +146 -0
  652. package/src/trace/cardinality/analyze.ts +173 -0
  653. package/src/trace/cardinality/branch-context.ts +214 -0
  654. package/src/trace/cardinality/check.ts +33 -0
  655. package/src/trace/cardinality/classify-edge.ts +64 -0
  656. package/src/trace/cardinality/structural-max.ts +107 -0
  657. package/src/trace/check/coverage-check.ts +188 -0
  658. package/src/trace/check/flow-check.ts +103 -0
  659. package/src/trace/check/flow-step-site.ts +234 -0
  660. package/src/trace/check/io-pattern.ts +116 -0
  661. package/src/trace/codemod/add-cardinality.ts +152 -0
  662. package/src/trace/codemod/run.ts +80 -0
  663. package/src/trace/entry/child-trace.ts +21 -0
  664. package/src/trace/entry/close-trace.ts +87 -0
  665. package/src/trace/entry/finalize-trace.ts +28 -0
  666. package/src/trace/entry/open-trace.ts +138 -0
  667. package/src/trace/entry/parent-trace.ts +20 -0
  668. package/src/trace/entry/parse-traceparent.ts +39 -0
  669. package/src/trace/entry/process-start.ts +38 -0
  670. package/src/trace/entry/trace-context.ts +26 -0
  671. package/src/trace/entry/tracing-enabled.ts +10 -0
  672. package/src/trace/external/record.ts +195 -0
  673. package/src/trace/external/report.ts +66 -0
  674. package/src/trace/namespace.ts +19 -0
  675. package/src/trace/partition/day.ts +47 -0
  676. package/src/trace/partition/index.ts +14 -0
  677. package/src/trace/partition/partition-name.ts +23 -0
  678. package/src/trace/partition/plan.ts +82 -0
  679. package/src/trace/partition/schedule.ts +26 -0
  680. package/src/trace/partition/startup-hook.ts +39 -0
  681. package/src/trace/partition/sync.ts +154 -0
  682. package/src/trace/partition/tables.ts +53 -0
  683. package/src/trace/read/tree.ts +83 -0
  684. package/src/trace/retain/copy-on-error.ts +76 -0
  685. package/src/trace/runtime/expire-stale-traces.ts +54 -0
  686. package/src/trace/runtime/flow-step.ts +147 -0
  687. package/src/trace/runtime/flush-steps.ts +31 -0
  688. package/src/trace/runtime/pending-writes.ts +54 -0
  689. package/src/trace/runtime/step-buffer.ts +298 -0
  690. package/src/trace/runtime/step-error.ts +21 -0
  691. package/src/trace/runtime/step-meta.ts +55 -0
  692. package/src/trace/runtime/thenable.ts +12 -0
  693. package/src/trace/runtime/truncate-trace.ts +62 -0
  694. package/src/trace/schema/trace-fields.ts +52 -0
  695. package/src/trace/schema/trace-retained.ts +22 -0
  696. package/src/trace/schema/trace-step-fields.ts +44 -0
  697. package/src/trace/schema/trace-step-retained.ts +25 -0
  698. package/src/trace/schema/trace-step.ts +18 -0
  699. package/src/trace/schema/trace.ts +12 -0
  700. package/src/trace/transformer/apply.ts +79 -0
  701. package/src/trace/transformer/build-meta-literal.ts +30 -0
  702. package/src/trace/transformer/find-flow-step-sites.ts +85 -0
  703. package/src/trace/transformer/inject-flow-step-import.ts +39 -0
  704. package/src/trace/transformer/transform-error.ts +14 -0
  705. package/src/trace/transformer/transform-flow-steps.ts +123 -0
  706. package/src/trace/transformer/wrap-function-declaration.ts +62 -0
  707. package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
  708. package/src/workspace/affected.ts +110 -0
  709. package/src/workspace/changed-files.ts +163 -0
  710. package/src/workspace/check-deps.ts +213 -0
  711. package/src/workspace/checksum.ts +20 -0
  712. package/src/workspace/dependency-graph.ts +67 -0
  713. package/src/workspace/detect-pipework-copies.ts +93 -0
  714. package/src/workspace/discover.ts +206 -0
  715. package/src/workspace/generate.ts +401 -0
  716. package/src/workspace/index.ts +7 -0
  717. package/src/workspace/module-imports-pipework.ts +39 -0
  718. package/src/workspace/module-package-name.ts +12 -0
  719. package/src/workspace/scrub-git-env.ts +23 -0
  720. package/src/workspace/source-paths.ts +36 -0
  721. package/src/workspace/sync.ts +182 -0
  722. package/dist/REFERENCE.md +0 -1477
  723. package/dist/infra/access-links/rate-limit.d.ts +0 -10
  724. package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
  725. package/dist/infra/access-links/rate-limit.js +0 -28
  726. package/dist/infra/access-links/rate-limit.js.map +0 -1
  727. package/dist/infra/email/sigv4.d.ts +0 -24
  728. package/dist/infra/email/sigv4.d.ts.map +0 -1
  729. package/dist/infra/email/sigv4.js +0 -55
  730. package/dist/infra/email/sigv4.js.map +0 -1
@@ -0,0 +1,517 @@
1
+ import { sql } from '../../data/db/sql.js'
2
+ import type { DB } from '../../data/db/index.js'
3
+ import { pipeSystem } from '../../data/db/db.js'
4
+ import { insert, remove } from '../../data/write/index.js'
5
+ import { schema } from '../../data/schema/namespace.js'
6
+ import { fitting } from '../../request/di/index.js'
7
+ import { Builder } from '../../request/di/builder.js'
8
+ import { and, eq } from '../../data/query/index.js'
9
+ import { getContext } from '../../request/context/store.js'
10
+ import { ValidationError } from '../../core/errors/index.js'
11
+ import { getBaseLogger } from '../../core/logging/index.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 { auditEmail } from './audit.js'
16
+ import type { MailProvider } from './providers.js'
17
+ import { unscopedExecutor, requireUnscopedExecutor } from '../../auth/tenant/scoped-db.js'
18
+ import { describeError } from '../../core/logging/error-chain.js'
19
+ import { emailUnsubscribes } from './schema.js'
20
+ import { LIST_RE, MIN_UNSUBSCRIBE_SECRET, signUnsubscribeToken, unsubscribeLink, verifyUnsubscribeToken } from './unsubscribe.js'
21
+
22
+ /** What a template renders: the subject and the text body, optionally an HTML body. */
23
+ export interface RenderedEmail {
24
+ readonly subject: string
25
+ readonly text: string
26
+ readonly html?: string
27
+ }
28
+
29
+ /** What a template learns about the message it renders, as its second argument. */
30
+ export interface EmailRenderMeta {
31
+ /** The message's one-click unsubscribe link (the same one its `List-Unsubscribe` header carries); null for a message with no `list`. */
32
+ readonly unsubscribeUrl: string | null
33
+ }
34
+
35
+ /** The app's templates: a function per name from the data (and the render meta) to the rendered message. */
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- each template has its own data type; `send` checks it per name
37
+ export type EmailTemplates = Record<string, (data: any, meta: EmailRenderMeta) => RenderedEmail>
38
+
39
+ export interface MailConfig<T extends EmailTemplates> {
40
+ readonly provider: MailProvider
41
+ readonly templates: T
42
+ /** The From address, e.g. `Verity <no-reply@mail.verity.pub>`. */
43
+ readonly from: string
44
+ /** The app's job queue; its worker surface must run `mail.jobs`. */
45
+ readonly queue: Queue
46
+ readonly configurationSet?: string
47
+ /** The app's environment. `devStandIn` is refused when this is `production`. */
48
+ readonly environment?: string
49
+ /** Seconds after which a message left `sending` by a crashed run may be sent again. Default 600. */
50
+ readonly sendingStaleSeconds?: number
51
+ /** The queue priority of this module's delivery jobs; higher is claimed first. Default: the queue's (0). */
52
+ readonly priority?: number
53
+ /**
54
+ * One-click unsubscribe for mail sent with a `list`. `url` is the absolute https URL of the route `unsubscribeHttp()`
55
+ * serves (http is allowed outside production); `secret` signs the links, at least 32 characters and the same in every
56
+ * process. A `send` with a `list` throws when this is unset.
57
+ */
58
+ readonly unsubscribe?: { readonly url: string; readonly secret: string }
59
+ }
60
+
61
+ export interface SendInput<T extends EmailTemplates, K extends keyof T & string> {
62
+ readonly to: string
63
+ readonly template: K
64
+ readonly data: Parameters<T[K]>[0]
65
+ /** Idempotency key: a second send with the same key sends nothing. */
66
+ readonly key: string
67
+ /**
68
+ * Puts the message in a conversation, so a reply to it threads there (the provider's message id
69
+ * is matched against `References`/`In-Reply-To` of received mail). Default: the conversation of `inReplyTo`.
70
+ */
71
+ readonly conversationId?: string
72
+ /**
73
+ * The id of a received message (`ReceivedEmail.id`) this one answers: the sent message carries
74
+ * `In-Reply-To: <its Message-ID>` and `References` with its references plus its Message-ID (at most 20 ids).
75
+ */
76
+ readonly inReplyTo?: string
77
+ /** The queue priority of this message's delivery job; higher is claimed first. Default: the module's `priority`. */
78
+ readonly priority?: number
79
+ /**
80
+ * Data computed when the message is delivered, never stored: `name` is a function registered with
81
+ * `registerLateData`, `ref` the reference it receives. Its result is merged over `data` for rendering only.
82
+ * The stored row holds `name` and `ref` under the reserved data key `__late`. Delivery fails if `name` is not registered.
83
+ */
84
+ readonly lateData?: { readonly name: string; readonly ref: string }
85
+ /**
86
+ * Puts the message on a list, e.g. `'notice-digest'` (letters, digits, `.`, `_`, `-`; at most 64). It then carries
87
+ * RFC 8058 `List-Unsubscribe` and `List-Unsubscribe-Post` headers, and is skipped at delivery (status `unsubscribed`,
88
+ * no provider call) when the address has opted out of that list. Needs `MailConfig.unsubscribe`. Leave it off for
89
+ * transactional mail: that is never affected by an opt-out.
90
+ */
91
+ readonly list?: string
92
+ }
93
+
94
+ /** Computes the template data a message gets at delivery from the reference stored with it. */
95
+ export type LateData = (db: DB, ref: string) => Promise<Record<string, unknown>>
96
+
97
+ export interface SendResult {
98
+ readonly messageId: string
99
+ /** False when the key was already used; `messageId` is then the earlier message. */
100
+ readonly queued: boolean
101
+ }
102
+
103
+ /** A message as `mail.sentTo` reads it. `rendered` is null when its template is gone or throws. */
104
+ export interface SentMail {
105
+ readonly id: string
106
+ readonly template: string
107
+ readonly status: string
108
+ /** The data stored with the message, without the `__late` reference. */
109
+ readonly data: unknown
110
+ readonly createdAt: Date
111
+ readonly rendered: RenderedEmail | null
112
+ }
113
+
114
+ export interface Mail<T extends EmailTemplates> {
115
+ /**
116
+ * Records the message and enqueues its send in `db`, so the caller's rollback drops both.
117
+ * The same `key` twice gives one message, one job and one delivery.
118
+ */
119
+ send<K extends keyof T & string>(db: DB, input: SendInput<T, K>): Promise<SendResult>
120
+ /**
121
+ * For tests and support tools: the messages to an address (any case), oldest first, optionally of one template,
122
+ * each rendered from its stored data (late data resolved) as delivery would render it.
123
+ */
124
+ sentTo(db: DB, address: string, options?: { readonly template?: keyof T & string }): Promise<readonly SentMail[]>
125
+ /**
126
+ * Records that `address` (any case) opted out of `list`; a second call changes nothing. Later sends on that list to
127
+ * that address are skipped; mail on other lists and mail with no list is unaffected. Writes through `pipe.insert`
128
+ * (needs a Flow), so it is audited when the calling step declares a touch on `pipework_email_unsubscribes`.
129
+ */
130
+ unsubscribe(db: DB, address: string, list: string): Promise<void>
131
+ /** True when `address` (any case) has opted out of `list`. */
132
+ unsubscribed(db: DB, address: string, list: string): Promise<boolean>
133
+ /** Removes the opt-out of `address` from `list`, so its mail is delivered again. Writes like `unsubscribe`. */
134
+ resubscribe(db: DB, address: string, list: string): Promise<void>
135
+ /**
136
+ * The routes behind the unsubscribe link, at the path of `MailConfig.unsubscribe.url`: GET shows a confirm page
137
+ * with a form, POST (the mail provider's one-click, form-urlencoded, multipart or empty body, never read) records
138
+ * the opt-out. A token that is not valid for this secret is a 400 page. Public, no tenant; the address is
139
+ * escaped on the page and never logged. Mount it on the HTTP surface like `alerts.http`. Throws without `unsubscribe` config.
140
+ */
141
+ unsubscribeHttp(): Handler<Record<string, unknown>, unknown>[]
142
+ /** Registers (or replaces) a late-data function by name; see `SendInput.lateData`. Used by other pipework modules. */
143
+ registerLateData(name: string, fn: LateData): void
144
+ /** Job handlers for the send job; spread into the worker surface's `jobs`. */
145
+ readonly jobs: readonly Handler<Record<string, unknown>, unknown>[]
146
+ }
147
+
148
+ export const SEND_JOB = 'pipework.email.send'
149
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
150
+ const MAX_REFERENCES = 20
151
+ const MAX_REFERENCES_CHARS = 900
152
+ const ADDRESS_RE = /^[^\s@<>,;"]+@[^\s@<>,;"]+$/
153
+
154
+ /** The reply headers for a message answering a received one: In-Reply-To, and References capped to 20 ids and the provider's header length. */
155
+ export function replyHeaders(messageId: string, references: readonly string[]): Record<string, string> {
156
+ const ids = [...references, messageId].filter(id => /^[\x21-\x7e]+$/.test(id) && !/[<>]/.test(id))
157
+ const unique = ids.filter((id, i) => ids.lastIndexOf(id) === i).slice(-MAX_REFERENCES)
158
+ while (unique.length > 1 && unique.map(i => `<${i}>`).join(' ').length > MAX_REFERENCES_CHARS) unique.shift()
159
+ return { 'In-Reply-To': `<${messageId}>`, 'References': unique.map(i => `<${i}>`).join(' ') }
160
+ }
161
+
162
+ function lateReference(data: unknown): { name: string; ref: string } | null {
163
+ if (typeof data !== 'object' || data === null) return null
164
+ const late = (data as Record<string, unknown>)['__late']
165
+ if (typeof late !== 'object' || late === null) return null
166
+ const { name, ref } = late as Record<string, unknown>
167
+ return typeof name === 'string' && typeof ref === 'string' ? { name, ref } : null
168
+ }
169
+
170
+ function storedData(input: { data?: unknown; lateData?: { name: string; ref: string } }): unknown {
171
+ if (input.lateData === undefined) return input.data ?? null
172
+ const base = typeof input.data === 'object' && input.data !== null ? input.data : {}
173
+ return { ...base, __late: { name: input.lateData.name, ref: input.lateData.ref } }
174
+ }
175
+
176
+ export function normalizeAddress(address: string): string {
177
+ return address.trim().toLowerCase()
178
+ }
179
+
180
+ /**
181
+ * Creates the transactional email interface.
182
+ *
183
+ * `send(db, { to, template, data, key })` writes a `pipework_email_messages` row and
184
+ * enqueues a `pipework.email.send` job in the caller's transaction. The job renders the
185
+ * app's template and sends through the provider, at most once per key, skipping an
186
+ * address on the suppression list (status `suppressed`, no provider call), and a message on a `list` whose address opted out of it (status `unsubscribed`, no provider call). The
187
+ * provider call is a trace step and `Email:sent` / `Email:suppressed` / `Email:unsubscribed` / `Email:failed`
188
+ * audit records are written under it, in the trace of the flow that called `send`.
189
+ *
190
+ * Needs blueprint `email: {}` (tables `pipework_email_messages`, `pipework_email_suppressions`, `pipework_email_inbound`, `pipework_email_unsubscribes`).
191
+ * A provider error returns the message to `queued` with `error` set and rethrows, so the
192
+ * queue retries (set `maxAttempts` on the queue); if the queue gives up the row keeps the error.
193
+ * A run claims the message with a conditional update (`status` `queued`, or `sending` for longer than
194
+ * `sendingStaleSeconds`), so a re-run of a job whose first run is still in flight does not call the provider.
195
+ * Residual window: a crash mid-send leaves the message `sending` for the stale window, after which a retry
196
+ * may send it once more; likewise if the provider accepted it and the process died before the row was marked `sent`. SES v2 has no idempotency token to close it.
197
+ * With `conversationId` / `inReplyTo` the message joins a conversation and carries reply headers (see `SendInput`).
198
+ * Message bodies and template data are never logged.
199
+ */
200
+ export function createMail<T extends EmailTemplates>(config: MailConfig<T>): Mail<T> {
201
+ if (config.provider.name === 'dev-stand-in' && config.environment === 'production') {
202
+ throw new Error(
203
+ '[pipework] mail.devStandIn is refused in production.\n\n' +
204
+ ' The stand-in writes messages to files instead of sending them.\n' +
205
+ ' Configure a real provider, e.g. mail.ses({ region, credentials }).\n',
206
+ )
207
+ }
208
+
209
+ const unsubscribe = config.unsubscribe
210
+ if (unsubscribe !== undefined) validateUnsubscribe(unsubscribe, config.environment)
211
+
212
+ const staleSeconds = config.sendingStaleSeconds ?? 600
213
+ const lateFns = new Map<string, LateData>()
214
+
215
+ async function isUnsubscribed(db: DB, address: string, list: string): Promise<boolean> {
216
+ const rows = await db.execute(sql`SELECT 1 FROM pipework_email_unsubscribes WHERE address = ${normalizeAddress(address)} AND list = ${list}`)
217
+ return rows.length > 0
218
+ }
219
+
220
+ /** What a template is told: the signed link for a list message, null for any other. */
221
+ function renderMeta(messageId: string, to: string, list: string | null): EmailRenderMeta {
222
+ if (list === null) return { unsubscribeUrl: null }
223
+ if (unsubscribe === undefined) {
224
+ throw new Error(`[pipework] Email message ${messageId} is on list "${list}" but mail config has no unsubscribe: { url, secret }; it is not sent without an unsubscribe link.`)
225
+ }
226
+ return { unsubscribeUrl: unsubscribeLink(unsubscribe.url, signUnsubscribeToken(unsubscribe.secret, normalizeAddress(to), list)) }
227
+ }
228
+
229
+ /** The stored data with its late data computed and merged over it; throws when the named function is not registered. */
230
+ async function withLateData(db: DB, stored: unknown, messageId: string): Promise<unknown> {
231
+ const lateRef = lateReference(stored)
232
+ if (lateRef === null) return stored
233
+ const fn = lateFns.get(lateRef.name)
234
+ if (fn === undefined) throw new Error(`[pipework] Email message ${messageId}: no late-data function named "${lateRef.name}" is registered.`)
235
+ const rest = { ...(stored as Record<string, unknown>) }
236
+ delete rest['__late']
237
+ return { ...rest, ...(await fn(db, lateRef.ref)) }
238
+ }
239
+
240
+ const deliver = __flowStep({ ref: 'pipework/email:deliver' }, async (messageId: string): Promise<{ status: string }> => {
241
+ const db = pipeSystem()
242
+ const rows = await db.execute(sql`SELECT * FROM pipework_email_messages WHERE id = ${messageId}::uuid`)
243
+ const row = rows[0]
244
+ if (row === undefined) throw new Error(`[pipework] Email message ${messageId} does not exist.`)
245
+ const status = row['status'] as string
246
+ if (status === 'sent' || status === 'suppressed' || status === 'unsubscribed' || status === 'failed') return { status }
247
+
248
+ const environment = getContext()?.instance.config.environment
249
+ if (config.provider.name === 'dev-stand-in' && environment === 'production') {
250
+ throw new Error('[pipework] mail.devStandIn is refused in production.')
251
+ }
252
+
253
+ const to = row['to_address'] as string
254
+ const suppressed = await db.execute(sql`SELECT reason FROM pipework_email_suppressions WHERE address = ${normalizeAddress(to)}`)
255
+ if (suppressed.length > 0) {
256
+ await db.execute(sql`UPDATE pipework_email_messages SET status = 'suppressed' WHERE id = ${messageId}::uuid`)
257
+ await auditEmail('suppressed', messageId, { reason: suppressed[0]!['reason'] })
258
+ getBaseLogger().info({ event: 'email suppressed send', messageId }, 'email skipped: address is suppressed')
259
+ return { status: 'suppressed' }
260
+ }
261
+
262
+ const list = (row['list'] as string | null | undefined) ?? null
263
+ if (list !== null && await isUnsubscribed(db, to, list)) {
264
+ await db.execute(sql`UPDATE pipework_email_messages SET status = 'unsubscribed' WHERE id = ${messageId}::uuid`)
265
+ await auditEmail('unsubscribed', messageId, { list })
266
+ getBaseLogger().info({ event: 'email unsubscribed send', messageId, list }, 'email skipped: address has opted out of the list')
267
+ return { status: 'unsubscribed' }
268
+ }
269
+ const meta = renderMeta(messageId, to, list)
270
+
271
+ let headers: Record<string, string> | undefined
272
+ const inReplyToInbound = row['in_reply_to_inbound'] as string | null
273
+ if (inReplyToInbound !== null) {
274
+ const parent = (await db.execute(sql`SELECT message_id_header, "references" FROM pipework_email_inbound WHERE id = ${inReplyToInbound}::uuid`))[0]
275
+ const parentId = (parent?.['message_id_header'] as string | null | undefined) ?? null
276
+ if (parentId !== null) headers = replyHeaders(parentId, (parent!['references'] as string[] | null) ?? [])
277
+ }
278
+
279
+ if (meta.unsubscribeUrl !== null) {
280
+ headers = { ...headers, 'List-Unsubscribe': `<${meta.unsubscribeUrl}>`, 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click' }
281
+ }
282
+
283
+ const templateName = row['template'] as string
284
+ const lateRef = lateReference(row['data'])
285
+ let templateData: unknown
286
+ try {
287
+ templateData = await withLateData(db, row['data'], messageId)
288
+ } catch (err) {
289
+ if (lateRef !== null) {
290
+ const message = `no late-data function named "${lateRef.name}" is registered`
291
+ getBaseLogger().error({ event: 'email failed', messageId }, `email not sent: ${message}`)
292
+ }
293
+ throw err
294
+ }
295
+ const template = config.templates[templateName]
296
+ let rendered: RenderedEmail | null = null
297
+ let renderError = ''
298
+ if (template === undefined) {
299
+ renderError = `no template named "${templateName}"`
300
+ } else {
301
+ try {
302
+ rendered = template(templateData, meta)
303
+ } catch (err) {
304
+ renderError = `template "${templateName}" threw: ${describeError(err).message}`
305
+ }
306
+ }
307
+ if (rendered === null) {
308
+ await db.execute(sql`UPDATE pipework_email_messages SET status = 'failed', failed_at = now(), error = ${renderError} WHERE id = ${messageId}::uuid`)
309
+ await auditEmail('failed', messageId, { error: renderError })
310
+ getBaseLogger().error({ event: 'email failed', messageId }, `email not sent: ${renderError}`)
311
+ return { status: 'failed' }
312
+ }
313
+
314
+ const claimed = await db.execute(sql`
315
+ UPDATE pipework_email_messages SET status = 'sending', sending_at = now()
316
+ WHERE id = ${messageId}::uuid
317
+ AND (status = 'queued' OR (status = 'sending' AND sending_at < now() - make_interval(secs => ${staleSeconds})))
318
+ RETURNING id`)
319
+ if (claimed.length === 0) {
320
+ getBaseLogger().info({ event: 'email send held elsewhere', messageId }, 'email not sent: another run holds the message or it is finished')
321
+ return { status: 'held' }
322
+ }
323
+ try {
324
+ const sent = await config.provider.send({
325
+ messageId, from: config.from, to,
326
+ subject: rendered.subject, text: rendered.text,
327
+ ...(rendered.html !== undefined ? { html: rendered.html } : {}),
328
+ ...(config.configurationSet !== undefined ? { configurationSet: config.configurationSet } : {}),
329
+ ...(headers !== undefined ? { headers } : {}),
330
+ })
331
+ await db.execute(sql`
332
+ UPDATE pipework_email_messages
333
+ SET status = 'sent', sending_at = NULL, provider_message_id = ${sent.providerMessageId}, sent_at = now(), error = NULL
334
+ WHERE id = ${messageId}::uuid`)
335
+ await auditEmail('sent', messageId, { provider: config.provider.name, providerMessageId: sent.providerMessageId })
336
+ return { status: 'sent' }
337
+ } catch (err) {
338
+ const message = describeError(err).message
339
+ await db.execute(sql`UPDATE pipework_email_messages SET status = 'queued', sending_at = NULL, error = ${message} WHERE id = ${messageId}::uuid`)
340
+ await auditEmail('failed', messageId, { error: message, willRetry: true })
341
+ throw err
342
+ }
343
+ })
344
+
345
+ const sendJob = fitting.use().job(SEND_JOB)
346
+ .input(schema.check.object({ messageId: schema.check.string() }))
347
+ .fit(async ({ input }: { input: { messageId: string } }) => deliver(input.messageId))
348
+
349
+ async function optOut(_db: DB, address: string, list: string): Promise<void> {
350
+ requireList(list)
351
+ await insert(emailUnsubscribes).values({ address: normalizeAddress(address), list }).onConflictDoNothing()
352
+ }
353
+
354
+ return {
355
+ async send(handle, input) {
356
+ const db = unscopedExecutor(handle).db
357
+ if (config.templates[input.template] === undefined) {
358
+ throw new ValidationError(
359
+ { template: [`no template named "${input.template}" (have: ${Object.keys(config.templates).join(', ')})`] },
360
+ `mail.send: unknown template "${input.template}".`,
361
+ )
362
+ }
363
+ if (!ADDRESS_RE.test(input.to.trim())) {
364
+ throw new ValidationError({ to: ['is not an email address'] }, 'mail.send: "to" is not an email address.')
365
+ }
366
+ if (input.key.trim() === '') {
367
+ throw new ValidationError({ key: ['must not be empty'] }, 'mail.send: "key" must not be empty.')
368
+ }
369
+ if (input.conversationId !== undefined && !UUID_RE.test(input.conversationId)) {
370
+ throw new ValidationError({ conversationId: ['must be a UUID'] }, 'mail.send: "conversationId" must be a UUID.')
371
+ }
372
+ if (input.list !== undefined) {
373
+ if (typeof input.list !== 'string' || !LIST_RE.test(input.list)) {
374
+ throw new ValidationError({ list: ['must be letters, digits, ".", "_" or "-", starting with a letter or digit, at most 64 characters'] }, 'mail.send: "list" is not a valid list name.')
375
+ }
376
+ if (unsubscribe === undefined) {
377
+ throw new Error('[pipework] mail.send with a list needs mail config unsubscribe: { url, secret }')
378
+ }
379
+ }
380
+ const priority = input.priority ?? config.priority
381
+ if (priority !== undefined && !Number.isInteger(priority)) {
382
+ throw new ValidationError({ priority: ['must be an integer'] }, 'mail.send: "priority" must be an integer.')
383
+ }
384
+ let conversationId = input.conversationId ?? null
385
+ if (input.inReplyTo !== undefined) {
386
+ if (!UUID_RE.test(input.inReplyTo)) {
387
+ throw new ValidationError({ inReplyTo: ['must be a received message id'] }, 'mail.send: "inReplyTo" must be a received message id.')
388
+ }
389
+ const parent = (await db.execute(sql`SELECT conversation_id FROM pipework_email_inbound WHERE id = ${input.inReplyTo}::uuid`))[0]
390
+ if (parent === undefined) {
391
+ throw new ValidationError({ inReplyTo: ['no received message has this id'] }, `mail.send: no received message ${input.inReplyTo}.`)
392
+ }
393
+ conversationId ??= (parent['conversation_id'] as string | null) ?? null
394
+ }
395
+ const traceId = getContext()?.trace?.id ?? null
396
+ const inserted = await db.execute(sql`
397
+ INSERT INTO pipework_email_messages (key, to_address, template, data, trace_id, conversation_id, in_reply_to_inbound, list, created_at)
398
+ VALUES (${input.key}, ${input.to.trim()}, ${input.template}, ${JSON.stringify(storedData(input))}::jsonb, ${traceId}::uuid,
399
+ ${conversationId}::uuid, ${input.inReplyTo ?? null}::uuid, ${input.list ?? null}, clock_timestamp())
400
+ ON CONFLICT (key) DO NOTHING
401
+ RETURNING id`)
402
+ if (inserted.length === 0) {
403
+ const existing = await db.execute(sql`SELECT id FROM pipework_email_messages WHERE key = ${input.key}`)
404
+ return { messageId: existing[0]!['id'] as string, queued: false }
405
+ }
406
+ const messageId = inserted[0]!['id'] as string
407
+ await config.queue.enqueue(handle, { type: SEND_JOB, payload: { messageId }, continueFlow: true, ...(priority !== undefined ? { priority } : {}) })
408
+ return { messageId, queued: true }
409
+ },
410
+ async sentTo(handle, address, options) {
411
+ const db = requireUnscopedExecutor(handle, 'mail.sentTo()', 'it reads every tenant\'s messages to the address')
412
+ const rows = await db.execute(sql`
413
+ SELECT id::text, to_address, template, status, data, list, created_at FROM pipework_email_messages
414
+ WHERE lower(btrim(to_address)) = ${normalizeAddress(address)}${options?.template === undefined ? sql`` : sql` AND template = ${options.template}`}
415
+ ORDER BY created_at, id`)
416
+ const out: SentMail[] = []
417
+ for (const row of rows) {
418
+ const id = row['id'] as string
419
+ const template = row['template'] as string
420
+ const stored = row['data']
421
+ let rendered: RenderedEmail | null = null
422
+ try {
423
+ const fn = config.templates[template]
424
+ if (fn !== undefined) rendered = fn(await withLateData(db, stored, id), renderMeta(id, row['to_address'] as string, (row['list'] as string | null) ?? null))
425
+ } catch { rendered = null }
426
+ let data = stored
427
+ if (lateReference(stored) !== null) {
428
+ const rest = { ...(stored as Record<string, unknown>) }
429
+ delete rest['__late']
430
+ data = rest
431
+ }
432
+ out.push({ id, template, status: row['status'] as string, data, createdAt: new Date(row['created_at'] as string | Date), rendered })
433
+ }
434
+ return out
435
+ },
436
+ unsubscribe: optOut,
437
+ async unsubscribed(handle, address, list) {
438
+ requireList(list)
439
+ return isUnsubscribed(requireUnscopedExecutor(handle, 'mail.unsubscribed()', 'an opt-out is not tenant data'), address, list)
440
+ },
441
+ async resubscribe(_db, address, list) {
442
+ requireList(list)
443
+ await remove(emailUnsubscribes).where(and(eq(emailUnsubscribes.address, normalizeAddress(address)), eq(emailUnsubscribes.list, list))!)
444
+ },
445
+ unsubscribeHttp() {
446
+ if (unsubscribe === undefined) {
447
+ throw new Error('[pipework] mail.unsubscribeHttp needs mail config unsubscribe: { url, secret }')
448
+ }
449
+ return unsubscribeRoutes(unsubscribe, (address, list) => optOut(pipeSystem(), address, list))
450
+ },
451
+ registerLateData(name, fn) { lateFns.set(name, fn) },
452
+ jobs: [sendJob as unknown as Handler<Record<string, unknown>, unknown>],
453
+ }
454
+ }
455
+
456
+ function requireList(list: string): void {
457
+ if (typeof list !== 'string' || !LIST_RE.test(list)) {
458
+ throw new ValidationError({ list: ['is not a valid list name'] }, 'mail: "list" is not a valid list name.')
459
+ }
460
+ }
461
+
462
+ function validateUnsubscribe(options: { readonly url: string; readonly secret: string }, environment: string | undefined): void {
463
+ let parsed: URL | null = null
464
+ try { parsed = new URL(options.url) } catch { /* reported below */ }
465
+ const allowed = environment === 'production' ? ['https:'] : ['https:', 'http:']
466
+ if (typeof options.url !== 'string' || parsed === null || !allowed.includes(parsed.protocol)) {
467
+ throw new Error(`[pipework] mail.create: unsubscribe.url must be an absolute ${environment === 'production' ? 'https' : 'https (or http outside production)'} URL, got ${JSON.stringify(options.url)}.`)
468
+ }
469
+ if (typeof options.secret !== 'string' || options.secret.length < MIN_UNSUBSCRIBE_SECRET) {
470
+ throw new Error(`[pipework] mail.create: unsubscribe.secret must be at least ${String(MIN_UNSUBSCRIBE_SECRET)} characters; use a long random value, the same in every process.`)
471
+ }
472
+ }
473
+
474
+ const escapeHtml = (text: string): string =>
475
+ text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;')
476
+
477
+ function htmlPage(title: string, body: string): string {
478
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">` +
479
+ `<meta name="robots" content="noindex"><title>${escapeHtml(title)}</title>` +
480
+ `<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;line-height:1.5}button{font:inherit;padding:.5rem 1rem}</style>` +
481
+ `</head><body><h1>${escapeHtml(title)}</h1>${body}</body></html>`
482
+ }
483
+
484
+ const INVALID_PAGE = htmlPage('Link not valid', '<p>This unsubscribe link is not valid. Use the link in the most recent email.</p>')
485
+
486
+ function unsubscribeRoutes(
487
+ options: { readonly url: string; readonly secret: string },
488
+ record: (address: string, list: string) => Promise<void>,
489
+ ): Handler<Record<string, unknown>, unknown>[] {
490
+ const path = new URL(options.url).pathname
491
+ const any = schema.check.unknown()
492
+ const tokenOf = (query: unknown): unknown => (query as Record<string, unknown> | null | undefined)?.['t']
493
+ const base = () => new Builder().public().request().response().query(any).params(any)
494
+ const answer = (response: { header(name: string, value: string): void }, status: number, html: string) => {
495
+ response.header('content-type', 'text/html; charset=utf-8')
496
+ response.header('cache-control', 'no-store')
497
+ response.header('referrer-policy', 'no-referrer')
498
+ response.header('content-security-policy', "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'")
499
+ return { status, body: html }
500
+ }
501
+ const get = base().route('GET', path).fit(({ query, response }) => {
502
+ const named = verifyUnsubscribeToken(options.secret, tokenOf(query))
503
+ if (named === null) return Promise.resolve(answer(response, 400, INVALID_PAGE))
504
+ return Promise.resolve(answer(response, 200, htmlPage('Unsubscribe',
505
+ `<p>Stop receiving <strong>${escapeHtml(named.list)}</strong> email at <strong>${escapeHtml(named.address)}</strong>?</p>` +
506
+ `<form method="post" action="${escapeHtml(path)}?t=${escapeHtml(encodeURIComponent(String(tokenOf(query))))}">` +
507
+ `<input type="hidden" name="List-Unsubscribe" value="One-Click"><button type="submit">Unsubscribe</button></form>`)))
508
+ })
509
+ const post = base().ignoreBody().route('POST', path).fit(async ({ query, response }) => {
510
+ const named = verifyUnsubscribeToken(options.secret, tokenOf(query))
511
+ if (named === null) return answer(response, 400, INVALID_PAGE)
512
+ await record(named.address, named.list)
513
+ return answer(response, 200, htmlPage('Unsubscribed',
514
+ `<p>You will no longer receive <strong>${escapeHtml(named.list)}</strong> email at <strong>${escapeHtml(named.address)}</strong>.</p>`))
515
+ })
516
+ return [get, post] as unknown as Handler<Record<string, unknown>, unknown>[]
517
+ }