pipework 0.252.0 → 0.253.0

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 (695) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/infra/support/handle.d.ts +8 -1
  3. package/dist/infra/support/handle.d.ts.map +1 -1
  4. package/dist/infra/support/handle.js +9 -2
  5. package/dist/infra/support/handle.js.map +1 -1
  6. package/dist/infra/support/namespace.d.ts.map +1 -1
  7. package/package.json +4 -3
  8. package/src/async/jobs/abortable-delay.ts +18 -0
  9. package/src/async/jobs/claims.ts +52 -0
  10. package/src/async/jobs/cron.ts +179 -0
  11. package/src/async/jobs/errors.ts +56 -0
  12. package/src/async/jobs/exclusion.ts +117 -0
  13. package/src/async/jobs/execute.ts +219 -0
  14. package/src/async/jobs/index.ts +10 -0
  15. package/src/async/jobs/listener.ts +135 -0
  16. package/src/async/jobs/namespace.ts +20 -0
  17. package/src/async/jobs/queue-door.ts +44 -0
  18. package/src/async/jobs/queue.ts +1012 -0
  19. package/src/async/jobs/run-script.ts +67 -0
  20. package/src/async/jobs/scheduler.ts +218 -0
  21. package/src/async/jobs/schedules-table.ts +37 -0
  22. package/src/async/jobs/serialize-error.ts +48 -0
  23. package/src/async/jobs/table.ts +191 -0
  24. package/src/async/pipeline/define.ts +53 -0
  25. package/src/async/pipeline/errors.ts +35 -0
  26. package/src/async/pipeline/execute.ts +53 -0
  27. package/src/async/pipeline/index.ts +13 -0
  28. package/src/async/pipeline/namespace.ts +12 -0
  29. package/src/async/pipeline/resume.ts +67 -0
  30. package/src/async/pipeline/run-steps.ts +98 -0
  31. package/src/async/pipeline/types.ts +57 -0
  32. package/src/async/state-machine/define.ts +64 -0
  33. package/src/async/state-machine/errors.ts +29 -0
  34. package/src/async/state-machine/index.ts +3 -0
  35. package/src/async/state-machine/namespace.ts +12 -0
  36. package/src/async/state-machine/transition.ts +90 -0
  37. package/src/async/state-machine/types.ts +37 -0
  38. package/src/async/steps/errors.ts +31 -0
  39. package/src/async/steps/graph.ts +1214 -0
  40. package/src/async/steps/hash.ts +40 -0
  41. package/src/async/steps/index.ts +7 -0
  42. package/src/async/steps/namespace.ts +7 -0
  43. package/src/async/steps/table.ts +97 -0
  44. package/src/async/steps/types.ts +318 -0
  45. package/src/audit/chain/index.ts +9 -0
  46. package/src/audit/chain/install.ts +113 -0
  47. package/src/audit/chain/retire.ts +52 -0
  48. package/src/audit/chain/verify.ts +257 -0
  49. package/src/audit/query/cursor.ts +37 -0
  50. package/src/audit/query/index.ts +9 -0
  51. package/src/audit/query/query.ts +223 -0
  52. package/src/audit/schema/audit-record-fields.ts +68 -0
  53. package/src/audit/schema/audit-record.ts +23 -0
  54. package/src/auth/auth/audit.ts +31 -0
  55. package/src/auth/auth/chain.ts +84 -0
  56. package/src/auth/auth/cookie.ts +136 -0
  57. package/src/auth/auth/email-links.ts +311 -0
  58. package/src/auth/auth/guard.ts +128 -0
  59. package/src/auth/auth/index.ts +18 -0
  60. package/src/auth/auth/multi-org.ts +337 -0
  61. package/src/auth/auth/namespace.ts +34 -0
  62. package/src/auth/auth/parse-expiry.ts +13 -0
  63. package/src/auth/auth/passwords.ts +110 -0
  64. package/src/auth/auth/schema.ts +110 -0
  65. package/src/auth/auth/sessions.ts +565 -0
  66. package/src/auth/auth/sign-in.ts +156 -0
  67. package/src/auth/auth/signing-keys.ts +69 -0
  68. package/src/auth/auth/types.ts +26 -0
  69. package/src/auth/auth/uuid.ts +10 -0
  70. package/src/auth/rbac/cache.ts +55 -0
  71. package/src/auth/rbac/check.ts +30 -0
  72. package/src/auth/rbac/create.ts +35 -0
  73. package/src/auth/rbac/index.ts +5 -0
  74. package/src/auth/rbac/middleware.ts +20 -0
  75. package/src/auth/rbac/namespace.ts +15 -0
  76. package/src/auth/rbac/resolve.ts +31 -0
  77. package/src/auth/rbac/schema.sql +31 -0
  78. package/src/auth/rbac/seed.ts +67 -0
  79. package/src/auth/rbac/tables.ts +32 -0
  80. package/src/auth/rbac/types.ts +65 -0
  81. package/src/auth/tenant/extract.ts +33 -0
  82. package/src/auth/tenant/in-tenant.ts +49 -0
  83. package/src/auth/tenant/index.ts +7 -0
  84. package/src/auth/tenant/namespace.ts +23 -0
  85. package/src/auth/tenant/propagate.ts +20 -0
  86. package/src/auth/tenant/rls.ts +120 -0
  87. package/src/auth/tenant/scope.ts +33 -0
  88. package/src/auth/tenant/scoped-db.ts +581 -0
  89. package/src/auth/tenant/types.ts +14 -0
  90. package/src/auth/tenant/validate.ts +27 -0
  91. package/src/cli/commands/add.ts +158 -0
  92. package/src/cli/commands/alerts.ts +98 -0
  93. package/src/cli/commands/check.ts +1418 -0
  94. package/src/cli/commands/codemod.ts +78 -0
  95. package/src/cli/commands/db.ts +356 -0
  96. package/src/cli/commands/dev.ts +101 -0
  97. package/src/cli/commands/generate.ts +75 -0
  98. package/src/cli/commands/init.ts +36 -0
  99. package/src/cli/commands/install.ts +28 -0
  100. package/src/cli/commands/lint.ts +62 -0
  101. package/src/cli/commands/migrate.ts +132 -0
  102. package/src/cli/commands/openapi.ts +63 -0
  103. package/src/cli/commands/remove.ts +72 -0
  104. package/src/cli/commands/run.ts +50 -0
  105. package/src/cli/commands/scope.ts +54 -0
  106. package/src/cli/commands/serve.ts +68 -0
  107. package/src/cli/commands/shard.ts +31 -0
  108. package/src/cli/commands/shared/config-pipework-version.ts +28 -0
  109. package/src/cli/commands/shared/find-config.ts +22 -0
  110. package/src/cli/commands/shared/load-config.ts +35 -0
  111. package/src/cli/commands/shared/pnpm-install.ts +95 -0
  112. package/src/cli/commands/shared/report-removed.ts +8 -0
  113. package/src/cli/commands/sync.ts +32 -0
  114. package/src/cli/commands/test.ts +307 -0
  115. package/src/cli/commands/upgrade.ts +48 -0
  116. package/src/cli/dev-bootstrap.ts +24 -0
  117. package/src/cli/errors.ts +10 -0
  118. package/src/cli/index.ts +303 -0
  119. package/src/cli/output.ts +17 -0
  120. package/src/cli/port.ts +15 -0
  121. package/src/cli/surface-kinds.ts +20 -0
  122. package/src/client/index.ts +4 -0
  123. package/src/client/poll.ts +130 -0
  124. package/src/client/report-fault.ts +119 -0
  125. package/src/core/config/app-pool.ts +19 -0
  126. package/src/core/config/aws-env.ts +29 -0
  127. package/src/core/config/base-dir.ts +24 -0
  128. package/src/core/config/discover.ts +64 -0
  129. package/src/core/config/env-resolve.ts +87 -0
  130. package/src/core/config/env-types.ts +116 -0
  131. package/src/core/config/env.ts +55 -0
  132. package/src/core/config/errors.ts +9 -0
  133. package/src/core/config/import-config.ts +40 -0
  134. package/src/core/config/index.ts +8 -0
  135. package/src/core/config/load.ts +417 -0
  136. package/src/core/config/namespace.ts +14 -0
  137. package/src/core/config/resolve.ts +195 -0
  138. package/src/core/config/schema.ts +488 -0
  139. package/src/core/config/statement-observer.ts +48 -0
  140. package/src/core/config/ts-inline-hooks.ts +75 -0
  141. package/src/core/config/ts-register.ts +4 -0
  142. package/src/core/config/ts-resolve-hooks.ts +101 -0
  143. package/src/core/errors/base.ts +49 -0
  144. package/src/core/errors/http.ts +146 -0
  145. package/src/core/errors/index.ts +2 -0
  146. package/src/core/errors/namespace.ts +31 -0
  147. package/src/core/lifecycle/drain.ts +33 -0
  148. package/src/core/lifecycle/health.ts +72 -0
  149. package/src/core/lifecycle/index.ts +4 -0
  150. package/src/core/lifecycle/namespace.ts +7 -0
  151. package/src/core/lifecycle/startup.ts +102 -0
  152. package/src/core/lifecycle/types.ts +36 -0
  153. package/src/core/logging/context.ts +60 -0
  154. package/src/core/logging/error-chain.ts +68 -0
  155. package/src/core/logging/index.ts +5 -0
  156. package/src/core/logging/logger.ts +65 -0
  157. package/src/core/logging/namespace.ts +16 -0
  158. package/src/core/logging/proxy.ts +22 -0
  159. package/src/core/logging/redact-match.ts +24 -0
  160. package/src/core/logging/types.ts +47 -0
  161. package/src/core/pipework.ts +566 -0
  162. package/src/core/runtime/broker.ts +69 -0
  163. package/src/core/surface/http.ts +133 -0
  164. package/src/core/surface/index.ts +11 -0
  165. package/src/core/surface/job-timeout.ts +40 -0
  166. package/src/core/surface/namespace.ts +13 -0
  167. package/src/core/surface/script.ts +33 -0
  168. package/src/core/surface/start.ts +142 -0
  169. package/src/core/surface/types.ts +225 -0
  170. package/src/core/surface/validate.ts +88 -0
  171. package/src/core/surface/worker.ts +528 -0
  172. package/src/core/validation/connectivity.ts +49 -0
  173. package/src/core/validation/domain.ts +99 -0
  174. package/src/core/validation/index.ts +5 -0
  175. package/src/core/validation/queue-tables.ts +312 -0
  176. package/src/core/validation/rls-role.ts +138 -0
  177. package/src/core/validation/startup.ts +51 -0
  178. package/src/core/validation/surfaces.ts +65 -0
  179. package/src/core/validation/types.ts +25 -0
  180. package/src/data/db/advisory-lock.ts +55 -0
  181. package/src/data/db/aggregates.ts +172 -0
  182. package/src/data/db/array-elements.ts +111 -0
  183. package/src/data/db/bulk-set.ts +53 -0
  184. package/src/data/db/col/index.ts +14 -0
  185. package/src/data/db/composed.ts +53 -0
  186. package/src/data/db/db.ts +276 -0
  187. package/src/data/db/excluded.ts +16 -0
  188. package/src/data/db/expressions.ts +473 -0
  189. package/src/data/db/filter.ts +90 -0
  190. package/src/data/db/function-source.ts +114 -0
  191. package/src/data/db/identifiers.ts +16 -0
  192. package/src/data/db/idx/index.ts +9 -0
  193. package/src/data/db/in-list.ts +49 -0
  194. package/src/data/db/index.ts +9 -0
  195. package/src/data/db/insert-split.ts +137 -0
  196. package/src/data/db/instrumentation.ts +82 -0
  197. package/src/data/db/maintenance.ts +28 -0
  198. package/src/data/db/namespace.ts +107 -0
  199. package/src/data/db/owner-labels.ts +9 -0
  200. package/src/data/db/pool.ts +187 -0
  201. package/src/data/db/query-label.ts +13 -0
  202. package/src/data/db/query-watch.ts +116 -0
  203. package/src/data/db/raw-client-warning.ts +37 -0
  204. package/src/data/db/readonly.ts +23 -0
  205. package/src/data/db/reservation.ts +151 -0
  206. package/src/data/db/schema-features.ts +14 -0
  207. package/src/data/db/serializable.ts +83 -0
  208. package/src/data/db/sql.ts +1 -0
  209. package/src/data/db/timeouts.ts +76 -0
  210. package/src/data/db/types.ts +18 -0
  211. package/src/data/db/unbound.ts +33 -0
  212. package/src/data/db/values.ts +29 -0
  213. package/src/data/domain/brand.ts +15 -0
  214. package/src/data/domain/build-factory.ts +193 -0
  215. package/src/data/domain/build-schema.ts +161 -0
  216. package/src/data/domain/build-table.ts +355 -0
  217. package/src/data/domain/define.ts +239 -0
  218. package/src/data/domain/external-registry.ts +34 -0
  219. package/src/data/domain/external.ts +77 -0
  220. package/src/data/domain/field.ts +279 -0
  221. package/src/data/domain/index.ts +13 -0
  222. package/src/data/domain/naming.ts +6 -0
  223. package/src/data/domain/projections.ts +57 -0
  224. package/src/data/domain/trace-safe-projection.ts +31 -0
  225. package/src/data/domain/traits.ts +108 -0
  226. package/src/data/domain/types.ts +289 -0
  227. package/src/data/jsonb/composed.ts +18 -0
  228. package/src/data/jsonb/index.ts +1 -0
  229. package/src/data/jsonb/namespace.ts +30 -0
  230. package/src/data/jsonb/query.ts +86 -0
  231. package/src/data/migrate/append-only-generate.ts +47 -0
  232. package/src/data/migrate/applied.ts +47 -0
  233. package/src/data/migrate/apply.ts +309 -0
  234. package/src/data/migrate/check.ts +119 -0
  235. package/src/data/migrate/diff.ts +357 -0
  236. package/src/data/migrate/door-generate.ts +130 -0
  237. package/src/data/migrate/framework-rls.ts +58 -0
  238. package/src/data/migrate/generate.ts +201 -0
  239. package/src/data/migrate/history.ts +215 -0
  240. package/src/data/migrate/index.ts +9 -0
  241. package/src/data/migrate/internal-definitions.ts +175 -0
  242. package/src/data/migrate/post-process.ts +73 -0
  243. package/src/data/migrate/pragma.ts +73 -0
  244. package/src/data/migrate/provision-framework.ts +147 -0
  245. package/src/data/migrate/rls-generate.ts +61 -0
  246. package/src/data/migrate/run.ts +124 -0
  247. package/src/data/migrate/safety.ts +167 -0
  248. package/src/data/migrate/snapshot.ts +368 -0
  249. package/src/data/migrate/sql-emit.ts +392 -0
  250. package/src/data/migrate/statements.ts +312 -0
  251. package/src/data/query/alias.ts +126 -0
  252. package/src/data/query/batch.ts +8 -0
  253. package/src/data/query/cache/core/cache.ts +78 -0
  254. package/src/data/query/cache/core/index.ts +1 -0
  255. package/src/data/query/cache/core/types.ts +29 -0
  256. package/src/data/query/cache/readme.md +230 -0
  257. package/src/data/query/casing.ts +76 -0
  258. package/src/data/query/collation.ts +11 -0
  259. package/src/data/query/column-builder.ts +370 -0
  260. package/src/data/query/column.ts +150 -0
  261. package/src/data/query/entity.ts +42 -0
  262. package/src/data/query/errors.ts +33 -0
  263. package/src/data/query/index.ts +14 -0
  264. package/src/data/query/logger.ts +47 -0
  265. package/src/data/query/operations.ts +45 -0
  266. package/src/data/query/pg-core/alias.ts +12 -0
  267. package/src/data/query/pg-core/checks.ts +32 -0
  268. package/src/data/query/pg-core/columns/all.ts +71 -0
  269. package/src/data/query/pg-core/columns/bigint.ts +110 -0
  270. package/src/data/query/pg-core/columns/bigserial.ts +128 -0
  271. package/src/data/query/pg-core/columns/boolean.ts +43 -0
  272. package/src/data/query/pg-core/columns/char.ts +85 -0
  273. package/src/data/query/pg-core/columns/cidr.ts +43 -0
  274. package/src/data/query/pg-core/columns/common.ts +366 -0
  275. package/src/data/query/pg-core/columns/custom.ts +232 -0
  276. package/src/data/query/pg-core/columns/date.common.ts +15 -0
  277. package/src/data/query/pg-core/columns/date.ts +112 -0
  278. package/src/data/query/pg-core/columns/double-precision.ts +55 -0
  279. package/src/data/query/pg-core/columns/enum.ts +201 -0
  280. package/src/data/query/pg-core/columns/index.ts +35 -0
  281. package/src/data/query/pg-core/columns/inet.ts +43 -0
  282. package/src/data/query/pg-core/columns/int.common.ts +57 -0
  283. package/src/data/query/pg-core/columns/integer.ts +53 -0
  284. package/src/data/query/pg-core/columns/interval.ts +83 -0
  285. package/src/data/query/pg-core/columns/json.ts +58 -0
  286. package/src/data/query/pg-core/columns/jsonb.ts +56 -0
  287. package/src/data/query/pg-core/columns/line.ts +117 -0
  288. package/src/data/query/pg-core/columns/macaddr.ts +43 -0
  289. package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
  290. package/src/data/query/pg-core/columns/numeric.ts +227 -0
  291. package/src/data/query/pg-core/columns/point.ts +127 -0
  292. package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
  293. package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
  294. package/src/data/query/pg-core/columns/real.ts +58 -0
  295. package/src/data/query/pg-core/columns/serial.ts +55 -0
  296. package/src/data/query/pg-core/columns/smallint.ts +53 -0
  297. package/src/data/query/pg-core/columns/smallserial.ts +60 -0
  298. package/src/data/query/pg-core/columns/text.ts +67 -0
  299. package/src/data/query/pg-core/columns/time.ts +72 -0
  300. package/src/data/query/pg-core/columns/timestamp.ts +163 -0
  301. package/src/data/query/pg-core/columns/uuid.ts +51 -0
  302. package/src/data/query/pg-core/columns/varchar.ts +89 -0
  303. package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
  304. package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
  305. package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
  306. package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
  307. package/src/data/query/pg-core/db.ts +674 -0
  308. package/src/data/query/pg-core/dialect.ts +1404 -0
  309. package/src/data/query/pg-core/expressions.ts +25 -0
  310. package/src/data/query/pg-core/foreign-keys.ts +130 -0
  311. package/src/data/query/pg-core/index.ts +21 -0
  312. package/src/data/query/pg-core/indexes.ts +276 -0
  313. package/src/data/query/pg-core/policies.ts +56 -0
  314. package/src/data/query/pg-core/primary-keys.ts +62 -0
  315. package/src/data/query/pg-core/query-builders/count.ts +86 -0
  316. package/src/data/query/pg-core/query-builders/delete.ts +293 -0
  317. package/src/data/query/pg-core/query-builders/index.ts +7 -0
  318. package/src/data/query/pg-core/query-builders/insert.ts +452 -0
  319. package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
  320. package/src/data/query/pg-core/query-builders/query.ts +157 -0
  321. package/src/data/query/pg-core/query-builders/raw.ts +49 -0
  322. package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
  323. package/src/data/query/pg-core/query-builders/select.ts +1346 -0
  324. package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
  325. package/src/data/query/pg-core/query-builders/update.ts +626 -0
  326. package/src/data/query/pg-core/roles.ts +41 -0
  327. package/src/data/query/pg-core/schema.ts +72 -0
  328. package/src/data/query/pg-core/sequence.ts +41 -0
  329. package/src/data/query/pg-core/session.ts +292 -0
  330. package/src/data/query/pg-core/subquery.ts +46 -0
  331. package/src/data/query/pg-core/table.ts +252 -0
  332. package/src/data/query/pg-core/unique-constraint.ts +73 -0
  333. package/src/data/query/pg-core/utils/array.ts +95 -0
  334. package/src/data/query/pg-core/utils/index.ts +1 -0
  335. package/src/data/query/pg-core/utils.ts +104 -0
  336. package/src/data/query/pg-core/view-base.ts +14 -0
  337. package/src/data/query/pg-core/view-common.ts +1 -0
  338. package/src/data/query/pg-core/view.ts +435 -0
  339. package/src/data/query/pg-core/with-builder.ts +41 -0
  340. package/src/data/query/postgres-js/README.md +46 -0
  341. package/src/data/query/postgres-js/array-parser.ts +45 -0
  342. package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
  343. package/src/data/query/postgres-js/driver.ts +163 -0
  344. package/src/data/query/postgres-js/index.ts +2 -0
  345. package/src/data/query/postgres-js/session.ts +234 -0
  346. package/src/data/query/primary-key.ts +11 -0
  347. package/src/data/query/query-builders/query-builder.ts +19 -0
  348. package/src/data/query/query-builders/select.types.ts +175 -0
  349. package/src/data/query/query-promise.ts +35 -0
  350. package/src/data/query/relations.ts +725 -0
  351. package/src/data/query/runnable-query.ts +12 -0
  352. package/src/data/query/selection-proxy.ts +121 -0
  353. package/src/data/query/session.ts +8 -0
  354. package/src/data/query/sql/expressions/conditions.ts +741 -0
  355. package/src/data/query/sql/expressions/index.ts +2 -0
  356. package/src/data/query/sql/expressions/select.ts +45 -0
  357. package/src/data/query/sql/functions/aggregate.ts +129 -0
  358. package/src/data/query/sql/functions/index.ts +2 -0
  359. package/src/data/query/sql/functions/vector.ts +178 -0
  360. package/src/data/query/sql/index.ts +3 -0
  361. package/src/data/query/sql/sql.ts +794 -0
  362. package/src/data/query/subquery.ts +53 -0
  363. package/src/data/query/table.ts +208 -0
  364. package/src/data/query/table.utils.ts +2 -0
  365. package/src/data/query/tracing-utils.ts +3 -0
  366. package/src/data/query/tracing.ts +11 -0
  367. package/src/data/query/utils.ts +357 -0
  368. package/src/data/query/version.ts +4 -0
  369. package/src/data/query/view-common.ts +1 -0
  370. package/src/data/schema/branded.ts +13 -0
  371. package/src/data/schema/generated/coerce.ts +5 -0
  372. package/src/data/schema/generated/combinator.ts +6 -0
  373. package/src/data/schema/generated/encoding.ts +5 -0
  374. package/src/data/schema/generated/error.ts +6 -0
  375. package/src/data/schema/generated/format.ts +5 -0
  376. package/src/data/schema/generated/type-constructor.ts +10 -0
  377. package/src/data/schema/generated/type.ts +6 -0
  378. package/src/data/schema/index.ts +6 -0
  379. package/src/data/schema/json-schema.ts +33 -0
  380. package/src/data/schema/namespace.ts +55 -0
  381. package/src/data/schema/parse.ts +39 -0
  382. package/src/data/schema/types.ts +39 -0
  383. package/src/data/temporal/append-only.ts +134 -0
  384. package/src/data/temporal/close.ts +27 -0
  385. package/src/data/temporal/columns.ts +10 -0
  386. package/src/data/temporal/definition-queries.ts +70 -0
  387. package/src/data/temporal/errors.ts +10 -0
  388. package/src/data/temporal/get-current.ts +22 -0
  389. package/src/data/temporal/index.ts +9 -0
  390. package/src/data/temporal/namespace.ts +36 -0
  391. package/src/data/temporal/query.ts +25 -0
  392. package/src/data/temporal/resolve.ts +17 -0
  393. package/src/data/temporal/revise.ts +67 -0
  394. package/src/data/temporal/types.ts +22 -0
  395. package/src/data/valve/artifact.ts +33 -0
  396. package/src/data/valve/contract-diff.ts +21 -0
  397. package/src/data/valve/cross.ts +57 -0
  398. package/src/data/valve/crossing-ledger.ts +50 -0
  399. package/src/data/valve/decision.ts +44 -0
  400. package/src/data/valve/durable-crossing.ts +52 -0
  401. package/src/data/valve/execute-crossing.ts +77 -0
  402. package/src/data/valve/faucet.ts +56 -0
  403. package/src/data/valve/gate.ts +114 -0
  404. package/src/data/valve/graph.ts +77 -0
  405. package/src/data/valve/index.ts +53 -0
  406. package/src/data/valve/namespace.ts +57 -0
  407. package/src/data/valve/registry.ts +50 -0
  408. package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
  409. package/src/data/valve/schema/valve-crossing.ts +9 -0
  410. package/src/data/valve/supply.ts +154 -0
  411. package/src/data/valve/types.ts +143 -0
  412. package/src/data/vector/columns.ts +27 -0
  413. package/src/data/vector/index.ts +4 -0
  414. package/src/data/vector/namespace.ts +33 -0
  415. package/src/data/vector/query.ts +85 -0
  416. package/src/data/vector/validate.ts +16 -0
  417. package/src/data/write/audit/actor.ts +15 -0
  418. package/src/data/write/audit/audit-columns.ts +18 -0
  419. package/src/data/write/audit/audit-select.ts +111 -0
  420. package/src/data/write/audit/bound-transaction.ts +14 -0
  421. package/src/data/write/audit/cte-compile.ts +32 -0
  422. package/src/data/write/audit/decode-returning.ts +47 -0
  423. package/src/data/write/audit/final-select.ts +43 -0
  424. package/src/data/write/audit/infra-audit.ts +36 -0
  425. package/src/data/write/audit/primary-key.ts +65 -0
  426. package/src/data/write/audit/projection.ts +41 -0
  427. package/src/data/write/audit/read-touches.ts +86 -0
  428. package/src/data/write/audit/touch-match.ts +78 -0
  429. package/src/data/write/audit/verb-delete.ts +62 -0
  430. package/src/data/write/audit/verb-insert.ts +36 -0
  431. package/src/data/write/audit/verb-select.ts +47 -0
  432. package/src/data/write/audit/verb-update.ts +60 -0
  433. package/src/data/write/audit/verb-upsert.ts +104 -0
  434. package/src/data/write/conflict.ts +43 -0
  435. package/src/data/write/delete.ts +38 -0
  436. package/src/data/write/execute.ts +243 -0
  437. package/src/data/write/index.ts +11 -0
  438. package/src/data/write/insert.ts +63 -0
  439. package/src/data/write/state.ts +43 -0
  440. package/src/data/write/update.ts +54 -0
  441. package/src/data/write/upsert.ts +58 -0
  442. package/src/data/write/values.ts +19 -0
  443. package/src/index.ts +230 -0
  444. package/src/infra/access-links/code.ts +70 -0
  445. package/src/infra/access-links/handle.ts +892 -0
  446. package/src/infra/access-links/index.ts +9 -0
  447. package/src/infra/access-links/namespace.ts +30 -0
  448. package/src/infra/access-links/person-check.ts +177 -0
  449. package/src/infra/access-links/schema.ts +71 -0
  450. package/src/infra/alerts/async-sources.ts +231 -0
  451. package/src/infra/alerts/audit.ts +36 -0
  452. package/src/infra/alerts/beat.ts +123 -0
  453. package/src/infra/alerts/client-faults.ts +111 -0
  454. package/src/infra/alerts/faults.ts +156 -0
  455. package/src/infra/alerts/handle.ts +778 -0
  456. package/src/infra/alerts/http.ts +192 -0
  457. package/src/infra/alerts/index.ts +12 -0
  458. package/src/infra/alerts/infra-sources.ts +180 -0
  459. package/src/infra/alerts/namespace.ts +81 -0
  460. package/src/infra/alerts/redact.ts +21 -0
  461. package/src/infra/alerts/request-faults.ts +41 -0
  462. package/src/infra/alerts/schema.ts +124 -0
  463. package/src/infra/alerts/sinks.ts +125 -0
  464. package/src/infra/alerts/source-options.ts +26 -0
  465. package/src/infra/alerts/sources.ts +503 -0
  466. package/src/infra/alerts/subsystem-sources.ts +137 -0
  467. package/src/infra/audit/emit.ts +87 -0
  468. package/src/infra/audit/index.ts +2 -0
  469. package/src/infra/audit/namespace.ts +22 -0
  470. package/src/infra/audit/schema.sql +35 -0
  471. package/src/infra/audit/types.ts +18 -0
  472. package/src/infra/aws/credentials.ts +147 -0
  473. package/src/infra/aws/sigv4.ts +140 -0
  474. package/src/infra/cache/create.ts +58 -0
  475. package/src/infra/cache/entry.ts +81 -0
  476. package/src/infra/cache/index.ts +4 -0
  477. package/src/infra/cache/namespace.ts +10 -0
  478. package/src/infra/cache/tenant.ts +93 -0
  479. package/src/infra/cache/types.ts +24 -0
  480. package/src/infra/crawl/audit.ts +35 -0
  481. package/src/infra/crawl/errors.ts +25 -0
  482. package/src/infra/crawl/handle.ts +693 -0
  483. package/src/infra/crawl/index.ts +9 -0
  484. package/src/infra/crawl/links.ts +160 -0
  485. package/src/infra/crawl/namespace.ts +36 -0
  486. package/src/infra/crawl/robots.ts +100 -0
  487. package/src/infra/crawl/schema.ts +89 -0
  488. package/src/infra/credit-line/handle.ts +883 -0
  489. package/src/infra/credit-line/index.ts +6 -0
  490. package/src/infra/credit-line/namespace.ts +7 -0
  491. package/src/infra/credit-line/schema.ts +96 -0
  492. package/src/infra/credits/handle.ts +706 -0
  493. package/src/infra/credits/index.ts +5 -0
  494. package/src/infra/credits/namespace.ts +7 -0
  495. package/src/infra/credits/schema.ts +83 -0
  496. package/src/infra/email/audit.ts +36 -0
  497. package/src/infra/email/inbound.ts +622 -0
  498. package/src/infra/email/index.ts +11 -0
  499. package/src/infra/email/mail.ts +517 -0
  500. package/src/infra/email/mime.ts +447 -0
  501. package/src/infra/email/namespace.ts +22 -0
  502. package/src/infra/email/providers.ts +111 -0
  503. package/src/infra/email/schema.ts +96 -0
  504. package/src/infra/email/sns.ts +200 -0
  505. package/src/infra/email/unsubscribe.ts +44 -0
  506. package/src/infra/fetch/address.ts +88 -0
  507. package/src/infra/fetch/connect-retry.ts +87 -0
  508. package/src/infra/fetch/handle.ts +329 -0
  509. package/src/infra/fetch/index.ts +7 -0
  510. package/src/infra/fetch/namespace.ts +70 -0
  511. package/src/infra/idempotency/index.ts +3 -0
  512. package/src/infra/idempotency/middleware.ts +87 -0
  513. package/src/infra/idempotency/store.ts +92 -0
  514. package/src/infra/idempotency/types.ts +30 -0
  515. package/src/infra/join-requests/handle.ts +312 -0
  516. package/src/infra/join-requests/index.ts +2 -0
  517. package/src/infra/join-requests/namespace.ts +7 -0
  518. package/src/infra/join-requests/schema.ts +33 -0
  519. package/src/infra/outbox/core.ts +165 -0
  520. package/src/infra/quota/errors.ts +32 -0
  521. package/src/infra/quota/handle.ts +215 -0
  522. package/src/infra/quota/index.ts +5 -0
  523. package/src/infra/quota/namespace.ts +7 -0
  524. package/src/infra/quota/schema.ts +15 -0
  525. package/src/infra/storage/backend.ts +34 -0
  526. package/src/infra/storage/handle.ts +513 -0
  527. package/src/infra/storage/index.ts +9 -0
  528. package/src/infra/storage/local.ts +104 -0
  529. package/src/infra/storage/namespace.ts +13 -0
  530. package/src/infra/storage/s3.ts +257 -0
  531. package/src/infra/storage/schema.ts +24 -0
  532. package/src/infra/support/handle.ts +869 -0
  533. package/src/infra/support/http.ts +229 -0
  534. package/src/infra/support/index.ts +7 -0
  535. package/src/infra/support/namespace.ts +12 -0
  536. package/src/infra/support/schema.ts +96 -0
  537. package/src/infra/webhook/inbound.ts +45 -0
  538. package/src/infra/webhook/index.ts +5 -0
  539. package/src/infra/webhook/namespace.ts +24 -0
  540. package/src/infra/webhook/outbound.ts +315 -0
  541. package/src/infra/webhook/sign.ts +32 -0
  542. package/src/infra/webhook/verify.ts +82 -0
  543. package/src/invariants/assert.ts +26 -0
  544. package/src/invariants/index.ts +1 -0
  545. package/src/lint/config.ts +56 -0
  546. package/src/lint/doctrine.ts +163 -0
  547. package/src/lint/include.ts +97 -0
  548. package/src/lint/index.ts +3 -0
  549. package/src/lint/preset.ts +10 -0
  550. package/src/lint/resolver.ts +218 -0
  551. package/src/lint/rules/no-reexports.ts +46 -0
  552. package/src/request/behavior/audited.ts +85 -0
  553. package/src/request/behavior/cached.ts +62 -0
  554. package/src/request/behavior/compose.ts +28 -0
  555. package/src/request/behavior/index.ts +5 -0
  556. package/src/request/behavior/namespace.ts +16 -0
  557. package/src/request/behavior/types.ts +26 -0
  558. package/src/request/behavior/versioned.ts +54 -0
  559. package/src/request/context/create.ts +103 -0
  560. package/src/request/context/guard.ts +49 -0
  561. package/src/request/context/index.ts +4 -0
  562. package/src/request/context/run-in-transaction.ts +200 -0
  563. package/src/request/context/store.ts +28 -0
  564. package/src/request/context/types.ts +50 -0
  565. package/src/request/di/builder.ts +535 -0
  566. package/src/request/di/index.ts +8 -0
  567. package/src/request/di/resolve.ts +271 -0
  568. package/src/request/di/types.ts +262 -0
  569. package/src/request/fitting/namespace.ts +7 -0
  570. package/src/request/fixture/batch.ts +71 -0
  571. package/src/request/fixture/builder.ts +26 -0
  572. package/src/request/fixture/errors.ts +10 -0
  573. package/src/request/fixture/index.ts +8 -0
  574. package/src/request/fixture/namespace.ts +23 -0
  575. package/src/request/fixture/paginate.ts +44 -0
  576. package/src/request/fixture/to-handlers.ts +260 -0
  577. package/src/request/fixture/types.ts +79 -0
  578. package/src/request/flow/act-as.ts +78 -0
  579. package/src/request/flow/namespace.ts +17 -0
  580. package/src/request/http/error-step.ts +42 -0
  581. package/src/request/http/file.ts +141 -0
  582. package/src/request/http/index.ts +4 -0
  583. package/src/request/http/middleware.ts +141 -0
  584. package/src/request/http/namespace.ts +5 -0
  585. package/src/request/http/rate-limit-classes.ts +170 -0
  586. package/src/request/http/response.ts +82 -0
  587. package/src/request/http/server.ts +916 -0
  588. package/src/request/http/sniff.ts +261 -0
  589. package/src/request/http/types.ts +250 -0
  590. package/src/request/http/upload.ts +284 -0
  591. package/src/request/internal/text.ts +6 -0
  592. package/src/request/openapi/generate.ts +205 -0
  593. package/src/request/openapi/index.ts +2 -0
  594. package/src/request/openapi/namespace.ts +7 -0
  595. package/src/test/auth.ts +13 -0
  596. package/src/test/auto-setup.ts +45 -0
  597. package/src/test/context.ts +91 -0
  598. package/src/test/crawl-fixture.ts +157 -0
  599. package/src/test/discover-fork.ts +17 -0
  600. package/src/test/fingerprint.ts +135 -0
  601. package/src/test/globals.d.ts +7 -0
  602. package/src/test/index.ts +19 -0
  603. package/src/test/namespace.ts +87 -0
  604. package/src/test/plugin.ts +189 -0
  605. package/src/test/progress.ts +185 -0
  606. package/src/test/reap-after-run.ts +28 -0
  607. package/src/test/reap.ts +286 -0
  608. package/src/test/reset.ts +253 -0
  609. package/src/test/savepoints.ts +172 -0
  610. package/src/test/setup.ts +1422 -0
  611. package/src/test/system-channel.ts +50 -0
  612. package/src/test/vitest-plugin.ts +142 -0
  613. package/src/test/vitest-workspace.ts +507 -0
  614. package/src/test/vitest.ts +333 -0
  615. package/src/test/watch.ts +105 -0
  616. package/src/trace/capture/bounded-serializer.ts +146 -0
  617. package/src/trace/cardinality/analyze.ts +173 -0
  618. package/src/trace/cardinality/branch-context.ts +214 -0
  619. package/src/trace/cardinality/check.ts +33 -0
  620. package/src/trace/cardinality/classify-edge.ts +64 -0
  621. package/src/trace/cardinality/structural-max.ts +107 -0
  622. package/src/trace/check/coverage-check.ts +188 -0
  623. package/src/trace/check/flow-check.ts +103 -0
  624. package/src/trace/check/flow-step-site.ts +234 -0
  625. package/src/trace/check/io-pattern.ts +116 -0
  626. package/src/trace/codemod/add-cardinality.ts +152 -0
  627. package/src/trace/codemod/run.ts +80 -0
  628. package/src/trace/entry/child-trace.ts +21 -0
  629. package/src/trace/entry/close-trace.ts +87 -0
  630. package/src/trace/entry/finalize-trace.ts +28 -0
  631. package/src/trace/entry/open-trace.ts +138 -0
  632. package/src/trace/entry/parent-trace.ts +20 -0
  633. package/src/trace/entry/parse-traceparent.ts +39 -0
  634. package/src/trace/entry/process-start.ts +38 -0
  635. package/src/trace/entry/trace-context.ts +26 -0
  636. package/src/trace/entry/tracing-enabled.ts +10 -0
  637. package/src/trace/external/record.ts +195 -0
  638. package/src/trace/external/report.ts +66 -0
  639. package/src/trace/namespace.ts +19 -0
  640. package/src/trace/partition/day.ts +47 -0
  641. package/src/trace/partition/index.ts +14 -0
  642. package/src/trace/partition/partition-name.ts +23 -0
  643. package/src/trace/partition/plan.ts +82 -0
  644. package/src/trace/partition/schedule.ts +26 -0
  645. package/src/trace/partition/startup-hook.ts +39 -0
  646. package/src/trace/partition/sync.ts +154 -0
  647. package/src/trace/partition/tables.ts +53 -0
  648. package/src/trace/read/tree.ts +83 -0
  649. package/src/trace/retain/copy-on-error.ts +76 -0
  650. package/src/trace/runtime/expire-stale-traces.ts +54 -0
  651. package/src/trace/runtime/flow-step.ts +147 -0
  652. package/src/trace/runtime/flush-steps.ts +31 -0
  653. package/src/trace/runtime/pending-writes.ts +54 -0
  654. package/src/trace/runtime/step-buffer.ts +298 -0
  655. package/src/trace/runtime/step-error.ts +21 -0
  656. package/src/trace/runtime/step-meta.ts +55 -0
  657. package/src/trace/runtime/thenable.ts +12 -0
  658. package/src/trace/runtime/truncate-trace.ts +62 -0
  659. package/src/trace/schema/trace-fields.ts +52 -0
  660. package/src/trace/schema/trace-retained.ts +22 -0
  661. package/src/trace/schema/trace-step-fields.ts +44 -0
  662. package/src/trace/schema/trace-step-retained.ts +25 -0
  663. package/src/trace/schema/trace-step.ts +18 -0
  664. package/src/trace/schema/trace.ts +12 -0
  665. package/src/trace/transformer/apply.ts +79 -0
  666. package/src/trace/transformer/build-meta-literal.ts +30 -0
  667. package/src/trace/transformer/find-flow-step-sites.ts +85 -0
  668. package/src/trace/transformer/inject-flow-step-import.ts +39 -0
  669. package/src/trace/transformer/transform-error.ts +14 -0
  670. package/src/trace/transformer/transform-flow-steps.ts +123 -0
  671. package/src/trace/transformer/wrap-function-declaration.ts +62 -0
  672. package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
  673. package/src/workspace/affected.ts +110 -0
  674. package/src/workspace/changed-files.ts +163 -0
  675. package/src/workspace/check-deps.ts +213 -0
  676. package/src/workspace/checksum.ts +20 -0
  677. package/src/workspace/dependency-graph.ts +67 -0
  678. package/src/workspace/detect-pipework-copies.ts +93 -0
  679. package/src/workspace/discover.ts +206 -0
  680. package/src/workspace/generate.ts +401 -0
  681. package/src/workspace/index.ts +7 -0
  682. package/src/workspace/module-imports-pipework.ts +39 -0
  683. package/src/workspace/module-package-name.ts +12 -0
  684. package/src/workspace/scrub-git-env.ts +23 -0
  685. package/src/workspace/source-paths.ts +36 -0
  686. package/src/workspace/sync.ts +182 -0
  687. package/dist/REFERENCE.md +0 -1477
  688. package/dist/infra/access-links/rate-limit.d.ts +0 -10
  689. package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
  690. package/dist/infra/access-links/rate-limit.js +0 -28
  691. package/dist/infra/access-links/rate-limit.js.map +0 -1
  692. package/dist/infra/email/sigv4.d.ts +0 -24
  693. package/dist/infra/email/sigv4.d.ts.map +0 -1
  694. package/dist/infra/email/sigv4.js +0 -55
  695. package/dist/infra/email/sigv4.js.map +0 -1
