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,916 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { Readable } from 'node:stream'
3
+ import Fastify, { type FastifyInstance, type FastifyRequest, type FastifyReply } from 'fastify'
4
+ import fastifyCors from '@fastify/cors'
5
+ import fastifyHelmet from '@fastify/helmet'
6
+ import fastifyRateLimit from '@fastify/rate-limit'
7
+ import fastifyMultipart from '@fastify/multipart'
8
+ import type { Manifold } from '../../core/pipework.js'
9
+ import type { Handler, HttpMethod } from '../di/types.js'
10
+ import type { CorsConfig, ServerConfig, RouteRegistration, HandlerResponse, InjectOptions, InjectResponse } from './types.js'
11
+ import { runInContext } from '../context/store.js'
12
+ import { createRequestContext } from '../context/index.js'
13
+ import { executeInTransaction } from '../context/run-in-transaction.js'
14
+ import { resolveAndExecute, parseRouteInputs, resolveHandlerDatabases, buildCheckpoint, type ResolveOptions } from '../di/resolve.js'
15
+ import { createRateLimitClasses } from './rate-limit-classes.js'
16
+ import { buildContext, type ContextOptions } from './middleware.js'
17
+ import { closeTrace } from '../../trace/entry/close-trace.js'
18
+ import { recordErrorStep } from './error-step.js'
19
+ import type { Flow } from '../context/index.js'
20
+ import type { ErrorResponse } from './types.js'
21
+ import { buildErrorResponse, buildInternalErrorResponse, buildClientErrorResponse, sendError } from './response.js'
22
+ import { isPipeworkError } from '../../core/errors/index.js'
23
+ import { checkUploadHeaders, uploadBodyLimit, startUpload, settleUpload, isUploadRefusal, newUploadTrack, type UploadTrack, MULTIPART_FIELD_LIMIT } from './upload.js'
24
+ import type { BinaryUpload, MultipartUpload, UploadConfig, UploadRefusal, RouteRefusal, RouteRefusedHook, HandlerMeta } from '../di/types.js'
25
+ import { __flowStep } from '../../trace/runtime/flow-step.js'
26
+ import { isFileResponse, fileResponseProblem, contentDisposition, primeStream, type FileResponse } from './file.js'
27
+ import type { FileConfig } from '../di/types.js'
28
+ import { toJsonSchema } from '../../data/schema/json-schema.js'
29
+ import { generateOpenAPI } from '../openapi/generate.js'
30
+ import { getBaseLogger, setLoggingConfig } from '../../core/logging/index.js'
31
+ import { settleTraceWrites } from '../../trace/runtime/pending-writes.js'
32
+ import { setupDrain } from '../../core/lifecycle/drain.js'
33
+ import { registerHealthRoutes } from '../../core/lifecycle/health.js'
34
+ import { validateSessionVarName } from '../../auth/tenant/index.js'
35
+ import type { Rbac } from '../../auth/rbac/types.js'
36
+ import { ForbiddenError } from '../../core/errors/index.js'
37
+ import { withTenantSetting } from '../../auth/tenant/in-tenant.js'
38
+ import type { Resource } from '../fixture/types.js'
39
+ import { resourceToHandlers } from '../fixture/to-handlers.js'
40
+
41
+ /** Fastify server wrapper with route registration, handler auto-wiring, and inject() for testing. */
42
+ export interface PipeworkServer {
43
+ route(method: HttpMethod, path: string, handler: Handler<Record<string, unknown>, unknown>): void
44
+ registerHandlers(handlers: Handler<Record<string, unknown>, unknown>[]): void
45
+ registerResource(resource: Resource<unknown>): void
46
+ ready(): Promise<void>
47
+ inject(opts: InjectOptions): Promise<InjectResponse>
48
+ listen(): Promise<string>
49
+ close(): Promise<void>
50
+ /**
51
+ * Stops the listener at once and drains in-flight requests, leaving the pools open. When `force` aborts, the
52
+ * remaining sockets are destroyed and this returns without waiting for their handlers.
53
+ */
54
+ stop(force?: AbortSignal): Promise<void>
55
+ }
56
+
57
+ /** Creates a PipeworkServer — wraps Fastify with auto request IDs, CORS, rate limiting, and transaction-per-request. */
58
+ export function createServer(instance: Manifold, config: ServerConfig = {}): PipeworkServer {
59
+ if (instance.config.environment === 'production') {
60
+ validateProductionConfig(config)
61
+ }
62
+
63
+ const fastifyOpts: Record<string, unknown> = {
64
+ trustProxy: Array.isArray(config.trustProxy) ? [...config.trustProxy] : (config.trustProxy ?? false),
65
+ genReqId: () => randomUUID(),
66
+ requestIdHeader: false,
67
+ }
68
+
69
+ if (config.bodyLimit !== undefined) {
70
+ fastifyOpts['bodyLimit'] = config.bodyLimit
71
+ }
72
+
73
+ if (config.logger === false) {
74
+ fastifyOpts['logger'] = false
75
+ } else {
76
+ fastifyOpts['loggerInstance'] = config.logger ?? getBaseLogger()
77
+ }
78
+
79
+ const app = Fastify(fastifyOpts)
80
+
81
+ // Fastify's own refusals (content type, bad JSON, body too large) answer in
82
+ // Pipework's error shape, never through a route's declared response schema.
83
+ // The Flow of a request that got one, for the internal-error hook.
84
+ const requestFlows = new WeakMap<object, Flow>()
85
+ const refusedRequests = new WeakSet<object>()
86
+
87
+ // Tells `onInternalError` about an error answered with a 5xx, in the request's
88
+ // Flow (no transaction is open: the request's has rolled back). Awaited; a
89
+ // failure is logged and the response is unchanged.
90
+ async function reportInternalError(req: FastifyRequest, error: unknown, handler: Handler<Record<string, unknown>, unknown> | null = null): Promise<void> {
91
+ const hook = config.onInternalError
92
+ if (hook === undefined) return
93
+ try {
94
+ const route = (req.routeOptions as { url?: string } | undefined)?.url ?? req.url.split('?')[0]!
95
+ const ctx = requestFlows.get(req) ?? createRequestContext({
96
+ instance,
97
+ requestId: String(req.id),
98
+ metadata: { method: req.method, path: req.url, startedAt: Date.now(), traceId: String(req.id), sessionId: null },
99
+ })
100
+ const inputs = parseRouteInputs(handler?.meta ?? null, req.query, req.params)
101
+ await runInContext(ctx, () => hook({
102
+ error,
103
+ params: inputs.params,
104
+ query: inputs.query,
105
+ method: req.method,
106
+ route,
107
+ requestId: String(req.id),
108
+ tenantId: ctx.tenant,
109
+ userId: ctx.user ?? ((ctx.auth as Record<string, unknown> | null)?.['userId'] ?? null) as string | null,
110
+ traceId: ctx.trace?.id ?? null,
111
+ }))
112
+ } catch (hookError) {
113
+ req.log.error(hookError, 'onInternalError failed')
114
+ }
115
+ }
116
+
117
+ // Runs a route's `onRefused` hook for a 4xx about to be sent, in the request's
118
+ // Flow (a minimal one where the request never got its own), as a trace step.
119
+ // What it returns is merged into `response.error` except pipework's own keys;
120
+ // a throw is logged and the response is left as it was.
121
+ async function runRefusedHook(
122
+ req: FastifyRequest,
123
+ meta: HandlerMeta | null,
124
+ ctx: Flow | undefined,
125
+ status: number,
126
+ response: ErrorResponse,
127
+ ): Promise<void> {
128
+ const hook = meta?.onRefused
129
+ if (meta === null || hook === undefined || hook === null) return
130
+ if (refusedRequests.has(req)) return
131
+ refusedRequests.add(req)
132
+ try {
133
+ const flow = ctx ?? createRequestContext({
134
+ instance,
135
+ requestId: String(req.id),
136
+ metadata: { method: req.method, path: req.url, startedAt: Date.now(), traceId: String(req.id), sessionId: null },
137
+ })
138
+ const inputs = parseRouteInputs(meta, req.query, req.params)
139
+ const refusal: RouteRefusal = {
140
+ status,
141
+ code: response.error.code,
142
+ message: response.error.message,
143
+ fields: response.error.fields ?? null,
144
+ retryAfterSeconds: status === 429 ? response.error.retryAfterSeconds ?? null : null,
145
+ auth: ctx?.auth ?? null,
146
+ params: inputs.params,
147
+ query: inputs.query,
148
+ request: {
149
+ headers: req.headers as Record<string, string | undefined>,
150
+ ip: req.ip,
151
+ ips: [...(req.ips ?? [req.ip])],
152
+ },
153
+ requestId: String(req.id),
154
+ }
155
+ const extra: unknown = await runInContext(flow, () => refusedStep(hook, refusal))
156
+ if (typeof extra !== 'object' || extra === null || Array.isArray(extra)) return
157
+ const target = response.error as unknown as Record<string, unknown>
158
+ for (const [key, value] of Object.entries(extra as Record<string, unknown>)) {
159
+ if (REFUSAL_OWN_KEYS.has(key)) {
160
+ req.log.warn({ key, requestId: String(req.id) }, 'onRefused hook returned a key pipework owns; dropped')
161
+ continue
162
+ }
163
+ Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true })
164
+ }
165
+ } catch (hookError) {
166
+ req.log.error({ err: hookError, requestId: String(req.id) }, 'onRefused hook threw')
167
+ }
168
+ }
169
+
170
+ app.setErrorHandler(async (error, req, reply) => {
171
+ const status = (error as { statusCode?: unknown }).statusCode
172
+ const open = requestFlows.get(req)
173
+ const routeOf = (): string => (req.routeOptions as { url?: string } | undefined)?.url ?? req.url.split('?')[0]!
174
+ const record = async (code: string, httpStatus: number, refused?: () => Promise<void>): Promise<void> => {
175
+ if (open === undefined) {
176
+ await refused?.()
177
+ return
178
+ }
179
+ recordErrorStep(open, { method: req.method, route: routeOf(), status: httpStatus, code, error })
180
+ try {
181
+ await refused?.()
182
+ } finally {
183
+ void closeTrace(instance, open.trace, 'error')
184
+ }
185
+ }
186
+ if (typeof status === 'number' && status >= 400 && status < 500) {
187
+ const response = buildClientErrorResponse(status, error, reply.getHeader('retry-after'))
188
+ const owner = (req.routeOptions as { config?: { pipeworkHandler?: Handler<Record<string, unknown>, unknown> } } | undefined)?.config?.pipeworkHandler
189
+ await record(response.error.code, status, () => runRefusedHook(req, owner?.meta ?? null, open, status, response))
190
+ return sendError(reply, status, response)
191
+ }
192
+ req.log.error(error, 'Unhandled error')
193
+ await record('INTERNAL_ERROR', 500)
194
+ await reportInternalError(req, error)
195
+ return sendError(reply, 500, buildInternalErrorResponse(error, config.exposeErrorDetails))
196
+ })
197
+
198
+ // Validation is handled by Pipework's DI layer (parse + ValidationError).
199
+ // Pass-through compiler lets us attach JSON Schema for OpenAPI without AJV double-validating.
200
+ app.setValidatorCompiler(() => (data) => ({ value: data }))
201
+
202
+ // CORS is closed unless granted: a route's own `.cors()` replaces the server's
203
+ // `cors` for that route, and a route with neither grants nothing. The plugin
204
+ // loads only if some grant exists, which is known once routes are registered.
205
+ // A preflight names no route of its own, so each path that has a grant gets
206
+ // one OPTIONS route carrying the grant per method; Fastify does the matching.
207
+ const corsPaths = new Map<string, { byMethod: Record<string, CorsConfig | false> }>()
208
+ let grantCount = 0
209
+ const serverGrant: CorsConfig | null = config.cors !== undefined && config.cors !== false ? config.cors : null
210
+ const corsPlugin = async (scope: FastifyInstance): Promise<void> => {
211
+ if (serverGrant === null && grantCount === 0) return
212
+ await scope.register(fastifyCors, {
213
+ delegator: (req: FastifyRequest, cb: (err: Error | null, options?: Record<string, unknown>) => void) => {
214
+ let grant: CorsConfig | null
215
+ const requested = req.headers['access-control-request-method']
216
+ const routeConfig = req.routeOptions.config as { cors?: CorsConfig | false; corsByMethod?: Record<string, CorsConfig | false> } | undefined
217
+ if (req.method === 'OPTIONS' && typeof requested === 'string') {
218
+ const g = routeConfig?.corsByMethod?.[requested.toUpperCase()]
219
+ grant = g === undefined || g === false ? null : g
220
+ } else {
221
+ const own = routeConfig?.cors
222
+ grant = own === undefined ? serverGrant : own === false ? null : own
223
+ }
224
+ cb(null, grant === null ? { origin: false } : (grant as Record<string, unknown>))
225
+ },
226
+ } as never)
227
+ for (const [path, entry] of corsPaths) {
228
+ if (!Object.values(entry.byMethod).some((g) => g !== false)) continue
229
+ scope.options(path, { config: { corsByMethod: entry.byMethod } }, (_req, reply) => reply.status(204).send())
230
+ }
231
+ }
232
+ ;(corsPlugin as unknown as Record<symbol, boolean>)[Symbol.for('skip-override')] = true
233
+ void app.register(corsPlugin)
234
+
235
+ if (config.security !== false) {
236
+ void app.register(fastifyHelmet, (config.security ?? {}) as Record<string, unknown>)
237
+ }
238
+
239
+ if (config.rateLimit !== false) {
240
+ void app.register(fastifyRateLimit, (config.rateLimit ?? {}) as Record<string, unknown>)
241
+ }
242
+
243
+ if (config.logging) {
244
+ setLoggingConfig(config.logging)
245
+ }
246
+
247
+ app.decorateRequest('rawBody', undefined)
248
+ app.decorateRequest('pipeworkTraceId', null)
249
+
250
+ app.addHook('onSend', (_req, reply, payload, done) => {
251
+ void reply.header('x-request-id', _req.id)
252
+ const traceId = (_req as FastifyRequest & { pipeworkTraceId: string | null }).pipeworkTraceId
253
+ if (traceId !== null) void reply.header('x-pipework-trace-id', traceId)
254
+ done(null, payload)
255
+ })
256
+
257
+ const routes: RouteRegistration[] = []
258
+
259
+ const authConfig = config.auth
260
+ const strategies = authConfig
261
+ ? ('strategies' in authConfig ? authConfig.strategies : [authConfig.strategy])
262
+ : []
263
+ const enrichers = authConfig && 'enrichers' in authConfig ? (authConfig.enrichers ?? []) : []
264
+ const tenantResolver = authConfig && 'tenantResolver' in authConfig ? authConfig.tenantResolver : undefined
265
+
266
+ const tenantConfig = config.tenant ?? null
267
+ if (tenantConfig?.sessionVars) {
268
+ for (const name of Object.keys(tenantConfig.sessionVars)) {
269
+ validateSessionVarName(name)
270
+ }
271
+ }
272
+
273
+ const contextOptions: ContextOptions = {
274
+ instance,
275
+ strategies,
276
+ enrichers,
277
+ tenantResolver,
278
+ tenantConfig,
279
+ authOptional: authConfig?.optional ?? false,
280
+ logging: config.logging ?? null,
281
+ }
282
+
283
+ const gracefulShutdownMs = config.gracefulShutdownMs ?? 10000
284
+ const drain = setupDrain(app)
285
+
286
+ if (config.health !== false) {
287
+ // Registered on the app itself: probes are never rate limited.
288
+ registerHealthRoutes(app, instance, config.healthChecks ?? [], {
289
+ health: config.healthPath ?? '/health',
290
+ ready: config.readyPath ?? '/ready',
291
+ })
292
+ }
293
+
294
+ const rbacInstance = config.rbac ?? null
295
+ const rateLimitClasses = config.rateLimits === undefined ? null : createRateLimitClasses(instance, config.rateLimits)
296
+
297
+ const txDatabases: readonly string[] | null = config.transactionPerRequest !== undefined && config.transactionPerRequest !== false
298
+ ? (config.transactionPerRequest === true
299
+ ? [...instance.config.databases().keys()]
300
+ : config.transactionPerRequest)
301
+ : null
302
+ const txTimeoutMs = config.transactionTimeoutMs ?? 5000
303
+
304
+ if (tenantConfig?.sessionVars !== undefined && txDatabases === null) {
305
+ throw new Error(
306
+ '[pipework] Tenant session variables require transactionPerRequest.\n\n' +
307
+ ' Your server config has tenant.sessionVars configured, but transactionPerRequest\n' +
308
+ ' is not enabled. Session variables are propagated via SET LOCAL inside a\n' +
309
+ ' transaction — without transactionPerRequest, they are silently skipped.\n\n' +
310
+ ' Fix: Add transactionPerRequest: true (or a list of database names) to your\n' +
311
+ ' createServer() config.\n',
312
+ )
313
+ }
314
+
315
+ function route(
316
+ method: HttpMethod,
317
+ path: string,
318
+ handler: Handler<Record<string, unknown>, unknown>,
319
+ ): void {
320
+ routes.push({ method, path, handler })
321
+ rateLimitClasses?.checkRoute(method, path, handler.meta)
322
+
323
+ const fastifyMethod = method.toLowerCase() as 'get' | 'post' | 'put' | 'patch' | 'delete'
324
+
325
+ const schema: Record<string, unknown> = {}
326
+ if (handler.meta.inputSchema) {
327
+ schema['body'] = toJsonSchema(handler.meta.inputSchema)
328
+ }
329
+ if (handler.meta.outputSchemas.size > 0) {
330
+ const response: Record<string, unknown> = {}
331
+ for (const [status, outputSchema] of handler.meta.outputSchemas) {
332
+ response[String(status)] = toJsonSchema(outputSchema)
333
+ }
334
+ schema['response'] = response
335
+ }
336
+ if (handler.meta.querySchema) {
337
+ schema['querystring'] = toJsonSchema(handler.meta.querySchema)
338
+ }
339
+ if (handler.meta.paramsSchema) {
340
+ schema['params'] = toJsonSchema(handler.meta.paramsSchema)
341
+ }
342
+ const opts: Record<string, unknown> = Object.keys(schema).length > 0 ? { schema } : {}
343
+
344
+ const ownCors = handler.meta.cors ?? undefined
345
+ const entry = corsPaths.get(path) ?? { byMethod: {} }
346
+ const effective = ownCors === undefined ? (serverGrant ?? false) : ownCors
347
+ entry.byMethod[method] = effective
348
+ corsPaths.set(path, entry)
349
+ if (ownCors !== undefined) {
350
+ grantCount++
351
+ opts['config'] = { ...(opts['config'] as object | undefined), cors: ownCors }
352
+ if (instance.config.environment === 'production') {
353
+ const problem = credentialedWildcard(ownCors)
354
+ if (problem !== null) throw new Error(`[pipework] ${method} ${path}: ${problem}`)
355
+ }
356
+ }
357
+
358
+ if (handler.meta.onRefused) {
359
+ opts['config'] = { ...(opts['config'] as object | undefined), pipeworkHandler: handler }
360
+ }
361
+
362
+ const upload = handler.meta.upload ?? null
363
+ if (upload !== null) {
364
+ opts['bodyLimit'] = uploadBodyLimit(upload)
365
+ } else if (handler.meta.route?.bodyLimit !== undefined) {
366
+ opts['bodyLimit'] = handler.meta.route.bodyLimit
367
+ }
368
+
369
+ const screen = handler.meta.route?.screen
370
+ if (screen !== undefined) {
371
+ // Runs before the body is parsed, so a route can refuse on headers alone
372
+ // (its own status + named reason) before the body limit turns it into 413.
373
+ opts['onRequest'] = async (req: FastifyRequest, reply: FastifyReply) => {
374
+ const raw = req.headers['content-length']
375
+ const declared = raw === undefined || raw === '' ? NaN : Number(raw)
376
+ let refusal
377
+ try {
378
+ // The screen runs inside its own minimal request context so `pipe.system()`
379
+ // works. No auth chain and no transaction: the screen is pre-parse and
380
+ // pre-auth, and its reads are committed-only, outside request atomicity.
381
+ const screenCtx = createRequestContext({
382
+ instance,
383
+ requestId: String(req.id),
384
+ metadata: { method: req.method, path: req.url, startedAt: Date.now(), traceId: String(req.id), sessionId: null },
385
+ })
386
+ refusal = await runInContext(screenCtx, () => screen({
387
+ method: req.method,
388
+ url: req.url,
389
+ headers: req.headers as Record<string, string | undefined>,
390
+ contentLength: Number.isFinite(declared) ? declared : null,
391
+ params: parseRouteInputs(handler.meta, req.query, req.params).params,
392
+ }))
393
+ } catch (error) {
394
+ req.log.error(error, 'Route screen threw')
395
+ await reportInternalError(req, error, handler)
396
+ return sendError(reply, 500, {
397
+ error: 'ScreenFailed',
398
+ message:
399
+ `[pipework] The pre-parse screen for ${method} ${path} threw: ${error instanceof Error ? error.message : String(error)}\n\n` +
400
+ ' A screen must inspect headers and either return a refusal\n' +
401
+ ' ({ status, reason }) or return nothing. Fix the screen, or catch\n' +
402
+ ' inside it and return the refusal you want the caller to see.\n',
403
+ })
404
+ }
405
+ if (refusal !== undefined && refusal !== null) {
406
+ return sendError(reply, refusal.status, { ...refusal.body, reason: refusal.reason })
407
+ }
408
+ return undefined
409
+ }
410
+ }
411
+
412
+ if (upload !== null && upload.kind === 'multipart') {
413
+ // After the screen: a body that is not multipart/form-data is a protocol
414
+ // error, refused from the headers before anything is read.
415
+ const headerCheck = async (req: FastifyRequest, reply: FastifyReply) => {
416
+ const refusal = checkUploadHeaders(upload, req.headers)
417
+ if (refusal === null) return undefined
418
+ void reply.header('connection', 'close')
419
+ const response = buildErrorResponse(refusal)
420
+ await runRefusedHook(req, handler.meta, undefined, refusal.httpStatus, response)
421
+ return sendError(reply, refusal.httpStatus, response)
422
+ }
423
+ const existing = opts['onRequest'] as ((req: FastifyRequest, reply: FastifyReply) => Promise<unknown>) | undefined
424
+ opts['onRequest'] = existing === undefined ? [headerCheck] : [existing, headerCheck]
425
+ }
426
+
427
+ if (handler.meta.wantsRawBody) {
428
+ opts['preParsing'] = async (req: FastifyRequest, _reply: FastifyReply, payload: AsyncIterable<Buffer>) => {
429
+ const chunks: Buffer[] = []
430
+ for await (const chunk of payload) chunks.push(chunk)
431
+ const raw = Buffer.concat(chunks)
432
+ ;(req as FastifyRequest & { rawBody: Buffer }).rawBody = raw
433
+ return Readable.from(raw)
434
+ }
435
+ }
436
+
437
+ const routeHandler = async (req: FastifyRequest, reply: FastifyReply) => {
438
+ let ctx: Flow | undefined
439
+ let uploaded: BinaryUpload | MultipartUpload | undefined
440
+ const track = newUploadTrack()
441
+ // Ends the upload stream and closes the connection if the body was left unread.
442
+ const finishUpload = (failed: boolean): void => {
443
+ if (settleUpload(uploaded?.stream) || (failed && upload !== null)) void reply.header('connection', 'close')
444
+ }
445
+ try {
446
+ ctx = await buildContext(req, contextOptions, handler.meta.authRequirement)
447
+ ;(req as FastifyRequest & { pipeworkTraceId: string | null }).pipeworkTraceId = ctx.trace?.id ?? null
448
+ requestFlows.set(req, ctx)
449
+
450
+ if (rateLimitClasses !== null) await rateLimitClasses.check(req, ctx, handler.meta, { method, url: path })
451
+
452
+ if (handler.meta.permission && rbacInstance) {
453
+ await enforceHandlerPermission(instance, ctx, handler.meta, rbacInstance)
454
+ }
455
+
456
+ const requestObj: import('./types.js').HttpRequest = {
457
+ headers: req.headers as Record<string, string | undefined>,
458
+ ip: req.ip,
459
+ ips: [...(req.ips ?? [req.ip])],
460
+ }
461
+ const rawBodyBuffer = handler.meta.wantsRawBody
462
+ ? (req as FastifyRequest & { rawBody: Buffer }).rawBody
463
+ : undefined
464
+ if (rawBodyBuffer !== undefined) {
465
+ (requestObj as { rawBody: Buffer }).rawBody = rawBodyBuffer
466
+ }
467
+
468
+ if (upload !== null) {
469
+ uploaded = await startUpload(req, upload, track)
470
+ }
471
+
472
+ const httpOptions: ResolveOptions = {
473
+ request: requestObj,
474
+ response: {
475
+ header: (name: string, value: string | readonly string[]) => { void reply.header(name, typeof value === 'string' ? value : [...value]) },
476
+ onFinish: (cb) => watchResponse(reply, cb, req.log),
477
+ },
478
+ ...(rawBodyBuffer !== undefined ? { rawBody: rawBodyBuffer } : {}),
479
+ ...(uploaded !== undefined ? { upload: uploaded } : {}),
480
+ }
481
+
482
+ // Fastify hands a missing body over as null; the input sees undefined.
483
+ // A body that is literally JSON null names a content-type and stays null.
484
+ const body = req.body === null && req.headers['content-type'] === undefined ? undefined : req.body
485
+
486
+ let result: unknown
487
+ if (handler.meta.jobTransaction === 'manual') {
488
+ // The route owns its commit boundaries: no request-wide transaction
489
+ // is opened, and every checkpoint commits on its own. Between them
490
+ // the handler holds no connection, which is the point — a lease can
491
+ // be written and committed, a network call made, and the outcome
492
+ // written again, all under the request's own tenant with RLS
493
+ // enforced. What it costs is stated on `.transaction('manual')`.
494
+ const checkpoint = buildCheckpoint(
495
+ handler,
496
+ ctx,
497
+ resolveHandlerDatabases(handler, instance),
498
+ tenantConfig,
499
+ txTimeoutMs,
500
+ )
501
+ result = await runInContext(ctx, () =>
502
+ resolveAndExecute(handler, body, req.query, req.params, { ...httpOptions, checkpoint }),
503
+ )
504
+ } else if (txDatabases !== null && txDatabases.length > 0) {
505
+ result = await executeInTransaction(
506
+ ctx,
507
+ txDatabases,
508
+ tenantConfig,
509
+ txTimeoutMs,
510
+ () => resolveAndExecute(handler, body, req.query, req.params, httpOptions),
511
+ )
512
+ } else {
513
+ result = await runInContext(ctx, () => resolveAndExecute(handler, body, req.query, req.params, httpOptions))
514
+ }
515
+
516
+ finishUpload(false)
517
+ void closeTrace(instance, ctx.trace, 'closed')
518
+ return await sendResult(reply, req, result, handler.meta.file ?? null, `${method} ${path}`)
519
+ } catch (error) {
520
+ finishUpload(true)
521
+ // Records the error step, runs `refused` (the route's onRefused hook) while the
522
+ // trace is still open, then closes the trace.
523
+ const failWith = async (status: number, code: string, refused?: () => Promise<void>): Promise<void> => {
524
+ recordErrorStep(ctx, { method, route: path, status, code, error })
525
+ try {
526
+ await refused?.()
527
+ } finally {
528
+ if (ctx !== undefined) void closeTrace(instance, ctx.trace, 'error')
529
+ }
530
+ }
531
+
532
+ if (error instanceof FileResponseError) {
533
+ await failWith(500, 'FILE_RESPONSE_INVALID')
534
+ req.log.error({ route: `${method} ${path}`, requestId: String(req.id), err: error }, 'File response refused')
535
+ await reportInternalError(req, error, handler)
536
+ return sendError(reply, 500, { error: { code: 'FILE_RESPONSE_INVALID', message: error.message } })
537
+ }
538
+
539
+ if (isPipeworkError(error)) {
540
+ const response = buildErrorResponse(error)
541
+ const isClient = error.httpStatus >= 400 && error.httpStatus < 500
542
+ await failWith(error.httpStatus, error.code, async () => {
543
+ if (upload !== null && ctx !== undefined && isUploadRefusal(error)) {
544
+ const message = await notifyRefusal(req, ctx, handler, upload, track, error)
545
+ if (message !== undefined) response.error.message = message
546
+ }
547
+ if (isClient) await runRefusedHook(req, handler.meta, ctx, error.httpStatus, response)
548
+ })
549
+ const retryAfter = response.error.retryAfterSeconds
550
+ if (retryAfter !== undefined) void reply.header('Retry-After', String(retryAfter))
551
+ if (error.httpStatus >= 500) await reportInternalError(req, error, handler)
552
+ return sendError(reply, error.httpStatus, response)
553
+ }
554
+
555
+ // A malformed multipart body surfaces as a Fastify client error (4xx).
556
+ const clientStatus = upload !== null ? (error as { statusCode?: unknown } | null)?.statusCode : undefined
557
+ if (typeof clientStatus === 'number' && clientStatus >= 400 && clientStatus < 500) {
558
+ const response = buildClientErrorResponse(clientStatus, error)
559
+ await failWith(clientStatus, response.error.code, () => runRefusedHook(req, handler.meta, ctx, clientStatus, response))
560
+ return sendError(reply, clientStatus, response)
561
+ }
562
+
563
+ await failWith(500, 'INTERNAL_ERROR')
564
+ req.log.error(error, 'Unhandled error in route handler')
565
+ await reportInternalError(req, error, handler)
566
+ return sendError(reply, 500, buildInternalErrorResponse(error, config.exposeErrorDetails))
567
+ }
568
+ }
569
+
570
+ // Its own plugin scope, so the route loads after @fastify/rate-limit and
571
+ // gets its limit (a route added to `app` directly would be registered
572
+ // before the plugin's onRoute hook exists). An upload's body parsers
573
+ // replace the JSON parser for this route only, so no other route's
574
+ // parsing changes.
575
+ void app.register(async (scope) => {
576
+ if (upload !== null) {
577
+ scope.removeAllContentTypeParsers()
578
+ if (upload.kind === 'binary') {
579
+ scope.addContentTypeParser('*', (_req, payload, done) => { done(null, payload) })
580
+ } else {
581
+ await scope.register(fastifyMultipart, {
582
+ throwFileSizeLimit: false,
583
+ limits: { fileSize: upload.maxBytes + 1024, fieldSize: MULTIPART_FIELD_LIMIT, files: 1, fields: 100 },
584
+ })
585
+ }
586
+ }
587
+ if (handler.meta.ignoresBody === true && upload === null) {
588
+ scope.removeAllContentTypeParsers()
589
+ scope.addContentTypeParser('*', (req, payload, done) => {
590
+ const declared = Number(req.headers['content-length'])
591
+ if (Number.isFinite(declared) && declared > IGNORED_BODY_LIMIT) {
592
+ done(Object.assign(new Error('Request body is too large'), { statusCode: 413 }))
593
+ return
594
+ }
595
+ payload.resume()
596
+ done(null, undefined)
597
+ })
598
+ }
599
+ scope[fastifyMethod](path, opts, routeHandler)
600
+ })
601
+ }
602
+
603
+ // Tells the route's `onRefuse` hook about a file pipework refused, in the
604
+ // request's context. Returns the hook's replacement message, if it gave one.
605
+ async function notifyRefusal(
606
+ req: FastifyRequest,
607
+ ctx: Flow,
608
+ handler: Handler<Record<string, unknown>, unknown>,
609
+ upload: UploadConfig,
610
+ track: UploadTrack,
611
+ error: { httpStatus: number; code: string },
612
+ ): Promise<string | undefined> {
613
+ if (upload.onRefuse === undefined) return undefined
614
+ const declaredLength = Number(req.headers['content-length'])
615
+ const inputs = parseRouteInputs(handler.meta, req.query, req.params)
616
+ const refusal: UploadRefusal = {
617
+ status: error.httpStatus as 413 | 415,
618
+ code: error.code,
619
+ detectedType: track.detectedType,
620
+ declaredType: track.declaredType,
621
+ extension: track.extension,
622
+ filename: track.filename,
623
+ declaredLength: req.headers['content-length'] !== undefined && Number.isFinite(declaredLength) ? declaredLength : null,
624
+ bytesRead: track.bytesRead,
625
+ accepted: upload.mediaTypes,
626
+ auth: ctx.auth,
627
+ requestId: ctx.requestId,
628
+ params: inputs.params,
629
+ query: inputs.query,
630
+ }
631
+ const hook = upload.onRefuse
632
+ try {
633
+ const message = await runInContext(ctx, async () => hook(refusal))
634
+ return typeof message === 'string' && message !== '' ? message : undefined
635
+ } catch (hookError) {
636
+ req.log.error({ err: hookError, requestId: ctx.requestId }, 'Upload onRefuse hook threw')
637
+ return undefined
638
+ }
639
+ }
640
+
641
+ function registerHandlers(handlers: Handler<Record<string, unknown>, unknown>[]): void {
642
+ for (const handler of handlers) {
643
+ if (handler.meta.route) {
644
+ route(handler.meta.route.method, handler.meta.route.path, handler)
645
+ }
646
+ }
647
+ }
648
+
649
+ function registerResourceFn(resource: Resource<unknown>): void {
650
+ registerHandlers(resourceToHandlers(resource))
651
+ }
652
+
653
+ if (config.openapi) {
654
+ const openApiPath = config.openApiPath ?? '/openapi.json'
655
+ const openApiInfo = config.openapi
656
+ // Registered on the app itself: the document is not rate limited.
657
+ app.get(openApiPath, async (_req, reply) => {
658
+ return reply.send(generateOpenAPI(openApiInfo, routes, { rateLimited: config.rateLimit !== false || config.rateLimits !== undefined }))
659
+ })
660
+ }
661
+
662
+ async function listen(): Promise<string> {
663
+ const address = await app.listen({
664
+ port: config.port ?? 3000,
665
+ host: config.host ?? '0.0.0.0',
666
+ })
667
+
668
+ if (config.managed !== true) {
669
+ setupGracefulShutdown(app, instance, drain, gracefulShutdownMs)
670
+ }
671
+
672
+ return address
673
+ }
674
+
675
+ async function close(): Promise<void> {
676
+ await app.close()
677
+ await settleTraceWrites()
678
+ await instance.pool.closeAll()
679
+ }
680
+
681
+ async function stop(force?: AbortSignal): Promise<void> {
682
+ const node = app.server
683
+ const destroy = (): void => { node.closeAllConnections() }
684
+ if (node.listening) node.close()
685
+ if (force?.aborted === true) destroy()
686
+ else force?.addEventListener('abort', destroy, { once: true })
687
+ try {
688
+ await app.close()
689
+ } finally {
690
+ force?.removeEventListener('abort', destroy)
691
+ }
692
+ if (force?.aborted !== true) await settleTraceWrites()
693
+ }
694
+
695
+ async function ready(): Promise<void> {
696
+ await app.ready()
697
+ }
698
+
699
+ async function inject(opts: InjectOptions): Promise<InjectResponse> {
700
+ const response = await app.inject({
701
+ method: opts.method as 'GET',
702
+ url: opts.url,
703
+ headers: opts.headers as Record<string, string>,
704
+ payload: opts.payload as string,
705
+ query: opts.query as Record<string, string>,
706
+ ...(opts.remoteAddress !== undefined ? { remoteAddress: opts.remoteAddress } : {}),
707
+ })
708
+ const r = response as unknown as { statusCode: number; headers: Record<string, string | number | string[] | undefined>; body: string; rawPayload: Buffer; json(): unknown }
709
+ return { statusCode: r.statusCode, headers: r.headers, body: r.body, rawPayload: r.rawPayload, json: () => r.json() }
710
+ }
711
+
712
+ return { route, registerHandlers, registerResource: registerResourceFn, ready, inject, listen, close, stop }
713
+ }
714
+
715
+ const REFUSAL_OWN_KEYS: ReadonlySet<string> = new Set(['code', 'message', 'fields', 'details', 'retryAfterSeconds'])
716
+
717
+ const refusedStep = __flowStep(
718
+ { ref: 'pipework/http:on-refused' },
719
+ async (hook: RouteRefusedHook, refusal: RouteRefusal) => hook(refusal),
720
+ )
721
+
722
+ function setupGracefulShutdown(
723
+ app: FastifyInstance,
724
+ instance: Manifold,
725
+ drain: ReturnType<typeof setupDrain>,
726
+ timeoutMs: number,
727
+ ): void {
728
+ const shutdown = async (signal: string) => {
729
+ app.log.info({ signal }, 'Graceful shutdown initiated')
730
+
731
+ const timer = setTimeout(() => {
732
+ process.exit(1)
733
+ }, timeoutMs)
734
+
735
+ let exitCode = 0
736
+ try {
737
+ await drain.waitForDrain(timeoutMs - 1000)
738
+ await app.close()
739
+ await settleTraceWrites()
740
+ await instance.pool.closeAll()
741
+ } catch {
742
+ exitCode = 1
743
+ } finally {
744
+ clearTimeout(timer)
745
+ process.exit(exitCode)
746
+ }
747
+ }
748
+
749
+ process.once('SIGTERM', () => { void shutdown('SIGTERM') })
750
+ process.once('SIGINT', () => { void shutdown('SIGINT') })
751
+ }
752
+
753
+ // Runs `cb` once with how the response ended. `finish` fires when the last byte
754
+ // is handed to the socket; `close` without `finish` means the connection died first.
755
+ function watchResponse(
756
+ reply: FastifyReply,
757
+ cb: (outcome: 'sent' | 'aborted') => void | Promise<void>,
758
+ log: { error(obj: unknown, msg?: string): void },
759
+ ): void {
760
+ const raw = reply.raw
761
+ let called = false
762
+ const settle = (outcome: 'sent' | 'aborted'): void => {
763
+ if (called) return
764
+ called = true
765
+ raw.removeListener('finish', onFinish)
766
+ raw.removeListener('close', onClose)
767
+ void (async () => {
768
+ try {
769
+ await cb(outcome)
770
+ } catch (err) {
771
+ log.error(err, 'HttpResponse.onFinish callback threw')
772
+ }
773
+ })()
774
+ }
775
+ const onFinish = (): void => settle('sent')
776
+ const onClose = (): void => settle(raw.writableFinished ? 'sent' : 'aborted')
777
+ if (raw.writableFinished) return settle('sent')
778
+ if (raw.destroyed) return settle('aborted')
779
+ raw.once('finish', onFinish)
780
+ raw.once('close', onClose)
781
+ }
782
+
783
+ /** A file response the route cannot send; becomes a 500 naming the reason. */
784
+ class FileResponseError extends Error {}
785
+
786
+ async function sendFile(reply: FastifyReply, req: FastifyRequest, file: FileResponse, declared: FileConfig | null, route: string): Promise<unknown> {
787
+ const problem = fileResponseProblem(declared, file)
788
+ if (problem !== null) {
789
+ if (!Buffer.isBuffer(file.body)) file.body.destroy()
790
+ throw new FileResponseError(`[pipework] ${route}: ${problem}`)
791
+ }
792
+
793
+ let body: Buffer | Readable
794
+ let length: number | undefined
795
+ if (Buffer.isBuffer(file.body)) {
796
+ body = file.body
797
+ length = file.body.length
798
+ } else {
799
+ body = await primeStream(file.body, file.length)
800
+ length = file.length
801
+ const raw = reply.raw
802
+ body.on('error', (err) => {
803
+ // Headers are out: the only honest signal left is a dead connection.
804
+ req.log.error({ route, requestId: String(req.id), err }, 'File stream failed while sending')
805
+ raw.destroy()
806
+ })
807
+ raw.once('close', () => { if (!(body as Readable).destroyed) (body as Readable).destroy() })
808
+ }
809
+
810
+ void reply.status(declared?.status ?? 200)
811
+ void reply.header('content-type', file.mediaType)
812
+ void reply.header('content-disposition', contentDisposition(file.disposition, file.filename))
813
+ void reply.header('x-content-type-options', 'nosniff')
814
+ if (length !== undefined) void reply.header('content-length', String(length))
815
+ return reply.send(body)
816
+ }
817
+
818
+ /** The largest declared body a route that ignores its body drains. */
819
+ const IGNORED_BODY_LIMIT = 64 * 1024
820
+
821
+ function isStatusResponse(value: unknown): value is HandlerResponse {
822
+ return value !== null && typeof value === 'object' && 'status' in value && typeof (value as Record<string, unknown>)['status'] === 'number'
823
+ }
824
+
825
+ async function sendResult(reply: FastifyReply, req: FastifyRequest, result: unknown, fileConfig: FileConfig | null, route: string): Promise<unknown> {
826
+ if (isFileResponse(result)) return sendFile(reply, req, result, fileConfig, route)
827
+ if (fileConfig !== null) {
828
+ const other = isStatusResponse(result) && result.status !== fileConfig.status
829
+ if (!other) {
830
+ throw new FileResponseError(`[pipework] ${route} is declared with .file() and must return http.file({ body, mediaType, filename? }) at status ${fileConfig.status}; it returned ${result === null ? 'null' : typeof result}. Only a { status } answer at another status may stand in for the file.`)
831
+ }
832
+ }
833
+ if (isStatusResponse(result)) {
834
+ const response = result as HandlerResponse
835
+ if (response.body !== undefined) {
836
+ return reply.status(response.status).send(response.body)
837
+ }
838
+ return reply.status(response.status).send()
839
+ }
840
+ return reply.status(200).send(result)
841
+ }
842
+
843
+ function credentialedWildcard(grant: CorsConfig | false): string | null {
844
+ if (grant === false || grant.credentials !== true) return null
845
+ const origin = grant.origin
846
+ if (origin === true || origin === '*' || (Array.isArray(origin) && origin.includes('*'))) {
847
+ return 'a grant with credentials: true must name its origins; origin: true or "*" lets any site read credentialed responses.\n' +
848
+ ' Fix: List the exact origins, or drop credentials.'
849
+ }
850
+ return null
851
+ }
852
+
853
+ function validateProductionConfig(config: ServerConfig): void {
854
+ const problems: string[] = []
855
+
856
+ if (config.cors !== undefined && config.cors !== false) {
857
+ const problem = credentialedWildcard(config.cors)
858
+ if (problem !== null) problems.push(`CORS: ${problem}`)
859
+ }
860
+
861
+ if (config.rateLimit === undefined && config.rateLimits === undefined) {
862
+ problems.push(
863
+ 'Rate limiting: Not configured. Production APIs should have rate limiting.\n' +
864
+ ' Fix: Set rateLimit: { max: 100, timeWindow: "1 minute" }, set rateLimits: { classes: {...} }, or set rateLimit: false to explicitly opt out.',
865
+ )
866
+ }
867
+
868
+ if (config.auth !== undefined && config.tenant === undefined) {
869
+ problems.push(
870
+ 'Tenant isolation: Auth is configured but no tenant config is set.\n' +
871
+ ' Fix: Set tenant: { mode: "schema" | "rls" | "header", ... } to enforce tenant isolation,\n' +
872
+ ' or acknowledge this by setting tenant: null in your server config.',
873
+ )
874
+ }
875
+
876
+ if (problems.length > 0) {
877
+ throw new Error(
878
+ `[pipework] Production server configuration is missing security settings:\n\n${problems.map((p, i) => ` ${i + 1}. ${p}`).join('\n\n')}\n\n` +
879
+ ' Pipework refuses to start a production server with insecure defaults.\n' +
880
+ ' Set each option explicitly to confirm your security posture.\n',
881
+ )
882
+ }
883
+ }
884
+
885
+ async function enforceHandlerPermission(
886
+ instance: Manifold,
887
+ ctx: import('../context/types.js').Flow,
888
+ meta: import('../di/types.js').HandlerMeta,
889
+ rbac: Rbac,
890
+ ): Promise<void> {
891
+ const permission = meta.permission!
892
+
893
+ if (ctx.auth === null || typeof ctx.auth !== 'object') {
894
+ throw new ForbiddenError(`Missing permission: ${permission.resource}:${permission.action}`)
895
+ }
896
+
897
+ const auth = ctx.auth as Record<string, unknown>
898
+ const userId = typeof auth['userId'] === 'string' ? auth['userId'] : null
899
+ const tenantId = ctx.tenant
900
+ const authVersion = typeof auth['authVersion'] === 'number' ? auth['authVersion'] : 1
901
+
902
+ if (userId === null || tenantId === null) {
903
+ throw new ForbiddenError(`Missing permission: ${permission.resource}:${permission.action}`)
904
+ }
905
+
906
+ const dbName = meta.databases[0] ?? [...instance.config.databases().keys()][0]
907
+ if (dbName === undefined) {
908
+ throw new ForbiddenError(`Missing permission: ${permission.resource}:${permission.action}`)
909
+ }
910
+
911
+ const permissionSet = await withTenantSetting(instance, dbName, tenantId, db => rbac.resolve(db, userId, tenantId, authVersion))
912
+ const granted = rbac.check(permissionSet, permission.resource, permission.action, permission.scope)
913
+ if (!granted) {
914
+ throw new ForbiddenError(`Missing permission: ${permission.resource}:${permission.action}`)
915
+ }
916
+ }