pipework 0.251.0 → 0.252.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (730) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/core/config/load.d.ts +2 -0
  3. package/dist/core/config/load.d.ts.map +1 -1
  4. package/dist/core/config/load.js +1 -0
  5. package/dist/core/config/load.js.map +1 -1
  6. package/dist/core/config/namespace.d.ts +1 -0
  7. package/dist/core/config/namespace.d.ts.map +1 -1
  8. package/dist/core/config/schema.d.ts +2 -0
  9. package/dist/core/config/schema.d.ts.map +1 -1
  10. package/dist/core/config/schema.js +1 -0
  11. package/dist/core/config/schema.js.map +1 -1
  12. package/dist/core/surface/types.d.ts +1 -0
  13. package/dist/core/surface/types.d.ts.map +1 -1
  14. package/dist/data/db/namespace.d.ts +3 -0
  15. package/dist/data/db/namespace.d.ts.map +1 -1
  16. package/dist/data/db/namespace.js +3 -0
  17. package/dist/data/db/namespace.js.map +1 -1
  18. package/dist/data/db/pool.d.ts.map +1 -1
  19. package/dist/data/db/pool.js +6 -1
  20. package/dist/data/db/pool.js.map +1 -1
  21. package/dist/data/db/query-label.d.ts +5 -0
  22. package/dist/data/db/query-label.d.ts.map +1 -0
  23. package/dist/data/db/query-label.js +11 -0
  24. package/dist/data/db/query-label.js.map +1 -0
  25. package/dist/data/db/query-watch.d.ts +21 -0
  26. package/dist/data/db/query-watch.d.ts.map +1 -0
  27. package/dist/data/db/query-watch.js +90 -0
  28. package/dist/data/db/query-watch.js.map +1 -0
  29. package/dist/data/query/postgres-js/driver.js +1 -1
  30. package/dist/data/query/postgres-js/driver.js.map +1 -1
  31. package/dist/data/query/postgres-js/session.d.ts +5 -1
  32. package/dist/data/query/postgres-js/session.d.ts.map +1 -1
  33. package/dist/data/query/postgres-js/session.js +16 -11
  34. package/dist/data/query/postgres-js/session.js.map +1 -1
  35. package/dist/data/query/utils.d.ts +2 -0
  36. package/dist/data/query/utils.d.ts.map +1 -1
  37. package/dist/data/query/utils.js.map +1 -1
  38. package/dist/infra/credits/schema.d.ts +1 -1
  39. package/dist/test/plugin.d.ts.map +1 -1
  40. package/dist/test/plugin.js +10 -4
  41. package/dist/test/plugin.js.map +1 -1
  42. package/package.json +4 -3
  43. package/src/async/jobs/abortable-delay.ts +18 -0
  44. package/src/async/jobs/claims.ts +52 -0
  45. package/src/async/jobs/cron.ts +179 -0
  46. package/src/async/jobs/errors.ts +56 -0
  47. package/src/async/jobs/exclusion.ts +117 -0
  48. package/src/async/jobs/execute.ts +219 -0
  49. package/src/async/jobs/index.ts +10 -0
  50. package/src/async/jobs/listener.ts +135 -0
  51. package/src/async/jobs/namespace.ts +20 -0
  52. package/src/async/jobs/queue-door.ts +44 -0
  53. package/src/async/jobs/queue.ts +1012 -0
  54. package/src/async/jobs/run-script.ts +67 -0
  55. package/src/async/jobs/scheduler.ts +218 -0
  56. package/src/async/jobs/schedules-table.ts +37 -0
  57. package/src/async/jobs/serialize-error.ts +48 -0
  58. package/src/async/jobs/table.ts +191 -0
  59. package/src/async/pipeline/define.ts +53 -0
  60. package/src/async/pipeline/errors.ts +35 -0
  61. package/src/async/pipeline/execute.ts +53 -0
  62. package/src/async/pipeline/index.ts +13 -0
  63. package/src/async/pipeline/namespace.ts +12 -0
  64. package/src/async/pipeline/resume.ts +67 -0
  65. package/src/async/pipeline/run-steps.ts +98 -0
  66. package/src/async/pipeline/types.ts +57 -0
  67. package/src/async/state-machine/define.ts +64 -0
  68. package/src/async/state-machine/errors.ts +29 -0
  69. package/src/async/state-machine/index.ts +3 -0
  70. package/src/async/state-machine/namespace.ts +12 -0
  71. package/src/async/state-machine/transition.ts +90 -0
  72. package/src/async/state-machine/types.ts +37 -0
  73. package/src/async/steps/errors.ts +31 -0
  74. package/src/async/steps/graph.ts +1214 -0
  75. package/src/async/steps/hash.ts +40 -0
  76. package/src/async/steps/index.ts +7 -0
  77. package/src/async/steps/namespace.ts +7 -0
  78. package/src/async/steps/table.ts +97 -0
  79. package/src/async/steps/types.ts +318 -0
  80. package/src/audit/chain/index.ts +9 -0
  81. package/src/audit/chain/install.ts +113 -0
  82. package/src/audit/chain/retire.ts +52 -0
  83. package/src/audit/chain/verify.ts +257 -0
  84. package/src/audit/query/cursor.ts +37 -0
  85. package/src/audit/query/index.ts +9 -0
  86. package/src/audit/query/query.ts +223 -0
  87. package/src/audit/schema/audit-record-fields.ts +68 -0
  88. package/src/audit/schema/audit-record.ts +23 -0
  89. package/src/auth/auth/audit.ts +31 -0
  90. package/src/auth/auth/chain.ts +84 -0
  91. package/src/auth/auth/cookie.ts +136 -0
  92. package/src/auth/auth/email-links.ts +311 -0
  93. package/src/auth/auth/guard.ts +128 -0
  94. package/src/auth/auth/index.ts +18 -0
  95. package/src/auth/auth/multi-org.ts +337 -0
  96. package/src/auth/auth/namespace.ts +34 -0
  97. package/src/auth/auth/parse-expiry.ts +13 -0
  98. package/src/auth/auth/passwords.ts +110 -0
  99. package/src/auth/auth/schema.ts +110 -0
  100. package/src/auth/auth/sessions.ts +565 -0
  101. package/src/auth/auth/sign-in.ts +156 -0
  102. package/src/auth/auth/signing-keys.ts +69 -0
  103. package/src/auth/auth/types.ts +26 -0
  104. package/src/auth/auth/uuid.ts +10 -0
  105. package/src/auth/rbac/cache.ts +55 -0
  106. package/src/auth/rbac/check.ts +30 -0
  107. package/src/auth/rbac/create.ts +35 -0
  108. package/src/auth/rbac/index.ts +5 -0
  109. package/src/auth/rbac/middleware.ts +20 -0
  110. package/src/auth/rbac/namespace.ts +15 -0
  111. package/src/auth/rbac/resolve.ts +31 -0
  112. package/src/auth/rbac/schema.sql +31 -0
  113. package/src/auth/rbac/seed.ts +67 -0
  114. package/src/auth/rbac/tables.ts +32 -0
  115. package/src/auth/rbac/types.ts +65 -0
  116. package/src/auth/tenant/extract.ts +33 -0
  117. package/src/auth/tenant/in-tenant.ts +49 -0
  118. package/src/auth/tenant/index.ts +7 -0
  119. package/src/auth/tenant/namespace.ts +23 -0
  120. package/src/auth/tenant/propagate.ts +20 -0
  121. package/src/auth/tenant/rls.ts +120 -0
  122. package/src/auth/tenant/scope.ts +33 -0
  123. package/src/auth/tenant/scoped-db.ts +581 -0
  124. package/src/auth/tenant/types.ts +14 -0
  125. package/src/auth/tenant/validate.ts +27 -0
  126. package/src/cli/commands/add.ts +158 -0
  127. package/src/cli/commands/alerts.ts +98 -0
  128. package/src/cli/commands/check.ts +1418 -0
  129. package/src/cli/commands/codemod.ts +78 -0
  130. package/src/cli/commands/db.ts +356 -0
  131. package/src/cli/commands/dev.ts +101 -0
  132. package/src/cli/commands/generate.ts +75 -0
  133. package/src/cli/commands/init.ts +36 -0
  134. package/src/cli/commands/install.ts +28 -0
  135. package/src/cli/commands/lint.ts +62 -0
  136. package/src/cli/commands/migrate.ts +132 -0
  137. package/src/cli/commands/openapi.ts +63 -0
  138. package/src/cli/commands/remove.ts +72 -0
  139. package/src/cli/commands/run.ts +50 -0
  140. package/src/cli/commands/scope.ts +54 -0
  141. package/src/cli/commands/serve.ts +68 -0
  142. package/src/cli/commands/shard.ts +31 -0
  143. package/src/cli/commands/shared/config-pipework-version.ts +28 -0
  144. package/src/cli/commands/shared/find-config.ts +22 -0
  145. package/src/cli/commands/shared/load-config.ts +35 -0
  146. package/src/cli/commands/shared/pnpm-install.ts +95 -0
  147. package/src/cli/commands/shared/report-removed.ts +8 -0
  148. package/src/cli/commands/sync.ts +32 -0
  149. package/src/cli/commands/test.ts +307 -0
  150. package/src/cli/commands/upgrade.ts +48 -0
  151. package/src/cli/dev-bootstrap.ts +24 -0
  152. package/src/cli/errors.ts +10 -0
  153. package/src/cli/index.ts +303 -0
  154. package/src/cli/output.ts +17 -0
  155. package/src/cli/port.ts +15 -0
  156. package/src/cli/surface-kinds.ts +20 -0
  157. package/src/client/index.ts +4 -0
  158. package/src/client/poll.ts +130 -0
  159. package/src/client/report-fault.ts +119 -0
  160. package/src/core/config/app-pool.ts +19 -0
  161. package/src/core/config/aws-env.ts +29 -0
  162. package/src/core/config/base-dir.ts +24 -0
  163. package/src/core/config/discover.ts +64 -0
  164. package/src/core/config/env-resolve.ts +87 -0
  165. package/src/core/config/env-types.ts +116 -0
  166. package/src/core/config/env.ts +55 -0
  167. package/src/core/config/errors.ts +9 -0
  168. package/src/core/config/import-config.ts +40 -0
  169. package/src/core/config/index.ts +8 -0
  170. package/src/core/config/load.ts +417 -0
  171. package/src/core/config/namespace.ts +14 -0
  172. package/src/core/config/resolve.ts +195 -0
  173. package/src/core/config/schema.ts +488 -0
  174. package/src/core/config/statement-observer.ts +48 -0
  175. package/src/core/config/ts-inline-hooks.ts +75 -0
  176. package/src/core/config/ts-register.ts +4 -0
  177. package/src/core/config/ts-resolve-hooks.ts +101 -0
  178. package/src/core/errors/base.ts +49 -0
  179. package/src/core/errors/http.ts +146 -0
  180. package/src/core/errors/index.ts +2 -0
  181. package/src/core/errors/namespace.ts +31 -0
  182. package/src/core/lifecycle/drain.ts +33 -0
  183. package/src/core/lifecycle/health.ts +72 -0
  184. package/src/core/lifecycle/index.ts +4 -0
  185. package/src/core/lifecycle/namespace.ts +7 -0
  186. package/src/core/lifecycle/startup.ts +102 -0
  187. package/src/core/lifecycle/types.ts +36 -0
  188. package/src/core/logging/context.ts +60 -0
  189. package/src/core/logging/error-chain.ts +68 -0
  190. package/src/core/logging/index.ts +5 -0
  191. package/src/core/logging/logger.ts +65 -0
  192. package/src/core/logging/namespace.ts +16 -0
  193. package/src/core/logging/proxy.ts +22 -0
  194. package/src/core/logging/redact-match.ts +24 -0
  195. package/src/core/logging/types.ts +47 -0
  196. package/src/core/pipework.ts +566 -0
  197. package/src/core/runtime/broker.ts +69 -0
  198. package/src/core/surface/http.ts +133 -0
  199. package/src/core/surface/index.ts +11 -0
  200. package/src/core/surface/job-timeout.ts +40 -0
  201. package/src/core/surface/namespace.ts +13 -0
  202. package/src/core/surface/script.ts +33 -0
  203. package/src/core/surface/start.ts +142 -0
  204. package/src/core/surface/types.ts +225 -0
  205. package/src/core/surface/validate.ts +88 -0
  206. package/src/core/surface/worker.ts +528 -0
  207. package/src/core/validation/connectivity.ts +49 -0
  208. package/src/core/validation/domain.ts +99 -0
  209. package/src/core/validation/index.ts +5 -0
  210. package/src/core/validation/queue-tables.ts +312 -0
  211. package/src/core/validation/rls-role.ts +138 -0
  212. package/src/core/validation/startup.ts +51 -0
  213. package/src/core/validation/surfaces.ts +65 -0
  214. package/src/core/validation/types.ts +25 -0
  215. package/src/data/db/advisory-lock.ts +55 -0
  216. package/src/data/db/aggregates.ts +172 -0
  217. package/src/data/db/array-elements.ts +111 -0
  218. package/src/data/db/bulk-set.ts +53 -0
  219. package/src/data/db/col/index.ts +14 -0
  220. package/src/data/db/composed.ts +53 -0
  221. package/src/data/db/db.ts +276 -0
  222. package/src/data/db/excluded.ts +16 -0
  223. package/src/data/db/expressions.ts +473 -0
  224. package/src/data/db/filter.ts +90 -0
  225. package/src/data/db/function-source.ts +114 -0
  226. package/src/data/db/identifiers.ts +16 -0
  227. package/src/data/db/idx/index.ts +9 -0
  228. package/src/data/db/in-list.ts +49 -0
  229. package/src/data/db/index.ts +9 -0
  230. package/src/data/db/insert-split.ts +137 -0
  231. package/src/data/db/instrumentation.ts +82 -0
  232. package/src/data/db/maintenance.ts +28 -0
  233. package/src/data/db/namespace.ts +107 -0
  234. package/src/data/db/owner-labels.ts +9 -0
  235. package/src/data/db/pool.ts +187 -0
  236. package/src/data/db/query-label.ts +13 -0
  237. package/src/data/db/query-watch.ts +116 -0
  238. package/src/data/db/raw-client-warning.ts +37 -0
  239. package/src/data/db/readonly.ts +23 -0
  240. package/src/data/db/reservation.ts +151 -0
  241. package/src/data/db/schema-features.ts +14 -0
  242. package/src/data/db/serializable.ts +83 -0
  243. package/src/data/db/sql.ts +1 -0
  244. package/src/data/db/timeouts.ts +76 -0
  245. package/src/data/db/types.ts +18 -0
  246. package/src/data/db/unbound.ts +33 -0
  247. package/src/data/db/values.ts +29 -0
  248. package/src/data/domain/brand.ts +15 -0
  249. package/src/data/domain/build-factory.ts +193 -0
  250. package/src/data/domain/build-schema.ts +161 -0
  251. package/src/data/domain/build-table.ts +355 -0
  252. package/src/data/domain/define.ts +239 -0
  253. package/src/data/domain/external-registry.ts +34 -0
  254. package/src/data/domain/external.ts +77 -0
  255. package/src/data/domain/field.ts +279 -0
  256. package/src/data/domain/index.ts +13 -0
  257. package/src/data/domain/naming.ts +6 -0
  258. package/src/data/domain/projections.ts +57 -0
  259. package/src/data/domain/trace-safe-projection.ts +31 -0
  260. package/src/data/domain/traits.ts +108 -0
  261. package/src/data/domain/types.ts +289 -0
  262. package/src/data/jsonb/composed.ts +18 -0
  263. package/src/data/jsonb/index.ts +1 -0
  264. package/src/data/jsonb/namespace.ts +30 -0
  265. package/src/data/jsonb/query.ts +86 -0
  266. package/src/data/migrate/append-only-generate.ts +47 -0
  267. package/src/data/migrate/applied.ts +47 -0
  268. package/src/data/migrate/apply.ts +309 -0
  269. package/src/data/migrate/check.ts +119 -0
  270. package/src/data/migrate/diff.ts +357 -0
  271. package/src/data/migrate/door-generate.ts +130 -0
  272. package/src/data/migrate/framework-rls.ts +58 -0
  273. package/src/data/migrate/generate.ts +201 -0
  274. package/src/data/migrate/history.ts +215 -0
  275. package/src/data/migrate/index.ts +9 -0
  276. package/src/data/migrate/internal-definitions.ts +175 -0
  277. package/src/data/migrate/post-process.ts +73 -0
  278. package/src/data/migrate/pragma.ts +73 -0
  279. package/src/data/migrate/provision-framework.ts +147 -0
  280. package/src/data/migrate/rls-generate.ts +61 -0
  281. package/src/data/migrate/run.ts +124 -0
  282. package/src/data/migrate/safety.ts +167 -0
  283. package/src/data/migrate/snapshot.ts +368 -0
  284. package/src/data/migrate/sql-emit.ts +392 -0
  285. package/src/data/migrate/statements.ts +312 -0
  286. package/src/data/query/alias.ts +126 -0
  287. package/src/data/query/batch.ts +8 -0
  288. package/src/data/query/cache/core/cache.ts +78 -0
  289. package/src/data/query/cache/core/index.ts +1 -0
  290. package/src/data/query/cache/core/types.ts +29 -0
  291. package/src/data/query/cache/readme.md +230 -0
  292. package/src/data/query/casing.ts +76 -0
  293. package/src/data/query/collation.ts +11 -0
  294. package/src/data/query/column-builder.ts +370 -0
  295. package/src/data/query/column.ts +150 -0
  296. package/src/data/query/entity.ts +42 -0
  297. package/src/data/query/errors.ts +33 -0
  298. package/src/data/query/index.ts +14 -0
  299. package/src/data/query/logger.ts +47 -0
  300. package/src/data/query/operations.ts +45 -0
  301. package/src/data/query/pg-core/alias.ts +12 -0
  302. package/src/data/query/pg-core/checks.ts +32 -0
  303. package/src/data/query/pg-core/columns/all.ts +71 -0
  304. package/src/data/query/pg-core/columns/bigint.ts +110 -0
  305. package/src/data/query/pg-core/columns/bigserial.ts +128 -0
  306. package/src/data/query/pg-core/columns/boolean.ts +43 -0
  307. package/src/data/query/pg-core/columns/char.ts +85 -0
  308. package/src/data/query/pg-core/columns/cidr.ts +43 -0
  309. package/src/data/query/pg-core/columns/common.ts +366 -0
  310. package/src/data/query/pg-core/columns/custom.ts +232 -0
  311. package/src/data/query/pg-core/columns/date.common.ts +15 -0
  312. package/src/data/query/pg-core/columns/date.ts +112 -0
  313. package/src/data/query/pg-core/columns/double-precision.ts +55 -0
  314. package/src/data/query/pg-core/columns/enum.ts +201 -0
  315. package/src/data/query/pg-core/columns/index.ts +35 -0
  316. package/src/data/query/pg-core/columns/inet.ts +43 -0
  317. package/src/data/query/pg-core/columns/int.common.ts +57 -0
  318. package/src/data/query/pg-core/columns/integer.ts +53 -0
  319. package/src/data/query/pg-core/columns/interval.ts +83 -0
  320. package/src/data/query/pg-core/columns/json.ts +58 -0
  321. package/src/data/query/pg-core/columns/jsonb.ts +56 -0
  322. package/src/data/query/pg-core/columns/line.ts +117 -0
  323. package/src/data/query/pg-core/columns/macaddr.ts +43 -0
  324. package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
  325. package/src/data/query/pg-core/columns/numeric.ts +227 -0
  326. package/src/data/query/pg-core/columns/point.ts +127 -0
  327. package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
  328. package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
  329. package/src/data/query/pg-core/columns/real.ts +58 -0
  330. package/src/data/query/pg-core/columns/serial.ts +55 -0
  331. package/src/data/query/pg-core/columns/smallint.ts +53 -0
  332. package/src/data/query/pg-core/columns/smallserial.ts +60 -0
  333. package/src/data/query/pg-core/columns/text.ts +67 -0
  334. package/src/data/query/pg-core/columns/time.ts +72 -0
  335. package/src/data/query/pg-core/columns/timestamp.ts +163 -0
  336. package/src/data/query/pg-core/columns/uuid.ts +51 -0
  337. package/src/data/query/pg-core/columns/varchar.ts +89 -0
  338. package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
  339. package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
  340. package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
  341. package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
  342. package/src/data/query/pg-core/db.ts +674 -0
  343. package/src/data/query/pg-core/dialect.ts +1404 -0
  344. package/src/data/query/pg-core/expressions.ts +25 -0
  345. package/src/data/query/pg-core/foreign-keys.ts +130 -0
  346. package/src/data/query/pg-core/index.ts +21 -0
  347. package/src/data/query/pg-core/indexes.ts +276 -0
  348. package/src/data/query/pg-core/policies.ts +56 -0
  349. package/src/data/query/pg-core/primary-keys.ts +62 -0
  350. package/src/data/query/pg-core/query-builders/count.ts +86 -0
  351. package/src/data/query/pg-core/query-builders/delete.ts +293 -0
  352. package/src/data/query/pg-core/query-builders/index.ts +7 -0
  353. package/src/data/query/pg-core/query-builders/insert.ts +452 -0
  354. package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
  355. package/src/data/query/pg-core/query-builders/query.ts +157 -0
  356. package/src/data/query/pg-core/query-builders/raw.ts +49 -0
  357. package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
  358. package/src/data/query/pg-core/query-builders/select.ts +1346 -0
  359. package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
  360. package/src/data/query/pg-core/query-builders/update.ts +626 -0
  361. package/src/data/query/pg-core/roles.ts +41 -0
  362. package/src/data/query/pg-core/schema.ts +72 -0
  363. package/src/data/query/pg-core/sequence.ts +41 -0
  364. package/src/data/query/pg-core/session.ts +292 -0
  365. package/src/data/query/pg-core/subquery.ts +46 -0
  366. package/src/data/query/pg-core/table.ts +252 -0
  367. package/src/data/query/pg-core/unique-constraint.ts +73 -0
  368. package/src/data/query/pg-core/utils/array.ts +95 -0
  369. package/src/data/query/pg-core/utils/index.ts +1 -0
  370. package/src/data/query/pg-core/utils.ts +104 -0
  371. package/src/data/query/pg-core/view-base.ts +14 -0
  372. package/src/data/query/pg-core/view-common.ts +1 -0
  373. package/src/data/query/pg-core/view.ts +435 -0
  374. package/src/data/query/pg-core/with-builder.ts +41 -0
  375. package/src/data/query/postgres-js/README.md +46 -0
  376. package/src/data/query/postgres-js/array-parser.ts +45 -0
  377. package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
  378. package/src/data/query/postgres-js/driver.ts +163 -0
  379. package/src/data/query/postgres-js/index.ts +2 -0
  380. package/src/data/query/postgres-js/session.ts +234 -0
  381. package/src/data/query/primary-key.ts +11 -0
  382. package/src/data/query/query-builders/query-builder.ts +19 -0
  383. package/src/data/query/query-builders/select.types.ts +175 -0
  384. package/src/data/query/query-promise.ts +35 -0
  385. package/src/data/query/relations.ts +725 -0
  386. package/src/data/query/runnable-query.ts +12 -0
  387. package/src/data/query/selection-proxy.ts +121 -0
  388. package/src/data/query/session.ts +8 -0
  389. package/src/data/query/sql/expressions/conditions.ts +741 -0
  390. package/src/data/query/sql/expressions/index.ts +2 -0
  391. package/src/data/query/sql/expressions/select.ts +45 -0
  392. package/src/data/query/sql/functions/aggregate.ts +129 -0
  393. package/src/data/query/sql/functions/index.ts +2 -0
  394. package/src/data/query/sql/functions/vector.ts +178 -0
  395. package/src/data/query/sql/index.ts +3 -0
  396. package/src/data/query/sql/sql.ts +794 -0
  397. package/src/data/query/subquery.ts +53 -0
  398. package/src/data/query/table.ts +208 -0
  399. package/src/data/query/table.utils.ts +2 -0
  400. package/src/data/query/tracing-utils.ts +3 -0
  401. package/src/data/query/tracing.ts +11 -0
  402. package/src/data/query/utils.ts +357 -0
  403. package/src/data/query/version.ts +4 -0
  404. package/src/data/query/view-common.ts +1 -0
  405. package/src/data/schema/branded.ts +13 -0
  406. package/src/data/schema/generated/coerce.ts +5 -0
  407. package/src/data/schema/generated/combinator.ts +6 -0
  408. package/src/data/schema/generated/encoding.ts +5 -0
  409. package/src/data/schema/generated/error.ts +6 -0
  410. package/src/data/schema/generated/format.ts +5 -0
  411. package/src/data/schema/generated/type-constructor.ts +10 -0
  412. package/src/data/schema/generated/type.ts +6 -0
  413. package/src/data/schema/index.ts +6 -0
  414. package/src/data/schema/json-schema.ts +33 -0
  415. package/src/data/schema/namespace.ts +55 -0
  416. package/src/data/schema/parse.ts +39 -0
  417. package/src/data/schema/types.ts +39 -0
  418. package/src/data/temporal/append-only.ts +134 -0
  419. package/src/data/temporal/close.ts +27 -0
  420. package/src/data/temporal/columns.ts +10 -0
  421. package/src/data/temporal/definition-queries.ts +70 -0
  422. package/src/data/temporal/errors.ts +10 -0
  423. package/src/data/temporal/get-current.ts +22 -0
  424. package/src/data/temporal/index.ts +9 -0
  425. package/src/data/temporal/namespace.ts +36 -0
  426. package/src/data/temporal/query.ts +25 -0
  427. package/src/data/temporal/resolve.ts +17 -0
  428. package/src/data/temporal/revise.ts +67 -0
  429. package/src/data/temporal/types.ts +22 -0
  430. package/src/data/valve/artifact.ts +33 -0
  431. package/src/data/valve/contract-diff.ts +21 -0
  432. package/src/data/valve/cross.ts +57 -0
  433. package/src/data/valve/crossing-ledger.ts +50 -0
  434. package/src/data/valve/decision.ts +44 -0
  435. package/src/data/valve/durable-crossing.ts +52 -0
  436. package/src/data/valve/execute-crossing.ts +77 -0
  437. package/src/data/valve/faucet.ts +56 -0
  438. package/src/data/valve/gate.ts +114 -0
  439. package/src/data/valve/graph.ts +77 -0
  440. package/src/data/valve/index.ts +53 -0
  441. package/src/data/valve/namespace.ts +57 -0
  442. package/src/data/valve/registry.ts +50 -0
  443. package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
  444. package/src/data/valve/schema/valve-crossing.ts +9 -0
  445. package/src/data/valve/supply.ts +154 -0
  446. package/src/data/valve/types.ts +143 -0
  447. package/src/data/vector/columns.ts +27 -0
  448. package/src/data/vector/index.ts +4 -0
  449. package/src/data/vector/namespace.ts +33 -0
  450. package/src/data/vector/query.ts +85 -0
  451. package/src/data/vector/validate.ts +16 -0
  452. package/src/data/write/audit/actor.ts +15 -0
  453. package/src/data/write/audit/audit-columns.ts +18 -0
  454. package/src/data/write/audit/audit-select.ts +111 -0
  455. package/src/data/write/audit/bound-transaction.ts +14 -0
  456. package/src/data/write/audit/cte-compile.ts +32 -0
  457. package/src/data/write/audit/decode-returning.ts +47 -0
  458. package/src/data/write/audit/final-select.ts +43 -0
  459. package/src/data/write/audit/infra-audit.ts +36 -0
  460. package/src/data/write/audit/primary-key.ts +65 -0
  461. package/src/data/write/audit/projection.ts +41 -0
  462. package/src/data/write/audit/read-touches.ts +86 -0
  463. package/src/data/write/audit/touch-match.ts +78 -0
  464. package/src/data/write/audit/verb-delete.ts +62 -0
  465. package/src/data/write/audit/verb-insert.ts +36 -0
  466. package/src/data/write/audit/verb-select.ts +47 -0
  467. package/src/data/write/audit/verb-update.ts +60 -0
  468. package/src/data/write/audit/verb-upsert.ts +104 -0
  469. package/src/data/write/conflict.ts +43 -0
  470. package/src/data/write/delete.ts +38 -0
  471. package/src/data/write/execute.ts +243 -0
  472. package/src/data/write/index.ts +11 -0
  473. package/src/data/write/insert.ts +63 -0
  474. package/src/data/write/state.ts +43 -0
  475. package/src/data/write/update.ts +54 -0
  476. package/src/data/write/upsert.ts +58 -0
  477. package/src/data/write/values.ts +19 -0
  478. package/src/index.ts +230 -0
  479. package/src/infra/access-links/code.ts +70 -0
  480. package/src/infra/access-links/handle.ts +892 -0
  481. package/src/infra/access-links/index.ts +9 -0
  482. package/src/infra/access-links/namespace.ts +30 -0
  483. package/src/infra/access-links/person-check.ts +177 -0
  484. package/src/infra/access-links/schema.ts +71 -0
  485. package/src/infra/alerts/async-sources.ts +231 -0
  486. package/src/infra/alerts/audit.ts +36 -0
  487. package/src/infra/alerts/beat.ts +123 -0
  488. package/src/infra/alerts/client-faults.ts +111 -0
  489. package/src/infra/alerts/faults.ts +156 -0
  490. package/src/infra/alerts/handle.ts +778 -0
  491. package/src/infra/alerts/http.ts +192 -0
  492. package/src/infra/alerts/index.ts +12 -0
  493. package/src/infra/alerts/infra-sources.ts +180 -0
  494. package/src/infra/alerts/namespace.ts +81 -0
  495. package/src/infra/alerts/redact.ts +21 -0
  496. package/src/infra/alerts/request-faults.ts +41 -0
  497. package/src/infra/alerts/schema.ts +124 -0
  498. package/src/infra/alerts/sinks.ts +125 -0
  499. package/src/infra/alerts/source-options.ts +26 -0
  500. package/src/infra/alerts/sources.ts +503 -0
  501. package/src/infra/alerts/subsystem-sources.ts +137 -0
  502. package/src/infra/audit/emit.ts +87 -0
  503. package/src/infra/audit/index.ts +2 -0
  504. package/src/infra/audit/namespace.ts +22 -0
  505. package/src/infra/audit/schema.sql +35 -0
  506. package/src/infra/audit/types.ts +18 -0
  507. package/src/infra/aws/credentials.ts +147 -0
  508. package/src/infra/aws/sigv4.ts +140 -0
  509. package/src/infra/cache/create.ts +58 -0
  510. package/src/infra/cache/entry.ts +81 -0
  511. package/src/infra/cache/index.ts +4 -0
  512. package/src/infra/cache/namespace.ts +10 -0
  513. package/src/infra/cache/tenant.ts +93 -0
  514. package/src/infra/cache/types.ts +24 -0
  515. package/src/infra/crawl/audit.ts +35 -0
  516. package/src/infra/crawl/errors.ts +25 -0
  517. package/src/infra/crawl/handle.ts +693 -0
  518. package/src/infra/crawl/index.ts +9 -0
  519. package/src/infra/crawl/links.ts +160 -0
  520. package/src/infra/crawl/namespace.ts +36 -0
  521. package/src/infra/crawl/robots.ts +100 -0
  522. package/src/infra/crawl/schema.ts +89 -0
  523. package/src/infra/credit-line/handle.ts +883 -0
  524. package/src/infra/credit-line/index.ts +6 -0
  525. package/src/infra/credit-line/namespace.ts +7 -0
  526. package/src/infra/credit-line/schema.ts +96 -0
  527. package/src/infra/credits/handle.ts +706 -0
  528. package/src/infra/credits/index.ts +5 -0
  529. package/src/infra/credits/namespace.ts +7 -0
  530. package/src/infra/credits/schema.ts +83 -0
  531. package/src/infra/email/audit.ts +36 -0
  532. package/src/infra/email/inbound.ts +622 -0
  533. package/src/infra/email/index.ts +11 -0
  534. package/src/infra/email/mail.ts +517 -0
  535. package/src/infra/email/mime.ts +447 -0
  536. package/src/infra/email/namespace.ts +22 -0
  537. package/src/infra/email/providers.ts +111 -0
  538. package/src/infra/email/schema.ts +96 -0
  539. package/src/infra/email/sns.ts +200 -0
  540. package/src/infra/email/unsubscribe.ts +44 -0
  541. package/src/infra/fetch/address.ts +88 -0
  542. package/src/infra/fetch/connect-retry.ts +87 -0
  543. package/src/infra/fetch/handle.ts +329 -0
  544. package/src/infra/fetch/index.ts +7 -0
  545. package/src/infra/fetch/namespace.ts +70 -0
  546. package/src/infra/idempotency/index.ts +3 -0
  547. package/src/infra/idempotency/middleware.ts +87 -0
  548. package/src/infra/idempotency/store.ts +92 -0
  549. package/src/infra/idempotency/types.ts +30 -0
  550. package/src/infra/join-requests/handle.ts +312 -0
  551. package/src/infra/join-requests/index.ts +2 -0
  552. package/src/infra/join-requests/namespace.ts +7 -0
  553. package/src/infra/join-requests/schema.ts +33 -0
  554. package/src/infra/outbox/core.ts +165 -0
  555. package/src/infra/quota/errors.ts +32 -0
  556. package/src/infra/quota/handle.ts +215 -0
  557. package/src/infra/quota/index.ts +5 -0
  558. package/src/infra/quota/namespace.ts +7 -0
  559. package/src/infra/quota/schema.ts +15 -0
  560. package/src/infra/storage/backend.ts +34 -0
  561. package/src/infra/storage/handle.ts +513 -0
  562. package/src/infra/storage/index.ts +9 -0
  563. package/src/infra/storage/local.ts +104 -0
  564. package/src/infra/storage/namespace.ts +13 -0
  565. package/src/infra/storage/s3.ts +257 -0
  566. package/src/infra/storage/schema.ts +24 -0
  567. package/src/infra/support/handle.ts +854 -0
  568. package/src/infra/support/http.ts +229 -0
  569. package/src/infra/support/index.ts +7 -0
  570. package/src/infra/support/namespace.ts +12 -0
  571. package/src/infra/support/schema.ts +96 -0
  572. package/src/infra/webhook/inbound.ts +45 -0
  573. package/src/infra/webhook/index.ts +5 -0
  574. package/src/infra/webhook/namespace.ts +24 -0
  575. package/src/infra/webhook/outbound.ts +315 -0
  576. package/src/infra/webhook/sign.ts +32 -0
  577. package/src/infra/webhook/verify.ts +82 -0
  578. package/src/invariants/assert.ts +26 -0
  579. package/src/invariants/index.ts +1 -0
  580. package/src/lint/config.ts +56 -0
  581. package/src/lint/doctrine.ts +163 -0
  582. package/src/lint/include.ts +97 -0
  583. package/src/lint/index.ts +3 -0
  584. package/src/lint/preset.ts +10 -0
  585. package/src/lint/resolver.ts +218 -0
  586. package/src/lint/rules/no-reexports.ts +46 -0
  587. package/src/request/behavior/audited.ts +85 -0
  588. package/src/request/behavior/cached.ts +62 -0
  589. package/src/request/behavior/compose.ts +28 -0
  590. package/src/request/behavior/index.ts +5 -0
  591. package/src/request/behavior/namespace.ts +16 -0
  592. package/src/request/behavior/types.ts +26 -0
  593. package/src/request/behavior/versioned.ts +54 -0
  594. package/src/request/context/create.ts +103 -0
  595. package/src/request/context/guard.ts +49 -0
  596. package/src/request/context/index.ts +4 -0
  597. package/src/request/context/run-in-transaction.ts +200 -0
  598. package/src/request/context/store.ts +28 -0
  599. package/src/request/context/types.ts +50 -0
  600. package/src/request/di/builder.ts +535 -0
  601. package/src/request/di/index.ts +8 -0
  602. package/src/request/di/resolve.ts +271 -0
  603. package/src/request/di/types.ts +262 -0
  604. package/src/request/fitting/namespace.ts +7 -0
  605. package/src/request/fixture/batch.ts +71 -0
  606. package/src/request/fixture/builder.ts +26 -0
  607. package/src/request/fixture/errors.ts +10 -0
  608. package/src/request/fixture/index.ts +8 -0
  609. package/src/request/fixture/namespace.ts +23 -0
  610. package/src/request/fixture/paginate.ts +44 -0
  611. package/src/request/fixture/to-handlers.ts +260 -0
  612. package/src/request/fixture/types.ts +79 -0
  613. package/src/request/flow/act-as.ts +78 -0
  614. package/src/request/flow/namespace.ts +17 -0
  615. package/src/request/http/error-step.ts +42 -0
  616. package/src/request/http/file.ts +141 -0
  617. package/src/request/http/index.ts +4 -0
  618. package/src/request/http/middleware.ts +141 -0
  619. package/src/request/http/namespace.ts +5 -0
  620. package/src/request/http/rate-limit-classes.ts +170 -0
  621. package/src/request/http/response.ts +82 -0
  622. package/src/request/http/server.ts +916 -0
  623. package/src/request/http/sniff.ts +261 -0
  624. package/src/request/http/types.ts +250 -0
  625. package/src/request/http/upload.ts +284 -0
  626. package/src/request/internal/text.ts +6 -0
  627. package/src/request/openapi/generate.ts +205 -0
  628. package/src/request/openapi/index.ts +2 -0
  629. package/src/request/openapi/namespace.ts +7 -0
  630. package/src/test/auth.ts +13 -0
  631. package/src/test/auto-setup.ts +45 -0
  632. package/src/test/context.ts +91 -0
  633. package/src/test/crawl-fixture.ts +157 -0
  634. package/src/test/discover-fork.ts +17 -0
  635. package/src/test/fingerprint.ts +135 -0
  636. package/src/test/globals.d.ts +7 -0
  637. package/src/test/index.ts +19 -0
  638. package/src/test/namespace.ts +87 -0
  639. package/src/test/plugin.ts +189 -0
  640. package/src/test/progress.ts +185 -0
  641. package/src/test/reap-after-run.ts +28 -0
  642. package/src/test/reap.ts +286 -0
  643. package/src/test/reset.ts +253 -0
  644. package/src/test/savepoints.ts +172 -0
  645. package/src/test/setup.ts +1422 -0
  646. package/src/test/system-channel.ts +50 -0
  647. package/src/test/vitest-plugin.ts +142 -0
  648. package/src/test/vitest-workspace.ts +507 -0
  649. package/src/test/vitest.ts +333 -0
  650. package/src/test/watch.ts +105 -0
  651. package/src/trace/capture/bounded-serializer.ts +146 -0
  652. package/src/trace/cardinality/analyze.ts +173 -0
  653. package/src/trace/cardinality/branch-context.ts +214 -0
  654. package/src/trace/cardinality/check.ts +33 -0
  655. package/src/trace/cardinality/classify-edge.ts +64 -0
  656. package/src/trace/cardinality/structural-max.ts +107 -0
  657. package/src/trace/check/coverage-check.ts +188 -0
  658. package/src/trace/check/flow-check.ts +103 -0
  659. package/src/trace/check/flow-step-site.ts +234 -0
  660. package/src/trace/check/io-pattern.ts +116 -0
  661. package/src/trace/codemod/add-cardinality.ts +152 -0
  662. package/src/trace/codemod/run.ts +80 -0
  663. package/src/trace/entry/child-trace.ts +21 -0
  664. package/src/trace/entry/close-trace.ts +87 -0
  665. package/src/trace/entry/finalize-trace.ts +28 -0
  666. package/src/trace/entry/open-trace.ts +138 -0
  667. package/src/trace/entry/parent-trace.ts +20 -0
  668. package/src/trace/entry/parse-traceparent.ts +39 -0
  669. package/src/trace/entry/process-start.ts +38 -0
  670. package/src/trace/entry/trace-context.ts +26 -0
  671. package/src/trace/entry/tracing-enabled.ts +10 -0
  672. package/src/trace/external/record.ts +195 -0
  673. package/src/trace/external/report.ts +66 -0
  674. package/src/trace/namespace.ts +19 -0
  675. package/src/trace/partition/day.ts +47 -0
  676. package/src/trace/partition/index.ts +14 -0
  677. package/src/trace/partition/partition-name.ts +23 -0
  678. package/src/trace/partition/plan.ts +82 -0
  679. package/src/trace/partition/schedule.ts +26 -0
  680. package/src/trace/partition/startup-hook.ts +39 -0
  681. package/src/trace/partition/sync.ts +154 -0
  682. package/src/trace/partition/tables.ts +53 -0
  683. package/src/trace/read/tree.ts +83 -0
  684. package/src/trace/retain/copy-on-error.ts +76 -0
  685. package/src/trace/runtime/expire-stale-traces.ts +54 -0
  686. package/src/trace/runtime/flow-step.ts +147 -0
  687. package/src/trace/runtime/flush-steps.ts +31 -0
  688. package/src/trace/runtime/pending-writes.ts +54 -0
  689. package/src/trace/runtime/step-buffer.ts +298 -0
  690. package/src/trace/runtime/step-error.ts +21 -0
  691. package/src/trace/runtime/step-meta.ts +55 -0
  692. package/src/trace/runtime/thenable.ts +12 -0
  693. package/src/trace/runtime/truncate-trace.ts +62 -0
  694. package/src/trace/schema/trace-fields.ts +52 -0
  695. package/src/trace/schema/trace-retained.ts +22 -0
  696. package/src/trace/schema/trace-step-fields.ts +44 -0
  697. package/src/trace/schema/trace-step-retained.ts +25 -0
  698. package/src/trace/schema/trace-step.ts +18 -0
  699. package/src/trace/schema/trace.ts +12 -0
  700. package/src/trace/transformer/apply.ts +79 -0
  701. package/src/trace/transformer/build-meta-literal.ts +30 -0
  702. package/src/trace/transformer/find-flow-step-sites.ts +85 -0
  703. package/src/trace/transformer/inject-flow-step-import.ts +39 -0
  704. package/src/trace/transformer/transform-error.ts +14 -0
  705. package/src/trace/transformer/transform-flow-steps.ts +123 -0
  706. package/src/trace/transformer/wrap-function-declaration.ts +62 -0
  707. package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
  708. package/src/workspace/affected.ts +110 -0
  709. package/src/workspace/changed-files.ts +163 -0
  710. package/src/workspace/check-deps.ts +213 -0
  711. package/src/workspace/checksum.ts +20 -0
  712. package/src/workspace/dependency-graph.ts +67 -0
  713. package/src/workspace/detect-pipework-copies.ts +93 -0
  714. package/src/workspace/discover.ts +206 -0
  715. package/src/workspace/generate.ts +401 -0
  716. package/src/workspace/index.ts +7 -0
  717. package/src/workspace/module-imports-pipework.ts +39 -0
  718. package/src/workspace/module-package-name.ts +12 -0
  719. package/src/workspace/scrub-git-env.ts +23 -0
  720. package/src/workspace/source-paths.ts +36 -0
  721. package/src/workspace/sync.ts +182 -0
  722. package/dist/REFERENCE.md +0 -1477
  723. package/dist/infra/access-links/rate-limit.d.ts +0 -10
  724. package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
  725. package/dist/infra/access-links/rate-limit.js +0 -28
  726. package/dist/infra/access-links/rate-limit.js.map +0 -1
  727. package/dist/infra/email/sigv4.d.ts +0 -24
  728. package/dist/infra/email/sigv4.d.ts.map +0 -1
  729. package/dist/infra/email/sigv4.js +0 -55
  730. package/dist/infra/email/sigv4.js.map +0 -1