@@ -0,0 +1,137 @@
1
+ // Sources over the failures pipework's own subsystems already record: outboxes that gave up, webhook deliveries,
2
+ // credit settlements. Each is a composition of `querySource` and `newRowsSource`.
3
+
4
+ import { sql } from '../../data/db/sql.js'
5
+ import { assertValidIdentifier, quoteIdentifier } from '../../data/db/identifiers.js'
6
+ import { DEFAULT_WEBHOOK_DELIVERY_TABLE } from '../webhook/outbound.js'
7
+ import { common, positive, text, FIFTEEN_MIN, HOUR, type Basics } from './source-options.js'
8
+ import { composeSources, newRowsSource, querySource, type AlertSource } from './sources.js'
9
+
10
+ /**
11
+ * Raises `delivery-failed:<table>:<sink>` for outbox rows that gave up (`failed_at`) since the last tick, and
12
+ * `delivery-stuck:<table>:<sink>` while rows are still pending more than `stuckAfterMs` (default 15 min) past
13
+ * `next_attempt_at`. `tables` defaults to `['pipework_alert_deliveries']`; name `support_event_delivery` to add the support outbox.
14
+ * The failed condition clears by the handle's quiet period; the stuck one resolves when the rows drain.
15
+ */
16
+ export function deliveryFailedSource(options: Basics & { tables?: readonly string[]; stuckAfterMs?: number; settleMs?: number } = {}): AlertSource {
17
+ const tables = [...new Set(options.tables ?? ['pipework_alert_deliveries'])]
18
+ if (tables.length === 0) throw new Error('[pipework] alerts.source.deliveryFailed tables must not be empty; omit it to watch the alert outbox.')
19
+ for (const t of tables) assertValidIdentifier(t, 'deliveryFailed table')
20
+ const stuckAfter = positive(options.stuckAfterMs ?? FIFTEEN_MIN, 'stuckAfterMs', 'alerts.source.deliveryFailed')
21
+ const failed = tables.map(table => newRowsSource({
22
+ ...common(options),
23
+ name: 'delivery-failed',
24
+ stateKey: `delivery-failed:${table}`,
25
+ table,
26
+ at: 'failed_at',
27
+ groupBy: sql.raw(`'${table}:' || t."sink"`),
28
+ ...(options.settleMs !== undefined ? { settleMs: options.settleMs } : {}),
29
+ message: g => `${String(g.count)} delivery(ies) to ${String(g.last['sink'])} gave up; last error: ${text(g.last['last_error'])}`,
30
+ subject: g => ({ entity: 'sink', entityId: String(g.last['sink']) }),
31
+ context: g => ({ table, sink: String(g.last['sink']), count: g.count, lastError: g.last['last_error'] === null ? null : String(g.last['last_error']) }),
32
+ }))
33
+ const pending = tables.map(table => `
34
+ SELECT '${table}' AS tbl, sink, count(*)::int AS n,
35
+ (extract(epoch FROM clock_timestamp() - min(next_attempt_at)) * 1000)::bigint AS oldest_ms
36
+ FROM ${quoteIdentifier(table, 'deliveryFailed table')}
37
+ WHERE delivered_at IS NULL AND failed_at IS NULL
38
+ AND next_attempt_at < clock_timestamp() - ${String(stuckAfter)} * interval '1 millisecond'
39
+ GROUP BY sink`).join(' UNION ALL ')
40
+ const stuck = querySource({
41
+ ...common(options),
42
+ name: 'delivery-stuck',
43
+ query: sql`SELECT * FROM (${sql.raw(pending)}) p ORDER BY tbl, sink`,
44
+ id: r => `${String(r['tbl'])}:${String(r['sink'])}`,
45
+ message: r => `${String(r['n'])} delivery(ies) to ${String(r['sink'])} pending more than ${String(stuckAfter)} ms past their retry time (oldest ${String(r['oldest_ms'])} ms)`,
46
+ subject: r => ({ entity: 'sink', entityId: String(r['sink']) }),
47
+ context: r => ({ table: String(r['tbl']), sink: String(r['sink']), count: Number(r['n']), oldestMs: Number(r['oldest_ms']) }),
48
+ })
49
+ return composeSources('delivery-failed', [...failed, stuck])
50
+ }
51
+
52
+ /**
53
+ * Raises `webhook-failed:<endpointId>` for outbound webhook deliveries that gave up (completed with no 2xx status)
54
+ * since the last tick, and `webhook-stalled:<endpointId>` while deliveries are more than `stalledAfterMs`
55
+ * (default 15 min) past their `next_retry_at`. `table` is the delivery table (default the outbound webhook's).
56
+ */
57
+ export function webhookFailedSource(options: Basics & { table?: string; stalledAfterMs?: number; settleMs?: number } = {}): AlertSource {
58
+ const table = options.table ?? DEFAULT_WEBHOOK_DELIVERY_TABLE
59
+ assertValidIdentifier(table, 'webhookFailed table')
60
+ const stalledAfter = positive(options.stalledAfterMs ?? FIFTEEN_MIN, 'stalledAfterMs', 'alerts.source.webhookFailed')
61
+ const failed = newRowsSource({
62
+ ...common(options),
63
+ name: 'webhook-failed',
64
+ stateKey: `webhook-failed:${table}`,
65
+ table,
66
+ at: 'completed_at',
67
+ where: sql`(t.status_code IS NULL OR t.status_code < 200 OR t.status_code >= 300)`,
68
+ groupBy: 'endpoint_id',
69
+ ...(options.settleMs !== undefined ? { settleMs: options.settleMs } : {}),
70
+ message: g => `${String(g.count)} webhook delivery(ies) to endpoint ${g.id} gave up; last status ${text(g.last['status_code'])}`,
71
+ subject: g => ({ tenantId: String(g.last['tenant_id']), entity: 'webhook-endpoint', entityId: g.id }),
72
+ context: g => ({ endpointId: g.id, count: g.count, lastStatus: g.last['status_code'] === null ? null : Number(g.last['status_code']) }),
73
+ })
74
+ const stalled = querySource({
75
+ ...common(options),
76
+ name: 'webhook-stalled',
77
+ query: sql`
78
+ SELECT endpoint_id, count(*)::int AS n, (extract(epoch FROM clock_timestamp() - min(next_retry_at)) * 1000)::bigint AS oldest_ms
79
+ FROM ${sql.raw(quoteIdentifier(table, 'webhookFailed table'))}
80
+ WHERE completed_at IS NULL AND next_retry_at < clock_timestamp() - ${stalledAfter} * interval '1 millisecond'
81
+ GROUP BY endpoint_id ORDER BY endpoint_id`,
82
+ id: r => String(r['endpoint_id']),
83
+ message: r => `${String(r['n'])} webhook delivery(ies) to endpoint ${String(r['endpoint_id'])} more than ${String(stalledAfter)} ms past their retry time (oldest ${String(r['oldest_ms'])} ms)`,
84
+ subject: r => ({ entity: 'webhook-endpoint', entityId: String(r['endpoint_id']) }),
85
+ context: r => ({ endpointId: String(r['endpoint_id']), count: Number(r['n']), oldestMs: Number(r['oldest_ms']) }),
86
+ })
87
+ return composeSources('webhook-failed', [failed, stalled])
88
+ }
89
+
90
+ /**
91
+ * Raises `credit-overdraft:<tenantId>` for settlements that overdrew a tenant since the last tick,
92
+ * `credit-held:<tenantId>` while a reservation has been held longer than `heldOlderThanMs` (default 1 h), and
93
+ * `credit-expiry-undrained:all` while grants expired more than `expiryGraceMs` (default 1 h) ago still have credit left,
94
+ * which means nothing is running the credits expiry job.
95
+ */
96
+ export function creditsSource(options: Basics & { heldOlderThanMs?: number; expiryGraceMs?: number; settleMs?: number } = {}): AlertSource {
97
+ const held = positive(options.heldOlderThanMs ?? HOUR, 'heldOlderThanMs', 'alerts.source.credits')
98
+ const grace = positive(options.expiryGraceMs ?? HOUR, 'expiryGraceMs', 'alerts.source.credits')
99
+ const overdraft = newRowsSource({
100
+ ...common(options),
101
+ name: 'credit-overdraft',
102
+ table: 'credit_reservation',
103
+ at: 'settled_at',
104
+ where: sql`t.overdraft > 0`,
105
+ groupBy: 'tenant_id',
106
+ sum: 'overdraft',
107
+ ...(options.settleMs !== undefined ? { settleMs: options.settleMs } : {}),
108
+ message: g => `${String(g.count)} settlement(s) overdrew ${g.id} by ${String(g.sum)}`,
109
+ subject: g => ({ tenantId: g.id }),
110
+ context: g => ({ count: g.count, overdraft: g.sum }),
111
+ })
112
+ const stale = querySource({
113
+ ...common(options),
114
+ name: 'credit-held',
115
+ query: sql`
116
+ SELECT tenant_id, count(*)::int AS n, (extract(epoch FROM clock_timestamp() - min(created_at)) * 1000)::bigint AS oldest_ms
117
+ FROM credit_reservation
118
+ WHERE status = 'held' AND created_at < clock_timestamp() - ${held} * interval '1 millisecond'
119
+ GROUP BY tenant_id ORDER BY tenant_id`,
120
+ id: r => String(r['tenant_id']),
121
+ message: r => `${String(r['n'])} credit hold(s) for ${String(r['tenant_id'])} older than ${String(held)} ms (oldest ${String(r['oldest_ms'])} ms)`,
122
+ subject: r => ({ tenantId: String(r['tenant_id']) }),
123
+ context: r => ({ count: Number(r['n']), oldestMs: Number(r['oldest_ms']) }),
124
+ })
125
+ const expiry = querySource({
126
+ ...common(options),
127
+ name: 'credit-expiry-undrained',
128
+ query: sql`
129
+ SELECT count(*)::int AS n FROM credit_grant
130
+ WHERE remaining > 0 AND expires_at <= clock_timestamp() - ${grace} * interval '1 millisecond'
131
+ HAVING count(*) > 0`,
132
+ id: () => 'all',
133
+ message: r => `${String(r['n'])} expired grant(s) not drained; is the credits expiry job scheduled?`,
134
+ context: r => ({ count: Number(r['n']) }),
135
+ })
136
+ return composeSources('credits', [overdraft, stale, expiry])
137
+ }
@@ -0,0 +1,87 @@
1
+ import { sql } from '../../data/db/sql.js'
2
+ import type { AuditConfig, AuditEmitOptions, Audit } from './types.js'
3
+ import { getContext } from '../../request/context/store.js'
4
+ import { assertValidIdentifier, quoteIdentifier } from '../../data/db/identifiers.js'
5
+
6
+ export function createAudit(config: AuditConfig): Audit {
7
+ const table = config.table ?? 'pipework_audit_events'
8
+ assertValidIdentifier(table, 'audit table')
9
+
10
+ return {
11
+ async emit(options: AuditEmitOptions): Promise<string> {
12
+ const ctx = getContext()
13
+ if (!ctx) {
14
+ throw new Error(
15
+ '[pipework] audit.emit() called outside a managed context. ' +
16
+ 'It must run inside a request handler or job.',
17
+ )
18
+ }
19
+
20
+ if (ctx.tenant === null) {
21
+ throw new Error(
22
+ '[pipework] audit.emit() requires tenant context. ' +
23
+ 'Configure tenant extraction in createServer().',
24
+ )
25
+ }
26
+
27
+ if (ctx.auth === null) {
28
+ throw new Error(
29
+ '[pipework] audit.emit() requires authenticated context. ' +
30
+ 'Ensure the handler uses .auth() or the route is not .public().',
31
+ )
32
+ }
33
+
34
+ const userId = extractUserId(ctx.auth)
35
+ if (userId === null) {
36
+ throw new Error(
37
+ '[pipework] audit.emit() requires auth.userId. ' +
38
+ 'Ensure the auth object has a userId field.',
39
+ )
40
+ }
41
+
42
+ const bound = ctx.transactions.get(config.database)
43
+ if (!bound) {
44
+ throw new Error(
45
+ `[pipework] audit.emit() requires an active transaction on database "${config.database}". ` +
46
+ `Configure transactionPerRequest: ['${config.database}'] in createServer().`,
47
+ )
48
+ }
49
+ const tx = bound.drizzle
50
+
51
+ const sessionId = config.sessionIdField !== undefined && typeof ctx.auth === 'object'
52
+ ? (ctx.auth as Record<string, unknown>)[config.sessionIdField] as string | null ?? null
53
+ : null
54
+
55
+ const beforeJson = options.before !== undefined ? JSON.stringify(options.before) : null
56
+ const afterJson = options.after !== undefined ? JSON.stringify(options.after) : null
57
+ const metadataJson = options.metadata !== undefined ? JSON.stringify(options.metadata) : null
58
+
59
+ const result = await tx.execute(sql`
60
+ INSERT INTO ${sql.raw(quoteIdentifier(table, 'audit table'))} (tenant_id, request_id, user_id, session_id, action_type, entity_type, entity_id, before_state, after_state, metadata)
61
+ VALUES (
62
+ ${ctx.tenant},
63
+ ${ctx.requestId},
64
+ ${userId},
65
+ ${sessionId ?? null},
66
+ ${options.actionType},
67
+ ${options.entityType ?? null},
68
+ ${options.entityId ?? null},
69
+ ${beforeJson !== null ? sql`${beforeJson}::jsonb` : sql`NULL`},
70
+ ${afterJson !== null ? sql`${afterJson}::jsonb` : sql`NULL`},
71
+ ${metadataJson !== null ? sql`${metadataJson}::jsonb` : sql`NULL`}
72
+ )
73
+ RETURNING id::text
74
+ `)
75
+
76
+ return (result[0] as { id: string }).id
77
+ },
78
+ }
79
+ }
80
+
81
+ function extractUserId(auth: unknown): string | null {
82
+ if (auth !== null && auth !== undefined && typeof auth === 'object' && 'userId' in auth) {
83
+ const val = (auth as { userId: unknown }).userId
84
+ return typeof val === 'string' ? val : null
85
+ }
86
+ return null
87
+ }
@@ -0,0 +1,2 @@
1
+ export type { AuditConfig, AuditEmitOptions, Audit } from './types.js'
2
+ export { createAudit } from './emit.js'
@@ -0,0 +1,22 @@
1
+ import { createAudit } from './index.js'
2
+ import { query } from '../../audit/query/index.js'
3
+ import { auditEntityId } from '../../data/write/audit/primary-key.js'
4
+ import { chainHead, verify, removeTenant } from '../../audit/chain/index.js'
5
+
6
+ /** Audit logging namespace — structured audit trail for data changes. */
7
+ export const audit = {
8
+ /** Creates an Audit instance that writes audit records to a Postgres table. */
9
+ create: createAudit,
10
+ /** Queries the framework-managed `audit_record` table. Tenant-scoped via the active Flow. `{ entity, entityId }` returns one row's history from an index; build a composite key's `entityId` with `audit.entityId`. */
11
+ query,
12
+ /**
13
+ * The `entityId` filter text for a written table's row: the primary key's value as text, or for a composite key a compact JSON array of the key values as strings in key order (`["a","7"]`). Pass the table and a row (or just its key fields). Dates become ISO strings.
14
+ */
15
+ entityId: auditEntityId,
16
+ /** The newest record's `{ seq, hash }` in the tenant's audit hash chain (blueprint `audit.chain`), to store outside the database; `null` when empty. */
17
+ chainHead,
18
+ /** Walks the tenant's audit hash chain and reports the first break (hash-mismatch, prev-mismatch, missing, head-mismatch, anchor-mismatch), or `{ ok: true }`. */
19
+ verify,
20
+ /** Deletes every audit record of one tenant and its hash chain, for the closure of that tenant's account. Irreversible; returns the number of records removed. Other tenants' records and chains are untouched. */
21
+ removeTenant,
22
+ }
@@ -0,0 +1,35 @@
1
+ CREATE TABLE IF NOT EXISTS pipework_audit_events (
2
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3
+ tenant_id text NOT NULL,
4
+ request_id text NOT NULL,
5
+ user_id text NOT NULL,
6
+ session_id text,
7
+ action_type text NOT NULL,
8
+ entity_type text,
9
+ entity_id text,
10
+ before_state jsonb,
11
+ after_state jsonb,
12
+ metadata jsonb,
13
+ created_at timestamptz NOT NULL DEFAULT now()
14
+ );
15
+
16
+ CREATE INDEX IF NOT EXISTS idx_audit_tenant_action ON pipework_audit_events (tenant_id, action_type, created_at);
17
+ CREATE INDEX IF NOT EXISTS idx_audit_tenant_entity ON pipework_audit_events (tenant_id, entity_type, entity_id, created_at);
18
+ CREATE INDEX IF NOT EXISTS idx_audit_tenant_user ON pipework_audit_events (tenant_id, user_id, created_at);
19
+ CREATE INDEX IF NOT EXISTS idx_audit_tenant_request ON pipework_audit_events (tenant_id, request_id);
20
+
21
+ CREATE OR REPLACE FUNCTION pipework_audit_immutable() RETURNS trigger AS $$
22
+ BEGIN
23
+ RAISE EXCEPTION 'pipework_audit_events is append-only: UPDATE and DELETE are not permitted';
24
+ END;
25
+ $$ LANGUAGE plpgsql;
26
+
27
+ DO $$ BEGIN
28
+ IF NOT EXISTS (
29
+ SELECT 1 FROM pg_trigger WHERE tgname = 'pipework_audit_no_modify'
30
+ ) THEN
31
+ CREATE TRIGGER pipework_audit_no_modify
32
+ BEFORE UPDATE OR DELETE ON pipework_audit_events
33
+ FOR EACH ROW EXECUTE FUNCTION pipework_audit_immutable();
34
+ END IF;
35
+ END $$;
@@ -0,0 +1,18 @@
1
+ export interface AuditConfig {
2
+ readonly database: string
3
+ readonly table?: string
4
+ readonly sessionIdField?: string
5
+ }
6
+
7
+ export interface AuditEmitOptions {
8
+ readonly actionType: string
9
+ readonly entityType?: string
10
+ readonly entityId?: string
11
+ readonly before?: unknown
12
+ readonly after?: unknown
13
+ readonly metadata?: Record<string, unknown>
14
+ }
15
+
16
+ export interface Audit {
17
+ emit(options: AuditEmitOptions): Promise<string>
18
+ }
@@ -0,0 +1,147 @@
1
+ import { readFile as fsReadFile } from 'node:fs/promises'
2
+ import { readAwsEnv, type AwsEnv } from '../../core/config/aws-env.js'
3
+ import type { AwsCredentials } from './sigv4.js'
4
+
5
+ export interface AwsCredentialsOptions {
6
+ /** For tests: where the AWS variables come from. Default: the process environment. */
7
+ readonly env?: () => AwsEnv
8
+ /** For tests: replaces global `fetch`. */
9
+ readonly fetch?: typeof fetch
10
+ /** For tests: the clock, in epoch milliseconds. */
11
+ readonly now?: () => number
12
+ /** For tests: reads AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE. */
13
+ readonly readFile?: (path: string) => Promise<string>
14
+ }
15
+
16
+ const REFRESH_BEFORE_MS = 5 * 60 * 1000
17
+ const ECS_HOST = 'http://169.254.170.2'
18
+ const IMDS_HOST = 'http://169.254.169.254'
19
+ const IMDS_TIMEOUT_MS = 1000
20
+ const CONTAINER_TIMEOUT_MS = 2000
21
+
22
+ interface Cached { readonly credentials: AwsCredentials; readonly expiresAt: number }
23
+
24
+ /**
25
+ * Default AWS credential provider: environment keys, then the ECS container
26
+ * endpoint, then EC2 instance metadata (IMDSv2). Container and IMDS credentials
27
+ * are cached until five minutes before `Expiration`. Profiles and SSO are not read.
28
+ * `storage.s3` and `mail.ses` use it when `credentials` is omitted.
29
+ */
30
+ export function awsCredentials(options: AwsCredentialsOptions = {}): () => Promise<AwsCredentials> {
31
+ const env = options.env ?? readAwsEnv
32
+ const doFetch = options.fetch ?? fetch
33
+ const now = options.now ?? Date.now
34
+ const readFile = options.readFile ?? ((path: string) => fsReadFile(path, 'utf8'))
35
+ let cached: Cached | undefined
36
+ let inflight: Promise<Cached> | undefined
37
+
38
+ async function fetchJson(url: string, init: RequestInit, timeoutMs: number, what: string): Promise<Record<string, unknown>> {
39
+ let res: Response
40
+ try {
41
+ res = await doFetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) })
42
+ } catch (err) {
43
+ throw new Error(`[pipework] ${what} at ${url} could not be reached: ${err instanceof Error ? err.message : String(err)}`)
44
+ }
45
+ if (!res.ok) throw new Error(`[pipework] ${what} at ${url} answered HTTP ${res.status}`)
46
+ const raw = await res.text()
47
+ try {
48
+ const parsed: unknown = JSON.parse(raw)
49
+ if (typeof parsed !== 'object' || parsed === null) throw new Error('not an object')
50
+ return parsed as Record<string, unknown>
51
+ } catch {
52
+ throw new Error(`[pipework] ${what} at ${url} answered HTTP ${res.status} with malformed JSON`)
53
+ }
54
+ }
55
+
56
+ function parse(body: Record<string, unknown>, url: string, what: string): Cached {
57
+ const id = body['AccessKeyId']
58
+ const secret = body['SecretAccessKey']
59
+ const token = body['Token']
60
+ const expiration = body['Expiration']
61
+ const expiresAt = typeof expiration === 'string' ? Date.parse(expiration) : NaN
62
+ if (typeof id !== 'string' || typeof secret !== 'string' || Number.isNaN(expiresAt)) {
63
+ throw new Error(`[pipework] ${what} at ${url} answered HTTP 200 without AccessKeyId, SecretAccessKey and Expiration`)
64
+ }
65
+ return {
66
+ credentials: { accessKeyId: id, secretAccessKey: secret, ...(typeof token === 'string' ? { sessionToken: token } : {}) },
67
+ expiresAt,
68
+ }
69
+ }
70
+
71
+ async function fromContainer(e: AwsEnv): Promise<Cached> {
72
+ const rel = e.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
73
+ const url = rel !== undefined ? `${ECS_HOST}${rel}` : e.AWS_CONTAINER_CREDENTIALS_FULL_URI!
74
+ let token = e.AWS_CONTAINER_AUTHORIZATION_TOKEN
75
+ if (e.AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE !== undefined) {
76
+ token = (await readFile(e.AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE)).trim()
77
+ }
78
+ const headers: Record<string, string> = token !== undefined ? { Authorization: token } : {}
79
+ const what = 'the ECS container credentials endpoint'
80
+ return parse(await fetchJson(url, { method: 'GET', headers }, CONTAINER_TIMEOUT_MS, what), url, what)
81
+ }
82
+
83
+ async function fromImds(): Promise<Cached> {
84
+ const tokenRes = await doFetch(`${IMDS_HOST}/latest/api/token`, {
85
+ method: 'PUT',
86
+ headers: { 'X-aws-ec2-metadata-token-ttl-seconds': '21600' },
87
+ signal: AbortSignal.timeout(IMDS_TIMEOUT_MS),
88
+ })
89
+ if (!tokenRes.ok) throw new Error(`token request answered HTTP ${tokenRes.status}`)
90
+ const token = (await tokenRes.text()).trim()
91
+ const headers = { 'X-aws-ec2-metadata-token': token }
92
+ const listUrl = `${IMDS_HOST}/latest/meta-data/iam/security-credentials/`
93
+ const listRes = await doFetch(listUrl, { method: 'GET', headers, signal: AbortSignal.timeout(IMDS_TIMEOUT_MS) })
94
+ if (!listRes.ok) throw new Error(`role lookup at ${listUrl} answered HTTP ${listRes.status}`)
95
+ const role = (await listRes.text()).split(/\r?\n/)[0]!.trim()
96
+ if (role === '') throw new Error(`no instance role attached (${listUrl} was empty)`)
97
+ const url = `${listUrl}${role}`
98
+ return parse(await fetchJson(url, { method: 'GET', headers }, IMDS_TIMEOUT_MS, 'EC2 instance metadata'), url, 'EC2 instance metadata')
99
+ }
100
+
101
+ async function resolve(): Promise<Cached> {
102
+ const e = env()
103
+ if (e.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI !== undefined || e.AWS_CONTAINER_CREDENTIALS_FULL_URI !== undefined) {
104
+ return fromContainer(e)
105
+ }
106
+ let imds: string
107
+ if (e.AWS_EC2_METADATA_DISABLED?.toLowerCase() === 'true') {
108
+ imds = 'disabled (AWS_EC2_METADATA_DISABLED=true)'
109
+ } else {
110
+ try {
111
+ return await fromImds()
112
+ } catch (err) {
113
+ imds = `failed (${err instanceof Error ? err.message : String(err)})`
114
+ }
115
+ }
116
+ throw new Error(
117
+ '[pipework] No AWS credentials found. Looked at:\n' +
118
+ ' - environment: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (AWS_SESSION_TOKEN optional), not set\n' +
119
+ ' - container endpoint: AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, AWS_CONTAINER_CREDENTIALS_FULL_URI, not set\n' +
120
+ ` - EC2 instance metadata (IMDSv2): ${imds}`,
121
+ )
122
+ }
123
+
124
+ return async () => {
125
+ const e = env()
126
+ const id = e.AWS_ACCESS_KEY_ID
127
+ const secret = e.AWS_SECRET_ACCESS_KEY
128
+ if (id !== undefined || secret !== undefined) {
129
+ if (id === undefined) throw new Error('[pipework] AWS_SECRET_ACCESS_KEY is set but AWS_ACCESS_KEY_ID is missing.')
130
+ if (secret === undefined) throw new Error('[pipework] AWS_ACCESS_KEY_ID is set but AWS_SECRET_ACCESS_KEY is missing.')
131
+ return { accessKeyId: id, secretAccessKey: secret, ...(e.AWS_SESSION_TOKEN !== undefined ? { sessionToken: e.AWS_SESSION_TOKEN } : {}) }
132
+ }
133
+ if (cached !== undefined && now() < cached.expiresAt - REFRESH_BEFORE_MS) return cached.credentials
134
+ if (inflight === undefined) {
135
+ inflight = resolve().then(
136
+ (c) => { cached = c; inflight = undefined; return c },
137
+ (err: unknown) => { inflight = undefined; throw err },
138
+ )
139
+ }
140
+ try {
141
+ return (await inflight).credentials
142
+ } catch (err) {
143
+ if (cached !== undefined && now() < cached.expiresAt) return cached.credentials
144
+ throw err
145
+ }
146
+ }
147
+ }
@@ -0,0 +1,140 @@
1
+ import { createHash, createHmac } from 'node:crypto'
2
+
3
+ export interface AwsCredentials {
4
+ readonly accessKeyId: string
5
+ readonly secretAccessKey: string
6
+ readonly sessionToken?: string
7
+ }
8
+
9
+ export interface SigV4Request {
10
+ readonly method: string
11
+ readonly url: string
12
+ /** Headers to sign and send, other than host and x-amz-date, which are added. */
13
+ readonly headers: Readonly<Record<string, string>>
14
+ readonly body: string | Buffer
15
+ /**
16
+ * The hex SHA-256 of the body, or `UNSIGNED-PAYLOAD`, used in place of hashing `body`.
17
+ * For a streamed S3 body; the caller also sends and signs `x-amz-content-sha256`.
18
+ */
19
+ readonly payloadHash?: string
20
+ readonly region: string
21
+ readonly service: string
22
+ readonly credentials: AwsCredentials
23
+ readonly now?: Date
24
+ }
25
+
26
+ function sha256Hex(data: string | Buffer): string {
27
+ return createHash('sha256').update(data).digest('hex')
28
+ }
29
+
30
+ function hmac(key: string | Buffer, data: string): Buffer {
31
+ return createHmac('sha256', key).update(data).digest()
32
+ }
33
+
34
+ function uriEncode(s: string): string {
35
+ return encodeURIComponent(s).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)
36
+ }
37
+
38
+ function canonicalPathOf(url: URL, service: string): string {
39
+ // S3 encodes each path segment once; every other service encodes the already-encoded path again.
40
+ if (service === 's3') return url.pathname.split('/').map(seg => uriEncode(decodeURIComponent(seg))).join('/')
41
+ return url.pathname.split('/').map(uriEncode).join('/')
42
+ }
43
+
44
+ function canonicalQueryOf(params: Iterable<readonly [string, string]>): string {
45
+ return [...params]
46
+ .map(([k, v]) => [uriEncode(k), uriEncode(v)] as const)
47
+ .sort((a, b) => (a[0] === b[0] ? (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) : (a[0] < b[0] ? -1 : 1)))
48
+ .map(([k, v]) => `${k}=${v}`)
49
+ .join('&')
50
+ }
51
+
52
+ function signingKey(secret: string, day: string, region: string, service: string): Buffer {
53
+ return hmac(hmac(hmac(hmac(`AWS4${secret}`, day), region), service), 'aws4_request')
54
+ }
55
+
56
+ function amzDate(d: Date): string {
57
+ return d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '')
58
+ }
59
+
60
+ /**
61
+ * Signs a request with AWS Signature Version 4 and returns the headers to send:
62
+ * the given ones, `x-amz-date`, `x-amz-security-token` when the credentials carry
63
+ * a session token, and `authorization`. `host` is signed but not returned; the
64
+ * HTTP client sets it from the URL.
65
+ */
66
+ export function signV4(req: SigV4Request): Record<string, string> {
67
+ const url = new URL(req.url)
68
+ const stamp = amzDate(req.now ?? new Date())
69
+ const day = stamp.slice(0, 8)
70
+
71
+ const headers: Record<string, string> = {}
72
+ for (const [name, value] of Object.entries(req.headers)) headers[name.toLowerCase()] = value.trim().replace(/\s+/g, ' ')
73
+ headers['x-amz-date'] = stamp
74
+ if (req.credentials.sessionToken !== undefined) headers['x-amz-security-token'] = req.credentials.sessionToken
75
+
76
+ const signed: Record<string, string> = { ...headers, host: url.host }
77
+ const names = Object.keys(signed).sort()
78
+ const canonicalHeaders = names.map(n => `${n}:${signed[n]!}\n`).join('')
79
+ const signedHeaders = names.join(';')
80
+
81
+ const canonicalRequest = [
82
+ req.method.toUpperCase(), canonicalPathOf(url, req.service), canonicalQueryOf(url.searchParams.entries()),
83
+ canonicalHeaders, signedHeaders, req.payloadHash ?? sha256Hex(req.body),
84
+ ].join('\n')
85
+
86
+ const scope = `${day}/${req.region}/${req.service}/aws4_request`
87
+ const stringToSign = ['AWS4-HMAC-SHA256', stamp, scope, sha256Hex(canonicalRequest)].join('\n')
88
+ const signature = createHmac('sha256', signingKey(req.credentials.secretAccessKey, day, req.region, req.service))
89
+ .update(stringToSign).digest('hex')
90
+
91
+ return {
92
+ ...headers,
93
+ authorization:
94
+ `AWS4-HMAC-SHA256 Credential=${req.credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
95
+ }
96
+ }
97
+
98
+ export interface PresignRequest {
99
+ readonly method: string
100
+ /** The full URL, query included (e.g. `response-content-disposition`); it is signed with the rest. */
101
+ readonly url: string
102
+ readonly region: string
103
+ readonly service: string
104
+ readonly credentials: AwsCredentials
105
+ /** 1 to 604800 (seven days). */
106
+ readonly expiresInSeconds: number
107
+ readonly now?: Date
108
+ }
109
+
110
+ /**
111
+ * Returns `url` with SigV4 query-string signing added (`X-Amz-Algorithm`, `-Credential`, `-Date`,
112
+ * `-Expires`, `-SignedHeaders=host`, `-Security-Token` for a session token, `-Signature`), so a client
113
+ * with no credentials can make the one request until it expires. The payload is `UNSIGNED-PAYLOAD`.
114
+ */
115
+ export function presignV4(req: PresignRequest): string {
116
+ if (!Number.isInteger(req.expiresInSeconds) || req.expiresInSeconds < 1 || req.expiresInSeconds > 604800) {
117
+ throw new Error('[pipework] presigned URL expiry must be an integer from 1 to 604800 seconds.')
118
+ }
119
+ const url = new URL(req.url)
120
+ const stamp = amzDate(req.now ?? new Date())
121
+ const day = stamp.slice(0, 8)
122
+ const scope = `${day}/${req.region}/${req.service}/aws4_request`
123
+ url.searchParams.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256')
124
+ url.searchParams.set('X-Amz-Credential', `${req.credentials.accessKeyId}/${scope}`)
125
+ url.searchParams.set('X-Amz-Date', stamp)
126
+ url.searchParams.set('X-Amz-Expires', String(req.expiresInSeconds))
127
+ url.searchParams.set('X-Amz-SignedHeaders', 'host')
128
+ if (req.credentials.sessionToken !== undefined) url.searchParams.set('X-Amz-Security-Token', req.credentials.sessionToken)
129
+
130
+ const canonicalRequest = [
131
+ req.method.toUpperCase(), canonicalPathOf(url, req.service), canonicalQueryOf(url.searchParams.entries()),
132
+ `host:${url.host}\n`, 'host', 'UNSIGNED-PAYLOAD',
133
+ ].join('\n')
134
+ const stringToSign = ['AWS4-HMAC-SHA256', stamp, scope, sha256Hex(canonicalRequest)].join('\n')
135
+ const signature = createHmac('sha256', signingKey(req.credentials.secretAccessKey, day, req.region, req.service))
136
+ .update(stringToSign).digest('hex')
137
+ // Keep the query exactly as signed: rebuild it with the same encoding the canonical form used.
138
+ const query = canonicalQueryOf(url.searchParams.entries())
139
+ return `${url.origin}${url.pathname}?${query}&X-Amz-Signature=${signature}`
140
+ }
@@ -0,0 +1,58 @@
1
+ import { getContext } from '../../request/context/store.js'
2
+ import type { CacheConfig, Cache, CacheStats } from './types.js'
3
+ import { loadWithDb, createEmptyEntry, isExpired, getOrLoad, preloadEntry, type CacheEntryState } from './entry.js'
4
+
5
+ export function createCache<T>(config: CacheConfig<T>): Cache<T> {
6
+ const entry = createEmptyEntry<T>()
7
+ let lastLoadAt: number | null = null
8
+ let lastLoadDurationMs: number | null = null
9
+
10
+ async function doLoad(e: CacheEntryState<T>): Promise<T> {
11
+ const start = Date.now()
12
+ const result = await loadWithDb(config.database, config.name, getContext()?.tenant ?? null, db => config.load(db))
13
+ lastLoadDurationMs = Date.now() - start
14
+ lastLoadAt = Date.now()
15
+ e.loadedAt = Date.now()
16
+ e.data = result
17
+ e.loadPromise = null
18
+ return result
19
+ }
20
+
21
+ return {
22
+ async get(): Promise<T> {
23
+ return getOrLoad(entry, config.ttlMs, doLoad)
24
+ },
25
+
26
+ invalidate(): void {
27
+ entry.data = undefined
28
+ entry.loadedAt = null
29
+ entry.loadPromise = null
30
+ },
31
+
32
+ stats(): CacheStats {
33
+ let status: CacheStats['status']
34
+ if (entry.loadPromise !== null) {
35
+ status = 'loading'
36
+ } else if (entry.data === undefined) {
37
+ status = 'empty'
38
+ } else if (isExpired(entry, config.ttlMs)) {
39
+ status = 'stale'
40
+ } else {
41
+ status = 'loaded'
42
+ }
43
+
44
+ return {
45
+ name: config.name,
46
+ status,
47
+ hits: entry.hits,
48
+ misses: entry.misses,
49
+ lastLoadAt,
50
+ lastLoadDurationMs,
51
+ }
52
+ },
53
+
54
+ async preload(): Promise<void> {
55
+ return preloadEntry(entry, config.ttlMs, doLoad)
56
+ },
57
+ }
58
+ }