@vercel/factory 0.0.14 → 0.0.16

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 (510) hide show
  1. package/CHANGELOG.md +356 -0
  2. package/README.md +96 -2
  3. package/dist/agent-routes.d.mts +113 -0
  4. package/dist/agent-routes.mjs +91 -0
  5. package/dist/agent-routes.mjs.map +1 -0
  6. package/dist/api-contracts.d.mts +1768 -0
  7. package/dist/api-contracts.mjs +119 -0
  8. package/dist/api-contracts.mjs.map +1 -0
  9. package/dist/api-images.d.mts +30 -0
  10. package/dist/api-images.mjs +28 -0
  11. package/dist/api-images.mjs.map +1 -0
  12. package/dist/api-task-graph.mjs +57 -0
  13. package/dist/api-task-graph.mjs.map +1 -0
  14. package/dist/api.d.mts +111 -216
  15. package/dist/api.mjs +1063 -99
  16. package/dist/api.mjs.map +1 -1
  17. package/dist/approval-access.d.mts +39 -0
  18. package/dist/approval-access.mjs +62 -0
  19. package/dist/approval-access.mjs.map +1 -0
  20. package/dist/approval-contracts.d.mts +698 -0
  21. package/dist/approval-contracts.mjs +27 -0
  22. package/dist/approval-contracts.mjs.map +1 -0
  23. package/dist/approval.d.mts +49 -0
  24. package/dist/approval.mjs +39 -0
  25. package/dist/approval.mjs.map +1 -0
  26. package/dist/blob/artifacts.d.mts +37 -0
  27. package/dist/blob/artifacts.mjs +45 -0
  28. package/dist/blob/artifacts.mjs.map +1 -0
  29. package/dist/blob/index.d.mts +84 -12
  30. package/dist/blob/index.mjs +179 -45
  31. package/dist/blob/index.mjs.map +1 -1
  32. package/dist/budget.d.mts +115 -0
  33. package/dist/budget.mjs +147 -0
  34. package/dist/budget.mjs.map +1 -0
  35. package/dist/budgets.d.mts +4 -0
  36. package/dist/budgets.mjs +4 -0
  37. package/dist/build-factory.d.mts +7 -4
  38. package/dist/build-factory.mjs +2 -1
  39. package/dist/build-factory.mjs.map +1 -1
  40. package/dist/change-verification/brief.d.mts +9 -0
  41. package/dist/change-verification/brief.mjs +13 -0
  42. package/dist/change-verification/brief.mjs.map +1 -0
  43. package/dist/change-verification/dispatch.d.mts +37 -0
  44. package/dist/change-verification/dispatch.mjs +83 -0
  45. package/dist/change-verification/dispatch.mjs.map +1 -0
  46. package/dist/change-verification/eve-tool.d.mts +61 -0
  47. package/dist/change-verification/eve-tool.mjs +89 -0
  48. package/dist/change-verification/eve-tool.mjs.map +1 -0
  49. package/dist/change-verification/result.mjs +130 -0
  50. package/dist/change-verification/result.mjs.map +1 -0
  51. package/dist/changes/eve-record-change.d.mts +56 -0
  52. package/dist/changes/eve-record-change.mjs +131 -0
  53. package/dist/changes/eve-record-change.mjs.map +1 -0
  54. package/dist/changes/presentation.d.mts +14 -0
  55. package/dist/changes.d.mts +24 -0
  56. package/dist/changes.mjs +29 -0
  57. package/dist/changes.mjs.map +1 -0
  58. package/dist/client-events.d.mts +25 -0
  59. package/dist/client-events.mjs +45 -0
  60. package/dist/client-events.mjs.map +1 -0
  61. package/dist/client-stream.d.mts +28 -0
  62. package/dist/client-stream.mjs +210 -0
  63. package/dist/client-stream.mjs.map +1 -0
  64. package/dist/client-transcript.d.mts +41 -0
  65. package/dist/client-transcript.mjs +233 -0
  66. package/dist/client-transcript.mjs.map +1 -0
  67. package/dist/client.d.mts +343 -0
  68. package/dist/client.mjs +366 -0
  69. package/dist/client.mjs.map +1 -0
  70. package/dist/code-review/contracts.d.mts +27 -0
  71. package/dist/code-review/eve-post-review.d.mts +70 -0
  72. package/dist/code-review/eve-post-review.mjs +195 -0
  73. package/dist/code-review/eve-post-review.mjs.map +1 -0
  74. package/dist/code-review/eve-review-comments.d.mts +75 -0
  75. package/dist/code-review/eve-review-comments.mjs +193 -0
  76. package/dist/code-review/eve-review-comments.mjs.map +1 -0
  77. package/dist/code-review/github-contracts.d.mts +10 -0
  78. package/dist/code-review/github-reporter.d.mts +46 -0
  79. package/dist/code-review/github-reporter.mjs +262 -0
  80. package/dist/code-review/github-reporter.mjs.map +1 -0
  81. package/dist/code-review/preparation.d.mts +34 -0
  82. package/dist/code-review/preparation.mjs +23 -0
  83. package/dist/code-review/preparation.mjs.map +1 -0
  84. package/dist/code-review/stall.d.mts +17 -0
  85. package/dist/code-review/stall.mjs +30 -0
  86. package/dist/code-review/stall.mjs.map +1 -0
  87. package/dist/code-review.d.mts +7 -0
  88. package/dist/code-review.mjs +5 -0
  89. package/dist/communication.d.mts +9 -0
  90. package/dist/communication.mjs +9 -0
  91. package/dist/connectors/github.d.mts +26 -0
  92. package/dist/connectors/github.mjs +55 -0
  93. package/dist/connectors/github.mjs.map +1 -0
  94. package/dist/connectors/index.d.mts +35 -3
  95. package/dist/connectors/index.mjs +3 -52
  96. package/dist/connectors/index.mjs.map +1 -1
  97. package/dist/connectors/outbox.d.mts +34 -0
  98. package/dist/connectors/outbox.mjs +50 -0
  99. package/dist/connectors/outbox.mjs.map +1 -0
  100. package/dist/connectors/slack-approval-blocks.mjs +136 -0
  101. package/dist/connectors/slack-approval-blocks.mjs.map +1 -0
  102. package/dist/connectors/slack-approvals.d.mts +29 -0
  103. package/dist/connectors/slack-approvals.mjs +346 -0
  104. package/dist/connectors/slack-approvals.mjs.map +1 -0
  105. package/dist/connectors/slack.d.mts +35 -52
  106. package/dist/connectors/slack.mjs +13 -85
  107. package/dist/connectors/slack.mjs.map +1 -1
  108. package/dist/deepsec/eve-tool.d.mts +110 -0
  109. package/dist/deepsec/eve-tool.mjs +166 -0
  110. package/dist/deepsec/eve-tool.mjs.map +1 -0
  111. package/dist/deepsec/finding.d.mts +84 -0
  112. package/dist/deepsec/finding.mjs +56 -0
  113. package/dist/deepsec/finding.mjs.map +1 -0
  114. package/dist/deepsec.d.mts +3 -0
  115. package/dist/delivery-metrics/metrics.d.mts +93 -0
  116. package/dist/delivery-metrics/metrics.mjs +200 -0
  117. package/dist/delivery-metrics/metrics.mjs.map +1 -0
  118. package/dist/delivery-metrics.d.mts +2 -0
  119. package/dist/delivery-metrics.mjs +2 -0
  120. package/dist/dispatch.d.mts +152 -0
  121. package/dist/dispatch.mjs +199 -0
  122. package/dist/dispatch.mjs.map +1 -0
  123. package/dist/errors.d.mts +15 -0
  124. package/dist/errors.mjs +38 -0
  125. package/dist/errors.mjs.map +1 -0
  126. package/dist/eve/index.d.mts +209 -1277
  127. package/dist/eve/index.mjs +312 -1073
  128. package/dist/eve/index.mjs.map +1 -1
  129. package/dist/eve/invoke.d.mts +144 -0
  130. package/dist/eve/invoke.mjs +254 -0
  131. package/dist/eve/invoke.mjs.map +1 -0
  132. package/dist/eve/session-client.d.mts +186 -0
  133. package/dist/eve/session-client.mjs +245 -0
  134. package/dist/eve/session-client.mjs.map +1 -0
  135. package/dist/eve/task-execution.d.mts +380 -0
  136. package/dist/eve/task-execution.mjs +73 -0
  137. package/dist/eve/task-execution.mjs.map +1 -0
  138. package/dist/eve/task-session.d.mts +129 -0
  139. package/dist/eve/task-session.mjs +245 -0
  140. package/dist/eve/task-session.mjs.map +1 -0
  141. package/dist/eve/transcript.d.mts +37 -0
  142. package/dist/eve/transcript.mjs +69 -0
  143. package/dist/eve/transcript.mjs.map +1 -0
  144. package/dist/execution-public.d.mts +8 -0
  145. package/dist/execution-public.mjs +7 -0
  146. package/dist/execution.d.mts +183 -0
  147. package/dist/execution.mjs +223 -0
  148. package/dist/execution.mjs.map +1 -0
  149. package/dist/finding-remediation/admission.d.mts +53 -0
  150. package/dist/finding-remediation/admission.mjs +90 -0
  151. package/dist/finding-remediation/admission.mjs.map +1 -0
  152. package/dist/finding-remediation/brief.d.mts +14 -0
  153. package/dist/finding-remediation/brief.mjs +25 -0
  154. package/dist/finding-remediation/brief.mjs.map +1 -0
  155. package/dist/finding-remediation/eve-tool.d.mts +29 -0
  156. package/dist/finding-remediation/eve-tool.mjs +34 -0
  157. package/dist/finding-remediation/eve-tool.mjs.map +1 -0
  158. package/dist/findings/fingerprint.d.mts +14 -0
  159. package/dist/findings/fingerprint.mjs +28 -0
  160. package/dist/findings/fingerprint.mjs.map +1 -0
  161. package/dist/findings.d.mts +7 -0
  162. package/dist/findings.mjs +6 -0
  163. package/dist/github-app.d.mts +54 -0
  164. package/dist/github-app.mjs +121 -0
  165. package/dist/github-app.mjs.map +1 -0
  166. package/dist/github-publication.d.mts +199 -0
  167. package/dist/github-publication.mjs +442 -0
  168. package/dist/github-publication.mjs.map +1 -0
  169. package/dist/github-transfer.d.mts +81 -0
  170. package/dist/github-transfer.mjs +273 -0
  171. package/dist/github-transfer.mjs.map +1 -0
  172. package/dist/github.d.mts +446 -2
  173. package/dist/github.mjs +357 -56
  174. package/dist/github.mjs.map +1 -1
  175. package/dist/image-input.d.mts +51 -0
  176. package/dist/image-input.mjs +63 -0
  177. package/dist/image-input.mjs.map +1 -0
  178. package/dist/inbox-activity.d.mts +53 -0
  179. package/dist/inbox-activity.mjs +41 -0
  180. package/dist/inbox-activity.mjs.map +1 -0
  181. package/dist/index.d.mts +16 -484
  182. package/dist/index.mjs +15 -522
  183. package/dist/intake-contracts.d.mts +55 -0
  184. package/dist/intake-contracts.mjs +33 -0
  185. package/dist/intake-contracts.mjs.map +1 -0
  186. package/dist/integrations/deepsec.d.mts +5 -0
  187. package/dist/integrations/deepsec.mjs +3 -0
  188. package/dist/integrations/github.d.mts +11 -0
  189. package/dist/integrations/github.mjs +9 -0
  190. package/dist/integrations/slack.d.mts +5 -0
  191. package/dist/integrations/slack.mjs +5 -0
  192. package/dist/integrations/vercel.d.mts +5 -0
  193. package/dist/integrations/vercel.mjs +4 -0
  194. package/dist/merge-resolution/contracts.d.mts +17 -0
  195. package/dist/merge-resolution/contracts.mjs +19 -0
  196. package/dist/merge-resolution/contracts.mjs.map +1 -0
  197. package/dist/merge-resolution/eve-tools.d.mts +47 -0
  198. package/dist/merge-resolution/eve-tools.mjs +122 -0
  199. package/dist/merge-resolution/eve-tools.mjs.map +1 -0
  200. package/dist/model-settings.d.mts +41 -0
  201. package/dist/model-settings.mjs +35 -0
  202. package/dist/model-settings.mjs.map +1 -0
  203. package/dist/multi-repository-changes/coordination.d.mts +49 -0
  204. package/dist/multi-repository-changes/coordination.mjs +115 -0
  205. package/dist/multi-repository-changes/coordination.mjs.map +1 -0
  206. package/dist/planning/cancellation.mjs +82 -0
  207. package/dist/planning/cancellation.mjs.map +1 -0
  208. package/dist/planning/configuration.d.mts +23 -0
  209. package/dist/planning/configuration.mjs +56 -0
  210. package/dist/planning/configuration.mjs.map +1 -0
  211. package/dist/planning/contracts.d.mts +136 -0
  212. package/dist/planning/contracts.mjs +42 -0
  213. package/dist/planning/contracts.mjs.map +1 -0
  214. package/dist/planning/dependencies.mjs +44 -0
  215. package/dist/planning/dependencies.mjs.map +1 -0
  216. package/dist/planning/effects.mjs +122 -0
  217. package/dist/planning/effects.mjs.map +1 -0
  218. package/dist/planning/reconcile.mjs +279 -0
  219. package/dist/planning/reconcile.mjs.map +1 -0
  220. package/dist/planning/runtime.d.mts +27 -0
  221. package/dist/planning/runtime.mjs +25 -0
  222. package/dist/planning/runtime.mjs.map +1 -0
  223. package/dist/planning/state.mjs +92 -0
  224. package/dist/planning/state.mjs.map +1 -0
  225. package/dist/planning/turns.mjs +262 -0
  226. package/dist/planning/turns.mjs.map +1 -0
  227. package/dist/planning/validation.mjs +106 -0
  228. package/dist/planning/validation.mjs.map +1 -0
  229. package/dist/postgres/index.d.mts +48 -1
  230. package/dist/postgres/index.mjs +45 -3
  231. package/dist/postgres/index.mjs.map +1 -1
  232. package/dist/presets/software-development/approval.d.mts +14 -0
  233. package/dist/presets/software-development/approval.mjs +21 -0
  234. package/dist/presets/software-development/approval.mjs.map +1 -0
  235. package/dist/presets/software-development/deepsec-brief.d.mts +8 -0
  236. package/dist/presets/software-development/deepsec-brief.mjs +14 -0
  237. package/dist/presets/software-development/deepsec-brief.mjs.map +1 -0
  238. package/dist/presets/software-development/dispatch.d.mts +56 -0
  239. package/dist/presets/software-development/dispatch.mjs +67 -0
  240. package/dist/presets/software-development/dispatch.mjs.map +1 -0
  241. package/dist/presets/software-development/eve-delegation-policy.d.mts +7 -0
  242. package/dist/presets/software-development/eve-delegation-policy.mjs +34 -0
  243. package/dist/presets/software-development/eve-delegation-policy.mjs.map +1 -0
  244. package/dist/presets/software-development/eve.d.mts +18 -0
  245. package/dist/presets/software-development/eve.mjs +19 -0
  246. package/dist/presets/software-development/eve.mjs.map +1 -0
  247. package/dist/presets/software-development/finish-task-policy.d.mts +7 -0
  248. package/dist/presets/software-development/finish-task-policy.mjs +9 -0
  249. package/dist/presets/software-development/finish-task-policy.mjs.map +1 -0
  250. package/dist/presets/software-development/recovery.d.mts +32 -0
  251. package/dist/presets/software-development/recovery.mjs +120 -0
  252. package/dist/presets/software-development/recovery.mjs.map +1 -0
  253. package/dist/presets/software-development/task-communication.d.mts +24 -0
  254. package/dist/presets/software-development/task-communication.mjs +157 -0
  255. package/dist/presets/software-development/task-communication.mjs.map +1 -0
  256. package/dist/presets/software-development.d.mts +10 -0
  257. package/dist/presets/software-development.mjs +9 -0
  258. package/dist/pull-requests/contracts.d.mts +72 -0
  259. package/dist/pull-requests/eve-fetch.d.mts +57 -0
  260. package/dist/pull-requests/eve-fetch.mjs +151 -0
  261. package/dist/pull-requests/eve-fetch.mjs.map +1 -0
  262. package/dist/pull-requests/github-label-manager.d.mts +27 -0
  263. package/dist/pull-requests/github-label-manager.mjs +40 -0
  264. package/dist/pull-requests/github-label-manager.mjs.map +1 -0
  265. package/dist/pull-requests/github-publisher.d.mts +67 -0
  266. package/dist/pull-requests/github-publisher.mjs +405 -0
  267. package/dist/pull-requests/github-publisher.mjs.map +1 -0
  268. package/dist/pull-requests.d.mts +6 -0
  269. package/dist/pull-requests.mjs +4 -0
  270. package/dist/receipts.d.mts +4 -0
  271. package/dist/receipts.mjs +3 -0
  272. package/dist/repositories.d.mts +18 -0
  273. package/dist/repositories.mjs +33 -0
  274. package/dist/repositories.mjs.map +1 -0
  275. package/dist/sandbox/command.d.mts +18 -0
  276. package/dist/sandbox/execution-abort.d.mts +16 -0
  277. package/dist/sandbox/execution-abort.mjs +49 -0
  278. package/dist/sandbox/execution-abort.mjs.map +1 -0
  279. package/dist/sandbox/execution-environment.d.mts +25 -0
  280. package/dist/sandbox/execution-run-ledger.d.mts +115 -0
  281. package/dist/sandbox/execution-run-ledger.mjs +209 -0
  282. package/dist/sandbox/execution-run-ledger.mjs.map +1 -0
  283. package/dist/sandbox/execution.d.mts +54 -0
  284. package/dist/sandbox/execution.mjs +228 -0
  285. package/dist/sandbox/execution.mjs.map +1 -0
  286. package/dist/sandbox/harness-runner.d.mts +79 -0
  287. package/dist/sandbox/harness-runner.mjs +162 -0
  288. package/dist/sandbox/harness-runner.mjs.map +1 -0
  289. package/dist/sandbox/index.d.mts +10 -0
  290. package/dist/sandbox/index.mjs +7 -0
  291. package/dist/sandbox/task-environment.d.mts +50 -0
  292. package/dist/sandbox/task-environment.mjs +140 -0
  293. package/dist/sandbox/task-environment.mjs.map +1 -0
  294. package/dist/sandbox/workspace.d.mts +29 -0
  295. package/dist/sandbox/workspace.mjs +28 -0
  296. package/dist/sandbox/workspace.mjs.map +1 -0
  297. package/dist/schema/agent-route.d.mts +40 -0
  298. package/dist/schema/agent-route.mjs +33 -0
  299. package/dist/schema/agent-route.mjs.map +1 -0
  300. package/dist/schema/answer.d.mts +23 -0
  301. package/dist/schema/answer.mjs +21 -0
  302. package/dist/schema/answer.mjs.map +1 -0
  303. package/dist/schema/budget.d.mts +38 -0
  304. package/dist/schema/budget.mjs +30 -0
  305. package/dist/schema/budget.mjs.map +1 -0
  306. package/dist/schema/catalog.mjs +72 -0
  307. package/dist/schema/catalog.mjs.map +1 -0
  308. package/dist/schema/change.d.mts +117 -0
  309. package/dist/schema/change.mjs +68 -0
  310. package/dist/schema/change.mjs.map +1 -0
  311. package/dist/schema/common.d.mts +33 -0
  312. package/dist/{id-BLIkCVue.mjs → schema/common.mjs} +7 -16
  313. package/dist/schema/common.mjs.map +1 -0
  314. package/dist/schema/communication.d.mts +52 -0
  315. package/dist/{communication-Kd9nU5RP.mjs → schema/communication.mjs} +8 -3
  316. package/dist/schema/communication.mjs.map +1 -0
  317. package/dist/schema/execution.d.mts +101 -0
  318. package/dist/schema/execution.mjs +86 -0
  319. package/dist/schema/execution.mjs.map +1 -0
  320. package/dist/schema/factory-config.d.mts +122 -0
  321. package/dist/schema/factory-config.mjs +90 -0
  322. package/dist/schema/factory-config.mjs.map +1 -0
  323. package/dist/schema/finding.d.mts +125 -0
  324. package/dist/schema/finding.mjs +67 -0
  325. package/dist/schema/finding.mjs.map +1 -0
  326. package/dist/schema/id.d.mts +43 -0
  327. package/dist/schema/id.mjs +32 -0
  328. package/dist/schema/id.mjs.map +1 -0
  329. package/dist/schema/outbox.d.mts +240 -0
  330. package/dist/schema/outbox.mjs +53 -0
  331. package/dist/schema/outbox.mjs.map +1 -0
  332. package/dist/schema/question-route.mjs +16 -0
  333. package/dist/schema/question-route.mjs.map +1 -0
  334. package/dist/schema/receipt.d.mts +81 -0
  335. package/dist/schema/receipt.mjs +94 -0
  336. package/dist/schema/receipt.mjs.map +1 -0
  337. package/dist/schema/repository.d.mts +30 -0
  338. package/dist/schema/repository.mjs +29 -0
  339. package/dist/schema/repository.mjs.map +1 -0
  340. package/dist/schema/session.d.mts +136 -0
  341. package/dist/schema/session.mjs +54 -0
  342. package/dist/schema/session.mjs.map +1 -0
  343. package/dist/schema/signal.d.mts +105 -0
  344. package/dist/schema/signal.mjs +81 -0
  345. package/dist/schema/signal.mjs.map +1 -0
  346. package/dist/schema/slack-pr-notifications.d.mts +12 -0
  347. package/dist/schema/slack-pr-notifications.mjs +11 -0
  348. package/dist/schema/slack-pr-notifications.mjs.map +1 -0
  349. package/dist/schema/stored-session.mjs +10 -0
  350. package/dist/schema/stored-session.mjs.map +1 -0
  351. package/dist/schema/stored-task.mjs +20 -0
  352. package/dist/schema/stored-task.mjs.map +1 -0
  353. package/dist/schema/task-effect.d.mts +39 -0
  354. package/dist/schema/task-effect.mjs +29 -0
  355. package/dist/schema/task-effect.mjs.map +1 -0
  356. package/dist/schema/task-graph.d.mts +3173 -0
  357. package/dist/schema/task-graph.mjs +97 -0
  358. package/dist/schema/task-graph.mjs.map +1 -0
  359. package/dist/schema/task-message.d.mts +254 -0
  360. package/dist/schema/task-message.mjs +189 -0
  361. package/dist/schema/task-message.mjs.map +1 -0
  362. package/dist/schema/task.d.mts +273 -0
  363. package/dist/schema/task.mjs +200 -0
  364. package/dist/schema/task.mjs.map +1 -0
  365. package/dist/schema/transcript.d.mts +176 -0
  366. package/dist/schema/transcript.mjs +87 -0
  367. package/dist/schema/transcript.mjs.map +1 -0
  368. package/dist/schema/transitions.d.mts +50 -0
  369. package/dist/schema/transitions.mjs +303 -0
  370. package/dist/schema/transitions.mjs.map +1 -0
  371. package/dist/schema/work.d.mts +150 -0
  372. package/dist/schema/work.mjs +102 -0
  373. package/dist/schema/work.mjs.map +1 -0
  374. package/dist/session-previews.d.mts +76 -0
  375. package/dist/session-previews.mjs +55 -0
  376. package/dist/session-previews.mjs.map +1 -0
  377. package/dist/session-review.d.mts +120 -0
  378. package/dist/session-review.mjs +79 -0
  379. package/dist/session-review.mjs.map +1 -0
  380. package/dist/signal-triage.d.mts +20 -0
  381. package/dist/signal-triage.mjs +85 -0
  382. package/dist/signal-triage.mjs.map +1 -0
  383. package/dist/signals.d.mts +20 -0
  384. package/dist/signals.mjs +19 -0
  385. package/dist/signals.mjs.map +1 -0
  386. package/dist/stall.d.mts +41 -0
  387. package/dist/stall.mjs +83 -0
  388. package/dist/stall.mjs.map +1 -0
  389. package/dist/storage.d.mts +4 -0
  390. package/dist/storage.mjs +4 -0
  391. package/dist/store/driver.d.mts +65 -0
  392. package/dist/store/driver.mjs +42 -0
  393. package/dist/store/driver.mjs.map +1 -0
  394. package/dist/store/engine.d.mts +683 -0
  395. package/dist/store/engine.mjs +2309 -0
  396. package/dist/store/engine.mjs.map +1 -0
  397. package/dist/store/memory.d.mts +24 -0
  398. package/dist/store/memory.mjs +69 -0
  399. package/dist/store/memory.mjs.map +1 -0
  400. package/dist/store/slack-pr-notifications.d.mts +44 -0
  401. package/dist/store/slack-pr-notifications.mjs +121 -0
  402. package/dist/store/slack-pr-notifications.mjs.map +1 -0
  403. package/dist/store/task-effects.d.mts +38 -0
  404. package/dist/store/task-effects.mjs +118 -0
  405. package/dist/store/task-effects.mjs.map +1 -0
  406. package/dist/store/task-graphs.d.mts +60 -0
  407. package/dist/store/task-graphs.mjs +212 -0
  408. package/dist/store/task-graphs.mjs.map +1 -0
  409. package/dist/store/task-messages.d.mts +136 -0
  410. package/dist/store/task-messages.mjs +475 -0
  411. package/dist/store/task-messages.mjs.map +1 -0
  412. package/dist/store/task-work.d.mts +180 -0
  413. package/dist/store/task-work.mjs +137 -0
  414. package/dist/store/task-work.mjs.map +1 -0
  415. package/dist/store/work-notifications.d.mts +19 -0
  416. package/dist/store/work-notifications.mjs +48 -0
  417. package/dist/store/work-notifications.mjs.map +1 -0
  418. package/dist/sweep.d.mts +79 -0
  419. package/dist/sweep.mjs +171 -0
  420. package/dist/sweep.mjs.map +1 -0
  421. package/dist/task-communication.d.mts +35 -0
  422. package/dist/task-communication.mjs +47 -0
  423. package/dist/task-communication.mjs.map +1 -0
  424. package/dist/task-graph-view.d.mts +267 -0
  425. package/dist/task-graph-view.mjs +17 -0
  426. package/dist/task-graph-view.mjs.map +1 -0
  427. package/dist/task-message-processing.d.mts +60 -0
  428. package/dist/task-message-processing.mjs +200 -0
  429. package/dist/task-message-processing.mjs.map +1 -0
  430. package/dist/task-protocols.d.mts +80 -0
  431. package/dist/task-protocols.mjs +47 -0
  432. package/dist/task-protocols.mjs.map +1 -0
  433. package/dist/tasks.d.mts +17 -0
  434. package/dist/tasks.mjs +17 -0
  435. package/dist/vercel-git.d.mts +53 -0
  436. package/dist/vercel-git.mjs +344 -0
  437. package/dist/vercel-git.mjs.map +1 -0
  438. package/dist/vercel-github-api.d.mts +103 -0
  439. package/dist/vercel-github-api.mjs +363 -0
  440. package/dist/vercel-github-api.mjs.map +1 -0
  441. package/dist/vercel.d.mts +34 -2
  442. package/dist/vercel.mjs +49 -3
  443. package/dist/vercel.mjs.map +1 -1
  444. package/dist/work-triage/eve-tool.d.mts +29 -0
  445. package/dist/work-triage/eve-tool.mjs +32 -0
  446. package/dist/work-triage/eve-tool.mjs.map +1 -0
  447. package/dist/work-triage/find-existing-work.d.mts +32 -0
  448. package/dist/work-triage/find-existing-work.mjs +31 -0
  449. package/dist/work-triage/find-existing-work.mjs.map +1 -0
  450. package/dist/work-triage/github.d.mts +15 -0
  451. package/dist/work-triage/github.mjs +14 -0
  452. package/dist/work-triage/github.mjs.map +1 -0
  453. package/dist/work-triage.d.mts +4 -0
  454. package/dist/work-triage.mjs +3 -0
  455. package/dist/workflows.d.mts +146 -0
  456. package/dist/workflows.mjs +139 -0
  457. package/dist/workflows.mjs.map +1 -0
  458. package/dist/workspace-diff-assembly.d.mts +33 -0
  459. package/dist/workspace-diff-assembly.mjs +95 -0
  460. package/dist/workspace-diff-assembly.mjs.map +1 -0
  461. package/dist/workspace-diff.d.mts +90 -0
  462. package/dist/workspace-diff.mjs +61 -0
  463. package/dist/workspace-diff.mjs.map +1 -0
  464. package/dist/workspace-files-git.d.mts +15 -0
  465. package/dist/workspace-files-git.mjs +61 -0
  466. package/dist/workspace-files-git.mjs.map +1 -0
  467. package/dist/workspace-files.d.mts +107 -0
  468. package/dist/workspace-files.mjs +74 -0
  469. package/dist/workspace-files.mjs.map +1 -0
  470. package/dist/workspace-snapshot.d.mts +26 -0
  471. package/dist/workspace-snapshot.mjs +80 -0
  472. package/dist/workspace-snapshot.mjs.map +1 -0
  473. package/docs/getting-started.md +104 -0
  474. package/docs/index.md +100 -0
  475. package/docs/recipes/cancellation.md +215 -0
  476. package/docs/recipes/custom-workflow.md +153 -0
  477. package/docs/recipes/dependent-tasks.md +207 -0
  478. package/docs/recipes/eve-agent.md +277 -0
  479. package/docs/recipes/human-input.md +204 -0
  480. package/docs/recipes/persistence-recovery.md +268 -0
  481. package/docs/recipes/retry-recovery.md +241 -0
  482. package/docs/recipes/task-messaging.md +215 -0
  483. package/docs/recipes/typed-eve-result.md +161 -0
  484. package/docs/runtime-integration.md +137 -0
  485. package/package.json +142 -46
  486. package/dist/approval-JX2Px9IC.mjs +0 -132
  487. package/dist/approval-JX2Px9IC.mjs.map +0 -1
  488. package/dist/budget-DzSiIA4t.mjs +0 -42
  489. package/dist/budget-DzSiIA4t.mjs.map +0 -1
  490. package/dist/change-D2U6v7Ek.mjs +0 -554
  491. package/dist/change-D2U6v7Ek.mjs.map +0 -1
  492. package/dist/communication-Kd9nU5RP.mjs.map +0 -1
  493. package/dist/coordination-Cq08hb1l.mjs +0 -76
  494. package/dist/coordination-Cq08hb1l.mjs.map +0 -1
  495. package/dist/dispatch-BaZS3PT8.d.mts +0 -76
  496. package/dist/driver-BC0Mt5KG.d.mts +0 -34
  497. package/dist/driver-Bbd25rNM.mjs +0 -23
  498. package/dist/driver-Bbd25rNM.mjs.map +0 -1
  499. package/dist/engine-DFHU2Pe-.d.mts +0 -781
  500. package/dist/engine-WaUM2cGA.mjs +0 -1068
  501. package/dist/engine-WaUM2cGA.mjs.map +0 -1
  502. package/dist/factory-config-BoRXOw43.mjs +0 -50
  503. package/dist/factory-config-BoRXOw43.mjs.map +0 -1
  504. package/dist/factory-config-D2yU3G0c.d.mts +0 -58
  505. package/dist/github-C5HNBN6p.d.mts +0 -279
  506. package/dist/id-BLIkCVue.mjs.map +0 -1
  507. package/dist/index-Bw6hggoH.d.mts +0 -40
  508. package/dist/index.mjs.map +0 -1
  509. package/dist/task-CcOyOJ3k.mjs +0 -154
  510. package/dist/task-CcOyOJ3k.mjs.map +0 -1
