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,125 @@
1
+ import { signPayload } from '../webhook/sign.js'
2
+ import type { WebhookSignatureOptions } from '../webhook/sign.js'
3
+ import type { Sink, SinkDelivery } from '../outbox/core.js'
4
+
5
+ /** One alert's event, delivered at once. */
6
+ export interface AlertEventPayload {
7
+ readonly kind: 'raise' | 'repeat' | 'resolve' | 'reopen'
8
+ readonly key: string
9
+ readonly severity: 'info' | 'warning' | 'error' | 'critical'
10
+ readonly message: string
11
+ readonly subject: { readonly tenantId?: string; readonly entity?: string; readonly entityId?: string } | null
12
+ readonly owner: string | null
13
+ readonly runbook: string | null
14
+ readonly context: Readonly<Record<string, unknown>> | null
15
+ readonly count: number
16
+ /** Times a raise came after a resolve. */
17
+ readonly reopenCount: number
18
+ readonly firstSeenAt: string
19
+ readonly lastSeenAt: string
20
+ readonly resolvedAt: string | null
21
+ readonly acknowledgedAt: string | null
22
+ readonly acknowledgedBy: string | null
23
+ /** On a `reopen`, those of the resolve it reopens. */
24
+ readonly resolvedBy: string | null
25
+ readonly resolveNote: string | null
26
+ readonly resolvedRelease: string | null
27
+ readonly issueUrl: string | null
28
+ readonly environment: string
29
+ /** The latest traces that raised it, newest first. */
30
+ readonly traceIds: readonly string[]
31
+ }
32
+
33
+ /** The alerts of the digest severities that changed in one window, sent once per sink. */
34
+ export interface AlertDigestPayload {
35
+ readonly kind: 'digest'
36
+ readonly key: 'pipework.alerts.digest'
37
+ readonly environment: string
38
+ /** The previous digest's time; null on the first. */
39
+ readonly since: string | null
40
+ readonly until: string
41
+ readonly alerts: readonly {
42
+ readonly key: string
43
+ readonly severity: 'info' | 'warning' | 'error' | 'critical'
44
+ readonly status: 'open' | 'acknowledged' | 'resolved'
45
+ readonly message: string
46
+ readonly owner: string | null
47
+ readonly count: number
48
+ readonly reopenCount: number
49
+ readonly lastSeenAt: string
50
+ }[]
51
+ }
52
+
53
+ /** What a sink receives, and what the webhook sink POSTs as its body; tell them apart by `kind`. */
54
+ export type AlertPayload = AlertEventPayload | AlertDigestPayload
55
+
56
+ export type AlertDelivery = SinkDelivery<AlertPayload>
57
+
58
+ /** A place alerts are sent; see `Sink`. */
59
+ export type AlertSink = Sink<AlertPayload>
60
+
61
+ export interface AlertWebhookConfig {
62
+ /** Sink name; default `'webhook'`. Two sinks on one handle need distinct names. */
63
+ readonly name?: string
64
+ readonly url: string
65
+ readonly secret: string
66
+ /** Request timeout; default 10 000, at most 20 000 (the handle gives a send 10 s and leases the row for 30 s). */
67
+ readonly timeoutMs?: number
68
+ readonly signing?: WebhookSignatureOptions
69
+ }
70
+
71
+ /**
72
+ * POSTs the payload as JSON with `X-Webhook-Id`, `X-Webhook-Timestamp` and `X-Webhook-Signature`
73
+ * (`X-Webhook-Id` is the delivery id, the same on every attempt; HMAC over `<id>.<timestamp>.<body>`, as `webhook.sign` makes them). Any non-2xx status fails the attempt.
74
+ */
75
+ export function webhookSink<P extends { readonly kind: string } = AlertPayload>(config: AlertWebhookConfig): Sink<P> {
76
+ const timeoutMs = config.timeoutMs ?? 10_000
77
+ if (timeoutMs > 20_000) {
78
+ throw new Error(`[pipework] alerts.webhook: timeoutMs ${String(timeoutMs)} is over 20000. A delivery is leased for 30 s while it is sent; a longer send could be picked up and sent twice.`)
79
+ }
80
+ return {
81
+ name: config.name ?? 'webhook',
82
+ async send(delivery) {
83
+ const signed = signPayload(delivery.payload, config.secret, { ...config.signing, messageId: delivery.id })
84
+ const controller = new AbortController()
85
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
86
+ try {
87
+ const response = await fetch(config.url, {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ 'X-Webhook-Id': signed.messageId,
92
+ 'X-Webhook-Timestamp': signed.timestamp.toString(),
93
+ 'X-Webhook-Signature': signed.signature,
94
+ },
95
+ body: signed.body,
96
+ signal: controller.signal,
97
+ })
98
+ await response.text().catch(() => '')
99
+ if (response.status < 200 || response.status >= 300) throw new Error(`webhook answered HTTP ${String(response.status)}`)
100
+ } finally {
101
+ clearTimeout(timer)
102
+ }
103
+ },
104
+ }
105
+ }
106
+
107
+ export interface MemoryAlertSink<P extends { readonly kind: string } = AlertPayload> extends Sink<P> {
108
+ /** Every delivery received, in order. */
109
+ readonly delivered: SinkDelivery<P>[]
110
+ clear(): void
111
+ }
112
+
113
+ /** A sink that keeps what it receives, for tests. Generic over the payload so it fits `support.create` (`memorySink<SupportEventPayload>()`, or inferred where a support sink is expected). */
114
+ export function memorySink<P extends { readonly kind: string } = AlertPayload>(options?: { name?: string }): MemoryAlertSink<P>
115
+ export function memorySink(options?: { name?: string }): MemoryAlertSink
116
+ export function memorySink(options: { name?: string } = {}): MemoryAlertSink<{ readonly kind: string }> {
117
+ const delivered: SinkDelivery<{ readonly kind: string }>[] = []
118
+ return {
119
+ name: options.name ?? 'memory',
120
+ delivered,
121
+ // eslint-disable-next-line @typescript-eslint/require-await -- the sink contract is async
122
+ async send(delivery) { delivered.push(delivery) },
123
+ clear() { delivered.length = 0 },
124
+ }
125
+ }
@@ -0,0 +1,26 @@
1
+ // Options and helpers shared by the alert sources built on `querySource` and `newRowsSource`.
2
+
3
+ import type { AlertSeverity } from './handle.js'
4
+
5
+ export interface Basics { owner?: string; runbook?: string; severity?: AlertSeverity; database?: string }
6
+
7
+ export const FIFTEEN_MIN = 15 * 60_000
8
+ export const HOUR = 3_600_000
9
+
10
+ export function positive(value: number, label: string, who: string): number {
11
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
12
+ throw new Error(`[pipework] ${who} ${label} must be a positive number of milliseconds, got ${String(value)}.`)
13
+ }
14
+ return Math.trunc(value)
15
+ }
16
+
17
+ export function common(options: Basics): Basics {
18
+ return {
19
+ ...(options.owner !== undefined ? { owner: options.owner } : {}),
20
+ ...(options.runbook !== undefined ? { runbook: options.runbook } : {}),
21
+ ...(options.severity !== undefined ? { severity: options.severity } : {}),
22
+ ...(options.database !== undefined ? { database: options.database } : {}),
23
+ }
24
+ }
25
+
26
+ export const text = (v: unknown): string => v === null || v === undefined ? 'none' : String(v)
@@ -0,0 +1,503 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { sql } from '../../data/db/sql.js'
3
+ import { pipeSystem } from '../../data/db/db.js'
4
+ import { assertValidIdentifier, quoteIdentifier } from '../../data/db/identifiers.js'
5
+ import type { HealthCheck } from '../../core/lifecycle/types.js'
6
+ import type { SQL } from '../../data/db/index.js'
7
+ import type { Manifold } from '../../core/pipework.js'
8
+ import type { AlertSeverity, JsonValue } from './handle.js'
9
+ import { ALERTS_TICK_LOOP, LOOP_BEATS_QUERY, staleLoops } from './beat.js'
10
+ import { getContext } from '../../request/context/store.js'
11
+ import { describeError } from '../../core/logging/error-chain.js'
12
+
13
+ /** A condition a source finds true right now. */
14
+ export interface AlertCondition {
15
+ readonly key: string
16
+ readonly severity: AlertSeverity
17
+ readonly message: string
18
+ readonly subject?: { tenantId?: string; entity?: string; entityId?: string }
19
+ /** Passed to `alerts.raise` with the condition: who answers it, where the runbook is, and structured detail. */
20
+ readonly owner?: string
21
+ readonly runbook?: string
22
+ readonly context?: Readonly<Record<string, JsonValue>>
23
+ }
24
+
25
+ /**
26
+ * Evaluated on every tick. Each returned condition is raised (so an ongoing one groups). When
27
+ * `owns` is given, an open alert the source owns that it no longer returns is resolved.
28
+ */
29
+ export interface AlertSource {
30
+ readonly name: string
31
+ owns?(key: string): boolean
32
+ evaluate(): Promise<AlertCondition[]>
33
+ }
34
+
35
+ interface TableOption { readonly table?: string; readonly database?: string }
36
+
37
+ type Row = Record<string, unknown>
38
+
39
+ interface SourceBasics { readonly severity?: AlertSeverity; readonly owner?: string; readonly runbook?: string; readonly database?: string }
40
+
41
+ function assertSourceName(name: string, who: string): void {
42
+ if (typeof name !== 'string' || name === '' || name.includes(':')) {
43
+ throw new Error(`[pipework] ${who}: name must be a non-empty string without ':', got ${JSON.stringify(name)}.`)
44
+ }
45
+ }
46
+
47
+ function ownerFields(options: { owner?: string; runbook?: string }): { owner?: string; runbook?: string } {
48
+ return {
49
+ ...(options.owner !== undefined ? { owner: options.owner } : {}),
50
+ ...(options.runbook !== undefined ? { runbook: options.runbook } : {}),
51
+ }
52
+ }
53
+
54
+ /** One source from several: it evaluates each in turn and owns what any of them owns. */
55
+ export function composeSources(name: string, parts: readonly AlertSource[]): AlertSource {
56
+ const owning = parts.filter(p => p.owns !== undefined)
57
+ return {
58
+ name,
59
+ ...(owning.length > 0 ? { owns: (key: string) => owning.some(p => p.owns!(key)) } : {}),
60
+ async evaluate() {
61
+ const out: AlertCondition[] = []
62
+ for (const part of parts) out.push(...await part.evaluate())
63
+ return out
64
+ },
65
+ }
66
+ }
67
+
68
+ export interface QuerySourceOptions extends SourceBasics {
69
+ /** Key prefix: conditions are keyed `${name}:${id}`. Non-empty, no ':'. */
70
+ readonly name: string
71
+ /** Returns one row per subject that is in trouble right now. */
72
+ readonly query: SQL
73
+ readonly id: (row: Row) => string
74
+ readonly message: (row: Row) => string
75
+ readonly context?: (row: Row) => Record<string, JsonValue>
76
+ readonly subject?: (row: Row) => AlertCondition['subject']
77
+ }
78
+
79
+ /**
80
+ * For a condition that is true right now: runs `query` (as the system, any tenant) on every tick and raises
81
+ * `${name}:${id}` for each row. An open alert under `${name}:` that a tick no longer returns is resolved.
82
+ */
83
+ export function querySource(options: QuerySourceOptions): AlertSource {
84
+ assertSourceName(options.name, 'alerts.source.query')
85
+ const prefix = `${options.name}:`
86
+ const severity = options.severity ?? 'error'
87
+ return {
88
+ name: options.name,
89
+ owns: key => key.startsWith(prefix),
90
+ async evaluate() {
91
+ const rows = await pipeSystem(options.database).execute(options.query)
92
+ return rows.map((r: Row): AlertCondition => {
93
+ const subject = options.subject?.(r)
94
+ const context = options.context?.(r)
95
+ return {
96
+ key: `${prefix}${options.id(r)}`,
97
+ severity,
98
+ message: options.message(r),
99
+ ...(subject !== undefined ? { subject } : {}),
100
+ ...ownerFields(options),
101
+ ...(context !== undefined ? { context } : {}),
102
+ }
103
+ })
104
+ },
105
+ }
106
+ }
107
+
108
+ export interface NewRowsGroup { readonly id: string; readonly count: number; readonly last: Row; readonly sum: number }
109
+
110
+ export interface NewRowsSourceOptions extends SourceBasics {
111
+ /** Key prefix: conditions are keyed `${name}:${id}`; also the cursor's state key unless `stateKey` is given. */
112
+ readonly name: string
113
+ /** The table holding the recorded failures (an identifier, not schema-qualified). */
114
+ readonly table: string
115
+ /** The timestamp column a row is stamped with when the failure is recorded. */
116
+ readonly at: string
117
+ /** Extra filter; the table is aliased `t`. */
118
+ readonly where?: SQL
119
+ /** A column of the table, or an SQL expression over alias `t`; one condition per distinct value. */
120
+ readonly groupBy: string | SQL
121
+ readonly message: (group: NewRowsGroup) => string
122
+ readonly context?: (group: NewRowsGroup) => Record<string, JsonValue>
123
+ readonly subject?: (group: NewRowsGroup) => AlertCondition['subject']
124
+ /** A numeric column to total per group, handed to the callbacks as `sum` (0 without it). */
125
+ readonly sum?: string
126
+ /** Rows are read once they are older than this many ms (default 30_000); see `newRowsSource`. */
127
+ readonly settleMs?: number
128
+ /** The cursor's key in `pipework_alert_source_state`; default `name`. */
129
+ readonly stateKey?: string
130
+ }
131
+
132
+ /**
133
+ * For a condition that is "new since the last tick": raises `${name}:${id}` for each group of rows in `table`
134
+ * stamped in `at` after the cursor. The cursor lives in `pipework_alert_source_state`, shared by every process;
135
+ * the first evaluation only stores it, so history is not replayed. Each evaluation reads rows with
136
+ * `cursor < at <= clock_timestamp() - settleMs` and moves the cursor to that upper bound, so a row stamped with
137
+ * its transaction's start time that commits within `settleMs` is still read. `settleMs` must exceed the longest
138
+ * transaction that writes the table. It has no `owns`: the condition clears by the handle's quiet period
139
+ * (`resolveAfterQuietMs` on `alerts.create`).
140
+ */
141
+ export function newRowsSource(options: NewRowsSourceOptions): AlertSource {
142
+ assertSourceName(options.name, 'alerts.source.newRows')
143
+ const settleMs = options.settleMs ?? 30_000
144
+ if (typeof settleMs !== 'number' || !Number.isFinite(settleMs) || settleMs < 0) {
145
+ throw new Error(`[pipework] alerts.source.newRows settleMs must be a non-negative number of milliseconds, got ${String(settleMs)}.`)
146
+ }
147
+ const table = quoteIdentifier(options.table, 'newRows table')
148
+ const at = `t.${quoteIdentifier(options.at, 'newRows at column')}`
149
+ const group = typeof options.groupBy === 'string' ? sql.raw(`t.${quoteIdentifier(options.groupBy, 'newRows groupBy column')}`) : options.groupBy
150
+ const total = options.sum === undefined ? sql`0` : sql.raw(`sum(t.${quoteIdentifier(options.sum, 'newRows sum column')})`)
151
+ const where = options.where === undefined ? sql`` : sql`AND (${options.where})`
152
+ const stateKey = options.stateKey ?? options.name
153
+ const severity = options.severity ?? 'error'
154
+ const prefix = `${options.name}:`
155
+ return {
156
+ name: options.name,
157
+ async evaluate() {
158
+ const db = pipeSystem(options.database)
159
+ const first = await db.execute(sql`
160
+ INSERT INTO pipework_alert_source_state (name, cursor_at)
161
+ VALUES (${stateKey}, clock_timestamp() - ${settleMs} * interval '1 millisecond')
162
+ ON CONFLICT (name) DO NOTHING RETURNING name`)
163
+ if (first.length > 0) return []
164
+ const rows = await db.execute(sql`
165
+ WITH cur AS (
166
+ SELECT cursor_at, clock_timestamp() - ${settleMs} * interval '1 millisecond' AS upto
167
+ FROM pipework_alert_source_state WHERE name = ${stateKey} FOR UPDATE),
168
+ totals AS (
169
+ SELECT (${group})::text AS gid, count(*)::int AS n, (${total})::float8 AS total
170
+ FROM ${sql.raw(table)} t, cur
171
+ WHERE ${sql.raw(at)} > cur.cursor_at AND ${sql.raw(at)} <= cur.upto ${where}
172
+ GROUP BY 1),
173
+ newest AS (
174
+ SELECT DISTINCT ON (1) (${group})::text AS gid, to_jsonb(t) AS last
175
+ FROM ${sql.raw(table)} t, cur
176
+ WHERE ${sql.raw(at)} > cur.cursor_at AND ${sql.raw(at)} <= cur.upto ${where}
177
+ ORDER BY 1, ${sql.raw(at)} DESC),
178
+ found AS (
179
+ SELECT totals.gid AS id, n, total, last FROM totals JOIN newest ON newest.gid IS NOT DISTINCT FROM totals.gid),
180
+ moved AS (
181
+ UPDATE pipework_alert_source_state s SET cursor_at = GREATEST(s.cursor_at, cur.upto)
182
+ FROM cur WHERE s.name = ${stateKey} RETURNING 1)
183
+ SELECT id, n, total, last, (SELECT count(*) FROM moved) AS moved FROM found ORDER BY id`)
184
+ return rows.map((r: Row): AlertCondition => {
185
+ const last = (typeof r['last'] === 'string' ? JSON.parse(r['last']) : r['last']) as Row
186
+ const g: NewRowsGroup = { id: r['id'] === null ? '(none)' : String(r['id']), count: Number(r['n']), last, sum: Number(r['total']) }
187
+ const subject = options.subject?.(g)
188
+ const context = options.context?.(g)
189
+ return {
190
+ key: `${prefix}${g.id}`,
191
+ severity,
192
+ message: options.message(g),
193
+ ...(subject !== undefined ? { subject } : {}),
194
+ ...ownerFields(options),
195
+ ...(context !== undefined ? { context } : {}),
196
+ }
197
+ })
198
+ },
199
+ }
200
+ }
201
+
202
+ function jobsTable(table: string | undefined): string {
203
+ const name = table ?? 'pipework_jobs'
204
+ assertValidIdentifier(name, 'queue table')
205
+ return name
206
+ }
207
+
208
+ /**
209
+ * Raises `job-dead-letter:<jobType>` for jobs that exhausted their attempts since the last
210
+ * evaluation (any process: the position is kept in `pipework_alert_source_state`; the first
211
+ * evaluation starts from now, so history is not replayed). The condition clears only by the
212
+ * handle's quiet period, so set `resolveAfterQuietMs` on `alerts.create`. `types` limits it to those job
213
+ * types and `excludeTypes` leaves them out (not both, neither empty); each table and each filter keeps its own position.
214
+ * Jobs are read `settleMs` (default 30_000) after they dead-letter, so one that commits late is not missed.
215
+ * Conditions carry `owner`, `runbook` and a context of `count` and `lastError`.
216
+ */
217
+ export function deadLetterSource(options: TableOption & { severity?: AlertSeverity; types?: readonly string[]; excludeTypes?: readonly string[]; owner?: string; runbook?: string; settleMs?: number } = {}): AlertSource {
218
+ if (options.types !== undefined && options.excludeTypes !== undefined) {
219
+ throw new Error('[pipework] alerts.source.deadLetter takes types or excludeTypes, not both.')
220
+ }
221
+ for (const opt of ['types', 'excludeTypes'] as const) {
222
+ if (options[opt] !== undefined && options[opt].length === 0) {
223
+ throw new Error(`[pipework] alerts.source.deadLetter ${opt} must not be empty; omit it to include every job type.`)
224
+ }
225
+ }
226
+ const jobs = jobsTable(options.table)
227
+ const name = 'job-dead-letter'
228
+ const mode = options.types !== undefined ? 'only' : options.excludeTypes !== undefined ? 'except' : null
229
+ const listed = [...new Set(options.types ?? options.excludeTypes ?? [])].sort()
230
+ const joined = listed.join(',')
231
+ const base = jobs === 'pipework_jobs' ? name : `${name}@${jobs}`
232
+ const stateKey = mode === null ? base
233
+ : `${base}:${mode}:${joined.length > 200 ? createHash('sha256').update(joined).digest('hex') : joined}`
234
+ const listJson = JSON.stringify(listed)
235
+ const typeFilter = mode === 'only' ? sql`AND t.job_type IN (SELECT jsonb_array_elements_text(${listJson}::jsonb))`
236
+ : mode === 'except' ? sql`AND t.job_type NOT IN (SELECT jsonb_array_elements_text(${listJson}::jsonb))`
237
+ : sql``
238
+ const lastError = (g: NewRowsGroup): string | null => {
239
+ const payload = g.last['error_payload']
240
+ const message = typeof payload === 'object' && payload !== null ? (payload as Row)['message'] : undefined
241
+ return message === null || message === undefined ? null : String(message)
242
+ }
243
+ return newRowsSource({
244
+ name,
245
+ stateKey,
246
+ table: jobs,
247
+ at: 'completed_at',
248
+ where: sql`t.status = 'dead_letter' ${typeFilter}`,
249
+ groupBy: 'job_type',
250
+ ...(options.settleMs !== undefined ? { settleMs: options.settleMs } : {}),
251
+ ...(options.severity !== undefined ? { severity: options.severity } : {}),
252
+ ...(options.database !== undefined ? { database: options.database } : {}),
253
+ ...ownerFields(options),
254
+ message: g => `${String(g.count)} ${g.id} job(s) dead-lettered` + (lastError(g) !== null ? `; last error: ${lastError(g)!}` : ''),
255
+ subject: g => ({ entity: 'job', entityId: g.id }),
256
+ context: g => ({ count: g.count, lastError: lastError(g) }),
257
+ })
258
+ }
259
+
260
+ /** Raises `job-long-running:<jobType>` while a job of that type has been claimed for longer than `olderThanMs`; resolves when none is. */
261
+ export function longRunningSource(options: TableOption & { olderThanMs: number; severity?: AlertSeverity }): AlertSource {
262
+ const jobs = quoteIdentifier(jobsTable(options.table), 'queue table')
263
+ return querySource({
264
+ name: 'job-long-running',
265
+ severity: options.severity ?? 'warning',
266
+ ...(options.database !== undefined ? { database: options.database } : {}),
267
+ query: sql`
268
+ SELECT job_type, count(*)::int AS n, (extract(epoch FROM clock_timestamp() - min(claimed_at)) * 1000)::bigint AS oldest_ms
269
+ FROM ${sql.raw(jobs)}
270
+ WHERE status IN ('claimed', 'running')
271
+ AND claimed_at < clock_timestamp() - ${options.olderThanMs} * interval '1 millisecond'
272
+ GROUP BY job_type ORDER BY job_type`,
273
+ id: r => String(r['job_type']),
274
+ message: r => `${String(r['n'])} ${String(r['job_type'])} job(s) running for more than ${String(options.olderThanMs)} ms (oldest ${String(r['oldest_ms'])} ms)`,
275
+ subject: r => ({ entity: 'job', entityId: String(r['job_type']) }),
276
+ })
277
+ }
278
+
279
+ /**
280
+ * Raises `queue-backlog:<jobType>` while more than `maxPending` jobs of a type are ready to claim, or the
281
+ * oldest has waited more than `maxOldestAgeMs`; resolves when the type is under both. Each condition carries
282
+ * `owner` and `runbook` and a context of `ready`, `running` and `oldestReadyWaitMs`.
283
+ */
284
+ export function backlogSource(options: TableOption & { maxPending?: number; maxOldestAgeMs?: number; severity?: AlertSeverity; owner?: string; runbook?: string }): AlertSource {
285
+ if (options.maxPending === undefined && options.maxOldestAgeMs === undefined) {
286
+ throw new Error('[pipework] alerts.source.backlog needs maxPending, maxOldestAgeMs or both.')
287
+ }
288
+ const jobs = quoteIdentifier(jobsTable(options.table), 'queue table')
289
+ const maxPending = options.maxPending ?? null
290
+ const maxAge = options.maxOldestAgeMs ?? null
291
+ return querySource({
292
+ name: 'queue-backlog',
293
+ severity: options.severity ?? 'warning',
294
+ ...(options.database !== undefined ? { database: options.database } : {}),
295
+ ...ownerFields(options),
296
+ query: sql`
297
+ SELECT job_type, n, running, oldest_ms FROM (
298
+ SELECT job_type,
299
+ (count(*) FILTER (WHERE status = 'pending' AND (next_retry_at IS NULL OR next_retry_at <= clock_timestamp())))::int AS n,
300
+ (count(*) FILTER (WHERE status IN ('claimed', 'running')))::int AS running,
301
+ (extract(epoch FROM clock_timestamp() - min(queued_at) FILTER (WHERE status = 'pending' AND (next_retry_at IS NULL OR next_retry_at <= clock_timestamp()))) * 1000)::bigint AS oldest_ms
302
+ FROM ${sql.raw(jobs)}
303
+ WHERE status IN ('pending', 'claimed', 'running')
304
+ GROUP BY job_type) p
305
+ WHERE (${maxPending}::int IS NOT NULL AND n > ${maxPending}::int)
306
+ OR (${maxAge}::bigint IS NOT NULL AND oldest_ms > ${maxAge}::bigint)
307
+ ORDER BY job_type`,
308
+ id: r => String(r['job_type']),
309
+ message: r => `${String(r['n'])} ${String(r['job_type'])} job(s) waiting (${String(r['running'])} running), the oldest for ${String(Number(r['oldest_ms']))} ms`,
310
+ subject: r => ({ entity: 'job', entityId: String(r['job_type']) }),
311
+ context: r => ({ ready: Number(r['n']), running: Number(r['running']), oldestReadyWaitMs: Number(r['oldest_ms']) }),
312
+ })
313
+ }
314
+
315
+ /** Raises `health:<name>` while a check reports unhealthy or throws; resolves when it is healthy again. Degraded is not a fault. */
316
+ export function healthSource(checks: readonly HealthCheck[], options: { severity?: AlertSeverity } = {}): AlertSource {
317
+ const severity = options.severity ?? 'error'
318
+ const keys = new Set(checks.map(c => `health:${c.name}`))
319
+ return {
320
+ name: 'health',
321
+ owns: key => keys.has(key),
322
+ async evaluate() {
323
+ const found: AlertCondition[] = []
324
+ for (const check of checks) {
325
+ let message: string | null = null
326
+ try {
327
+ const result = await check.check()
328
+ if (result.status === 'unhealthy') message = `${check.name} is unhealthy${result.message !== undefined ? `: ${result.message}` : ''}`
329
+ } catch (err) {
330
+ message = `${check.name} check threw: ${describeError(err).message}`
331
+ }
332
+ if (message !== null) found.push({ key: `health:${check.name}`, severity, message, subject: { entity: 'health', entityId: check.name } })
333
+ }
334
+ return found
335
+ },
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Raises `unit-fault:<unit>` for each systemd unit with an abnormal stop in the last `window` ms (rows written by
341
+ * `pipework alerts unit-fault`), with the count and last result; resolves once a unit has no fault in the window.
342
+ * `units` limits it to those units.
343
+ */
344
+ export function unitFaultSource(options: { window: number; owner?: string; runbook?: string; severity?: AlertSeverity; units?: readonly string[]; database?: string }): AlertSource {
345
+ if (typeof options.window !== 'number' || !Number.isFinite(options.window) || options.window <= 0) {
346
+ throw new Error('[pipework] alerts.source.unitFault needs window to be a positive number of milliseconds.')
347
+ }
348
+ if (options.units !== undefined && options.units.length === 0) {
349
+ throw new Error('[pipework] alerts.source.unitFault units must not be empty; omit it to watch every unit.')
350
+ }
351
+ const units = options.units === undefined ? null : JSON.stringify(options.units)
352
+ const text = (v: unknown): string | null => v === null || v === undefined ? null : String(v)
353
+ return querySource({
354
+ name: 'unit-fault',
355
+ severity: options.severity ?? 'error',
356
+ ...(options.database !== undefined ? { database: options.database } : {}),
357
+ ...ownerFields(options),
358
+ query: sql`
359
+ SELECT unit, count(*)::int AS n,
360
+ (array_agg(result ORDER BY at DESC))[1] AS last_result,
361
+ (array_agg(exit_code ORDER BY at DESC))[1] AS last_exit_code,
362
+ (array_agg(exit_status ORDER BY at DESC))[1] AS last_exit_status,
363
+ min(at) AS first_at, max(at) AS last_at,
364
+ ceil(extract(epoch FROM clock_timestamp() - min(at)) / 60)::int AS minutes
365
+ FROM pipework_unit_faults
366
+ WHERE at > clock_timestamp() - ${options.window} * interval '1 millisecond'
367
+ AND (${units}::jsonb IS NULL OR unit IN (SELECT jsonb_array_elements_text(${units}::jsonb)))
368
+ GROUP BY unit ORDER BY unit`,
369
+ id: r => String(r['unit']),
370
+ message: r => `${String(r['unit'])}: ${String(r['last_result'])} x${String(Number(r['n']))} in ${String(Math.max(1, Number(r['minutes'])))} min`,
371
+ subject: r => ({ entity: 'unit', entityId: String(r['unit']) }),
372
+ context: r => ({
373
+ count: Number(r['n']),
374
+ lastResult: String(r['last_result']),
375
+ lastExitCode: text(r['last_exit_code']),
376
+ lastExitStatus: text(r['last_exit_status']),
377
+ firstAt: new Date(r['first_at'] as string | Date).toISOString(),
378
+ lastAt: new Date(r['last_at'] as string | Date).toISOString(),
379
+ }),
380
+ })
381
+ }
382
+
383
+ const pad2 = (n: number): string => String(n).padStart(2, '0')
384
+
385
+ function untilText(reset: Date, now: Date): string {
386
+ const day = reset.toISOString().slice(0, 10)
387
+ const hhmm = `${pad2(reset.getUTCHours())}:${pad2(reset.getUTCMinutes())}Z`
388
+ return day === now.toISOString().slice(0, 10) ? hhmm : `${day} ${hhmm}`
389
+ }
390
+
391
+ /**
392
+ * Raises `quota-exhausted:<key>` while an upstream quota row (needs `quota` configured) has remaining at or
393
+ * under `floor` (default 0) before its reset, or refused a call since it was last recorded; resolves when the
394
+ * row recovers or its reset passes. `keys` limits it to those keys (a non-empty array or a RegExp). Conditions
395
+ * carry `owner`, `runbook` and a context of `key`, `remaining`, `limit`, `resetAt` and `refused`.
396
+ */
397
+ export function quotaExhaustedSource(options: { owner?: string; runbook?: string; keys?: readonly string[] | RegExp; floor?: number; severity?: AlertSeverity; database?: string } = {}): AlertSource {
398
+ const floor = options.floor ?? 0
399
+ if (!Number.isInteger(floor) || floor < 0) {
400
+ throw new Error(`[pipework] alerts.source.quotaExhausted floor must be a non-negative integer, got ${String(floor)}.`)
401
+ }
402
+ const keys = options.keys
403
+ if (Array.isArray(keys) && keys.length === 0) {
404
+ throw new Error('[pipework] alerts.source.quotaExhausted keys must not be empty; omit it to watch every key.')
405
+ }
406
+ const keyFilter = Array.isArray(keys)
407
+ ? sql`AND key IN (SELECT jsonb_array_elements_text(${JSON.stringify([...new Set(keys as readonly string[])])}::jsonb))`
408
+ : sql``
409
+ const base = querySource({
410
+ name: 'quota-exhausted',
411
+ severity: options.severity ?? 'error',
412
+ ...(options.database !== undefined ? { database: options.database } : {}),
413
+ ...ownerFields(options),
414
+ query: sql`
415
+ SELECT key, remaining, "limit", reset_at,
416
+ (remaining > ${floor} AND refused_at IS NOT NULL AND refused_at >= updated_at) AS refused_only,
417
+ (refused_at IS NOT NULL AND refused_at >= updated_at) AS refused
418
+ FROM outbound_quota
419
+ WHERE reset_at > now() ${keyFilter}
420
+ AND (remaining <= ${floor} OR (refused_at IS NOT NULL AND refused_at >= updated_at))
421
+ ORDER BY key`,
422
+ id: r => String(r['key']),
423
+ message: r => {
424
+ const remaining = Number(r['remaining'])
425
+ const limit = r['limit'] === null ? null : Number(r['limit'])
426
+ const head = limit === null ? `${String(r['key'])} ${String(remaining)} remaining` : `${String(r['key'])} ${String(remaining)}/${String(limit)}`
427
+ return `${head} until ${untilText(new Date(r['reset_at'] as string | Date), new Date())}${r['refused_only'] === true ? '; a call was refused' : ''}`
428
+ },
429
+ subject: r => ({ entity: 'quota', entityId: String(r['key']) }),
430
+ context: r => ({
431
+ key: String(r['key']),
432
+ remaining: Number(r['remaining']),
433
+ limit: r['limit'] === null ? null : Number(r['limit']),
434
+ resetAt: new Date(r['reset_at'] as string | Date).toISOString(),
435
+ refused: r['refused'] === true,
436
+ }),
437
+ })
438
+ if (!(keys instanceof RegExp)) return base
439
+ return {
440
+ ...base,
441
+ async evaluate() {
442
+ return (await base.evaluate()).filter(c => {
443
+ keys.lastIndex = 0
444
+ return keys.test(c.key.slice('quota-exhausted:'.length))
445
+ })
446
+ },
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Raises `loop-stalled:<name>` for each background loop (rows in `pipework_loop_beats`, written by the alerts tick,
452
+ * the job scheduler, reaper and claim loop, and the support events drain) that has not run, or not succeeded, within
453
+ * its allowance: `max(3 * its expected interval, 60 s)`, or `maxAge[name]` ms. `alerts.tick` is always left out, since
454
+ * a stopped tick cannot report itself; `pipework alerts stale` and `alerts.loopCheck` cover it from outside.
455
+ * `exclude` leaves other loops out. Conditions carry a context of `name`, `lastAt`, `lastOkAt`, `lastError`, `expectEveryMs` and `allowanceMs`.
456
+ */
457
+ export function loopStalledSource(options: { maxAge?: Readonly<Record<string, number>>; owner?: string; runbook?: string; severity?: AlertSeverity; exclude?: readonly string[]; database?: string } = {}): AlertSource {
458
+ const severity = options.severity ?? 'error'
459
+ const exclude = [ALERTS_TICK_LOOP, ...(options.exclude ?? [])]
460
+ return {
461
+ name: 'loop-stalled',
462
+ owns: key => key.startsWith('loop-stalled:'),
463
+ async evaluate() {
464
+ const rows = await pipeSystem(options.database).execute(sql.raw(LOOP_BEATS_QUERY))
465
+ return staleLoops(rows, { ...(options.maxAge !== undefined ? { maxAge: options.maxAge } : {}), exclude }).map(l => ({
466
+ key: `loop-stalled:${l.name}`,
467
+ severity,
468
+ message: l.message,
469
+ subject: { entity: 'loop', entityId: l.name },
470
+ ...(options.owner !== undefined ? { owner: options.owner } : {}),
471
+ ...(options.runbook !== undefined ? { runbook: options.runbook } : {}),
472
+ context: { name: l.name, lastAt: l.lastAt, lastOkAt: l.lastOkAt, lastError: l.lastError, expectEveryMs: l.expectEveryMs, allowanceMs: l.allowanceMs },
473
+ }))
474
+ },
475
+ }
476
+ }
477
+
478
+ /**
479
+ * A health check named `loops` for `/ready`: unhealthy while any included loop (default all, `alerts.tick` too) is
480
+ * stale, with the loops and their ages in the message. Inside a managed context it uses that instance; for
481
+ * `/ready` pass the `instance` (the route runs outside one).
482
+ */
483
+ export function loopCheck(options: { maxAge?: Readonly<Record<string, number>>; include?: readonly string[]; database?: string; instance?: Manifold } = {}): HealthCheck {
484
+ return {
485
+ name: 'loops',
486
+ async check() {
487
+ const instance = options.instance ?? getContext()?.instance
488
+ if (instance === undefined) {
489
+ throw new Error('[pipework] alerts.loopCheck needs { instance } when it runs outside a managed context (as /ready does).')
490
+ }
491
+ const name = options.database ?? instance.config.defaultDatabaseName()
492
+ const db = instance.pool.getOrCreate(instance.config.database(name)).drizzle
493
+ const rows = await db.execute(sql.raw(LOOP_BEATS_QUERY))
494
+ if (rows.length === 0) return { status: 'unhealthy', message: 'no loop has ever recorded a beat' }
495
+ const stale = staleLoops(rows, {
496
+ ...(options.maxAge !== undefined ? { maxAge: options.maxAge } : {}),
497
+ ...(options.include !== undefined ? { include: options.include } : {}),
498
+ })
499
+ if (stale.length === 0) return { status: 'healthy' }
500
+ return { status: 'unhealthy', message: stale.map(l => l.message).join('; ') }
501
+ },
502
+ }
503
+ }