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,693 @@
1
+ import { Readable } from 'node:stream'
2
+ import { sql } from '../../data/db/sql.js'
3
+ import { pipeSystem } from '../../data/db/db.js'
4
+ import type { DB } from '../../data/db/index.js'
5
+ import { fitting } from '../../request/di/index.js'
6
+ import type { Handler } from '../../request/di/types.js'
7
+ import { getContext } from '../../request/context/store.js'
8
+ import { resolveActorId } from '../../data/write/audit/actor.js'
9
+ import { schema } from '../../data/schema/namespace.js'
10
+ import { __flowStep } from '../../trace/runtime/flow-step.js'
11
+ import { getBaseLogger } from '../../core/logging/index.js'
12
+ import { ConflictError, NotFoundError } from '../../core/errors/index.js'
13
+ import type { Manifold } from '../../core/pipework.js'
14
+ import type { Queue } from '../../async/jobs/queue.js'
15
+ import { untrustedGet } from '../fetch/handle.js'
16
+ import type { FetchTarget } from '../fetch/handle.js'
17
+ import type { Storage } from '../storage/handle.js'
18
+ import type { Socket } from 'node:net'
19
+ import { auditCrawl } from './audit.js'
20
+ import { CrawlNotEnabledError, CrawlSeedError } from './errors.js'
21
+ import { extractLinks } from './links.js'
22
+ import { ALLOW_ALL, DISALLOW_ALL, parseRobots, type RobotsRules } from './robots.js'
23
+
24
+ export type CrawlScope = 'host' | { readonly domain: string }
25
+ export type CrawlState = 'awaiting_approval' | 'running' | 'done' | 'stopped' | 'failed'
26
+ export type CrawlStoppedBy = 'pages' | 'depth' | 'bytes' | 'wall' | 'robots_unreachable'
27
+
28
+ /** Limits of one crawl; each is optional and the default is in the field's description. */
29
+ export interface CrawlLimits {
30
+ /** Requests in flight at once to one host. Default 1. */
31
+ readonly perHostConcurrency?: number
32
+ /** Least milliseconds between requests to a host; the gap used is the larger of this and the host's `Crawl-delay`. Default 1000. */
33
+ readonly minGapMs?: number
34
+ /** Most pages and files requested (robots.txt not counted). Default 1000. */
35
+ readonly maxPages?: number
36
+ /** Most links deep from the seed. Default 10. */
37
+ readonly maxDepth?: number
38
+ /** Most body bytes fetched in all. Default 1 GiB. */
39
+ readonly maxBytes?: number
40
+ /** Most bytes of one response; a larger one is refused as `too_large`. Default 25 MiB. */
41
+ readonly maxBytesPerResponse?: number
42
+ /** Most milliseconds the crawl runs, counted from its start. Default one hour. */
43
+ readonly maxWallMs?: number
44
+ /** Time limit of one request. Default 30 s. */
45
+ readonly requestTimeoutMs?: number
46
+ /** Most redirects followed in a chain. Default 5. */
47
+ readonly maxRedirects?: number
48
+ /** Time limit of one `onPage` call. Default 5000. */
49
+ readonly pageHookMs?: number
50
+ }
51
+
52
+ export type ResolvedCrawlLimits = Required<CrawlLimits>
53
+
54
+ export interface CrawlPage {
55
+ readonly url: string
56
+ /** The page's html, already limited to `maxBytesPerResponse`. */
57
+ readonly html: string
58
+ /** Aborted when the hook has overrun `pageHookMs`. */
59
+ readonly signal: AbortSignal
60
+ }
61
+
62
+ export interface CrawlConfig {
63
+ /** The app's manifold; its Blueprint must have a `crawl` block. */
64
+ readonly instance: Manifold
65
+ /** Names the site and its job type (`pipework.crawl.<name>`): letters, digits, `_`, `.` and `-`, starting with a letter. */
66
+ readonly name: string
67
+ /** The queue the crawl job runs on; its worker surface must run `site.job`. */
68
+ readonly queue: Pick<Queue, 'enqueue'>
69
+ /** Which resources are listed. In-scope `text/html` is followed for links whatever this says; any other type this refuses is not read. */
70
+ readonly accept: (resource: { url: string; contentType: string }) => boolean
71
+ /** Called with each followed page's html. A hook that throws or overruns `pageHookMs` is recorded against the URL and the crawl goes on. */
72
+ readonly onPage?: (page: CrawlPage) => Promise<void> | void
73
+ /** When given, each listed resource's bytes are put here (the job's tenant) and the row records the sha256 and file id. */
74
+ readonly store?: Storage
75
+ /** Sent on every request; its product token (the leading letters) picks the robots.txt group. */
76
+ readonly userAgent: string
77
+ /** For tests: replaces DNS and the socket, as in `untrustedFetch.get`. */
78
+ readonly network?: {
79
+ readonly resolve?: (hostname: string) => Promise<string[]>
80
+ readonly connect?: (target: FetchTarget) => Socket
81
+ }
82
+ /** Record the full URL in each request's trace step. Default true: crawled links are public. */
83
+ readonly traceUrls?: boolean
84
+ /** Database holding the crawl tables; default the default database. */
85
+ readonly database?: string
86
+ /** Attempts the queue gives the job: a job that dies or throws is retried and carries on from the queued URLs. Default 5. */
87
+ readonly maxAttempts?: number
88
+ /** First wait before a robots.txt retry, doubling each time. Default 2000. */
89
+ readonly robotsRetryMs?: number
90
+ }
91
+
92
+ export interface CrawlStartOptions {
93
+ readonly seed: string
94
+ /** `'host'` (default): the seed's exact host. `{ domain: 'example.gov.uk' }`: that host and every host under it. */
95
+ readonly scope?: CrawlScope
96
+ readonly limits?: CrawlLimits
97
+ }
98
+
99
+ export interface CrawlCounts {
100
+ readonly queued: number
101
+ readonly fetching: number
102
+ readonly done: number
103
+ readonly refused: number
104
+ readonly listed: number
105
+ readonly unchanged: number
106
+ /** Body bytes fetched. */
107
+ readonly bytes: number
108
+ }
109
+
110
+ export interface CrawlStatus {
111
+ readonly state: CrawlState
112
+ /** What the crawl stays inside. A `'host'` scope moves to the new host when the seed's own redirect chain leaves the seed's host (example.gov.uk to www.example.gov.uk). */
113
+ readonly scope: { readonly host: string } | { readonly domain: string }
114
+ /** Present when `state` is `stopped`. */
115
+ readonly stoppedBy?: CrawlStoppedBy
116
+ /** Present when `state` is `failed`. */
117
+ readonly error?: string
118
+ readonly counts: CrawlCounts
119
+ }
120
+
121
+ export interface CrawlResource {
122
+ readonly url: string
123
+ /** The page that linked to it; null for the seed. */
124
+ readonly from: string | null
125
+ readonly status: number
126
+ readonly contentType: string | null
127
+ readonly bytes: number
128
+ readonly etag: string | null
129
+ readonly lastModified: string | null
130
+ readonly sha256: string | null
131
+ readonly fileId: string | null
132
+ /** True when the server answered a conditional request with 304; the other fields are those of the earlier crawl. */
133
+ readonly unchanged: boolean
134
+ }
135
+
136
+ export interface CrawlRefusal {
137
+ readonly url: string
138
+ readonly from: string | null
139
+ /** `scope`, `robots`, `depth`, `page_hook`, or the `untrustedFetch.get` reason (`address`, `too_large`, `rejected`, `status`, ...). */
140
+ readonly reason: string
141
+ readonly message: string
142
+ }
143
+
144
+ export interface CrawlSite {
145
+ /** Add to a worker surface's `jobs`. */
146
+ readonly job: Handler<Record<string, unknown>, unknown>
147
+ /** Job type of `job`. */
148
+ readonly jobType: string
149
+ /**
150
+ * Records a crawl and, unless the Blueprint's `crawl.requireApproval` is true, queues its job. Needs a Flow. Throws
151
+ * `CrawlSeedError` for a seed that is not http(s) or not inside its own scope. One audit record: who, seed, scope, limits.
152
+ */
153
+ start(options: CrawlStartOptions): Promise<{ id: string }>
154
+ /** Queues the job of a crawl that is `awaiting_approval`; one audit record. Throws `ConflictError` for a crawl in another state. */
155
+ approve(id: string): Promise<void>
156
+ status(id: string): Promise<CrawlStatus>
157
+ /** The listed resources, in the order they were found. */
158
+ resources(id: string): Promise<CrawlResource[]>
159
+ /** Every URL the crawl declined to fetch or could not, and every `onPage` failure, with the reason. */
160
+ refusals(id: string): Promise<CrawlRefusal[]>
161
+ }
162
+
163
+ export const DEFAULT_LIMITS: ResolvedCrawlLimits = {
164
+ perHostConcurrency: 1, minGapMs: 1000, maxPages: 1000, maxDepth: 10, maxBytes: 1024 ** 3, maxBytesPerResponse: 25 * 1024 * 1024,
165
+ maxWallMs: 3_600_000, requestTimeoutMs: 30_000, maxRedirects: 5, pageHookMs: 5000,
166
+ }
167
+
168
+ const NAME_RE = /^[A-Za-z][A-Za-z0-9_.-]*$/
169
+ const ROBOTS_MAX_BYTES = 500 * 1024
170
+ const ROBOTS_TRIES = 4
171
+ /** Rules that allow nothing because the host's robots.txt could not be fetched for a reason that also bars the page itself. */
172
+ interface BarredRules extends RobotsRules { readonly refusal: { reason: string; message: string } }
173
+ const barred = (reason: string, message: string): BarredRules => ({ ...DISALLOW_ALL, refusal: { reason, message } })
174
+ const HTML_TYPES = new Set(['text/html', 'application/xhtml+xml'])
175
+
176
+ type StoredScope = { kind: 'host'; host: string } | { kind: 'domain'; domain: string }
177
+
178
+ interface UrlRow { id: string; url: string; depth: number; fromUrl: string | null; redirects: number; seedChain: boolean }
179
+
180
+ interface RunState {
181
+ stop: CrawlStoppedBy | null
182
+ inFlight: number
183
+ /** Per host: when the last request ended (started, with several in flight). */
184
+ last: Map<string, number>
185
+ hostInit: Map<string, Promise<void>>
186
+ robots: Map<string, Promise<RobotsRules | null>>
187
+ }
188
+
189
+ const num = (v: unknown): number => Number(v ?? 0)
190
+ const str = (v: unknown): string | null => (v === null || v === undefined ? null : String(v))
191
+ const baseType = (contentType: string | null): string => (contentType ?? '').split(';')[0]!.trim().toLowerCase()
192
+ const describeError = (err: unknown): string => (err instanceof Error ? err.message : String(err)).slice(0, 500)
193
+
194
+ function sleep(ms: number, signal: AbortSignal): Promise<void> {
195
+ return new Promise<void>(resolve => {
196
+ if (ms <= 0 || signal.aborted) { resolve(); return }
197
+ const done = (): void => { clearTimeout(timer); signal.removeEventListener('abort', done); resolve() }
198
+ const timer = setTimeout(done, ms)
199
+ signal.addEventListener('abort', done, { once: true })
200
+ })
201
+ }
202
+
203
+ function inScope(scope: StoredScope, hostname: string): boolean {
204
+ if (scope.kind === 'host') return hostname === scope.host
205
+ return hostname === scope.domain || hostname.endsWith('.' + scope.domain)
206
+ }
207
+
208
+ function resolveLimits(given: CrawlLimits | undefined): ResolvedCrawlLimits {
209
+ const out: Record<string, number> = { ...DEFAULT_LIMITS }
210
+ for (const [key, value] of Object.entries(given ?? {})) {
211
+ if (!(key in DEFAULT_LIMITS)) throw new CrawlSeedError(`unknown limit "${key}" (limits: ${Object.keys(DEFAULT_LIMITS).join(', ')}).`)
212
+ if (value === undefined) continue
213
+ const min = key === 'minGapMs' || key === 'maxRedirects' || key === 'maxDepth' ? 0 : 1
214
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < min) throw new CrawlSeedError(`limit ${key} must be a number of at least ${String(min)}, got ${String(value)}.`)
215
+ out[key] = value
216
+ }
217
+ return out as unknown as ResolvedCrawlLimits
218
+ }
219
+
220
+ function parseSeed(seed: string, scope: CrawlScope | undefined): { url: URL; stored: StoredScope } {
221
+ let url: URL
222
+ try { url = new URL(seed) } catch { throw new CrawlSeedError(`the seed "${seed}" is not a URL.`) }
223
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new CrawlSeedError(`the seed must be an http or https URL, got ${url.protocol}`)
224
+ url.hash = ''
225
+ let stored: StoredScope
226
+ if (scope === undefined || scope === 'host') {
227
+ stored = { kind: 'host', host: url.hostname }
228
+ } else if (typeof scope === 'object' && typeof scope.domain === 'string' && scope.domain.trim() !== '') {
229
+ stored = { kind: 'domain', domain: scope.domain.trim().toLowerCase().replace(/^\.+|\.+$/g, '') }
230
+ } else {
231
+ throw new CrawlSeedError(`scope must be 'host' or { domain: 'example.gov.uk' }.`)
232
+ }
233
+ if (!inScope(stored, url.hostname)) {
234
+ throw new CrawlSeedError(`the seed's host ${url.hostname} is not inside its scope (${stored.kind === 'host' ? stored.host : stored.domain}).`)
235
+ }
236
+ return { url, stored }
237
+ }
238
+
239
+ /** Creates a site crawler; see `crawl.create`. */
240
+ export function createCrawl(config: CrawlConfig): CrawlSite {
241
+ const blueprint = config.instance.config.raw.crawl
242
+ if (blueprint === undefined) throw new CrawlNotEnabledError()
243
+ if (typeof config.name !== 'string' || !NAME_RE.test(config.name)) {
244
+ throw new Error(`[pipework] crawl.create: name "${String(config.name)}" must start with a letter and use letters, digits, "_", "." or "-".\n`)
245
+ }
246
+ if (typeof config.userAgent !== 'string' || config.userAgent.trim() === '') throw new Error('[pipework] crawl.create: userAgent is required: sites read it to decide what you may fetch.\n')
247
+ const requireApproval = blueprint.requireApproval === true
248
+ const jobType = `pipework.crawl.${config.name}`
249
+ const database = config.database
250
+ const db = (): DB => pipeSystem(database)
251
+ const robotsRetryMs = config.robotsRetryMs ?? 2000
252
+ const urlIsSafeToTrace = config.traceUrls !== false
253
+
254
+ const enqueue = async (id: string): Promise<void> => {
255
+ await config.queue.enqueue(db(), { type: jobType, payload: { crawlId: id }, liveKey: id, continueFlow: true, maxAttempts: config.maxAttempts ?? 5 })
256
+ }
257
+
258
+ const startStep = __flowStep({ ref: 'pipework/crawl:start', capture: [null] }, async (options: CrawlStartOptions): Promise<{ id: string }> => {
259
+ const { url, stored } = parseSeed(options.seed, options.scope)
260
+ const limits = resolveLimits(options.limits)
261
+ const flow = getContext()
262
+ const actor = flow === undefined ? null : resolveActorId(flow)
263
+ const state: CrawlState = requireApproval ? 'awaiting_approval' : 'running'
264
+ const rows = await db().execute(sql`
265
+ INSERT INTO pipework_crawl (site, seed, scope, limits, state, tenant_id, started_by, run_started_at)
266
+ VALUES (${config.name}, ${url.href}, ${JSON.stringify(stored)}::jsonb, ${JSON.stringify(limits)}::jsonb, ${state},
267
+ ${flow?.tenant ?? null}, ${actor}, ${requireApproval ? null : sql`now()`})
268
+ RETURNING id::text AS id`)
269
+ const id = rows[0]!['id'] as string
270
+ await db().execute(sql`INSERT INTO pipework_crawl_url (crawl_id, url, depth, seed_chain) VALUES (${id}::uuid, ${url.href}, 0, true)`)
271
+ await auditCrawl('start', id, { site: config.name, seed: url.href, scope: stored, limits, awaitingApproval: requireApproval }, database)
272
+ if (!requireApproval) await enqueue(id)
273
+ return { id }
274
+ })
275
+
276
+ const approveStep = __flowStep({ ref: 'pipework/crawl:approve', capture: [null] }, async (id: string): Promise<void> => {
277
+ await requireCrawl(id)
278
+ const flow = getContext()
279
+ const actor = flow === undefined ? null : resolveActorId(flow)
280
+ const rows = await db().execute(sql`
281
+ UPDATE pipework_crawl SET state = 'running', approved_by = ${actor}, run_started_at = now()
282
+ WHERE id = ${id}::uuid AND site = ${config.name} AND state = 'awaiting_approval' RETURNING seed`)
283
+ if (rows.length === 0) {
284
+ const found = await db().execute(sql`SELECT state FROM pipework_crawl WHERE id = ${id}::uuid AND site = ${config.name}`)
285
+ if (found.length === 0) throw new NotFoundError(`Crawl ${id}`)
286
+ throw new ConflictError(`Crawl ${id} is not awaiting approval (it is ${String(found[0]!['state'])}).`)
287
+ }
288
+ await auditCrawl('approve', id, { site: config.name, seed: rows[0]!['seed'] }, database)
289
+ await enqueue(id)
290
+ })
291
+
292
+ async function loadCrawl(id: string): Promise<{ state: CrawlState; limits: ResolvedCrawlLimits; scope: StoredScope; seed: string; runStartedAt: number; tenantId: string | null; stoppedBy: CrawlStoppedBy | null; error: string | null; bytes: number } | null> {
293
+ const rows = await db().execute(sql`
294
+ SELECT state, limits, scope, seed, tenant_id, stopped_by, error, bytes::float8 AS bytes,
295
+ (extract(epoch from run_started_at) * 1000)::float8 AS run_started_ms
296
+ FROM pipework_crawl WHERE id = ${id}::uuid AND site = ${config.name}`)
297
+ const r = rows[0]
298
+ if (r === undefined) return null
299
+ return {
300
+ state: r['state'] as CrawlState, limits: r['limits'] as ResolvedCrawlLimits, scope: r['scope'] as StoredScope, seed: r['seed'] as string,
301
+ runStartedAt: num(r['run_started_ms']), tenantId: str(r['tenant_id']),
302
+ stoppedBy: str(r['stopped_by']) as CrawlStoppedBy | null, error: str(r['error']), bytes: num(r['bytes']),
303
+ }
304
+ }
305
+
306
+ /** The crawl, or NotFoundError: also when it belongs to another tenant than the Flow's. */
307
+ const requireCrawl = async (id: string): Promise<NonNullable<Awaited<ReturnType<typeof loadCrawl>>>> => {
308
+ const crawl = await loadCrawl(id)
309
+ const tenant = getContext()?.tenant ?? null
310
+ if (crawl === null || (crawl.tenantId !== null && crawl.tenantId !== tenant)) throw new NotFoundError(`Crawl ${id}`)
311
+ return crawl
312
+ }
313
+
314
+ const status = async (id: string): Promise<CrawlStatus> => {
315
+ const crawl = await requireCrawl(id)
316
+ const counted = await db().execute(sql`
317
+ SELECT count(*) FILTER (WHERE state = 'queued')::int AS queued, count(*) FILTER (WHERE state = 'fetching')::int AS fetching,
318
+ count(*) FILTER (WHERE state = 'done')::int AS done, count(*) FILTER (WHERE state = 'refused')::int AS refused
319
+ FROM pipework_crawl_url WHERE crawl_id = ${id}::uuid`)
320
+ const listed = await db().execute(sql`
321
+ SELECT count(*)::int AS listed, count(*) FILTER (WHERE unchanged)::int AS unchanged FROM pipework_crawl_resource WHERE crawl_id = ${id}::uuid`)
322
+ const c = counted[0]!
323
+ return {
324
+ state: crawl.state,
325
+ scope: crawl.scope.kind === 'host' ? { host: crawl.scope.host } : { domain: crawl.scope.domain },
326
+ ...(crawl.stoppedBy !== null ? { stoppedBy: crawl.stoppedBy } : {}),
327
+ ...(crawl.error !== null ? { error: crawl.error } : {}),
328
+ counts: {
329
+ queued: num(c['queued']), fetching: num(c['fetching']), done: num(c['done']), refused: num(c['refused']),
330
+ listed: num(listed[0]!['listed']), unchanged: num(listed[0]!['unchanged']), bytes: crawl.bytes,
331
+ },
332
+ }
333
+ }
334
+
335
+ const resources = async (id: string): Promise<CrawlResource[]> => {
336
+ await requireCrawl(id)
337
+ const rows = await db().execute(sql`
338
+ SELECT r.url, r.from_url, r.status, r.content_type, r.bytes::float8 AS bytes, r.etag, r.last_modified, r.sha256, r.file_id, r.unchanged
339
+ FROM pipework_crawl_resource r JOIN pipework_crawl c ON c.id = r.crawl_id
340
+ WHERE r.crawl_id = ${id}::uuid AND c.site = ${config.name} ORDER BY r.created_at, r.id`)
341
+ return rows.map(r => ({
342
+ url: r['url'] as string, from: str(r['from_url']), status: num(r['status']), contentType: str(r['content_type']), bytes: num(r['bytes']),
343
+ etag: str(r['etag']), lastModified: str(r['last_modified']), sha256: str(r['sha256']), fileId: str(r['file_id']), unchanged: r['unchanged'] === true,
344
+ }))
345
+ }
346
+
347
+ const refusals = async (id: string): Promise<CrawlRefusal[]> => {
348
+ await requireCrawl(id)
349
+ const rows = await db().execute(sql`
350
+ SELECT u.url, u.from_url, u.reason, u.message
351
+ FROM pipework_crawl_url u JOIN pipework_crawl c ON c.id = u.crawl_id
352
+ WHERE u.crawl_id = ${id}::uuid AND c.site = ${config.name} AND (u.state = 'refused' OR u.reason = 'page_hook') ORDER BY u.seq`)
353
+ return rows.map(r => ({ url: r['url'] as string, from: str(r['from_url']), reason: str(r['reason']) ?? '', message: str(r['message']) ?? '' }))
354
+ }
355
+
356
+ // ---- the job ----
357
+
358
+ async function run(crawlId: string, signal: AbortSignal): Promise<void> {
359
+ const crawl = await loadCrawl(crawlId)
360
+ if (crawl === null || crawl.state !== 'running') return
361
+ const { limits } = crawl
362
+ let scope = crawl.scope
363
+ const state: RunState = { stop: null, inFlight: 0, last: new Map(), hostInit: new Map(), robots: new Map() }
364
+ // Only one job runs a crawl, so a row still 'fetching' is the one that was in flight when the last attempt died.
365
+ await db().execute(sql`UPDATE pipework_crawl_url SET state = 'queued' WHERE crawl_id = ${crawlId}::uuid AND state = 'fetching'`)
366
+
367
+ const hostRow = async (host: string): Promise<{ robotsState: string | null; robotsText: string | null; attempts: number; lastMs: number }> => {
368
+ await db().execute(sql`INSERT INTO pipework_crawl_host (crawl_id, host) VALUES (${crawlId}::uuid, ${host}) ON CONFLICT (crawl_id, host) DO NOTHING`)
369
+ const r = (await db().execute(sql`
370
+ SELECT robots_state, robots_text, robots_attempts, last_request_at_ms::float8 AS last_ms FROM pipework_crawl_host WHERE crawl_id = ${crawlId}::uuid AND host = ${host}`))[0]!
371
+ return { robotsState: str(r['robots_state']), robotsText: str(r['robots_text']), attempts: num(r['robots_attempts']), lastMs: num(r['last_ms']) }
372
+ }
373
+
374
+ const gapFor = (rules: RobotsRules | null): number => Math.max(limits.minGapMs, (rules?.crawlDelaySeconds ?? 0) * 1000)
375
+
376
+ /** Waits for this host's turn, then returns a function that records the request's end. A request starts at least `gap` after the last one ended (started, with several in flight). */
377
+ const takeTurn = async (host: string, gap: number): Promise<() => Promise<void>> => {
378
+ let init = state.hostInit.get(host)
379
+ if (init === undefined) {
380
+ init = hostRow(host).then(row => { state.last.set(host, row.lastMs) })
381
+ state.hostInit.set(host, init)
382
+ }
383
+ await init
384
+ const start = Math.max(Date.now(), state.last.get(host)! + gap)
385
+ if (limits.perHostConcurrency > 1) state.last.set(host, start)
386
+ await sleep(start - Date.now(), signal)
387
+ return async () => {
388
+ const end = Date.now()
389
+ state.last.set(host, Math.max(state.last.get(host)!, end))
390
+ await db().execute(sql`UPDATE pipework_crawl_host SET last_request_at_ms = ${end} WHERE crawl_id = ${crawlId}::uuid AND host = ${host}`)
391
+ }
392
+ }
393
+
394
+ const ua = { 'user-agent': config.userAgent }
395
+ const network = {
396
+ ...(config.network?.resolve !== undefined ? { resolve: config.network.resolve } : {}),
397
+ ...(config.network?.connect !== undefined ? { connect: config.network.connect } : {}),
398
+ }
399
+
400
+ /** The host's robots rules, fetched once before any other request to it; null when it stayed unreachable (the crawl stops). */
401
+ const robotsFor = (host: string, protocol: string): Promise<RobotsRules | null> => {
402
+ let pending = state.robots.get(host)
403
+ if (pending === undefined) {
404
+ pending = loadRobots(host, protocol)
405
+ state.robots.set(host, pending)
406
+ }
407
+ return pending
408
+ }
409
+
410
+ const loadRobots = async (host: string, protocol: string): Promise<RobotsRules | null> => {
411
+ const row = await hostRow(host)
412
+ if (row.robotsState === 'ok') return parseRobots(row.robotsText ?? '', config.userAgent)
413
+ if (row.robotsState === 'allow_all') return ALLOW_ALL
414
+ if (row.robotsState === 'disallow_all') {
415
+ const kept = JSON.parse(row.robotsText ?? 'null') as { reason: string; message: string } | null
416
+ return kept === null ? DISALLOW_ALL : barred(kept.reason, kept.message)
417
+ }
418
+ const settle = async (robotsState: string, text: string | null): Promise<void> => {
419
+ await db().execute(sql`UPDATE pipework_crawl_host SET robots_state = ${robotsState}, robots_text = ${text} WHERE crawl_id = ${crawlId}::uuid AND host = ${host}`)
420
+ }
421
+ for (let attempt = row.attempts; attempt < ROBOTS_TRIES; attempt++) {
422
+ if (signal.aborted) return null
423
+ if (attempt > 0) await sleep(robotsRetryMs * 2 ** (attempt - 1), signal)
424
+ const done = await takeTurn(host, limits.minGapMs)
425
+ const got = await untrustedGet(`${protocol}//${host}/robots.txt`, {
426
+ maxBytes: ROBOTS_MAX_BYTES, timeoutMs: limits.requestTimeoutMs, maxRedirects: limits.maxRedirects, headers: ua, urlIsSafeToTrace,
427
+ followRedirect: (to) => new URL(to).host === host ? true : 'robots.txt redirects to another host',
428
+ ...network,
429
+ })
430
+ await done()
431
+ if (got.ok) {
432
+ const text = new TextDecoder().decode(got.body)
433
+ await settle('ok', text)
434
+ return parseRobots(text, config.userAgent)
435
+ }
436
+ if (got.reason === 'status' && got.status !== undefined && got.status >= 400 && got.status < 500) {
437
+ await settle('allow_all', null)
438
+ return ALLOW_ALL
439
+ }
440
+ if (got.reason === 'status' || got.reason === 'network' || got.reason === 'timeout') {
441
+ await db().execute(sql`UPDATE pipework_crawl_host SET robots_attempts = ${attempt + 1} WHERE crawl_id = ${crawlId}::uuid AND host = ${host}`)
442
+ continue
443
+ }
444
+ const refusal = { reason: got.reason, message: got.message }
445
+ await settle('disallow_all', JSON.stringify(refusal))
446
+ return barred(refusal.reason, refusal.message)
447
+ }
448
+ return signal.aborted ? null : (state.stop = 'robots_unreachable', null)
449
+ }
450
+
451
+ const insertLinks = async (tx: DB, rows: Array<{ url: string; depth: number; from: string; state: string; reason: string | null; message: string | null; redirects: number; seedChain?: boolean }>): Promise<void> => {
452
+ if (rows.length === 0) return
453
+ await tx.execute(sql`
454
+ INSERT INTO pipework_crawl_url (crawl_id, url, depth, from_url, redirects, state, reason, message, seed_chain)
455
+ SELECT ${crawlId}::uuid, x.url, x.depth, x.from_url, x.redirects, x.state, x.reason, x.message, x.seed_chain
456
+ FROM jsonb_to_recordset(${JSON.stringify(rows.map(r => ({ url: r.url, depth: r.depth, from_url: r.from, redirects: r.redirects, state: r.state, reason: r.reason, message: r.message, seed_chain: r.seedChain === true })))}::jsonb)
457
+ AS x(url text, depth int, from_url text, redirects int, state text, reason text, message text, seed_chain boolean)
458
+ ON CONFLICT (crawl_id, url) DO NOTHING`)
459
+ }
460
+
461
+ const finishUrl = async (row: UrlRow, outcome: { state: 'done' | 'refused'; reason?: string; message?: string; status?: number | null; requested?: boolean }, extra?: (tx: DB) => Promise<void>): Promise<void> => {
462
+ await db().transaction(async (tx) => {
463
+ await extra?.(tx)
464
+ await tx.execute(sql`
465
+ UPDATE pipework_crawl_url SET state = ${outcome.state}, reason = ${outcome.reason ?? null}, message = ${outcome.message ?? null}, status = ${outcome.status ?? null}, requested = requested OR ${outcome.requested === true}
466
+ WHERE id = ${row.id}::uuid`)
467
+ })
468
+ }
469
+
470
+ const runHook = async (url: string, html: string): Promise<string | null> => {
471
+ const hook = config.onPage
472
+ if (hook === undefined) return null
473
+ const controller = new AbortController()
474
+ let timer: ReturnType<typeof setTimeout> | undefined
475
+ const overrun = new Promise<never>((_, reject) => {
476
+ timer = setTimeout(() => {
477
+ const err = new Error(`onPage took longer than ${String(limits.pageHookMs)} ms.`)
478
+ controller.abort(err)
479
+ reject(err)
480
+ }, limits.pageHookMs)
481
+ })
482
+ try {
483
+ const work = Promise.resolve().then(() => hook({ url, html, signal: controller.signal }))
484
+ work.catch(() => undefined)
485
+ await Promise.race([work, overrun])
486
+ return null
487
+ } catch (err) {
488
+ return describeError(err)
489
+ } finally {
490
+ clearTimeout(timer)
491
+ }
492
+ }
493
+
494
+ const previous = async (url: string): Promise<{ etag: string | null; lastModified: string | null; status: number; contentType: string | null; bytes: number; sha256: string | null; fileId: string | null } | null> => {
495
+ const rows = await db().execute(sql`
496
+ SELECT r.etag, r.last_modified, r.status, r.content_type, r.bytes::float8 AS bytes, r.sha256, r.file_id
497
+ FROM pipework_crawl_resource r JOIN pipework_crawl c ON c.id = r.crawl_id
498
+ WHERE c.site = ${config.name} AND c.seed = ${crawl.seed} AND c.id <> ${crawlId}::uuid AND c.created_at < (SELECT created_at FROM pipework_crawl WHERE id = ${crawlId}::uuid)
499
+ AND r.url = ${url} AND (r.etag IS NOT NULL OR r.last_modified IS NOT NULL)
500
+ ORDER BY c.created_at DESC LIMIT 1`)
501
+ const r = rows[0]
502
+ if (r === undefined) return null
503
+ return { etag: str(r['etag']), lastModified: str(r['last_modified']), status: num(r['status']), contentType: str(r['content_type']), bytes: num(r['bytes']), sha256: str(r['sha256']), fileId: str(r['file_id']) }
504
+ }
505
+
506
+ const movesScope = (row: UrlRow, to: URL): boolean => scope.kind === 'host' && row.seedChain && row.depth === 0 && !inScope(scope, to.hostname)
507
+
508
+ const processUrl = async (row: UrlRow): Promise<void> => {
509
+ const target = new URL(row.url)
510
+ const rules = await robotsFor(target.host, target.protocol)
511
+ if (rules === null) {
512
+ if (!signal.aborted) await db().execute(sql`UPDATE pipework_crawl_url SET state = 'queued' WHERE id = ${row.id}::uuid`)
513
+ return
514
+ }
515
+ if (!rules.allows(target.pathname + target.search)) {
516
+ const why = (rules as Partial<BarredRules>).refusal
517
+ await finishUrl(row, { state: 'refused', reason: why?.reason ?? 'robots', message: why?.message ?? 'robots.txt does not allow this path.' })
518
+ return
519
+ }
520
+ const done = await takeTurn(target.host, gapFor(rules))
521
+ if (signal.aborted) {
522
+ await db().execute(sql`UPDATE pipework_crawl_url SET state = 'queued' WHERE id = ${row.id}::uuid`)
523
+ return
524
+ }
525
+ const prior = await previous(row.url)
526
+ const headers: Record<string, string> = { ...ua }
527
+ if (prior?.etag != null) headers['if-none-match'] = prior.etag
528
+ if (prior?.lastModified != null) headers['if-modified-since'] = prior.lastModified
529
+ const hop: { to: string | null } = { to: null }
530
+ const got = await untrustedGet(row.url, {
531
+ maxBytes: limits.maxBytesPerResponse, timeoutMs: limits.requestTimeoutMs, maxRedirects: limits.maxRedirects, headers, urlIsSafeToTrace,
532
+ // A redirect is not followed inside the request: its target is a URL of the crawl like any link, so it meets scope, robots and the host's gap.
533
+ followRedirect: (to) => { hop.to = to; return 'The crawler handles redirects itself.' },
534
+ accept: (head) => {
535
+ const type = baseType(head.contentType)
536
+ if (HTML_TYPES.has(type)) return true
537
+ return config.accept({ url: row.url, contentType: type }) || 'This is neither a page nor a file the app lists.'
538
+ },
539
+ ...network,
540
+ })
541
+ await done()
542
+
543
+ if (!got.ok) {
544
+ if (got.reason === 'rejected' && hop.to !== null) {
545
+ const hops = row.redirects + 1
546
+ let to: URL | null = null
547
+ try { to = new URL(hop.to) } catch { /* refused below */ }
548
+ if (hops > limits.maxRedirects) await finishUrl(row, { requested: true, state: 'refused', reason: 'redirects', message: 'The link redirects too many times.' })
549
+ else if (to === null || (!inScope(scope, to.hostname) && !movesScope(row, to))) await finishUrl(row, { requested: true, state: 'refused', reason: 'scope', message: 'The link redirects to a page outside the crawl\'s scope.' })
550
+ else {
551
+ to.hash = ''
552
+ // The seed's own redirect chain may move a host scope (example.gov.uk to www.example.gov.uk).
553
+ const moved: StoredScope | null = inScope(scope, to.hostname) ? null : { kind: 'host', host: to.hostname }
554
+ await finishUrl(row, { requested: true, state: 'done', reason: 'redirected', message: 'Redirected; the target is queued.' }, async (tx) => {
555
+ if (moved !== null) await tx.execute(sql`UPDATE pipework_crawl SET scope = ${JSON.stringify(moved)}::jsonb WHERE id = ${crawlId}::uuid`)
556
+ await insertLinks(tx, [{ url: to.href, depth: row.depth, from: row.fromUrl ?? row.url, state: 'queued', reason: null, message: null, redirects: hops, seedChain: row.seedChain }])
557
+ })
558
+ if (moved !== null) scope = moved
559
+ }
560
+ return
561
+ }
562
+ if (got.reason === 'status' && got.status === 304 && prior !== null) {
563
+ await finishUrl(row, { requested: true, state: 'done', status: 304 }, tx => tx.execute(sql`
564
+ INSERT INTO pipework_crawl_resource (crawl_id, url, from_url, status, content_type, bytes, etag, last_modified, sha256, file_id, unchanged)
565
+ VALUES (${crawlId}::uuid, ${row.url}, ${row.fromUrl}, ${prior.status}, ${prior.contentType}, ${prior.bytes}, ${prior.etag}, ${prior.lastModified}, ${prior.sha256}, ${prior.fileId}, true)
566
+ ON CONFLICT (crawl_id, url) DO NOTHING`).then(() => undefined))
567
+ return
568
+ }
569
+ await finishUrl(row, { requested: true, state: 'refused', reason: got.reason, message: got.message, status: got.status ?? null })
570
+ return
571
+ }
572
+
573
+ const type = baseType(got.contentType)
574
+ const isHtml = HTML_TYPES.has(type)
575
+ const listed = config.accept({ url: got.url, contentType: type })
576
+ let hookFault: string | null = null
577
+ let links: string[] = []
578
+ if (isHtml) {
579
+ const html = new TextDecoder().decode(got.body)
580
+ links = extractLinks(html, got.url)
581
+ hookFault = await runHook(row.url, html)
582
+ }
583
+ let sha256: string | null = null
584
+ let fileId: string | null = null
585
+ if (listed && config.store !== undefined) {
586
+ const file = await config.store.put({ body: Readable.from(Buffer.from(got.body)), mediaType: type === '' ? 'application/octet-stream' : type, metadata: { url: got.url, crawl: crawlId } })
587
+ sha256 = file.sha256
588
+ fileId = file.id
589
+ }
590
+ const children = links.map(link => {
591
+ const u = new URL(link)
592
+ const depth = row.depth + 1
593
+ if (!inScope(scope, u.hostname)) return { url: link, depth, from: row.url, state: 'refused', reason: 'scope', message: 'The link points outside the crawl\'s scope.', redirects: 0 }
594
+ if (depth > limits.maxDepth) return { url: link, depth, from: row.url, state: 'refused', reason: 'depth', message: `The link is more than ${String(limits.maxDepth)} links from the seed.`, redirects: 0 }
595
+ return { url: link, depth, from: row.url, state: 'queued', reason: null, message: null, redirects: 0 }
596
+ })
597
+ await finishUrl(
598
+ row,
599
+ hookFault === null ? { requested: true, state: 'done', status: got.status } : { requested: true, state: 'done', status: got.status, reason: 'page_hook', message: hookFault },
600
+ async (tx) => {
601
+ await tx.execute(sql`UPDATE pipework_crawl SET bytes = bytes + ${got.bytes} WHERE id = ${crawlId}::uuid`)
602
+ if (listed) {
603
+ await tx.execute(sql`
604
+ INSERT INTO pipework_crawl_resource (crawl_id, url, from_url, status, content_type, bytes, etag, last_modified, sha256, file_id)
605
+ VALUES (${crawlId}::uuid, ${row.url}, ${row.fromUrl}, ${got.status}, ${type === '' ? null : type}, ${got.bytes}, ${got.etag}, ${got.lastModified}, ${sha256}, ${fileId})
606
+ ON CONFLICT (crawl_id, url) DO NOTHING`)
607
+ }
608
+ await insertLinks(tx, children)
609
+ },
610
+ )
611
+ }
612
+
613
+ /** The next queued URL, claimed so no other lane takes it; breadth first. */
614
+ const claim = async (): Promise<UrlRow | null> => {
615
+ const rows = await db().execute(sql`
616
+ UPDATE pipework_crawl_url SET state = 'fetching'
617
+ WHERE id = (SELECT id FROM pipework_crawl_url WHERE crawl_id = ${crawlId}::uuid AND state = 'queued'
618
+ ORDER BY depth, seq LIMIT 1 FOR UPDATE SKIP LOCKED)
619
+ RETURNING id::text AS id, url, depth, from_url, redirects, seed_chain`)
620
+ const r = rows[0]
621
+ return r === undefined ? null : { id: r['id'] as string, url: r['url'] as string, depth: num(r['depth']), fromUrl: str(r['from_url']), redirects: num(r['redirects']), seedChain: r['seed_chain'] === true }
622
+ }
623
+
624
+ const limitHit = async (): Promise<CrawlStoppedBy | null> => {
625
+ if (Date.now() - crawl.runStartedAt > limits.maxWallMs) return 'wall'
626
+ const r = (await db().execute(sql`
627
+ SELECT (SELECT count(*) FROM pipework_crawl_url WHERE crawl_id = ${crawlId}::uuid AND requested)::int AS pages,
628
+ (SELECT bytes::float8 FROM pipework_crawl WHERE id = ${crawlId}::uuid) AS bytes`))[0]!
629
+ if (num(r['pages']) >= limits.maxPages) return 'pages'
630
+ if (num(r['bytes']) >= limits.maxBytes) return 'bytes'
631
+ return null
632
+ }
633
+
634
+ const lane = async (): Promise<void> => {
635
+ while (!signal.aborted && state.stop === null) {
636
+ const hit = await limitHit()
637
+ if (hit !== null) {
638
+ const queued = hit === 'wall' || hit === 'bytes' || num((await db().execute(sql`SELECT count(*)::int AS n FROM pipework_crawl_url WHERE crawl_id = ${crawlId}::uuid AND state = 'queued'`))[0]!['n']) > 0
639
+ if (queued) state.stop = hit
640
+ else if (state.inFlight === 0) return
641
+ }
642
+ if (state.stop !== null) return
643
+ const row = await claim()
644
+ if (row === null) {
645
+ if (state.inFlight === 0) return
646
+ await sleep(25, signal)
647
+ continue
648
+ }
649
+ state.inFlight++
650
+ try { await processUrl(row) } finally { state.inFlight-- }
651
+ }
652
+ }
653
+
654
+ await Promise.all(Array.from({ length: limits.perHostConcurrency }, () => lane()))
655
+ if (signal.aborted) return
656
+ if (state.stop !== null) {
657
+ await db().execute(sql`UPDATE pipework_crawl SET state = 'stopped', stopped_by = ${state.stop}, finished_at = now() WHERE id = ${crawlId}::uuid`)
658
+ return
659
+ }
660
+ const depthCut = await db().execute(sql`SELECT 1 FROM pipework_crawl_url WHERE crawl_id = ${crawlId}::uuid AND reason = 'depth' LIMIT 1`)
661
+ if (depthCut.length > 0) {
662
+ await db().execute(sql`UPDATE pipework_crawl SET state = 'stopped', stopped_by = 'depth', finished_at = now() WHERE id = ${crawlId}::uuid`)
663
+ return
664
+ }
665
+ await db().execute(sql`UPDATE pipework_crawl SET state = 'done', finished_at = now() WHERE id = ${crawlId}::uuid`)
666
+ }
667
+
668
+ const builder = database !== undefined ? fitting.use(database) : fitting.use()
669
+ const job = builder.job(jobType)
670
+ .transaction('manual')
671
+ .input(schema.check.object({ crawlId: schema.check.string() }))
672
+ .fit((async ({ input, signal }: { input: { crawlId: string }; signal: AbortSignal }) => {
673
+ try {
674
+ await run(input.crawlId, signal)
675
+ } catch (err) {
676
+ const flow = getContext()
677
+ const last = flow?.jobAttempt != null && flow.jobMaxAttempts != null && flow.jobAttempt >= flow.jobMaxAttempts
678
+ if (last) {
679
+ getBaseLogger().error({ err, crawlId: input.crawlId }, 'Crawl job failed on its last attempt')
680
+ await db().execute(sql`UPDATE pipework_crawl SET state = 'failed', error = ${describeError(err)}, finished_at = now() WHERE id = ${input.crawlId}::uuid`)
681
+ }
682
+ throw err
683
+ }
684
+ }) as never) as unknown as Handler<Record<string, unknown>, unknown>
685
+
686
+ return {
687
+ job,
688
+ jobType,
689
+ start: (options) => startStep(options),
690
+ approve: (id) => approveStep(id),
691
+ status, resources, refusals,
692
+ }
693
+ }