@@ -1,4 +1,5 @@
1
- import { n as RecordNotFoundError, r as VersionConflictError, t as RecordExistsError } from "../driver-Bbd25rNM.mjs";
1
+ import { RecordExistsError, RecordNotFoundError, TransientStoreError, VersionConflictError } from "../store/driver.mjs";
2
+ import { artifactReference, createArtifactStore, parseArtifactRef } from "./artifacts.mjs";
2
3
  import { get, list, put } from "@vercel/blob";
3
4
  //#region src/blob/index.ts
4
5
  function isNotFoundError(error) {
@@ -8,10 +9,49 @@ function isAlreadyExistsError(error) {
8
9
  return error instanceof Error && /already exists|not allowed to overwrite|overwrite/iu.test(error.message);
9
10
  }
10
11
  function isPreconditionError(error) {
11
- return error instanceof Error && (error.name === "BlobPreconditionFailedError" || /precondition|etag|412/iu.test(error.message));
12
+ return error instanceof Error && (error.name === "BlobPreconditionFailedError" || /precondition|etag|412/iu.test(error.message) || error.message === "Vercel Blob: The conditional request cannot succeed due to a conflicting operation against this resource.");
12
13
  }
14
+ const TRANSIENT_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
15
+ "ECONNRESET",
16
+ "ECONNREFUSED",
17
+ "ETIMEDOUT",
18
+ "ENOTFOUND",
19
+ "EAI_AGAIN",
20
+ "EPIPE",
21
+ "EHOSTUNREACH",
22
+ "ENETUNREACH",
23
+ "UND_ERR_CONNECT_TIMEOUT",
24
+ "UND_ERR_SOCKET",
25
+ "UND_ERR_HEADERS_TIMEOUT"
26
+ ]);
27
+ function isTransientBlobError(error) {
28
+ if (!(error instanceof Error)) return false;
29
+ if (isAlreadyExistsError(error) || isPreconditionError(error) || isNotFoundError(error)) return false;
30
+ const code = error.code;
31
+ if (code !== void 0 && TRANSIENT_NETWORK_ERROR_CODES.has(code)) return true;
32
+ if (error.name === "AbortError") return true;
33
+ if (/fetch failed|network|socket hang up|timed? ?out|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN/iu.test(error.message)) return true;
34
+ return /(?:^|[^0-9])(?:429|5\d{2})(?:[^0-9]|$)/u.test(error.message);
35
+ }
36
+ function jsonValuesEqual(a, b) {
37
+ if (a === b) return true;
38
+ if (typeof a !== typeof b) return false;
39
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
40
+ if (Array.isArray(a) || Array.isArray(b)) {
41
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
42
+ return a.every((item, index) => jsonValuesEqual(item, b[index]));
43
+ }
44
+ const aKeys = Object.keys(a);
45
+ const bKeys = Object.keys(b);
46
+ if (aKeys.length !== bKeys.length) return false;
47
+ return aKeys.every((key) => Object.hasOwn(b, key) && jsonValuesEqual(a[key], b[key]));
48
+ }
49
+ /**
50
+ * Default private Vercel Blob client used by the durable Factory store driver.
51
+ * Reads explicitly bypass the CDN cache, and writes resolve only after the SDK accepts them.
52
+ */
13
53
  const vercelBlobClient = {
14
- async read(pathname) {
54
+ async read({ pathname }) {
15
55
  let result;
16
56
  try {
17
57
  result = await get(pathname, {
@@ -28,15 +68,15 @@ const vercelBlobClient = {
28
68
  etag: result.blob.etag
29
69
  };
30
70
  },
31
- async write(pathname, body, overwrite, ifMatch) {
32
- await put(pathname, body, {
71
+ async write({ pathname, body, overwrite, ifMatch }) {
72
+ await put(pathname, body === "" ? Buffer.alloc(0) : body, {
33
73
  access: "private",
34
74
  allowOverwrite: overwrite,
35
75
  contentType: "application/json",
36
- ...ifMatch === void 0 ? {} : { ifMatch }
76
+ ...ifMatch === void 0 ? {} : { ifMatch: ifMatch.replace(/^W\//u, "") }
37
77
  });
38
78
  },
39
- async listPaths(prefix) {
79
+ async listPaths({ prefix }) {
40
80
  const paths = [];
41
81
  let cursor;
42
82
  do {
@@ -52,24 +92,71 @@ const vercelBlobClient = {
52
92
  }
53
93
  };
54
94
  /**
55
- * Store driver on Vercel Blob: durable and dependency-free (one Blob store on
56
- * the project, no database). Inserts are create-if-absent. Updates read the latest content,
57
- * bypassing Blob's CDN cache, and write with an ETag precondition, so a
58
- * writer that lost a race gets a VersionConflictError rather than
59
- * overwriting the winner.
95
+ * Creates a durable Vercel Blob driver with atomic compare-and-swap updates.
96
+ *
97
+ * @remarks
98
+ * Inserts are create-if-absent. Updates bypass Blob's CDN cache and use the current ETag, so a
99
+ * writer that loses a race receives `VersionConflictError` instead of overwriting the winner.
100
+ * Reads and writes retry only 5xx, 429, and network failures within the configured attempt and
101
+ * elapsed-time bounds; exhaustion surfaces as `TransientStoreError`. If a transient write may
102
+ * have landed before its response was lost, the retry reads the record back and treats only the
103
+ * exact intended value and version as success. A different value remains a real conflict.
104
+ *
105
+ * Collection listing downloads records under the shared `readConcurrency` bound, coalesces
106
+ * concurrent lists of the same collection, and rejects malformed persisted values. Listing the
107
+ * provider's paths itself is not retried by this driver. The caller owns credentials and any
108
+ * injected client's lifecycle.
109
+ *
110
+ * @param options - Optional Blob transport, namespace, read limit, retry policy, and retry clock.
111
+ * @returns A `blob` driver suitable for `createStores`.
112
+ * @throws When concurrency or retry bounds are invalid.
60
113
  */
61
114
  function createBlobDriver(options = {}) {
62
115
  const client = options.client ?? vercelBlobClient;
116
+ const now = options.now ?? Date.now;
63
117
  const prefix = options.prefix ?? "factory";
64
118
  const readConcurrency = options.readConcurrency ?? 32;
65
119
  if (!Number.isInteger(readConcurrency) || readConcurrency < 1) throw new Error("Blob read concurrency must be a positive integer");
120
+ const retry = {
121
+ attempts: options.retry?.attempts ?? 3,
122
+ baseDelayMs: options.retry?.baseDelayMs ?? 250,
123
+ maxTotalMs: options.retry?.maxTotalMs ?? 5e3
124
+ };
125
+ if (!Number.isInteger(retry.attempts) || retry.attempts < 1) throw new Error("Blob retry attempts must be a positive integer");
126
+ if (!Number.isFinite(retry.baseDelayMs) || retry.baseDelayMs < 0) throw new Error("Blob retry base delay must be a finite non-negative number");
127
+ if (!Number.isFinite(retry.maxTotalMs) || retry.maxTotalMs < 0) throw new Error("Blob retry elapsed-time limit must be a finite non-negative number");
128
+ let activeReads = 0;
129
+ const readWaiters = [];
130
+ const activeLists = /* @__PURE__ */ new Map();
131
+ async function withTransientRetry(collection, id, operation) {
132
+ const start = now();
133
+ for (let attempt = 0;; attempt += 1) try {
134
+ return await operation();
135
+ } catch (error) {
136
+ if (!isTransientBlobError(error)) throw error;
137
+ const nextDelayMs = retry.baseDelayMs * 2 ** attempt;
138
+ if (attempt >= retry.attempts - 1 || now() - start + nextDelayMs > retry.maxTotalMs) throw new TransientStoreError(collection, id, error);
139
+ await new Promise((resolve) => setTimeout(resolve, nextDelayMs));
140
+ }
141
+ }
142
+ async function limitedRead(pathname) {
143
+ if (activeReads < readConcurrency) activeReads += 1;
144
+ else await new Promise((resolve) => readWaiters.push(resolve));
145
+ try {
146
+ return await client.read({ pathname });
147
+ } finally {
148
+ const waiter = readWaiters.shift();
149
+ if (waiter === void 0) activeReads -= 1;
150
+ else waiter();
151
+ }
152
+ }
66
153
  function pathFor(collection, id) {
67
154
  return `${prefix}/${collection}/${id}.json`;
68
155
  }
69
156
  async function readRecord(collection, id) {
70
157
  let object;
71
158
  try {
72
- object = await client.read(pathFor(collection, id));
159
+ object = await withTransientRetry(collection, id, () => limitedRead(pathFor(collection, id)));
73
160
  } catch (error) {
74
161
  if (isNotFoundError(error)) return null;
75
162
  throw error;
@@ -83,6 +170,14 @@ function createBlobDriver(options = {}) {
83
170
  etag: object.etag
84
171
  };
85
172
  }
173
+ async function wroteThisInsert(collection, id, value) {
174
+ const after = await readRecord(collection, id);
175
+ return after !== null && after.version === 1 && jsonValuesEqual(after.value, value);
176
+ }
177
+ async function wroteThisUpdate(collection, id, value, expectedNewVersion) {
178
+ const after = await readRecord(collection, id);
179
+ return after !== null && after.version === expectedNewVersion && jsonValuesEqual(after.value, value);
180
+ }
86
181
  return {
87
182
  name: "blob",
88
183
  async get(collection, id) {
@@ -92,56 +187,95 @@ function createBlobDriver(options = {}) {
92
187
  version: record.version
93
188
  };
94
189
  },
95
- async insert(collection, id, value) {
190
+ async insert({ collection, id, value }) {
191
+ let attempts = 0;
96
192
  try {
97
- await client.write(pathFor(collection, id), JSON.stringify({
98
- value,
99
- version: 1
100
- }), false);
193
+ await withTransientRetry(collection, id, () => {
194
+ attempts += 1;
195
+ return client.write({
196
+ pathname: pathFor(collection, id),
197
+ body: JSON.stringify({
198
+ value,
199
+ version: 1
200
+ }),
201
+ overwrite: false
202
+ });
203
+ });
101
204
  } catch (error) {
102
- if (isAlreadyExistsError(error)) throw new RecordExistsError(collection, id);
205
+ if (isAlreadyExistsError(error)) {
206
+ if (attempts > 1 && await wroteThisInsert(collection, id, value)) return;
207
+ throw new RecordExistsError(collection, id);
208
+ }
103
209
  throw error;
104
210
  }
105
211
  },
106
- async update(collection, id, value, expectedVersion) {
212
+ async update({ collection, id, value, expectedVersion }) {
107
213
  const existing = await readRecord(collection, id);
108
214
  if (existing === null) throw new RecordNotFoundError(collection, id);
109
215
  if (existing.version !== expectedVersion) throw new VersionConflictError(collection, id, expectedVersion);
216
+ let attempts = 0;
110
217
  try {
111
- await client.write(pathFor(collection, id), JSON.stringify({
112
- value,
113
- version: expectedVersion + 1
114
- }), true, existing.etag.replace(/^W\//u, ""));
218
+ await withTransientRetry(collection, id, () => {
219
+ attempts += 1;
220
+ return client.write({
221
+ pathname: pathFor(collection, id),
222
+ body: JSON.stringify({
223
+ value,
224
+ version: expectedVersion + 1
225
+ }),
226
+ overwrite: true,
227
+ ifMatch: existing.etag.replace(/^W\//u, "")
228
+ });
229
+ });
115
230
  } catch (error) {
116
- if (isPreconditionError(error)) throw new VersionConflictError(collection, id, expectedVersion);
231
+ if (isPreconditionError(error)) {
232
+ if (attempts > 1 && await wroteThisUpdate(collection, id, value, expectedVersion + 1)) return;
233
+ throw new VersionConflictError(collection, id, expectedVersion);
234
+ }
117
235
  throw error;
118
236
  }
119
237
  },
120
238
  async list(collection) {
121
- const dir = `${prefix}/${collection}/`;
122
- const paths = await client.listPaths(dir);
123
- const entries = new Array(paths.length);
124
- let next = 0;
125
- const workers = Array.from({ length: Math.min(readConcurrency, paths.length) }, async () => {
126
- while (next < paths.length) {
127
- const index = next++;
128
- const id = paths[index].slice(dir.length).replace(/\.json$/u, "");
129
- const record = await readRecord(collection, id);
130
- entries[index] = record === null ? null : {
131
- id,
132
- record: {
133
- value: record.value,
134
- version: record.version
239
+ const active = activeLists.get(collection);
240
+ if (active !== void 0) return active;
241
+ const listing = (async () => {
242
+ const dir = `${prefix}/${collection}/`;
243
+ const paths = await client.listPaths({ prefix: dir });
244
+ const entries = new Array(paths.length);
245
+ let next = 0;
246
+ let failure;
247
+ const workers = Array.from({ length: Math.min(readConcurrency, paths.length) }, async () => {
248
+ while (next < paths.length && failure === void 0) {
249
+ const index = next++;
250
+ const id = paths[index].slice(dir.length).replace(/\.json$/u, "");
251
+ try {
252
+ const record = await readRecord(collection, id);
253
+ entries[index] = record === null ? null : {
254
+ id,
255
+ record: {
256
+ value: record.value,
257
+ version: record.version
258
+ }
259
+ };
260
+ } catch (error) {
261
+ failure ??= error;
135
262
  }
136
- };
137
- }
138
- });
139
- await Promise.all(workers);
140
- return entries.filter((entry) => entry !== null);
263
+ }
264
+ });
265
+ await Promise.all(workers);
266
+ if (failure !== void 0) throw failure;
267
+ return entries.filter((entry) => entry !== null);
268
+ })();
269
+ activeLists.set(collection, listing);
270
+ try {
271
+ return await listing;
272
+ } finally {
273
+ if (activeLists.get(collection) === listing) activeLists.delete(collection);
274
+ }
141
275
  }
142
276
  };
143
277
  }
144
278
  //#endregion
145
- export { createBlobDriver, vercelBlobClient };
279
+ export { artifactReference, createArtifactStore, createBlobDriver, parseArtifactRef, vercelBlobClient };
146
280
 
147
281
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/blob/index.ts"],"sourcesContent":["import { get, list, put } from \"@vercel/blob\";\nimport {\n RecordExistsError,\n RecordNotFoundError,\n VersionConflictError,\n type CollectionName,\n type StoredEntry,\n type StoreDriver,\n} from \"../store/driver\";\n\nexport interface BlobObject {\n body: string;\n etag: string;\n}\n\n/** The narrow blob operations the driver needs; injectable for tests. */\nexport interface BlobClient {\n /** Must return the latest content, never a cached copy. */\n read(pathname: string): Promise<BlobObject | null>;\n /** Must reject if overwrite is false and the path exists, or if ifMatch is given and the ETag differs. */\n write(pathname: string, body: string, overwrite: boolean, ifMatch?: string): Promise<void>;\n listPaths(prefix: string): Promise<string[]>;\n}\n\n// @vercel/blob throws BlobNotFoundError with the message\n// \"Vercel Blob: The requested blob does not exist\".\nfunction isNotFoundError(error: unknown): boolean {\n return (\n error instanceof Error &&\n (error.name === \"BlobNotFoundError\" ||\n /does not exist|not.?found|404/iu.test(error.message))\n );\n}\n\nfunction isAlreadyExistsError(error: unknown): boolean {\n return (\n error instanceof Error &&\n /already exists|not allowed to overwrite|overwrite/iu.test(error.message)\n );\n}\n\n// @vercel/blob throws BlobPreconditionFailedError when ifMatch does not hold.\nfunction isPreconditionError(error: unknown): boolean {\n return (\n error instanceof Error &&\n (error.name === \"BlobPreconditionFailedError\" || /precondition|etag|412/iu.test(error.message))\n );\n}\n\nexport const vercelBlobClient: BlobClient = {\n async read(pathname) {\n let result;\n try {\n // Uncached: the ledger is read-modify-write and must see the latest version.\n result = await get(pathname, { access: \"private\", useCache: false });\n } catch (error) {\n if (isNotFoundError(error)) {\n return null;\n }\n throw error;\n }\n if (result === null || result.stream === null) {\n return null;\n }\n return { body: await new Response(result.stream).text(), etag: result.blob.etag };\n },\n\n async write(pathname, body, overwrite, ifMatch) {\n await put(pathname, body, {\n access: \"private\",\n allowOverwrite: overwrite,\n contentType: \"application/json\",\n ...(ifMatch === undefined ? {} : { ifMatch }),\n });\n },\n\n async listPaths(prefix) {\n const paths: string[] = [];\n let cursor: string | undefined;\n do {\n const page = await list({ prefix, cursor, limit: 1000 });\n paths.push(...page.blobs.map((blob) => blob.pathname));\n cursor = page.hasMore ? page.cursor : undefined;\n } while (cursor !== undefined);\n return paths;\n },\n};\n\nexport interface BlobDriverOptions {\n client?: BlobClient;\n /** Path prefix inside the blob store; defaults to \"factory\". */\n prefix?: string;\n /** Maximum Blob downloads in flight while listing a collection. Default 32. */\n readConcurrency?: number;\n}\n\n/**\n * Store driver on Vercel Blob: durable and dependency-free (one Blob store on\n * the project, no database). Inserts are create-if-absent. Updates read the latest content,\n * bypassing Blob's CDN cache, and write with an ETag precondition, so a\n * writer that lost a race gets a VersionConflictError rather than\n * overwriting the winner.\n */\nexport function createBlobDriver(options: BlobDriverOptions = {}): StoreDriver {\n const client = options.client ?? vercelBlobClient;\n const prefix = options.prefix ?? \"factory\";\n const readConcurrency = options.readConcurrency ?? 32;\n if (!Number.isInteger(readConcurrency) || readConcurrency < 1) {\n throw new Error(\"Blob read concurrency must be a positive integer\");\n }\n\n function pathFor(collection: CollectionName, id: string): string {\n return `${prefix}/${collection}/${id}.json`;\n }\n\n async function readRecord(\n collection: CollectionName,\n id: string,\n ): Promise<{ value: unknown; version: number; etag: string } | null> {\n let object: BlobObject | null;\n try {\n object = await client.read(pathFor(collection, id));\n } catch (error) {\n if (isNotFoundError(error)) {\n return null;\n }\n throw error;\n }\n if (object === null) {\n return null;\n }\n const parsed = JSON.parse(object.body) as { value: unknown; version: number };\n if (typeof parsed.version !== \"number\") {\n throw new Error(`malformed stored record at ${pathFor(collection, id)}`);\n }\n return { value: parsed.value, version: parsed.version, etag: object.etag };\n }\n\n return {\n name: \"blob\",\n\n async get(collection, id) {\n const record = await readRecord(collection, id);\n return record === null ? null : { value: record.value, version: record.version };\n },\n\n async insert(collection, id, value) {\n try {\n await client.write(\n pathFor(collection, id),\n JSON.stringify({ value, version: 1 }),\n false,\n );\n } catch (error) {\n if (isAlreadyExistsError(error)) {\n throw new RecordExistsError(collection, id);\n }\n throw error;\n }\n },\n\n async update(collection, id, value, expectedVersion) {\n const existing = await readRecord(collection, id);\n if (existing === null) {\n throw new RecordNotFoundError(collection, id);\n }\n if (existing.version !== expectedVersion) {\n throw new VersionConflictError(collection, id, expectedVersion);\n }\n try {\n await client.write(\n pathFor(collection, id),\n JSON.stringify({ value, version: expectedVersion + 1 }),\n true,\n // The write API's ifMatch rejects the weak form (W/\"…\") that reads return.\n existing.etag.replace(/^W\\//u, \"\"),\n );\n } catch (error) {\n if (isPreconditionError(error)) {\n throw new VersionConflictError(collection, id, expectedVersion);\n }\n throw error;\n }\n },\n\n async list(collection) {\n const dir = `${prefix}/${collection}/`;\n const paths = await client.listPaths(dir);\n const entries: (StoredEntry | null)[] = new Array(paths.length);\n let next = 0;\n const workers = Array.from(\n { length: Math.min(readConcurrency, paths.length) },\n async () => {\n while (next < paths.length) {\n const index = next++;\n const pathname = paths[index]!;\n const id = pathname.slice(dir.length).replace(/\\.json$/u, \"\");\n const record = await readRecord(collection, id);\n entries[index] = record === null\n ? null\n : { id, record: { value: record.value, version: record.version } };\n }\n },\n );\n await Promise.all(workers);\n return entries.filter((entry): entry is StoredEntry => entry !== null);\n },\n };\n}\n"],"mappings":";;;AA0BA,SAAS,gBAAgB,OAAyB;CAChD,OACE,iBAAiB,UAChB,MAAM,SAAS,uBACd,kCAAkC,KAAK,MAAM,OAAO;AAE1D;AAEA,SAAS,qBAAqB,OAAyB;CACrD,OACE,iBAAiB,SACjB,sDAAsD,KAAK,MAAM,OAAO;AAE5E;AAGA,SAAS,oBAAoB,OAAyB;CACpD,OACE,iBAAiB,UAChB,MAAM,SAAS,iCAAiC,0BAA0B,KAAK,MAAM,OAAO;AAEjG;AAEA,MAAa,mBAA+B;CAC1C,MAAM,KAAK,UAAU;EACnB,IAAI;EACJ,IAAI;GAEF,SAAS,MAAM,IAAI,UAAU;IAAE,QAAQ;IAAW,UAAU;GAAM,CAAC;EACrE,SAAS,OAAO;GACd,IAAI,gBAAgB,KAAK,GACvB,OAAO;GAET,MAAM;EACR;EACA,IAAI,WAAW,QAAQ,OAAO,WAAW,MACvC,OAAO;EAET,OAAO;GAAE,MAAM,MAAM,IAAI,SAAS,OAAO,MAAM,CAAC,CAAC,KAAK;GAAG,MAAM,OAAO,KAAK;EAAK;CAClF;CAEA,MAAM,MAAM,UAAU,MAAM,WAAW,SAAS;EAC9C,MAAM,IAAI,UAAU,MAAM;GACxB,QAAQ;GACR,gBAAgB;GAChB,aAAa;GACb,GAAI,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ;EAC7C,CAAC;CACH;CAEA,MAAM,UAAU,QAAQ;EACtB,MAAM,QAAkB,CAAC;EACzB,IAAI;EACJ,GAAG;GACD,MAAM,OAAO,MAAM,KAAK;IAAE;IAAQ;IAAQ,OAAO;GAAK,CAAC;GACvD,MAAM,KAAK,GAAG,KAAK,MAAM,KAAK,SAAS,KAAK,QAAQ,CAAC;GACrD,SAAS,KAAK,UAAU,KAAK,SAAS,KAAA;EACxC,SAAS,WAAW,KAAA;EACpB,OAAO;CACT;AACF;;;;;;;;AAiBA,SAAgB,iBAAiB,UAA6B,CAAC,GAAgB;CAC7E,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,kBAAkB,QAAQ,mBAAmB;CACnD,IAAI,CAAC,OAAO,UAAU,eAAe,KAAK,kBAAkB,GAC1D,MAAM,IAAI,MAAM,kDAAkD;CAGpE,SAAS,QAAQ,YAA4B,IAAoB;EAC/D,OAAO,GAAG,OAAO,GAAG,WAAW,GAAG,GAAG;CACvC;CAEA,eAAe,WACb,YACA,IACmE;EACnE,IAAI;EACJ,IAAI;GACF,SAAS,MAAM,OAAO,KAAK,QAAQ,YAAY,EAAE,CAAC;EACpD,SAAS,OAAO;GACd,IAAI,gBAAgB,KAAK,GACvB,OAAO;GAET,MAAM;EACR;EACA,IAAI,WAAW,MACb,OAAO;EAET,MAAM,SAAS,KAAK,MAAM,OAAO,IAAI;EACrC,IAAI,OAAO,OAAO,YAAY,UAC5B,MAAM,IAAI,MAAM,8BAA8B,QAAQ,YAAY,EAAE,GAAG;EAEzE,OAAO;GAAE,OAAO,OAAO;GAAO,SAAS,OAAO;GAAS,MAAM,OAAO;EAAK;CAC3E;CAEA,OAAO;EACL,MAAM;EAEN,MAAM,IAAI,YAAY,IAAI;GACxB,MAAM,SAAS,MAAM,WAAW,YAAY,EAAE;GAC9C,OAAO,WAAW,OAAO,OAAO;IAAE,OAAO,OAAO;IAAO,SAAS,OAAO;GAAQ;EACjF;EAEA,MAAM,OAAO,YAAY,IAAI,OAAO;GAClC,IAAI;IACF,MAAM,OAAO,MACX,QAAQ,YAAY,EAAE,GACtB,KAAK,UAAU;KAAE;KAAO,SAAS;IAAE,CAAC,GACpC,KACF;GACF,SAAS,OAAO;IACd,IAAI,qBAAqB,KAAK,GAC5B,MAAM,IAAI,kBAAkB,YAAY,EAAE;IAE5C,MAAM;GACR;EACF;EAEA,MAAM,OAAO,YAAY,IAAI,OAAO,iBAAiB;GACnD,MAAM,WAAW,MAAM,WAAW,YAAY,EAAE;GAChD,IAAI,aAAa,MACf,MAAM,IAAI,oBAAoB,YAAY,EAAE;GAE9C,IAAI,SAAS,YAAY,iBACvB,MAAM,IAAI,qBAAqB,YAAY,IAAI,eAAe;GAEhE,IAAI;IACF,MAAM,OAAO,MACX,QAAQ,YAAY,EAAE,GACtB,KAAK,UAAU;KAAE;KAAO,SAAS,kBAAkB;IAAE,CAAC,GACtD,MAEA,SAAS,KAAK,QAAQ,SAAS,EAAE,CACnC;GACF,SAAS,OAAO;IACd,IAAI,oBAAoB,KAAK,GAC3B,MAAM,IAAI,qBAAqB,YAAY,IAAI,eAAe;IAEhE,MAAM;GACR;EACF;EAEA,MAAM,KAAK,YAAY;GACrB,MAAM,MAAM,GAAG,OAAO,GAAG,WAAW;GACpC,MAAM,QAAQ,MAAM,OAAO,UAAU,GAAG;GACxC,MAAM,UAAkC,IAAI,MAAM,MAAM,MAAM;GAC9D,IAAI,OAAO;GACX,MAAM,UAAU,MAAM,KACpB,EAAE,QAAQ,KAAK,IAAI,iBAAiB,MAAM,MAAM,EAAE,GAClD,YAAY;IACV,OAAO,OAAO,MAAM,QAAQ;KAC1B,MAAM,QAAQ;KAEd,MAAM,KADW,MAAM,MACJ,CAAC,MAAM,IAAI,MAAM,CAAC,CAAC,QAAQ,YAAY,EAAE;KAC5D,MAAM,SAAS,MAAM,WAAW,YAAY,EAAE;KAC9C,QAAQ,SAAS,WAAW,OACxB,OACA;MAAE;MAAI,QAAQ;OAAE,OAAO,OAAO;OAAO,SAAS,OAAO;MAAQ;KAAE;IACrE;GACF,CACF;GACA,MAAM,QAAQ,IAAI,OAAO;GACzB,OAAO,QAAQ,QAAQ,UAAgC,UAAU,IAAI;EACvE;CACF;AACF"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/blob/index.ts"],"sourcesContent":["import { get, list, put } from \"@vercel/blob\";\nexport {\n artifactReference,\n createArtifactStore,\n parseArtifactRef,\n type ArtifactRef,\n type ArtifactInput,\n type ArtifactKind,\n type ReadArtifactInput,\n type ArtifactStore,\n} from \"./artifacts\";\nimport {\n RecordExistsError,\n RecordNotFoundError,\n TransientStoreError,\n VersionConflictError,\n type CollectionName,\n type StoredEntry,\n type StoreDriver,\n} from \"../store/driver\";\nimport type { MillisecondClock } from \"../schema/common\";\n\n/** Blob content paired with the ETag required for compare-and-swap persistence. */\nexport interface BlobObject {\n /** Complete UTF-8 body returned by an uncached read. */\n body: string;\n /** Current object ETag used to fence a later conditional write. */\n etag: string;\n}\n\n/** Exact blob pathname requested from storage. */\nexport interface ReadBlobInput {\n /** Exact namespaced object path; the client must not apply a second namespace. */\n pathname: string;\n}\n\n/** Blob body and conditional-write policy used for one storage write. */\nexport interface WriteBlobInput extends ReadBlobInput {\n /** Complete replacement body to persist. */\n body: string;\n /** Whether an existing path may be replaced; `false` means create-if-absent. */\n overwrite: boolean;\n /** Optional ETag that must still match atomically when overwriting. */\n ifMatch?: string;\n}\n\n/** Path prefix used to list matching blobs. */\nexport interface ListBlobPathsInput {\n /** Path prefix whose complete matching object names are requested. */\n prefix: string;\n}\n\n/** The narrow blob operations the driver needs; injectable for tests. */\nexport interface BlobClient {\n /**\n * Returns the latest content rather than a cached copy, or `null` when absent.\n *\n * @param input - Exact object path to read.\n * @returns Current body and ETag, or `null` when the path does not exist.\n */\n read(input: ReadBlobInput): Promise<BlobObject | null>;\n /**\n * Persists one complete object and enforces its create or ETag precondition atomically.\n *\n * @param input - Exact path, body, overwrite policy, and optional ETag fence.\n * @returns After the object write is durable at the provider boundary.\n */\n write(input: WriteBlobInput): Promise<void>;\n /**\n * Lists every object path matching the prefix, following all provider pages.\n *\n * @param input - Namespace prefix to enumerate.\n * @returns Matching exact paths; callers do not depend on their order.\n */\n listPaths(input: ListBlobPathsInput): Promise<readonly string[]>;\n}\n\n// @vercel/blob throws BlobNotFoundError with the message\n// \"Vercel Blob: The requested blob does not exist\".\nfunction isNotFoundError(error: unknown): boolean {\n return (\n error instanceof Error &&\n (error.name === \"BlobNotFoundError\" || /does not exist|not.?found|404/iu.test(error.message))\n );\n}\n\nfunction isAlreadyExistsError(error: unknown): boolean {\n return (\n error instanceof Error &&\n /already exists|not allowed to overwrite|overwrite/iu.test(error.message)\n );\n}\n\n// The SDK also forwards provider conditional-operation conflicts as generic BlobErrors.\nfunction isPreconditionError(error: unknown): boolean {\n return (\n error instanceof Error &&\n (error.name === \"BlobPreconditionFailedError\" ||\n /precondition|etag|412/iu.test(error.message) ||\n error.message ===\n \"Vercel Blob: The conditional request cannot succeed due to a conflicting operation against this resource.\")\n );\n}\n\nconst TRANSIENT_NETWORK_ERROR_CODES = new Set([\n \"ECONNRESET\",\n \"ECONNREFUSED\",\n \"ETIMEDOUT\",\n \"ENOTFOUND\",\n \"EAI_AGAIN\",\n \"EPIPE\",\n \"EHOSTUNREACH\",\n \"ENETUNREACH\",\n \"UND_ERR_CONNECT_TIMEOUT\",\n \"UND_ERR_SOCKET\",\n \"UND_ERR_HEADERS_TIMEOUT\",\n]);\n\n// Blob 5xx/429s and plain network failures are transient: the same read or\n// write is worth retrying with backoff. Direct downloads bypass the SDK's\n// own request-retry policy entirely, and outages can also surface as a\n// generic HTTP error on the write path, so this checks the error's code and\n// message rather than one fixed message shape.\nfunction isTransientBlobError(error: unknown): boolean {\n if (!(error instanceof Error)) return false;\n if (isAlreadyExistsError(error) || isPreconditionError(error) || isNotFoundError(error))\n return false;\n const code = (error as NodeJS.ErrnoException).code;\n if (code !== undefined && TRANSIENT_NETWORK_ERROR_CODES.has(code)) return true;\n if (error.name === \"AbortError\") return true;\n if (\n /fetch failed|network|socket hang up|timed? ?out|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN/iu.test(\n error.message,\n )\n ) {\n return true;\n }\n // Matches an HTTP 429 or 5xx status appearing anywhere in the message,\n // whether from the direct-download fetch or a generic API error.\n return /(?:^|[^0-9])(?:429|5\\d{2})(?:[^0-9]|$)/u.test(error.message);\n}\n\n// Structural equality over JSON-serializable values: good enough to tell\n// whether a record we just read back is the exact one we tried to write,\n// without depending on key order surviving a JSON round-trip.\nfunction jsonValuesEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (a === null || b === null || typeof a !== \"object\" || typeof b !== \"object\") return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;\n return a.every((item, index) => jsonValuesEqual(item, b[index]));\n }\n const aKeys = Object.keys(a as Record<string, unknown>);\n const bKeys = Object.keys(b as Record<string, unknown>);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every(\n (key) =>\n Object.hasOwn(b as Record<string, unknown>, key) &&\n jsonValuesEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n );\n}\n\n/**\n * Default private Vercel Blob client used by the durable Factory store driver.\n * Reads explicitly bypass the CDN cache, and writes resolve only after the SDK accepts them.\n */\nexport const vercelBlobClient: BlobClient = {\n async read({ pathname }) {\n let result;\n try {\n // Uncached: the ledger is read-modify-write and must see the latest version.\n result = await get(pathname, { access: \"private\", useCache: false });\n } catch (error) {\n if (isNotFoundError(error)) {\n return null;\n }\n throw error;\n }\n if (result === null || result.stream === null) {\n return null;\n }\n return { body: await new Response(result.stream).text(), etag: result.blob.etag };\n },\n\n async write({ pathname, body, overwrite, ifMatch }) {\n // The SDK treats an empty string as a missing body.\n await put(pathname, body === \"\" ? Buffer.alloc(0) : body, {\n access: \"private\",\n allowOverwrite: overwrite,\n contentType: \"application/json\",\n ...(ifMatch === undefined ? {} : { ifMatch: ifMatch.replace(/^W\\//u, \"\") }),\n });\n },\n\n async listPaths({ prefix }) {\n const paths: string[] = [];\n let cursor: string | undefined;\n do {\n const page = await list({ prefix, cursor, limit: 1000 });\n paths.push(...page.blobs.map((blob) => blob.pathname));\n cursor = page.hasMore ? page.cursor : undefined;\n } while (cursor !== undefined);\n return paths;\n },\n};\n\n/** Bounds retries and exponential backoff for transient Blob operations. */\nexport interface BlobRetryOptions {\n /** Positive integer attempts for one read or write, including the first. Default 3. */\n attempts?: number;\n /** Finite non-negative backoff base in milliseconds; doubles each retry. Default 250. */\n baseDelayMs?: number;\n /** Finite non-negative elapsed-time ceiling in milliseconds. Default 5000. */\n maxTotalMs?: number;\n}\n\n/** Configures the Blob client, namespace, read concurrency, and transient retries. */\nexport interface BlobDriverOptions {\n /** Blob transport used for reads, conditional writes, and listings; defaults to Vercel Blob. */\n client?: BlobClient;\n /** Path prefix inside the blob store; defaults to \"factory\". */\n prefix?: string;\n /** Maximum Blob downloads in flight while listing a collection. Default 32. */\n readConcurrency?: number;\n /** Bounds retry of transient (5xx/429/network) Blob failures on reads and writes. */\n retry?: BlobRetryOptions;\n /** Millisecond clock used to bound total retry time. */\n now?: MillisecondClock;\n}\n\n/**\n * Creates a durable Vercel Blob driver with atomic compare-and-swap updates.\n *\n * @remarks\n * Inserts are create-if-absent. Updates bypass Blob's CDN cache and use the current ETag, so a\n * writer that loses a race receives `VersionConflictError` instead of overwriting the winner.\n * Reads and writes retry only 5xx, 429, and network failures within the configured attempt and\n * elapsed-time bounds; exhaustion surfaces as `TransientStoreError`. If a transient write may\n * have landed before its response was lost, the retry reads the record back and treats only the\n * exact intended value and version as success. A different value remains a real conflict.\n *\n * Collection listing downloads records under the shared `readConcurrency` bound, coalesces\n * concurrent lists of the same collection, and rejects malformed persisted values. Listing the\n * provider's paths itself is not retried by this driver. The caller owns credentials and any\n * injected client's lifecycle.\n *\n * @param options - Optional Blob transport, namespace, read limit, retry policy, and retry clock.\n * @returns A `blob` driver suitable for `createStores`.\n * @throws When concurrency or retry bounds are invalid.\n */\nexport function createBlobDriver(options: BlobDriverOptions = {}): StoreDriver {\n const client = options.client ?? vercelBlobClient;\n const now = options.now ?? Date.now;\n const prefix = options.prefix ?? \"factory\";\n const readConcurrency = options.readConcurrency ?? 32;\n if (!Number.isInteger(readConcurrency) || readConcurrency < 1) {\n throw new Error(\"Blob read concurrency must be a positive integer\");\n }\n const retry = {\n attempts: options.retry?.attempts ?? 3,\n baseDelayMs: options.retry?.baseDelayMs ?? 250,\n maxTotalMs: options.retry?.maxTotalMs ?? 5000,\n };\n if (!Number.isInteger(retry.attempts) || retry.attempts < 1) {\n throw new Error(\"Blob retry attempts must be a positive integer\");\n }\n if (!Number.isFinite(retry.baseDelayMs) || retry.baseDelayMs < 0) {\n throw new Error(\"Blob retry base delay must be a finite non-negative number\");\n }\n if (!Number.isFinite(retry.maxTotalMs) || retry.maxTotalMs < 0) {\n throw new Error(\"Blob retry elapsed-time limit must be a finite non-negative number\");\n }\n let activeReads = 0;\n const readWaiters: (() => void)[] = [];\n const activeLists = new Map<CollectionName, Promise<StoredEntry[]>>();\n\n // Retries only transient failures, with exponential backoff bounded by\n // both attempt count and total elapsed time. Non-transient errors (not\n // found, already exists, precondition/version conflicts, malformed data)\n // are rethrown immediately and untouched by this loop.\n async function withTransientRetry<T>(\n collection: CollectionName,\n id: string,\n operation: () => Promise<T>,\n ): Promise<T> {\n const start = now();\n for (let attempt = 0; ; attempt += 1) {\n try {\n return await operation();\n } catch (error) {\n if (!isTransientBlobError(error)) throw error;\n const nextDelayMs = retry.baseDelayMs * 2 ** attempt;\n if (attempt >= retry.attempts - 1 || now() - start + nextDelayMs > retry.maxTotalMs) {\n throw new TransientStoreError(collection, id, error);\n }\n await new Promise((resolve) => setTimeout(resolve, nextDelayMs));\n }\n }\n }\n\n async function limitedRead(pathname: string): Promise<BlobObject | null> {\n if (activeReads < readConcurrency) activeReads += 1;\n else await new Promise<void>((resolve) => readWaiters.push(resolve));\n try {\n return await client.read({ pathname });\n } finally {\n const waiter = readWaiters.shift();\n if (waiter === undefined) activeReads -= 1;\n else waiter();\n }\n }\n\n function pathFor(collection: CollectionName, id: string): string {\n return `${prefix}/${collection}/${id}.json`;\n }\n\n async function readRecord(\n collection: CollectionName,\n id: string,\n ): Promise<{ value: unknown; version: number; etag: string } | null> {\n let object: BlobObject | null;\n try {\n object = await withTransientRetry(collection, id, () => limitedRead(pathFor(collection, id)));\n } catch (error) {\n if (isNotFoundError(error)) {\n return null;\n }\n throw error;\n }\n if (object === null) {\n return null;\n }\n const parsed = JSON.parse(object.body) as { value: unknown; version: number };\n if (typeof parsed.version !== \"number\") {\n throw new Error(`malformed stored record at ${pathFor(collection, id)}`);\n }\n return { value: parsed.value, version: parsed.version, etag: object.etag };\n }\n\n // A write that fails with a transient error (network drop, 5xx) may still\n // have landed on the server before the response reached us: the retry\n // withTransientRetry just took then legitimately collides with our own\n // prior success. These two helpers disambiguate that case from a real\n // conflict with another writer by reading the record back and comparing\n // it to what this call tried to write; only called after this call itself\n // retried (a fresh, non-retried collision is always a real conflict).\n async function wroteThisInsert(\n collection: CollectionName,\n id: string,\n value: unknown,\n ): Promise<boolean> {\n const after = await readRecord(collection, id);\n return after !== null && after.version === 1 && jsonValuesEqual(after.value, value);\n }\n\n async function wroteThisUpdate(\n collection: CollectionName,\n id: string,\n value: unknown,\n expectedNewVersion: number,\n ): Promise<boolean> {\n const after = await readRecord(collection, id);\n return (\n after !== null && after.version === expectedNewVersion && jsonValuesEqual(after.value, value)\n );\n }\n\n return {\n name: \"blob\",\n\n async get(collection, id) {\n const record = await readRecord(collection, id);\n return record === null ? null : { value: record.value, version: record.version };\n },\n\n async insert({ collection, id, value }) {\n // Tracks whether this call itself retried: a write that fails with a\n // transient error can still have landed on the server before the\n // response was lost, so a *retried* write's own \"already exists\"\n // collision is ambiguous rather than automatically a real conflict.\n let attempts = 0;\n try {\n await withTransientRetry(collection, id, () => {\n attempts += 1;\n return client.write({\n pathname: pathFor(collection, id),\n body: JSON.stringify({ value, version: 1 }),\n overwrite: false,\n });\n });\n } catch (error) {\n if (isAlreadyExistsError(error)) {\n if (attempts > 1 && (await wroteThisInsert(collection, id, value))) {\n return;\n }\n throw new RecordExistsError(collection, id);\n }\n throw error;\n }\n },\n\n async update({ collection, id, value, expectedVersion }) {\n const existing = await readRecord(collection, id);\n if (existing === null) {\n throw new RecordNotFoundError(collection, id);\n }\n if (existing.version !== expectedVersion) {\n throw new VersionConflictError(collection, id, expectedVersion);\n }\n // Tracks whether this call itself retried: see the matching comment on\n // insert() above. A retried update's own precondition failure is\n // ambiguous in the same way.\n let attempts = 0;\n try {\n await withTransientRetry(collection, id, () => {\n attempts += 1;\n return client.write({\n pathname: pathFor(collection, id),\n body: JSON.stringify({ value, version: expectedVersion + 1 }),\n overwrite: true,\n // The write API's ifMatch rejects the weak form (W/\"…\") that reads return.\n ifMatch: existing.etag.replace(/^W\\//u, \"\"),\n });\n });\n } catch (error) {\n if (isPreconditionError(error)) {\n if (attempts > 1 && (await wroteThisUpdate(collection, id, value, expectedVersion + 1))) {\n return;\n }\n throw new VersionConflictError(collection, id, expectedVersion);\n }\n throw error;\n }\n },\n\n async list(collection) {\n const active = activeLists.get(collection);\n if (active !== undefined) return active;\n const listing = (async () => {\n const dir = `${prefix}/${collection}/`;\n const paths = await client.listPaths({ prefix: dir });\n const entries: (StoredEntry | null)[] = new Array(paths.length);\n let next = 0;\n let failure: unknown;\n const workers = Array.from(\n { length: Math.min(readConcurrency, paths.length) },\n async () => {\n while (next < paths.length && failure === undefined) {\n const index = next++;\n const pathname = paths[index]!;\n const id = pathname.slice(dir.length).replace(/\\.json$/u, \"\");\n try {\n const record = await readRecord(collection, id);\n entries[index] =\n record === null\n ? null\n : { id, record: { value: record.value, version: record.version } };\n } catch (error) {\n failure ??= error;\n }\n }\n },\n );\n await Promise.all(workers);\n if (failure !== undefined) throw failure;\n return entries.filter((entry): entry is StoredEntry => entry !== null);\n })();\n activeLists.set(collection, listing);\n try {\n return await listing;\n } finally {\n if (activeLists.get(collection) === listing) activeLists.delete(collection);\n }\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ import { FactoryId, TaskId } from "./schema/id.mjs";
2
+ import { Task } from "./schema/task.mjs";
3
+ import { DateClock } from "./schema/common.mjs";
4
+ import { BudgetLimits, BudgetMonth } from "./schema/budget.mjs";
5
+ import { FactoryStores } from "./store/engine.mjs";
6
+ //#region src/budget.d.ts
7
+ /**
8
+ * Stable factory identity for budget rows. Nothing mints factory ids yet, so
9
+ * the app derives one from a deployment-stable seed such as the Vercel
10
+ * project id.
11
+ */
12
+ declare function deriveFactoryId(seed: string): FactoryId;
13
+ /** The budget month ("YYYY-MM", UTC) a timestamp falls into. */
14
+ declare function budgetMonthOf(isoDateTime: string): BudgetMonth;
15
+ /**
16
+ * Monthly spend is derived from task rows, never stored: each task counts its
17
+ * settled cost once that exists, otherwise its held reservation. A task
18
+ * settles once, so a settled row never also counts its reservation.
19
+ * Attribution uses the reservation time so tasks that wait across a month
20
+ * boundary charge the month they actually ran in.
21
+ */
22
+ declare function computeMonthSpendUsd(tasks: readonly Task[], month: BudgetMonth): number;
23
+ /** Derived monthly capacity state before another per-Task reservation. */
24
+ type BudgetStatus = "ok" | "warning" | "exhausted";
25
+ /**
26
+ * True when one more per-task slice fits under the monthly total. Dispatch
27
+ * refuses on this exact predicate and budgetStatus reports "exhausted" on its
28
+ * negation, so refusing and reporting can never disagree.
29
+ */
30
+ declare function canReserve(spendUsd: number, limits: Pick<BudgetLimits, "totalUsd" | "perTaskReservationUsd">): boolean;
31
+ /** Classify monthly spend using the same exact-fit predicate enforced by dispatch. */
32
+ declare function budgetStatus(spendUsd: number, limits: Pick<BudgetLimits, "totalUsd" | "perTaskReservationUsd" | "warningPercent">): BudgetStatus;
33
+ /** Grace period, clock, and optional snapshot for terminal usage settlement. */
34
+ interface SweepSettlementsOptions {
35
+ /** How long a terminal task waits for late usage before the sweep settles it. */
36
+ graceMinutes?: number;
37
+ now?: DateClock;
38
+ /** A dispatcher-wide snapshot avoids another complete task read. */
39
+ tasks?: readonly Task[];
40
+ }
41
+ /** Task-local charges newly settled by one reconciliation pass. */
42
+ interface SweepSettlementsResult {
43
+ readonly settled: readonly {
44
+ readonly taskId: TaskId;
45
+ readonly totalUsd: number;
46
+ }[];
47
+ }
48
+ /** Task capability required to settle terminal reservations. */
49
+ type SettlementSweepStores = Pick<FactoryStores, "tasks">;
50
+ /**
51
+ * Settle reserved terminal tasks the session hook did not: a task that ended
52
+ * after its session (a change verified later) or whose session died. Settles
53
+ * at the recorded usage, or at zero with a warning reason when none was
54
+ * recorded within the grace window.
55
+ */
56
+ declare function sweepSettlements(stores: SettlementSweepStores, options?: SweepSettlementsOptions): Promise<SweepSettlementsResult>;
57
+ /** Owner-only notification emitted when monthly spend crosses a budget threshold. */
58
+ interface BudgetNotice {
59
+ outcome: "budget_warning" | "budget_exhausted";
60
+ month: BudgetMonth;
61
+ spendUsd: number;
62
+ limits: BudgetLimits;
63
+ }
64
+ /** Monthly limits, delivery hook, and optional snapshot used for budget reconciliation. */
65
+ interface BudgetSweepOptions {
66
+ factoryId: FactoryId;
67
+ limits: BudgetLimits;
68
+ /** Defaults to true. False reports spending without threshold checks or budget notices. */
69
+ enforce?: boolean;
70
+ /** Defaults to the current UTC month. */
71
+ month?: BudgetMonth;
72
+ now?: DateClock;
73
+ /** Owner-only delivery; invoked at most once per month per outcome. */
74
+ notify?: (notice: BudgetNotice) => Promise<void>;
75
+ /** A dispatcher-wide snapshot avoids another complete task read. */
76
+ tasks?: readonly Task[];
77
+ }
78
+ /**
79
+ * Derived monthly spend and exact notice outcome produced by budget reconciliation.
80
+ * `stamped` means this sweep recorded the threshold, `already_stamped` means no
81
+ * new stamp was needed, and `delivery_failed` remains eligible for retry.
82
+ */
83
+ type BudgetSweepResult = {
84
+ readonly month: BudgetMonth;
85
+ readonly spendUsd: number;
86
+ readonly status: "disabled";
87
+ readonly noticeState: "not_applicable";
88
+ } | {
89
+ readonly month: BudgetMonth;
90
+ readonly spendUsd: number;
91
+ readonly status: "ok";
92
+ readonly noticeState: "not_applicable";
93
+ } | {
94
+ readonly month: BudgetMonth;
95
+ readonly spendUsd: number;
96
+ readonly status: "warning";
97
+ readonly noticeState: "stamped" | "already_stamped" | "delivery_failed";
98
+ } | {
99
+ readonly month: BudgetMonth;
100
+ readonly spendUsd: number;
101
+ readonly status: "exhausted";
102
+ readonly noticeState: "stamped" | "already_stamped" | "delivery_failed";
103
+ };
104
+ /** Budget and Task capabilities required to reconcile monthly budget thresholds. */
105
+ type BudgetSweepStores = Pick<FactoryStores, "budgets" | "tasks">;
106
+ /**
107
+ * Ensure the month's budget row, derive spend, and record threshold crossings
108
+ * write-once. Delivery comes first and the stamp only lands after it, so a
109
+ * failed notification retries on the next sweep instead of being lost; the
110
+ * write-once stamp still keeps each threshold to one message per month.
111
+ */
112
+ declare function sweepBudget(stores: BudgetSweepStores, options: BudgetSweepOptions): Promise<BudgetSweepResult>;
113
+ //#endregion
114
+ export { BudgetNotice, BudgetStatus, BudgetSweepOptions, BudgetSweepResult, BudgetSweepStores, SettlementSweepStores, SweepSettlementsOptions, SweepSettlementsResult, budgetMonthOf, budgetStatus, canReserve, computeMonthSpendUsd, deriveFactoryId, sweepBudget, sweepSettlements };
115
+ //# sourceMappingURL=budget.d.mts.map
@@ -0,0 +1,147 @@
1
+ import { factoryIdSchema } from "./schema/id.mjs";
2
+ import { budgetMonthSchema } from "./schema/budget.mjs";
3
+ import { createHash } from "node:crypto";
4
+ //#region src/budget.ts
5
+ /**
6
+ * Stable factory identity for budget rows. Nothing mints factory ids yet, so
7
+ * the app derives one from a deployment-stable seed such as the Vercel
8
+ * project id.
9
+ */
10
+ function deriveFactoryId(seed) {
11
+ return factoryIdSchema.parse(`fac_${createHash("sha256").update(seed).digest("hex").slice(0, 32)}`);
12
+ }
13
+ /** The budget month ("YYYY-MM", UTC) a timestamp falls into. */
14
+ function budgetMonthOf(isoDateTime) {
15
+ return budgetMonthSchema.parse(isoDateTime.slice(0, 7));
16
+ }
17
+ function microsOf(usd) {
18
+ return Math.round(usd * 1e6);
19
+ }
20
+ /**
21
+ * Monthly spend is derived from task rows, never stored: each task counts its
22
+ * settled cost once that exists, otherwise its held reservation. A task
23
+ * settles once, so a settled row never also counts its reservation.
24
+ * Attribution uses the reservation time so tasks that wait across a month
25
+ * boundary charge the month they actually ran in.
26
+ */
27
+ function computeMonthSpendUsd(tasks, month) {
28
+ let spendMicros = 0;
29
+ for (const task of tasks) {
30
+ if (budgetMonthOf(task.reservedAt ?? task.createdAt) !== month) continue;
31
+ spendMicros += microsOf(task.settledUsd ?? task.reservedUsd ?? 0);
32
+ }
33
+ return spendMicros / 1e6;
34
+ }
35
+ /**
36
+ * True when one more per-task slice fits under the monthly total. Dispatch
37
+ * refuses on this exact predicate and budgetStatus reports "exhausted" on its
38
+ * negation, so refusing and reporting can never disagree.
39
+ */
40
+ function canReserve(spendUsd, limits) {
41
+ return microsOf(spendUsd) + microsOf(limits.perTaskReservationUsd) <= microsOf(limits.totalUsd);
42
+ }
43
+ /** Classify monthly spend using the same exact-fit predicate enforced by dispatch. */
44
+ function budgetStatus(spendUsd, limits) {
45
+ if (!canReserve(spendUsd, limits)) return "exhausted";
46
+ if (microsOf(spendUsd) * 100 >= microsOf(limits.totalUsd) * limits.warningPercent) return "warning";
47
+ return "ok";
48
+ }
49
+ const TERMINAL_TASK_STATES = /* @__PURE__ */ new Set([
50
+ "succeeded",
51
+ "failed",
52
+ "cancelled"
53
+ ]);
54
+ /**
55
+ * Settle reserved terminal tasks the session hook did not: a task that ended
56
+ * after its session (a change verified later) or whose session died. Settles
57
+ * at the recorded usage, or at zero with a warning reason when none was
58
+ * recorded within the grace window.
59
+ */
60
+ async function sweepSettlements(stores, options = {}) {
61
+ const graceMs = (options.graceMinutes ?? 10) * 6e4;
62
+ const nowMs = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
63
+ const settled = [];
64
+ for (const task of options.tasks ?? await stores.tasks.list()) {
65
+ if (task.reservedUsd === void 0 || task.settledUsd !== void 0) continue;
66
+ if (!TERMINAL_TASK_STATES.has(task.state)) continue;
67
+ if (nowMs - Date.parse(task.updatedAt) < graceMs) continue;
68
+ const totalUsd = task.usage?.costUsd ?? 0;
69
+ try {
70
+ const result = await stores.tasks.settle(task.id, {
71
+ cost: { totalUsd },
72
+ reason: task.usage === void 0 ? "usage missing after ten minutes; settled at zero" : "measured usage settled by sweep"
73
+ });
74
+ settled.push({
75
+ taskId: result.id,
76
+ totalUsd
77
+ });
78
+ } catch {
79
+ continue;
80
+ }
81
+ }
82
+ return { settled };
83
+ }
84
+ /**
85
+ * Ensure the month's budget row, derive spend, and record threshold crossings
86
+ * write-once. Delivery comes first and the stamp only lands after it, so a
87
+ * failed notification retries on the next sweep instead of being lost; the
88
+ * write-once stamp still keeps each threshold to one message per month.
89
+ */
90
+ async function sweepBudget(stores, options) {
91
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
92
+ const month = options.month ?? budgetMonthOf(now().toISOString());
93
+ const budget = await stores.budgets.ensure({
94
+ factoryId: options.factoryId,
95
+ month,
96
+ limits: options.limits
97
+ });
98
+ const spendUsd = computeMonthSpendUsd(options.tasks ?? await stores.tasks.list(), month);
99
+ if (options.enforce === false) return {
100
+ month,
101
+ spendUsd,
102
+ status: "disabled",
103
+ noticeState: "not_applicable"
104
+ };
105
+ const status = budgetStatus(spendUsd, options.limits);
106
+ async function crossThreshold(notice, outcome) {
107
+ const field = notice === "warning" ? "warningAt" : "exhaustedAt";
108
+ if (budget.notices[field] !== void 0) return "already_stamped";
109
+ if (options.notify !== void 0) try {
110
+ await options.notify({
111
+ outcome,
112
+ month,
113
+ spendUsd,
114
+ limits: options.limits
115
+ });
116
+ } catch {
117
+ return "delivery_failed";
118
+ }
119
+ return (await stores.budgets.stampNotice(month, notice)).stamped ? "stamped" : "already_stamped";
120
+ }
121
+ if (status === "exhausted") {
122
+ const noticeState = await crossThreshold("exhausted", "budget_exhausted");
123
+ if (budget.notices.warningAt === void 0) await stores.budgets.stampNotice(month, "warning");
124
+ return {
125
+ month,
126
+ spendUsd,
127
+ status,
128
+ noticeState
129
+ };
130
+ }
131
+ if (status === "warning") return {
132
+ month,
133
+ spendUsd,
134
+ status,
135
+ noticeState: await crossThreshold("warning", "budget_warning")
136
+ };
137
+ return {
138
+ month,
139
+ spendUsd,
140
+ status,
141
+ noticeState: "not_applicable"
142
+ };
143
+ }
144
+ //#endregion
145
+ export { budgetMonthOf, budgetStatus, canReserve, computeMonthSpendUsd, deriveFactoryId, sweepBudget, sweepSettlements };
146
+
147
+ //# sourceMappingURL=budget.mjs.map