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,706 @@
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 { getBaseLogger } from '../../core/logging/index.js'
10
+ import { recordFault, faultMessage } from '../alerts/faults.js'
11
+ import { ValidationError } from '../../core/errors/index.js'
12
+
13
+ export interface CreditsConfig {
14
+ /**
15
+ * The low-balance line for tenants that have not set their own (`setLowBalanceLine`).
16
+ * When available credits fall from at-or-above a tenant's line to below it, the crossing
17
+ * fires once: `onLowBalanceCrossed`, `onLowBalance` and a `credits low balance` warning.
18
+ */
19
+ readonly lowBalanceThreshold?: number
20
+ /** Fires after the operation's transaction body, once per crossing; errors are logged and dropped. */
21
+ readonly onLowBalance?: (event: { tenantId: string; available: number }) => void
22
+ /**
23
+ * Awaited INSIDE the transaction of the operation that crossed the line (reserve, settle, expire,
24
+ * setLowBalanceLine), after its ledger write, in a Flow whose tenant is the operation's tenant and
25
+ * where `pipe()` is that same transaction: what it writes commits or rolls back with the operation.
26
+ * If it throws, the operation fails and rolls back.
27
+ */
28
+ readonly onLowBalanceCrossed?: (event: LowBalanceCrossing) => void | Promise<void>
29
+ /** Called when a `reserve` is refused for lack of credits. */
30
+ readonly onInsufficient?: (event: { tenantId: string; jobId: string; requested: number; available: number }) => void
31
+ }
32
+
33
+ export interface LowBalanceCrossing {
34
+ readonly tenantId: string
35
+ readonly available: number
36
+ /** The line that was crossed. */
37
+ readonly line: number
38
+ }
39
+
40
+ /** A tenant's low-balance line and whether its balance is under it. */
41
+ export interface LowBalanceState {
42
+ /** The effective line; `null` when there is none. */
43
+ readonly line: number | null
44
+ /** `'tenant'`: set by the tenant; `'default'`: the config's `lowBalanceThreshold`; `'none'`: no line. */
45
+ readonly source: 'tenant' | 'default' | 'none'
46
+ readonly available: number
47
+ /** Available is under the line; false when there is no line. */
48
+ readonly below: boolean
49
+ }
50
+
51
+ export interface CreditGrant {
52
+ readonly id: string
53
+ readonly tenantId: string
54
+ readonly kind: string
55
+ readonly amount: number
56
+ readonly remaining: number
57
+ readonly grantedAt: Date
58
+ /** `null` = never expires. */
59
+ readonly expiresAt: Date | null
60
+ readonly sourceRef: string | null
61
+ readonly metadata: unknown
62
+ }
63
+
64
+ export interface GrantOptions {
65
+ /** Positive integer, in the app's own unit. */
66
+ readonly amount: number
67
+ /** `'allocation'`, `'pack'`, or any string the app uses. */
68
+ readonly kind: string
69
+ readonly expiresAt?: Date
70
+ /** Idempotency key for the grant, e.g. a payment event id: the same ref again credits nothing. */
71
+ readonly sourceRef?: string
72
+ readonly metadata?: unknown
73
+ }
74
+
75
+ export interface CreditReservation {
76
+ readonly id: string
77
+ readonly tenantId: string
78
+ readonly jobId: string
79
+ readonly amount: number
80
+ readonly status: 'held' | 'settled' | 'released'
81
+ readonly settledAmount: number | null
82
+ /** Units a settle used beyond what the grants could cover. */
83
+ readonly overdraft: number
84
+ readonly createdAt: Date
85
+ readonly settledAt: Date | null
86
+ }
87
+
88
+ export type GrantResult = { readonly grant: CreditGrant; readonly created: boolean }
89
+
90
+ export type ReserveResult =
91
+ | { readonly ok: true; readonly reservation: CreditReservation }
92
+ | { readonly ok: false; readonly reason: 'insufficient'; readonly available: number; readonly requested: number }
93
+
94
+ export interface CreditBalance {
95
+ /** Sum of `remaining` over grants that have not expired (reserved credits are already drawn out of it). */
96
+ readonly available: number
97
+ /** Sum of the amounts of reservations still held. */
98
+ readonly held: number
99
+ readonly grants: readonly { id: string; kind: string; remaining: number; expiresAt: Date | null }[]
100
+ }
101
+
102
+ export interface CreditLedgerEntry {
103
+ readonly id: string
104
+ readonly tenantId: string
105
+ readonly at: Date
106
+ readonly kind: 'grant' | 'reserve' | 'settle' | 'release' | 'expire'
107
+ /** Signed: what the entry did to the available balance. */
108
+ readonly amount: number
109
+ readonly grantId: string | null
110
+ readonly reservationId: string | null
111
+ readonly jobId: string | null
112
+ readonly availableAfter: number
113
+ }
114
+
115
+ export interface HistoryOptions {
116
+ readonly limit?: number
117
+ /** The `next` of a previous page. */
118
+ readonly before?: string
119
+ }
120
+
121
+ export interface HistoryPage {
122
+ readonly entries: CreditLedgerEntry[]
123
+ /** Pass as `before` for the next (older) page; `null` at the end. */
124
+ readonly next: string | null
125
+ }
126
+
127
+ export interface Credits {
128
+ grant(options: GrantOptions): Promise<GrantResult>
129
+ grantFor(tenantId: string, options: GrantOptions): Promise<GrantResult>
130
+ /**
131
+ * Holds `amount` for a job, drawn soonest-expiring first. A job id that already has a
132
+ * reservation returns it unchanged, whatever amount is asked this time.
133
+ */
134
+ reserve(options: { jobId: string; amount: number }): Promise<ReserveResult>
135
+ reserveFor(tenantId: string, options: { jobId: string; amount: number }): Promise<ReserveResult>
136
+ /**
137
+ * Records the actual usage of a reserved job. Less than reserved refunds the difference to the
138
+ * grants it came from (latest-expiring first; a refund to an expired grant is lost). More draws
139
+ * the excess and records what could not be covered as `overdraft`; it never fails for lack of
140
+ * balance. The same amount again is a no-op; a different amount, a released job or an unknown job throws.
141
+ */
142
+ settle(options: { jobId: string; amount: number }): Promise<CreditReservation>
143
+ settleFor(tenantId: string, options: { jobId: string; amount: number }): Promise<CreditReservation>
144
+ /** Gives back everything a held reservation drew. No reservation: `{ released: false, reservation: null }`; already released: `released: false`. A settled job throws. */
145
+ release(options: { jobId: string }): Promise<{ released: boolean; reservation: CreditReservation | null }>
146
+ releaseFor(tenantId: string, options: { jobId: string }): Promise<{ released: boolean; reservation: CreditReservation | null }>
147
+ /** The tenant's reservations still held, oldest first. */
148
+ held(): Promise<CreditReservation[]>
149
+ /** The given tenant's reservations still held, oldest first. */
150
+ heldFor(tenantId: string): Promise<CreditReservation[]>
151
+ balance(): Promise<CreditBalance>
152
+ balanceFor(tenantId: string): Promise<CreditBalance>
153
+ /** Ledger entries newest first. */
154
+ history(options?: HistoryOptions): Promise<HistoryPage>
155
+ historyFor(tenantId: string, options?: HistoryOptions): Promise<HistoryPage>
156
+ /** Writes one `expire` entry per grant past its expiry that still has credits, and zeroes it. For a cron; `balance` is right without it. Returns the number of grants expired. */
157
+ expire(): Promise<number>
158
+ expireFor(tenantId: string): Promise<number>
159
+ /**
160
+ * Sets the tenant's own low-balance line, or `null` to go back to the config's `lowBalanceThreshold`.
161
+ * Takes effect for the next operation. A change that puts the current balance under the new line
162
+ * (it was at-or-above the old line, or there was none) is a crossing and fires exactly as an
163
+ * operation's does, inside this call's transaction. Audited as `CreditAccount` `set_low_balance_line`.
164
+ * Throws ValidationError unless the line is a non-negative safe integer or null.
165
+ */
166
+ setLowBalanceLine(line: number | null): Promise<LowBalanceState>
167
+ setLowBalanceLineFor(tenantId: string, line: number | null): Promise<LowBalanceState>
168
+ /** The tenant's line, where it comes from, its available balance and whether that is under the line. Read-only. */
169
+ lowBalance(): Promise<LowBalanceState>
170
+ lowBalanceFor(tenantId: string): Promise<LowBalanceState>
171
+ }
172
+
173
+ const DEFAULT_HISTORY_LIMIT = 50
174
+ const MAX_HISTORY_LIMIT = 500
175
+
176
+ function toDate(value: unknown): Date {
177
+ if (value instanceof Date) return value
178
+ const text = String(value)
179
+ return new Date(/(Z|[+-]\d\d(:?\d\d)?)$/.test(text) ? text : `${text.replace(' ', 'T')}Z`)
180
+ }
181
+
182
+ function toDateOrNull(value: unknown): Date | null {
183
+ return value === null || value === undefined ? null : toDate(value)
184
+ }
185
+
186
+ function num(value: unknown): number {
187
+ return Number(value)
188
+ }
189
+
190
+ function numOrNull(value: unknown): number | null {
191
+ return value === null || value === undefined ? null : Number(value)
192
+ }
193
+
194
+ function rowToGrant(row: Record<string, unknown>): CreditGrant {
195
+ return {
196
+ id: row['id'] as string,
197
+ tenantId: row['tenant_id'] as string,
198
+ kind: row['kind'] as string,
199
+ amount: num(row['amount']),
200
+ remaining: num(row['remaining']),
201
+ grantedAt: toDate(row['granted_at']),
202
+ expiresAt: toDateOrNull(row['expires_at']),
203
+ sourceRef: (row['source_ref'] as string | null) ?? null,
204
+ metadata: row['metadata'] ?? null,
205
+ }
206
+ }
207
+
208
+ function rowToReservation(row: Record<string, unknown>): CreditReservation {
209
+ return {
210
+ id: row['id'] as string,
211
+ tenantId: row['tenant_id'] as string,
212
+ jobId: row['job_id'] as string,
213
+ amount: num(row['amount']),
214
+ status: row['status'] as CreditReservation['status'],
215
+ settledAmount: numOrNull(row['settled_amount']),
216
+ overdraft: num(row['overdraft']),
217
+ createdAt: toDate(row['created_at']),
218
+ settledAt: toDateOrNull(row['settled_at']),
219
+ }
220
+ }
221
+
222
+ function rowToEntry(row: Record<string, unknown>): CreditLedgerEntry {
223
+ return {
224
+ id: row['id'] as string,
225
+ tenantId: row['tenant_id'] as string,
226
+ at: toDate(row['at']),
227
+ kind: row['kind'] as CreditLedgerEntry['kind'],
228
+ amount: num(row['amount']),
229
+ grantId: (row['grant_id'] as string | null) ?? null,
230
+ reservationId: (row['reservation_id'] as string | null) ?? null,
231
+ jobId: (row['job_id'] as string | null) ?? null,
232
+ availableAfter: num(row['available_after']),
233
+ }
234
+ }
235
+
236
+ function requireUnits(name: string, value: number, min: 0 | 1): void {
237
+ if (!Number.isSafeInteger(value) || value < min) {
238
+ const detail = min === 1 ? 'must be a positive integer' : 'must be a non-negative integer'
239
+ throw new ValidationError({ [name]: [detail] }, `Credits ${name} ${detail}.`)
240
+ }
241
+ }
242
+
243
+ function requireJobId(jobId: string): void {
244
+ if (typeof jobId !== 'string' || jobId === '') {
245
+ throw new ValidationError({ jobId: ['must be a non-empty string'] }, 'Credits jobId must be a non-empty string.')
246
+ }
247
+ }
248
+
249
+ const LIVE = sql`remaining > 0 AND (expires_at IS NULL OR expires_at > now())`
250
+
251
+ interface Measured<T> {
252
+ readonly value: T
253
+ /** The line this operation crossed, if it did. */
254
+ readonly crossed?: { line: number; available: number }
255
+ readonly insufficient?: { jobId: string; requested: number; available: number }
256
+ }
257
+
258
+ export function createCredits(config: CreditsConfig = {}): Credits {
259
+ const threshold = config.lowBalanceThreshold
260
+ if (threshold !== undefined && (!Number.isSafeInteger(threshold) || threshold < 0)) {
261
+ throw new Error('[pipework] credits lowBalanceThreshold must be a non-negative integer.\n')
262
+ }
263
+
264
+ function callback(name: string, fn: () => void): void {
265
+ try {
266
+ fn()
267
+ } catch (err) {
268
+ getBaseLogger().error({ err }, `credits ${name} callback threw`)
269
+ void recordFault('credits-callback', `credits ${name} callback threw: ${faultMessage(err)}`, { callback: name })
270
+ }
271
+ }
272
+
273
+ // Runs `fn` in a transaction for the tenant: the Flow's own (joining the
274
+ // request's transaction) or, for the `...For` forms, an explicit one.
275
+ type InFlow = <R>(body: () => Promise<R>) => Promise<R>
276
+
277
+ async function run<T>(explicit: string | null, fn: (tx: DB, tenantId: string, inFlow: InFlow) => Promise<T>): Promise<T> {
278
+ if (explicit !== null) {
279
+ requireTenantId(explicit)
280
+ const flow = requireContext()
281
+ return inTenant(flow, explicit, tx => fn(tx, explicit, body => runForTenant(flow, explicit, tx, body)))
282
+ }
283
+ const flow = requireContext()
284
+ if (flow.tenant === null) {
285
+ throw new Error(
286
+ '[pipework] This credits method needs a tenant in the Flow.\n\n' +
287
+ ' Call it from an authenticated tenant route, or use the ...For(tenantId, ...) form\n' +
288
+ ' (grantFor, reserveFor, settleFor, ...) from a webhook or a job with no tenant.\n',
289
+ )
290
+ }
291
+ const tenantId = flow.tenant
292
+ // pipe.transaction already runs its body in a child Flow with the transaction bound.
293
+ return pipe.transaction((tx: DB) => fn(tx, tenantId, body => body()))
294
+ }
295
+
296
+ // The `...For` transaction is not bound into the Flow, and the Flow's tenant is not the
297
+ // explicit one: run `body` in a child Flow that is both, as pipe.transaction does.
298
+ function runForTenant<R>(flow: Flow, tenantId: string, tx: DB, body: () => Promise<R>): Promise<R> {
299
+ const dbName = flow.instance.config.defaultDatabaseName()
300
+ const child: Flow = { ...flow, tenant: tenantId, transactions: new Map(flow.transactions) }
301
+ child.transactions.set(dbName, bindTransaction(child, dbName, tx))
302
+ return runInContext(child, body)
303
+ }
304
+
305
+ function requireTenantId(tenantId: string): void {
306
+ if (typeof tenantId !== 'string' || tenantId === '') {
307
+ throw new ValidationError({ tenantId: ['must be a non-empty string'] }, 'Credits tenantId must be a non-empty string.')
308
+ }
309
+ }
310
+
311
+ // Every mutating operation takes the tenant's lock first, so operations on one
312
+ // tenant's credits run one after another; the grant rows are also locked FOR UPDATE.
313
+ async function lockTenant(tx: DB, tenantId: string): Promise<void> {
314
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${`pipework.credits:${tenantId}`}, 0))`)
315
+ }
316
+
317
+ async function availableOf(tx: DB, tenantId: string): Promise<number> {
318
+ const rows = await tx.execute(sql`
319
+ SELECT COALESCE(SUM(remaining), 0) AS n FROM credit_grant WHERE tenant_id = ${tenantId} AND ${LIVE}
320
+ `)
321
+ return num(rows[0]!['n'])
322
+ }
323
+
324
+ // The tenant's effective line, read in the operation's own transaction.
325
+ async function lineOf(tx: DB, tenantId: string): Promise<{ line: number | null; source: LowBalanceState['source']; stored: number | null }> {
326
+ const rows = await tx.execute(sql`SELECT low_balance_line AS line FROM credit_account WHERE tenant_id = ${tenantId}`)
327
+ const stored = rows.length === 0 ? null : numOrNull(rows[0]!['line'])
328
+ if (stored !== null) return { line: stored, source: 'tenant', stored }
329
+ return threshold === undefined ? { line: null, source: 'none', stored } : { line: threshold, source: 'default', stored }
330
+ }
331
+
332
+ // Applies the crossing rule to a balance change measured in this transaction; on a crossing
333
+ // awaits `onLowBalanceCrossed` in the tenant's Flow, so its error rolls the operation back.
334
+ async function cross<T>(tx: DB, tenantId: string, inFlow: InFlow, value: T, before: number, after: number): Promise<Measured<T>> {
335
+ if (after >= before) return { value }
336
+ const { line } = await lineOf(tx, tenantId)
337
+ if (line === null || before < line || after >= line) return { value }
338
+ const crossed = { line, available: after }
339
+ const fn = config.onLowBalanceCrossed
340
+ if (fn !== undefined) await inFlow(async () => { await fn({ tenantId, available: after, line }) })
341
+ return { value, crossed }
342
+ }
343
+
344
+ async function writeLedger(tx: DB, tenantId: string, entry: {
345
+ kind: CreditLedgerEntry['kind']; amount: number; grantId?: string | null; reservationId?: string | null; jobId?: string | null
346
+ }): Promise<void> {
347
+ const availableAfter = await availableOf(tx, tenantId)
348
+ const written = await tx.execute(sql`
349
+ INSERT INTO credit_ledger (tenant_id, at, kind, amount, grant_id, reservation_id, job_id, available_after)
350
+ VALUES (${tenantId}, clock_timestamp(), ${entry.kind}, ${entry.amount}, ${entry.grantId ?? null},
351
+ ${entry.reservationId ?? null}, ${entry.jobId ?? null}, ${availableAfter})
352
+ RETURNING id
353
+ `)
354
+ const flow = getContext()
355
+ if (flow === undefined) return
356
+ await writeInfraAudit(flow, tx, {
357
+ tenantId, entity: 'CreditLedger', verb: entry.kind, entityId: written[0]!['id'] as string,
358
+ after: {
359
+ kind: entry.kind, amount: entry.amount, jobId: entry.jobId ?? null, grantId: entry.grantId ?? null,
360
+ reservationId: entry.reservationId ?? null, availableAfter,
361
+ },
362
+ })
363
+ }
364
+
365
+ // Draws up to `need` from live grants, soonest-expiring first; returns what it drew.
366
+ async function drawFrom(tx: DB, tenantId: string, reservationId: string, need: number, firstSeq: number): Promise<number> {
367
+ const grants = await tx.execute(sql`
368
+ SELECT id, remaining FROM credit_grant WHERE tenant_id = ${tenantId} AND ${LIVE}
369
+ ORDER BY expires_at ASC NULLS LAST, granted_at, id FOR UPDATE
370
+ `)
371
+ let left = need
372
+ let seq = firstSeq
373
+ for (const grant of grants) {
374
+ if (left === 0) break
375
+ const take = Math.min(num(grant['remaining']), left)
376
+ await tx.execute(sql`UPDATE credit_grant SET remaining = remaining - ${take} WHERE id = ${grant['id'] as string}`)
377
+ await tx.execute(sql`
378
+ INSERT INTO credit_draw (tenant_id, reservation_id, grant_id, amount, seq)
379
+ VALUES (${tenantId}, ${reservationId}, ${grant['id'] as string}, ${take}, ${seq})
380
+ `)
381
+ left -= take
382
+ seq += 1
383
+ }
384
+ return need - left
385
+ }
386
+
387
+ // Gives back up to `amount` of a reservation's draws, latest draw first, to grants
388
+ // that have not expired; returns what reached a grant.
389
+ async function refund(tx: DB, tenantId: string, reservationId: string, amount: number): Promise<number> {
390
+ const draws = await tx.execute(sql`
391
+ SELECT id, grant_id, amount FROM credit_draw
392
+ WHERE tenant_id = ${tenantId} AND reservation_id = ${reservationId} AND amount > 0
393
+ ORDER BY seq DESC FOR UPDATE
394
+ `)
395
+ let left = amount
396
+ let refunded = 0
397
+ for (const draw of draws) {
398
+ if (left === 0) break
399
+ const take = Math.min(num(draw['amount']), left)
400
+ await tx.execute(sql`UPDATE credit_draw SET amount = amount - ${take} WHERE id = ${draw['id'] as string}`)
401
+ const back = await tx.execute(sql`
402
+ UPDATE credit_grant SET remaining = remaining + ${take}
403
+ WHERE id = ${draw['grant_id'] as string} AND (expires_at IS NULL OR expires_at > now())
404
+ RETURNING id
405
+ `)
406
+ if (back.length > 0) refunded += take
407
+ left -= take
408
+ }
409
+ return refunded
410
+ }
411
+
412
+ async function findReservation(tx: DB, tenantId: string, jobId: string, lock: boolean): Promise<CreditReservation | null> {
413
+ const rows = await tx.execute(lock
414
+ ? sql`SELECT * FROM credit_reservation WHERE tenant_id = ${tenantId} AND job_id = ${jobId} FOR UPDATE`
415
+ : sql`SELECT * FROM credit_reservation WHERE tenant_id = ${tenantId} AND job_id = ${jobId}`)
416
+ return rows.length === 0 ? null : rowToReservation(rows[0]!)
417
+ }
418
+
419
+ // Calls the callbacks once the operation's transaction body has finished.
420
+ function signal<T>(tenantId: string, measured: Measured<T>): T {
421
+ if (measured.insufficient !== undefined && config.onInsufficient !== undefined) {
422
+ const event = { tenantId, ...measured.insufficient }
423
+ const fn = config.onInsufficient
424
+ callback('onInsufficient', () => fn(event))
425
+ }
426
+ if (measured.crossed !== undefined) {
427
+ const available = measured.crossed.available
428
+ getBaseLogger().warn({ event: 'credits low balance', tenantId, available }, 'credits low balance')
429
+ const fn = config.onLowBalance
430
+ if (fn !== undefined) callback('onLowBalance', () => fn({ tenantId, available }))
431
+ }
432
+ return measured.value
433
+ }
434
+
435
+ async function doGrant(explicit: string | null, options: GrantOptions): Promise<GrantResult> {
436
+ requireUnits('amount', options.amount, 1)
437
+ if (typeof options.kind !== 'string' || options.kind === '') {
438
+ throw new ValidationError({ kind: ['must be a non-empty string'] }, 'Credits grant kind must be a non-empty string.')
439
+ }
440
+ if (options.sourceRef !== undefined && options.sourceRef === '') {
441
+ throw new ValidationError({ sourceRef: ['must not be empty'] }, 'Credits grant sourceRef must not be empty.')
442
+ }
443
+ return run(explicit, async (tx, tenantId) => {
444
+ await lockTenant(tx, tenantId)
445
+ if (options.sourceRef !== undefined) {
446
+ const existing = await tx.execute(sql`
447
+ SELECT * FROM credit_grant WHERE tenant_id = ${tenantId} AND source_ref = ${options.sourceRef}
448
+ `)
449
+ if (existing.length > 0) return { grant: rowToGrant(existing[0]!), created: false }
450
+ }
451
+ const metadata = options.metadata === undefined ? null : JSON.stringify(options.metadata)
452
+ const rows = await tx.execute(sql`
453
+ INSERT INTO credit_grant (tenant_id, kind, amount, remaining, granted_at, expires_at, source_ref, metadata)
454
+ VALUES (${tenantId}, ${options.kind}, ${options.amount}, ${options.amount}, now(),
455
+ ${options.expiresAt?.toISOString() ?? null}, ${options.sourceRef ?? null},
456
+ ${metadata === null ? sql`NULL` : sql`${metadata}::jsonb`})
457
+ RETURNING *
458
+ `)
459
+ const grant = rowToGrant(rows[0]!)
460
+ await writeLedger(tx, tenantId, { kind: 'grant', amount: options.amount, grantId: grant.id })
461
+ return { grant, created: true }
462
+ })
463
+ }
464
+
465
+ async function doReserve(explicit: string | null, options: { jobId: string; amount: number }): Promise<ReserveResult> {
466
+ requireJobId(options.jobId)
467
+ requireUnits('amount', options.amount, 1)
468
+ const done = await run(explicit, async (tx, tenantId, inFlow): Promise<{ tenantId: string; measured: Measured<ReserveResult> }> => {
469
+ await lockTenant(tx, tenantId)
470
+ const existing = await findReservation(tx, tenantId, options.jobId, false)
471
+ if (existing !== null) {
472
+ return { tenantId, measured: { value: { ok: true, reservation: existing } } }
473
+ }
474
+ const before = await availableOf(tx, tenantId)
475
+ if (before < options.amount) {
476
+ return {
477
+ tenantId,
478
+ measured: {
479
+ value: { ok: false, reason: 'insufficient', available: before, requested: options.amount },
480
+ insufficient: { jobId: options.jobId, requested: options.amount, available: before },
481
+ },
482
+ }
483
+ }
484
+ const inserted = await tx.execute(sql`
485
+ INSERT INTO credit_reservation (tenant_id, job_id, amount, status)
486
+ VALUES (${tenantId}, ${options.jobId}, ${options.amount}, 'held')
487
+ ON CONFLICT (tenant_id, job_id) DO NOTHING
488
+ RETURNING *
489
+ `)
490
+ if (inserted.length === 0) {
491
+ const winner = (await findReservation(tx, tenantId, options.jobId, false))!
492
+ return { tenantId, measured: { value: { ok: true, reservation: winner } } }
493
+ }
494
+ const reservation = rowToReservation(inserted[0]!)
495
+ await drawFrom(tx, tenantId, reservation.id, options.amount, 1)
496
+ await writeLedger(tx, tenantId, { kind: 'reserve', amount: -options.amount, reservationId: reservation.id, jobId: options.jobId })
497
+ return { tenantId, measured: await cross<ReserveResult>(tx, tenantId, inFlow, { ok: true, reservation }, before, before - options.amount) }
498
+ })
499
+ return signal(done.tenantId, done.measured)
500
+ }
501
+
502
+ async function doSettle(explicit: string | null, options: { jobId: string; amount: number }): Promise<CreditReservation> {
503
+ requireJobId(options.jobId)
504
+ requireUnits('amount', options.amount, 0)
505
+ const done = await run(explicit, async (tx, tenantId, inFlow) => {
506
+ await lockTenant(tx, tenantId)
507
+ const reservation = await findReservation(tx, tenantId, options.jobId, true)
508
+ if (reservation === null) {
509
+ throw new Error(`[pipework] credits.settle: no reservation for job "${options.jobId}".\n\n Call reserve({ jobId, amount }) before the job, with the same jobId.\n`)
510
+ }
511
+ if (reservation.status === 'released') {
512
+ throw new Error(`[pipework] credits.settle: the reservation for job "${options.jobId}" was released.\n`)
513
+ }
514
+ if (reservation.status === 'settled') {
515
+ if (reservation.settledAmount === options.amount) {
516
+ return { tenantId, measured: { value: reservation } as Measured<CreditReservation> }
517
+ }
518
+ throw new Error(
519
+ `[pipework] credits.settle: job "${options.jobId}" was already settled at ${reservation.settledAmount}, ` +
520
+ `not ${options.amount}.\n`,
521
+ )
522
+ }
523
+ const before = await availableOf(tx, tenantId)
524
+ const delta = options.amount - reservation.amount
525
+ let change = 0
526
+ let overdraft = 0
527
+ if (delta < 0) {
528
+ change = await refund(tx, tenantId, reservation.id, -delta)
529
+ } else if (delta > 0) {
530
+ const seqRows = await tx.execute(sql`SELECT COALESCE(MAX(seq), 0) AS n FROM credit_draw WHERE reservation_id = ${reservation.id}`)
531
+ const drawn = await drawFrom(tx, tenantId, reservation.id, delta, num(seqRows[0]!['n']) + 1)
532
+ change = -drawn
533
+ overdraft = delta - drawn
534
+ }
535
+ const rows = await tx.execute(sql`
536
+ UPDATE credit_reservation
537
+ SET status = 'settled', settled_amount = ${options.amount}, overdraft = ${overdraft}, settled_at = now()
538
+ WHERE id = ${reservation.id}
539
+ RETURNING *
540
+ `)
541
+ await writeLedger(tx, tenantId, { kind: 'settle', amount: change, reservationId: reservation.id, jobId: options.jobId })
542
+ return { tenantId, measured: await cross<CreditReservation>(tx, tenantId, inFlow, rowToReservation(rows[0]!), before, before + change) }
543
+ })
544
+ return signal(done.tenantId, done.measured)
545
+ }
546
+
547
+ async function doRelease(explicit: string | null, options: { jobId: string }): Promise<{ released: boolean; reservation: CreditReservation | null }> {
548
+ requireJobId(options.jobId)
549
+ return run(explicit, async (tx, tenantId) => {
550
+ await lockTenant(tx, tenantId)
551
+ const reservation = await findReservation(tx, tenantId, options.jobId, true)
552
+ if (reservation === null) return { released: false, reservation: null }
553
+ if (reservation.status === 'released') return { released: false, reservation }
554
+ if (reservation.status === 'settled') {
555
+ throw new Error(`[pipework] credits.release: job "${options.jobId}" was already settled; settled usage cannot be released.\n`)
556
+ }
557
+ const refunded = await refund(tx, tenantId, reservation.id, reservation.amount)
558
+ const rows = await tx.execute(sql`
559
+ UPDATE credit_reservation SET status = 'released', settled_at = now() WHERE id = ${reservation.id} RETURNING *
560
+ `)
561
+ await writeLedger(tx, tenantId, { kind: 'release', amount: refunded, reservationId: reservation.id, jobId: options.jobId })
562
+ return { released: true, reservation: rowToReservation(rows[0]!) }
563
+ })
564
+ }
565
+
566
+ async function doBalance(explicit: string | null): Promise<CreditBalance> {
567
+ return run(explicit, async (tx, tenantId) => {
568
+ const grants = await tx.execute(sql`
569
+ SELECT id, kind, remaining, expires_at FROM credit_grant WHERE tenant_id = ${tenantId} AND ${LIVE}
570
+ ORDER BY expires_at ASC NULLS LAST, granted_at, id
571
+ `)
572
+ const held = await tx.execute(sql`
573
+ SELECT COALESCE(SUM(amount), 0) AS n FROM credit_reservation WHERE tenant_id = ${tenantId} AND status = 'held'
574
+ `)
575
+ const list = grants.map(g => ({
576
+ id: g['id'] as string, kind: g['kind'] as string, remaining: num(g['remaining']), expiresAt: toDateOrNull(g['expires_at']),
577
+ }))
578
+ return { available: list.reduce((sum, g) => sum + g.remaining, 0), held: num(held[0]!['n']), grants: list }
579
+ })
580
+ }
581
+
582
+ async function doHeld(explicit: string | null): Promise<CreditReservation[]> {
583
+ return run(explicit, async (tx, tenantId) => {
584
+ const found = await tx.execute(sql`
585
+ SELECT * FROM credit_reservation WHERE tenant_id = ${tenantId} AND status = 'held' ORDER BY created_at, id
586
+ `)
587
+ return found.map(rowToReservation)
588
+ })
589
+ }
590
+
591
+ async function doHistory(explicit: string | null, options: HistoryOptions = {}): Promise<HistoryPage> {
592
+ const limit = options.limit ?? DEFAULT_HISTORY_LIMIT
593
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_HISTORY_LIMIT) {
594
+ throw new ValidationError({ limit: [`must be an integer from 1 to ${MAX_HISTORY_LIMIT}`] }, 'Credits history limit is out of range.')
595
+ }
596
+ let cursor: { at: string; id: string } | null = null
597
+ if (options.before !== undefined) {
598
+ const cut = options.before.lastIndexOf('|')
599
+ if (cut < 1) throw new ValidationError({ before: ['is not a cursor from history()'] }, 'Credits history cursor is not valid.')
600
+ cursor = { at: options.before.slice(0, cut), id: options.before.slice(cut + 1) }
601
+ }
602
+ return run(explicit, async (tx, tenantId) => {
603
+ const rows = await tx.execute(cursor === null
604
+ ? sql`
605
+ SELECT *, "at"::text AS at_text FROM credit_ledger WHERE tenant_id = ${tenantId}
606
+ ORDER BY "at" DESC, id DESC LIMIT ${limit + 1}`
607
+ : sql`
608
+ SELECT *, "at"::text AS at_text FROM credit_ledger
609
+ WHERE tenant_id = ${tenantId} AND ("at", id) < (${cursor.at}::timestamp, ${cursor.id}::uuid)
610
+ ORDER BY "at" DESC, id DESC LIMIT ${limit + 1}`)
611
+ const page = rows.slice(0, limit)
612
+ const last = page[page.length - 1]
613
+ return {
614
+ entries: page.map(rowToEntry),
615
+ next: rows.length > limit && last !== undefined ? `${last['at_text'] as string}|${last['id'] as string}` : null,
616
+ }
617
+ })
618
+ }
619
+
620
+ async function doExpire(explicit: string | null): Promise<number> {
621
+ const done = await run(explicit, async (tx, tenantId, inFlow) => {
622
+ await lockTenant(tx, tenantId)
623
+ const due = await tx.execute(sql`
624
+ SELECT id, remaining FROM credit_grant
625
+ WHERE tenant_id = ${tenantId} AND remaining > 0 AND expires_at IS NOT NULL AND expires_at <= now()
626
+ ORDER BY expires_at, id FOR UPDATE
627
+ `)
628
+ // `available` already leaves out an expired grant, so the balance the ledger last
629
+ // recorded is the live balance plus what the due grants still hold.
630
+ const lapsing = due.reduce((sum, grant) => sum + num(grant['remaining']), 0)
631
+ const before = (await availableOf(tx, tenantId)) + lapsing
632
+ for (const grant of due) {
633
+ await tx.execute(sql`UPDATE credit_grant SET remaining = 0 WHERE id = ${grant['id'] as string}`)
634
+ await writeLedger(tx, tenantId, { kind: 'expire', amount: -num(grant['remaining']), grantId: grant['id'] as string })
635
+ }
636
+ const after = await availableOf(tx, tenantId)
637
+ return { tenantId, measured: await cross<number>(tx, tenantId, inFlow, due.length, before, after) }
638
+ })
639
+ return signal(done.tenantId, done.measured)
640
+ }
641
+
642
+ async function stateOf(tx: DB, tenantId: string): Promise<LowBalanceState> {
643
+ const { line, source } = await lineOf(tx, tenantId)
644
+ const available = await availableOf(tx, tenantId)
645
+ return { line, source, available, below: line !== null && available < line }
646
+ }
647
+
648
+ async function doLowBalance(explicit: string | null): Promise<LowBalanceState> {
649
+ return run(explicit, (tx, tenantId) => stateOf(tx, tenantId))
650
+ }
651
+
652
+ async function doSetLine(explicit: string | null, line: number | null): Promise<LowBalanceState> {
653
+ if (line !== null) requireUnits('line', line, 0)
654
+ const done = await run(explicit, async (tx, tenantId, inFlow) => {
655
+ await lockTenant(tx, tenantId)
656
+ const old = await lineOf(tx, tenantId)
657
+ const written = await tx.execute(sql`
658
+ INSERT INTO credit_account (tenant_id, low_balance_line, updated_at) VALUES (${tenantId}, ${line}, now())
659
+ ON CONFLICT (tenant_id) DO UPDATE SET low_balance_line = EXCLUDED.low_balance_line, updated_at = now()
660
+ RETURNING id
661
+ `)
662
+ const flow = getContext()
663
+ if (flow !== undefined) {
664
+ await writeInfraAudit(flow, tx, {
665
+ tenantId, entity: 'CreditAccount', verb: 'set_low_balance_line', entityId: written[0]!['id'] as string,
666
+ before: { lowBalanceLine: old.stored }, after: { lowBalanceLine: line },
667
+ })
668
+ }
669
+ const state = await stateOf(tx, tenantId)
670
+ // Crossing: at-or-above the old effective line (or none), under the new one.
671
+ const wasAbove = old.line === null || state.available >= old.line
672
+ let measured: Measured<LowBalanceState> = { value: state }
673
+ if (state.below && wasAbove && state.line !== null) {
674
+ const crossed = { line: state.line, available: state.available }
675
+ const fn = config.onLowBalanceCrossed
676
+ if (fn !== undefined) await inFlow(async () => { await fn({ tenantId, ...crossed }) })
677
+ measured = { value: state, crossed }
678
+ }
679
+ return { tenantId, measured }
680
+ })
681
+ return signal(done.tenantId, done.measured)
682
+ }
683
+
684
+ return {
685
+ grant: o => doGrant(null, o),
686
+ grantFor: (t, o) => doGrant(t, o),
687
+ reserve: o => doReserve(null, o),
688
+ reserveFor: (t, o) => doReserve(t, o),
689
+ settle: o => doSettle(null, o),
690
+ settleFor: (t, o) => doSettle(t, o),
691
+ release: o => doRelease(null, o),
692
+ releaseFor: (t, o) => doRelease(t, o),
693
+ held: () => doHeld(null),
694
+ heldFor: t => doHeld(t),
695
+ balance: () => doBalance(null),
696
+ balanceFor: t => doBalance(t),
697
+ history: o => doHistory(null, o),
698
+ historyFor: (t, o) => doHistory(t, o),
699
+ expire: () => doExpire(null),
700
+ expireFor: t => doExpire(t),
701
+ setLowBalanceLine: l => doSetLine(null, l),
702
+ setLowBalanceLineFor: (t, l) => doSetLine(t, l),
703
+ lowBalance: () => doLowBalance(null),
704
+ lowBalanceFor: t => doLowBalance(t),
705
+ }
706
+ }