pipework 0.252.0 → 0.252.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (690) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/package.json +4 -3
  3. package/src/async/jobs/abortable-delay.ts +18 -0
  4. package/src/async/jobs/claims.ts +52 -0
  5. package/src/async/jobs/cron.ts +179 -0
  6. package/src/async/jobs/errors.ts +56 -0
  7. package/src/async/jobs/exclusion.ts +117 -0
  8. package/src/async/jobs/execute.ts +219 -0
  9. package/src/async/jobs/index.ts +10 -0
  10. package/src/async/jobs/listener.ts +135 -0
  11. package/src/async/jobs/namespace.ts +20 -0
  12. package/src/async/jobs/queue-door.ts +44 -0
  13. package/src/async/jobs/queue.ts +1012 -0
  14. package/src/async/jobs/run-script.ts +67 -0
  15. package/src/async/jobs/scheduler.ts +218 -0
  16. package/src/async/jobs/schedules-table.ts +37 -0
  17. package/src/async/jobs/serialize-error.ts +48 -0
  18. package/src/async/jobs/table.ts +191 -0
  19. package/src/async/pipeline/define.ts +53 -0
  20. package/src/async/pipeline/errors.ts +35 -0
  21. package/src/async/pipeline/execute.ts +53 -0
  22. package/src/async/pipeline/index.ts +13 -0
  23. package/src/async/pipeline/namespace.ts +12 -0
  24. package/src/async/pipeline/resume.ts +67 -0
  25. package/src/async/pipeline/run-steps.ts +98 -0
  26. package/src/async/pipeline/types.ts +57 -0
  27. package/src/async/state-machine/define.ts +64 -0
  28. package/src/async/state-machine/errors.ts +29 -0
  29. package/src/async/state-machine/index.ts +3 -0
  30. package/src/async/state-machine/namespace.ts +12 -0
  31. package/src/async/state-machine/transition.ts +90 -0
  32. package/src/async/state-machine/types.ts +37 -0
  33. package/src/async/steps/errors.ts +31 -0
  34. package/src/async/steps/graph.ts +1214 -0
  35. package/src/async/steps/hash.ts +40 -0
  36. package/src/async/steps/index.ts +7 -0
  37. package/src/async/steps/namespace.ts +7 -0
  38. package/src/async/steps/table.ts +97 -0
  39. package/src/async/steps/types.ts +318 -0
  40. package/src/audit/chain/index.ts +9 -0
  41. package/src/audit/chain/install.ts +113 -0
  42. package/src/audit/chain/retire.ts +52 -0
  43. package/src/audit/chain/verify.ts +257 -0
  44. package/src/audit/query/cursor.ts +37 -0
  45. package/src/audit/query/index.ts +9 -0
  46. package/src/audit/query/query.ts +223 -0
  47. package/src/audit/schema/audit-record-fields.ts +68 -0
  48. package/src/audit/schema/audit-record.ts +23 -0
  49. package/src/auth/auth/audit.ts +31 -0
  50. package/src/auth/auth/chain.ts +84 -0
  51. package/src/auth/auth/cookie.ts +136 -0
  52. package/src/auth/auth/email-links.ts +311 -0
  53. package/src/auth/auth/guard.ts +128 -0
  54. package/src/auth/auth/index.ts +18 -0
  55. package/src/auth/auth/multi-org.ts +337 -0
  56. package/src/auth/auth/namespace.ts +34 -0
  57. package/src/auth/auth/parse-expiry.ts +13 -0
  58. package/src/auth/auth/passwords.ts +110 -0
  59. package/src/auth/auth/schema.ts +110 -0
  60. package/src/auth/auth/sessions.ts +565 -0
  61. package/src/auth/auth/sign-in.ts +156 -0
  62. package/src/auth/auth/signing-keys.ts +69 -0
  63. package/src/auth/auth/types.ts +26 -0
  64. package/src/auth/auth/uuid.ts +10 -0
  65. package/src/auth/rbac/cache.ts +55 -0
  66. package/src/auth/rbac/check.ts +30 -0
  67. package/src/auth/rbac/create.ts +35 -0
  68. package/src/auth/rbac/index.ts +5 -0
  69. package/src/auth/rbac/middleware.ts +20 -0
  70. package/src/auth/rbac/namespace.ts +15 -0
  71. package/src/auth/rbac/resolve.ts +31 -0
  72. package/src/auth/rbac/schema.sql +31 -0
  73. package/src/auth/rbac/seed.ts +67 -0
  74. package/src/auth/rbac/tables.ts +32 -0
  75. package/src/auth/rbac/types.ts +65 -0
  76. package/src/auth/tenant/extract.ts +33 -0
  77. package/src/auth/tenant/in-tenant.ts +49 -0
  78. package/src/auth/tenant/index.ts +7 -0
  79. package/src/auth/tenant/namespace.ts +23 -0
  80. package/src/auth/tenant/propagate.ts +20 -0
  81. package/src/auth/tenant/rls.ts +120 -0
  82. package/src/auth/tenant/scope.ts +33 -0
  83. package/src/auth/tenant/scoped-db.ts +581 -0
  84. package/src/auth/tenant/types.ts +14 -0
  85. package/src/auth/tenant/validate.ts +27 -0
  86. package/src/cli/commands/add.ts +158 -0
  87. package/src/cli/commands/alerts.ts +98 -0
  88. package/src/cli/commands/check.ts +1418 -0
  89. package/src/cli/commands/codemod.ts +78 -0
  90. package/src/cli/commands/db.ts +356 -0
  91. package/src/cli/commands/dev.ts +101 -0
  92. package/src/cli/commands/generate.ts +75 -0
  93. package/src/cli/commands/init.ts +36 -0
  94. package/src/cli/commands/install.ts +28 -0
  95. package/src/cli/commands/lint.ts +62 -0
  96. package/src/cli/commands/migrate.ts +132 -0
  97. package/src/cli/commands/openapi.ts +63 -0
  98. package/src/cli/commands/remove.ts +72 -0
  99. package/src/cli/commands/run.ts +50 -0
  100. package/src/cli/commands/scope.ts +54 -0
  101. package/src/cli/commands/serve.ts +68 -0
  102. package/src/cli/commands/shard.ts +31 -0
  103. package/src/cli/commands/shared/config-pipework-version.ts +28 -0
  104. package/src/cli/commands/shared/find-config.ts +22 -0
  105. package/src/cli/commands/shared/load-config.ts +35 -0
  106. package/src/cli/commands/shared/pnpm-install.ts +95 -0
  107. package/src/cli/commands/shared/report-removed.ts +8 -0
  108. package/src/cli/commands/sync.ts +32 -0
  109. package/src/cli/commands/test.ts +307 -0
  110. package/src/cli/commands/upgrade.ts +48 -0
  111. package/src/cli/dev-bootstrap.ts +24 -0
  112. package/src/cli/errors.ts +10 -0
  113. package/src/cli/index.ts +303 -0
  114. package/src/cli/output.ts +17 -0
  115. package/src/cli/port.ts +15 -0
  116. package/src/cli/surface-kinds.ts +20 -0
  117. package/src/client/index.ts +4 -0
  118. package/src/client/poll.ts +130 -0
  119. package/src/client/report-fault.ts +119 -0
  120. package/src/core/config/app-pool.ts +19 -0
  121. package/src/core/config/aws-env.ts +29 -0
  122. package/src/core/config/base-dir.ts +24 -0
  123. package/src/core/config/discover.ts +64 -0
  124. package/src/core/config/env-resolve.ts +87 -0
  125. package/src/core/config/env-types.ts +116 -0
  126. package/src/core/config/env.ts +55 -0
  127. package/src/core/config/errors.ts +9 -0
  128. package/src/core/config/import-config.ts +40 -0
  129. package/src/core/config/index.ts +8 -0
  130. package/src/core/config/load.ts +417 -0
  131. package/src/core/config/namespace.ts +14 -0
  132. package/src/core/config/resolve.ts +195 -0
  133. package/src/core/config/schema.ts +488 -0
  134. package/src/core/config/statement-observer.ts +48 -0
  135. package/src/core/config/ts-inline-hooks.ts +75 -0
  136. package/src/core/config/ts-register.ts +4 -0
  137. package/src/core/config/ts-resolve-hooks.ts +101 -0
  138. package/src/core/errors/base.ts +49 -0
  139. package/src/core/errors/http.ts +146 -0
  140. package/src/core/errors/index.ts +2 -0
  141. package/src/core/errors/namespace.ts +31 -0
  142. package/src/core/lifecycle/drain.ts +33 -0
  143. package/src/core/lifecycle/health.ts +72 -0
  144. package/src/core/lifecycle/index.ts +4 -0
  145. package/src/core/lifecycle/namespace.ts +7 -0
  146. package/src/core/lifecycle/startup.ts +102 -0
  147. package/src/core/lifecycle/types.ts +36 -0
  148. package/src/core/logging/context.ts +60 -0
  149. package/src/core/logging/error-chain.ts +68 -0
  150. package/src/core/logging/index.ts +5 -0
  151. package/src/core/logging/logger.ts +65 -0
  152. package/src/core/logging/namespace.ts +16 -0
  153. package/src/core/logging/proxy.ts +22 -0
  154. package/src/core/logging/redact-match.ts +24 -0
  155. package/src/core/logging/types.ts +47 -0
  156. package/src/core/pipework.ts +566 -0
  157. package/src/core/runtime/broker.ts +69 -0
  158. package/src/core/surface/http.ts +133 -0
  159. package/src/core/surface/index.ts +11 -0
  160. package/src/core/surface/job-timeout.ts +40 -0
  161. package/src/core/surface/namespace.ts +13 -0
  162. package/src/core/surface/script.ts +33 -0
  163. package/src/core/surface/start.ts +142 -0
  164. package/src/core/surface/types.ts +225 -0
  165. package/src/core/surface/validate.ts +88 -0
  166. package/src/core/surface/worker.ts +528 -0
  167. package/src/core/validation/connectivity.ts +49 -0
  168. package/src/core/validation/domain.ts +99 -0
  169. package/src/core/validation/index.ts +5 -0
  170. package/src/core/validation/queue-tables.ts +312 -0
  171. package/src/core/validation/rls-role.ts +138 -0
  172. package/src/core/validation/startup.ts +51 -0
  173. package/src/core/validation/surfaces.ts +65 -0
  174. package/src/core/validation/types.ts +25 -0
  175. package/src/data/db/advisory-lock.ts +55 -0
  176. package/src/data/db/aggregates.ts +172 -0
  177. package/src/data/db/array-elements.ts +111 -0
  178. package/src/data/db/bulk-set.ts +53 -0
  179. package/src/data/db/col/index.ts +14 -0
  180. package/src/data/db/composed.ts +53 -0
  181. package/src/data/db/db.ts +276 -0
  182. package/src/data/db/excluded.ts +16 -0
  183. package/src/data/db/expressions.ts +473 -0
  184. package/src/data/db/filter.ts +90 -0
  185. package/src/data/db/function-source.ts +114 -0
  186. package/src/data/db/identifiers.ts +16 -0
  187. package/src/data/db/idx/index.ts +9 -0
  188. package/src/data/db/in-list.ts +49 -0
  189. package/src/data/db/index.ts +9 -0
  190. package/src/data/db/insert-split.ts +137 -0
  191. package/src/data/db/instrumentation.ts +82 -0
  192. package/src/data/db/maintenance.ts +28 -0
  193. package/src/data/db/namespace.ts +107 -0
  194. package/src/data/db/owner-labels.ts +9 -0
  195. package/src/data/db/pool.ts +187 -0
  196. package/src/data/db/query-label.ts +13 -0
  197. package/src/data/db/query-watch.ts +116 -0
  198. package/src/data/db/raw-client-warning.ts +37 -0
  199. package/src/data/db/readonly.ts +23 -0
  200. package/src/data/db/reservation.ts +151 -0
  201. package/src/data/db/schema-features.ts +14 -0
  202. package/src/data/db/serializable.ts +83 -0
  203. package/src/data/db/sql.ts +1 -0
  204. package/src/data/db/timeouts.ts +76 -0
  205. package/src/data/db/types.ts +18 -0
  206. package/src/data/db/unbound.ts +33 -0
  207. package/src/data/db/values.ts +29 -0
  208. package/src/data/domain/brand.ts +15 -0
  209. package/src/data/domain/build-factory.ts +193 -0
  210. package/src/data/domain/build-schema.ts +161 -0
  211. package/src/data/domain/build-table.ts +355 -0
  212. package/src/data/domain/define.ts +239 -0
  213. package/src/data/domain/external-registry.ts +34 -0
  214. package/src/data/domain/external.ts +77 -0
  215. package/src/data/domain/field.ts +279 -0
  216. package/src/data/domain/index.ts +13 -0
  217. package/src/data/domain/naming.ts +6 -0
  218. package/src/data/domain/projections.ts +57 -0
  219. package/src/data/domain/trace-safe-projection.ts +31 -0
  220. package/src/data/domain/traits.ts +108 -0
  221. package/src/data/domain/types.ts +289 -0
  222. package/src/data/jsonb/composed.ts +18 -0
  223. package/src/data/jsonb/index.ts +1 -0
  224. package/src/data/jsonb/namespace.ts +30 -0
  225. package/src/data/jsonb/query.ts +86 -0
  226. package/src/data/migrate/append-only-generate.ts +47 -0
  227. package/src/data/migrate/applied.ts +47 -0
  228. package/src/data/migrate/apply.ts +309 -0
  229. package/src/data/migrate/check.ts +119 -0
  230. package/src/data/migrate/diff.ts +357 -0
  231. package/src/data/migrate/door-generate.ts +130 -0
  232. package/src/data/migrate/framework-rls.ts +58 -0
  233. package/src/data/migrate/generate.ts +201 -0
  234. package/src/data/migrate/history.ts +215 -0
  235. package/src/data/migrate/index.ts +9 -0
  236. package/src/data/migrate/internal-definitions.ts +175 -0
  237. package/src/data/migrate/post-process.ts +73 -0
  238. package/src/data/migrate/pragma.ts +73 -0
  239. package/src/data/migrate/provision-framework.ts +147 -0
  240. package/src/data/migrate/rls-generate.ts +61 -0
  241. package/src/data/migrate/run.ts +124 -0
  242. package/src/data/migrate/safety.ts +167 -0
  243. package/src/data/migrate/snapshot.ts +368 -0
  244. package/src/data/migrate/sql-emit.ts +392 -0
  245. package/src/data/migrate/statements.ts +312 -0
  246. package/src/data/query/alias.ts +126 -0
  247. package/src/data/query/batch.ts +8 -0
  248. package/src/data/query/cache/core/cache.ts +78 -0
  249. package/src/data/query/cache/core/index.ts +1 -0
  250. package/src/data/query/cache/core/types.ts +29 -0
  251. package/src/data/query/cache/readme.md +230 -0
  252. package/src/data/query/casing.ts +76 -0
  253. package/src/data/query/collation.ts +11 -0
  254. package/src/data/query/column-builder.ts +370 -0
  255. package/src/data/query/column.ts +150 -0
  256. package/src/data/query/entity.ts +42 -0
  257. package/src/data/query/errors.ts +33 -0
  258. package/src/data/query/index.ts +14 -0
  259. package/src/data/query/logger.ts +47 -0
  260. package/src/data/query/operations.ts +45 -0
  261. package/src/data/query/pg-core/alias.ts +12 -0
  262. package/src/data/query/pg-core/checks.ts +32 -0
  263. package/src/data/query/pg-core/columns/all.ts +71 -0
  264. package/src/data/query/pg-core/columns/bigint.ts +110 -0
  265. package/src/data/query/pg-core/columns/bigserial.ts +128 -0
  266. package/src/data/query/pg-core/columns/boolean.ts +43 -0
  267. package/src/data/query/pg-core/columns/char.ts +85 -0
  268. package/src/data/query/pg-core/columns/cidr.ts +43 -0
  269. package/src/data/query/pg-core/columns/common.ts +366 -0
  270. package/src/data/query/pg-core/columns/custom.ts +232 -0
  271. package/src/data/query/pg-core/columns/date.common.ts +15 -0
  272. package/src/data/query/pg-core/columns/date.ts +112 -0
  273. package/src/data/query/pg-core/columns/double-precision.ts +55 -0
  274. package/src/data/query/pg-core/columns/enum.ts +201 -0
  275. package/src/data/query/pg-core/columns/index.ts +35 -0
  276. package/src/data/query/pg-core/columns/inet.ts +43 -0
  277. package/src/data/query/pg-core/columns/int.common.ts +57 -0
  278. package/src/data/query/pg-core/columns/integer.ts +53 -0
  279. package/src/data/query/pg-core/columns/interval.ts +83 -0
  280. package/src/data/query/pg-core/columns/json.ts +58 -0
  281. package/src/data/query/pg-core/columns/jsonb.ts +56 -0
  282. package/src/data/query/pg-core/columns/line.ts +117 -0
  283. package/src/data/query/pg-core/columns/macaddr.ts +43 -0
  284. package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
  285. package/src/data/query/pg-core/columns/numeric.ts +227 -0
  286. package/src/data/query/pg-core/columns/point.ts +127 -0
  287. package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
  288. package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
  289. package/src/data/query/pg-core/columns/real.ts +58 -0
  290. package/src/data/query/pg-core/columns/serial.ts +55 -0
  291. package/src/data/query/pg-core/columns/smallint.ts +53 -0
  292. package/src/data/query/pg-core/columns/smallserial.ts +60 -0
  293. package/src/data/query/pg-core/columns/text.ts +67 -0
  294. package/src/data/query/pg-core/columns/time.ts +72 -0
  295. package/src/data/query/pg-core/columns/timestamp.ts +163 -0
  296. package/src/data/query/pg-core/columns/uuid.ts +51 -0
  297. package/src/data/query/pg-core/columns/varchar.ts +89 -0
  298. package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
  299. package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
  300. package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
  301. package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
  302. package/src/data/query/pg-core/db.ts +674 -0
  303. package/src/data/query/pg-core/dialect.ts +1404 -0
  304. package/src/data/query/pg-core/expressions.ts +25 -0
  305. package/src/data/query/pg-core/foreign-keys.ts +130 -0
  306. package/src/data/query/pg-core/index.ts +21 -0
  307. package/src/data/query/pg-core/indexes.ts +276 -0
  308. package/src/data/query/pg-core/policies.ts +56 -0
  309. package/src/data/query/pg-core/primary-keys.ts +62 -0
  310. package/src/data/query/pg-core/query-builders/count.ts +86 -0
  311. package/src/data/query/pg-core/query-builders/delete.ts +293 -0
  312. package/src/data/query/pg-core/query-builders/index.ts +7 -0
  313. package/src/data/query/pg-core/query-builders/insert.ts +452 -0
  314. package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
  315. package/src/data/query/pg-core/query-builders/query.ts +157 -0
  316. package/src/data/query/pg-core/query-builders/raw.ts +49 -0
  317. package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
  318. package/src/data/query/pg-core/query-builders/select.ts +1346 -0
  319. package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
  320. package/src/data/query/pg-core/query-builders/update.ts +626 -0
  321. package/src/data/query/pg-core/roles.ts +41 -0
  322. package/src/data/query/pg-core/schema.ts +72 -0
  323. package/src/data/query/pg-core/sequence.ts +41 -0
  324. package/src/data/query/pg-core/session.ts +292 -0
  325. package/src/data/query/pg-core/subquery.ts +46 -0
  326. package/src/data/query/pg-core/table.ts +252 -0
  327. package/src/data/query/pg-core/unique-constraint.ts +73 -0
  328. package/src/data/query/pg-core/utils/array.ts +95 -0
  329. package/src/data/query/pg-core/utils/index.ts +1 -0
  330. package/src/data/query/pg-core/utils.ts +104 -0
  331. package/src/data/query/pg-core/view-base.ts +14 -0
  332. package/src/data/query/pg-core/view-common.ts +1 -0
  333. package/src/data/query/pg-core/view.ts +435 -0
  334. package/src/data/query/pg-core/with-builder.ts +41 -0
  335. package/src/data/query/postgres-js/README.md +46 -0
  336. package/src/data/query/postgres-js/array-parser.ts +45 -0
  337. package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
  338. package/src/data/query/postgres-js/driver.ts +163 -0
  339. package/src/data/query/postgres-js/index.ts +2 -0
  340. package/src/data/query/postgres-js/session.ts +234 -0
  341. package/src/data/query/primary-key.ts +11 -0
  342. package/src/data/query/query-builders/query-builder.ts +19 -0
  343. package/src/data/query/query-builders/select.types.ts +175 -0
  344. package/src/data/query/query-promise.ts +35 -0
  345. package/src/data/query/relations.ts +725 -0
  346. package/src/data/query/runnable-query.ts +12 -0
  347. package/src/data/query/selection-proxy.ts +121 -0
  348. package/src/data/query/session.ts +8 -0
  349. package/src/data/query/sql/expressions/conditions.ts +741 -0
  350. package/src/data/query/sql/expressions/index.ts +2 -0
  351. package/src/data/query/sql/expressions/select.ts +45 -0
  352. package/src/data/query/sql/functions/aggregate.ts +129 -0
  353. package/src/data/query/sql/functions/index.ts +2 -0
  354. package/src/data/query/sql/functions/vector.ts +178 -0
  355. package/src/data/query/sql/index.ts +3 -0
  356. package/src/data/query/sql/sql.ts +794 -0
  357. package/src/data/query/subquery.ts +53 -0
  358. package/src/data/query/table.ts +208 -0
  359. package/src/data/query/table.utils.ts +2 -0
  360. package/src/data/query/tracing-utils.ts +3 -0
  361. package/src/data/query/tracing.ts +11 -0
  362. package/src/data/query/utils.ts +357 -0
  363. package/src/data/query/version.ts +4 -0
  364. package/src/data/query/view-common.ts +1 -0
  365. package/src/data/schema/branded.ts +13 -0
  366. package/src/data/schema/generated/coerce.ts +5 -0
  367. package/src/data/schema/generated/combinator.ts +6 -0
  368. package/src/data/schema/generated/encoding.ts +5 -0
  369. package/src/data/schema/generated/error.ts +6 -0
  370. package/src/data/schema/generated/format.ts +5 -0
  371. package/src/data/schema/generated/type-constructor.ts +10 -0
  372. package/src/data/schema/generated/type.ts +6 -0
  373. package/src/data/schema/index.ts +6 -0
  374. package/src/data/schema/json-schema.ts +33 -0
  375. package/src/data/schema/namespace.ts +55 -0
  376. package/src/data/schema/parse.ts +39 -0
  377. package/src/data/schema/types.ts +39 -0
  378. package/src/data/temporal/append-only.ts +134 -0
  379. package/src/data/temporal/close.ts +27 -0
  380. package/src/data/temporal/columns.ts +10 -0
  381. package/src/data/temporal/definition-queries.ts +70 -0
  382. package/src/data/temporal/errors.ts +10 -0
  383. package/src/data/temporal/get-current.ts +22 -0
  384. package/src/data/temporal/index.ts +9 -0
  385. package/src/data/temporal/namespace.ts +36 -0
  386. package/src/data/temporal/query.ts +25 -0
  387. package/src/data/temporal/resolve.ts +17 -0
  388. package/src/data/temporal/revise.ts +67 -0
  389. package/src/data/temporal/types.ts +22 -0
  390. package/src/data/valve/artifact.ts +33 -0
  391. package/src/data/valve/contract-diff.ts +21 -0
  392. package/src/data/valve/cross.ts +57 -0
  393. package/src/data/valve/crossing-ledger.ts +50 -0
  394. package/src/data/valve/decision.ts +44 -0
  395. package/src/data/valve/durable-crossing.ts +52 -0
  396. package/src/data/valve/execute-crossing.ts +77 -0
  397. package/src/data/valve/faucet.ts +56 -0
  398. package/src/data/valve/gate.ts +114 -0
  399. package/src/data/valve/graph.ts +77 -0
  400. package/src/data/valve/index.ts +53 -0
  401. package/src/data/valve/namespace.ts +57 -0
  402. package/src/data/valve/registry.ts +50 -0
  403. package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
  404. package/src/data/valve/schema/valve-crossing.ts +9 -0
  405. package/src/data/valve/supply.ts +154 -0
  406. package/src/data/valve/types.ts +143 -0
  407. package/src/data/vector/columns.ts +27 -0
  408. package/src/data/vector/index.ts +4 -0
  409. package/src/data/vector/namespace.ts +33 -0
  410. package/src/data/vector/query.ts +85 -0
  411. package/src/data/vector/validate.ts +16 -0
  412. package/src/data/write/audit/actor.ts +15 -0
  413. package/src/data/write/audit/audit-columns.ts +18 -0
  414. package/src/data/write/audit/audit-select.ts +111 -0
  415. package/src/data/write/audit/bound-transaction.ts +14 -0
  416. package/src/data/write/audit/cte-compile.ts +32 -0
  417. package/src/data/write/audit/decode-returning.ts +47 -0
  418. package/src/data/write/audit/final-select.ts +43 -0
  419. package/src/data/write/audit/infra-audit.ts +36 -0
  420. package/src/data/write/audit/primary-key.ts +65 -0
  421. package/src/data/write/audit/projection.ts +41 -0
  422. package/src/data/write/audit/read-touches.ts +86 -0
  423. package/src/data/write/audit/touch-match.ts +78 -0
  424. package/src/data/write/audit/verb-delete.ts +62 -0
  425. package/src/data/write/audit/verb-insert.ts +36 -0
  426. package/src/data/write/audit/verb-select.ts +47 -0
  427. package/src/data/write/audit/verb-update.ts +60 -0
  428. package/src/data/write/audit/verb-upsert.ts +104 -0
  429. package/src/data/write/conflict.ts +43 -0
  430. package/src/data/write/delete.ts +38 -0
  431. package/src/data/write/execute.ts +243 -0
  432. package/src/data/write/index.ts +11 -0
  433. package/src/data/write/insert.ts +63 -0
  434. package/src/data/write/state.ts +43 -0
  435. package/src/data/write/update.ts +54 -0
  436. package/src/data/write/upsert.ts +58 -0
  437. package/src/data/write/values.ts +19 -0
  438. package/src/index.ts +230 -0
  439. package/src/infra/access-links/code.ts +70 -0
  440. package/src/infra/access-links/handle.ts +892 -0
  441. package/src/infra/access-links/index.ts +9 -0
  442. package/src/infra/access-links/namespace.ts +30 -0
  443. package/src/infra/access-links/person-check.ts +177 -0
  444. package/src/infra/access-links/schema.ts +71 -0
  445. package/src/infra/alerts/async-sources.ts +231 -0
  446. package/src/infra/alerts/audit.ts +36 -0
  447. package/src/infra/alerts/beat.ts +123 -0
  448. package/src/infra/alerts/client-faults.ts +111 -0
  449. package/src/infra/alerts/faults.ts +156 -0
  450. package/src/infra/alerts/handle.ts +778 -0
  451. package/src/infra/alerts/http.ts +192 -0
  452. package/src/infra/alerts/index.ts +12 -0
  453. package/src/infra/alerts/infra-sources.ts +180 -0
  454. package/src/infra/alerts/namespace.ts +81 -0
  455. package/src/infra/alerts/redact.ts +21 -0
  456. package/src/infra/alerts/request-faults.ts +41 -0
  457. package/src/infra/alerts/schema.ts +124 -0
  458. package/src/infra/alerts/sinks.ts +125 -0
  459. package/src/infra/alerts/source-options.ts +26 -0
  460. package/src/infra/alerts/sources.ts +503 -0
  461. package/src/infra/alerts/subsystem-sources.ts +137 -0
  462. package/src/infra/audit/emit.ts +87 -0
  463. package/src/infra/audit/index.ts +2 -0
  464. package/src/infra/audit/namespace.ts +22 -0
  465. package/src/infra/audit/schema.sql +35 -0
  466. package/src/infra/audit/types.ts +18 -0
  467. package/src/infra/aws/credentials.ts +147 -0
  468. package/src/infra/aws/sigv4.ts +140 -0
  469. package/src/infra/cache/create.ts +58 -0
  470. package/src/infra/cache/entry.ts +81 -0
  471. package/src/infra/cache/index.ts +4 -0
  472. package/src/infra/cache/namespace.ts +10 -0
  473. package/src/infra/cache/tenant.ts +93 -0
  474. package/src/infra/cache/types.ts +24 -0
  475. package/src/infra/crawl/audit.ts +35 -0
  476. package/src/infra/crawl/errors.ts +25 -0
  477. package/src/infra/crawl/handle.ts +693 -0
  478. package/src/infra/crawl/index.ts +9 -0
  479. package/src/infra/crawl/links.ts +160 -0
  480. package/src/infra/crawl/namespace.ts +36 -0
  481. package/src/infra/crawl/robots.ts +100 -0
  482. package/src/infra/crawl/schema.ts +89 -0
  483. package/src/infra/credit-line/handle.ts +883 -0
  484. package/src/infra/credit-line/index.ts +6 -0
  485. package/src/infra/credit-line/namespace.ts +7 -0
  486. package/src/infra/credit-line/schema.ts +96 -0
  487. package/src/infra/credits/handle.ts +706 -0
  488. package/src/infra/credits/index.ts +5 -0
  489. package/src/infra/credits/namespace.ts +7 -0
  490. package/src/infra/credits/schema.ts +83 -0
  491. package/src/infra/email/audit.ts +36 -0
  492. package/src/infra/email/inbound.ts +622 -0
  493. package/src/infra/email/index.ts +11 -0
  494. package/src/infra/email/mail.ts +517 -0
  495. package/src/infra/email/mime.ts +447 -0
  496. package/src/infra/email/namespace.ts +22 -0
  497. package/src/infra/email/providers.ts +111 -0
  498. package/src/infra/email/schema.ts +96 -0
  499. package/src/infra/email/sns.ts +200 -0
  500. package/src/infra/email/unsubscribe.ts +44 -0
  501. package/src/infra/fetch/address.ts +88 -0
  502. package/src/infra/fetch/connect-retry.ts +87 -0
  503. package/src/infra/fetch/handle.ts +329 -0
  504. package/src/infra/fetch/index.ts +7 -0
  505. package/src/infra/fetch/namespace.ts +70 -0
  506. package/src/infra/idempotency/index.ts +3 -0
  507. package/src/infra/idempotency/middleware.ts +87 -0
  508. package/src/infra/idempotency/store.ts +92 -0
  509. package/src/infra/idempotency/types.ts +30 -0
  510. package/src/infra/join-requests/handle.ts +312 -0
  511. package/src/infra/join-requests/index.ts +2 -0
  512. package/src/infra/join-requests/namespace.ts +7 -0
  513. package/src/infra/join-requests/schema.ts +33 -0
  514. package/src/infra/outbox/core.ts +165 -0
  515. package/src/infra/quota/errors.ts +32 -0
  516. package/src/infra/quota/handle.ts +215 -0
  517. package/src/infra/quota/index.ts +5 -0
  518. package/src/infra/quota/namespace.ts +7 -0
  519. package/src/infra/quota/schema.ts +15 -0
  520. package/src/infra/storage/backend.ts +34 -0
  521. package/src/infra/storage/handle.ts +513 -0
  522. package/src/infra/storage/index.ts +9 -0
  523. package/src/infra/storage/local.ts +104 -0
  524. package/src/infra/storage/namespace.ts +13 -0
  525. package/src/infra/storage/s3.ts +257 -0
  526. package/src/infra/storage/schema.ts +24 -0
  527. package/src/infra/support/handle.ts +854 -0
  528. package/src/infra/support/http.ts +229 -0
  529. package/src/infra/support/index.ts +7 -0
  530. package/src/infra/support/namespace.ts +12 -0
  531. package/src/infra/support/schema.ts +96 -0
  532. package/src/infra/webhook/inbound.ts +45 -0
  533. package/src/infra/webhook/index.ts +5 -0
  534. package/src/infra/webhook/namespace.ts +24 -0
  535. package/src/infra/webhook/outbound.ts +315 -0
  536. package/src/infra/webhook/sign.ts +32 -0
  537. package/src/infra/webhook/verify.ts +82 -0
  538. package/src/invariants/assert.ts +26 -0
  539. package/src/invariants/index.ts +1 -0
  540. package/src/lint/config.ts +56 -0
  541. package/src/lint/doctrine.ts +163 -0
  542. package/src/lint/include.ts +97 -0
  543. package/src/lint/index.ts +3 -0
  544. package/src/lint/preset.ts +10 -0
  545. package/src/lint/resolver.ts +218 -0
  546. package/src/lint/rules/no-reexports.ts +46 -0
  547. package/src/request/behavior/audited.ts +85 -0
  548. package/src/request/behavior/cached.ts +62 -0
  549. package/src/request/behavior/compose.ts +28 -0
  550. package/src/request/behavior/index.ts +5 -0
  551. package/src/request/behavior/namespace.ts +16 -0
  552. package/src/request/behavior/types.ts +26 -0
  553. package/src/request/behavior/versioned.ts +54 -0
  554. package/src/request/context/create.ts +103 -0
  555. package/src/request/context/guard.ts +49 -0
  556. package/src/request/context/index.ts +4 -0
  557. package/src/request/context/run-in-transaction.ts +200 -0
  558. package/src/request/context/store.ts +28 -0
  559. package/src/request/context/types.ts +50 -0
  560. package/src/request/di/builder.ts +535 -0
  561. package/src/request/di/index.ts +8 -0
  562. package/src/request/di/resolve.ts +271 -0
  563. package/src/request/di/types.ts +262 -0
  564. package/src/request/fitting/namespace.ts +7 -0
  565. package/src/request/fixture/batch.ts +71 -0
  566. package/src/request/fixture/builder.ts +26 -0
  567. package/src/request/fixture/errors.ts +10 -0
  568. package/src/request/fixture/index.ts +8 -0
  569. package/src/request/fixture/namespace.ts +23 -0
  570. package/src/request/fixture/paginate.ts +44 -0
  571. package/src/request/fixture/to-handlers.ts +260 -0
  572. package/src/request/fixture/types.ts +79 -0
  573. package/src/request/flow/act-as.ts +78 -0
  574. package/src/request/flow/namespace.ts +17 -0
  575. package/src/request/http/error-step.ts +42 -0
  576. package/src/request/http/file.ts +141 -0
  577. package/src/request/http/index.ts +4 -0
  578. package/src/request/http/middleware.ts +141 -0
  579. package/src/request/http/namespace.ts +5 -0
  580. package/src/request/http/rate-limit-classes.ts +170 -0
  581. package/src/request/http/response.ts +82 -0
  582. package/src/request/http/server.ts +916 -0
  583. package/src/request/http/sniff.ts +261 -0
  584. package/src/request/http/types.ts +250 -0
  585. package/src/request/http/upload.ts +284 -0
  586. package/src/request/internal/text.ts +6 -0
  587. package/src/request/openapi/generate.ts +205 -0
  588. package/src/request/openapi/index.ts +2 -0
  589. package/src/request/openapi/namespace.ts +7 -0
  590. package/src/test/auth.ts +13 -0
  591. package/src/test/auto-setup.ts +45 -0
  592. package/src/test/context.ts +91 -0
  593. package/src/test/crawl-fixture.ts +157 -0
  594. package/src/test/discover-fork.ts +17 -0
  595. package/src/test/fingerprint.ts +135 -0
  596. package/src/test/globals.d.ts +7 -0
  597. package/src/test/index.ts +19 -0
  598. package/src/test/namespace.ts +87 -0
  599. package/src/test/plugin.ts +189 -0
  600. package/src/test/progress.ts +185 -0
  601. package/src/test/reap-after-run.ts +28 -0
  602. package/src/test/reap.ts +286 -0
  603. package/src/test/reset.ts +253 -0
  604. package/src/test/savepoints.ts +172 -0
  605. package/src/test/setup.ts +1422 -0
  606. package/src/test/system-channel.ts +50 -0
  607. package/src/test/vitest-plugin.ts +142 -0
  608. package/src/test/vitest-workspace.ts +507 -0
  609. package/src/test/vitest.ts +333 -0
  610. package/src/test/watch.ts +105 -0
  611. package/src/trace/capture/bounded-serializer.ts +146 -0
  612. package/src/trace/cardinality/analyze.ts +173 -0
  613. package/src/trace/cardinality/branch-context.ts +214 -0
  614. package/src/trace/cardinality/check.ts +33 -0
  615. package/src/trace/cardinality/classify-edge.ts +64 -0
  616. package/src/trace/cardinality/structural-max.ts +107 -0
  617. package/src/trace/check/coverage-check.ts +188 -0
  618. package/src/trace/check/flow-check.ts +103 -0
  619. package/src/trace/check/flow-step-site.ts +234 -0
  620. package/src/trace/check/io-pattern.ts +116 -0
  621. package/src/trace/codemod/add-cardinality.ts +152 -0
  622. package/src/trace/codemod/run.ts +80 -0
  623. package/src/trace/entry/child-trace.ts +21 -0
  624. package/src/trace/entry/close-trace.ts +87 -0
  625. package/src/trace/entry/finalize-trace.ts +28 -0
  626. package/src/trace/entry/open-trace.ts +138 -0
  627. package/src/trace/entry/parent-trace.ts +20 -0
  628. package/src/trace/entry/parse-traceparent.ts +39 -0
  629. package/src/trace/entry/process-start.ts +38 -0
  630. package/src/trace/entry/trace-context.ts +26 -0
  631. package/src/trace/entry/tracing-enabled.ts +10 -0
  632. package/src/trace/external/record.ts +195 -0
  633. package/src/trace/external/report.ts +66 -0
  634. package/src/trace/namespace.ts +19 -0
  635. package/src/trace/partition/day.ts +47 -0
  636. package/src/trace/partition/index.ts +14 -0
  637. package/src/trace/partition/partition-name.ts +23 -0
  638. package/src/trace/partition/plan.ts +82 -0
  639. package/src/trace/partition/schedule.ts +26 -0
  640. package/src/trace/partition/startup-hook.ts +39 -0
  641. package/src/trace/partition/sync.ts +154 -0
  642. package/src/trace/partition/tables.ts +53 -0
  643. package/src/trace/read/tree.ts +83 -0
  644. package/src/trace/retain/copy-on-error.ts +76 -0
  645. package/src/trace/runtime/expire-stale-traces.ts +54 -0
  646. package/src/trace/runtime/flow-step.ts +147 -0
  647. package/src/trace/runtime/flush-steps.ts +31 -0
  648. package/src/trace/runtime/pending-writes.ts +54 -0
  649. package/src/trace/runtime/step-buffer.ts +298 -0
  650. package/src/trace/runtime/step-error.ts +21 -0
  651. package/src/trace/runtime/step-meta.ts +55 -0
  652. package/src/trace/runtime/thenable.ts +12 -0
  653. package/src/trace/runtime/truncate-trace.ts +62 -0
  654. package/src/trace/schema/trace-fields.ts +52 -0
  655. package/src/trace/schema/trace-retained.ts +22 -0
  656. package/src/trace/schema/trace-step-fields.ts +44 -0
  657. package/src/trace/schema/trace-step-retained.ts +25 -0
  658. package/src/trace/schema/trace-step.ts +18 -0
  659. package/src/trace/schema/trace.ts +12 -0
  660. package/src/trace/transformer/apply.ts +79 -0
  661. package/src/trace/transformer/build-meta-literal.ts +30 -0
  662. package/src/trace/transformer/find-flow-step-sites.ts +85 -0
  663. package/src/trace/transformer/inject-flow-step-import.ts +39 -0
  664. package/src/trace/transformer/transform-error.ts +14 -0
  665. package/src/trace/transformer/transform-flow-steps.ts +123 -0
  666. package/src/trace/transformer/wrap-function-declaration.ts +62 -0
  667. package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
  668. package/src/workspace/affected.ts +110 -0
  669. package/src/workspace/changed-files.ts +163 -0
  670. package/src/workspace/check-deps.ts +213 -0
  671. package/src/workspace/checksum.ts +20 -0
  672. package/src/workspace/dependency-graph.ts +67 -0
  673. package/src/workspace/detect-pipework-copies.ts +93 -0
  674. package/src/workspace/discover.ts +206 -0
  675. package/src/workspace/generate.ts +401 -0
  676. package/src/workspace/index.ts +7 -0
  677. package/src/workspace/module-imports-pipework.ts +39 -0
  678. package/src/workspace/module-package-name.ts +12 -0
  679. package/src/workspace/scrub-git-env.ts +23 -0
  680. package/src/workspace/source-paths.ts +36 -0
  681. package/src/workspace/sync.ts +182 -0
  682. package/dist/REFERENCE.md +0 -1477
  683. package/dist/infra/access-links/rate-limit.d.ts +0 -10
  684. package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
  685. package/dist/infra/access-links/rate-limit.js +0 -28
  686. package/dist/infra/access-links/rate-limit.js.map +0 -1
  687. package/dist/infra/email/sigv4.d.ts +0 -24
  688. package/dist/infra/email/sigv4.d.ts.map +0 -1
  689. package/dist/infra/email/sigv4.js +0 -55
  690. package/dist/infra/email/sigv4.js.map +0 -1
