pipework 0.252.0 → 0.252.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (690) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/package.json +4 -3
  3. package/src/async/jobs/abortable-delay.ts +18 -0
  4. package/src/async/jobs/claims.ts +52 -0
  5. package/src/async/jobs/cron.ts +179 -0
  6. package/src/async/jobs/errors.ts +56 -0
  7. package/src/async/jobs/exclusion.ts +117 -0
  8. package/src/async/jobs/execute.ts +219 -0
  9. package/src/async/jobs/index.ts +10 -0
  10. package/src/async/jobs/listener.ts +135 -0
  11. package/src/async/jobs/namespace.ts +20 -0
  12. package/src/async/jobs/queue-door.ts +44 -0
  13. package/src/async/jobs/queue.ts +1012 -0
  14. package/src/async/jobs/run-script.ts +67 -0
  15. package/src/async/jobs/scheduler.ts +218 -0
  16. package/src/async/jobs/schedules-table.ts +37 -0
  17. package/src/async/jobs/serialize-error.ts +48 -0
  18. package/src/async/jobs/table.ts +191 -0
  19. package/src/async/pipeline/define.ts +53 -0
  20. package/src/async/pipeline/errors.ts +35 -0
  21. package/src/async/pipeline/execute.ts +53 -0
  22. package/src/async/pipeline/index.ts +13 -0
  23. package/src/async/pipeline/namespace.ts +12 -0
  24. package/src/async/pipeline/resume.ts +67 -0
  25. package/src/async/pipeline/run-steps.ts +98 -0
  26. package/src/async/pipeline/types.ts +57 -0
  27. package/src/async/state-machine/define.ts +64 -0
  28. package/src/async/state-machine/errors.ts +29 -0
  29. package/src/async/state-machine/index.ts +3 -0
  30. package/src/async/state-machine/namespace.ts +12 -0
  31. package/src/async/state-machine/transition.ts +90 -0
  32. package/src/async/state-machine/types.ts +37 -0
  33. package/src/async/steps/errors.ts +31 -0
  34. package/src/async/steps/graph.ts +1214 -0
  35. package/src/async/steps/hash.ts +40 -0
  36. package/src/async/steps/index.ts +7 -0
  37. package/src/async/steps/namespace.ts +7 -0
  38. package/src/async/steps/table.ts +97 -0
  39. package/src/async/steps/types.ts +318 -0
  40. package/src/audit/chain/index.ts +9 -0
  41. package/src/audit/chain/install.ts +113 -0
  42. package/src/audit/chain/retire.ts +52 -0
  43. package/src/audit/chain/verify.ts +257 -0
  44. package/src/audit/query/cursor.ts +37 -0
  45. package/src/audit/query/index.ts +9 -0
  46. package/src/audit/query/query.ts +223 -0
  47. package/src/audit/schema/audit-record-fields.ts +68 -0
  48. package/src/audit/schema/audit-record.ts +23 -0
  49. package/src/auth/auth/audit.ts +31 -0
  50. package/src/auth/auth/chain.ts +84 -0
  51. package/src/auth/auth/cookie.ts +136 -0
  52. package/src/auth/auth/email-links.ts +311 -0
  53. package/src/auth/auth/guard.ts +128 -0
  54. package/src/auth/auth/index.ts +18 -0
  55. package/src/auth/auth/multi-org.ts +337 -0
  56. package/src/auth/auth/namespace.ts +34 -0
  57. package/src/auth/auth/parse-expiry.ts +13 -0
  58. package/src/auth/auth/passwords.ts +110 -0
  59. package/src/auth/auth/schema.ts +110 -0
  60. package/src/auth/auth/sessions.ts +565 -0
  61. package/src/auth/auth/sign-in.ts +156 -0
  62. package/src/auth/auth/signing-keys.ts +69 -0
  63. package/src/auth/auth/types.ts +26 -0
  64. package/src/auth/auth/uuid.ts +10 -0
  65. package/src/auth/rbac/cache.ts +55 -0
  66. package/src/auth/rbac/check.ts +30 -0
  67. package/src/auth/rbac/create.ts +35 -0
  68. package/src/auth/rbac/index.ts +5 -0
  69. package/src/auth/rbac/middleware.ts +20 -0
  70. package/src/auth/rbac/namespace.ts +15 -0
  71. package/src/auth/rbac/resolve.ts +31 -0
  72. package/src/auth/rbac/schema.sql +31 -0
  73. package/src/auth/rbac/seed.ts +67 -0
  74. package/src/auth/rbac/tables.ts +32 -0
  75. package/src/auth/rbac/types.ts +65 -0
  76. package/src/auth/tenant/extract.ts +33 -0
  77. package/src/auth/tenant/in-tenant.ts +49 -0
  78. package/src/auth/tenant/index.ts +7 -0
  79. package/src/auth/tenant/namespace.ts +23 -0
  80. package/src/auth/tenant/propagate.ts +20 -0
  81. package/src/auth/tenant/rls.ts +120 -0
  82. package/src/auth/tenant/scope.ts +33 -0
  83. package/src/auth/tenant/scoped-db.ts +581 -0
  84. package/src/auth/tenant/types.ts +14 -0
  85. package/src/auth/tenant/validate.ts +27 -0
  86. package/src/cli/commands/add.ts +158 -0
  87. package/src/cli/commands/alerts.ts +98 -0
  88. package/src/cli/commands/check.ts +1418 -0
  89. package/src/cli/commands/codemod.ts +78 -0
  90. package/src/cli/commands/db.ts +356 -0
  91. package/src/cli/commands/dev.ts +101 -0
  92. package/src/cli/commands/generate.ts +75 -0
  93. package/src/cli/commands/init.ts +36 -0
  94. package/src/cli/commands/install.ts +28 -0
  95. package/src/cli/commands/lint.ts +62 -0
  96. package/src/cli/commands/migrate.ts +132 -0
  97. package/src/cli/commands/openapi.ts +63 -0
  98. package/src/cli/commands/remove.ts +72 -0
  99. package/src/cli/commands/run.ts +50 -0
  100. package/src/cli/commands/scope.ts +54 -0
  101. package/src/cli/commands/serve.ts +68 -0
  102. package/src/cli/commands/shard.ts +31 -0
  103. package/src/cli/commands/shared/config-pipework-version.ts +28 -0
  104. package/src/cli/commands/shared/find-config.ts +22 -0
  105. package/src/cli/commands/shared/load-config.ts +35 -0
  106. package/src/cli/commands/shared/pnpm-install.ts +95 -0
  107. package/src/cli/commands/shared/report-removed.ts +8 -0
  108. package/src/cli/commands/sync.ts +32 -0
  109. package/src/cli/commands/test.ts +307 -0
  110. package/src/cli/commands/upgrade.ts +48 -0
  111. package/src/cli/dev-bootstrap.ts +24 -0
  112. package/src/cli/errors.ts +10 -0
  113. package/src/cli/index.ts +303 -0
  114. package/src/cli/output.ts +17 -0
  115. package/src/cli/port.ts +15 -0
  116. package/src/cli/surface-kinds.ts +20 -0
  117. package/src/client/index.ts +4 -0
  118. package/src/client/poll.ts +130 -0
  119. package/src/client/report-fault.ts +119 -0
  120. package/src/core/config/app-pool.ts +19 -0
  121. package/src/core/config/aws-env.ts +29 -0
  122. package/src/core/config/base-dir.ts +24 -0
  123. package/src/core/config/discover.ts +64 -0
  124. package/src/core/config/env-resolve.ts +87 -0
  125. package/src/core/config/env-types.ts +116 -0
  126. package/src/core/config/env.ts +55 -0
  127. package/src/core/config/errors.ts +9 -0
  128. package/src/core/config/import-config.ts +40 -0
  129. package/src/core/config/index.ts +8 -0
  130. package/src/core/config/load.ts +417 -0
  131. package/src/core/config/namespace.ts +14 -0
  132. package/src/core/config/resolve.ts +195 -0
  133. package/src/core/config/schema.ts +488 -0
  134. package/src/core/config/statement-observer.ts +48 -0
  135. package/src/core/config/ts-inline-hooks.ts +75 -0
  136. package/src/core/config/ts-register.ts +4 -0
  137. package/src/core/config/ts-resolve-hooks.ts +101 -0
  138. package/src/core/errors/base.ts +49 -0
  139. package/src/core/errors/http.ts +146 -0
  140. package/src/core/errors/index.ts +2 -0
  141. package/src/core/errors/namespace.ts +31 -0
  142. package/src/core/lifecycle/drain.ts +33 -0
  143. package/src/core/lifecycle/health.ts +72 -0
  144. package/src/core/lifecycle/index.ts +4 -0
  145. package/src/core/lifecycle/namespace.ts +7 -0
  146. package/src/core/lifecycle/startup.ts +102 -0
  147. package/src/core/lifecycle/types.ts +36 -0
  148. package/src/core/logging/context.ts +60 -0
  149. package/src/core/logging/error-chain.ts +68 -0
  150. package/src/core/logging/index.ts +5 -0
  151. package/src/core/logging/logger.ts +65 -0
  152. package/src/core/logging/namespace.ts +16 -0
  153. package/src/core/logging/proxy.ts +22 -0
  154. package/src/core/logging/redact-match.ts +24 -0
  155. package/src/core/logging/types.ts +47 -0
  156. package/src/core/pipework.ts +566 -0
  157. package/src/core/runtime/broker.ts +69 -0
  158. package/src/core/surface/http.ts +133 -0
  159. package/src/core/surface/index.ts +11 -0
  160. package/src/core/surface/job-timeout.ts +40 -0
  161. package/src/core/surface/namespace.ts +13 -0
  162. package/src/core/surface/script.ts +33 -0
  163. package/src/core/surface/start.ts +142 -0
  164. package/src/core/surface/types.ts +225 -0
  165. package/src/core/surface/validate.ts +88 -0
  166. package/src/core/surface/worker.ts +528 -0
  167. package/src/core/validation/connectivity.ts +49 -0
  168. package/src/core/validation/domain.ts +99 -0
  169. package/src/core/validation/index.ts +5 -0
  170. package/src/core/validation/queue-tables.ts +312 -0
  171. package/src/core/validation/rls-role.ts +138 -0
  172. package/src/core/validation/startup.ts +51 -0
  173. package/src/core/validation/surfaces.ts +65 -0
  174. package/src/core/validation/types.ts +25 -0
  175. package/src/data/db/advisory-lock.ts +55 -0
  176. package/src/data/db/aggregates.ts +172 -0
  177. package/src/data/db/array-elements.ts +111 -0
  178. package/src/data/db/bulk-set.ts +53 -0
  179. package/src/data/db/col/index.ts +14 -0
  180. package/src/data/db/composed.ts +53 -0
  181. package/src/data/db/db.ts +276 -0
  182. package/src/data/db/excluded.ts +16 -0
  183. package/src/data/db/expressions.ts +473 -0
  184. package/src/data/db/filter.ts +90 -0
  185. package/src/data/db/function-source.ts +114 -0
  186. package/src/data/db/identifiers.ts +16 -0
  187. package/src/data/db/idx/index.ts +9 -0
  188. package/src/data/db/in-list.ts +49 -0
  189. package/src/data/db/index.ts +9 -0
  190. package/src/data/db/insert-split.ts +137 -0
  191. package/src/data/db/instrumentation.ts +82 -0
  192. package/src/data/db/maintenance.ts +28 -0
  193. package/src/data/db/namespace.ts +107 -0
  194. package/src/data/db/owner-labels.ts +9 -0
  195. package/src/data/db/pool.ts +187 -0
  196. package/src/data/db/query-label.ts +13 -0
  197. package/src/data/db/query-watch.ts +116 -0
  198. package/src/data/db/raw-client-warning.ts +37 -0
  199. package/src/data/db/readonly.ts +23 -0
  200. package/src/data/db/reservation.ts +151 -0
  201. package/src/data/db/schema-features.ts +14 -0
  202. package/src/data/db/serializable.ts +83 -0
  203. package/src/data/db/sql.ts +1 -0
  204. package/src/data/db/timeouts.ts +76 -0
  205. package/src/data/db/types.ts +18 -0
  206. package/src/data/db/unbound.ts +33 -0
  207. package/src/data/db/values.ts +29 -0
  208. package/src/data/domain/brand.ts +15 -0
  209. package/src/data/domain/build-factory.ts +193 -0
  210. package/src/data/domain/build-schema.ts +161 -0
  211. package/src/data/domain/build-table.ts +355 -0
  212. package/src/data/domain/define.ts +239 -0
  213. package/src/data/domain/external-registry.ts +34 -0
  214. package/src/data/domain/external.ts +77 -0
  215. package/src/data/domain/field.ts +279 -0
  216. package/src/data/domain/index.ts +13 -0
  217. package/src/data/domain/naming.ts +6 -0
  218. package/src/data/domain/projections.ts +57 -0
  219. package/src/data/domain/trace-safe-projection.ts +31 -0
  220. package/src/data/domain/traits.ts +108 -0
  221. package/src/data/domain/types.ts +289 -0
  222. package/src/data/jsonb/composed.ts +18 -0
  223. package/src/data/jsonb/index.ts +1 -0
  224. package/src/data/jsonb/namespace.ts +30 -0
  225. package/src/data/jsonb/query.ts +86 -0
  226. package/src/data/migrate/append-only-generate.ts +47 -0
  227. package/src/data/migrate/applied.ts +47 -0
  228. package/src/data/migrate/apply.ts +309 -0
  229. package/src/data/migrate/check.ts +119 -0
  230. package/src/data/migrate/diff.ts +357 -0
  231. package/src/data/migrate/door-generate.ts +130 -0
  232. package/src/data/migrate/framework-rls.ts +58 -0
  233. package/src/data/migrate/generate.ts +201 -0
  234. package/src/data/migrate/history.ts +215 -0
  235. package/src/data/migrate/index.ts +9 -0
  236. package/src/data/migrate/internal-definitions.ts +175 -0
  237. package/src/data/migrate/post-process.ts +73 -0
  238. package/src/data/migrate/pragma.ts +73 -0
  239. package/src/data/migrate/provision-framework.ts +147 -0
  240. package/src/data/migrate/rls-generate.ts +61 -0
  241. package/src/data/migrate/run.ts +124 -0
  242. package/src/data/migrate/safety.ts +167 -0
  243. package/src/data/migrate/snapshot.ts +368 -0
  244. package/src/data/migrate/sql-emit.ts +392 -0
  245. package/src/data/migrate/statements.ts +312 -0
  246. package/src/data/query/alias.ts +126 -0
  247. package/src/data/query/batch.ts +8 -0
  248. package/src/data/query/cache/core/cache.ts +78 -0
  249. package/src/data/query/cache/core/index.ts +1 -0
  250. package/src/data/query/cache/core/types.ts +29 -0
  251. package/src/data/query/cache/readme.md +230 -0
  252. package/src/data/query/casing.ts +76 -0
  253. package/src/data/query/collation.ts +11 -0
  254. package/src/data/query/column-builder.ts +370 -0
  255. package/src/data/query/column.ts +150 -0
  256. package/src/data/query/entity.ts +42 -0
  257. package/src/data/query/errors.ts +33 -0
  258. package/src/data/query/index.ts +14 -0
  259. package/src/data/query/logger.ts +47 -0
  260. package/src/data/query/operations.ts +45 -0
  261. package/src/data/query/pg-core/alias.ts +12 -0
  262. package/src/data/query/pg-core/checks.ts +32 -0
  263. package/src/data/query/pg-core/columns/all.ts +71 -0
  264. package/src/data/query/pg-core/columns/bigint.ts +110 -0
  265. package/src/data/query/pg-core/columns/bigserial.ts +128 -0
  266. package/src/data/query/pg-core/columns/boolean.ts +43 -0
  267. package/src/data/query/pg-core/columns/char.ts +85 -0
  268. package/src/data/query/pg-core/columns/cidr.ts +43 -0
  269. package/src/data/query/pg-core/columns/common.ts +366 -0
  270. package/src/data/query/pg-core/columns/custom.ts +232 -0
  271. package/src/data/query/pg-core/columns/date.common.ts +15 -0
  272. package/src/data/query/pg-core/columns/date.ts +112 -0
  273. package/src/data/query/pg-core/columns/double-precision.ts +55 -0
  274. package/src/data/query/pg-core/columns/enum.ts +201 -0
  275. package/src/data/query/pg-core/columns/index.ts +35 -0
  276. package/src/data/query/pg-core/columns/inet.ts +43 -0
  277. package/src/data/query/pg-core/columns/int.common.ts +57 -0
  278. package/src/data/query/pg-core/columns/integer.ts +53 -0
  279. package/src/data/query/pg-core/columns/interval.ts +83 -0
  280. package/src/data/query/pg-core/columns/json.ts +58 -0
  281. package/src/data/query/pg-core/columns/jsonb.ts +56 -0
  282. package/src/data/query/pg-core/columns/line.ts +117 -0
  283. package/src/data/query/pg-core/columns/macaddr.ts +43 -0
  284. package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
  285. package/src/data/query/pg-core/columns/numeric.ts +227 -0
  286. package/src/data/query/pg-core/columns/point.ts +127 -0
  287. package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
  288. package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
  289. package/src/data/query/pg-core/columns/real.ts +58 -0
  290. package/src/data/query/pg-core/columns/serial.ts +55 -0
  291. package/src/data/query/pg-core/columns/smallint.ts +53 -0
  292. package/src/data/query/pg-core/columns/smallserial.ts +60 -0
  293. package/src/data/query/pg-core/columns/text.ts +67 -0
  294. package/src/data/query/pg-core/columns/time.ts +72 -0
  295. package/src/data/query/pg-core/columns/timestamp.ts +163 -0
  296. package/src/data/query/pg-core/columns/uuid.ts +51 -0
  297. package/src/data/query/pg-core/columns/varchar.ts +89 -0
  298. package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
  299. package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
  300. package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
  301. package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
  302. package/src/data/query/pg-core/db.ts +674 -0
  303. package/src/data/query/pg-core/dialect.ts +1404 -0
  304. package/src/data/query/pg-core/expressions.ts +25 -0
  305. package/src/data/query/pg-core/foreign-keys.ts +130 -0
  306. package/src/data/query/pg-core/index.ts +21 -0
  307. package/src/data/query/pg-core/indexes.ts +276 -0
  308. package/src/data/query/pg-core/policies.ts +56 -0
  309. package/src/data/query/pg-core/primary-keys.ts +62 -0
  310. package/src/data/query/pg-core/query-builders/count.ts +86 -0
  311. package/src/data/query/pg-core/query-builders/delete.ts +293 -0
  312. package/src/data/query/pg-core/query-builders/index.ts +7 -0
  313. package/src/data/query/pg-core/query-builders/insert.ts +452 -0
  314. package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
  315. package/src/data/query/pg-core/query-builders/query.ts +157 -0
  316. package/src/data/query/pg-core/query-builders/raw.ts +49 -0
  317. package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
  318. package/src/data/query/pg-core/query-builders/select.ts +1346 -0
  319. package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
  320. package/src/data/query/pg-core/query-builders/update.ts +626 -0
  321. package/src/data/query/pg-core/roles.ts +41 -0
  322. package/src/data/query/pg-core/schema.ts +72 -0
  323. package/src/data/query/pg-core/sequence.ts +41 -0
  324. package/src/data/query/pg-core/session.ts +292 -0
  325. package/src/data/query/pg-core/subquery.ts +46 -0
  326. package/src/data/query/pg-core/table.ts +252 -0
  327. package/src/data/query/pg-core/unique-constraint.ts +73 -0
  328. package/src/data/query/pg-core/utils/array.ts +95 -0
  329. package/src/data/query/pg-core/utils/index.ts +1 -0
  330. package/src/data/query/pg-core/utils.ts +104 -0
  331. package/src/data/query/pg-core/view-base.ts +14 -0
  332. package/src/data/query/pg-core/view-common.ts +1 -0
  333. package/src/data/query/pg-core/view.ts +435 -0
  334. package/src/data/query/pg-core/with-builder.ts +41 -0
  335. package/src/data/query/postgres-js/README.md +46 -0
  336. package/src/data/query/postgres-js/array-parser.ts +45 -0
  337. package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
  338. package/src/data/query/postgres-js/driver.ts +163 -0
  339. package/src/data/query/postgres-js/index.ts +2 -0
  340. package/src/data/query/postgres-js/session.ts +234 -0
  341. package/src/data/query/primary-key.ts +11 -0
  342. package/src/data/query/query-builders/query-builder.ts +19 -0
  343. package/src/data/query/query-builders/select.types.ts +175 -0
  344. package/src/data/query/query-promise.ts +35 -0
  345. package/src/data/query/relations.ts +725 -0
  346. package/src/data/query/runnable-query.ts +12 -0
  347. package/src/data/query/selection-proxy.ts +121 -0
  348. package/src/data/query/session.ts +8 -0
  349. package/src/data/query/sql/expressions/conditions.ts +741 -0
  350. package/src/data/query/sql/expressions/index.ts +2 -0
  351. package/src/data/query/sql/expressions/select.ts +45 -0
  352. package/src/data/query/sql/functions/aggregate.ts +129 -0
  353. package/src/data/query/sql/functions/index.ts +2 -0
  354. package/src/data/query/sql/functions/vector.ts +178 -0
  355. package/src/data/query/sql/index.ts +3 -0
  356. package/src/data/query/sql/sql.ts +794 -0
  357. package/src/data/query/subquery.ts +53 -0
  358. package/src/data/query/table.ts +208 -0
  359. package/src/data/query/table.utils.ts +2 -0
  360. package/src/data/query/tracing-utils.ts +3 -0
  361. package/src/data/query/tracing.ts +11 -0
  362. package/src/data/query/utils.ts +357 -0
  363. package/src/data/query/version.ts +4 -0
  364. package/src/data/query/view-common.ts +1 -0
  365. package/src/data/schema/branded.ts +13 -0
  366. package/src/data/schema/generated/coerce.ts +5 -0
  367. package/src/data/schema/generated/combinator.ts +6 -0
  368. package/src/data/schema/generated/encoding.ts +5 -0
  369. package/src/data/schema/generated/error.ts +6 -0
  370. package/src/data/schema/generated/format.ts +5 -0
  371. package/src/data/schema/generated/type-constructor.ts +10 -0
  372. package/src/data/schema/generated/type.ts +6 -0
  373. package/src/data/schema/index.ts +6 -0
  374. package/src/data/schema/json-schema.ts +33 -0
  375. package/src/data/schema/namespace.ts +55 -0
  376. package/src/data/schema/parse.ts +39 -0
  377. package/src/data/schema/types.ts +39 -0
  378. package/src/data/temporal/append-only.ts +134 -0
  379. package/src/data/temporal/close.ts +27 -0
  380. package/src/data/temporal/columns.ts +10 -0
  381. package/src/data/temporal/definition-queries.ts +70 -0
  382. package/src/data/temporal/errors.ts +10 -0
  383. package/src/data/temporal/get-current.ts +22 -0
  384. package/src/data/temporal/index.ts +9 -0
  385. package/src/data/temporal/namespace.ts +36 -0
  386. package/src/data/temporal/query.ts +25 -0
  387. package/src/data/temporal/resolve.ts +17 -0
  388. package/src/data/temporal/revise.ts +67 -0
  389. package/src/data/temporal/types.ts +22 -0
  390. package/src/data/valve/artifact.ts +33 -0
  391. package/src/data/valve/contract-diff.ts +21 -0
  392. package/src/data/valve/cross.ts +57 -0
  393. package/src/data/valve/crossing-ledger.ts +50 -0
  394. package/src/data/valve/decision.ts +44 -0
  395. package/src/data/valve/durable-crossing.ts +52 -0
  396. package/src/data/valve/execute-crossing.ts +77 -0
  397. package/src/data/valve/faucet.ts +56 -0
  398. package/src/data/valve/gate.ts +114 -0
  399. package/src/data/valve/graph.ts +77 -0
  400. package/src/data/valve/index.ts +53 -0
  401. package/src/data/valve/namespace.ts +57 -0
  402. package/src/data/valve/registry.ts +50 -0
  403. package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
  404. package/src/data/valve/schema/valve-crossing.ts +9 -0
  405. package/src/data/valve/supply.ts +154 -0
  406. package/src/data/valve/types.ts +143 -0
  407. package/src/data/vector/columns.ts +27 -0
  408. package/src/data/vector/index.ts +4 -0
  409. package/src/data/vector/namespace.ts +33 -0
  410. package/src/data/vector/query.ts +85 -0
  411. package/src/data/vector/validate.ts +16 -0
  412. package/src/data/write/audit/actor.ts +15 -0
  413. package/src/data/write/audit/audit-columns.ts +18 -0
  414. package/src/data/write/audit/audit-select.ts +111 -0
  415. package/src/data/write/audit/bound-transaction.ts +14 -0
  416. package/src/data/write/audit/cte-compile.ts +32 -0
  417. package/src/data/write/audit/decode-returning.ts +47 -0
  418. package/src/data/write/audit/final-select.ts +43 -0
  419. package/src/data/write/audit/infra-audit.ts +36 -0
  420. package/src/data/write/audit/primary-key.ts +65 -0
  421. package/src/data/write/audit/projection.ts +41 -0
  422. package/src/data/write/audit/read-touches.ts +86 -0
  423. package/src/data/write/audit/touch-match.ts +78 -0
  424. package/src/data/write/audit/verb-delete.ts +62 -0
  425. package/src/data/write/audit/verb-insert.ts +36 -0
  426. package/src/data/write/audit/verb-select.ts +47 -0
  427. package/src/data/write/audit/verb-update.ts +60 -0
  428. package/src/data/write/audit/verb-upsert.ts +104 -0
  429. package/src/data/write/conflict.ts +43 -0
  430. package/src/data/write/delete.ts +38 -0
  431. package/src/data/write/execute.ts +243 -0
  432. package/src/data/write/index.ts +11 -0
  433. package/src/data/write/insert.ts +63 -0
  434. package/src/data/write/state.ts +43 -0
  435. package/src/data/write/update.ts +54 -0
  436. package/src/data/write/upsert.ts +58 -0
  437. package/src/data/write/values.ts +19 -0
  438. package/src/index.ts +230 -0
  439. package/src/infra/access-links/code.ts +70 -0
  440. package/src/infra/access-links/handle.ts +892 -0
  441. package/src/infra/access-links/index.ts +9 -0
  442. package/src/infra/access-links/namespace.ts +30 -0
  443. package/src/infra/access-links/person-check.ts +177 -0
  444. package/src/infra/access-links/schema.ts +71 -0
  445. package/src/infra/alerts/async-sources.ts +231 -0
  446. package/src/infra/alerts/audit.ts +36 -0
  447. package/src/infra/alerts/beat.ts +123 -0
  448. package/src/infra/alerts/client-faults.ts +111 -0
  449. package/src/infra/alerts/faults.ts +156 -0
  450. package/src/infra/alerts/handle.ts +778 -0
  451. package/src/infra/alerts/http.ts +192 -0
  452. package/src/infra/alerts/index.ts +12 -0
  453. package/src/infra/alerts/infra-sources.ts +180 -0
  454. package/src/infra/alerts/namespace.ts +81 -0
  455. package/src/infra/alerts/redact.ts +21 -0
  456. package/src/infra/alerts/request-faults.ts +41 -0
  457. package/src/infra/alerts/schema.ts +124 -0
  458. package/src/infra/alerts/sinks.ts +125 -0
  459. package/src/infra/alerts/source-options.ts +26 -0
  460. package/src/infra/alerts/sources.ts +503 -0
  461. package/src/infra/alerts/subsystem-sources.ts +137 -0
  462. package/src/infra/audit/emit.ts +87 -0
  463. package/src/infra/audit/index.ts +2 -0
  464. package/src/infra/audit/namespace.ts +22 -0
  465. package/src/infra/audit/schema.sql +35 -0
  466. package/src/infra/audit/types.ts +18 -0
  467. package/src/infra/aws/credentials.ts +147 -0
  468. package/src/infra/aws/sigv4.ts +140 -0
  469. package/src/infra/cache/create.ts +58 -0
  470. package/src/infra/cache/entry.ts +81 -0
  471. package/src/infra/cache/index.ts +4 -0
  472. package/src/infra/cache/namespace.ts +10 -0
  473. package/src/infra/cache/tenant.ts +93 -0
  474. package/src/infra/cache/types.ts +24 -0
  475. package/src/infra/crawl/audit.ts +35 -0
  476. package/src/infra/crawl/errors.ts +25 -0
  477. package/src/infra/crawl/handle.ts +693 -0
  478. package/src/infra/crawl/index.ts +9 -0
  479. package/src/infra/crawl/links.ts +160 -0
  480. package/src/infra/crawl/namespace.ts +36 -0
  481. package/src/infra/crawl/robots.ts +100 -0
  482. package/src/infra/crawl/schema.ts +89 -0
  483. package/src/infra/credit-line/handle.ts +883 -0
  484. package/src/infra/credit-line/index.ts +6 -0
  485. package/src/infra/credit-line/namespace.ts +7 -0
  486. package/src/infra/credit-line/schema.ts +96 -0
  487. package/src/infra/credits/handle.ts +706 -0
  488. package/src/infra/credits/index.ts +5 -0
  489. package/src/infra/credits/namespace.ts +7 -0
  490. package/src/infra/credits/schema.ts +83 -0
  491. package/src/infra/email/audit.ts +36 -0
  492. package/src/infra/email/inbound.ts +622 -0
  493. package/src/infra/email/index.ts +11 -0
  494. package/src/infra/email/mail.ts +517 -0
  495. package/src/infra/email/mime.ts +447 -0
  496. package/src/infra/email/namespace.ts +22 -0
  497. package/src/infra/email/providers.ts +111 -0
  498. package/src/infra/email/schema.ts +96 -0
  499. package/src/infra/email/sns.ts +200 -0
  500. package/src/infra/email/unsubscribe.ts +44 -0
  501. package/src/infra/fetch/address.ts +88 -0
  502. package/src/infra/fetch/connect-retry.ts +87 -0
  503. package/src/infra/fetch/handle.ts +329 -0
  504. package/src/infra/fetch/index.ts +7 -0
  505. package/src/infra/fetch/namespace.ts +70 -0
  506. package/src/infra/idempotency/index.ts +3 -0
  507. package/src/infra/idempotency/middleware.ts +87 -0
  508. package/src/infra/idempotency/store.ts +92 -0
  509. package/src/infra/idempotency/types.ts +30 -0
  510. package/src/infra/join-requests/handle.ts +312 -0
  511. package/src/infra/join-requests/index.ts +2 -0
  512. package/src/infra/join-requests/namespace.ts +7 -0
  513. package/src/infra/join-requests/schema.ts +33 -0
  514. package/src/infra/outbox/core.ts +165 -0
  515. package/src/infra/quota/errors.ts +32 -0
  516. package/src/infra/quota/handle.ts +215 -0
  517. package/src/infra/quota/index.ts +5 -0
  518. package/src/infra/quota/namespace.ts +7 -0
  519. package/src/infra/quota/schema.ts +15 -0
  520. package/src/infra/storage/backend.ts +34 -0
  521. package/src/infra/storage/handle.ts +513 -0
  522. package/src/infra/storage/index.ts +9 -0
  523. package/src/infra/storage/local.ts +104 -0
  524. package/src/infra/storage/namespace.ts +13 -0
  525. package/src/infra/storage/s3.ts +257 -0
  526. package/src/infra/storage/schema.ts +24 -0
  527. package/src/infra/support/handle.ts +854 -0
  528. package/src/infra/support/http.ts +229 -0
  529. package/src/infra/support/index.ts +7 -0
  530. package/src/infra/support/namespace.ts +12 -0
  531. package/src/infra/support/schema.ts +96 -0
  532. package/src/infra/webhook/inbound.ts +45 -0
  533. package/src/infra/webhook/index.ts +5 -0
  534. package/src/infra/webhook/namespace.ts +24 -0
  535. package/src/infra/webhook/outbound.ts +315 -0
  536. package/src/infra/webhook/sign.ts +32 -0
  537. package/src/infra/webhook/verify.ts +82 -0
  538. package/src/invariants/assert.ts +26 -0
  539. package/src/invariants/index.ts +1 -0
  540. package/src/lint/config.ts +56 -0
  541. package/src/lint/doctrine.ts +163 -0
  542. package/src/lint/include.ts +97 -0
  543. package/src/lint/index.ts +3 -0
  544. package/src/lint/preset.ts +10 -0
  545. package/src/lint/resolver.ts +218 -0
  546. package/src/lint/rules/no-reexports.ts +46 -0
  547. package/src/request/behavior/audited.ts +85 -0
  548. package/src/request/behavior/cached.ts +62 -0
  549. package/src/request/behavior/compose.ts +28 -0
  550. package/src/request/behavior/index.ts +5 -0
  551. package/src/request/behavior/namespace.ts +16 -0
  552. package/src/request/behavior/types.ts +26 -0
  553. package/src/request/behavior/versioned.ts +54 -0
  554. package/src/request/context/create.ts +103 -0
  555. package/src/request/context/guard.ts +49 -0
  556. package/src/request/context/index.ts +4 -0
  557. package/src/request/context/run-in-transaction.ts +200 -0
  558. package/src/request/context/store.ts +28 -0
  559. package/src/request/context/types.ts +50 -0
  560. package/src/request/di/builder.ts +535 -0
  561. package/src/request/di/index.ts +8 -0
  562. package/src/request/di/resolve.ts +271 -0
  563. package/src/request/di/types.ts +262 -0
  564. package/src/request/fitting/namespace.ts +7 -0
  565. package/src/request/fixture/batch.ts +71 -0
  566. package/src/request/fixture/builder.ts +26 -0
  567. package/src/request/fixture/errors.ts +10 -0
  568. package/src/request/fixture/index.ts +8 -0
  569. package/src/request/fixture/namespace.ts +23 -0
  570. package/src/request/fixture/paginate.ts +44 -0
  571. package/src/request/fixture/to-handlers.ts +260 -0
  572. package/src/request/fixture/types.ts +79 -0
  573. package/src/request/flow/act-as.ts +78 -0
  574. package/src/request/flow/namespace.ts +17 -0
  575. package/src/request/http/error-step.ts +42 -0
  576. package/src/request/http/file.ts +141 -0
  577. package/src/request/http/index.ts +4 -0
  578. package/src/request/http/middleware.ts +141 -0
  579. package/src/request/http/namespace.ts +5 -0
  580. package/src/request/http/rate-limit-classes.ts +170 -0
  581. package/src/request/http/response.ts +82 -0
  582. package/src/request/http/server.ts +916 -0
  583. package/src/request/http/sniff.ts +261 -0
  584. package/src/request/http/types.ts +250 -0
  585. package/src/request/http/upload.ts +284 -0
  586. package/src/request/internal/text.ts +6 -0
  587. package/src/request/openapi/generate.ts +205 -0
  588. package/src/request/openapi/index.ts +2 -0
  589. package/src/request/openapi/namespace.ts +7 -0
  590. package/src/test/auth.ts +13 -0
  591. package/src/test/auto-setup.ts +45 -0
  592. package/src/test/context.ts +91 -0
  593. package/src/test/crawl-fixture.ts +157 -0
  594. package/src/test/discover-fork.ts +17 -0
  595. package/src/test/fingerprint.ts +135 -0
  596. package/src/test/globals.d.ts +7 -0
  597. package/src/test/index.ts +19 -0
  598. package/src/test/namespace.ts +87 -0
  599. package/src/test/plugin.ts +189 -0
  600. package/src/test/progress.ts +185 -0
  601. package/src/test/reap-after-run.ts +28 -0
  602. package/src/test/reap.ts +286 -0
  603. package/src/test/reset.ts +253 -0
  604. package/src/test/savepoints.ts +172 -0
  605. package/src/test/setup.ts +1422 -0
  606. package/src/test/system-channel.ts +50 -0
  607. package/src/test/vitest-plugin.ts +142 -0
  608. package/src/test/vitest-workspace.ts +507 -0
  609. package/src/test/vitest.ts +333 -0
  610. package/src/test/watch.ts +105 -0
  611. package/src/trace/capture/bounded-serializer.ts +146 -0
  612. package/src/trace/cardinality/analyze.ts +173 -0
  613. package/src/trace/cardinality/branch-context.ts +214 -0
  614. package/src/trace/cardinality/check.ts +33 -0
  615. package/src/trace/cardinality/classify-edge.ts +64 -0
  616. package/src/trace/cardinality/structural-max.ts +107 -0
  617. package/src/trace/check/coverage-check.ts +188 -0
  618. package/src/trace/check/flow-check.ts +103 -0
  619. package/src/trace/check/flow-step-site.ts +234 -0
  620. package/src/trace/check/io-pattern.ts +116 -0
  621. package/src/trace/codemod/add-cardinality.ts +152 -0
  622. package/src/trace/codemod/run.ts +80 -0
  623. package/src/trace/entry/child-trace.ts +21 -0
  624. package/src/trace/entry/close-trace.ts +87 -0
  625. package/src/trace/entry/finalize-trace.ts +28 -0
  626. package/src/trace/entry/open-trace.ts +138 -0
  627. package/src/trace/entry/parent-trace.ts +20 -0
  628. package/src/trace/entry/parse-traceparent.ts +39 -0
  629. package/src/trace/entry/process-start.ts +38 -0
  630. package/src/trace/entry/trace-context.ts +26 -0
  631. package/src/trace/entry/tracing-enabled.ts +10 -0
  632. package/src/trace/external/record.ts +195 -0
  633. package/src/trace/external/report.ts +66 -0
  634. package/src/trace/namespace.ts +19 -0
  635. package/src/trace/partition/day.ts +47 -0
  636. package/src/trace/partition/index.ts +14 -0
  637. package/src/trace/partition/partition-name.ts +23 -0
  638. package/src/trace/partition/plan.ts +82 -0
  639. package/src/trace/partition/schedule.ts +26 -0
  640. package/src/trace/partition/startup-hook.ts +39 -0
  641. package/src/trace/partition/sync.ts +154 -0
  642. package/src/trace/partition/tables.ts +53 -0
  643. package/src/trace/read/tree.ts +83 -0
  644. package/src/trace/retain/copy-on-error.ts +76 -0
  645. package/src/trace/runtime/expire-stale-traces.ts +54 -0
  646. package/src/trace/runtime/flow-step.ts +147 -0
  647. package/src/trace/runtime/flush-steps.ts +31 -0
  648. package/src/trace/runtime/pending-writes.ts +54 -0
  649. package/src/trace/runtime/step-buffer.ts +298 -0
  650. package/src/trace/runtime/step-error.ts +21 -0
  651. package/src/trace/runtime/step-meta.ts +55 -0
  652. package/src/trace/runtime/thenable.ts +12 -0
  653. package/src/trace/runtime/truncate-trace.ts +62 -0
  654. package/src/trace/schema/trace-fields.ts +52 -0
  655. package/src/trace/schema/trace-retained.ts +22 -0
  656. package/src/trace/schema/trace-step-fields.ts +44 -0
  657. package/src/trace/schema/trace-step-retained.ts +25 -0
  658. package/src/trace/schema/trace-step.ts +18 -0
  659. package/src/trace/schema/trace.ts +12 -0
  660. package/src/trace/transformer/apply.ts +79 -0
  661. package/src/trace/transformer/build-meta-literal.ts +30 -0
  662. package/src/trace/transformer/find-flow-step-sites.ts +85 -0
  663. package/src/trace/transformer/inject-flow-step-import.ts +39 -0
  664. package/src/trace/transformer/transform-error.ts +14 -0
  665. package/src/trace/transformer/transform-flow-steps.ts +123 -0
  666. package/src/trace/transformer/wrap-function-declaration.ts +62 -0
  667. package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
  668. package/src/workspace/affected.ts +110 -0
  669. package/src/workspace/changed-files.ts +163 -0
  670. package/src/workspace/check-deps.ts +213 -0
  671. package/src/workspace/checksum.ts +20 -0
  672. package/src/workspace/dependency-graph.ts +67 -0
  673. package/src/workspace/detect-pipework-copies.ts +93 -0
  674. package/src/workspace/discover.ts +206 -0
  675. package/src/workspace/generate.ts +401 -0
  676. package/src/workspace/index.ts +7 -0
  677. package/src/workspace/module-imports-pipework.ts +39 -0
  678. package/src/workspace/module-package-name.ts +12 -0
  679. package/src/workspace/scrub-git-env.ts +23 -0
  680. package/src/workspace/source-paths.ts +36 -0
  681. package/src/workspace/sync.ts +182 -0
  682. package/dist/REFERENCE.md +0 -1477
  683. package/dist/infra/access-links/rate-limit.d.ts +0 -10
  684. package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
  685. package/dist/infra/access-links/rate-limit.js +0 -28
  686. package/dist/infra/access-links/rate-limit.js.map +0 -1
  687. package/dist/infra/email/sigv4.d.ts +0 -24
  688. package/dist/infra/email/sigv4.d.ts.map +0 -1
  689. package/dist/infra/email/sigv4.js +0 -55
  690. package/dist/infra/email/sigv4.js.map +0 -1
