pipework 0.252.0 → 0.252.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (690) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/package.json +4 -3
  3. package/src/async/jobs/abortable-delay.ts +18 -0
  4. package/src/async/jobs/claims.ts +52 -0
  5. package/src/async/jobs/cron.ts +179 -0
  6. package/src/async/jobs/errors.ts +56 -0
  7. package/src/async/jobs/exclusion.ts +117 -0
  8. package/src/async/jobs/execute.ts +219 -0
  9. package/src/async/jobs/index.ts +10 -0
  10. package/src/async/jobs/listener.ts +135 -0
  11. package/src/async/jobs/namespace.ts +20 -0
  12. package/src/async/jobs/queue-door.ts +44 -0
  13. package/src/async/jobs/queue.ts +1012 -0
  14. package/src/async/jobs/run-script.ts +67 -0
  15. package/src/async/jobs/scheduler.ts +218 -0
  16. package/src/async/jobs/schedules-table.ts +37 -0
  17. package/src/async/jobs/serialize-error.ts +48 -0
  18. package/src/async/jobs/table.ts +191 -0
  19. package/src/async/pipeline/define.ts +53 -0
  20. package/src/async/pipeline/errors.ts +35 -0
  21. package/src/async/pipeline/execute.ts +53 -0
  22. package/src/async/pipeline/index.ts +13 -0
  23. package/src/async/pipeline/namespace.ts +12 -0
  24. package/src/async/pipeline/resume.ts +67 -0
  25. package/src/async/pipeline/run-steps.ts +98 -0
  26. package/src/async/pipeline/types.ts +57 -0
  27. package/src/async/state-machine/define.ts +64 -0
  28. package/src/async/state-machine/errors.ts +29 -0
  29. package/src/async/state-machine/index.ts +3 -0
  30. package/src/async/state-machine/namespace.ts +12 -0
  31. package/src/async/state-machine/transition.ts +90 -0
  32. package/src/async/state-machine/types.ts +37 -0
  33. package/src/async/steps/errors.ts +31 -0
  34. package/src/async/steps/graph.ts +1214 -0
  35. package/src/async/steps/hash.ts +40 -0
  36. package/src/async/steps/index.ts +7 -0
  37. package/src/async/steps/namespace.ts +7 -0
  38. package/src/async/steps/table.ts +97 -0
  39. package/src/async/steps/types.ts +318 -0
  40. package/src/audit/chain/index.ts +9 -0
  41. package/src/audit/chain/install.ts +113 -0
  42. package/src/audit/chain/retire.ts +52 -0
  43. package/src/audit/chain/verify.ts +257 -0
  44. package/src/audit/query/cursor.ts +37 -0
  45. package/src/audit/query/index.ts +9 -0
  46. package/src/audit/query/query.ts +223 -0
  47. package/src/audit/schema/audit-record-fields.ts +68 -0
  48. package/src/audit/schema/audit-record.ts +23 -0
  49. package/src/auth/auth/audit.ts +31 -0
  50. package/src/auth/auth/chain.ts +84 -0
  51. package/src/auth/auth/cookie.ts +136 -0
  52. package/src/auth/auth/email-links.ts +311 -0
  53. package/src/auth/auth/guard.ts +128 -0
  54. package/src/auth/auth/index.ts +18 -0
  55. package/src/auth/auth/multi-org.ts +337 -0
  56. package/src/auth/auth/namespace.ts +34 -0
  57. package/src/auth/auth/parse-expiry.ts +13 -0
  58. package/src/auth/auth/passwords.ts +110 -0
  59. package/src/auth/auth/schema.ts +110 -0
  60. package/src/auth/auth/sessions.ts +565 -0
  61. package/src/auth/auth/sign-in.ts +156 -0
  62. package/src/auth/auth/signing-keys.ts +69 -0
  63. package/src/auth/auth/types.ts +26 -0
  64. package/src/auth/auth/uuid.ts +10 -0
  65. package/src/auth/rbac/cache.ts +55 -0
  66. package/src/auth/rbac/check.ts +30 -0
  67. package/src/auth/rbac/create.ts +35 -0
  68. package/src/auth/rbac/index.ts +5 -0
  69. package/src/auth/rbac/middleware.ts +20 -0
  70. package/src/auth/rbac/namespace.ts +15 -0
  71. package/src/auth/rbac/resolve.ts +31 -0
  72. package/src/auth/rbac/schema.sql +31 -0
  73. package/src/auth/rbac/seed.ts +67 -0
  74. package/src/auth/rbac/tables.ts +32 -0
  75. package/src/auth/rbac/types.ts +65 -0
  76. package/src/auth/tenant/extract.ts +33 -0
  77. package/src/auth/tenant/in-tenant.ts +49 -0
  78. package/src/auth/tenant/index.ts +7 -0
  79. package/src/auth/tenant/namespace.ts +23 -0
  80. package/src/auth/tenant/propagate.ts +20 -0
  81. package/src/auth/tenant/rls.ts +120 -0
  82. package/src/auth/tenant/scope.ts +33 -0
  83. package/src/auth/tenant/scoped-db.ts +581 -0
  84. package/src/auth/tenant/types.ts +14 -0
  85. package/src/auth/tenant/validate.ts +27 -0
  86. package/src/cli/commands/add.ts +158 -0
  87. package/src/cli/commands/alerts.ts +98 -0
  88. package/src/cli/commands/check.ts +1418 -0
  89. package/src/cli/commands/codemod.ts +78 -0
  90. package/src/cli/commands/db.ts +356 -0
  91. package/src/cli/commands/dev.ts +101 -0
  92. package/src/cli/commands/generate.ts +75 -0
  93. package/src/cli/commands/init.ts +36 -0
  94. package/src/cli/commands/install.ts +28 -0
  95. package/src/cli/commands/lint.ts +62 -0
  96. package/src/cli/commands/migrate.ts +132 -0
  97. package/src/cli/commands/openapi.ts +63 -0
  98. package/src/cli/commands/remove.ts +72 -0
  99. package/src/cli/commands/run.ts +50 -0
  100. package/src/cli/commands/scope.ts +54 -0
  101. package/src/cli/commands/serve.ts +68 -0
  102. package/src/cli/commands/shard.ts +31 -0
  103. package/src/cli/commands/shared/config-pipework-version.ts +28 -0
  104. package/src/cli/commands/shared/find-config.ts +22 -0
  105. package/src/cli/commands/shared/load-config.ts +35 -0
  106. package/src/cli/commands/shared/pnpm-install.ts +95 -0
  107. package/src/cli/commands/shared/report-removed.ts +8 -0
  108. package/src/cli/commands/sync.ts +32 -0
  109. package/src/cli/commands/test.ts +307 -0
  110. package/src/cli/commands/upgrade.ts +48 -0
  111. package/src/cli/dev-bootstrap.ts +24 -0
  112. package/src/cli/errors.ts +10 -0
  113. package/src/cli/index.ts +303 -0
  114. package/src/cli/output.ts +17 -0
  115. package/src/cli/port.ts +15 -0
  116. package/src/cli/surface-kinds.ts +20 -0
  117. package/src/client/index.ts +4 -0
  118. package/src/client/poll.ts +130 -0
  119. package/src/client/report-fault.ts +119 -0
  120. package/src/core/config/app-pool.ts +19 -0
  121. package/src/core/config/aws-env.ts +29 -0
  122. package/src/core/config/base-dir.ts +24 -0
  123. package/src/core/config/discover.ts +64 -0
  124. package/src/core/config/env-resolve.ts +87 -0
  125. package/src/core/config/env-types.ts +116 -0
  126. package/src/core/config/env.ts +55 -0
  127. package/src/core/config/errors.ts +9 -0
  128. package/src/core/config/import-config.ts +40 -0
  129. package/src/core/config/index.ts +8 -0
  130. package/src/core/config/load.ts +417 -0
  131. package/src/core/config/namespace.ts +14 -0
  132. package/src/core/config/resolve.ts +195 -0
  133. package/src/core/config/schema.ts +488 -0
  134. package/src/core/config/statement-observer.ts +48 -0
  135. package/src/core/config/ts-inline-hooks.ts +75 -0
  136. package/src/core/config/ts-register.ts +4 -0
  137. package/src/core/config/ts-resolve-hooks.ts +101 -0
  138. package/src/core/errors/base.ts +49 -0
  139. package/src/core/errors/http.ts +146 -0
  140. package/src/core/errors/index.ts +2 -0
  141. package/src/core/errors/namespace.ts +31 -0
  142. package/src/core/lifecycle/drain.ts +33 -0
  143. package/src/core/lifecycle/health.ts +72 -0
  144. package/src/core/lifecycle/index.ts +4 -0
  145. package/src/core/lifecycle/namespace.ts +7 -0
  146. package/src/core/lifecycle/startup.ts +102 -0
  147. package/src/core/lifecycle/types.ts +36 -0
  148. package/src/core/logging/context.ts +60 -0
  149. package/src/core/logging/error-chain.ts +68 -0
  150. package/src/core/logging/index.ts +5 -0
  151. package/src/core/logging/logger.ts +65 -0
  152. package/src/core/logging/namespace.ts +16 -0
  153. package/src/core/logging/proxy.ts +22 -0
  154. package/src/core/logging/redact-match.ts +24 -0
  155. package/src/core/logging/types.ts +47 -0
  156. package/src/core/pipework.ts +566 -0
  157. package/src/core/runtime/broker.ts +69 -0
  158. package/src/core/surface/http.ts +133 -0
  159. package/src/core/surface/index.ts +11 -0
  160. package/src/core/surface/job-timeout.ts +40 -0
  161. package/src/core/surface/namespace.ts +13 -0
  162. package/src/core/surface/script.ts +33 -0
  163. package/src/core/surface/start.ts +142 -0
  164. package/src/core/surface/types.ts +225 -0
  165. package/src/core/surface/validate.ts +88 -0
  166. package/src/core/surface/worker.ts +528 -0
  167. package/src/core/validation/connectivity.ts +49 -0
  168. package/src/core/validation/domain.ts +99 -0
  169. package/src/core/validation/index.ts +5 -0
  170. package/src/core/validation/queue-tables.ts +312 -0
  171. package/src/core/validation/rls-role.ts +138 -0
  172. package/src/core/validation/startup.ts +51 -0
  173. package/src/core/validation/surfaces.ts +65 -0
  174. package/src/core/validation/types.ts +25 -0
  175. package/src/data/db/advisory-lock.ts +55 -0
  176. package/src/data/db/aggregates.ts +172 -0
  177. package/src/data/db/array-elements.ts +111 -0
  178. package/src/data/db/bulk-set.ts +53 -0
  179. package/src/data/db/col/index.ts +14 -0
  180. package/src/data/db/composed.ts +53 -0
  181. package/src/data/db/db.ts +276 -0
  182. package/src/data/db/excluded.ts +16 -0
  183. package/src/data/db/expressions.ts +473 -0
  184. package/src/data/db/filter.ts +90 -0
  185. package/src/data/db/function-source.ts +114 -0
  186. package/src/data/db/identifiers.ts +16 -0
  187. package/src/data/db/idx/index.ts +9 -0
  188. package/src/data/db/in-list.ts +49 -0
  189. package/src/data/db/index.ts +9 -0
  190. package/src/data/db/insert-split.ts +137 -0
  191. package/src/data/db/instrumentation.ts +82 -0
  192. package/src/data/db/maintenance.ts +28 -0
  193. package/src/data/db/namespace.ts +107 -0
  194. package/src/data/db/owner-labels.ts +9 -0
  195. package/src/data/db/pool.ts +187 -0
  196. package/src/data/db/query-label.ts +13 -0
  197. package/src/data/db/query-watch.ts +116 -0
  198. package/src/data/db/raw-client-warning.ts +37 -0
  199. package/src/data/db/readonly.ts +23 -0
  200. package/src/data/db/reservation.ts +151 -0
  201. package/src/data/db/schema-features.ts +14 -0
  202. package/src/data/db/serializable.ts +83 -0
  203. package/src/data/db/sql.ts +1 -0
  204. package/src/data/db/timeouts.ts +76 -0
  205. package/src/data/db/types.ts +18 -0
  206. package/src/data/db/unbound.ts +33 -0
  207. package/src/data/db/values.ts +29 -0
  208. package/src/data/domain/brand.ts +15 -0
  209. package/src/data/domain/build-factory.ts +193 -0
  210. package/src/data/domain/build-schema.ts +161 -0
  211. package/src/data/domain/build-table.ts +355 -0
  212. package/src/data/domain/define.ts +239 -0
  213. package/src/data/domain/external-registry.ts +34 -0
  214. package/src/data/domain/external.ts +77 -0
  215. package/src/data/domain/field.ts +279 -0
  216. package/src/data/domain/index.ts +13 -0
  217. package/src/data/domain/naming.ts +6 -0
  218. package/src/data/domain/projections.ts +57 -0
  219. package/src/data/domain/trace-safe-projection.ts +31 -0
  220. package/src/data/domain/traits.ts +108 -0
  221. package/src/data/domain/types.ts +289 -0
  222. package/src/data/jsonb/composed.ts +18 -0
  223. package/src/data/jsonb/index.ts +1 -0
  224. package/src/data/jsonb/namespace.ts +30 -0
  225. package/src/data/jsonb/query.ts +86 -0
  226. package/src/data/migrate/append-only-generate.ts +47 -0
  227. package/src/data/migrate/applied.ts +47 -0
  228. package/src/data/migrate/apply.ts +309 -0
  229. package/src/data/migrate/check.ts +119 -0
  230. package/src/data/migrate/diff.ts +357 -0
  231. package/src/data/migrate/door-generate.ts +130 -0
  232. package/src/data/migrate/framework-rls.ts +58 -0
  233. package/src/data/migrate/generate.ts +201 -0
  234. package/src/data/migrate/history.ts +215 -0
  235. package/src/data/migrate/index.ts +9 -0
  236. package/src/data/migrate/internal-definitions.ts +175 -0
  237. package/src/data/migrate/post-process.ts +73 -0
  238. package/src/data/migrate/pragma.ts +73 -0
  239. package/src/data/migrate/provision-framework.ts +147 -0
  240. package/src/data/migrate/rls-generate.ts +61 -0
  241. package/src/data/migrate/run.ts +124 -0
  242. package/src/data/migrate/safety.ts +167 -0
  243. package/src/data/migrate/snapshot.ts +368 -0
  244. package/src/data/migrate/sql-emit.ts +392 -0
  245. package/src/data/migrate/statements.ts +312 -0
  246. package/src/data/query/alias.ts +126 -0
  247. package/src/data/query/batch.ts +8 -0
  248. package/src/data/query/cache/core/cache.ts +78 -0
  249. package/src/data/query/cache/core/index.ts +1 -0
  250. package/src/data/query/cache/core/types.ts +29 -0
  251. package/src/data/query/cache/readme.md +230 -0
  252. package/src/data/query/casing.ts +76 -0
  253. package/src/data/query/collation.ts +11 -0
  254. package/src/data/query/column-builder.ts +370 -0
  255. package/src/data/query/column.ts +150 -0
  256. package/src/data/query/entity.ts +42 -0
  257. package/src/data/query/errors.ts +33 -0
  258. package/src/data/query/index.ts +14 -0
  259. package/src/data/query/logger.ts +47 -0
  260. package/src/data/query/operations.ts +45 -0
  261. package/src/data/query/pg-core/alias.ts +12 -0
  262. package/src/data/query/pg-core/checks.ts +32 -0
  263. package/src/data/query/pg-core/columns/all.ts +71 -0
  264. package/src/data/query/pg-core/columns/bigint.ts +110 -0
  265. package/src/data/query/pg-core/columns/bigserial.ts +128 -0
  266. package/src/data/query/pg-core/columns/boolean.ts +43 -0
  267. package/src/data/query/pg-core/columns/char.ts +85 -0
  268. package/src/data/query/pg-core/columns/cidr.ts +43 -0
  269. package/src/data/query/pg-core/columns/common.ts +366 -0
  270. package/src/data/query/pg-core/columns/custom.ts +232 -0
  271. package/src/data/query/pg-core/columns/date.common.ts +15 -0
  272. package/src/data/query/pg-core/columns/date.ts +112 -0
  273. package/src/data/query/pg-core/columns/double-precision.ts +55 -0
  274. package/src/data/query/pg-core/columns/enum.ts +201 -0
  275. package/src/data/query/pg-core/columns/index.ts +35 -0
  276. package/src/data/query/pg-core/columns/inet.ts +43 -0
  277. package/src/data/query/pg-core/columns/int.common.ts +57 -0
  278. package/src/data/query/pg-core/columns/integer.ts +53 -0
  279. package/src/data/query/pg-core/columns/interval.ts +83 -0
  280. package/src/data/query/pg-core/columns/json.ts +58 -0
  281. package/src/data/query/pg-core/columns/jsonb.ts +56 -0
  282. package/src/data/query/pg-core/columns/line.ts +117 -0
  283. package/src/data/query/pg-core/columns/macaddr.ts +43 -0
  284. package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
  285. package/src/data/query/pg-core/columns/numeric.ts +227 -0
  286. package/src/data/query/pg-core/columns/point.ts +127 -0
  287. package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
  288. package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
  289. package/src/data/query/pg-core/columns/real.ts +58 -0
  290. package/src/data/query/pg-core/columns/serial.ts +55 -0
  291. package/src/data/query/pg-core/columns/smallint.ts +53 -0
  292. package/src/data/query/pg-core/columns/smallserial.ts +60 -0
  293. package/src/data/query/pg-core/columns/text.ts +67 -0
  294. package/src/data/query/pg-core/columns/time.ts +72 -0
  295. package/src/data/query/pg-core/columns/timestamp.ts +163 -0
  296. package/src/data/query/pg-core/columns/uuid.ts +51 -0
  297. package/src/data/query/pg-core/columns/varchar.ts +89 -0
  298. package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
  299. package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
  300. package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
  301. package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
  302. package/src/data/query/pg-core/db.ts +674 -0
  303. package/src/data/query/pg-core/dialect.ts +1404 -0
  304. package/src/data/query/pg-core/expressions.ts +25 -0
  305. package/src/data/query/pg-core/foreign-keys.ts +130 -0
  306. package/src/data/query/pg-core/index.ts +21 -0
  307. package/src/data/query/pg-core/indexes.ts +276 -0
  308. package/src/data/query/pg-core/policies.ts +56 -0
  309. package/src/data/query/pg-core/primary-keys.ts +62 -0
  310. package/src/data/query/pg-core/query-builders/count.ts +86 -0
  311. package/src/data/query/pg-core/query-builders/delete.ts +293 -0
  312. package/src/data/query/pg-core/query-builders/index.ts +7 -0
  313. package/src/data/query/pg-core/query-builders/insert.ts +452 -0
  314. package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
  315. package/src/data/query/pg-core/query-builders/query.ts +157 -0
  316. package/src/data/query/pg-core/query-builders/raw.ts +49 -0
  317. package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
  318. package/src/data/query/pg-core/query-builders/select.ts +1346 -0
  319. package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
  320. package/src/data/query/pg-core/query-builders/update.ts +626 -0
  321. package/src/data/query/pg-core/roles.ts +41 -0
  322. package/src/data/query/pg-core/schema.ts +72 -0
  323. package/src/data/query/pg-core/sequence.ts +41 -0
  324. package/src/data/query/pg-core/session.ts +292 -0
  325. package/src/data/query/pg-core/subquery.ts +46 -0
  326. package/src/data/query/pg-core/table.ts +252 -0
  327. package/src/data/query/pg-core/unique-constraint.ts +73 -0
  328. package/src/data/query/pg-core/utils/array.ts +95 -0
  329. package/src/data/query/pg-core/utils/index.ts +1 -0
  330. package/src/data/query/pg-core/utils.ts +104 -0
  331. package/src/data/query/pg-core/view-base.ts +14 -0
  332. package/src/data/query/pg-core/view-common.ts +1 -0
  333. package/src/data/query/pg-core/view.ts +435 -0
  334. package/src/data/query/pg-core/with-builder.ts +41 -0
  335. package/src/data/query/postgres-js/README.md +46 -0
  336. package/src/data/query/postgres-js/array-parser.ts +45 -0
  337. package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
  338. package/src/data/query/postgres-js/driver.ts +163 -0
  339. package/src/data/query/postgres-js/index.ts +2 -0
  340. package/src/data/query/postgres-js/session.ts +234 -0
  341. package/src/data/query/primary-key.ts +11 -0
  342. package/src/data/query/query-builders/query-builder.ts +19 -0
  343. package/src/data/query/query-builders/select.types.ts +175 -0
  344. package/src/data/query/query-promise.ts +35 -0
  345. package/src/data/query/relations.ts +725 -0
  346. package/src/data/query/runnable-query.ts +12 -0
  347. package/src/data/query/selection-proxy.ts +121 -0
  348. package/src/data/query/session.ts +8 -0
  349. package/src/data/query/sql/expressions/conditions.ts +741 -0
  350. package/src/data/query/sql/expressions/index.ts +2 -0
  351. package/src/data/query/sql/expressions/select.ts +45 -0
  352. package/src/data/query/sql/functions/aggregate.ts +129 -0
  353. package/src/data/query/sql/functions/index.ts +2 -0
  354. package/src/data/query/sql/functions/vector.ts +178 -0
  355. package/src/data/query/sql/index.ts +3 -0
  356. package/src/data/query/sql/sql.ts +794 -0
  357. package/src/data/query/subquery.ts +53 -0
  358. package/src/data/query/table.ts +208 -0
  359. package/src/data/query/table.utils.ts +2 -0
  360. package/src/data/query/tracing-utils.ts +3 -0
  361. package/src/data/query/tracing.ts +11 -0
  362. package/src/data/query/utils.ts +357 -0
  363. package/src/data/query/version.ts +4 -0
  364. package/src/data/query/view-common.ts +1 -0
  365. package/src/data/schema/branded.ts +13 -0
  366. package/src/data/schema/generated/coerce.ts +5 -0
  367. package/src/data/schema/generated/combinator.ts +6 -0
  368. package/src/data/schema/generated/encoding.ts +5 -0
  369. package/src/data/schema/generated/error.ts +6 -0
  370. package/src/data/schema/generated/format.ts +5 -0
  371. package/src/data/schema/generated/type-constructor.ts +10 -0
  372. package/src/data/schema/generated/type.ts +6 -0
  373. package/src/data/schema/index.ts +6 -0
  374. package/src/data/schema/json-schema.ts +33 -0
  375. package/src/data/schema/namespace.ts +55 -0
  376. package/src/data/schema/parse.ts +39 -0
  377. package/src/data/schema/types.ts +39 -0
  378. package/src/data/temporal/append-only.ts +134 -0
  379. package/src/data/temporal/close.ts +27 -0
  380. package/src/data/temporal/columns.ts +10 -0
  381. package/src/data/temporal/definition-queries.ts +70 -0
  382. package/src/data/temporal/errors.ts +10 -0
  383. package/src/data/temporal/get-current.ts +22 -0
  384. package/src/data/temporal/index.ts +9 -0
  385. package/src/data/temporal/namespace.ts +36 -0
  386. package/src/data/temporal/query.ts +25 -0
  387. package/src/data/temporal/resolve.ts +17 -0
  388. package/src/data/temporal/revise.ts +67 -0
  389. package/src/data/temporal/types.ts +22 -0
  390. package/src/data/valve/artifact.ts +33 -0
  391. package/src/data/valve/contract-diff.ts +21 -0
  392. package/src/data/valve/cross.ts +57 -0
  393. package/src/data/valve/crossing-ledger.ts +50 -0
  394. package/src/data/valve/decision.ts +44 -0
  395. package/src/data/valve/durable-crossing.ts +52 -0
  396. package/src/data/valve/execute-crossing.ts +77 -0
  397. package/src/data/valve/faucet.ts +56 -0
  398. package/src/data/valve/gate.ts +114 -0
  399. package/src/data/valve/graph.ts +77 -0
  400. package/src/data/valve/index.ts +53 -0
  401. package/src/data/valve/namespace.ts +57 -0
  402. package/src/data/valve/registry.ts +50 -0
  403. package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
  404. package/src/data/valve/schema/valve-crossing.ts +9 -0
  405. package/src/data/valve/supply.ts +154 -0
  406. package/src/data/valve/types.ts +143 -0
  407. package/src/data/vector/columns.ts +27 -0
  408. package/src/data/vector/index.ts +4 -0
  409. package/src/data/vector/namespace.ts +33 -0
  410. package/src/data/vector/query.ts +85 -0
  411. package/src/data/vector/validate.ts +16 -0
  412. package/src/data/write/audit/actor.ts +15 -0
  413. package/src/data/write/audit/audit-columns.ts +18 -0
  414. package/src/data/write/audit/audit-select.ts +111 -0
  415. package/src/data/write/audit/bound-transaction.ts +14 -0
  416. package/src/data/write/audit/cte-compile.ts +32 -0
  417. package/src/data/write/audit/decode-returning.ts +47 -0
  418. package/src/data/write/audit/final-select.ts +43 -0
  419. package/src/data/write/audit/infra-audit.ts +36 -0
  420. package/src/data/write/audit/primary-key.ts +65 -0
  421. package/src/data/write/audit/projection.ts +41 -0
  422. package/src/data/write/audit/read-touches.ts +86 -0
  423. package/src/data/write/audit/touch-match.ts +78 -0
  424. package/src/data/write/audit/verb-delete.ts +62 -0
  425. package/src/data/write/audit/verb-insert.ts +36 -0
  426. package/src/data/write/audit/verb-select.ts +47 -0
  427. package/src/data/write/audit/verb-update.ts +60 -0
  428. package/src/data/write/audit/verb-upsert.ts +104 -0
  429. package/src/data/write/conflict.ts +43 -0
  430. package/src/data/write/delete.ts +38 -0
  431. package/src/data/write/execute.ts +243 -0
  432. package/src/data/write/index.ts +11 -0
  433. package/src/data/write/insert.ts +63 -0
  434. package/src/data/write/state.ts +43 -0
  435. package/src/data/write/update.ts +54 -0
  436. package/src/data/write/upsert.ts +58 -0
  437. package/src/data/write/values.ts +19 -0
  438. package/src/index.ts +230 -0
  439. package/src/infra/access-links/code.ts +70 -0
  440. package/src/infra/access-links/handle.ts +892 -0
  441. package/src/infra/access-links/index.ts +9 -0
  442. package/src/infra/access-links/namespace.ts +30 -0
  443. package/src/infra/access-links/person-check.ts +177 -0
  444. package/src/infra/access-links/schema.ts +71 -0
  445. package/src/infra/alerts/async-sources.ts +231 -0
  446. package/src/infra/alerts/audit.ts +36 -0
  447. package/src/infra/alerts/beat.ts +123 -0
  448. package/src/infra/alerts/client-faults.ts +111 -0
  449. package/src/infra/alerts/faults.ts +156 -0
  450. package/src/infra/alerts/handle.ts +778 -0
  451. package/src/infra/alerts/http.ts +192 -0
  452. package/src/infra/alerts/index.ts +12 -0
  453. package/src/infra/alerts/infra-sources.ts +180 -0
  454. package/src/infra/alerts/namespace.ts +81 -0
  455. package/src/infra/alerts/redact.ts +21 -0
  456. package/src/infra/alerts/request-faults.ts +41 -0
  457. package/src/infra/alerts/schema.ts +124 -0
  458. package/src/infra/alerts/sinks.ts +125 -0
  459. package/src/infra/alerts/source-options.ts +26 -0
  460. package/src/infra/alerts/sources.ts +503 -0
  461. package/src/infra/alerts/subsystem-sources.ts +137 -0
  462. package/src/infra/audit/emit.ts +87 -0
  463. package/src/infra/audit/index.ts +2 -0
  464. package/src/infra/audit/namespace.ts +22 -0
  465. package/src/infra/audit/schema.sql +35 -0
  466. package/src/infra/audit/types.ts +18 -0
  467. package/src/infra/aws/credentials.ts +147 -0
  468. package/src/infra/aws/sigv4.ts +140 -0
  469. package/src/infra/cache/create.ts +58 -0
  470. package/src/infra/cache/entry.ts +81 -0
  471. package/src/infra/cache/index.ts +4 -0
  472. package/src/infra/cache/namespace.ts +10 -0
  473. package/src/infra/cache/tenant.ts +93 -0
  474. package/src/infra/cache/types.ts +24 -0
  475. package/src/infra/crawl/audit.ts +35 -0
  476. package/src/infra/crawl/errors.ts +25 -0
  477. package/src/infra/crawl/handle.ts +693 -0
  478. package/src/infra/crawl/index.ts +9 -0
  479. package/src/infra/crawl/links.ts +160 -0
  480. package/src/infra/crawl/namespace.ts +36 -0
  481. package/src/infra/crawl/robots.ts +100 -0
  482. package/src/infra/crawl/schema.ts +89 -0
  483. package/src/infra/credit-line/handle.ts +883 -0
  484. package/src/infra/credit-line/index.ts +6 -0
  485. package/src/infra/credit-line/namespace.ts +7 -0
  486. package/src/infra/credit-line/schema.ts +96 -0
  487. package/src/infra/credits/handle.ts +706 -0
  488. package/src/infra/credits/index.ts +5 -0
  489. package/src/infra/credits/namespace.ts +7 -0
  490. package/src/infra/credits/schema.ts +83 -0
  491. package/src/infra/email/audit.ts +36 -0
  492. package/src/infra/email/inbound.ts +622 -0
  493. package/src/infra/email/index.ts +11 -0
  494. package/src/infra/email/mail.ts +517 -0
  495. package/src/infra/email/mime.ts +447 -0
  496. package/src/infra/email/namespace.ts +22 -0
  497. package/src/infra/email/providers.ts +111 -0
  498. package/src/infra/email/schema.ts +96 -0
  499. package/src/infra/email/sns.ts +200 -0
  500. package/src/infra/email/unsubscribe.ts +44 -0
  501. package/src/infra/fetch/address.ts +88 -0
  502. package/src/infra/fetch/connect-retry.ts +87 -0
  503. package/src/infra/fetch/handle.ts +329 -0
  504. package/src/infra/fetch/index.ts +7 -0
  505. package/src/infra/fetch/namespace.ts +70 -0
  506. package/src/infra/idempotency/index.ts +3 -0
  507. package/src/infra/idempotency/middleware.ts +87 -0
  508. package/src/infra/idempotency/store.ts +92 -0
  509. package/src/infra/idempotency/types.ts +30 -0
  510. package/src/infra/join-requests/handle.ts +312 -0
  511. package/src/infra/join-requests/index.ts +2 -0
  512. package/src/infra/join-requests/namespace.ts +7 -0
  513. package/src/infra/join-requests/schema.ts +33 -0
  514. package/src/infra/outbox/core.ts +165 -0
  515. package/src/infra/quota/errors.ts +32 -0
  516. package/src/infra/quota/handle.ts +215 -0
  517. package/src/infra/quota/index.ts +5 -0
  518. package/src/infra/quota/namespace.ts +7 -0
  519. package/src/infra/quota/schema.ts +15 -0
  520. package/src/infra/storage/backend.ts +34 -0
  521. package/src/infra/storage/handle.ts +513 -0
  522. package/src/infra/storage/index.ts +9 -0
  523. package/src/infra/storage/local.ts +104 -0
  524. package/src/infra/storage/namespace.ts +13 -0
  525. package/src/infra/storage/s3.ts +257 -0
  526. package/src/infra/storage/schema.ts +24 -0
  527. package/src/infra/support/handle.ts +854 -0
  528. package/src/infra/support/http.ts +229 -0
  529. package/src/infra/support/index.ts +7 -0
  530. package/src/infra/support/namespace.ts +12 -0
  531. package/src/infra/support/schema.ts +96 -0
  532. package/src/infra/webhook/inbound.ts +45 -0
  533. package/src/infra/webhook/index.ts +5 -0
  534. package/src/infra/webhook/namespace.ts +24 -0
  535. package/src/infra/webhook/outbound.ts +315 -0
  536. package/src/infra/webhook/sign.ts +32 -0
  537. package/src/infra/webhook/verify.ts +82 -0
  538. package/src/invariants/assert.ts +26 -0
  539. package/src/invariants/index.ts +1 -0
  540. package/src/lint/config.ts +56 -0
  541. package/src/lint/doctrine.ts +163 -0
  542. package/src/lint/include.ts +97 -0
  543. package/src/lint/index.ts +3 -0
  544. package/src/lint/preset.ts +10 -0
  545. package/src/lint/resolver.ts +218 -0
  546. package/src/lint/rules/no-reexports.ts +46 -0
  547. package/src/request/behavior/audited.ts +85 -0
  548. package/src/request/behavior/cached.ts +62 -0
  549. package/src/request/behavior/compose.ts +28 -0
  550. package/src/request/behavior/index.ts +5 -0
  551. package/src/request/behavior/namespace.ts +16 -0
  552. package/src/request/behavior/types.ts +26 -0
  553. package/src/request/behavior/versioned.ts +54 -0
  554. package/src/request/context/create.ts +103 -0
  555. package/src/request/context/guard.ts +49 -0
  556. package/src/request/context/index.ts +4 -0
  557. package/src/request/context/run-in-transaction.ts +200 -0
  558. package/src/request/context/store.ts +28 -0
  559. package/src/request/context/types.ts +50 -0
  560. package/src/request/di/builder.ts +535 -0
  561. package/src/request/di/index.ts +8 -0
  562. package/src/request/di/resolve.ts +271 -0
  563. package/src/request/di/types.ts +262 -0
  564. package/src/request/fitting/namespace.ts +7 -0
  565. package/src/request/fixture/batch.ts +71 -0
  566. package/src/request/fixture/builder.ts +26 -0
  567. package/src/request/fixture/errors.ts +10 -0
  568. package/src/request/fixture/index.ts +8 -0
  569. package/src/request/fixture/namespace.ts +23 -0
  570. package/src/request/fixture/paginate.ts +44 -0
  571. package/src/request/fixture/to-handlers.ts +260 -0
  572. package/src/request/fixture/types.ts +79 -0
  573. package/src/request/flow/act-as.ts +78 -0
  574. package/src/request/flow/namespace.ts +17 -0
  575. package/src/request/http/error-step.ts +42 -0
  576. package/src/request/http/file.ts +141 -0
  577. package/src/request/http/index.ts +4 -0
  578. package/src/request/http/middleware.ts +141 -0
  579. package/src/request/http/namespace.ts +5 -0
  580. package/src/request/http/rate-limit-classes.ts +170 -0
  581. package/src/request/http/response.ts +82 -0
  582. package/src/request/http/server.ts +916 -0
  583. package/src/request/http/sniff.ts +261 -0
  584. package/src/request/http/types.ts +250 -0
  585. package/src/request/http/upload.ts +284 -0
  586. package/src/request/internal/text.ts +6 -0
  587. package/src/request/openapi/generate.ts +205 -0
  588. package/src/request/openapi/index.ts +2 -0
  589. package/src/request/openapi/namespace.ts +7 -0
  590. package/src/test/auth.ts +13 -0
  591. package/src/test/auto-setup.ts +45 -0
  592. package/src/test/context.ts +91 -0
  593. package/src/test/crawl-fixture.ts +157 -0
  594. package/src/test/discover-fork.ts +17 -0
  595. package/src/test/fingerprint.ts +135 -0
  596. package/src/test/globals.d.ts +7 -0
  597. package/src/test/index.ts +19 -0
  598. package/src/test/namespace.ts +87 -0
  599. package/src/test/plugin.ts +189 -0
  600. package/src/test/progress.ts +185 -0
  601. package/src/test/reap-after-run.ts +28 -0
  602. package/src/test/reap.ts +286 -0
  603. package/src/test/reset.ts +253 -0
  604. package/src/test/savepoints.ts +172 -0
  605. package/src/test/setup.ts +1422 -0
  606. package/src/test/system-channel.ts +50 -0
  607. package/src/test/vitest-plugin.ts +142 -0
  608. package/src/test/vitest-workspace.ts +507 -0
  609. package/src/test/vitest.ts +333 -0
  610. package/src/test/watch.ts +105 -0
  611. package/src/trace/capture/bounded-serializer.ts +146 -0
  612. package/src/trace/cardinality/analyze.ts +173 -0
  613. package/src/trace/cardinality/branch-context.ts +214 -0
  614. package/src/trace/cardinality/check.ts +33 -0
  615. package/src/trace/cardinality/classify-edge.ts +64 -0
  616. package/src/trace/cardinality/structural-max.ts +107 -0
  617. package/src/trace/check/coverage-check.ts +188 -0
  618. package/src/trace/check/flow-check.ts +103 -0
  619. package/src/trace/check/flow-step-site.ts +234 -0
  620. package/src/trace/check/io-pattern.ts +116 -0
  621. package/src/trace/codemod/add-cardinality.ts +152 -0
  622. package/src/trace/codemod/run.ts +80 -0
  623. package/src/trace/entry/child-trace.ts +21 -0
  624. package/src/trace/entry/close-trace.ts +87 -0
  625. package/src/trace/entry/finalize-trace.ts +28 -0
  626. package/src/trace/entry/open-trace.ts +138 -0
  627. package/src/trace/entry/parent-trace.ts +20 -0
  628. package/src/trace/entry/parse-traceparent.ts +39 -0
  629. package/src/trace/entry/process-start.ts +38 -0
  630. package/src/trace/entry/trace-context.ts +26 -0
  631. package/src/trace/entry/tracing-enabled.ts +10 -0
  632. package/src/trace/external/record.ts +195 -0
  633. package/src/trace/external/report.ts +66 -0
  634. package/src/trace/namespace.ts +19 -0
  635. package/src/trace/partition/day.ts +47 -0
  636. package/src/trace/partition/index.ts +14 -0
  637. package/src/trace/partition/partition-name.ts +23 -0
  638. package/src/trace/partition/plan.ts +82 -0
  639. package/src/trace/partition/schedule.ts +26 -0
  640. package/src/trace/partition/startup-hook.ts +39 -0
  641. package/src/trace/partition/sync.ts +154 -0
  642. package/src/trace/partition/tables.ts +53 -0
  643. package/src/trace/read/tree.ts +83 -0
  644. package/src/trace/retain/copy-on-error.ts +76 -0
  645. package/src/trace/runtime/expire-stale-traces.ts +54 -0
  646. package/src/trace/runtime/flow-step.ts +147 -0
  647. package/src/trace/runtime/flush-steps.ts +31 -0
  648. package/src/trace/runtime/pending-writes.ts +54 -0
  649. package/src/trace/runtime/step-buffer.ts +298 -0
  650. package/src/trace/runtime/step-error.ts +21 -0
  651. package/src/trace/runtime/step-meta.ts +55 -0
  652. package/src/trace/runtime/thenable.ts +12 -0
  653. package/src/trace/runtime/truncate-trace.ts +62 -0
  654. package/src/trace/schema/trace-fields.ts +52 -0
  655. package/src/trace/schema/trace-retained.ts +22 -0
  656. package/src/trace/schema/trace-step-fields.ts +44 -0
  657. package/src/trace/schema/trace-step-retained.ts +25 -0
  658. package/src/trace/schema/trace-step.ts +18 -0
  659. package/src/trace/schema/trace.ts +12 -0
  660. package/src/trace/transformer/apply.ts +79 -0
  661. package/src/trace/transformer/build-meta-literal.ts +30 -0
  662. package/src/trace/transformer/find-flow-step-sites.ts +85 -0
  663. package/src/trace/transformer/inject-flow-step-import.ts +39 -0
  664. package/src/trace/transformer/transform-error.ts +14 -0
  665. package/src/trace/transformer/transform-flow-steps.ts +123 -0
  666. package/src/trace/transformer/wrap-function-declaration.ts +62 -0
  667. package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
  668. package/src/workspace/affected.ts +110 -0
  669. package/src/workspace/changed-files.ts +163 -0
  670. package/src/workspace/check-deps.ts +213 -0
  671. package/src/workspace/checksum.ts +20 -0
  672. package/src/workspace/dependency-graph.ts +67 -0
  673. package/src/workspace/detect-pipework-copies.ts +93 -0
  674. package/src/workspace/discover.ts +206 -0
  675. package/src/workspace/generate.ts +401 -0
  676. package/src/workspace/index.ts +7 -0
  677. package/src/workspace/module-imports-pipework.ts +39 -0
  678. package/src/workspace/module-package-name.ts +12 -0
  679. package/src/workspace/scrub-git-env.ts +23 -0
  680. package/src/workspace/source-paths.ts +36 -0
  681. package/src/workspace/sync.ts +182 -0
  682. package/dist/REFERENCE.md +0 -1477
  683. package/dist/infra/access-links/rate-limit.d.ts +0 -10
  684. package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
  685. package/dist/infra/access-links/rate-limit.js +0 -28
  686. package/dist/infra/access-links/rate-limit.js.map +0 -1
  687. package/dist/infra/email/sigv4.d.ts +0 -24
  688. package/dist/infra/email/sigv4.d.ts.map +0 -1
  689. package/dist/infra/email/sigv4.js +0 -55
  690. package/dist/infra/email/sigv4.js.map +0 -1