@@ -0,0 +1,329 @@
1
+ import { request as httpRequest, type ClientRequest, type IncomingMessage } from 'node:http'
2
+ import { request as httpsRequest } from 'node:https'
3
+ import { lookup as dnsLookup } from 'node:dns/promises'
4
+ import { isIP, type Socket } from 'node:net'
5
+ import { connect as tlsConnect } from 'node:tls'
6
+ import { __flowStep } from '../../trace/runtime/flow-step.js'
7
+ import { isPublicAddress } from './address.js'
8
+
9
+ /** What the app is shown of a response before the body is kept. */
10
+ export interface FetchHead {
11
+ status: number
12
+ contentType: string | null
13
+ /** The first bytes of the body: up to 1024, fewer when the body is shorter. */
14
+ firstBytes: Uint8Array
15
+ }
16
+
17
+ /** The connection the fetch is about to open, after the address check passed. */
18
+ export interface FetchTarget {
19
+ address: string
20
+ port: number
21
+ hostname: string
22
+ protocol: 'http:' | 'https:'
23
+ }
24
+
25
+ /** Options for `untrustedFetch.get`. The three limits are required: the app says. */
26
+ export interface UntrustedFetchOptions {
27
+ /** The most body bytes kept; the fetch stops the moment the total passes it. */
28
+ maxBytes: number
29
+ /** One time limit for the whole fetch, DNS and redirects included. */
30
+ timeoutMs: number
31
+ /** The most redirects followed. */
32
+ maxRedirects: number
33
+ /** Called with the status, content type and first bytes before the rest of the body is kept. Return false, or a message string, to refuse. */
34
+ accept?: (head: FetchHead) => boolean | string
35
+ /**
36
+ * Called before each redirect hop with the hop's absolute URL and the URL it came from. Return true to follow it; false, or a
37
+ * message string, refuses with `rejected`. The address check still runs on every hop that is followed.
38
+ */
39
+ followRedirect?: (to: string, from: string) => boolean | string
40
+ /** Headers sent to the first URL's origin only, never to another origin after a redirect. */
41
+ headers?: Record<string, string>
42
+ /** When true the full URL is recorded in the trace step; otherwise only scheme and host. */
43
+ urlIsSafeToTrace?: boolean
44
+ /** For tests: replaces DNS. Its answer is still address-checked. */
45
+ resolve?: (hostname: string) => Promise<string[]>
46
+ /** For tests: replaces the socket, after the address check passed for `target.address`. */
47
+ connect?: (target: FetchTarget) => Socket
48
+ }
49
+
50
+ /** Options for `untrustedFetch.post`: the same rules and limits as `get`, plus the body sent. */
51
+ export interface UntrustedPostOptions extends UntrustedFetchOptions {
52
+ /** The request body, sent as given; the app sets its own `content-type` in `headers`. */
53
+ body: string | Uint8Array
54
+ }
55
+
56
+ /** What is sent: GET has no body. */
57
+ interface Spec { method: 'GET' | 'POST'; body: Buffer | null }
58
+
59
+ export type FetchRefusal = 'scheme' | 'address' | 'redirects' | 'too_large' | 'timeout' | 'status' | 'rejected' | 'network'
60
+
61
+ /** A fetched body, or the rule that refused. Never thrown. */
62
+ export type UntrustedFetchResult =
63
+ | { ok: true; url: string; status: number; contentType: string | null; etag: string | null; lastModified: string | null; body: Uint8Array; bytes: number }
64
+ | { ok: false; reason: FetchRefusal; message: string; status?: number }
65
+
66
+ const REDIRECTS = new Set([301, 302, 303, 307, 308])
67
+ const HEAD_BYTES = 1024
68
+
69
+ class Refusal extends Error {
70
+ constructor(readonly reason: FetchRefusal, message: string, readonly status?: number) { super(message) }
71
+ }
72
+
73
+ const MESSAGES = {
74
+ scheme: 'Only http and https links can be fetched.',
75
+ address: 'The link points to a private network address.',
76
+ redirects: 'The link redirects too many times.',
77
+ too_large: 'The file is larger than allowed.',
78
+ timeout: 'The server took too long to answer.',
79
+ network: 'The server could not be reached.',
80
+ } as const
81
+
82
+ async function defaultResolve(hostname: string): Promise<string[]> {
83
+ return (await dnsLookup(hostname, { all: true, verbatim: true })).map(a => a.address)
84
+ }
85
+
86
+ interface Run { aborted: boolean; current: ClientRequest | null }
87
+
88
+ function bareHost(url: URL): string {
89
+ return url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname
90
+ }
91
+
92
+ async function fetchOnce(
93
+ run: Run, url: URL, options: UntrustedFetchOptions, headers: Record<string, string>, spec: Spec,
94
+ ): Promise<{ redirect: string; status: number } | { final: IncomingMessage; req: ClientRequest }> {
95
+ const hostname = bareHost(url)
96
+ const protocol = url.protocol as 'http:' | 'https:'
97
+ let addresses: string[]
98
+ if (isIP(hostname) !== 0) {
99
+ addresses = [hostname]
100
+ } else {
101
+ try {
102
+ addresses = await (options.resolve ?? defaultResolve)(hostname)
103
+ } catch {
104
+ throw new Refusal('network', MESSAGES.network)
105
+ }
106
+ }
107
+ if (run.aborted) throw new Refusal('timeout', MESSAGES.timeout)
108
+ if (addresses.length === 0) throw new Refusal('network', MESSAGES.network)
109
+ if (!addresses.every(isPublicAddress)) throw new Refusal('address', MESSAGES.address)
110
+ const address = addresses[0]!
111
+ const port = url.port === '' ? (protocol === 'https:' ? 443 : 80) : Number(url.port)
112
+
113
+ const base = {
114
+ method: spec.method, hostname, port, path: url.pathname + url.search,
115
+ headers: {
116
+ 'accept-encoding': 'identity', ...headers, host: url.host,
117
+ ...(spec.body !== null ? { 'content-length': String(spec.body.length) } : {}),
118
+ },
119
+ }
120
+ const doRequest = protocol === 'https:' ? httpsRequest : httpRequest
121
+ const req = options.connect !== undefined
122
+ ? doRequest({
123
+ ...base, agent: undefined,
124
+ createConnection: () => {
125
+ const socket = options.connect!({ address, port, hostname, protocol })
126
+ return protocol === 'https:' ? tlsConnect({ socket, ...(isIP(hostname) === 0 ? { servername: hostname } : {}) }) : socket
127
+ },
128
+ })
129
+ : doRequest({
130
+ ...base, agent: false,
131
+ lookup: (_h, opts, cb) => {
132
+ const entry = { address, family: isIP(address) }
133
+ if ((opts as { all?: boolean }).all === true) (cb as unknown as (e: null, a: unknown) => void)(null, [entry])
134
+ else cb(null, address, entry.family)
135
+ },
136
+ })
137
+ run.current = req
138
+ return await new Promise((resolve, reject) => {
139
+ req.on('error', () => { reject(run.aborted ? new Refusal('timeout', MESSAGES.timeout) : new Refusal('network', MESSAGES.network)) })
140
+ req.on('response', res => {
141
+ const status = res.statusCode ?? 0
142
+ const location = res.headers['location']
143
+ if (REDIRECTS.has(status) && typeof location === 'string') {
144
+ res.destroy()
145
+ req.destroy()
146
+ resolve({ redirect: location, status })
147
+ return
148
+ }
149
+ resolve({ final: res, req })
150
+ })
151
+ if (spec.body !== null) req.end(spec.body)
152
+ else req.end()
153
+ })
154
+ }
155
+
156
+ async function readBody(
157
+ res: IncomingMessage, req: ClientRequest, options: UntrustedFetchOptions, run: Run, spec: Spec,
158
+ ): Promise<{ body: Uint8Array; contentType: string | null; etag: string | null; lastModified: string | null }> {
159
+ const status = res.statusCode ?? 0
160
+ const rawType = res.headers['content-type']
161
+ const contentType = typeof rawType === 'string' ? rawType : null
162
+ const header = (name: string): string | null => { const v = res.headers[name]; return typeof v === 'string' ? v : null }
163
+ const etag = header('etag')
164
+ const lastModified = header('last-modified')
165
+ const stop = (refusal: Refusal): never => { req.destroy(); res.destroy(); throw refusal }
166
+ if (status < 200 || status > 299) return stop(new Refusal('status', `The server answered with HTTP status ${status}.`, status))
167
+ const declared = Number(res.headers['content-length'])
168
+ const tooLarge = (): Refusal => new Refusal('too_large', MESSAGES.too_large, spec.method === 'POST' ? status : undefined)
169
+ if (Number.isFinite(declared) && declared > options.maxBytes) return stop(tooLarge())
170
+
171
+ return await new Promise((resolve, reject) => {
172
+ const chunks: Buffer[] = []
173
+ let total = 0
174
+ let accepted = options.accept === undefined
175
+ const fail = (r: Refusal): void => { reject(r); req.destroy(); res.destroy() }
176
+ const runAccept = (): boolean => {
177
+ accepted = true
178
+ const first = Buffer.concat(chunks).subarray(0, HEAD_BYTES)
179
+ let verdict: boolean | string
180
+ try {
181
+ verdict = options.accept!({ status, contentType, firstBytes: new Uint8Array(first) })
182
+ } catch (err) {
183
+ reject(err)
184
+ req.destroy()
185
+ res.destroy()
186
+ return false
187
+ }
188
+ if (verdict === true) return true
189
+ fail(new Refusal('rejected', typeof verdict === 'string' ? verdict : 'The file was not accepted.'))
190
+ return false
191
+ }
192
+ res.on('data', (chunk: Buffer) => {
193
+ total += chunk.length
194
+ if (total > options.maxBytes) { fail(tooLarge()); return }
195
+ chunks.push(chunk)
196
+ if (!accepted && total >= HEAD_BYTES) runAccept()
197
+ })
198
+ res.on('end', () => {
199
+ if (!accepted && !runAccept()) return
200
+ resolve({ body: new Uint8Array(Buffer.concat(chunks)), contentType, etag, lastModified })
201
+ })
202
+ const broken = (): void => reject(run.aborted ? new Refusal('timeout', MESSAGES.timeout) : new Refusal('network', MESSAGES.network))
203
+ res.on('error', broken)
204
+ res.on('aborted', broken)
205
+ })
206
+ }
207
+
208
+ async function runFetch(
209
+ run: Run, rawUrl: string, options: UntrustedFetchOptions, first: Spec,
210
+ ): Promise<Extract<UntrustedFetchResult, { ok: true }>> {
211
+ let url: URL
212
+ try {
213
+ url = new URL(rawUrl)
214
+ } catch {
215
+ throw new Refusal('scheme', MESSAGES.scheme)
216
+ }
217
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new Refusal('scheme', MESSAGES.scheme)
218
+ const firstOrigin = url.origin
219
+ let spec = first
220
+ for (let hops = 0; ; hops++) {
221
+ let headers: Record<string, string> = url.origin === firstOrigin ? (options.headers ?? {}) : {}
222
+ if (spec.body === null && first.body !== null) {
223
+ headers = Object.fromEntries(Object.entries(headers).filter(([k]) => !k.toLowerCase().startsWith('content-')))
224
+ }
225
+ const step = await fetchOnce(run, url, options, headers, spec)
226
+ if ('final' in step) {
227
+ const status = step.final.statusCode ?? 0
228
+ const { body, contentType, etag, lastModified } = await readBody(step.final, step.req, options, run, spec)
229
+ return { ok: true, url: url.href, status, contentType, etag, lastModified, body, bytes: body.length }
230
+ }
231
+ if (hops >= options.maxRedirects) throw new Refusal('redirects', MESSAGES.redirects)
232
+ let next: URL
233
+ try {
234
+ next = new URL(step.redirect, url)
235
+ } catch {
236
+ throw new Refusal('scheme', MESSAGES.scheme)
237
+ }
238
+ if (next.protocol !== 'http:' && next.protocol !== 'https:') throw new Refusal('scheme', MESSAGES.scheme)
239
+ if (options.followRedirect !== undefined) {
240
+ const verdict = options.followRedirect(next.href, url.href)
241
+ if (verdict !== true) throw new Refusal('rejected', typeof verdict === 'string' ? verdict : 'The redirect was not followed.')
242
+ }
243
+ // 301, 302 and 303 turn a POST into a GET without a body; 307 and 308 resend it.
244
+ if (spec.method === 'POST' && step.status !== 307 && step.status !== 308) spec = { method: 'GET', body: null }
245
+ url = next
246
+ }
247
+ }
248
+
249
+ interface TraceInfo { host: string | null; url?: string }
250
+ interface TraceOutcome { host: string | null; status: number | null; bytes: number | null; reason: FetchRefusal | null }
251
+
252
+ /** Records the outcome as the captured input of a child step: a step's own capture is limited to its arguments. */
253
+ const recordOutcome = __flowStep({ ref: 'pipework/fetch:outcome', capture: [null] }, (o: TraceOutcome): TraceOutcome => o)
254
+
255
+ /** Fail-closed projection: the raw URL and options are never captured. */
256
+ const DROP = (): undefined => undefined
257
+
258
+ async function runTraced(info: TraceInfo, rawUrl: string, options: UntrustedFetchOptions, spec: Spec): Promise<UntrustedFetchResult> {
259
+ const run: Run = { aborted: false, current: null }
260
+ let timer: ReturnType<typeof setTimeout> | undefined
261
+ const timedOut = new Promise<never>((_, reject) => {
262
+ timer = setTimeout(() => {
263
+ run.aborted = true
264
+ run.current?.destroy()
265
+ reject(new Refusal('timeout', MESSAGES.timeout))
266
+ }, options.timeoutMs)
267
+ })
268
+ let result: UntrustedFetchResult
269
+ try {
270
+ const work = runFetch(run, rawUrl, options, spec)
271
+ work.catch(() => undefined)
272
+ result = await Promise.race([work, timedOut])
273
+ } catch (err) {
274
+ if (!(err instanceof Refusal)) throw err
275
+ result = { ok: false, reason: err.reason, message: err.message, ...(err.status !== undefined ? { status: err.status } : {}) }
276
+ } finally {
277
+ clearTimeout(timer)
278
+ }
279
+ recordOutcome({
280
+ host: info.host,
281
+ status: result.ok ? result.status : (result.status ?? null),
282
+ bytes: result.ok ? result.bytes : null,
283
+ reason: result.ok ? null : result.reason,
284
+ })
285
+ return result
286
+ }
287
+
288
+ const tracedGet = __flowStep(
289
+ { ref: 'pipework/fetch:get', capture: [null, DROP, DROP] },
290
+ (info: TraceInfo, rawUrl: string, options: UntrustedFetchOptions): Promise<UntrustedFetchResult> =>
291
+ runTraced(info, rawUrl, options, { method: 'GET', body: null }),
292
+ )
293
+
294
+ const tracedPost = __flowStep(
295
+ { ref: 'pipework/fetch:post', capture: [null, DROP, DROP] },
296
+ (info: TraceInfo, rawUrl: string, options: UntrustedPostOptions): Promise<UntrustedFetchResult> =>
297
+ runTraced(info, rawUrl, options, { method: 'POST', body: Buffer.from(options.body) }),
298
+ )
299
+
300
+ function traceInfo(url: string, options: UntrustedFetchOptions): TraceInfo {
301
+ let host: string | null = null
302
+ try {
303
+ host = new URL(url).host
304
+ } catch { /* the fetch refuses it as scheme */ }
305
+ return options.urlIsSafeToTrace === true ? { host, url } : { host }
306
+ }
307
+
308
+ /**
309
+ * Fetches one URL a stranger gave, with GET: http(s) only, public addresses only (every address the name resolves
310
+ * to is checked and the connection goes to a checked one), redirects up to `maxRedirects` with each hop checked
311
+ * as the first, a byte limit stopped the moment it is passed, one time limit for the whole fetch, and the
312
+ * content type and first bytes shown to `accept` before the body is kept. A refusal is returned with the rule
313
+ * that refused (`reason`) and a plain message that never carries an address or anything the server sent; it is
314
+ * not thrown. Recorded as the trace step `pipework/fetch:get` with a child step `pipework/fetch:outcome`
315
+ * holding host, status, bytes and reason; an ok result also carries the `ETag` and `Last-Modified` the server sent (null when absent); the full URL only when `urlIsSafeToTrace` is true.
316
+ */
317
+ export function untrustedGet(url: string, options: UntrustedFetchOptions): Promise<UntrustedFetchResult> {
318
+ return tracedGet(traceInfo(url, options), url, options)
319
+ }
320
+
321
+ /**
322
+ * `untrustedGet` with POST: every rule of `get` applies. A 307 or 308 redirect resends the body; 301, 302 and 303
323
+ * become a GET without a body. The app's `headers` go to the first origin only. A non-2xx answer is returned as
324
+ * `reason: 'status'` with the status and no body; a 2xx body over `maxBytes` is `too_large` and carries the status.
325
+ * Recorded as the trace step `pipework/fetch:post`.
326
+ */
327
+ export function untrustedPost(url: string, options: UntrustedPostOptions): Promise<UntrustedFetchResult> {
328
+ return tracedPost(traceInfo(url, options), url, options)
329
+ }
@@ -0,0 +1,7 @@
1
+ export { untrustedGet, untrustedPost } from './handle.js'
2
+ export { isPublicAddress } from './address.js'
3
+ export type {
4
+ UntrustedFetchOptions, UntrustedPostOptions, UntrustedFetchResult, FetchHead, FetchTarget, FetchRefusal,
5
+ } from './handle.js'
6
+ export { isUnsentFault, retryUnsent, fetchRetryingUnsent } from './connect-retry.js'
7
+ export type { ConnectRetryOptions } from './connect-retry.js'
@@ -0,0 +1,70 @@
1
+ import { untrustedGet, untrustedPost } from './handle.js'
2
+ import { retryUnsent, fetchRetryingUnsent, isUnsentFault } from './connect-retry.js'
3
+
4
+ /** Untrusted fetch — fetch one URL a stranger gave without letting them reach the app's private network. */
5
+ export const untrustedFetch = {
6
+ /**
7
+ * Fetches `url` with GET and returns `{ ok: true, url, status, contentType, etag, lastModified, body, bytes }` (`etag` and `lastModified` are the response headers, null when absent), or `{ ok: false, reason, message }`
8
+ * naming the rule that refused: `scheme`, `address`, `redirects`, `too_large`, `timeout`, `status`, `rejected` or `network`.
9
+ * It does not throw for these.
10
+ *
11
+ * Rules, none of which can be switched off: only `http:` and `https:`; the name is resolved and every address is checked
12
+ * (loopback, private, link-local including cloud metadata, CGNAT, unique-local and site-local IPv6, multicast, reserved,
13
+ * unspecified, and IPv4-mapped or NAT64 forms of these; literal IP URLs likewise) and the connection goes to a checked
14
+ * address; redirects are followed up to `maxRedirects` and each hop is checked as the first, and the app's `headers` go only to the first origin;
15
+ * the body is stopped the moment it passes `maxBytes`, whatever Content-Length says; `timeoutMs` covers the whole fetch.
16
+ * `accept({ status, contentType, firstBytes })` sees the first 1024 bytes before the body is kept: return false, or a string
17
+ * (your own message), to refuse with `rejected`. Messages never contain an address or anything a server sent.
18
+ *
19
+ * `followRedirect(to, from)` is asked before each redirect hop: true follows it; false, or a string (your message), refuses with `rejected`.
20
+ * A 304 answer is a `status` refusal with `status: 304`; send `If-None-Match` / `If-Modified-Since` through `headers`.
21
+ *
22
+ * `resolve` and `connect` are for tests: `resolve(hostname)` replaces DNS and `connect(target)` replaces the socket after the
23
+ * address check passed, so a test sends a fake public address to a local server. The address check always runs on what `resolve` returns.
24
+ *
25
+ * The call is the trace step `pipework/fetch:get`; its child `pipework/fetch:outcome` carries host, status, bytes and reason.
26
+ * The full URL is recorded only when `urlIsSafeToTrace` is true; otherwise only the host.
27
+ *
28
+ * ```ts
29
+ * const got = await untrustedFetch.get(pasted, { maxBytes: 5_000_000, timeoutMs: 10_000, maxRedirects: 3,
30
+ * accept: h => h.firstBytes[0] === 0x25 || 'That does not look like a PDF.' })
31
+ * if (!got.ok) return tell(got.message)
32
+ * ```
33
+ */
34
+ get: untrustedGet,
35
+ /**
36
+ * Posts `body` to `url` with every rule of `get`: scheme, every resolved address checked and the connection made to a checked one,
37
+ * each redirect hop re-checked, `maxBytes`, one `timeoutMs` for the whole fetch, `headers` to the first origin only.
38
+ * A 307 or 308 resends the body; 301, 302 and 303 become a GET without a body; `maxRedirects: 0` refuses any redirect.
39
+ * A non-2xx answer is `{ ok: false, reason: 'status', status }` with no body, as in `get`; a 2xx body over `maxBytes` is
40
+ * `too_large` and carries `status`. `resolve` and `connect` are for tests, as in `get`.
41
+ * The call is the trace step `pipework/fetch:post`; the full URL is recorded only when `urlIsSafeToTrace` is true.
42
+ *
43
+ * ```ts
44
+ * const sent = await untrustedFetch.post(endpoint.url, { body: json, headers: { 'content-type': 'application/json' },
45
+ * maxBytes: 65_536, timeoutMs: 10_000, maxRedirects: 0 })
46
+ * if (!sent.ok) return record(sent.reason)
47
+ * ```
48
+ */
49
+ post: untrustedPost,
50
+ } as const
51
+
52
+ /** Connect retry — send again only when the connection could not be made, so nothing reached the server. */
53
+ export const connectRetry = {
54
+ /**
55
+ * `fetch(input, init, opts)` that retries a fault in the connect phase (undici `UND_ERR_CONNECT_TIMEOUT`, `EAI_AGAIN`,
56
+ * `ENOTFOUND`, `ECONNREFUSED`, `ENETUNREACH`, `EHOSTUNREACH`), waiting `delaysMs` between tries (default 5 s, 15 s, 40 s: four
57
+ * tries over about a minute), then throws the last fault with its cause chain. A fault after the request was written
58
+ * (`ECONNRESET`, `UND_ERR_SOCKET`, a timeout, an abort) and a response of any status are never retried, because a POST may have taken effect.
59
+ * `signal` ends the waiting; `onRetry({ error, attempt, delayMs })` is called before each wait; `fetch` replaces the global fetch.
60
+ *
61
+ * ```ts
62
+ * const res = await connectRetry.fetch('https://api.github.com/repos/o/r/issues', { method: 'POST', body })
63
+ * ```
64
+ */
65
+ fetch: fetchRetryingUnsent,
66
+ /** Runs any `send()` with the same rule: retried only when `isUnsentFault(error)`; the last fault is thrown unchanged. */
67
+ call: retryUnsent,
68
+ /** True when the error, its `cause` chain or an `AggregateError`'s errors show a connect-phase fault and no sent-phase one. */
69
+ isUnsentFault,
70
+ } as const
@@ -0,0 +1,3 @@
1
+ export { createIdempotency, type IdempotencyMiddleware } from './middleware.js'
2
+ export { createStore } from './store.js'
3
+ export type { IdempotencyConfig, IdempotencyRecord, IdempotencyStore } from './types.js'
@@ -0,0 +1,87 @@
1
+ import type { IdempotencyConfig, IdempotencyStore } from './types.js'
2
+ import type { DB } from '../../data/db/index.js'
3
+ import type { Manifold } from '../../core/pipework.js'
4
+ import { createStore } from './store.js'
5
+
6
+ export interface IdempotencyMiddleware {
7
+ readonly store: IdempotencyStore
8
+ readonly config: Required<IdempotencyConfig>
9
+
10
+ check(
11
+ db: DB,
12
+ tenantId: string,
13
+ key: string,
14
+ method: string,
15
+ path: string,
16
+ ): Promise<{ status: 'new' } | { status: 'locked' } | { status: 'completed'; statusCode: number; body: unknown }>
17
+
18
+ complete(db: DB, tenantId: string, key: string, statusCode: number, body: unknown): Promise<void>
19
+ fail(db: DB, tenantId: string, key: string): Promise<void>
20
+ cleanup(db: DB): Promise<number>
21
+ }
22
+
23
+ const DEFAULTS = {
24
+ table: '__pipework_idempotency',
25
+ headerName: 'Idempotency-Key',
26
+ ttlSeconds: 86400,
27
+ lockTimeoutMs: 30_000,
28
+ methods: ['POST', 'PATCH', 'DELETE'] as const,
29
+ } as const
30
+
31
+ export function createIdempotency(instance: Manifold, config: IdempotencyConfig): IdempotencyMiddleware {
32
+ const resolved: Required<IdempotencyConfig> = {
33
+ database: config.database,
34
+ table: config.table ?? DEFAULTS.table,
35
+ headerName: config.headerName ?? DEFAULTS.headerName,
36
+ ttlSeconds: config.ttlSeconds ?? DEFAULTS.ttlSeconds,
37
+ lockTimeoutMs: config.lockTimeoutMs ?? DEFAULTS.lockTimeoutMs,
38
+ methods: config.methods ?? [...DEFAULTS.methods],
39
+ }
40
+
41
+ const store = createStore(resolved.table)
42
+
43
+ return {
44
+ store,
45
+ config: resolved,
46
+
47
+ async check(db, tenantId, key, method, path) {
48
+ const existing = await store.find(db, tenantId, key)
49
+
50
+ if (existing === null) {
51
+ const locked = await store.lock(db, tenantId, key, method, path, resolved.lockTimeoutMs)
52
+ if (!locked) {
53
+ return { status: 'locked' }
54
+ }
55
+ return { status: 'new' }
56
+ }
57
+
58
+ if (existing.statusCode !== null) {
59
+ return { status: 'completed', statusCode: existing.statusCode, body: existing.responseBody }
60
+ }
61
+
62
+ if (existing.lockedUntil !== null && existing.lockedUntil.getTime() > Date.now()) {
63
+ return { status: 'locked' }
64
+ }
65
+
66
+ // Lock expired — re-lock and retry
67
+ await store.remove(db, tenantId, key)
68
+ const locked = await store.lock(db, tenantId, key, method, path, resolved.lockTimeoutMs)
69
+ if (!locked) {
70
+ return { status: 'locked' }
71
+ }
72
+ return { status: 'new' }
73
+ },
74
+
75
+ async complete(db, tenantId, key, statusCode, body) {
76
+ await store.complete(db, tenantId, key, statusCode, body)
77
+ },
78
+
79
+ async fail(db, tenantId, key) {
80
+ await store.remove(db, tenantId, key)
81
+ },
82
+
83
+ async cleanup(db) {
84
+ return store.cleanup(db, resolved.ttlSeconds)
85
+ },
86
+ }
87
+ }
@@ -0,0 +1,92 @@
1
+ import { sql } from '../../data/db/sql.js'
2
+ import type { DB } from '../../data/db/index.js'
3
+ import type { IdempotencyStore, IdempotencyRecord } from './types.js'
4
+ import { unscopedExecutor, scopedExecutor, requireUnscopedExecutor } from '../../auth/tenant/scoped-db.js'
5
+
6
+ export function createStore(table: string): IdempotencyStore {
7
+ return {
8
+ async ensureTable(handle: DB): Promise<void> {
9
+ const db = unscopedExecutor(handle).db
10
+ await db.execute(sql.raw(`
11
+ CREATE TABLE IF NOT EXISTS ${table} (
12
+ key TEXT NOT NULL,
13
+ tenant_id TEXT NOT NULL,
14
+ method TEXT NOT NULL,
15
+ path TEXT NOT NULL,
16
+ status_code INTEGER,
17
+ response_body JSONB,
18
+ locked_until TIMESTAMPTZ,
19
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
20
+ PRIMARY KEY (tenant_id, key)
21
+ )
22
+ `))
23
+ },
24
+
25
+ async find(handle: DB, tenantId: string, key: string): Promise<IdempotencyRecord | null> {
26
+ const db = scopedExecutor(handle, tenantId, 'idempotency.find()').db
27
+ const rows: unknown[] = await db.execute(
28
+ sql`SELECT key, tenant_id, method, path, status_code, response_body, locked_until, created_at
29
+ FROM ${sql.raw(table)}
30
+ WHERE tenant_id = ${tenantId} AND key = ${key}`,
31
+ )
32
+ const row = rows[0] as Record<string, unknown> | undefined
33
+ if (row === undefined) return null
34
+
35
+ return {
36
+ key: row['key'] as string,
37
+ tenantId: row['tenant_id'] as string,
38
+ method: row['method'] as string,
39
+ path: row['path'] as string,
40
+ statusCode: row['status_code'] as number | null,
41
+ responseBody: row['response_body'],
42
+ lockedUntil: row['locked_until'] !== null ? new Date(row['locked_until'] as string) : null,
43
+ createdAt: new Date(row['created_at'] as string),
44
+ }
45
+ },
46
+
47
+ async lock(handle: DB, tenantId: string, key: string, method: string, path: string, lockMs: number): Promise<boolean> {
48
+ const db = scopedExecutor(handle, tenantId, 'idempotency.lock()').db
49
+ const lockUntil = new Date(Date.now() + lockMs)
50
+ const rows: unknown[] = await db.execute(
51
+ sql`INSERT INTO ${sql.raw(table)} (key, tenant_id, method, path, locked_until)
52
+ VALUES (${key}, ${tenantId}, ${method}, ${path}, ${lockUntil.toISOString()})
53
+ ON CONFLICT (tenant_id, key) DO NOTHING
54
+ RETURNING key`,
55
+ )
56
+ return (rows as unknown[]).length > 0
57
+ },
58
+
59
+ async complete(handle: DB, tenantId: string, key: string, statusCode: number, responseBody: unknown): Promise<void> {
60
+ const db = scopedExecutor(handle, tenantId, 'idempotency.complete()').db
61
+ // JSON.stringify(undefined) returns undefined, which binds as an empty
62
+ // SQL placeholder and triggers a syntax error. A 204-style response
63
+ // legitimately has no body; persist it as JSON null so replay surfaces
64
+ // the same null and the cached statusCode.
65
+ const body = responseBody === undefined ? null : responseBody
66
+ await db.execute(
67
+ sql`UPDATE ${sql.raw(table)}
68
+ SET status_code = ${statusCode},
69
+ response_body = ${JSON.stringify(body)},
70
+ locked_until = NULL
71
+ WHERE tenant_id = ${tenantId} AND key = ${key}`,
72
+ )
73
+ },
74
+
75
+ async remove(handle: DB, tenantId: string, key: string): Promise<void> {
76
+ const db = scopedExecutor(handle, tenantId, 'idempotency.remove()').db
77
+ await db.execute(
78
+ sql`DELETE FROM ${sql.raw(table)} WHERE tenant_id = ${tenantId} AND key = ${key}`,
79
+ )
80
+ },
81
+
82
+ async cleanup(handle: DB, ttlSeconds: number): Promise<number> {
83
+ const db = requireUnscopedExecutor(handle, 'idempotency.cleanup()', 'it deletes every tenant\'s expired keys')
84
+ const rows: unknown[] = await db.execute(
85
+ sql`DELETE FROM ${sql.raw(table)}
86
+ WHERE created_at < now() - ${ttlSeconds.toString() + ' seconds'}::interval
87
+ RETURNING key`,
88
+ )
89
+ return (rows as unknown[]).length
90
+ },
91
+ }
92
+ }
@@ -0,0 +1,30 @@
1
+ import type { DB } from '../../data/db/index.js'
2
+
3
+ export interface IdempotencyConfig {
4
+ database: string
5
+ table?: string
6
+ headerName?: string
7
+ ttlSeconds?: number
8
+ lockTimeoutMs?: number
9
+ methods?: ReadonlyArray<'POST' | 'PUT' | 'PATCH' | 'DELETE'>
10
+ }
11
+
12
+ export interface IdempotencyRecord {
13
+ key: string
14
+ tenantId: string
15
+ method: string
16
+ path: string
17
+ statusCode: number | null
18
+ responseBody: unknown
19
+ lockedUntil: Date | null
20
+ createdAt: Date
21
+ }
22
+
23
+ export interface IdempotencyStore {
24
+ ensureTable(db: DB): Promise<void>
25
+ find(db: DB, tenantId: string, key: string): Promise<IdempotencyRecord | null>
26
+ lock(db: DB, tenantId: string, key: string, method: string, path: string, lockMs: number): Promise<boolean>
27
+ complete(db: DB, tenantId: string, key: string, statusCode: number, responseBody: unknown): Promise<void>
28
+ remove(db: DB, tenantId: string, key: string): Promise<void>
29
+ cleanup(db: DB, ttlSeconds: number): Promise<number>
30
+ }