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,513 @@
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import type { Readable } from 'node:stream'
3
+ import { sql } from '../../data/db/sql.js'
4
+ import { pipe, pipeSystem, pipeFrameworkDoor } from '../../data/db/db.js'
5
+ import type { DB } from '../../data/db/index.js'
6
+ import { requireContext } from '../../request/context/store.js'
7
+ import type { Flow } from '../../request/context/types.js'
8
+ import { inTenant } from '../../auth/tenant/in-tenant.js'
9
+ import { resolveActorId } from '../../data/write/audit/actor.js'
10
+ import { getBaseLogger } from '../../core/logging/index.js'
11
+ import { recordFault, faultMessage } from '../alerts/faults.js'
12
+ import { NotFoundError, PayloadTooLargeError, ValidationError } from '../../core/errors/index.js'
13
+ import type { StorageBackend } from './backend.js'
14
+
15
+ const DEFAULT_MAX_BYTES = 1024 ** 3
16
+ const MAX_BACKEND_BYTES = 5 * 1024 ** 3
17
+ const MAX_SIGNED_SECONDS = 604800
18
+ const DEFAULT_SIGNED_SECONDS = 900
19
+ const DEFAULT_SWEEP_LIMIT = 1000
20
+ const TMP_MAX_AGE_MS = 24 * 3_600_000
21
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
22
+ const OBJECT_KEY = /^tenants\/([0-9a-f-]{36})\/sha256\/([0-9a-f]{64})$/
23
+ const TMP_KEY = /^tenants\/[0-9a-f-]{36}\/tmp\/[0-9a-f-]{36}$/
24
+
25
+ export interface StorageConfig {
26
+ readonly backend: StorageBackend
27
+ /** Largest file `put` accepts, in bytes. Default 1 GiB; at most 5 GiB. */
28
+ readonly maxBytes?: number
29
+ /** The app's environment. The local backend is refused when this is `production`. */
30
+ readonly environment?: string
31
+ }
32
+
33
+ /** The metadata row of a stored file. */
34
+ export interface StoredFile {
35
+ readonly id: string
36
+ readonly tenantId: string
37
+ /** Lowercase hex SHA-256 of the content. */
38
+ readonly sha256: string
39
+ readonly size: number
40
+ readonly mediaType: string
41
+ readonly filename: string | null
42
+ readonly createdAt: Date
43
+ /** The Flow's `auth.userId` when `put` ran. */
44
+ readonly createdBy: string | null
45
+ /** `null` = kept until deleted. */
46
+ readonly expiresAt: Date | null
47
+ readonly metadata: unknown
48
+ }
49
+
50
+ export interface PutOptions {
51
+ /** The bytes. Read once, never held whole; the length need not be known. */
52
+ readonly body: Readable
53
+ readonly mediaType: string
54
+ readonly filename?: string
55
+ /** Hex SHA-256 the content must have; a different digest fails with 422 and stores nothing. */
56
+ readonly expectedSha256?: string
57
+ /** After this time the file is no longer returned and `sweep` deletes it. */
58
+ readonly expiresAt?: Date
59
+ readonly metadata?: unknown
60
+ }
61
+
62
+ export interface SignedUrlOptions {
63
+ /** 1 to 604800 (seven days). Default 900. */
64
+ readonly expiresInSeconds?: number
65
+ /** `'attachment'` (default) downloads; `'inline'` lets the browser display it. */
66
+ readonly disposition?: 'attachment' | 'inline'
67
+ /** Name the browser saves under; default the stored filename. */
68
+ readonly filename?: string
69
+ }
70
+
71
+ export interface FindOptions {
72
+ /** Hex SHA-256 of the content, any case. */
73
+ readonly sha256?: string
74
+ /** Rows whose metadata contains this object (jsonb `@>`); nested objects match by containment. */
75
+ readonly metadata?: Record<string, unknown>
76
+ /** Most rows returned. Default 1000, at most 10000. */
77
+ readonly limit?: number
78
+ }
79
+
80
+ export interface SweepOptions {
81
+ /** Most rows expired and most objects deleted in one call (each). Default 1000. */
82
+ readonly limit?: number
83
+ }
84
+
85
+ export interface SweepResult {
86
+ /** Rows past `expiresAt` deleted. */
87
+ readonly expired: number
88
+ /** Content objects deleted because no row referenced them. */
89
+ readonly objects: number
90
+ /** Abandoned upload temp objects (over 24 hours old) deleted. */
91
+ readonly temp: number
92
+ }
93
+
94
+ export interface Storage {
95
+ /**
96
+ * Stores a stream for the Flow's tenant and returns its metadata row. The bytes are written to a
97
+ * temporary object while hashed and counted, then moved to the tenant's content address
98
+ * (`tenants/<tenantId>/sha256/<hex>`); the same bytes put again by the tenant add a row and no
99
+ * object. Over `maxBytes` throws `PayloadTooLargeError` (413); a digest other than `expectedSha256`
100
+ * throws `ValidationError` (422); either leaves no row and no object. The blob is written first
101
+ * and the row in the Flow's transaction, so a rolled-back transaction leaves an object that
102
+ * `sweep()` removes. An `StoredFile:put` audit record is written when the Flow has an open trace step.
103
+ */
104
+ put(options: PutOptions): Promise<StoredFile>
105
+ putFor(tenantId: string, options: PutOptions): Promise<StoredFile>
106
+ /** The file's row, or `null` when there is none (or it has expired). */
107
+ get(id: string): Promise<StoredFile | null>
108
+ getFor(tenantId: string, id: string): Promise<StoredFile | null>
109
+ /**
110
+ * The tenant's live (unexpired) files matching every given filter, oldest first. Give `sha256`,
111
+ * `metadata` or both; none throws `ValidationError`. Runs in the Flow's transaction, under the same RLS as `get`.
112
+ */
113
+ find(query: FindOptions): Promise<StoredFile[]>
114
+ findFor(tenantId: string, query: FindOptions): Promise<StoredFile[]>
115
+ /** The row and a stream of the content (pass `file.size` as `length` to `http.file()`). Throws `NotFoundError` when there is no row. */
116
+ open(id: string): Promise<{ file: StoredFile; stream: Readable }>
117
+ openFor(tenantId: string, id: string): Promise<{ file: StoredFile; stream: Readable }>
118
+ /**
119
+ * A URL that serves the content to anyone holding it until it expires, with the requested
120
+ * Content-Disposition and the stored media type. S3 backend only; the local backend throws.
121
+ */
122
+ signedUrl(id: string, options?: SignedUrlOptions): Promise<{ url: string; expiresAt: Date }>
123
+ signedUrlFor(tenantId: string, id: string, options?: SignedUrlOptions): Promise<{ url: string; expiresAt: Date }>
124
+ /** Deletes the row (audit `StoredFile:delete`); the object goes at the next `sweep()` once no row references it. Returns whether there was a row. */
125
+ delete(id: string): Promise<boolean>
126
+ deleteFor(tenantId: string, id: string): Promise<boolean>
127
+ /**
128
+ * For a cron (system, all tenants). Deletes rows past `expiresAt` (audit `StoredFile:expire`), then
129
+ * content objects no row references and upload temp objects over 24 hours old. Needs a Flow
130
+ * (a job or schedule run). Audit records need an open trace step, as in `put`.
131
+ */
132
+ sweep(options?: SweepOptions): Promise<SweepResult>
133
+ }
134
+
135
+ function toDate(value: unknown): Date {
136
+ if (value instanceof Date) return value
137
+ const text = String(value)
138
+ return new Date(/(Z|[+-]\d\d(:?\d\d)?)$/.test(text) ? text : `${text.replace(' ', 'T')}Z`)
139
+ }
140
+
141
+ function rowToFile(row: Record<string, unknown>): StoredFile {
142
+ const expires = row['expires_at']
143
+ return {
144
+ id: row['id'] as string,
145
+ tenantId: row['tenant_id'] as string,
146
+ sha256: row['sha256'] as string,
147
+ size: Number(row['size']),
148
+ mediaType: row['media_type'] as string,
149
+ filename: (row['filename'] as string | null) ?? null,
150
+ createdAt: toDate(row['created_at']),
151
+ createdBy: (row['created_by'] as string | null) ?? null,
152
+ expiresAt: expires === null || expires === undefined ? null : toDate(expires),
153
+ metadata: row['metadata'] ?? null,
154
+ }
155
+ }
156
+
157
+ function auditProjection(file: StoredFile): Record<string, unknown> {
158
+ return {
159
+ id: file.id, sha256: file.sha256, size: file.size, mediaType: file.mediaType, filename: file.filename,
160
+ createdBy: file.createdBy, expiresAt: file.expiresAt,
161
+ }
162
+ }
163
+
164
+ function requireId(name: string, value: string): void {
165
+ if (typeof value !== 'string' || !UUID.test(value)) {
166
+ throw new ValidationError({ [name]: ['must be a UUID'] }, `Storage ${name} must be a UUID.`)
167
+ }
168
+ }
169
+
170
+ function keyFor(tenantId: string, sha256: string): string {
171
+ return `tenants/${tenantId.toLowerCase()}/sha256/${sha256}`
172
+ }
173
+
174
+ /**
175
+ * Tenant-scoped, content-addressed file storage over a backend (`storage.s3(...)` or
176
+ * `storage.local(...)`). Requires blueprint `storage: {}` for the `storage_file` table.
177
+ * Throws when the local backend is used with `environment: 'production'`, or `maxBytes`
178
+ * is not an integer from 1 to 5 GiB.
179
+ */
180
+ export function createStorage(config: StorageConfig): Storage {
181
+ const backend = config.backend
182
+ const maxBytes = config.maxBytes ?? DEFAULT_MAX_BYTES
183
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MAX_BACKEND_BYTES) {
184
+ throw new Error('[pipework] storage maxBytes must be an integer from 1 to 5 GiB (5368709120).\n')
185
+ }
186
+ if (backend.name === 'local' && config.environment === 'production') {
187
+ throw new Error(
188
+ '[pipework] storage.local is refused in production.\n\n' +
189
+ ' It keeps files on this machine\'s disk. Configure storage.s3({ bucket, region, credentials }).\n',
190
+ )
191
+ }
192
+
193
+ function tenantOf(explicit: string | null, flow: Flow): string {
194
+ if (explicit !== null) {
195
+ requireId('tenantId', explicit)
196
+ return explicit
197
+ }
198
+ if (flow.tenant === null) {
199
+ throw new Error(
200
+ '[pipework] This storage method needs a tenant in the Flow.\n\n' +
201
+ ' Call it from an authenticated tenant route, or use the ...For(tenantId, ...) form\n' +
202
+ ' (putFor, getFor, openFor, ...) from a webhook or a job with no tenant.\n',
203
+ )
204
+ }
205
+ return flow.tenant
206
+ }
207
+
208
+ async function run<T>(explicit: string | null, fn: (tx: DB, tenantId: string, flow: Flow) => Promise<T>): Promise<T> {
209
+ const flow = requireContext()
210
+ const tenantId = tenantOf(explicit, flow)
211
+ if (explicit !== null) return inTenant(flow, tenantId, tx => fn(tx, tenantId, flow))
212
+ return pipe.transaction((tx: DB) => fn(tx, tenantId, flow))
213
+ }
214
+
215
+ async function audit(flow: Flow, tx: DB, tenantId: string, verb: string, fileId: string, details: Record<string, unknown>): Promise<void> {
216
+ const trace = flow.trace
217
+ const stepId = trace?.buffer.openStepId ?? null
218
+ if (trace === null || stepId === null) return
219
+ try {
220
+ await tx.transaction(async (sp: DB) => {
221
+ await sp.execute(sql`
222
+ INSERT INTO audit_record
223
+ (trace_day, trace_id, trace_step_id, tenant_id, actor_id, entity, verb, entity_id, after, occurred_at)
224
+ VALUES (${trace.openedDay}, ${trace.id}, ${stepId}, ${tenantId}, ${resolveActorId(flow)},
225
+ 'StoredFile', ${verb}, ${fileId}, ${JSON.stringify(details)}::jsonb, now())
226
+ `)
227
+ })
228
+ } catch (err) {
229
+ getBaseLogger().error({ err, traceId: trace.id }, `Failed to write the stored file ${verb} audit record`)
230
+ void recordFault('audit-write', faultMessage(err), { entity: 'StoredFile', verb })
231
+ }
232
+ }
233
+
234
+ // One tenant-and-content lock orders a put's "does the object exist, then the row" against a
235
+ // sweep's "is there a row, then delete the object". Held to the end of the transaction.
236
+ async function lockContent(tx: DB, tenantId: string, sha256: string): Promise<void> {
237
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${`pipework.storage:${tenantId.toLowerCase()}:${sha256}`}, 0))`)
238
+ }
239
+
240
+ async function discard(key: string): Promise<void> {
241
+ try {
242
+ await backend.remove(key)
243
+ } catch (err) {
244
+ getBaseLogger().error({ err, key }, 'storage failed to remove a temporary object; sweep() will')
245
+ }
246
+ }
247
+
248
+ async function doPut(explicit: string | null, options: PutOptions): Promise<StoredFile> {
249
+ if (typeof options.mediaType !== 'string' || options.mediaType.trim() === '' || /[\r\n]/.test(options.mediaType)) {
250
+ throw new ValidationError({ mediaType: ['must be a media type such as "application/pdf"'] }, 'Storage mediaType must be a non-empty single line.')
251
+ }
252
+ let expected: string | null = null
253
+ if (options.expectedSha256 !== undefined) {
254
+ if (!/^[0-9a-f]{64}$/i.test(options.expectedSha256)) {
255
+ throw new ValidationError({ expectedSha256: ['must be 64 hex characters'] }, 'Storage expectedSha256 must be a hex SHA-256.')
256
+ }
257
+ expected = options.expectedSha256.toLowerCase()
258
+ }
259
+ if (options.expiresAt !== undefined && !(options.expiresAt instanceof Date && !Number.isNaN(options.expiresAt.getTime()))) {
260
+ throw new ValidationError({ expiresAt: ['must be a valid Date'] }, 'Storage expiresAt must be a Date.')
261
+ }
262
+ // Resolve the tenant before any byte is written.
263
+ const flow = requireContext()
264
+ const tenantId = tenantOf(explicit, flow)
265
+
266
+ const hash = createHash('sha256')
267
+ let size = 0
268
+ const measured = (async function* (): AsyncGenerator<Buffer> {
269
+ for await (const chunk of options.body) {
270
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array)
271
+ size += buf.length
272
+ if (size > maxBytes) throw new PayloadTooLargeError(`File is larger than ${maxBytes} bytes`, { maxBytes })
273
+ hash.update(buf)
274
+ yield buf
275
+ }
276
+ })()
277
+
278
+ const tmpKey = `tenants/${tenantId.toLowerCase()}/tmp/${randomUUID()}`
279
+ try {
280
+ await backend.put(tmpKey, measured)
281
+ } catch (err) {
282
+ options.body.destroy()
283
+ await discard(tmpKey)
284
+ throw err
285
+ }
286
+ const sha256 = hash.digest('hex')
287
+ if (expected !== null && expected !== sha256) {
288
+ await discard(tmpKey)
289
+ throw new ValidationError(
290
+ { expectedSha256: [`content has sha256 ${sha256}`] },
291
+ 'The uploaded content does not match expectedSha256.',
292
+ )
293
+ }
294
+
295
+ const finalKey = keyFor(tenantId, sha256)
296
+ try {
297
+ return await run(explicit, async (tx, tenant, f) => {
298
+ await lockContent(tx, tenant, sha256)
299
+ if (!(await backend.exists(finalKey))) await backend.copy(tmpKey, finalKey)
300
+ const metadata = options.metadata === undefined ? null : JSON.stringify(options.metadata)
301
+ const rows = await tx.execute(sql`
302
+ INSERT INTO storage_file (tenant_id, sha256, size, media_type, filename, created_by, expires_at, metadata)
303
+ VALUES (${tenant}, ${sha256}, ${size}, ${options.mediaType.trim()}, ${options.filename ?? null},
304
+ ${resolveActorId(f)}, ${options.expiresAt?.toISOString() ?? null},
305
+ ${metadata === null ? sql`NULL` : sql`${metadata}::jsonb`})
306
+ RETURNING *
307
+ `)
308
+ const file = rowToFile(rows[0]!)
309
+ await audit(f, tx, tenant, 'put', file.id, auditProjection(file))
310
+ return file
311
+ })
312
+ } finally {
313
+ await discard(tmpKey)
314
+ }
315
+ }
316
+
317
+ async function findFile(tx: DB, tenantId: string, id: string, includeExpired: boolean): Promise<StoredFile | null> {
318
+ const rows = await tx.execute(includeExpired
319
+ ? sql`SELECT * FROM storage_file WHERE tenant_id = ${tenantId} AND id = ${id}`
320
+ : sql`SELECT * FROM storage_file WHERE tenant_id = ${tenantId} AND id = ${id} AND (expires_at IS NULL OR expires_at > now())`)
321
+ return rows.length === 0 ? null : rowToFile(rows[0]!)
322
+ }
323
+
324
+ async function doGet(explicit: string | null, id: string): Promise<StoredFile | null> {
325
+ requireId('id', id)
326
+ return run(explicit, (tx, tenantId) => findFile(tx, tenantId, id, false))
327
+ }
328
+
329
+ async function doFind(explicit: string | null, query: FindOptions): Promise<StoredFile[]> {
330
+ if ((query as unknown) === null || typeof query !== 'object') {
331
+ throw new ValidationError({ query: ['give sha256, metadata or both'] }, 'Storage find needs sha256, metadata or both.')
332
+ }
333
+ let hash: string | null = null
334
+ if (query.sha256 !== undefined) {
335
+ if (typeof query.sha256 !== 'string' || !/^[0-9a-f]{64}$/i.test(query.sha256)) {
336
+ throw new ValidationError({ sha256: ['must be 64 hex characters'] }, 'Storage find sha256 must be a hex SHA-256.')
337
+ }
338
+ hash = query.sha256.toLowerCase()
339
+ }
340
+ let contains: string | null = null
341
+ if (query.metadata !== undefined) {
342
+ const given: unknown = query.metadata
343
+ const proto: unknown = given === null || typeof given !== 'object' ? undefined : Object.getPrototypeOf(given)
344
+ if (proto !== Object.prototype && proto !== null) {
345
+ throw new ValidationError({ metadata: ['must be a plain object'] }, 'Storage find metadata must be a plain object.')
346
+ }
347
+ contains = JSON.stringify(query.metadata)
348
+ }
349
+ if (hash === null && contains === null) {
350
+ throw new ValidationError({ query: ['give sha256, metadata or both'] }, 'Storage find needs sha256, metadata or both.')
351
+ }
352
+ const limit = query.limit ?? 1000
353
+ if (!Number.isInteger(limit) || limit < 1 || limit > 10000) {
354
+ throw new ValidationError({ limit: ['must be an integer from 1 to 10000'] }, 'Storage find limit must be an integer from 1 to 10000.')
355
+ }
356
+ return run(explicit, async (tx, tenantId) => {
357
+ const rows = await tx.execute(sql`
358
+ SELECT * FROM storage_file
359
+ WHERE tenant_id = ${tenantId} AND (expires_at IS NULL OR expires_at > now())
360
+ ${hash === null ? sql`` : sql`AND sha256 = ${hash}`}
361
+ ${contains === null ? sql`` : sql`AND metadata @> ${contains}::jsonb`}
362
+ ORDER BY created_at, id
363
+ LIMIT ${limit}
364
+ `)
365
+ return rows.map(r => rowToFile(r))
366
+ })
367
+ }
368
+
369
+ async function doOpen(explicit: string | null, id: string): Promise<{ file: StoredFile; stream: Readable }> {
370
+ const file = await doGet(explicit, id)
371
+ if (file === null) throw new NotFoundError('File', { id })
372
+ return { file, stream: await backend.read(keyFor(file.tenantId, file.sha256)) }
373
+ }
374
+
375
+ async function doSignedUrl(explicit: string | null, id: string, options: SignedUrlOptions = {}): Promise<{ url: string; expiresAt: Date }> {
376
+ const seconds = options.expiresInSeconds ?? DEFAULT_SIGNED_SECONDS
377
+ if (!Number.isInteger(seconds) || seconds < 1 || seconds > MAX_SIGNED_SECONDS) {
378
+ throw new ValidationError({ expiresInSeconds: [`must be an integer from 1 to ${MAX_SIGNED_SECONDS}`] }, 'Storage expiresInSeconds is out of range.')
379
+ }
380
+ const disposition: string = options.disposition ?? 'attachment'
381
+ if (disposition !== 'attachment' && disposition !== 'inline') {
382
+ throw new ValidationError({ disposition: ["must be 'attachment' or 'inline'"] }, 'Storage disposition must be attachment or inline.')
383
+ }
384
+ const file = await doGet(explicit, id)
385
+ if (file === null) throw new NotFoundError('File', { id })
386
+ const filename = options.filename ?? file.filename ?? undefined
387
+ const issued = Date.now()
388
+ const url = await backend.signedUrl(keyFor(file.tenantId, file.sha256), {
389
+ expiresInSeconds: seconds, disposition: disposition as 'attachment' | 'inline', mediaType: file.mediaType, ...(filename !== undefined ? { filename } : {}),
390
+ })
391
+ return { url, expiresAt: new Date(issued + seconds * 1000) }
392
+ }
393
+
394
+ async function doDelete(explicit: string | null, id: string): Promise<boolean> {
395
+ requireId('id', id)
396
+ return run(explicit, async (tx, tenantId, flow) => {
397
+ const rows = await tx.execute(sql`DELETE FROM storage_file WHERE tenant_id = ${tenantId} AND id = ${id} RETURNING *`)
398
+ if (rows.length === 0) return false
399
+ const file = rowToFile(rows[0]!)
400
+ await audit(flow, tx, tenantId, 'delete', file.id, auditProjection(file))
401
+ return true
402
+ })
403
+ }
404
+
405
+ async function requireSweepDoor(): Promise<void> {
406
+ const wanted = ['pipework_door_pipework__storage_sweep', 'pipework_door_pipework__storage_sweep__delete']
407
+ const found = await pipeSystem().execute(sql`
408
+ SELECT p.polname FROM pg_policy p JOIN pg_class c ON c.oid = p.polrelid
409
+ WHERE c.relname = 'storage_file' AND c.relnamespace = 'public'::regnamespace AND p.polname IN ('pipework_door_pipework__storage_sweep', 'pipework_door_pipework__storage_sweep__delete')`)
410
+ const have = new Set(found.map(r => r['polname'] as string))
411
+ const missing = wanted.filter(n => !have.has(n))
412
+ if (missing.length > 0) {
413
+ throw new Error(
414
+ `[pipework] storage.sweep() refused: the door "pipework.storage_sweep" is missing its policy on storage_file (${missing.join(', ')}).\n\n` +
415
+ ' The database runs its system channel on a platform role, which sees storage_file only through that door; without it the sweep would\n' +
416
+ ' find no file referencing any object. Nothing was removed. Fix: run `pipework generate` and `pipework migrate`.\n',
417
+ )
418
+ }
419
+ }
420
+
421
+ async function doSweep(options: SweepOptions = {}): Promise<SweepResult> {
422
+ const limit = options.limit ?? DEFAULT_SWEEP_LIMIT
423
+ if (!Number.isSafeInteger(limit) || limit < 1) {
424
+ throw new ValidationError({ limit: ['must be a positive integer'] }, 'Storage sweep limit must be a positive integer.')
425
+ }
426
+ const flow = requireContext()
427
+ let expired = 0
428
+ let objects = 0
429
+ let temp = 0
430
+
431
+ // Where the database has a platform role, `storage_file` is readable and deletable by it only through the
432
+ // sweep's door. A missing door policy would read as "no file references anything" and remove every object,
433
+ // so it is checked first and the sweep refuses before it lists one.
434
+ const platform = flow.instance.config.database().rls?.platformRole !== undefined
435
+ if (platform) await requireSweepDoor()
436
+ // Row locking in the subselect needs an UPDATE policy as well, which the door does not grant; the
437
+ // DELETE takes the row locks itself, and a concurrent sweep deletes nothing twice.
438
+ const inSweepDoor = <R>(fn: (tx: DB) => Promise<R>): Promise<R> =>
439
+ platform ? pipeFrameworkDoor('pipework.storage_sweep', fn) : pipeSystem().transaction(fn)
440
+
441
+ await inSweepDoor(async (tx: DB) => {
442
+ const rows = await tx.execute(sql`
443
+ DELETE FROM storage_file WHERE id IN (
444
+ SELECT id FROM storage_file WHERE expires_at IS NOT NULL AND expires_at <= now()
445
+ ORDER BY expires_at LIMIT ${limit} ${platform ? sql`` : sql`FOR UPDATE SKIP LOCKED`}
446
+ ) RETURNING *
447
+ `)
448
+ for (const row of rows) {
449
+ const file = rowToFile(row)
450
+ await audit(flow, tx, file.tenantId, 'expire', file.id, auditProjection(file))
451
+ }
452
+ expired = rows.length
453
+ })
454
+
455
+ const cutoff = Date.now() - TMP_MAX_AGE_MS
456
+ let batch: { tenantId: string; sha256: string; key: string }[] = []
457
+
458
+ const settle = async (): Promise<void> => {
459
+ if (batch.length === 0) return
460
+ const candidates = batch
461
+ batch = []
462
+ const present = await inSweepDoor(tx => tx.execute(sql`
463
+ SELECT tenant_id::text AS t, sha256 AS h FROM storage_file
464
+ WHERE tenant_id::text || ':' || sha256 IN (SELECT jsonb_array_elements_text(${JSON.stringify(candidates.map(c => `${c.tenantId}:${c.sha256}`))}::jsonb))
465
+ `))
466
+ const referenced = new Set(present.map(r => `${r['t'] as string}:${r['h'] as string}`))
467
+ for (const c of candidates) {
468
+ if (objects >= limit) return
469
+ if (referenced.has(`${c.tenantId}:${c.sha256}`)) continue
470
+ const removed = await inSweepDoor(async (tx: DB) => {
471
+ await lockContent(tx, c.tenantId, c.sha256)
472
+ const again = await tx.execute(sql`
473
+ SELECT 1 FROM storage_file WHERE tenant_id = ${c.tenantId} AND sha256 = ${c.sha256} LIMIT 1
474
+ `)
475
+ if (again.length > 0) return false
476
+ await backend.remove(c.key)
477
+ return true
478
+ })
479
+ if (removed) objects += 1
480
+ }
481
+ }
482
+
483
+ for await (const object of backend.list('tenants/')) {
484
+ if (objects >= limit && temp >= limit) break
485
+ const match = OBJECT_KEY.exec(object.key)
486
+ if (match !== null) {
487
+ if (objects < limit) batch.push({ tenantId: match[1]!, sha256: match[2]!, key: object.key })
488
+ if (batch.length >= 200) await settle()
489
+ } else if (TMP_KEY.test(object.key) && object.lastModified.getTime() < cutoff && temp < limit) {
490
+ await backend.remove(object.key)
491
+ temp += 1
492
+ }
493
+ }
494
+ await settle()
495
+ return { expired, objects, temp }
496
+ }
497
+
498
+ return {
499
+ put: o => doPut(null, o),
500
+ putFor: (t, o) => doPut(t, o),
501
+ get: id => doGet(null, id),
502
+ getFor: (t, id) => doGet(t, id),
503
+ find: q => doFind(null, q),
504
+ findFor: (t, q) => doFind(t, q),
505
+ open: id => doOpen(null, id),
506
+ openFor: (t, id) => doOpen(t, id),
507
+ signedUrl: (id, o) => doSignedUrl(null, id, o),
508
+ signedUrlFor: (t, id, o) => doSignedUrl(t, id, o),
509
+ delete: id => doDelete(null, id),
510
+ deleteFor: (t, id) => doDelete(t, id),
511
+ sweep: o => doSweep(o),
512
+ }
513
+ }
@@ -0,0 +1,9 @@
1
+ export { createStorage } from './handle.js'
2
+ export { s3, encryptionHeaders, PART_SIZE } from './s3.js'
3
+ export { local } from './local.js'
4
+ export type { S3Config, S3Encryption } from './s3.js'
5
+ export type { LocalConfig } from './local.js'
6
+ export type { StorageBackend, BackendObject, BackendSignedUrlOptions } from './backend.js'
7
+ export type {
8
+ Storage, StorageConfig, StoredFile, PutOptions, FindOptions, SignedUrlOptions, SweepOptions, SweepResult,
9
+ } from './handle.js'
@@ -0,0 +1,104 @@
1
+ import { createReadStream, createWriteStream } from 'node:fs'
2
+ import { copyFile, mkdir, readdir, rename, rm, stat } from 'node:fs/promises'
3
+ import { dirname, join, resolve, sep } from 'node:path'
4
+ import { randomUUID } from 'node:crypto'
5
+ import { pipeline } from 'node:stream/promises'
6
+ import { Readable } from 'node:stream'
7
+ import type { BackendObject, StorageBackend } from './backend.js'
8
+
9
+ export interface LocalConfig {
10
+ /** Directory the files are written under; created on first write. */
11
+ readonly dir: string
12
+ }
13
+
14
+ /**
15
+ * Files on local disk, for development and tests. Refused by `storage.create` when
16
+ * its `environment` is `production`. There are no signed URLs: `signedUrl` throws
17
+ * here, so serve a file by streaming `open()` through `http.file()`.
18
+ *
19
+ * In a test, give each file its own directory (`mkdtemp`) and remove it in `afterAll`;
20
+ * rows written through the test harness roll back with its transaction, and the
21
+ * leftover blobs are then plain files in that directory.
22
+ */
23
+ export function local(config: LocalConfig): StorageBackend {
24
+ const root = resolve(config.dir)
25
+
26
+ function pathOf(key: string): string {
27
+ const full = resolve(root, ...key.split('/'))
28
+ if (full !== root && !full.startsWith(root + sep)) {
29
+ throw new Error(`[pipework] storage local: key "${key}" escapes the storage directory.\n`)
30
+ }
31
+ return full
32
+ }
33
+
34
+ return {
35
+ name: 'local',
36
+
37
+ async put(key, body) {
38
+ const target = pathOf(key)
39
+ await mkdir(dirname(target), { recursive: true })
40
+ const partial = `${target}.${randomUUID()}.partial`
41
+ try {
42
+ await pipeline(Readable.from(body, { objectMode: false }), createWriteStream(partial))
43
+ await rename(partial, target)
44
+ } catch (err) {
45
+ await rm(partial, { force: true })
46
+ throw err
47
+ }
48
+ },
49
+
50
+ async exists(key) {
51
+ try {
52
+ return (await stat(pathOf(key))).isFile()
53
+ } catch (err) {
54
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return false
55
+ throw err
56
+ }
57
+ },
58
+
59
+ async copy(from, to) {
60
+ const target = pathOf(to)
61
+ await mkdir(dirname(target), { recursive: true })
62
+ await copyFile(pathOf(from), target)
63
+ },
64
+
65
+ async remove(key) {
66
+ await rm(pathOf(key), { force: true })
67
+ },
68
+
69
+ async read(key) {
70
+ const path = pathOf(key)
71
+ await stat(path)
72
+ return createReadStream(path)
73
+ },
74
+
75
+ async *list(prefix): AsyncGenerator<BackendObject> {
76
+ const walk = async function* (dir: string): AsyncGenerator<string> {
77
+ let entries
78
+ try {
79
+ entries = await readdir(dir, { withFileTypes: true })
80
+ } catch (err) {
81
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return
82
+ throw err
83
+ }
84
+ for (const entry of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
85
+ const full = join(dir, entry.name)
86
+ if (entry.isDirectory()) yield* walk(full)
87
+ else if (entry.isFile() && !entry.name.endsWith('.partial')) yield full
88
+ }
89
+ }
90
+ for await (const file of walk(root)) {
91
+ const key = file.slice(root.length + 1).split(sep).join('/')
92
+ if (!key.startsWith(prefix)) continue
93
+ yield { key, lastModified: (await stat(file)).mtime }
94
+ }
95
+ },
96
+
97
+ signedUrl() {
98
+ return Promise.reject(new Error(
99
+ '[pipework] The local storage backend has no signed URLs.\n\n' +
100
+ ' Stream the file with storage.open(id) and send it with http.file(), or use storage.s3(...).\n',
101
+ ))
102
+ },
103
+ }
104
+ }
@@ -0,0 +1,13 @@
1
+ import { createStorage } from './handle.js'
2
+ import { s3 } from './s3.js'
3
+ import { local } from './local.js'
4
+
5
+ /** Storage namespace — tenant-scoped, content-addressed file storage with signed URLs and retention. */
6
+ export const storage = {
7
+ /** Creates a storage handle over a backend. */
8
+ create: createStorage,
9
+ /** S3-compatible backend (AWS S3, MinIO, R2): SigV4 over `fetch`, multipart streaming upload, optional server-side encryption. */
10
+ s3,
11
+ /** Local-disk backend for development and tests; refused in production. */
12
+ local,
13
+ } as const