@@ -0,0 +1,778 @@
1
+ import { sql } from '../../data/db/sql.js'
2
+ import { pipeSystem } from '../../data/db/db.js'
3
+ import { fitting } from '../../request/di/index.js'
4
+ import { getContext } from '../../request/context/store.js'
5
+ import { getBaseLogger } from '../../core/logging/index.js'
6
+ import type { Queue } from '../../async/jobs/queue.js'
7
+ import type { Handler } from '../../request/di/types.js'
8
+ import { __flowStep } from '../../trace/runtime/flow-step.js'
9
+ import { auditAlert } from './audit.js'
10
+ import { ALERT_SEVERITIES, ALERT_STATUSES } from './schema.js'
11
+ import { encodeCursor, decodeCursor } from '../../request/fixture/paginate.js'
12
+ import { createOutbox } from '../outbox/core.js'
13
+ import type { AlertDelivery, AlertPayload, AlertSink } from './sinks.js'
14
+ import type { AlertSource } from './sources.js'
15
+ import { redactJson, redactText } from './redact.js'
16
+ import { recordBeat, ALERTS_TICK_LOOP, DEFAULT_LOOP_EVERY_MS } from './beat.js'
17
+ import { describeError } from '../../core/logging/error-chain.js'
18
+ import { ValidationError } from '../../core/errors/index.js'
19
+
20
+ export type AlertSeverity = 'info' | 'warning' | 'error' | 'critical'
21
+
22
+ export interface AlertSubject {
23
+ readonly tenantId?: string
24
+ readonly entity?: string
25
+ readonly entityId?: string
26
+ }
27
+
28
+ export interface AlertsConfig {
29
+ /** Where alerts are sent. At least one; names must be unique. */
30
+ readonly sinks: readonly AlertSink[]
31
+ /** The queue the alerts job runs on; its worker surface must run `handle.job`. */
32
+ readonly queue: Pick<Queue, 'enqueue'>
33
+ /** Job type of `handle.job`; default `'pipework.alerts'`. */
34
+ readonly jobType?: string
35
+ /** Send again while an alert stays open and is raised again after this long since the last send. Default `null`: never. */
36
+ readonly resendAfterMs?: number | null
37
+ /** Resolve an open alert nobody raised for this long, on the job's tick. Default `null`: only `resolve`. */
38
+ readonly resolveAfterQuietMs?: number | null
39
+ /** Delivery attempts before a row is given up on; default 10. */
40
+ readonly maxAttempts?: number
41
+ /** First retry delay, doubling per attempt up to one hour; default 5000. */
42
+ readonly backoffMs?: number
43
+ /** How long an occurrence is kept; the tick deletes older ones. Default 30 days. */
44
+ readonly occurrenceRetentionMs?: number
45
+ /**
46
+ * Send alerts of these severities in one periodic `digest` delivery per sink instead of one at a time. A raise or
47
+ * resolve of such an alert queues nothing (so `resendAfterMs` has no effect on it); each tick at least `everyMs`
48
+ * after the last digest sends one naming those that changed since (the first, those changed in the last `everyMs`). `severities` default `['warning']`;
49
+ * `error` and `critical` always go one at a time. Default: no digest.
50
+ */
51
+ readonly digest?: { readonly everyMs: number; readonly severities?: readonly AlertSeverity[] }
52
+ /** The environment label on every delivery; default the raising Flow's `config.environment`. */
53
+ readonly environment?: string
54
+ readonly sources?: readonly AlertSource[]
55
+ /** Database holding the alert tables; default the default database. */
56
+ readonly database?: string
57
+ /** How often you schedule the tick; used to tell when it has stopped. Default 60_000. */
58
+ readonly tickEveryMs?: number
59
+ /**
60
+ * Email addresses in an alert's message and in the strings of its context become `[email]` before the alert is
61
+ * stored, recorded or sent. Each pattern here is replaced by `[redacted]` too (matched globally).
62
+ */
63
+ readonly redact?: readonly RegExp[]
64
+ }
65
+
66
+ export type AlertStatus = 'open' | 'acknowledged' | 'resolved'
67
+ export type JsonValue = string | number | boolean | null | readonly JsonValue[] | { readonly [k: string]: JsonValue }
68
+
69
+ export interface RaiseOptions {
70
+ readonly severity: AlertSeverity
71
+ readonly message: string
72
+ readonly subject?: AlertSubject
73
+ /** Which team most likely fixes it. Latest raise wins. */
74
+ readonly owner?: string
75
+ /** One line: what is safe to do. Latest raise wins. */
76
+ readonly runbook?: string
77
+ /** Ids and counts, stored on the alert (latest raise wins) and on the occurrence. A JSON object. */
78
+ readonly context?: Readonly<Record<string, JsonValue>>
79
+ /** Overrides the handle's `resendAfterMs` for this alert; `null`: this alert is never re-sent. */
80
+ readonly resendAfterMs?: number | null
81
+ /** Overrides the handle's `resolveAfterQuietMs` for this alert; `null`: only `resolve` closes it. */
82
+ readonly resolveAfterQuietMs?: number | null
83
+ }
84
+
85
+ export interface OwnerChange { readonly from: string | null; readonly to: string; readonly by: string; readonly at: Date }
86
+
87
+ export interface Alert {
88
+ readonly key: string
89
+ readonly status: AlertStatus
90
+ readonly severity: AlertSeverity
91
+ readonly message: string
92
+ readonly subject: AlertSubject | null
93
+ readonly owner: string | null
94
+ /** Each change of owner made by acknowledge, oldest first. Kept across reopens. */
95
+ readonly ownerHistory: readonly OwnerChange[]
96
+ readonly runbook: string | null
97
+ readonly context: Readonly<Record<string, JsonValue>> | null
98
+ /** Raises since first seen, across reopens. */
99
+ readonly count: number
100
+ /** Times a raise came after a resolve. */
101
+ readonly reopenCount: number
102
+ readonly firstSeenAt: Date
103
+ readonly lastSeenAt: Date
104
+ readonly lastSentAt: Date | null
105
+ readonly acknowledgedAt: Date | null
106
+ readonly acknowledgedBy: string | null
107
+ readonly resolvedAt: Date | null
108
+ readonly resolvedBy: string | null
109
+ readonly resolveNote: string | null
110
+ /** The release the resolve names (e.g. the fix's version). */
111
+ readonly resolvedRelease: string | null
112
+ readonly reopenedAt: Date | null
113
+ readonly issueUrl: string | null
114
+ /** Latest ten traces that raised it, newest first. */
115
+ readonly traceIds: readonly string[]
116
+ }
117
+
118
+ export interface AlertOccurrence {
119
+ readonly id: string
120
+ readonly key: string
121
+ readonly at: Date
122
+ readonly severity: AlertSeverity
123
+ readonly message: string
124
+ readonly context: Readonly<Record<string, JsonValue>> | null
125
+ readonly traceId: string | null
126
+ }
127
+
128
+ export interface Page<T> { readonly items: T[]; readonly next: string | null }
129
+
130
+ export interface AlertTotals {
131
+ /** Every severity, by status; zero counts included for all three statuses and four severities. */
132
+ readonly bySeverity: Record<AlertSeverity, Record<AlertStatus, number>>
133
+ }
134
+
135
+ export interface DeliveryHealth {
136
+ /** Undelivered, not given up. */
137
+ readonly pending: number
138
+ /** Given up (failed_at set). */
139
+ readonly failed: number
140
+ readonly lastDeliveredAt: Date | null
141
+ readonly lastFailedAt: Date | null
142
+ readonly bySink: readonly {
143
+ readonly sink: string; readonly pending: number; readonly failed: number
144
+ readonly lastDeliveredAt: Date | null; readonly lastFailedAt: Date | null; readonly lastError: string | null
145
+ }[]
146
+ }
147
+
148
+ export interface ResolveOptions {
149
+ readonly note?: string
150
+ readonly release?: string
151
+ readonly by?: string
152
+ /** Queue a `resolve` delivery; default true. */
153
+ readonly notify?: boolean
154
+ }
155
+
156
+ export interface DeliveryRow {
157
+ readonly id: string
158
+ readonly key: string
159
+ readonly kind: AlertPayload['kind']
160
+ readonly sink: string
161
+ readonly payload: AlertPayload
162
+ readonly attempts: number
163
+ readonly nextAttemptAt: Date
164
+ readonly deliveredAt: Date | null
165
+ readonly failedAt: Date | null
166
+ readonly lastError: string | null
167
+ }
168
+
169
+ export interface TickResult {
170
+ /** Alerts resolved because nobody raised them for their quiet period, or because a source cleared them. */
171
+ readonly resolved: number
172
+ readonly delivered: number
173
+ /** Attempts that failed and will be retried. */
174
+ readonly retried: number
175
+ /** Deliveries given up on. */
176
+ readonly failed: number
177
+ }
178
+
179
+ export interface Alerts {
180
+ /**
181
+ * Raises the condition `key`. New: opens it and sends once. After a resolve: reopens it (count continues,
182
+ * `reopenCount` + 1) and sends `reopen`. Open: groups it (count, last seen, trace id) and sends nothing, unless
183
+ * `resendAfterMs` has passed since the last send. Acknowledged: groups, never sends. Every raise writes an
184
+ * occurrence. Needs a Flow.
185
+ */
186
+ raise(key: string, options: RaiseOptions): Promise<{ status: 'opened' | 'grouped' | 'resent' | 'reopened' }>
187
+ /** Resolves an open or acknowledged alert and sends `resolve` once unless `notify` is false. Otherwise: false, nothing sent. */
188
+ resolve(key: string, options?: ResolveOptions): Promise<boolean>
189
+ /**
190
+ * open -> acknowledged. Not open: false. No delivery. `owner` replaces the stored owner and appends to `ownerHistory`
191
+ * (nothing is appended when it is the current owner). An acknowledged alert given a different `owner` changes the owner
192
+ * only, keeps who acknowledged it and when, and returns true; without `owner`, or the same one, it returns false.
193
+ */
194
+ acknowledge(key: string, options: { by: string; note?: string; owner?: string }): Promise<boolean>
195
+ /** Sets issueUrl on an alert in any status. Unknown key: false. No delivery. Must be an http(s) URL. */
196
+ link(key: string, url: string): Promise<boolean>
197
+ /** Newest `lastSeenAt` first, then key. `limit` default 50, at most 200. */
198
+ list(options?: {
199
+ status?: AlertStatus | readonly AlertStatus[]; severity?: AlertSeverity | readonly AlertSeverity[]
200
+ /** The current owner is one of these. */
201
+ owner?: readonly string[]; limit?: number; cursor?: string
202
+ }): Promise<Page<Alert>>
203
+ /** One alert's raises, newest first. `limit` default 50, at most 200. */
204
+ occurrences(key: string, options?: { limit?: number; cursor?: string }): Promise<Page<AlertOccurrence>>
205
+ totals(): Promise<AlertTotals>
206
+ health(): Promise<DeliveryHealth>
207
+ /** The job to add to a worker surface's `jobs` (and schedule, e.g. every minute): auto-resolve, sources, deliveries. */
208
+ readonly job: Handler<Record<string, unknown>, unknown>
209
+ /** What one run of `job` does; callable from a test. Needs a Flow. */
210
+ tick(): Promise<TickResult>
211
+ open(): Promise<Alert[]>
212
+ get(key: string): Promise<Alert | null>
213
+ deliveries(options?: { key?: string }): Promise<DeliveryRow[]>
214
+ /**
215
+ * Puts the deliveries that were given up (and never delivered) back on the queue, optionally only those of `sink`
216
+ * and/or that failed at or after `failedSince`; the next `tick` sends them. Delivery ids stay the same, so a receiver
217
+ * that dedupes on `delivery.id` takes each once. Writes a `requeue` audit row per delivery naming `by` (who asked:
218
+ * an operator or key name, required).
219
+ */
220
+ requeueFailed(options: { by: string; sink?: string; failedSince?: Date }): Promise<{ requeued: number }>
221
+ }
222
+
223
+ const SEVERITIES = new Set<string>(ALERT_SEVERITIES)
224
+ const STATUSES = new Set<string>(ALERT_STATUSES)
225
+ const DEFAULT_RETENTION_MS = 30 * 24 * 3_600_000
226
+ const RETENTION_BATCH = 10_000
227
+ const DEFAULT_LIMIT = 50
228
+ const MAX_LIMIT = 200
229
+ /** A row's time as text, UTC to the microsecond; a JS Date holds only milliseconds. */
230
+ const exact = (column: string) => sql.raw(`to_char(${column} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`)
231
+
232
+ function toAlert(row: Record<string, unknown>): Alert {
233
+ const date = (v: unknown): Date => new Date(v as string)
234
+ const dateOrNull = (v: unknown): Date | null => (v === null || v === undefined ? null : date(v))
235
+ return {
236
+ key: row['key'] as string,
237
+ status: row['status'] as Alert['status'],
238
+ severity: row['severity'] as AlertSeverity,
239
+ message: row['message'] as string,
240
+ subject: (row['subject'] ?? null) as AlertSubject | null,
241
+ owner: (row['owner'] ?? null) as string | null,
242
+ ownerHistory: ((row['owner_history'] ?? []) as { from: string | null; to: string; by: string; at: string }[])
243
+ .map(h => ({ from: h.from, to: h.to, by: h.by, at: date(h.at) })),
244
+ runbook: (row['runbook'] ?? null) as string | null,
245
+ context: (row['context'] ?? null) as Alert['context'],
246
+ count: Number(row['count']),
247
+ reopenCount: Number(row['reopen_count']),
248
+ firstSeenAt: date(row['first_seen_at']),
249
+ lastSeenAt: date(row['last_seen_at']),
250
+ lastSentAt: dateOrNull(row['last_sent_at']),
251
+ acknowledgedAt: dateOrNull(row['acknowledged_at']),
252
+ acknowledgedBy: (row['acknowledged_by'] ?? null) as string | null,
253
+ resolvedAt: dateOrNull(row['resolved_at']),
254
+ resolvedBy: (row['resolved_by'] ?? null) as string | null,
255
+ resolveNote: (row['resolve_note'] ?? null) as string | null,
256
+ resolvedRelease: (row['resolved_release'] ?? null) as string | null,
257
+ reopenedAt: dateOrNull(row['reopened_at']),
258
+ issueUrl: (row['issue_url'] ?? null) as string | null,
259
+ traceIds: (row['trace_ids'] ?? []) as string[],
260
+ }
261
+ }
262
+
263
+ function toDeliveryRow(row: Record<string, unknown>): DeliveryRow {
264
+ return {
265
+ id: row['id'] as string,
266
+ key: row['alert_key'] as string,
267
+ kind: row['kind'] as DeliveryRow['kind'],
268
+ sink: row['sink'] as string,
269
+ payload: row['payload'] as AlertPayload,
270
+ attempts: Number(row['attempts']),
271
+ nextAttemptAt: new Date(row['next_attempt_at'] as string),
272
+ deliveredAt: row['delivered_at'] === null ? null : new Date(row['delivered_at'] as string),
273
+ failedAt: row['failed_at'] === null ? null : new Date(row['failed_at'] as string),
274
+ lastError: (row['last_error'] ?? null) as string | null,
275
+ }
276
+ }
277
+
278
+ function toOccurrence(row: Record<string, unknown>): AlertOccurrence {
279
+ return {
280
+ id: row['id'] as string,
281
+ key: row['alert_key'] as string,
282
+ at: new Date(row['at'] as string),
283
+ severity: row['severity'] as AlertSeverity,
284
+ message: row['message'] as string,
285
+ context: (row['context'] ?? null) as AlertOccurrence['context'],
286
+ traceId: (row['trace_id'] ?? null) as string | null,
287
+ }
288
+ }
289
+
290
+ function checkLimit(fn: string, limit: number | undefined): number {
291
+ if (limit === undefined) return DEFAULT_LIMIT
292
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIMIT) {
293
+ throw new Error(`[pipework] alerts.${fn}: limit must be a whole number from 1 to ${String(MAX_LIMIT)}, got ${String(limit)}.`)
294
+ }
295
+ return limit
296
+ }
297
+
298
+ /** Reads `[time, tiebreak]` from a cursor this module encoded; anything else is refused by name. */
299
+ function readCursor(fn: string, cursor: string): [string, string] {
300
+ const bad = (): Error => new Error(`[pipework] alerts.${fn}: cursor is not one this handle returned. Pass the \`next\` value unchanged.`)
301
+ let parts: unknown[]
302
+ try { parts = decodeCursor(cursor) } catch { throw bad() }
303
+ const [at, tie] = parts
304
+ if (parts.length !== 2 || typeof at !== 'string' || typeof tie !== 'string' || Number.isNaN(new Date(at).getTime())) throw bad()
305
+ return [at, tie]
306
+ }
307
+
308
+ function pickList<T extends string>(fn: string, name: string, value: T | readonly T[] | undefined, allowed: Set<string>): T[] | null {
309
+ if (value === undefined) return null
310
+ const list = typeof value === 'string' ? [value] : [...value]
311
+ for (const v of list) {
312
+ if (!allowed.has(v)) throw new Error(`[pipework] alerts.${fn}: ${name} must be one of ${[...allowed].join(', ')}, got "${String(v)}".`)
313
+ }
314
+ return list
315
+ }
316
+
317
+ function pgTextArray(values: readonly string[]): string {
318
+ return `{${values.map(v => `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`).join(',')}}`
319
+ }
320
+
321
+ export function createAlerts(config: AlertsConfig): Alerts {
322
+ if (config.sinks.length === 0) throw new Error('[pipework] alerts.create needs at least one sink.')
323
+ const sinks = new Map<string, AlertSink>()
324
+ for (const s of config.sinks) {
325
+ if (sinks.has(s.name)) throw new Error(`[pipework] alerts.create: two sinks are named "${s.name}"; give each its own name.`)
326
+ sinks.set(s.name, s)
327
+ }
328
+ const sinkNames = pgTextArray([...sinks.keys()])
329
+ const digestEveryMs = config.digest?.everyMs ?? null
330
+ if (config.digest !== undefined) {
331
+ if (!Number.isInteger(config.digest.everyMs) || config.digest.everyMs < 1000) {
332
+ throw new Error(`[pipework] alerts.create: digest.everyMs must be a whole number of at least 1000, got ${String(config.digest.everyMs)}.`)
333
+ }
334
+ for (const v of config.digest.severities ?? []) {
335
+ if (v === 'error' || v === 'critical') throw new Error(`[pipework] alerts.create: digest.severities cannot include "${v}"; errors and critical go one at a time.`)
336
+ if (!SEVERITIES.has(v)) throw new Error(`[pipework] alerts.create: digest.severities must be from info, warning, got "${String(v)}".`)
337
+ }
338
+ if (config.digest.severities?.length === 0) throw new Error('[pipework] alerts.create: digest.severities needs at least one severity, or leave it out for warning.')
339
+ }
340
+ const digestSeverities = pgTextArray(config.digest === undefined ? [] : [...new Set(config.digest.severities ?? ['warning'])])
341
+ const jobType = config.jobType ?? 'pipework.alerts'
342
+ const maxAttempts = config.maxAttempts ?? 10
343
+ const backoffMs = config.backoffMs ?? 5000
344
+ const defaultResend = config.resendAfterMs ?? null
345
+ const defaultQuiet = config.resolveAfterQuietMs ?? null
346
+ const sources = config.sources ?? []
347
+ const database = config.database
348
+ const redactExtra = config.redact ?? []
349
+ const log = getBaseLogger()
350
+
351
+ const db = () => pipeSystem(database)
352
+
353
+ const retentionMs = config.occurrenceRetentionMs ?? DEFAULT_RETENTION_MS
354
+ const environmentOf = (): string => config.environment ?? getContext()?.instance.config.environment ?? 'unknown'
355
+
356
+ // `null` is stored as -1 ("never"); undefined as NULL ("the handle's default").
357
+ function storedMs(name: string, value: number | null | undefined): number | null {
358
+ if (value === undefined) return null
359
+ if (value === null) return -1
360
+ if (!Number.isInteger(value) || value < 0) throw new Error(`[pipework] alerts.raise: ${name} must be a non-negative integer or null, got ${String(value)}.`)
361
+ return value
362
+ }
363
+
364
+ // The alert and its deliveries move in one statement, so a crash cannot leave one without the other.
365
+ const payloadSql = (kind: ReturnType<typeof sql>, environment: string) => sql`jsonb_build_object(
366
+ 'kind', ${kind}, 'key', up.key, 'severity', up.severity, 'message', up.message, 'subject', up.subject,
367
+ 'owner', up.owner, 'runbook', up.runbook, 'context', up.context,
368
+ 'count', up.count, 'reopenCount', up.reopen_count, 'firstSeenAt', up.first_seen_at, 'lastSeenAt', up.last_seen_at,
369
+ 'resolvedAt', up.resolved_at, 'acknowledgedAt', up.acknowledged_at, 'acknowledgedBy', up.acknowledged_by,
370
+ 'resolvedBy', up.resolved_by, 'resolveNote', up.resolve_note, 'resolvedRelease', up.resolved_release,
371
+ 'issueUrl', up.issue_url, 'environment', ${environment}::text, 'traceIds', to_jsonb(up.trace_ids))`
372
+
373
+ async function enqueueJob(): Promise<void> {
374
+ await config.queue.enqueue(db(), { type: jobType, payload: {}, continueFlow: true })
375
+ }
376
+
377
+ // Inside a tick the deliveries are sent by that same tick, so it does not enqueue another.
378
+ async function raise(key: string, options: RaiseOptions, enqueue = true): Promise<{ status: 'opened' | 'grouped' | 'resent' | 'reopened' }> {
379
+ if (key.trim() === '') throw new Error('[pipework] alerts.raise needs a non-empty key.')
380
+ if (!SEVERITIES.has(options.severity)) throw new Error(`[pipework] alerts.raise: severity must be info, warning, error or critical, got "${String(options.severity)}".`)
381
+ const resendStored = storedMs('resendAfterMs', options.resendAfterMs)
382
+ const quietStored = storedMs('resolveAfterQuietMs', options.resolveAfterQuietMs)
383
+ const traceId = getContext()?.trace?.id ?? null
384
+ const traceArray = traceId === null ? '{}' : pgTextArray([traceId])
385
+ const subject = options.subject === undefined ? null : JSON.stringify(options.subject)
386
+ const context = options.context === undefined ? null : JSON.stringify(redactJson(options.context, redactExtra))
387
+ const message = redactText(options.message, redactExtra)
388
+ const environment = environmentOf()
389
+ // `last_seen_at` never moves back. A raise sends exactly when this statement sets `last_sent_at`, and then sets
390
+ // `last_seen_at` to the same value; a raise that does not send never leaves them equal, so RETURNING can tell the two apart.
391
+ const seenSql = sql`GREATEST(a.last_seen_at, EXCLUDED.last_seen_at)`
392
+ const sendsSql = sql`(a.status <> 'acknowledged' AND (a.status = 'resolved' OR a.last_sent_at IS NULL
393
+ OR (NULLIF(COALESCE(EXCLUDED.resend_after_ms, a.resend_after_ms, ${defaultResend}::int), -1) IS NOT NULL
394
+ AND EXCLUDED.last_sent_at - a.last_sent_at
395
+ >= NULLIF(COALESCE(EXCLUDED.resend_after_ms, a.resend_after_ms, ${defaultResend}::int), -1) * interval '1 millisecond')))`
396
+ const kindSql = sql`CASE WHEN up.inserted THEN 'raise' WHEN up.reopened THEN 'reopen' ELSE 'repeat' END`
397
+ const rows = await db().execute(sql`
398
+ WITH up AS (
399
+ INSERT INTO pipework_alerts AS a
400
+ (key, status, severity, message, subject, owner, runbook, context, count, first_seen_at, last_seen_at, last_sent_at,
401
+ trace_ids, resend_after_ms, resolve_after_quiet_ms, updated_at)
402
+ SELECT ${key}, 'open', ${options.severity}, ${message}, ${subject}::jsonb,
403
+ ${options.owner ?? null}::text, ${options.runbook ?? null}::text, ${context}::jsonb, 1, c.ts, c.ts, c.ts,
404
+ ${traceArray}::text[], ${resendStored}::int, ${quietStored}::int, c.ts
405
+ FROM (SELECT clock_timestamp() AS ts) c
406
+ ON CONFLICT (key) DO UPDATE SET
407
+ status = CASE WHEN a.status = 'acknowledged' THEN 'acknowledged' ELSE 'open' END,
408
+ severity = EXCLUDED.severity,
409
+ message = EXCLUDED.message,
410
+ subject = EXCLUDED.subject,
411
+ owner = CASE WHEN jsonb_array_length(a.owner_history) > 0 THEN a.owner ELSE EXCLUDED.owner END,
412
+ runbook = EXCLUDED.runbook,
413
+ context = EXCLUDED.context,
414
+ count = a.count + 1,
415
+ reopen_count = CASE WHEN a.status = 'resolved' THEN a.reopen_count + 1 ELSE a.reopen_count END,
416
+ reopened_at = CASE WHEN a.status = 'resolved' THEN ${seenSql} ELSE a.reopened_at END,
417
+ acknowledged_at = CASE WHEN a.status = 'resolved' THEN NULL ELSE a.acknowledged_at END,
418
+ acknowledged_by = CASE WHEN a.status = 'resolved' THEN NULL ELSE a.acknowledged_by END,
419
+ last_seen_at = CASE WHEN ${sendsSql} OR ${seenSql} IS DISTINCT FROM a.last_sent_at THEN ${seenSql}
420
+ ELSE ${seenSql} + interval '1 microsecond' END,
421
+ resolved_at = NULL,
422
+ trace_ids = CASE
423
+ WHEN a.status = 'resolved' THEN EXCLUDED.trace_ids
424
+ WHEN ${traceId}::text IS NULL THEN a.trace_ids
425
+ ELSE (EXCLUDED.trace_ids || array_remove(a.trace_ids, ${traceId}::text))[1:10] END,
426
+ resend_after_ms = COALESCE(EXCLUDED.resend_after_ms, CASE WHEN a.status = 'resolved' THEN NULL ELSE a.resend_after_ms END),
427
+ resolve_after_quiet_ms = COALESCE(EXCLUDED.resolve_after_quiet_ms, CASE WHEN a.status = 'resolved' THEN NULL ELSE a.resolve_after_quiet_ms END),
428
+ last_sent_at = CASE WHEN ${sendsSql} THEN ${seenSql} ELSE a.last_sent_at END,
429
+ updated_at = EXCLUDED.updated_at
430
+ RETURNING a.*, (a.last_sent_at = a.last_seen_at) AS sent, (xmax = 0) AS inserted,
431
+ (a.reopened_at IS NOT NULL AND a.reopened_at = a.last_seen_at) AS reopened
432
+ ),
433
+ occ AS (
434
+ INSERT INTO pipework_alert_occurrences (alert_key, at, severity, message, context, trace_id)
435
+ SELECT up.key, up.last_seen_at, up.severity, up.message, up.context, ${traceId}::text FROM up
436
+ ),
437
+ dels AS (
438
+ INSERT INTO pipework_alert_deliveries (alert_key, kind, sink, payload, created_at, next_attempt_at)
439
+ SELECT up.key, ${kindSql}, s.sink,
440
+ ${payloadSql(kindSql, environment)}, clock_timestamp(), clock_timestamp()
441
+ FROM up CROSS JOIN unnest(${sinkNames}::text[]) AS s(sink)
442
+ WHERE up.sent AND NOT up.severity = ANY(${digestSeverities}::text[])
443
+ RETURNING id
444
+ )
445
+ SELECT up.*, (SELECT count(*) FROM dels)::int AS queued FROM up`)
446
+ const row = rows[0]!
447
+ const alert = toAlert(row)
448
+ const sent = row['sent'] === true
449
+ const status = !sent ? 'grouped' : row['inserted'] === true ? 'opened' : row['reopened'] === true ? 'reopened' : 'resent'
450
+ await auditAlert(status === 'opened' ? 'raise' : status === 'reopened' ? 'reopen' : 'group', key, {
451
+ status, severity: alert.severity, count: alert.count, message: alert.message,
452
+ }, database)
453
+ if (sent && enqueue && Number(row['queued']) > 0) await enqueueJob()
454
+ return { status }
455
+ }
456
+
457
+ async function resolveKey(key: string, onlyIfQuiet: boolean, enqueue = true, options: ResolveOptions = {}): Promise<boolean> {
458
+ const notify = options.notify ?? true
459
+ const environment = environmentOf()
460
+ const rows = await db().execute(sql`
461
+ WITH up AS (
462
+ UPDATE pipework_alerts SET status = 'resolved', resolved_at = clock_timestamp(), updated_at = clock_timestamp(),
463
+ resolved_by = ${onlyIfQuiet ? sql`'quiet'` : sql`${options.by ?? null}::text`},
464
+ resolve_note = ${onlyIfQuiet ? sql`'quiet for ' || COALESCE(resolve_after_quiet_ms, ${defaultQuiet}::int) || ' ms'` : sql`${options.note ?? null}::text`},
465
+ resolved_release = ${options.release ?? null}::text
466
+ WHERE key = ${key} AND status IN ('open', 'acknowledged')
467
+ AND (NOT ${onlyIfQuiet} OR (resolve_after_quiet_ms IS DISTINCT FROM -1 AND last_seen_at < clock_timestamp()
468
+ - COALESCE(resolve_after_quiet_ms, ${defaultQuiet}::int) * interval '1 millisecond'))
469
+ RETURNING *
470
+ ),
471
+ dels AS (
472
+ INSERT INTO pipework_alert_deliveries (alert_key, kind, sink, payload, created_at, next_attempt_at)
473
+ SELECT up.key, 'resolve', s.sink, ${payloadSql(sql`'resolve'`, environment)}, clock_timestamp(), clock_timestamp()
474
+ FROM up CROSS JOIN unnest(${sinkNames}::text[]) AS s(sink)
475
+ WHERE ${notify}::boolean AND NOT up.severity = ANY(${digestSeverities}::text[])
476
+ RETURNING id
477
+ )
478
+ SELECT up.*, (SELECT count(*) FROM dels)::int AS queued FROM up`)
479
+ if (rows.length === 0) return false
480
+ const alert = toAlert(rows[0]!)
481
+ await auditAlert('resolve', key, { count: alert.count, auto: onlyIfQuiet, ...(onlyIfQuiet ? { by: 'quiet' } : options.by !== undefined ? { by: options.by } : {}) }, database)
482
+ if (enqueue && notify && Number(rows[0]!['queued']) > 0) await enqueueJob()
483
+ return true
484
+ }
485
+
486
+ async function acknowledge(key: string, options: { by: string; note?: string; owner?: string }): Promise<boolean> {
487
+ if (options.by.trim() === '') throw new Error('[pipework] alerts.acknowledge needs a non-empty by.')
488
+ if (options.owner !== undefined && options.owner.trim() === '') throw new Error('[pipework] alerts.acknowledge needs a non-empty owner when one is given.')
489
+ const owner = options.owner?.trim() ?? null
490
+ const rows = await db().execute(sql`
491
+ UPDATE pipework_alerts SET
492
+ status = 'acknowledged',
493
+ acknowledged_at = CASE WHEN status = 'open' THEN clock_timestamp() ELSE acknowledged_at END,
494
+ acknowledged_by = CASE WHEN status = 'open' THEN ${options.by} ELSE acknowledged_by END,
495
+ owner = COALESCE(${owner}::text, owner),
496
+ owner_history = CASE WHEN ${owner}::text IS NOT NULL AND ${owner}::text IS DISTINCT FROM owner
497
+ THEN owner_history || jsonb_build_array(jsonb_build_object('from', owner, 'to', ${owner}::text, 'by', ${options.by}::text,
498
+ 'at', to_char(clock_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')))
499
+ ELSE owner_history END,
500
+ updated_at = clock_timestamp()
501
+ WHERE key = ${key}
502
+ AND (status = 'open' OR (status = 'acknowledged' AND ${owner}::text IS NOT NULL AND ${owner}::text IS DISTINCT FROM owner))
503
+ RETURNING key`)
504
+ if (rows.length === 0) return false
505
+ await auditAlert('acknowledge', key, {
506
+ by: options.by, ...(options.note !== undefined ? { note: options.note } : {}), ...(owner !== null ? { owner } : {}),
507
+ }, database)
508
+ return true
509
+ }
510
+
511
+ async function link(key: string, url: string): Promise<boolean> {
512
+ let parsed: URL
513
+ try { parsed = new URL(url) } catch { throw new Error(`[pipework] alerts.link: url must be an http or https URL, got "${url}".`) }
514
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
515
+ throw new Error(`[pipework] alerts.link: url must be an http or https URL, got "${url}".`)
516
+ }
517
+ const rows = await db().execute(sql`UPDATE pipework_alerts SET issue_url = ${url}, updated_at = clock_timestamp() WHERE key = ${key} RETURNING key`)
518
+ if (rows.length === 0) return false
519
+ await auditAlert('link', key, { url }, database)
520
+ return true
521
+ }
522
+
523
+ async function list(options: Parameters<Alerts['list']>[0] = {}): Promise<Page<Alert>> {
524
+ const statuses = pickList('list', 'status', options.status, STATUSES)
525
+ const severities = pickList('list', 'severity', options.severity, SEVERITIES)
526
+ const owners = options.owner ?? null
527
+ const limit = checkLimit('list', options.limit)
528
+ const cursor = options.cursor === undefined ? null : readCursor('list', options.cursor)
529
+ const rows = await db().execute(sql`
530
+ SELECT *, ${exact('last_seen_at')} AS exact_at FROM pipework_alerts
531
+ WHERE (${statuses === null}::boolean OR status = ANY(${pgTextArray(statuses ?? [])}::text[]))
532
+ AND (${severities === null}::boolean OR severity = ANY(${pgTextArray(severities ?? [])}::text[]))
533
+ AND (${owners === null}::boolean OR owner = ANY(${pgTextArray([...(owners ?? [])])}::text[]))
534
+ AND (${cursor === null}::boolean OR last_seen_at < ${cursor?.[0] ?? null}::timestamptz
535
+ OR (last_seen_at = ${cursor?.[0] ?? null}::timestamptz AND key > ${cursor?.[1] ?? null}::text))
536
+ ORDER BY last_seen_at DESC, key ASC
537
+ LIMIT ${limit + 1}`)
538
+ const page = rows.slice(0, limit)
539
+ const last = page[page.length - 1]
540
+ return {
541
+ items: page.map(toAlert),
542
+ next: rows.length > limit && last !== undefined ? encodeCursor([last['exact_at'], last['key']]) : null,
543
+ }
544
+ }
545
+
546
+ async function occurrences(key: string, options: { limit?: number; cursor?: string } = {}): Promise<Page<AlertOccurrence>> {
547
+ const limit = checkLimit('occurrences', options.limit)
548
+ const cursor = options.cursor === undefined ? null : readCursor('occurrences', options.cursor)
549
+ const rows = await db().execute(sql`
550
+ SELECT *, ${exact('at')} AS exact_at FROM pipework_alert_occurrences
551
+ WHERE alert_key = ${key}
552
+ AND (${cursor === null}::boolean OR at < ${cursor?.[0] ?? null}::timestamptz
553
+ OR (at = ${cursor?.[0] ?? null}::timestamptz AND id < ${cursor?.[1] ?? null}::uuid))
554
+ ORDER BY at DESC, id DESC
555
+ LIMIT ${limit + 1}`)
556
+ const page = rows.slice(0, limit)
557
+ const last = page[page.length - 1]
558
+ return {
559
+ items: page.map(toOccurrence),
560
+ next: rows.length > limit && last !== undefined ? encodeCursor([last['exact_at'], last['id']]) : null,
561
+ }
562
+ }
563
+
564
+ async function totals(): Promise<AlertTotals> {
565
+ const rows = await db().execute(sql`SELECT severity, status, count(*)::int AS n FROM pipework_alerts GROUP BY severity, status`)
566
+ const bySeverity = {} as Record<AlertSeverity, Record<AlertStatus, number>>
567
+ for (const sev of ALERT_SEVERITIES) bySeverity[sev] = { open: 0, acknowledged: 0, resolved: 0 }
568
+ for (const row of rows) bySeverity[row['severity'] as AlertSeverity][row['status'] as AlertStatus] = Number(row['n'])
569
+ return { bySeverity }
570
+ }
571
+
572
+ async function health(): Promise<DeliveryHealth> {
573
+ const rows = await db().execute(sql`
574
+ SELECT sink,
575
+ count(*) FILTER (WHERE delivered_at IS NULL AND failed_at IS NULL)::int AS pending,
576
+ count(*) FILTER (WHERE failed_at IS NOT NULL)::int AS failed,
577
+ max(delivered_at) AS last_delivered_at,
578
+ max(failed_at) AS last_failed_at,
579
+ (array_agg(last_error ORDER BY COALESCE(failed_at, next_attempt_at) DESC) FILTER (WHERE last_error IS NOT NULL))[1] AS last_error
580
+ FROM pipework_alert_deliveries GROUP BY sink`)
581
+ const toDate = (v: unknown): Date | null => (v === null || v === undefined ? null : new Date(v as string))
582
+ const bySink = new Map<string, DeliveryHealth['bySink'][number]>()
583
+ for (const name of sinks.keys()) {
584
+ bySink.set(name, { sink: name, pending: 0, failed: 0, lastDeliveredAt: null, lastFailedAt: null, lastError: null })
585
+ }
586
+ let pending = 0
587
+ let failed = 0
588
+ let lastDeliveredAt: Date | null = null
589
+ let lastFailedAt: Date | null = null
590
+ const later = (a: Date | null, b: Date | null): Date | null => (a === null ? b : b === null ? a : a > b ? a : b)
591
+ for (const row of rows) {
592
+ const entry = {
593
+ sink: row['sink'] as string, pending: Number(row['pending']), failed: Number(row['failed']),
594
+ lastDeliveredAt: toDate(row['last_delivered_at']), lastFailedAt: toDate(row['last_failed_at']),
595
+ lastError: (row['last_error'] ?? null) as string | null,
596
+ }
597
+ bySink.set(entry.sink, entry)
598
+ pending += entry.pending
599
+ failed += entry.failed
600
+ lastDeliveredAt = later(lastDeliveredAt, entry.lastDeliveredAt)
601
+ lastFailedAt = later(lastFailedAt, entry.lastFailedAt)
602
+ }
603
+ return { pending, failed, lastDeliveredAt, lastFailedAt, bySink: [...bySink.values()].sort((x, y) => x.sink.localeCompare(y.sink)) }
604
+ }
605
+
606
+ async function pruneOccurrences(): Promise<void> {
607
+ await db().execute(sql`
608
+ DELETE FROM pipework_alert_occurrences WHERE id IN (
609
+ SELECT id FROM pipework_alert_occurrences
610
+ WHERE at < clock_timestamp() - ${retentionMs}::bigint * interval '1 millisecond' LIMIT ${RETENTION_BATCH})`)
611
+ await db().execute(sql`
612
+ DELETE FROM pipework_faults WHERE id IN (
613
+ SELECT id FROM pipework_faults
614
+ WHERE recorded_at < clock_timestamp() - ${retentionMs}::bigint * interval '1 millisecond' LIMIT ${RETENTION_BATCH})`)
615
+ }
616
+
617
+ async function autoResolve(): Promise<number> {
618
+ if (defaultQuiet === null) {
619
+ const any = await db().execute(sql`SELECT 1 FROM pipework_alerts WHERE status IN ('open', 'acknowledged') AND resolve_after_quiet_ms IS NOT NULL AND resolve_after_quiet_ms <> -1 LIMIT 1`)
620
+ if (any.length === 0) return 0
621
+ }
622
+ const due = await db().execute(sql`
623
+ SELECT key FROM pipework_alerts
624
+ WHERE status IN ('open', 'acknowledged')
625
+ AND resolve_after_quiet_ms IS DISTINCT FROM -1
626
+ AND last_seen_at < clock_timestamp() - COALESCE(resolve_after_quiet_ms, ${defaultQuiet}::int) * interval '1 millisecond'`)
627
+ let resolved = 0
628
+ for (const row of due) if (await resolveKey(row['key'] as string, true, false)) resolved++
629
+ return resolved
630
+ }
631
+
632
+ async function runSources(): Promise<number> {
633
+ let resolved = 0
634
+ for (const source of sources) {
635
+ let conditions
636
+ try {
637
+ conditions = await source.evaluate()
638
+ } catch (err) {
639
+ log.error({ err, source: source.name }, 'alert source failed')
640
+ try {
641
+ await raise(`alert-source-failed:${source.name}`, {
642
+ severity: 'error',
643
+ message: describeError(err).message,
644
+ }, false)
645
+ } catch (raiseErr) {
646
+ log.error({ err: raiseErr, source: source.name }, 'alert source failure could not be raised')
647
+ }
648
+ continue
649
+ }
650
+ if (await resolveKey(`alert-source-failed:${source.name}`, false, false)) resolved++
651
+ for (const c of conditions) {
652
+ await raise(c.key, {
653
+ severity: c.severity,
654
+ message: c.message,
655
+ ...(c.subject !== undefined ? { subject: c.subject } : {}),
656
+ ...(c.owner !== undefined ? { owner: c.owner } : {}),
657
+ ...(c.runbook !== undefined ? { runbook: c.runbook } : {}),
658
+ ...(c.context !== undefined ? { context: c.context } : {}),
659
+ }, false)
660
+ }
661
+ if (source.owns === undefined) continue
662
+ const live = new Set(conditions.map(c => c.key))
663
+ const opened = await db().execute(sql`SELECT key FROM pipework_alerts WHERE status IN ('open', 'acknowledged')`)
664
+ for (const row of opened) {
665
+ const key = row['key'] as string
666
+ if (source.owns(key) && !live.has(key) && await resolveKey(key, false, false)) resolved++
667
+ }
668
+ }
669
+ return resolved
670
+ }
671
+
672
+ const DIGEST_KEY = 'pipework.alerts.digest'
673
+
674
+ // One statement claims the window (only the handle that moves the cursor wins) and queues the digest.
675
+ async function sendDigest(): Promise<void> {
676
+ if (digestEveryMs === null) return
677
+ const environment = environmentOf()
678
+ const rows = await db().execute(sql`
679
+ WITH old AS (SELECT cursor_at FROM pipework_alert_source_state WHERE name = ${DIGEST_KEY}),
680
+ claim AS (
681
+ INSERT INTO pipework_alert_source_state AS s (name, cursor_at) VALUES (${DIGEST_KEY}, statement_timestamp())
682
+ ON CONFLICT (name) DO UPDATE SET cursor_at = EXCLUDED.cursor_at
683
+ WHERE s.cursor_at <= clock_timestamp() - ${digestEveryMs}::bigint * interval '1 millisecond'
684
+ RETURNING cursor_at AS until
685
+ ),
686
+ changed AS (
687
+ SELECT a.* FROM pipework_alerts a, claim
688
+ WHERE a.severity = ANY(${digestSeverities}::text[])
689
+ AND a.updated_at > COALESCE((SELECT cursor_at FROM old), statement_timestamp() - ${digestEveryMs}::bigint * interval '1 millisecond')
690
+ ),
691
+ dels AS (
692
+ INSERT INTO pipework_alert_deliveries (alert_key, kind, sink, payload, created_at, next_attempt_at)
693
+ SELECT ${DIGEST_KEY}, 'digest', s.sink,
694
+ jsonb_build_object('kind', 'digest', 'key', ${DIGEST_KEY}::text, 'environment', ${environment}::text,
695
+ 'since', (SELECT cursor_at FROM old), 'until', claim.until,
696
+ 'alerts', (SELECT jsonb_agg(jsonb_build_object('key', c.key, 'severity', c.severity, 'status', c.status,
697
+ 'message', c.message, 'owner', c.owner, 'count', c.count, 'reopenCount', c.reopen_count,
698
+ 'lastSeenAt', c.last_seen_at) ORDER BY c.key) FROM changed c)),
699
+ clock_timestamp(), clock_timestamp()
700
+ FROM claim CROSS JOIN unnest(${sinkNames}::text[]) AS s(sink)
701
+ WHERE EXISTS (SELECT 1 FROM changed)
702
+ RETURNING id
703
+ )
704
+ SELECT (SELECT count(*) FROM claim)::int AS claimed, (SELECT count(*) FROM dels)::int AS queued,
705
+ (SELECT coalesce(array_agg(key ORDER BY key), '{}') FROM changed) AS keys`)
706
+ const row = rows[0]!
707
+ if (Number(row['queued']) > 0) await auditAlert('digest', DIGEST_KEY, { keys: row['keys'] }, database)
708
+ }
709
+
710
+ const outbox = createOutbox<AlertDelivery>({
711
+ table: 'pipework_alert_deliveries', groupColumn: 'alert_key', database, sinks, maxAttempts, backoffMs,
712
+ label: 'alert delivery given up',
713
+ toDelivery: (row, attempt) => ({
714
+ id: row['id'] as string, kind: row['kind'] as AlertDelivery['kind'], key: row['alert_key'] as string,
715
+ sink: row['sink'] as string, payload: row['payload'] as AlertPayload, attempt,
716
+ }),
717
+ onDelivered: (row, attempt) => auditAlert('deliver', row['alert_key'] as string, { sink: row['sink'], kind: row['kind'], attempt }, database),
718
+ onGivenUp: (row, attempt, error) => auditAlert('deliver-failed', row['alert_key'] as string, { sink: row['sink'], kind: row['kind'], attempt, error }, database),
719
+ })
720
+
721
+ async function runTick(): Promise<TickResult> {
722
+ let resolved = await autoResolve()
723
+ resolved += await runSources()
724
+ await pruneOccurrences()
725
+ await sendDigest()
726
+ return { resolved, ...await outbox.drain() }
727
+ }
728
+
729
+ const flowTick = __flowStep({ ref: 'pipework/alerts:tick' }, runTick)
730
+ const tickEveryMs = config.tickEveryMs ?? DEFAULT_LOOP_EVERY_MS
731
+ const tickStep = async (): Promise<TickResult> => {
732
+ try {
733
+ const result = await flowTick()
734
+ await recordBeat(db, ALERTS_TICK_LOOP, { ok: true, everyMs: tickEveryMs })
735
+ return result
736
+ } catch (err) {
737
+ await recordBeat(db, ALERTS_TICK_LOOP, { ok: false, error: describeError(err).message, everyMs: tickEveryMs })
738
+ throw err
739
+ }
740
+ }
741
+ const job = (database !== undefined ? fitting.use(database) : fitting.use()).job(jobType).fit(async () => tickStep())
742
+
743
+ return {
744
+ raise: (key, options) => raise(key, options),
745
+ resolve: (key, options) => resolveKey(key, false, true, options),
746
+ acknowledge,
747
+ link,
748
+ list,
749
+ occurrences,
750
+ totals,
751
+ health,
752
+ job: job as unknown as Handler<Record<string, unknown>, unknown>,
753
+ tick: () => tickStep(),
754
+ async requeueFailed(options) {
755
+ if (typeof options.by !== 'string' || options.by.trim() === '') throw new ValidationError({ by: ['must be a non-empty string'] }, 'alerts.requeueFailed needs by, who is requeueing.')
756
+ const by = options.by.trim()
757
+ const requeued = await outbox.requeue({ ...(options.sink !== undefined ? { sink: options.sink } : {}), ...(options.failedSince !== undefined ? { failedSince: options.failedSince } : {}) })
758
+ for (const row of requeued) {
759
+ await auditAlert('requeue', row['alert_key'] as string, { sink: row['sink'], kind: row['kind'], by, attempts: Number(row['prev_attempts']), error: row['last_error'] ?? null }, database)
760
+ }
761
+ return { requeued: requeued.length }
762
+ },
763
+ async open() {
764
+ const rows = await db().execute(sql`SELECT * FROM pipework_alerts WHERE status = 'open' ORDER BY first_seen_at, key`)
765
+ return rows.map(toAlert)
766
+ },
767
+ async get(key) {
768
+ const rows = await db().execute(sql`SELECT * FROM pipework_alerts WHERE key = ${key}`)
769
+ return rows[0] === undefined ? null : toAlert(rows[0])
770
+ },
771
+ async deliveries(options = {}) {
772
+ const rows = options.key === undefined
773
+ ? await db().execute(sql`SELECT * FROM pipework_alert_deliveries ORDER BY created_at, id`)
774
+ : await db().execute(sql`SELECT * FROM pipework_alert_deliveries WHERE alert_key = ${options.key} ORDER BY created_at, id`)
775
+ return rows.map(toDeliveryRow)
776
+ },
777
+ }
778
+ }