@@ -0,0 +1,883 @@
1
+ import { sql } from '../../data/db/sql.js'
2
+ import { pipe } from '../../data/db/db.js'
3
+ import type { DB } from '../../data/db/index.js'
4
+ import { requireContext, getContext, runInContext } from '../../request/context/store.js'
5
+ import { writeInfraAudit } from '../../data/write/audit/infra-audit.js'
6
+ import { inTenant } from '../../auth/tenant/in-tenant.js'
7
+ import { bindTransaction } from '../../request/context/guard.js'
8
+ import type { Flow } from '../../request/context/types.js'
9
+ import { ValidationError } from '../../core/errors/index.js'
10
+
11
+ /** The span a cap sums settled work over. Calendar windows are cut in the config's `timeZone`; `{ from, to }` is half-open `[from, to)`. */
12
+ export type CapWindow = 'calendar_year' | 'calendar_month' | { readonly from: Date; readonly to: Date }
13
+
14
+ export interface CreditLineCap {
15
+ /** Unique within the line; this is what a refusal names and what an authorization's `replacesCaps` lists. */
16
+ readonly name: string
17
+ readonly cents: number
18
+ readonly window: CapWindow
19
+ /** Overrides the line's and the config's warn share for this cap. */
20
+ readonly warnShare?: number
21
+ }
22
+
23
+ export interface CreditLineConfig {
24
+ /** The share of a limit at which it warns, for every limit that sets none. Default 0.8. */
25
+ readonly warnShare?: number
26
+ /** The zone calendar windows are cut in. Default `'UTC'`. */
27
+ readonly timeZone?: string
28
+ /**
29
+ * Awaited INSIDE the transaction of the write that took a limit's use from below its warn amount to
30
+ * at-or-above it (reserve, a settle above its reservation, openLine lowering a limit), in a Flow whose
31
+ * tenant is the write's tenant and where `pipe()` is that same transaction. If it throws, the write
32
+ * rolls back. A release, a payment or a raised limit re-arms the warning; nothing is stored.
33
+ */
34
+ readonly onLineCrossed?: (event: LineCrossing) => void | Promise<void>
35
+ }
36
+
37
+ export interface LimitRef {
38
+ readonly kind: 'ceiling' | 'cap' | 'authorization'
39
+ /** `'ceiling'`, the cap's name, or the authorization's reference. */
40
+ readonly name: string
41
+ }
42
+
43
+ export interface LineCrossing {
44
+ readonly tenantId: string
45
+ readonly limit: LimitRef
46
+ readonly usedCents: number
47
+ readonly limitCents: number
48
+ readonly warnAtCents: number
49
+ }
50
+
51
+ export interface LineReservation {
52
+ readonly id: string
53
+ readonly tenantId: string
54
+ readonly jobId: string
55
+ readonly amountCents: number
56
+ readonly status: 'held' | 'settled' | 'released'
57
+ readonly settledAmountCents: number | null
58
+ /** The authorization in force when the job was reserved, if any. */
59
+ readonly authorizationId: string | null
60
+ readonly createdAt: Date
61
+ readonly settledAt: Date | null
62
+ }
63
+
64
+ export type LineReserveResult =
65
+ | { readonly ok: true; readonly reservation: LineReservation }
66
+ | { readonly ok: false; readonly reason: 'no-line' }
67
+ | {
68
+ readonly ok: false; readonly reason: 'limit'; readonly limit: LimitRef
69
+ readonly limitCents: number; readonly usedCents: number; readonly requestedCents: number
70
+ }
71
+
72
+ /** One settled job: unpaid work until payments clear it. */
73
+ export interface LineEntry {
74
+ readonly id: string
75
+ readonly tenantId: string
76
+ readonly jobId: string
77
+ readonly reservationId: string
78
+ readonly amountCents: number
79
+ readonly paidCents: number
80
+ readonly authorizationId: string | null
81
+ readonly at: Date
82
+ }
83
+
84
+ export interface Payment {
85
+ readonly id: string
86
+ readonly tenantId: string
87
+ readonly reference: string
88
+ readonly amountCents: number
89
+ /** What no entry has cleared yet; the next settle draws on it. */
90
+ readonly unappliedCents: number
91
+ readonly at: Date
92
+ }
93
+
94
+ export interface PaymentApplication {
95
+ readonly entryId: string
96
+ readonly jobId: string
97
+ readonly cents: number
98
+ }
99
+
100
+ export type PaymentWithEntries = Payment & { readonly applications: readonly PaymentApplication[] }
101
+
102
+ export interface Authorization {
103
+ readonly id: string
104
+ readonly tenantId: string
105
+ readonly reference: string
106
+ readonly amountCents: number
107
+ readonly startsAt: Date
108
+ readonly endsAt: Date
109
+ readonly replacesCaps: readonly string[]
110
+ readonly revokedAt: Date | null
111
+ readonly createdAt: Date
112
+ }
113
+
114
+ export type AuthorizationStatus = Authorization & {
115
+ /** Settled entries tagged with it. */
116
+ readonly usedCents: number
117
+ /** Held reservations tagged with it. */
118
+ readonly heldCents: number
119
+ readonly remainingCents: number
120
+ /** Started, not ended, not revoked. */
121
+ readonly inForce: boolean
122
+ }
123
+
124
+ export interface LineCapState extends CreditLineCap {
125
+ readonly usedCents: number
126
+ /** The authorization in force replaces this cap. */
127
+ readonly replaced: boolean
128
+ }
129
+
130
+ export interface LineState {
131
+ readonly tenantId: string
132
+ /** False when no line was ever opened, or it is closed. */
133
+ readonly open: boolean
134
+ readonly ceilingCents: number
135
+ readonly warnShare: number | null
136
+ /** Settled work not yet paid. */
137
+ readonly unpaidCents: number
138
+ /** Reservations still held. */
139
+ readonly heldCents: number
140
+ /** What a reserve could still take: the smallest remaining of the limits that apply now, at least 0. */
141
+ readonly availableCents: number
142
+ readonly caps: readonly LineCapState[]
143
+ readonly authorization: AuthorizationStatus | null
144
+ readonly openedAt: Date | null
145
+ readonly closedAt: Date | null
146
+ }
147
+
148
+ export interface Statement {
149
+ readonly from: Date
150
+ readonly to: Date
151
+ readonly entries: readonly LineEntry[]
152
+ readonly payments: readonly PaymentWithEntries[]
153
+ readonly totals: {
154
+ /** Sum of the entries in the period. */
155
+ readonly settledCents: number
156
+ /** Sum of the payments in the period. */
157
+ readonly paidCents: number
158
+ /** Everything unpaid as of now, not just this period's. */
159
+ readonly unpaidCents: number
160
+ }
161
+ }
162
+
163
+ export interface OpenLineOptions {
164
+ readonly ceilingCents: number
165
+ readonly caps?: readonly CreditLineCap[]
166
+ readonly warnShare?: number
167
+ }
168
+
169
+ export interface AuthorizeOptions {
170
+ readonly reference: string
171
+ readonly amountCents: number
172
+ readonly from: Date
173
+ readonly to: Date
174
+ /** Names of caps this authorization replaces while it is in force; every other cap still applies. */
175
+ readonly replacesCaps?: readonly string[]
176
+ }
177
+
178
+ export interface CreditLine {
179
+ /** Opens the tenant's line, or updates an open one (and reopens a closed one). Audited as `CreditLine` `open`. */
180
+ openLine(options: OpenLineOptions): Promise<LineState>
181
+ openLineFor(tenantId: string, options: OpenLineOptions): Promise<LineState>
182
+ /** New reserves fail `'no-line'`; held reservations may still settle or release; unpaid work stays. */
183
+ closeLine(): Promise<LineState>
184
+ closeLineFor(tenantId: string): Promise<LineState>
185
+ /**
186
+ * Holds `amountCents` for a job against the ceiling (or the authorization in force) and then each cap in
187
+ * order; the first limit it would pass is the refusal and nothing is written. A job id that already has a
188
+ * reservation returns it unchanged, whatever amount is asked this time.
189
+ */
190
+ reserve(options: { jobId: string; amountCents: number }): Promise<LineReserveResult>
191
+ reserveFor(tenantId: string, options: { jobId: string; amountCents: number }): Promise<LineReserveResult>
192
+ /**
193
+ * Records the actual cost of a reserved job as an unpaid entry; never fails on limits. The same amount again
194
+ * is a no-op; a different amount, a released job or an unknown job throws. Payments with unapplied cents clear it, oldest first.
195
+ */
196
+ settle(options: { jobId: string; amountCents: number }): Promise<LineReservation>
197
+ settleFor(tenantId: string, options: { jobId: string; amountCents: number }): Promise<LineReservation>
198
+ /** Gives up a held reservation. No reservation: `{ released: false, reservation: null }`; already released: `released: false`. A settled job throws. */
199
+ release(options: { jobId: string }): Promise<{ released: boolean; reservation: LineReservation | null }>
200
+ releaseFor(tenantId: string, options: { jobId: string }): Promise<{ released: boolean; reservation: LineReservation | null }>
201
+ /** Records a purchase order. Idempotent by reference. Dates that overlap an unrevoked authorization throw ValidationError. */
202
+ authorize(options: AuthorizeOptions): Promise<{ authorization: Authorization; created: boolean }>
203
+ authorizeFor(tenantId: string, options: AuthorizeOptions): Promise<{ authorization: Authorization; created: boolean }>
204
+ revokeAuthorization(reference: string): Promise<Authorization>
205
+ revokeAuthorizationFor(tenantId: string, reference: string): Promise<Authorization>
206
+ authorizations(): Promise<AuthorizationStatus[]>
207
+ authorizationsFor(tenantId: string): Promise<AuthorizationStatus[]>
208
+ /** Idempotent by reference. Clears unpaid entries oldest first and keeps any excess as `unappliedCents`. */
209
+ recordPayment(options: { amountCents: number; reference: string; at?: Date }): Promise<{ payment: Payment; created: boolean }>
210
+ recordPaymentFor(tenantId: string, options: { amountCents: number; reference: string; at?: Date }): Promise<{ payment: Payment; created: boolean }>
211
+ /** The entries and payments with `at` in `[from, to)`, each payment with the entries it cleared, and totals. */
212
+ statement(options: { from: Date; to: Date }): Promise<Statement>
213
+ statementFor(tenantId: string, options: { from: Date; to: Date }): Promise<Statement>
214
+ line(): Promise<LineState>
215
+ lineFor(tenantId: string): Promise<LineState>
216
+ }
217
+
218
+ type Row = Record<string, unknown>
219
+
220
+ const DEFAULT_WARN_SHARE = 0.8
221
+ const NOW = sql`(clock_timestamp() AT TIME ZONE 'UTC')`
222
+
223
+ function toDate(value: unknown): Date {
224
+ if (value instanceof Date) return value
225
+ const text = String(value)
226
+ return new Date(/(Z|[+-]\d\d(:?\d\d)?)$/.test(text) ? text : `${text.replace(' ', 'T')}Z`)
227
+ }
228
+
229
+ function toDateOrNull(value: unknown): Date | null {
230
+ return value === null || value === undefined ? null : toDate(value)
231
+ }
232
+
233
+ function num(value: unknown): number {
234
+ return Number(value)
235
+ }
236
+
237
+ function numOrNull(value: unknown): number | null {
238
+ return value === null || value === undefined ? null : Number(value)
239
+ }
240
+
241
+ function utc(date: Date) {
242
+ return sql`(${date.toISOString()}::timestamptz AT TIME ZONE 'UTC')`
243
+ }
244
+
245
+ function rowToReservation(row: Row): LineReservation {
246
+ return {
247
+ id: row['id'] as string,
248
+ tenantId: row['tenant_id'] as string,
249
+ jobId: row['job_id'] as string,
250
+ amountCents: num(row['amount']),
251
+ status: row['status'] as LineReservation['status'],
252
+ settledAmountCents: numOrNull(row['settled_amount']),
253
+ authorizationId: (row['authorization_id'] as string | null) ?? null,
254
+ createdAt: toDate(row['created_at']),
255
+ settledAt: toDateOrNull(row['settled_at']),
256
+ }
257
+ }
258
+
259
+ function rowToEntry(row: Row): LineEntry {
260
+ return {
261
+ id: row['id'] as string,
262
+ tenantId: row['tenant_id'] as string,
263
+ jobId: row['job_id'] as string,
264
+ reservationId: row['reservation_id'] as string,
265
+ amountCents: num(row['amount_cents']),
266
+ paidCents: num(row['paid_cents']),
267
+ authorizationId: (row['authorization_id'] as string | null) ?? null,
268
+ at: toDate(row['at']),
269
+ }
270
+ }
271
+
272
+ function rowToPayment(row: Row): Payment {
273
+ return {
274
+ id: row['id'] as string,
275
+ tenantId: row['tenant_id'] as string,
276
+ reference: row['reference'] as string,
277
+ amountCents: num(row['amount_cents']),
278
+ unappliedCents: num(row['unapplied_cents']),
279
+ at: toDate(row['at']),
280
+ }
281
+ }
282
+
283
+ function rowToAuthorization(row: Row): Authorization {
284
+ const names = row['replaces_caps']
285
+ return {
286
+ id: row['id'] as string,
287
+ tenantId: row['tenant_id'] as string,
288
+ reference: row['reference'] as string,
289
+ amountCents: num(row['amount_cents']),
290
+ startsAt: toDate(row['starts_at']),
291
+ endsAt: toDate(row['ends_at']),
292
+ replacesCaps: Array.isArray(names) ? (names as string[]) : [],
293
+ revokedAt: toDateOrNull(row['revoked_at']),
294
+ createdAt: toDate(row['created_at']),
295
+ }
296
+ }
297
+
298
+ interface StoredCap { name: string; cents: number; window: 'calendar_year' | 'calendar_month' | { from: string; to: string }; warnShare?: number }
299
+
300
+ function capsToJson(caps: readonly CreditLineCap[]): string {
301
+ return JSON.stringify(caps.map((c): StoredCap => ({
302
+ name: c.name,
303
+ cents: c.cents,
304
+ window: typeof c.window === 'string' ? c.window : { from: c.window.from.toISOString(), to: c.window.to.toISOString() },
305
+ ...(c.warnShare === undefined ? {} : { warnShare: c.warnShare }),
306
+ })))
307
+ }
308
+
309
+ function capsFromJson(value: unknown): CreditLineCap[] {
310
+ const list = Array.isArray(value) ? (value as StoredCap[]) : []
311
+ return list.map(c => ({
312
+ name: c.name,
313
+ cents: c.cents,
314
+ window: typeof c.window === 'string' ? c.window : { from: new Date(c.window.from), to: new Date(c.window.to) },
315
+ ...(c.warnShare === undefined ? {} : { warnShare: c.warnShare }),
316
+ }))
317
+ }
318
+
319
+ function fail(field: string, detail: string, message: string): never {
320
+ throw new ValidationError({ [field]: [detail] }, message)
321
+ }
322
+
323
+ function requireCents(name: string, value: number, min: 0 | 1): void {
324
+ if (!Number.isSafeInteger(value) || value < min) {
325
+ const detail = min === 1 ? 'must be a positive integer' : 'must be a non-negative integer'
326
+ fail(name, detail, `Credit line ${name} ${detail} (whole cents).`)
327
+ }
328
+ }
329
+
330
+ function requireText(name: string, value: unknown): void {
331
+ if (typeof value !== 'string' || value === '') fail(name, 'must be a non-empty string', `Credit line ${name} must be a non-empty string.`)
332
+ }
333
+
334
+ function requireShare(name: string, value: number): void {
335
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 1) {
336
+ fail(name, 'must be greater than 0 and at most 1', `Credit line ${name} must be greater than 0 and at most 1.`)
337
+ }
338
+ }
339
+
340
+ function requireSpan(name: string, from: Date, to: Date): void {
341
+ if (!(from instanceof Date) || !(to instanceof Date) || Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()) || from.getTime() >= to.getTime()) {
342
+ fail(name, 'from must be a valid date before to', `Credit line ${name}: from must be a valid date before to.`)
343
+ }
344
+ }
345
+
346
+ function requireCaps(caps: readonly CreditLineCap[]): void {
347
+ const seen = new Set<string>()
348
+ for (const cap of caps) {
349
+ requireText('caps.name', cap.name)
350
+ if (seen.has(cap.name)) fail('caps', `name "${cap.name}" is used twice`, `Credit line cap names must be unique; "${cap.name}" appears twice.`)
351
+ seen.add(cap.name)
352
+ requireCents(`cap "${cap.name}" cents`, cap.cents, 1)
353
+ if (cap.warnShare !== undefined) requireShare(`cap "${cap.name}" warnShare`, cap.warnShare)
354
+ const window: unknown = cap.window
355
+ if (window === 'calendar_year' || window === 'calendar_month') continue
356
+ const span = (typeof window === 'object' && window !== null ? window : {}) as { from?: Date; to?: Date }
357
+ if (span.from === undefined || span.to === undefined) {
358
+ fail('caps.window', 'must be calendar_year, calendar_month or { from, to }', `Credit line cap "${cap.name}" window must be 'calendar_year', 'calendar_month' or { from, to }.`)
359
+ }
360
+ requireSpan(`cap "${cap.name}" window`, span.from, span.to)
361
+ }
362
+ }
363
+
364
+ interface LineRow {
365
+ readonly id: string
366
+ readonly ceilingCents: number
367
+ readonly warnShare: number | null
368
+ readonly caps: CreditLineCap[]
369
+ readonly openedAt: Date
370
+ readonly closedAt: Date | null
371
+ }
372
+
373
+ interface Limit {
374
+ readonly ref: LimitRef
375
+ readonly limitCents: number
376
+ readonly usedCents: number
377
+ readonly warnAtCents: number
378
+ }
379
+
380
+ interface Snapshot {
381
+ readonly line: LineRow | null
382
+ readonly auth: AuthorizationStatus | null
383
+ readonly unpaid: number
384
+ readonly held: number
385
+ readonly caps: LineCapState[]
386
+ /** Limits in force, in check order: the ceiling or the authorization, then the caps not replaced. */
387
+ readonly limits: Limit[]
388
+ }
389
+
390
+ export function createCreditLine(config: CreditLineConfig = {}): CreditLine {
391
+ const defaultShare = config.warnShare ?? DEFAULT_WARN_SHARE
392
+ requireShare('config warnShare', defaultShare)
393
+ const timeZone = config.timeZone ?? 'UTC'
394
+ try {
395
+ new Intl.DateTimeFormat('en', { timeZone })
396
+ } catch {
397
+ throw new Error(`[pipework] creditLine timeZone "${timeZone}" is not a time zone name (use an IANA name such as "America/New_York").\n`)
398
+ }
399
+
400
+ type InFlow = <R>(body: () => Promise<R>) => Promise<R>
401
+
402
+ async function run<T>(explicit: string | null, fn: (tx: DB, tenantId: string, inFlow: InFlow) => Promise<T>): Promise<T> {
403
+ if (explicit !== null) {
404
+ requireTenantId(explicit)
405
+ const flow = requireContext()
406
+ return inTenant(flow, explicit, tx => fn(tx, explicit, body => runForTenant(flow, explicit, tx, body)))
407
+ }
408
+ const flow = requireContext()
409
+ if (flow.tenant === null) {
410
+ throw new Error(
411
+ '[pipework] This creditLine method needs a tenant in the Flow.\n\n' +
412
+ ' Call it from an authenticated tenant route, or use the ...For(tenantId, ...) form\n' +
413
+ ' (openLineFor, reserveFor, settleFor, ...) from a webhook or a job with no tenant.\n',
414
+ )
415
+ }
416
+ const tenantId = flow.tenant
417
+ return pipe.transaction((tx: DB) => fn(tx, tenantId, body => body()))
418
+ }
419
+
420
+ function runForTenant<R>(flow: Flow, tenantId: string, tx: DB, body: () => Promise<R>): Promise<R> {
421
+ const dbName = flow.instance.config.defaultDatabaseName()
422
+ const child: Flow = { ...flow, tenant: tenantId, transactions: new Map(flow.transactions) }
423
+ child.transactions.set(dbName, bindTransaction(child, dbName, tx))
424
+ return runInContext(child, body)
425
+ }
426
+
427
+ function requireTenantId(tenantId: string): void {
428
+ requireText('tenantId', tenantId)
429
+ }
430
+
431
+ async function lockTenant(tx: DB, tenantId: string): Promise<void> {
432
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${`pipework.credit_line:${tenantId}`}, 0))`)
433
+ }
434
+
435
+ async function auditStep(tx: DB, tenantId: string, lineId: string, verb: string, after: Record<string, unknown>, before?: Record<string, unknown>): Promise<void> {
436
+ const flow = getContext()
437
+ if (flow === undefined) return
438
+ await writeInfraAudit(flow, tx, { tenantId, entity: 'CreditLine', verb, entityId: lineId, after, ...(before === undefined ? {} : { before }) })
439
+ }
440
+
441
+ async function loadLine(tx: DB, tenantId: string): Promise<LineRow | null> {
442
+ const rows = await tx.execute(sql`SELECT * FROM credit_line WHERE tenant_id = ${tenantId}`)
443
+ if (rows.length === 0) return null
444
+ const r = rows[0]!
445
+ return {
446
+ id: r['id'] as string,
447
+ ceilingCents: num(r['ceiling_cents']),
448
+ warnShare: numOrNull(r['warn_share']),
449
+ caps: capsFromJson(r['caps']),
450
+ openedAt: toDate(r['opened_at']),
451
+ closedAt: toDateOrNull(r['closed_at']),
452
+ }
453
+ }
454
+
455
+ async function requireLine(tx: DB, tenantId: string, what: string): Promise<LineRow> {
456
+ const line = await loadLine(tx, tenantId)
457
+ if (line === null) {
458
+ throw new Error(`[pipework] creditLine.${what}: tenant ${tenantId} has no line.\n\n Call openLine({ ceilingCents }) first.\n`)
459
+ }
460
+ return line
461
+ }
462
+
463
+ function windowBounds(window: CapWindow) {
464
+ if (typeof window !== 'string') return { start: utc(window.from), end: utc(window.to) }
465
+ const unit = window === 'calendar_year' ? 'year' : 'month'
466
+ const local = sql`date_trunc(${sql.raw(`'${unit}'`)}, clock_timestamp() AT TIME ZONE ${timeZone}::text)`
467
+ return {
468
+ start: sql`((${local}) AT TIME ZONE ${timeZone}::text) AT TIME ZONE 'UTC'`,
469
+ end: sql`((${local} + ${sql.raw(`interval '1 ${unit}'`)}) AT TIME ZONE ${timeZone}::text) AT TIME ZONE 'UTC'`,
470
+ }
471
+ }
472
+
473
+ async function sumOf(tx: DB, query: ReturnType<typeof sql>): Promise<number> {
474
+ const rows = await tx.execute(query)
475
+ return num(rows[0]!['n'])
476
+ }
477
+
478
+ function authStatus(row: Row, used: number, held: number, inForce: boolean): AuthorizationStatus {
479
+ const a = rowToAuthorization(row)
480
+ return { ...a, usedCents: used, heldCents: held, remainingCents: Math.max(0, a.amountCents - used - held), inForce }
481
+ }
482
+
483
+ async function statusOf(tx: DB, tenantId: string, row: Row): Promise<AuthorizationStatus> {
484
+ const id = row['id'] as string
485
+ const used = await sumOf(tx, sql`SELECT COALESCE(SUM(amount_cents), 0) AS n FROM credit_line_entry WHERE tenant_id = ${tenantId} AND authorization_id = ${id}`)
486
+ const held = await sumOf(tx, sql`SELECT COALESCE(SUM(amount), 0) AS n FROM credit_line_reservation WHERE tenant_id = ${tenantId} AND authorization_id = ${id} AND status = 'held'`)
487
+ const flag = await tx.execute(sql`
488
+ SELECT (revoked_at IS NULL AND starts_at <= ${NOW} AND ${NOW} < ends_at) AS in_force FROM credit_line_authorization WHERE id = ${id}
489
+ `)
490
+ return authStatus(row, used, held, flag[0]!['in_force'] === true)
491
+ }
492
+
493
+ // Everything a limit check or a crossing needs, read in the operation's own transaction.
494
+ async function measure(tx: DB, tenantId: string): Promise<Snapshot> {
495
+ const line = await loadLine(tx, tenantId)
496
+ const unpaid = await sumOf(tx, sql`SELECT COALESCE(SUM(amount_cents - paid_cents), 0) AS n FROM credit_line_entry WHERE tenant_id = ${tenantId}`)
497
+ const held = await sumOf(tx, sql`SELECT COALESCE(SUM(amount), 0) AS n FROM credit_line_reservation WHERE tenant_id = ${tenantId} AND status = 'held'`)
498
+ if (line === null) return { line, auth: null, unpaid, held, caps: [], limits: [] }
499
+ const authRows = await tx.execute(sql`
500
+ SELECT * FROM credit_line_authorization
501
+ WHERE tenant_id = ${tenantId} AND revoked_at IS NULL AND starts_at <= ${NOW} AND ${NOW} < ends_at
502
+ ORDER BY starts_at DESC LIMIT 1
503
+ `)
504
+ const auth = authRows.length === 0 ? null : await statusOf(tx, tenantId, authRows[0]!)
505
+ const warnAt = (limit: number, own?: number | null): number =>
506
+ Math.floor(limit * (own ?? line.warnShare ?? defaultShare) + 1e-9)
507
+ const limits: Limit[] = auth === null
508
+ ? [{ ref: { kind: 'ceiling', name: 'ceiling' }, limitCents: line.ceilingCents, usedCents: unpaid + held, warnAtCents: warnAt(line.ceilingCents) }]
509
+ : [{ ref: { kind: 'authorization', name: auth.reference }, limitCents: auth.amountCents, usedCents: auth.usedCents + auth.heldCents, warnAtCents: warnAt(auth.amountCents) }]
510
+ const caps: LineCapState[] = []
511
+ for (const cap of line.caps) {
512
+ const { start, end } = windowBounds(cap.window)
513
+ const settled = await sumOf(tx, sql`
514
+ SELECT COALESCE(SUM(amount_cents), 0) AS n FROM credit_line_entry
515
+ WHERE tenant_id = ${tenantId} AND "at" >= ${start} AND "at" < ${end}
516
+ `)
517
+ const used = settled + held
518
+ const replaced = auth !== null && auth.replacesCaps.includes(cap.name)
519
+ caps.push({ ...cap, usedCents: used, replaced })
520
+ if (!replaced) limits.push({ ref: { kind: 'cap', name: cap.name }, limitCents: cap.cents, usedCents: used, warnAtCents: warnAt(cap.cents, cap.warnShare) })
521
+ }
522
+ return { line, auth, unpaid, held, caps, limits }
523
+ }
524
+
525
+ function stateOf(tenantId: string, s: Snapshot): LineState {
526
+ const remaining = s.limits.map(l => l.limitCents - l.usedCents)
527
+ return {
528
+ tenantId,
529
+ open: s.line !== null && s.line.closedAt === null,
530
+ ceilingCents: s.line?.ceilingCents ?? 0,
531
+ warnShare: s.line?.warnShare ?? null,
532
+ unpaidCents: s.unpaid,
533
+ heldCents: s.held,
534
+ availableCents: remaining.length === 0 ? 0 : Math.max(0, Math.min(...remaining)),
535
+ caps: s.caps,
536
+ authorization: s.auth,
537
+ openedAt: s.line?.openedAt ?? null,
538
+ closedAt: s.line?.closedAt ?? null,
539
+ }
540
+ }
541
+
542
+ // A limit warns when a write takes its use from below its warn amount to at-or-above it. Compared
543
+ // within the write, so nothing is stored and a fall back below re-arms it. Awaited in the tenant's
544
+ // Flow, so a throwing hook rolls the write back.
545
+ async function fireCrossings(tenantId: string, inFlow: InFlow, before: Snapshot, after: Snapshot): Promise<void> {
546
+ const fn = config.onLineCrossed
547
+ if (fn === undefined) return
548
+ const key = (l: Limit): string => `${l.ref.kind}:${l.ref.name}`
549
+ const prior = new Map(before.limits.map(l => [key(l), l]))
550
+ for (const l of after.limits) {
551
+ const b = prior.get(key(l))
552
+ if (b === undefined || !(b.usedCents < b.warnAtCents && l.usedCents >= l.warnAtCents)) continue
553
+ const event: LineCrossing = { tenantId, limit: l.ref, usedCents: l.usedCents, limitCents: l.limitCents, warnAtCents: l.warnAtCents }
554
+ await inFlow(async () => { await fn(event) })
555
+ }
556
+ }
557
+
558
+ async function findReservation(tx: DB, tenantId: string, jobId: string, lock: boolean): Promise<LineReservation | null> {
559
+ const rows = await tx.execute(lock
560
+ ? sql`SELECT * FROM credit_line_reservation WHERE tenant_id = ${tenantId} AND job_id = ${jobId} FOR UPDATE`
561
+ : sql`SELECT * FROM credit_line_reservation WHERE tenant_id = ${tenantId} AND job_id = ${jobId}`)
562
+ return rows.length === 0 ? null : rowToReservation(rows[0]!)
563
+ }
564
+
565
+ // Pays unpaid entries from a payment's unapplied cents, oldest entry first.
566
+ async function applyPayment(tx: DB, tenantId: string, paymentId: string, unapplied: number): Promise<number> {
567
+ const entries = await tx.execute(sql`
568
+ SELECT id, amount_cents, paid_cents FROM credit_line_entry
569
+ WHERE tenant_id = ${tenantId} AND amount_cents > paid_cents ORDER BY "at", id FOR UPDATE
570
+ `)
571
+ let left = unapplied
572
+ for (const e of entries) {
573
+ if (left === 0) break
574
+ const take = Math.min(num(e['amount_cents']) - num(e['paid_cents']), left)
575
+ await tx.execute(sql`UPDATE credit_line_entry SET paid_cents = paid_cents + ${take} WHERE id = ${e['id'] as string}`)
576
+ await tx.execute(sql`
577
+ INSERT INTO credit_line_payment_application (tenant_id, payment_id, entry_id, cents) VALUES (${tenantId}, ${paymentId}, ${e['id'] as string}, ${take})
578
+ `)
579
+ left -= take
580
+ }
581
+ await tx.execute(sql`UPDATE credit_line_payment SET unapplied_cents = ${left} WHERE id = ${paymentId}`)
582
+ return left
583
+ }
584
+
585
+ // Pays one new entry from payments with unapplied cents, oldest payment first.
586
+ async function clearEntry(tx: DB, tenantId: string, entryId: string, amount: number): Promise<number> {
587
+ const payments = await tx.execute(sql`
588
+ SELECT id, unapplied_cents FROM credit_line_payment
589
+ WHERE tenant_id = ${tenantId} AND unapplied_cents > 0 ORDER BY "at", id FOR UPDATE
590
+ `)
591
+ let need = amount
592
+ for (const p of payments) {
593
+ if (need === 0) break
594
+ const take = Math.min(num(p['unapplied_cents']), need)
595
+ await tx.execute(sql`UPDATE credit_line_payment SET unapplied_cents = unapplied_cents - ${take} WHERE id = ${p['id'] as string}`)
596
+ await tx.execute(sql`
597
+ INSERT INTO credit_line_payment_application (tenant_id, payment_id, entry_id, cents) VALUES (${tenantId}, ${p['id'] as string}, ${entryId}, ${take})
598
+ `)
599
+ need -= take
600
+ }
601
+ return amount - need
602
+ }
603
+
604
+ async function doOpen(explicit: string | null, o: OpenLineOptions): Promise<LineState> {
605
+ requireCents('ceilingCents', o.ceilingCents, 1)
606
+ if (o.warnShare !== undefined) requireShare('warnShare', o.warnShare)
607
+ const caps = o.caps ?? []
608
+ requireCaps(caps)
609
+ return run(explicit, async (tx, tenantId, inFlow) => {
610
+ await lockTenant(tx, tenantId)
611
+ const before = await measure(tx, tenantId)
612
+ const rows = await tx.execute(sql`
613
+ INSERT INTO credit_line (tenant_id, ceiling_cents, warn_share, caps, opened_at, closed_at)
614
+ VALUES (${tenantId}, ${o.ceilingCents}, ${o.warnShare ?? null}, ${capsToJson(caps)}::jsonb, ${NOW}, NULL)
615
+ ON CONFLICT (tenant_id) DO UPDATE
616
+ SET ceiling_cents = EXCLUDED.ceiling_cents, warn_share = EXCLUDED.warn_share, caps = EXCLUDED.caps, closed_at = NULL
617
+ RETURNING id
618
+ `)
619
+ const after = await measure(tx, tenantId)
620
+ const prev = before.line
621
+ await auditStep(tx, tenantId, rows[0]!['id'] as string, 'open', {
622
+ ceilingCents: o.ceilingCents, warnShare: o.warnShare ?? null, caps: JSON.parse(capsToJson(caps)) as unknown,
623
+ }, prev === null ? undefined : {
624
+ ceilingCents: prev.ceilingCents, warnShare: prev.warnShare, caps: JSON.parse(capsToJson(prev.caps)) as unknown, closed: prev.closedAt !== null,
625
+ })
626
+ await fireCrossings(tenantId, inFlow, before, after)
627
+ return stateOf(tenantId, after)
628
+ })
629
+ }
630
+
631
+ async function doClose(explicit: string | null): Promise<LineState> {
632
+ return run(explicit, async (tx, tenantId) => {
633
+ await lockTenant(tx, tenantId)
634
+ const line = await requireLine(tx, tenantId, 'closeLine')
635
+ if (line.closedAt === null) {
636
+ await tx.execute(sql`UPDATE credit_line SET closed_at = ${NOW} WHERE id = ${line.id}`)
637
+ await auditStep(tx, tenantId, line.id, 'close', { closed: true })
638
+ }
639
+ return stateOf(tenantId, await measure(tx, tenantId))
640
+ })
641
+ }
642
+
643
+ async function doReserve(explicit: string | null, o: { jobId: string; amountCents: number }): Promise<LineReserveResult> {
644
+ requireText('jobId', o.jobId)
645
+ requireCents('amountCents', o.amountCents, 1)
646
+ return run(explicit, async (tx, tenantId, inFlow): Promise<LineReserveResult> => {
647
+ await lockTenant(tx, tenantId)
648
+ const existing = await findReservation(tx, tenantId, o.jobId, false)
649
+ if (existing !== null) return { ok: true, reservation: existing }
650
+ const before = await measure(tx, tenantId)
651
+ if (before.line === null || before.line.closedAt !== null) return { ok: false, reason: 'no-line' }
652
+ for (const l of before.limits) {
653
+ if (l.usedCents + o.amountCents > l.limitCents) {
654
+ return { ok: false, reason: 'limit', limit: l.ref, limitCents: l.limitCents, usedCents: l.usedCents, requestedCents: o.amountCents }
655
+ }
656
+ }
657
+ const inserted = await tx.execute(sql`
658
+ INSERT INTO credit_line_reservation (tenant_id, job_id, amount, status, authorization_id, created_at)
659
+ VALUES (${tenantId}, ${o.jobId}, ${o.amountCents}, 'held', ${before.auth?.id ?? null}, ${NOW})
660
+ ON CONFLICT (tenant_id, job_id) DO NOTHING
661
+ RETURNING *
662
+ `)
663
+ if (inserted.length === 0) return { ok: true, reservation: (await findReservation(tx, tenantId, o.jobId, false))! }
664
+ const reservation = rowToReservation(inserted[0]!)
665
+ await auditStep(tx, tenantId, before.line.id, 'reserve', {
666
+ jobId: o.jobId, amountCents: o.amountCents, reservationId: reservation.id, authorizationId: reservation.authorizationId,
667
+ })
668
+ await fireCrossings(tenantId, inFlow, before, await measure(tx, tenantId))
669
+ return { ok: true, reservation }
670
+ })
671
+ }
672
+
673
+ async function doSettle(explicit: string | null, o: { jobId: string; amountCents: number }): Promise<LineReservation> {
674
+ requireText('jobId', o.jobId)
675
+ requireCents('amountCents', o.amountCents, 0)
676
+ return run(explicit, async (tx, tenantId, inFlow) => {
677
+ await lockTenant(tx, tenantId)
678
+ const reservation = await findReservation(tx, tenantId, o.jobId, true)
679
+ if (reservation === null) {
680
+ throw new Error(`[pipework] creditLine.settle: no reservation for job "${o.jobId}".\n\n Call reserve({ jobId, amountCents }) before the job, with the same jobId.\n`)
681
+ }
682
+ if (reservation.status === 'released') {
683
+ throw new Error(`[pipework] creditLine.settle: the reservation for job "${o.jobId}" was released.\n`)
684
+ }
685
+ if (reservation.status === 'settled') {
686
+ if (reservation.settledAmountCents === o.amountCents) return reservation
687
+ throw new Error(
688
+ `[pipework] creditLine.settle: job "${o.jobId}" was already settled at ${reservation.settledAmountCents}, not ${o.amountCents}.\n`,
689
+ )
690
+ }
691
+ const line = await requireLine(tx, tenantId, 'settle')
692
+ const before = await measure(tx, tenantId)
693
+ const rows = await tx.execute(sql`
694
+ UPDATE credit_line_reservation SET status = 'settled', settled_amount = ${o.amountCents}, settled_at = ${NOW}
695
+ WHERE id = ${reservation.id} RETURNING *
696
+ `)
697
+ const entry = await tx.execute(sql`
698
+ INSERT INTO credit_line_entry (tenant_id, job_id, reservation_id, amount_cents, paid_cents, authorization_id, "at")
699
+ VALUES (${tenantId}, ${o.jobId}, ${reservation.id}, ${o.amountCents}, 0, ${reservation.authorizationId}, ${NOW})
700
+ RETURNING id
701
+ `)
702
+ const entryId = entry[0]!['id'] as string
703
+ const cleared = await clearEntry(tx, tenantId, entryId, o.amountCents)
704
+ if (cleared > 0) await tx.execute(sql`UPDATE credit_line_entry SET paid_cents = ${cleared} WHERE id = ${entryId}`)
705
+ await auditStep(tx, tenantId, line.id, 'settle', {
706
+ jobId: o.jobId, amountCents: o.amountCents, reservedCents: reservation.amountCents, reservationId: reservation.id,
707
+ entryId, authorizationId: reservation.authorizationId, clearedCents: cleared,
708
+ })
709
+ await fireCrossings(tenantId, inFlow, before, await measure(tx, tenantId))
710
+ return rowToReservation(rows[0]!)
711
+ })
712
+ }
713
+
714
+ async function doRelease(explicit: string | null, o: { jobId: string }): Promise<{ released: boolean; reservation: LineReservation | null }> {
715
+ requireText('jobId', o.jobId)
716
+ return run(explicit, async (tx, tenantId) => {
717
+ await lockTenant(tx, tenantId)
718
+ const reservation = await findReservation(tx, tenantId, o.jobId, true)
719
+ if (reservation === null) return { released: false, reservation: null }
720
+ if (reservation.status === 'released') return { released: false, reservation }
721
+ if (reservation.status === 'settled') {
722
+ throw new Error(`[pipework] creditLine.release: job "${o.jobId}" was already settled; settled usage cannot be released.\n`)
723
+ }
724
+ const line = await requireLine(tx, tenantId, 'release')
725
+ const rows = await tx.execute(sql`
726
+ UPDATE credit_line_reservation SET status = 'released', settled_at = ${NOW} WHERE id = ${reservation.id} RETURNING *
727
+ `)
728
+ await auditStep(tx, tenantId, line.id, 'release', {
729
+ jobId: o.jobId, amountCents: reservation.amountCents, reservationId: reservation.id, authorizationId: reservation.authorizationId,
730
+ })
731
+ return { released: true, reservation: rowToReservation(rows[0]!) }
732
+ })
733
+ }
734
+
735
+ async function doAuthorize(explicit: string | null, o: AuthorizeOptions): Promise<{ authorization: Authorization; created: boolean }> {
736
+ requireText('reference', o.reference)
737
+ requireCents('amountCents', o.amountCents, 1)
738
+ requireSpan('authorization dates', o.from, o.to)
739
+ const replaces = o.replacesCaps ?? []
740
+ for (const name of replaces) requireText('replacesCaps', name)
741
+ return run(explicit, async (tx, tenantId) => {
742
+ await lockTenant(tx, tenantId)
743
+ const line = await requireLine(tx, tenantId, 'authorize')
744
+ const found = await tx.execute(sql`SELECT * FROM credit_line_authorization WHERE tenant_id = ${tenantId} AND reference = ${o.reference}`)
745
+ if (found.length > 0) return { authorization: rowToAuthorization(found[0]!), created: false }
746
+ const clash = await tx.execute(sql`
747
+ SELECT reference FROM credit_line_authorization
748
+ WHERE tenant_id = ${tenantId} AND revoked_at IS NULL AND starts_at < ${utc(o.to)} AND ${utc(o.from)} < ends_at
749
+ LIMIT 1
750
+ `)
751
+ if (clash.length > 0) {
752
+ fail('from', `overlaps authorization "${clash[0]!['reference'] as string}"`,
753
+ `Credit line authorization "${o.reference}" overlaps "${clash[0]!['reference'] as string}"; revoke that one or choose dates after it ends.`)
754
+ }
755
+ const rows = await tx.execute(sql`
756
+ INSERT INTO credit_line_authorization (tenant_id, reference, amount_cents, starts_at, ends_at, replaces_caps, created_at)
757
+ VALUES (${tenantId}, ${o.reference}, ${o.amountCents}, ${utc(o.from)}, ${utc(o.to)}, ${JSON.stringify(replaces)}::jsonb, ${NOW})
758
+ RETURNING *
759
+ `)
760
+ const authorization = rowToAuthorization(rows[0]!)
761
+ await auditStep(tx, tenantId, line.id, 'authorize', {
762
+ reference: o.reference, amountCents: o.amountCents, from: o.from.toISOString(), to: o.to.toISOString(), replacesCaps: replaces,
763
+ authorizationId: authorization.id,
764
+ })
765
+ return { authorization, created: true }
766
+ })
767
+ }
768
+
769
+ async function doRevoke(explicit: string | null, reference: string): Promise<Authorization> {
770
+ requireText('reference', reference)
771
+ return run(explicit, async (tx, tenantId) => {
772
+ await lockTenant(tx, tenantId)
773
+ const line = await requireLine(tx, tenantId, 'revokeAuthorization')
774
+ const found = await tx.execute(sql`SELECT * FROM credit_line_authorization WHERE tenant_id = ${tenantId} AND reference = ${reference} FOR UPDATE`)
775
+ if (found.length === 0) {
776
+ throw new Error(`[pipework] creditLine.revokeAuthorization: no authorization "${reference}" for tenant ${tenantId}.\n`)
777
+ }
778
+ const current = rowToAuthorization(found[0]!)
779
+ if (current.revokedAt !== null) return current
780
+ const rows = await tx.execute(sql`UPDATE credit_line_authorization SET revoked_at = ${NOW} WHERE id = ${current.id} RETURNING *`)
781
+ await auditStep(tx, tenantId, line.id, 'revoke_authorization', { reference, authorizationId: current.id })
782
+ return rowToAuthorization(rows[0]!)
783
+ })
784
+ }
785
+
786
+ async function doAuthorizations(explicit: string | null): Promise<AuthorizationStatus[]> {
787
+ return run(explicit, async (tx, tenantId) => {
788
+ const rows = await tx.execute(sql`SELECT * FROM credit_line_authorization WHERE tenant_id = ${tenantId} ORDER BY created_at, id`)
789
+ const out: AuthorizationStatus[] = []
790
+ for (const row of rows) out.push(await statusOf(tx, tenantId, row))
791
+ return out
792
+ })
793
+ }
794
+
795
+ async function doPayment(explicit: string | null, o: { amountCents: number; reference: string; at?: Date }): Promise<{ payment: Payment; created: boolean }> {
796
+ requireText('reference', o.reference)
797
+ requireCents('amountCents', o.amountCents, 1)
798
+ if (o.at !== undefined && Number.isNaN(o.at.getTime())) fail('at', 'must be a valid date', 'Credit line payment at must be a valid date.')
799
+ return run(explicit, async (tx, tenantId) => {
800
+ await lockTenant(tx, tenantId)
801
+ const line = await requireLine(tx, tenantId, 'recordPayment')
802
+ const found = await tx.execute(sql`SELECT * FROM credit_line_payment WHERE tenant_id = ${tenantId} AND reference = ${o.reference}`)
803
+ if (found.length > 0) return { payment: rowToPayment(found[0]!), created: false }
804
+ const rows = await tx.execute(sql`
805
+ INSERT INTO credit_line_payment (tenant_id, reference, amount_cents, unapplied_cents, "at")
806
+ VALUES (${tenantId}, ${o.reference}, ${o.amountCents}, ${o.amountCents}, ${o.at === undefined ? NOW : utc(o.at)})
807
+ RETURNING id
808
+ `)
809
+ const id = rows[0]!['id'] as string
810
+ const left = await applyPayment(tx, tenantId, id, o.amountCents)
811
+ await auditStep(tx, tenantId, line.id, 'payment', {
812
+ reference: o.reference, amountCents: o.amountCents, clearedCents: o.amountCents - left, unappliedCents: left, paymentId: id,
813
+ })
814
+ const saved = await tx.execute(sql`SELECT * FROM credit_line_payment WHERE id = ${id}`)
815
+ return { payment: rowToPayment(saved[0]!), created: true }
816
+ })
817
+ }
818
+
819
+ async function doStatement(explicit: string | null, o: { from: Date; to: Date }): Promise<Statement> {
820
+ requireSpan('statement period', o.from, o.to)
821
+ return run(explicit, async (tx, tenantId): Promise<Statement> => {
822
+ const entries = (await tx.execute(sql`
823
+ SELECT * FROM credit_line_entry WHERE tenant_id = ${tenantId} AND "at" >= ${utc(o.from)} AND "at" < ${utc(o.to)} ORDER BY "at", id
824
+ `)).map(rowToEntry)
825
+ const paymentRows = await tx.execute(sql`
826
+ SELECT * FROM credit_line_payment WHERE tenant_id = ${tenantId} AND "at" >= ${utc(o.from)} AND "at" < ${utc(o.to)} ORDER BY "at", id
827
+ `)
828
+ const payments: PaymentWithEntries[] = []
829
+ for (const row of paymentRows) {
830
+ const apps = await tx.execute(sql`
831
+ SELECT a.entry_id, a.cents, e.job_id FROM credit_line_payment_application a
832
+ JOIN credit_line_entry e ON e.id = a.entry_id
833
+ WHERE a.tenant_id = ${tenantId} AND a.payment_id = ${row['id'] as string} ORDER BY e."at", e.id
834
+ `)
835
+ payments.push({
836
+ ...rowToPayment(row),
837
+ applications: apps.map(a => ({ entryId: a['entry_id'] as string, jobId: a['job_id'] as string, cents: num(a['cents']) })),
838
+ })
839
+ }
840
+ const unpaid = await sumOf(tx, sql`SELECT COALESCE(SUM(amount_cents - paid_cents), 0) AS n FROM credit_line_entry WHERE tenant_id = ${tenantId}`)
841
+ return {
842
+ from: o.from,
843
+ to: o.to,
844
+ entries,
845
+ payments,
846
+ totals: {
847
+ settledCents: entries.reduce((s, e) => s + e.amountCents, 0),
848
+ paidCents: payments.reduce((s, p) => s + p.amountCents, 0),
849
+ unpaidCents: unpaid,
850
+ },
851
+ }
852
+ })
853
+ }
854
+
855
+ async function doLine(explicit: string | null): Promise<LineState> {
856
+ return run(explicit, async (tx, tenantId) => stateOf(tenantId, await measure(tx, tenantId)))
857
+ }
858
+
859
+ return {
860
+ openLine: o => doOpen(null, o),
861
+ openLineFor: (t, o) => doOpen(t, o),
862
+ closeLine: () => doClose(null),
863
+ closeLineFor: t => doClose(t),
864
+ reserve: o => doReserve(null, o),
865
+ reserveFor: (t, o) => doReserve(t, o),
866
+ settle: o => doSettle(null, o),
867
+ settleFor: (t, o) => doSettle(t, o),
868
+ release: o => doRelease(null, o),
869
+ releaseFor: (t, o) => doRelease(t, o),
870
+ authorize: o => doAuthorize(null, o),
871
+ authorizeFor: (t, o) => doAuthorize(t, o),
872
+ revokeAuthorization: r => doRevoke(null, r),
873
+ revokeAuthorizationFor: (t, r) => doRevoke(t, r),
874
+ authorizations: () => doAuthorizations(null),
875
+ authorizationsFor: t => doAuthorizations(t),
876
+ recordPayment: o => doPayment(null, o),
877
+ recordPaymentFor: (t, o) => doPayment(t, o),
878
+ statement: o => doStatement(null, o),
879
+ statementFor: (t, o) => doStatement(t, o),
880
+ line: () => doLine(null),
881
+ lineFor: t => doLine(t),
882
+ }
883
+ }