@@ -0,0 +1,892 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { openChildTrace } from '../../trace/entry/child-trace.js'
3
+ import { finalizeTrace } from '../../trace/entry/finalize-trace.js'
4
+ import { sql } from '../../data/db/sql.js'
5
+ import { pipe, pipeRaw, pipeSystem } from '../../data/db/db.js'
6
+ import type { DB, SQL } from '../../data/db/index.js'
7
+ import { requireContext, runInContext } from '../../request/context/store.js'
8
+ import { bindTransaction } from '../../request/context/guard.js'
9
+ import { propagateLocals } from '../../request/context/run-in-transaction.js'
10
+ import type { Flow } from '../../request/context/types.js'
11
+ import type { HttpResponse } from '../../request/http/types.js'
12
+ import { writeInfraAudit } from '../../data/write/audit/infra-audit.js'
13
+ import { resolveActorId } from '../../data/write/audit/actor.js'
14
+ import { getBaseLogger } from '../../core/logging/index.js'
15
+ import { recordFault, faultMessage } from '../alerts/faults.js'
16
+ import { ValidationError } from '../../core/errors/index.js'
17
+ import { inTenant as runAsTenant } from '../../auth/tenant/in-tenant.js'
18
+ import { encodeCursor, decodeCursor } from '../../request/fixture/paginate.js'
19
+ import { createCodeGenerator, type CodeConfig } from './code.js'
20
+
21
+ export interface RateLimitConfig {
22
+ readonly max: number
23
+ readonly windowMs: number
24
+ }
25
+
26
+ export interface AccessLinkTarget {
27
+ readonly type: string
28
+ readonly id: string
29
+ }
30
+
31
+ /** A link as its tenant sees it — includes the code and the app's `checkData`. */
32
+ export interface AccessLink {
33
+ readonly id: string
34
+ readonly tenantId: string
35
+ readonly code: string
36
+ readonly target: AccessLinkTarget
37
+ readonly permissions: readonly string[]
38
+ readonly note: string | null
39
+ readonly createdBy: string | null
40
+ readonly createdAt: Date
41
+ readonly expiresAt: Date | null
42
+ readonly maxUses: number | null
43
+ readonly requireChecks: readonly string[]
44
+ readonly checkData: unknown
45
+ readonly revokedAt: Date | null
46
+ readonly revokedBy: string | null
47
+ }
48
+
49
+ /** A link as a public route sees it — never `checkData`, never the code. */
50
+ export interface PublicLink {
51
+ readonly id: string
52
+ readonly target: AccessLinkTarget
53
+ readonly permissions: readonly string[]
54
+ readonly note: string | null
55
+ readonly expiresAt: Date | null
56
+ readonly maxUses: number | null
57
+ readonly usesCompleted: number
58
+ readonly requireChecks: readonly string[]
59
+ }
60
+
61
+ export interface AccessLinkUse {
62
+ readonly id: string
63
+ readonly linkId: string
64
+ readonly kind: string
65
+ readonly status: 'pending' | 'completed' | 'abandoned'
66
+ readonly startedAt: Date
67
+ readonly reservedUntil: Date
68
+ readonly completedAt: Date | null
69
+ readonly client: string | null
70
+ readonly location: string | null
71
+ /** App-defined detail given to `beginUse` / `completeUse` (shallow-merged); `null` when none was given. */
72
+ readonly detail: Readonly<Record<string, unknown>> | null
73
+ /** When `claimUse` took this use; `null` while unclaimed. */
74
+ readonly claimedAt: Date | null
75
+ }
76
+
77
+ export interface AccessLinkCheckArgs {
78
+ readonly link: AccessLink
79
+ readonly proof: unknown
80
+ readonly client: string | null
81
+ }
82
+
83
+ export type AccessLinkCheck = (args: AccessLinkCheckArgs) => Promise<boolean>
84
+
85
+ export interface AccessLinksConfig {
86
+ /** The app's permission names, e.g. `['view', 'download']`. */
87
+ readonly permissions: readonly string[]
88
+ readonly code?: CodeConfig
89
+ /** Pre-use verifiers by name; a link lists the ones it requires in `requireChecks`. */
90
+ readonly checks?: Readonly<Record<string, AccessLinkCheck>>
91
+ /** How long a begun use holds its slot before it frees. Default 15 minutes. */
92
+ readonly reservationMs?: number
93
+ /** Per-client limit on codes that match no link, counted in Postgres across processes. Default 30 misses per 60 s; a client over it is refused for every code. `false` turns it off. */
94
+ readonly resolveRateLimit?: RateLimitConfig | false
95
+ /** Called for every code that is not found — the app's guessing signal. */
96
+ readonly onMiss?: (info: { client: string | null; code: string }) => void
97
+ }
98
+
99
+ export interface LinkOptions {
100
+ readonly target: AccessLinkTarget
101
+ readonly permissions: readonly string[]
102
+ readonly note?: string
103
+ readonly expiresAt?: Date
104
+ readonly maxUses?: number
105
+ readonly requireChecks?: readonly string[]
106
+ readonly checkData?: unknown
107
+ }
108
+
109
+ type LinkState = 'not-found' | 'revoked' | 'expired' | 'exhausted'
110
+
111
+ /** A link in a listing: the link, its completed use count and its state. */
112
+ export type AccessLinkListed = AccessLink & {
113
+ readonly usesCompleted: number
114
+ readonly state: 'active' | 'revoked' | 'expired' | 'exhausted'
115
+ }
116
+
117
+ export interface AccessLinkPage {
118
+ readonly items: AccessLinkListed[]
119
+ /** Pass as `cursor` for the next (older) page; `null` at the end. */
120
+ readonly nextCursor: string | null
121
+ }
122
+
123
+ export type ResolveResult =
124
+ | { readonly status: 'active'; readonly link: PublicLink; readonly tenantId: string }
125
+ | { readonly status: LinkState | 'rate-limited' }
126
+
127
+ export type BeginUseResult =
128
+ | { readonly ok: true; readonly useToken: string; readonly link: PublicLink }
129
+ | { readonly ok: false; readonly reason: LinkState | 'kind-not-permitted' | 'check-failed' | 'rate-limited' }
130
+
131
+ export type CompleteUseResult =
132
+ | { readonly ok: true }
133
+ | { readonly ok: false; readonly reason: 'not-found' | 'expired-reservation' | 'already-settled' }
134
+
135
+ export type ClaimUseResult =
136
+ | { readonly ok: true; readonly link: PublicLink }
137
+ | { readonly ok: false; readonly reason: 'not-found' | 'already-claimed' | 'expired-reservation' | 'already-settled' }
138
+
139
+ export type AbandonUseResult =
140
+ | { readonly ok: true }
141
+ | { readonly ok: false; readonly reason: 'not-found' | 'already-settled' }
142
+
143
+ export interface AccessLinks {
144
+ /** Creates a link. When the Flow has a trace with an open step, an `audit_record` (`AccessLink`, verb `create`, the link without its code or `checkData`) is written in the same transaction. */
145
+ link(options: LinkOptions): Promise<AccessLink>
146
+ get(id: string): Promise<AccessLink | null>
147
+ list(options?: { target?: AccessLinkTarget }): Promise<AccessLink[]>
148
+ /** One page of the tenant's links, newest first, with completed use counts and state; `nextCursor` is `null` on the last page. `limit` defaults to 50, at most 500. */
149
+ listPage(options?: {
150
+ target?: AccessLinkTarget
151
+ createdBy?: string
152
+ state?: AccessLinkListed['state']
153
+ limit?: number
154
+ cursor?: string
155
+ }): Promise<AccessLinkPage>
156
+ /** Revokes a link; when this call revoked it and the Flow has an open trace step, an `AccessLink:revoke` audit record is written in the same transaction. */
157
+ revoke(id: string): Promise<AccessLink | null>
158
+ /**
159
+ * Deletes one link of the tenant with its `access_link_code` row and all its
160
+ * `access_link_use` rows, in one transaction (the caller's when there is one).
161
+ * Returns true when this call deleted it, false when the tenant has no such link.
162
+ * Writes an `AccessLink:delete` audit record (`{ id, target, usesDeleted }`, no
163
+ * personal fields) under the same open-step rule as `revoke`; the link's earlier
164
+ * audit records are left as they are. Afterwards `get`/`list`/`listPage` omit the
165
+ * link and `resolve`/`beginUse`/`completeUse`/`abandonUse`/`claimUse` with its code
166
+ * or tokens return `not-found`; a miss on the deleted code counts toward the
167
+ * guessing limit, as an unknown code does.
168
+ */
169
+ delete(id: string): Promise<boolean>
170
+ /**
171
+ * Deletes every link of the tenant whose target is `{ type, id }`, revoked and
172
+ * expired ones included, as `delete` does for each, in one transaction.
173
+ * Returns how many links it deleted.
174
+ */
175
+ deleteForTarget(target: AccessLinkTarget): Promise<number>
176
+ uses(linkId: string): Promise<AccessLinkUse[]>
177
+ resolve(code: string, options?: { client?: string | null }): Promise<ResolveResult>
178
+ beginUse(code: string, options: {
179
+ kind: string
180
+ client?: string | null
181
+ location?: string | null
182
+ proof?: Readonly<Record<string, unknown>>
183
+ /** App-defined detail stored on the use and shown by `uses()` (a JSON object). */
184
+ detail?: Readonly<Record<string, unknown>>
185
+ }): Promise<BeginUseResult>
186
+ /** `detail` is shallow-merged over the detail given to `beginUse` (new keys win). */
187
+ completeUse(useToken: string, options?: { detail?: Readonly<Record<string, unknown>> }): Promise<CompleteUseResult>
188
+ abandonUse(useToken: string): Promise<AbandonUseResult>
189
+ /**
190
+ * Takes a pending use exactly once, across processes: a single
191
+ * `UPDATE ... SET claimed_at = now() WHERE claimed_at IS NULL AND status = 'pending'
192
+ * AND reserved_until > clock_timestamp()`, so of any number of concurrent calls for
193
+ * one token exactly one gets `{ ok: true, link }`; the rest get `already-claimed`.
194
+ * Claiming does not count the use: `completeUse` / `abandonUse` still do. `link`
195
+ * is accepted by `asLink` like a `beginUse` result.
196
+ *
197
+ * Pattern for a single-use URL: `beginUse` when the URL is issued, hand out a URL
198
+ * carrying `useToken`, and on the follow `claimUse`, serve, then `completeUseWhenSent`.
199
+ * Other reasons: `not-found`, `expired-reservation` (reservation or link expiry
200
+ * passed), `already-settled` (completed or abandoned).
201
+ */
202
+ claimUse(useToken: string): Promise<ClaimUseResult>
203
+ /**
204
+ * Counts the use when the response has been sent, and frees it when the connection
205
+ * closed first: registers `response.onFinish`, then `completeUse(useToken, options)`
206
+ * on `'sent'` and `abandonUse(useToken)` on `'aborted'`. Call it before returning
207
+ * the handler's streamed body. The callback runs after the request's transaction
208
+ * has ended, with the Flow captured here and no transaction of its own, so it
209
+ * opens its own. The trace is closed by then, so the completion writes no
210
+ * `AccessLink:use` audit row; the use log row is the record. A failure is logged,
211
+ * never thrown into the response.
212
+ */
213
+ completeUseWhenSent(
214
+ response: HttpResponse,
215
+ useToken: string,
216
+ options?: { detail?: Readonly<Record<string, unknown>> },
217
+ ): void
218
+ /**
219
+ * Runs `fn` as the tenant an access link resolved to, from a `.public()` route.
220
+ * `grant` must be the object `resolve` returned with `status: 'active'` or
221
+ * `beginUse` or `claimUse` returned with `ok: true`; anything else (hand-built, copied,
222
+ * refused) throws, so a tenant cannot be named, only earned.
223
+ *
224
+ * Inside `fn` the Flow is a child of the current one — same request id — with `tenant` set to the link's tenant and
225
+ * `auth = { userId: 'link:<linkId>', accessLinkId }`, so `pipe()` is scoped to
226
+ * the tenant and an audited write records `actor_id = 'link:<linkId>'`. When
227
+ * the current Flow is traced, the work inside `fn` is recorded in a child trace
228
+ * (a child of the request's trace, opened at the step open on entry) that
229
+ * carries the link's tenant, so the tenant can read it; the request's own trace
230
+ * stays tenantless. The child trace closes when `fn`'s transaction settles. `fn`
231
+ * runs in its own transaction on the default database as the app role
232
+ * (row-level security applies), opened with the tenant's session variables.
233
+ * It commits when `fn` resolves and rolls back when `fn` throws; under the
234
+ * vitest harness it is a savepoint of the test's transaction. Nested
235
+ * `pipe.transaction` calls inside `fn` are savepoints of it.
236
+ *
237
+ * Only the default database is carried into `fn`: other databases bound in the
238
+ * outer Flow are not, so `pipe('<other>')` inside `fn` opens no bound
239
+ * transaction. Outside `fn` the Flow is unchanged: no tenant, no auth.
240
+ */
241
+ asLink<T>(grant: ResolveResult | BeginUseResult | ClaimUseResult, fn: () => Promise<T>): Promise<T>
242
+ }
243
+
244
+ const DEFAULT_RESERVATION_MS = 15 * 60 * 1000
245
+ const MISS_CLEANUP_EVERY_MS = 60_000
246
+ const DEFAULT_RATE_LIMIT: RateLimitConfig = { max: 30, windowMs: 60_000 }
247
+ const MAX_CODE_ATTEMPTS = 5
248
+ const DEFAULT_LIST_LIMIT = 50
249
+ const MAX_LIST_LIMIT = 500
250
+ const LIST_STATES: readonly string[] = ['active', 'revoked', 'expired', 'exhausted']
251
+
252
+ type Row = Record<string, unknown>
253
+
254
+ function asStringArray(value: unknown): string[] {
255
+ return Array.isArray(value) ? value.map(String) : []
256
+ }
257
+
258
+ // Raw `execute` rows carry timestamps as driver strings.
259
+ function toDate(value: unknown): Date {
260
+ return value instanceof Date ? value : new Date(String(value))
261
+ }
262
+
263
+ function toDateOrNull(value: unknown): Date | null {
264
+ return value === null || value === undefined ? null : toDate(value)
265
+ }
266
+
267
+ function rowToLink(row: Row): AccessLink {
268
+ return {
269
+ id: row['id'] as string,
270
+ tenantId: row['tenant_id'] as string,
271
+ code: row['code'] as string,
272
+ target: { type: row['target_type'] as string, id: row['target_id'] as string },
273
+ permissions: asStringArray(row['permissions']),
274
+ note: (row['note'] as string | null) ?? null,
275
+ createdBy: (row['created_by'] as string | null) ?? null,
276
+ createdAt: toDate(row['created_at']),
277
+ expiresAt: toDateOrNull(row['expires_at']),
278
+ maxUses: (row['max_uses'] as number | null) ?? null,
279
+ requireChecks: asStringArray(row['require_checks']),
280
+ checkData: row['check_data'] ?? null,
281
+ revokedAt: toDateOrNull(row['revoked_at']),
282
+ revokedBy: (row['revoked_by'] as string | null) ?? null,
283
+ }
284
+ }
285
+
286
+ function rowToPublic(row: Row): PublicLink {
287
+ const link = rowToLink(row)
288
+ return {
289
+ id: link.id,
290
+ target: link.target,
291
+ permissions: link.permissions,
292
+ note: link.note,
293
+ expiresAt: link.expiresAt,
294
+ maxUses: link.maxUses,
295
+ usesCompleted: Number(row['uses_completed'] ?? 0),
296
+ requireChecks: link.requireChecks,
297
+ }
298
+ }
299
+
300
+ function rowToUse(row: Row): AccessLinkUse {
301
+ return {
302
+ id: row['id'] as string,
303
+ linkId: row['link_id'] as string,
304
+ kind: row['kind'] as string,
305
+ status: row['status'] as AccessLinkUse['status'],
306
+ startedAt: toDate(row['started_at']),
307
+ reservedUntil: toDate(row['reserved_until']),
308
+ completedAt: toDateOrNull(row['completed_at']),
309
+ client: (row['client'] as string | null) ?? null,
310
+ location: (row['location'] as string | null) ?? null,
311
+ detail: (row['detail'] as Record<string, unknown> | null) ?? null,
312
+ claimedAt: toDateOrNull(row['claimed_at']),
313
+ }
314
+ }
315
+
316
+ function isUniqueViolation(err: unknown): boolean {
317
+ let current: unknown = err
318
+ for (let depth = 0; depth < 4 && typeof current === 'object' && current !== null; depth++) {
319
+ if ((current as { code?: unknown }).code === '23505') return true
320
+ current = (current as { cause?: unknown }).cause
321
+ }
322
+ return false
323
+ }
324
+
325
+ function splitToken(useToken: string): { code: string; useId: string } | null {
326
+ const dot = useToken.lastIndexOf('.')
327
+ if (dot <= 0 || dot === useToken.length - 1) return null
328
+ const useId = useToken.slice(dot + 1)
329
+ if (!/^[0-9a-f-]{36}$/i.test(useId)) return null
330
+ return { code: useToken.slice(0, dot), useId }
331
+ }
332
+
333
+ // A link row plus the state a use decision needs, all read with the database
334
+ // clock. The caller that must serialize locks the row in its own statement first.
335
+ function linkWithState(where: ReturnType<typeof sql>) {
336
+ return sql`
337
+ SELECT l.*,
338
+ (l.revoked_at IS NOT NULL) AS is_revoked,
339
+ (l.expires_at IS NOT NULL AND l.expires_at <= now()) AS is_expired,
340
+ (SELECT count(*)::int FROM access_link_use u WHERE u.link_id = l.id AND u.status = 'completed') AS uses_completed,
341
+ (SELECT count(*)::int FROM access_link_use u WHERE u.link_id = l.id
342
+ AND (u.status = 'completed' OR (u.status = 'pending' AND u.reserved_until > clock_timestamp()))) AS uses_held
343
+ FROM access_link l
344
+ WHERE ${where}
345
+ `
346
+ }
347
+
348
+ function stateOf(row: Row, mode: 'completed' | 'held'): LinkState | null {
349
+ if (row['is_revoked'] === true) return 'revoked'
350
+ if (row['is_expired'] === true) return 'expired'
351
+ const max = row['max_uses'] as number | null
352
+ const used = Number(mode === 'completed' ? row['uses_completed'] : row['uses_held'])
353
+ if (max !== null && used >= max) return 'exhausted'
354
+ return null
355
+ }
356
+
357
+ interface Grant {
358
+ readonly tenantId: string
359
+ readonly linkId: string
360
+ }
361
+
362
+ // The results `resolve` and `beginUse` handed out, by identity. `asLink` accepts
363
+ // only these, so no caller can name a tenant by building an object.
364
+ const grants = new WeakMap<object, Grant>()
365
+
366
+ function issue<R extends object>(result: R, tenantId: string, linkId: string): R {
367
+ grants.set(result, { tenantId, linkId })
368
+ return result
369
+ }
370
+
371
+ export function createAccessLinks(config: AccessLinksConfig): AccessLinks {
372
+ const permissionSet = new Set(config.permissions)
373
+ if (permissionSet.size === 0) {
374
+ throw new Error('[pipework] accessLinks.create needs a non-empty `permissions` list.\n')
375
+ }
376
+ const generator = createCodeGenerator(config.code)
377
+ const checks = config.checks ?? {}
378
+ const reservationMs = config.reservationMs ?? DEFAULT_RESERVATION_MS
379
+ if (!Number.isFinite(reservationMs) || reservationMs <= 0) {
380
+ throw new Error('[pipework] accessLinks reservationMs must be a positive number of milliseconds.\n')
381
+ }
382
+ const rateConfig = config.resolveRateLimit === undefined ? DEFAULT_RATE_LIMIT : config.resolveRateLimit
383
+ if (rateConfig !== false) {
384
+ if (!Number.isInteger(rateConfig.max) || rateConfig.max <= 0) {
385
+ throw new Error('[pipework] accessLinks resolveRateLimit.max must be a positive integer.\n')
386
+ }
387
+ if (!Number.isFinite(rateConfig.windowMs) || rateConfig.windowMs <= 0) {
388
+ throw new Error('[pipework] accessLinks resolveRateLimit.windowMs must be a positive number of milliseconds.\n')
389
+ }
390
+ }
391
+ let lastMissCleanup = 0
392
+
393
+ function requireTenant(): { flow: Flow; tenantId: string } {
394
+ const flow = requireContext()
395
+ if (flow.tenant === null) {
396
+ throw new Error(
397
+ '[pipework] This accessLinks method needs a tenant in the Flow.\n\n' +
398
+ ' link/get/list/revoke/delete/uses manage one tenant\'s links. Call them from an authenticated\n' +
399
+ ' tenant route; resolve/beginUse/completeUse/abandonUse are the ones for public routes.\n',
400
+ )
401
+ }
402
+ return { flow, tenantId: flow.tenant }
403
+ }
404
+
405
+ function inTenant<T>(tenantId: string, fn: (tx: DB) => Promise<T>): Promise<T> {
406
+ return runAsTenant(requireContext(), tenantId, fn)
407
+ }
408
+
409
+ async function findTenant(code: string): Promise<string | null> {
410
+ const rows = await pipeSystem().execute(sql`SELECT tenant_id FROM access_link_code WHERE code = ${code}`)
411
+ return rows.length === 0 ? null : (rows[0]!['tenant_id'] as string)
412
+ }
413
+
414
+ // True when the client has used up its miss budget in the current window.
415
+ async function overBudget(client: string | null): Promise<boolean> {
416
+ if (rateConfig === false || client === null) return false
417
+ const rows = await pipeSystem().execute(sql`
418
+ SELECT count FROM access_link_miss
419
+ WHERE client = ${client} AND window_start > now() - ${rateConfig.windowMs}::float8 * interval '1 millisecond'`)
420
+ return rows.length > 0 && Number(rows[0]!['count']) >= rateConfig.max
421
+ }
422
+
423
+ // Counts one miss against the client; atomic per client, shared by every process.
424
+ async function recordMiss(client: string | null): Promise<void> {
425
+ if (rateConfig === false || client === null) return
426
+ const db = pipeSystem()
427
+ await db.execute(sql`
428
+ INSERT INTO access_link_miss AS m (client, count, window_start)
429
+ VALUES (${client}, 1, now())
430
+ ON CONFLICT (client) DO UPDATE SET
431
+ count = CASE WHEN m.window_start <= now() - ${rateConfig.windowMs}::float8 * interval '1 millisecond' THEN 1 ELSE m.count + 1 END,
432
+ window_start = CASE WHEN m.window_start <= now() - ${rateConfig.windowMs}::float8 * interval '1 millisecond' THEN now() ELSE m.window_start END`)
433
+ const now = Date.now()
434
+ if (now - lastMissCleanup < MISS_CLEANUP_EVERY_MS) return
435
+ lastMissCleanup = now
436
+ void Promise.resolve(db.execute(sql`
437
+ DELETE FROM access_link_miss WHERE window_start <= now() - ${rateConfig.windowMs}::float8 * interval '1 millisecond'`))
438
+ .catch((err: unknown) => getBaseLogger().error({ err }, 'accessLinks miss cleanup failed'))
439
+ }
440
+
441
+ async function missed(code: string, client: string | null): Promise<void> {
442
+ await recordMiss(client)
443
+ signalMiss(code, client)
444
+ }
445
+
446
+ function signalMiss(code: string, client: string | null): void {
447
+ getBaseLogger().warn({ event: 'access link resolve miss', client, codeLength: code.length }, 'access link resolve miss')
448
+ try {
449
+ config.onMiss?.({ client, code })
450
+ } catch (err) {
451
+ getBaseLogger().error({ err }, 'accessLinks onMiss callback threw')
452
+ void recordFault('access-link-on-miss', faultMessage(err))
453
+ }
454
+ }
455
+
456
+ function audit(flow: Flow, tx: DB, tenantId: string, verb: string, linkId: string, details: Record<string, unknown>): Promise<void> {
457
+ return writeInfraAudit(flow, tx, { tenantId, entity: 'AccessLink', verb, entityId: linkId, after: details })
458
+ }
459
+
460
+ function auditProjection(link: AccessLink): Record<string, unknown> {
461
+ return {
462
+ id: link.id, target: link.target, permissions: link.permissions, note: link.note,
463
+ createdBy: link.createdBy, expiresAt: link.expiresAt, maxUses: link.maxUses,
464
+ requireChecks: link.requireChecks, revokedAt: link.revokedAt, revokedBy: link.revokedBy,
465
+ }
466
+ }
467
+
468
+ async function newLink(options: LinkOptions): Promise<AccessLink> {
469
+ const { flow, tenantId } = requireTenant()
470
+ if (options.permissions.length === 0) {
471
+ throw new ValidationError({ permissions: ['must name at least one permission'] }, 'Access link needs at least one permission.')
472
+ }
473
+ const unknownPermissions = options.permissions.filter(p => !permissionSet.has(p))
474
+ if (unknownPermissions.length > 0) {
475
+ const detail = `not in the configured set (${[...permissionSet].join(', ')}): ${unknownPermissions.join(', ')}`
476
+ throw new ValidationError({ permissions: [detail] }, `Access link permissions ${detail}`)
477
+ }
478
+ const requireChecks = options.requireChecks ?? []
479
+ const unknownChecks = requireChecks.filter(c => checks[c] === undefined)
480
+ if (unknownChecks.length > 0) {
481
+ const detail = `no such check in accessLinks.create({ checks }): ${unknownChecks.join(', ')}`
482
+ throw new ValidationError({ requireChecks: [detail] }, `Access link requireChecks ${detail}`)
483
+ }
484
+ if (options.maxUses !== undefined && (!Number.isInteger(options.maxUses) || options.maxUses < 1)) {
485
+ throw new ValidationError({ maxUses: ['must be a positive integer'] }, 'Access link maxUses must be a positive integer.')
486
+ }
487
+
488
+ const id = randomUUID()
489
+ const createdBy = resolveActorId(flow)
490
+ const permissions = JSON.stringify([...new Set(options.permissions)])
491
+ const checkData = options.checkData === undefined ? null : JSON.stringify(options.checkData)
492
+
493
+ return pipe.transaction(async (tx: DB) => {
494
+ for (let attempt = 1; attempt <= MAX_CODE_ATTEMPTS; attempt++) {
495
+ const code = generator.next()
496
+ try {
497
+ // A savepoint per attempt: a collision aborts only the attempt.
498
+ return await tx.transaction(async (sp: DB) => {
499
+ await sp.execute(sql`
500
+ INSERT INTO access_link_code (code, tenant_id, link_id) VALUES (${code}, ${tenantId}, ${id})
501
+ `)
502
+ const rows = await sp.execute(sql`
503
+ INSERT INTO access_link
504
+ (id, tenant_id, code, target_type, target_id, permissions, note, created_by,
505
+ expires_at, max_uses, require_checks, check_data)
506
+ VALUES (${id}, ${tenantId}, ${code}, ${options.target.type}, ${options.target.id},
507
+ ${permissions}::jsonb, ${options.note ?? null}, ${createdBy},
508
+ ${options.expiresAt?.toISOString() ?? null}, ${options.maxUses ?? null},
509
+ ${JSON.stringify(requireChecks)}::jsonb,
510
+ ${checkData === null ? sql`NULL` : sql`${checkData}::jsonb`})
511
+ RETURNING *
512
+ `)
513
+ const created = rowToLink(rows[0]!)
514
+ await audit(flow, sp, tenantId, 'create', created.id, auditProjection(created))
515
+ return created
516
+ })
517
+ } catch (err) {
518
+ if (!isUniqueViolation(err)) throw err
519
+ }
520
+ }
521
+ throw new Error(
522
+ `[pipework] accessLinks could not find a free code in ${MAX_CODE_ATTEMPTS} attempts.\n\n` +
523
+ ` Codes are ${generator.length} characters from an alphabet of ${generator.alphabet.length}; the space is\n` +
524
+ ' nearly full. Raise `code.length` or widen `code.alphabet`.\n',
525
+ )
526
+ })
527
+ }
528
+
529
+ // Locks the matching links first (serialising with beginUse), then removes uses, code and links.
530
+ async function deleteLinks(match: SQL): Promise<number> {
531
+ const { flow, tenantId } = requireTenant()
532
+ return pipe.transaction(async (tx: DB) => {
533
+ const found = await tx.execute(sql`
534
+ SELECT id FROM access_link WHERE tenant_id = ${tenantId} AND ${match} ORDER BY id FOR UPDATE
535
+ `)
536
+ if (found.length === 0) return 0
537
+ const ids = sql.inList(found.map(r => r['id'] as string), 'uuid')
538
+ const usesGone = await tx.execute(sql`
539
+ DELETE FROM access_link_use WHERE tenant_id = ${tenantId} AND link_id ${ids} RETURNING link_id
540
+ `)
541
+ const usesByLink = new Map<string, number>()
542
+ for (const u of usesGone) {
543
+ const k = u['link_id'] as string
544
+ usesByLink.set(k, (usesByLink.get(k) ?? 0) + 1)
545
+ }
546
+ await tx.execute(sql`DELETE FROM access_link_code WHERE tenant_id = ${tenantId} AND link_id ${ids}`)
547
+ const gone = await tx.execute(sql`
548
+ DELETE FROM access_link WHERE tenant_id = ${tenantId} AND id ${ids} RETURNING id, target_type, target_id
549
+ `)
550
+ for (const g of gone) {
551
+ const id = g['id'] as string
552
+ await audit(flow, tx, tenantId, 'delete', id, {
553
+ id,
554
+ target: { type: g['target_type'] as string, id: g['target_id'] as string },
555
+ usesDeleted: usesByLink.get(id) ?? 0,
556
+ })
557
+ }
558
+ return gone.length
559
+ })
560
+ }
561
+
562
+ const handle: AccessLinks = {
563
+ link: newLink,
564
+
565
+ async delete(id) {
566
+ return (await deleteLinks(sql`id = ${id}`)) > 0
567
+ },
568
+
569
+ async deleteForTarget(target) {
570
+ return deleteLinks(sql`target_type = ${target.type} AND target_id = ${target.id}`)
571
+ },
572
+
573
+ async get(id) {
574
+ const { tenantId } = requireTenant()
575
+ const rows = await pipeRaw().execute(sql`
576
+ SELECT * FROM access_link WHERE id = ${id} AND tenant_id = ${tenantId}
577
+ `)
578
+ return rows.length === 0 ? null : rowToLink(rows[0]!)
579
+ },
580
+
581
+ async list(options) {
582
+ const { tenantId } = requireTenant()
583
+ const target = options?.target
584
+ const rows = await pipeRaw().execute(sql`
585
+ SELECT * FROM access_link
586
+ WHERE tenant_id = ${tenantId}
587
+ ${target === undefined ? sql`` : sql`AND target_type = ${target.type} AND target_id = ${target.id}`}
588
+ ORDER BY created_at DESC, id
589
+ `)
590
+ return rows.map(rowToLink)
591
+ },
592
+
593
+ async listPage(options = {}) {
594
+ const { tenantId } = requireTenant()
595
+ const limit = options.limit ?? DEFAULT_LIST_LIMIT
596
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT) {
597
+ throw new ValidationError({ limit: [`must be an integer from 1 to ${MAX_LIST_LIMIT}`] }, 'Access link list limit is out of range.')
598
+ }
599
+ if (options.state !== undefined && !LIST_STATES.includes(options.state)) {
600
+ throw new ValidationError({ state: [`must be one of ${LIST_STATES.join(', ')}`] }, 'Access link list state is not valid.')
601
+ }
602
+ let cursor: { at: string; id: string } | null = null
603
+ if (options.cursor !== undefined) {
604
+ let parts: unknown[] = []
605
+ try { parts = decodeCursor(options.cursor) } catch { /* reported below */ }
606
+ if (parts.length !== 2 || typeof parts[0] !== 'string' || typeof parts[1] !== 'string'
607
+ || !/^[0-9a-f-]{36}$/i.test(parts[1]) || Number.isNaN(new Date(parts[0]).getTime())) {
608
+ throw new ValidationError('Invalid access link cursor: expected [createdAt, id]. Cursors are opaque — pass the value returned by listPage unchanged.')
609
+ }
610
+ cursor = { at: parts[0], id: parts[1] }
611
+ }
612
+ const { target, createdBy, state } = options
613
+ const rows = await pipeRaw().execute(sql`
614
+ SELECT * FROM (
615
+ SELECT s.*, s.created_at::text AS created_at_text,
616
+ CASE WHEN s.is_revoked THEN 'revoked'
617
+ WHEN s.is_expired THEN 'expired'
618
+ WHEN s.max_uses IS NOT NULL AND s.uses_completed >= s.max_uses THEN 'exhausted'
619
+ ELSE 'active' END AS state
620
+ FROM (${linkWithState(sql`l.tenant_id = ${tenantId}`)}) s
621
+ ) p
622
+ WHERE true
623
+ ${target === undefined ? sql`` : sql`AND p.target_type = ${target.type} AND p.target_id = ${target.id}`}
624
+ ${createdBy === undefined ? sql`` : sql`AND p.created_by = ${createdBy}`}
625
+ ${state === undefined ? sql`` : sql`AND p.state = ${state}`}
626
+ ${cursor === null ? sql`` : sql`AND (p.created_at, p.id) < (${cursor.at}::timestamptz, ${cursor.id}::uuid)`}
627
+ ORDER BY p.created_at DESC, p.id DESC
628
+ LIMIT ${limit + 1}
629
+ `)
630
+ const page = rows.slice(0, limit)
631
+ const last = page[page.length - 1]
632
+ return {
633
+ items: page.map(row => ({
634
+ ...rowToLink(row),
635
+ usesCompleted: Number(row['uses_completed']),
636
+ state: row['state'] as AccessLinkListed['state'],
637
+ })),
638
+ nextCursor: rows.length > limit && last !== undefined
639
+ ? encodeCursor([last['created_at_text'] as string, last['id'] as string])
640
+ : null,
641
+ }
642
+ },
643
+
644
+ async revoke(id) {
645
+ const { flow, tenantId } = requireTenant()
646
+ const db = pipeRaw()
647
+ const rows = await db.execute(sql`
648
+ UPDATE access_link SET revoked_at = now(), revoked_by = ${resolveActorId(flow)}
649
+ WHERE id = ${id} AND tenant_id = ${tenantId} AND revoked_at IS NULL
650
+ RETURNING *
651
+ `)
652
+ if (rows.length > 0) {
653
+ const revoked = rowToLink(rows[0]!)
654
+ await audit(flow, db, tenantId, 'revoke', revoked.id, auditProjection(revoked))
655
+ return revoked
656
+ }
657
+ return handle.get(id)
658
+ },
659
+
660
+ async uses(linkId) {
661
+ const { tenantId } = requireTenant()
662
+ const rows = await pipeRaw().execute(sql`
663
+ SELECT * FROM access_link_use WHERE link_id = ${linkId} AND tenant_id = ${tenantId}
664
+ ORDER BY started_at, id
665
+ `)
666
+ return rows.map(rowToUse)
667
+ },
668
+
669
+ async resolve(code, options) {
670
+ const client = options?.client ?? null
671
+ if (await overBudget(client)) return { status: 'rate-limited' }
672
+
673
+ const tenantId = await findTenant(code)
674
+ const row = tenantId === null ? undefined : (await inTenant(tenantId, tx => tx.execute(
675
+ linkWithState(sql`l.code = ${code} AND l.tenant_id = ${tenantId}`),
676
+ )))[0]
677
+ if (tenantId === null || row === undefined) {
678
+ await missed(code, client)
679
+ return { status: 'not-found' }
680
+ }
681
+ const state = stateOf(row, 'completed')
682
+ if (state !== null) return { status: state }
683
+ const link = rowToPublic(row)
684
+ return issue({ status: 'active' as const, link, tenantId }, tenantId, link.id)
685
+ },
686
+
687
+ async beginUse(code, options) {
688
+ const client = options.client ?? null
689
+ // Same budget as resolve: beginUse takes a raw code too, so it is a guessing path.
690
+ if (await overBudget(client)) return { ok: false, reason: 'rate-limited' }
691
+ const tenantId = await findTenant(code)
692
+ if (tenantId === null) {
693
+ await missed(code, client)
694
+ return { ok: false, reason: 'not-found' }
695
+ }
696
+
697
+ // Read first, without a lock: refuse what is already decided and run the
698
+ // verifiers (which may be slow) before any row is held.
699
+ const first = (await inTenant(tenantId, tx => tx.execute(
700
+ linkWithState(sql`l.code = ${code} AND l.tenant_id = ${tenantId}`),
701
+ )))[0]
702
+ if (first === undefined) {
703
+ await missed(code, client)
704
+ return { ok: false, reason: 'not-found' }
705
+ }
706
+ const early = stateOf(first, 'held')
707
+ if (early !== null) return { ok: false, reason: early }
708
+ const link = rowToLink(first)
709
+ if (!link.permissions.includes(options.kind)) return { ok: false, reason: 'kind-not-permitted' }
710
+
711
+ for (const name of link.requireChecks) {
712
+ const verify = checks[name]
713
+ let passed = false
714
+ try {
715
+ passed = verify !== undefined && await verify({ link, proof: options.proof?.[name], client }) === true
716
+ } catch (err) {
717
+ getBaseLogger().warn({ err, check: name, linkId: link.id }, 'access link check threw; treated as failed')
718
+ }
719
+ if (!passed) return { ok: false, reason: 'check-failed' }
720
+ }
721
+
722
+ // Then decide under the link's row lock: concurrent begins queue here,
723
+ // and each counts the uses the one before it left. The lock and the count
724
+ // are separate statements: a count in the locking statement reads the
725
+ // snapshot taken before the wait, missing the use the holder committed.
726
+ return inTenant(tenantId, async (tx): Promise<BeginUseResult> => {
727
+ await tx.execute(sql`SELECT 1 FROM access_link WHERE id = ${link.id} AND tenant_id = ${tenantId} FOR UPDATE`)
728
+ const locked = (await tx.execute(
729
+ linkWithState(sql`l.id = ${link.id} AND l.tenant_id = ${tenantId}`),
730
+ ))[0]
731
+ if (locked === undefined) return { ok: false, reason: 'not-found' }
732
+ const state = stateOf(locked, 'held')
733
+ if (state !== null) return { ok: false, reason: state }
734
+
735
+ const useId = randomUUID()
736
+ await tx.execute(sql`
737
+ INSERT INTO access_link_use (id, tenant_id, link_id, kind, status, reserved_until, client, location, detail)
738
+ VALUES (${useId}, ${tenantId}, ${link.id}, ${options.kind}, 'pending',
739
+ clock_timestamp() + ${reservationMs} * interval '1 millisecond',
740
+ ${client}, ${options.location ?? null},
741
+ ${options.detail === undefined ? sql`NULL` : sql`${JSON.stringify(options.detail)}::jsonb`})
742
+ `)
743
+ return issue({ ok: true as const, useToken: `${code}.${useId}`, link: rowToPublic(locked) }, tenantId, link.id)
744
+ })
745
+ },
746
+
747
+ async completeUse(useToken, options) {
748
+ const flow = requireContext()
749
+ const parts = splitToken(useToken)
750
+ if (parts === null) return { ok: false, reason: 'not-found' }
751
+ const tenantId = await findTenant(parts.code)
752
+ if (tenantId === null) return { ok: false, reason: 'not-found' }
753
+
754
+ return inTenant(tenantId, async (tx): Promise<CompleteUseResult> => {
755
+ const rows = await tx.execute(sql`
756
+ SELECT u.id, u.link_id, u.kind, u.status,
757
+ (u.reserved_until > clock_timestamp()) AS reservation_live,
758
+ (l.expires_at IS NOT NULL AND l.expires_at <= now()) AS link_expired,
759
+ l.target_type, l.target_id
760
+ FROM access_link_use u JOIN access_link l ON l.id = u.link_id
761
+ WHERE u.id = ${parts.useId} AND l.code = ${parts.code} AND u.tenant_id = ${tenantId}
762
+ FOR UPDATE OF u
763
+ `)
764
+ const row = rows[0]
765
+ if (row === undefined) return { ok: false, reason: 'not-found' }
766
+ if (row['status'] !== 'pending') return { ok: false, reason: 'already-settled' }
767
+ if (row['reservation_live'] !== true || row['link_expired'] === true) {
768
+ return { ok: false, reason: 'expired-reservation' }
769
+ }
770
+ const detail = options?.detail
771
+ const settled = await tx.execute(sql`
772
+ UPDATE access_link_use SET status = 'completed', completed_at = now()
773
+ ${detail === undefined ? sql`` : sql`, detail = COALESCE(detail, '{}'::jsonb) || ${JSON.stringify(detail)}::jsonb`}
774
+ WHERE id = ${parts.useId}
775
+ RETURNING detail
776
+ `)
777
+ await audit(flow, tx, tenantId, 'use', String(row['link_id']), {
778
+ linkId: row['link_id'], useId: parts.useId, kind: row['kind'],
779
+ targetType: row['target_type'], targetId: row['target_id'],
780
+ ...(settled[0]?.['detail'] != null ? { detail: settled[0]['detail'] } : {}),
781
+ })
782
+ return { ok: true }
783
+ })
784
+ },
785
+
786
+ async abandonUse(useToken) {
787
+ const parts = splitToken(useToken)
788
+ if (parts === null) return { ok: false, reason: 'not-found' }
789
+ const tenantId = await findTenant(parts.code)
790
+ if (tenantId === null) return { ok: false, reason: 'not-found' }
791
+
792
+ return inTenant(tenantId, async (tx): Promise<AbandonUseResult> => {
793
+ const updated = await tx.execute(sql`
794
+ UPDATE access_link_use u SET status = 'abandoned'
795
+ FROM access_link l
796
+ WHERE u.id = ${parts.useId} AND l.id = u.link_id AND l.code = ${parts.code}
797
+ AND u.tenant_id = ${tenantId} AND u.status = 'pending'
798
+ RETURNING u.id
799
+ `)
800
+ if (updated.length > 0) return { ok: true }
801
+ const exists = await tx.execute(sql`
802
+ SELECT 1 FROM access_link_use u JOIN access_link l ON l.id = u.link_id
803
+ WHERE u.id = ${parts.useId} AND l.code = ${parts.code} AND u.tenant_id = ${tenantId}
804
+ `)
805
+ return { ok: false, reason: exists.length === 0 ? 'not-found' : 'already-settled' }
806
+ })
807
+ },
808
+
809
+ async claimUse(useToken) {
810
+ const parts = splitToken(useToken)
811
+ if (parts === null) return { ok: false, reason: 'not-found' }
812
+ const tenantId = await findTenant(parts.code)
813
+ if (tenantId === null) return { ok: false, reason: 'not-found' }
814
+
815
+ return inTenant(tenantId, async (tx): Promise<ClaimUseResult> => {
816
+ const claimed = await tx.execute(sql`
817
+ UPDATE access_link_use u SET claimed_at = now()
818
+ FROM access_link l
819
+ WHERE u.id = ${parts.useId} AND l.id = u.link_id AND l.code = ${parts.code}
820
+ AND u.tenant_id = ${tenantId} AND u.claimed_at IS NULL AND u.status = 'pending'
821
+ AND u.reserved_until > clock_timestamp()
822
+ AND NOT (l.expires_at IS NOT NULL AND l.expires_at <= now())
823
+ RETURNING l.id AS link_id
824
+ `)
825
+ if (claimed.length > 0) {
826
+ const row = (await tx.execute(
827
+ linkWithState(sql`l.id = ${claimed[0]!['link_id'] as string} AND l.tenant_id = ${tenantId}`),
828
+ ))[0]!
829
+ const link = rowToPublic(row)
830
+ return issue({ ok: true as const, link }, tenantId, link.id)
831
+ }
832
+ const rows = await tx.execute(sql`
833
+ SELECT u.status, (u.claimed_at IS NOT NULL) AS claimed
834
+ FROM access_link_use u JOIN access_link l ON l.id = u.link_id
835
+ WHERE u.id = ${parts.useId} AND l.code = ${parts.code} AND u.tenant_id = ${tenantId}
836
+ `)
837
+ const row = rows[0]
838
+ if (row === undefined) return { ok: false, reason: 'not-found' }
839
+ if (row['status'] !== 'pending') return { ok: false, reason: 'already-settled' }
840
+ if (row['claimed'] === true) return { ok: false, reason: 'already-claimed' }
841
+ return { ok: false, reason: 'expired-reservation' }
842
+ })
843
+ },
844
+
845
+ completeUseWhenSent(response, useToken, options) {
846
+ const flow = requireContext()
847
+ // The request's transaction is over when this runs: keep the instance and
848
+ // the trace, drop the bound transactions.
849
+ const detached: Flow = { ...flow, _nonce: randomUUID(), transactions: new Map() }
850
+ response.onFinish(async (outcome) => {
851
+ await runInContext(detached, async () => {
852
+ if (outcome === 'sent') await handle.completeUse(useToken, options)
853
+ else await handle.abandonUse(useToken)
854
+ })
855
+ })
856
+ },
857
+
858
+ async asLink(grant, fn) {
859
+ const issued = grants.get(grant)
860
+ if (issued === undefined) {
861
+ throw new Error(
862
+ '[pipework] accessLinks.asLink needs the result of resolve (status "active") or beginUse (ok) ' +
863
+ 'exactly as this handle returned it.\n\n' +
864
+ ' A copied, rebuilt or refused result is not a grant. Check `status === "active"` / `ok` first.\n',
865
+ )
866
+ }
867
+ const flow = requireContext()
868
+ const dbName = flow.instance.config.defaultDatabaseName()
869
+ const auth = { userId: `link:${issued.linkId}`, accessLinkId: issued.linkId }
870
+ const childTrace = openChildTrace(flow, issued.tenantId, auth)
871
+ const child: Flow = {
872
+ ...flow,
873
+ _nonce: randomUUID(),
874
+ tenant: issued.tenantId,
875
+ user: null,
876
+ auth,
877
+ transactions: new Map(),
878
+ trace: childTrace,
879
+ }
880
+ // A NEW connection as the app role, with the link's tenant as its locals —
881
+ // the same transaction `pipe.transaction` opens, for the child's tenant.
882
+ const conn = flow.instance.pool.getOrCreate(flow.instance.config.database(dbName), 'app')
883
+ return finalizeTrace(flow.instance, childTrace, conn.drizzle.transaction(async (tx) => {
884
+ child.transactions.set(dbName, bindTransaction(child, dbName, tx))
885
+ await propagateLocals(tx, child, child.tenantConfig)
886
+ return runInContext(child, fn)
887
+ }))
888
+ },
889
+ }
890
+
891
+ return handle
892
+ }