@storyshelf/core 0.7.0 → 0.8.0

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 (531) hide show
  1. package/dist/adapter/capture-queue.d.mts +0 -2
  2. package/dist/adapter/capture-runner.d.mts +9 -4
  3. package/dist/adapter/capture-runner.mjs +6 -2
  4. package/dist/adapter/database.d.mts +63 -3
  5. package/dist/adapter/database.mjs +4 -3
  6. package/dist/adapter/database.mjs.map +1 -1
  7. package/dist/adapter/design.d.mts +0 -2
  8. package/dist/adapter/design.mjs +1 -2
  9. package/dist/adapter/email-sender.d.mts +0 -2
  10. package/dist/adapter/metadata.d.mts +0 -2
  11. package/dist/adapter/oauth.d.mts +0 -2
  12. package/dist/adapter/setup.d.mts +0 -2
  13. package/dist/adapter/setup.mjs +1 -2
  14. package/dist/adapter/storage.d.mts +1 -3
  15. package/dist/adapter/webhook-events.d.mts +0 -2
  16. package/dist/adapter/webhook-events.mjs +12 -13
  17. package/dist/adapter/webhook-events.mjs.map +1 -1
  18. package/dist/adapters/capture-target-offers.d.mts +0 -2
  19. package/dist/adapters/capture-target-offers.mjs +1 -2
  20. package/dist/adapters/capture-target.d.mts +0 -2
  21. package/dist/adapters/capture-target.mjs +1 -2
  22. package/dist/adapters/notifier/build-comment.d.mts +0 -2
  23. package/dist/adapters/notifier/build-comment.mjs +1 -2
  24. package/dist/adapters/notifier/build-state.d.mts +0 -2
  25. package/dist/adapters/notifier/build-state.mjs +1 -2
  26. package/dist/adapters/notifier/events.d.mts +0 -2
  27. package/dist/adapters/notifier/events.mjs +1 -2
  28. package/dist/adapters/notifier/format.d.mts +0 -2
  29. package/dist/adapters/notifier/format.mjs +1 -2
  30. package/dist/adapters/notifier/provider.d.mts +0 -2
  31. package/dist/adapters/notifier/provider.mjs +1 -2
  32. package/dist/adapters/notifier/state.d.mts +0 -2
  33. package/dist/adapters/notifier/state.mjs +1 -2
  34. package/dist/adapters/notifier/types.d.mts +0 -2
  35. package/dist/adapters/remote-browser.d.mts +112 -31
  36. package/dist/adapters/remote-browser.mjs +132 -34
  37. package/dist/adapters/remote-browser.mjs.map +1 -1
  38. package/dist/adapters/remote-lease-attempt.mjs +39 -0
  39. package/dist/adapters/remote-lease-attempt.mjs.map +1 -0
  40. package/dist/adapters/remote-lease-error.d.mts +25 -0
  41. package/dist/adapters/remote-lease-error.mjs +54 -0
  42. package/dist/adapters/remote-lease-error.mjs.map +1 -0
  43. package/dist/adapters/remote-lease-held.d.mts +15 -0
  44. package/dist/adapters/remote-lease-held.mjs +86 -0
  45. package/dist/adapters/remote-lease-held.mjs.map +1 -0
  46. package/dist/adapters/remote-lease-shutdown.d.mts +12 -0
  47. package/dist/adapters/remote-lease-shutdown.mjs +32 -0
  48. package/dist/adapters/remote-lease-shutdown.mjs.map +1 -0
  49. package/dist/adapters/remote-lease.d.mts +39 -0
  50. package/dist/adapters/remote-lease.mjs +175 -0
  51. package/dist/adapters/remote-lease.mjs.map +1 -0
  52. package/dist/adapters/remote-limiter.mjs +121 -0
  53. package/dist/adapters/remote-limiter.mjs.map +1 -0
  54. package/dist/adapters/remote-pool.d.mts +56 -0
  55. package/dist/adapters/remote-pool.mjs +195 -0
  56. package/dist/adapters/remote-pool.mjs.map +1 -0
  57. package/dist/adapters/render-runs.d.mts +0 -2
  58. package/dist/adapters/render-runs.mjs +1 -2
  59. package/dist/adapters/setup-runner.d.mts +0 -2
  60. package/dist/adapters/setup-runner.mjs +1 -2
  61. package/dist/adapters/sources.d.mts +0 -2
  62. package/dist/adapters/sources.mjs +1 -2
  63. package/dist/adapters/validate.d.mts +0 -2
  64. package/dist/adapters/validate.mjs +2 -2
  65. package/dist/adapters/validate.mjs.map +1 -1
  66. package/dist/ai.d.mts +0 -2
  67. package/dist/ai.mjs +1 -2
  68. package/dist/auth.d.mts +0 -2
  69. package/dist/auth.mjs +1 -2
  70. package/dist/brand-packs.d.mts +0 -2
  71. package/dist/brand-packs.mjs +0 -1
  72. package/dist/capture/a11y/axe-source.d.mts +0 -2
  73. package/dist/capture/a11y/axe-source.mjs +1 -2
  74. package/dist/capture/a11y/in-page.d.mts +1 -3
  75. package/dist/capture/a11y/in-page.mjs +3 -4
  76. package/dist/capture/a11y/in-page.mjs.map +1 -1
  77. package/dist/capture/a11y/map.d.mts +0 -2
  78. package/dist/capture/a11y/map.mjs +1 -2
  79. package/dist/capture/a11y/options.d.mts +0 -2
  80. package/dist/capture/a11y/options.mjs +1 -2
  81. package/dist/capture/adapter.d.mts +5 -2
  82. package/dist/capture/adapter.mjs +1 -2
  83. package/dist/capture/adapter.mjs.map +1 -1
  84. package/dist/capture/affected.d.mts +0 -2
  85. package/dist/capture/affected.mjs +1 -2
  86. package/dist/capture/attempt-logger.mjs +1 -2
  87. package/dist/capture/browsers.d.mts +0 -2
  88. package/dist/capture/browsers.mjs +1 -2
  89. package/dist/capture/build-state/deliver.d.mts +0 -2
  90. package/dist/capture/build-state/deliver.mjs +1 -2
  91. package/dist/capture/build-state/notify-job.d.mts +0 -2
  92. package/dist/capture/build-state/notify-job.mjs +1 -2
  93. package/dist/capture/build-state/publisher.d.mts +0 -2
  94. package/dist/capture/build-state/publisher.mjs +1 -2
  95. package/dist/capture/build-state/resume.d.mts +0 -2
  96. package/dist/capture/build-state/resume.mjs +1 -2
  97. package/dist/capture/build-state/types.d.mts +1 -3
  98. package/dist/capture/catalog/catalogue.d.mts +0 -2
  99. package/dist/capture/catalog/catalogue.mjs +1 -2
  100. package/dist/capture/catalog/components.d.mts +0 -2
  101. package/dist/capture/catalog/components.mjs +1 -2
  102. package/dist/capture/catalog/history.d.mts +0 -2
  103. package/dist/capture/catalog/history.mjs +1 -2
  104. package/dist/capture/catalog/hook.d.mts +0 -2
  105. package/dist/capture/catalog/hook.mjs +1 -2
  106. package/dist/capture/catalog/index.d.mts +2 -2
  107. package/dist/capture/catalog/load.d.mts +0 -2
  108. package/dist/capture/catalog/load.mjs +1 -2
  109. package/dist/capture/catalog/parse.d.mts +0 -2
  110. package/dist/capture/catalog/parse.mjs +1 -2
  111. package/dist/capture/catalog/read.d.mts +6 -3
  112. package/dist/capture/catalog/read.mjs +14 -8
  113. package/dist/capture/catalog/read.mjs.map +1 -1
  114. package/dist/capture/catalog/types.d.mts +0 -2
  115. package/dist/capture/comparison-reference.d.mts +27 -0
  116. package/dist/capture/comparison-reference.mjs +75 -0
  117. package/dist/capture/comparison-reference.mjs.map +1 -0
  118. package/dist/capture/consumer/capture-handler.d.mts +1 -3
  119. package/dist/capture/consumer/capture-handler.mjs +1 -2
  120. package/dist/capture/consumer/idle.d.mts +0 -2
  121. package/dist/capture/consumer/idle.mjs +1 -2
  122. package/dist/capture/consumer/index.d.mts +0 -2
  123. package/dist/capture/consumer/index.mjs +1 -2
  124. package/dist/capture/consumer/loop.mjs +1 -2
  125. package/dist/capture/consumer/metrics.d.mts +0 -2
  126. package/dist/capture/consumer/metrics.mjs +1 -2
  127. package/dist/capture/consumer/retry.d.mts +0 -2
  128. package/dist/capture/consumer/retry.mjs +1 -2
  129. package/dist/capture/consumer/run-job.mjs +1 -2
  130. package/dist/capture/consumer/startup.mjs +1 -2
  131. package/dist/capture/consumer/types.d.mts +0 -2
  132. package/dist/capture/crash-failure.d.mts +0 -2
  133. package/dist/capture/crash-failure.mjs +1 -2
  134. package/dist/capture/db-queue/cadence.d.mts +0 -2
  135. package/dist/capture/db-queue/cadence.mjs +1 -2
  136. package/dist/capture/db-queue/claim.d.mts +0 -2
  137. package/dist/capture/db-queue/claim.mjs +20 -4
  138. package/dist/capture/db-queue/claim.mjs.map +1 -1
  139. package/dist/capture/db-queue/clock.mjs +1 -2
  140. package/dist/capture/db-queue/codec.mjs +1 -2
  141. package/dist/capture/db-queue/index.d.mts +0 -2
  142. package/dist/capture/db-queue/index.mjs +3 -3
  143. package/dist/capture/db-queue/index.mjs.map +1 -1
  144. package/dist/capture/db-queue/keeper.d.mts +0 -2
  145. package/dist/capture/db-queue/keeper.mjs +1 -2
  146. package/dist/capture/db-queue/lease.mjs +1 -2
  147. package/dist/capture/db-queue/lifecycle.mjs +1 -2
  148. package/dist/capture/db-queue/operations.mjs +1 -2
  149. package/dist/capture/db-queue/poll-loop.d.mts +0 -2
  150. package/dist/capture/db-queue/poll-loop.mjs +1 -2
  151. package/dist/capture/db-queue/poll.d.mts +0 -2
  152. package/dist/capture/db-queue/poll.mjs +1 -2
  153. package/dist/capture/db-queue/timers.d.mts +0 -2
  154. package/dist/capture/db-queue/timers.mjs +1 -2
  155. package/dist/capture/db-queue/types.d.mts +0 -2
  156. package/dist/capture/db-queue/types.mjs +1 -2
  157. package/dist/capture/db-queue/types.mjs.map +1 -1
  158. package/dist/capture/db-queue/wake.mjs +1 -2
  159. package/dist/capture/design.mjs +1 -2
  160. package/dist/capture/diff-record.mjs +1 -2
  161. package/dist/capture/dispatch.d.mts +1 -3
  162. package/dist/capture/dispatch.mjs +17 -5
  163. package/dist/capture/dispatch.mjs.map +1 -1
  164. package/dist/capture/enrich.mjs +1 -2
  165. package/dist/capture/failure-sets.mjs +1 -2
  166. package/dist/capture/fingerprint.d.mts +0 -2
  167. package/dist/capture/fingerprint.mjs +1 -2
  168. package/dist/capture/forked/channel.d.mts +0 -2
  169. package/dist/capture/forked/child.d.mts +0 -2
  170. package/dist/capture/forked/child.mjs +1 -2
  171. package/dist/capture/forked/crash-tracker.mjs +1 -2
  172. package/dist/capture/forked/decision.d.mts +0 -2
  173. package/dist/capture/forked/decision.mjs +1 -2
  174. package/dist/capture/forked/exit.mjs +1 -2
  175. package/dist/capture/forked/fork.d.mts +0 -2
  176. package/dist/capture/forked/fork.mjs +1 -2
  177. package/dist/capture/forked/forked-capture.d.mts +0 -2
  178. package/dist/capture/forked/forked-capture.mjs +1 -2
  179. package/dist/capture/forked/lease-book.mjs +1 -2
  180. package/dist/capture/forked/protocol.d.mts +0 -2
  181. package/dist/capture/forked/protocol.mjs +1 -2
  182. package/dist/capture/forked/runs.mjs +1 -2
  183. package/dist/capture/guards.mjs +1 -2
  184. package/dist/capture/ignore-regions.d.mts +36 -0
  185. package/dist/capture/ignore-regions.mjs +65 -0
  186. package/dist/capture/ignore-regions.mjs.map +1 -0
  187. package/dist/capture/isolate.mjs +1 -2
  188. package/dist/capture/job-lease.d.mts +0 -2
  189. package/dist/capture/job-lease.mjs +1 -2
  190. package/dist/capture/matrix.mjs +4 -4
  191. package/dist/capture/matrix.mjs.map +1 -1
  192. package/dist/capture/modes.d.mts +0 -2
  193. package/dist/capture/modes.mjs +2 -3
  194. package/dist/capture/modes.mjs.map +1 -1
  195. package/dist/capture/orchestrator.d.mts +0 -2
  196. package/dist/capture/orchestrator.mjs +2 -3
  197. package/dist/capture/pipeline.d.mts +0 -2
  198. package/dist/capture/pipeline.mjs +37 -6
  199. package/dist/capture/pipeline.mjs.map +1 -1
  200. package/dist/capture/progress.d.mts +0 -2
  201. package/dist/capture/progress.mjs +1 -2
  202. package/dist/capture/publish/accept.d.mts +0 -2
  203. package/dist/capture/publish/accept.mjs +1 -2
  204. package/dist/capture/publish/complete.d.mts +1 -3
  205. package/dist/capture/publish/complete.mjs +1 -2
  206. package/dist/capture/publish/extract.d.mts +0 -2
  207. package/dist/capture/publish/extract.mjs +6 -5
  208. package/dist/capture/publish/extract.mjs.map +1 -1
  209. package/dist/capture/publish/handler.d.mts +1 -3
  210. package/dist/capture/publish/handler.mjs +1 -2
  211. package/dist/capture/publish/index.d.mts +2 -1
  212. package/dist/capture/publish/invalid-zip.d.mts +8 -0
  213. package/dist/capture/publish/invalid-zip.mjs +11 -0
  214. package/dist/capture/publish/invalid-zip.mjs.map +1 -0
  215. package/dist/capture/publish/limits.d.mts +0 -2
  216. package/dist/capture/publish/limits.mjs +1 -2
  217. package/dist/capture/publish/published.d.mts +0 -2
  218. package/dist/capture/publish/published.mjs +1 -2
  219. package/dist/capture/publish/run.d.mts +1 -3
  220. package/dist/capture/publish/run.mjs +1 -2
  221. package/dist/capture/rediff.d.mts +1 -3
  222. package/dist/capture/rediff.mjs +19 -6
  223. package/dist/capture/rediff.mjs.map +1 -1
  224. package/dist/capture/render.mjs +1 -2
  225. package/dist/capture/render.mjs.map +1 -1
  226. package/dist/capture/scratch.d.mts +0 -2
  227. package/dist/capture/scratch.mjs +2 -4
  228. package/dist/capture/scratch.mjs.map +1 -1
  229. package/dist/capture/shard-plan.mjs +1 -2
  230. package/dist/capture/shards/context.mjs +3 -3
  231. package/dist/capture/shards/context.mjs.map +1 -1
  232. package/dist/capture/shards/execute.mjs +4 -3
  233. package/dist/capture/shards/execute.mjs.map +1 -1
  234. package/dist/capture/shards/fan-out.mjs +1 -2
  235. package/dist/capture/shards/history.mjs +1 -2
  236. package/dist/capture/shards/reduce.mjs +1 -2
  237. package/dist/capture/sizing.d.mts +0 -2
  238. package/dist/capture/sizing.mjs +1 -2
  239. package/dist/capture/skip-checks.mjs +1 -2
  240. package/dist/capture/statics.d.mts +0 -2
  241. package/dist/capture/statics.mjs +2 -4
  242. package/dist/capture/statics.mjs.map +1 -1
  243. package/dist/capture/storybook-urls.d.mts +0 -2
  244. package/dist/capture/storybook-urls.mjs +3 -3
  245. package/dist/capture/storybook-urls.mjs.map +1 -1
  246. package/dist/capture/storybook.d.mts +0 -2
  247. package/dist/capture/storybook.mjs +8 -4
  248. package/dist/capture/storybook.mjs.map +1 -1
  249. package/dist/capture/target.mjs +1 -2
  250. package/dist/capture/targets.d.mts +0 -2
  251. package/dist/capture/targets.mjs +1 -2
  252. package/dist/capture/tests.d.mts +0 -2
  253. package/dist/capture/tests.mjs +13 -3
  254. package/dist/capture/tests.mjs.map +1 -1
  255. package/dist/capture/token-chunks.mjs +24 -7
  256. package/dist/capture/token-chunks.mjs.map +1 -1
  257. package/dist/capture.d.mts +5 -2
  258. package/dist/capture.mjs +5 -2
  259. package/dist/config.d.mts +16 -3
  260. package/dist/config.mjs +6 -3
  261. package/dist/config.mjs.map +1 -1
  262. package/dist/diff.d.mts +0 -2
  263. package/dist/diff.mjs +1 -2
  264. package/dist/index.d.mts +2 -2
  265. package/dist/index.mjs +2 -2
  266. package/dist/insights/budget.d.mts +0 -2
  267. package/dist/insights/budget.mjs +1 -2
  268. package/dist/insights/envelope.d.mts +0 -2
  269. package/dist/insights/envelope.mjs +1 -2
  270. package/dist/insights/evidence-health.d.mts +0 -2
  271. package/dist/insights/evidence-health.mjs +1 -2
  272. package/dist/insights/evidence.d.mts +0 -2
  273. package/dist/insights/evidence.mjs +1 -2
  274. package/dist/insights/hash.d.mts +0 -2
  275. package/dist/insights/hash.mjs +1 -2
  276. package/dist/insights/limits.d.mts +0 -2
  277. package/dist/insights/limits.mjs +1 -2
  278. package/dist/insights/prompts/v1.d.mts +0 -2
  279. package/dist/insights/prompts/v1.mjs +1 -2
  280. package/dist/insights/redact.d.mts +0 -2
  281. package/dist/insights/redact.mjs +1 -2
  282. package/dist/insights/text.mjs +1 -2
  283. package/dist/logger.d.mts +0 -2
  284. package/dist/logger.mjs +1 -2
  285. package/dist/models/ai-budget-alert.d.mts +0 -2
  286. package/dist/models/ai-budget-alert.mjs +1 -2
  287. package/dist/models/ai-usage.d.mts +0 -2
  288. package/dist/models/ai-usage.mjs +1 -2
  289. package/dist/models/baseline-approval.d.mts +18 -0
  290. package/dist/models/baseline-approval.mjs +41 -0
  291. package/dist/models/baseline-approval.mjs.map +1 -0
  292. package/dist/models/baseline-archive.d.mts +0 -2
  293. package/dist/models/baseline-archive.mjs +1 -2
  294. package/dist/models/baseline-atomic.mjs +66 -0
  295. package/dist/models/baseline-atomic.mjs.map +1 -0
  296. package/dist/models/baseline-conflict.d.mts +6 -0
  297. package/dist/models/baseline-conflict.mjs +9 -0
  298. package/dist/models/baseline-conflict.mjs.map +1 -0
  299. package/dist/models/baseline-export.d.mts +0 -2
  300. package/dist/models/baseline-export.mjs +1 -2
  301. package/dist/models/baseline-import.mjs +1 -2
  302. package/dist/models/baseline-origin.d.mts +16 -0
  303. package/dist/models/baseline-origin.mjs +35 -0
  304. package/dist/models/baseline-origin.mjs.map +1 -0
  305. package/dist/models/baseline-status.d.mts +0 -2
  306. package/dist/models/baseline-status.mjs +1 -2
  307. package/dist/models/baseline-transfer.d.mts +1 -2
  308. package/dist/models/baseline-transfer.mjs +30 -2
  309. package/dist/models/baseline-transfer.mjs.map +1 -1
  310. package/dist/models/baseline.d.mts +5 -2
  311. package/dist/models/baseline.mjs +91 -12
  312. package/dist/models/baseline.mjs.map +1 -1
  313. package/dist/models/build-component.d.mts +0 -2
  314. package/dist/models/build-component.mjs +1 -2
  315. package/dist/models/build-review-status.d.mts +0 -2
  316. package/dist/models/build-review-status.mjs +1 -2
  317. package/dist/models/build-review.d.mts +0 -2
  318. package/dist/models/build-review.mjs +1 -2
  319. package/dist/models/build.d.mts +1 -3
  320. package/dist/models/build.mjs +14 -15
  321. package/dist/models/build.mjs.map +1 -1
  322. package/dist/models/capture-attempt.d.mts +0 -2
  323. package/dist/models/capture-attempt.mjs +1 -2
  324. package/dist/models/capture-job.d.mts +0 -2
  325. package/dist/models/capture-job.mjs +1 -2
  326. package/dist/models/capture-log.d.mts +0 -2
  327. package/dist/models/capture-log.mjs +1 -2
  328. package/dist/models/capture-shard.d.mts +0 -2
  329. package/dist/models/capture-shard.mjs +1 -2
  330. package/dist/models/changeset.d.mts +0 -2
  331. package/dist/models/changeset.mjs +1 -2
  332. package/dist/models/columns.mjs +1 -2
  333. package/dist/models/comment.d.mts +0 -2
  334. package/dist/models/comment.mjs +1 -2
  335. package/dist/models/component.d.mts +0 -2
  336. package/dist/models/component.mjs +1 -2
  337. package/dist/models/connections.d.mts +0 -2
  338. package/dist/models/connections.mjs +1 -2
  339. package/dist/models/content-ref.d.mts +0 -2
  340. package/dist/models/content-ref.mjs +1 -2
  341. package/dist/models/credentials.d.mts +0 -2
  342. package/dist/models/credentials.mjs +1 -2
  343. package/dist/models/design-credentials.d.mts +0 -2
  344. package/dist/models/design-credentials.mjs +1 -2
  345. package/dist/models/design-health.d.mts +0 -2
  346. package/dist/models/design-health.mjs +1 -2
  347. package/dist/models/design-nodes.d.mts +2 -4
  348. package/dist/models/design-nodes.mjs +1 -2
  349. package/dist/models/design-renders.mjs +1 -2
  350. package/dist/models/design-resolve.d.mts +0 -2
  351. package/dist/models/design-resolve.mjs +1 -2
  352. package/dist/models/design.d.mts +0 -2
  353. package/dist/models/design.mjs +1 -2
  354. package/dist/models/inherited-ratio.d.mts +0 -2
  355. package/dist/models/inherited-ratio.mjs +1 -2
  356. package/dist/models/insight.d.mts +0 -2
  357. package/dist/models/insight.mjs +1 -2
  358. package/dist/models/label.d.mts +0 -2
  359. package/dist/models/label.mjs +1 -2
  360. package/dist/models/member.d.mts +0 -2
  361. package/dist/models/member.mjs +1 -2
  362. package/dist/models/notification-channel.d.mts +0 -2
  363. package/dist/models/notification-channel.mjs +1 -2
  364. package/dist/models/notification-subscription.d.mts +0 -2
  365. package/dist/models/notification-subscription.mjs +1 -2
  366. package/dist/models/project-group-mapping.d.mts +0 -2
  367. package/dist/models/project-group-mapping.mjs +1 -2
  368. package/dist/models/project.d.mts +0 -2
  369. package/dist/models/project.mjs +1 -2
  370. package/dist/models/public-origin.d.mts +0 -2
  371. package/dist/models/public-origin.mjs +1 -2
  372. package/dist/models/review-switch-off.d.mts +0 -2
  373. package/dist/models/review-switch-off.mjs +1 -2
  374. package/dist/models/site-settings.d.mts +0 -2
  375. package/dist/models/site-settings.mjs +1 -2
  376. package/dist/models/snapshot.d.mts +3 -2
  377. package/dist/models/snapshot.mjs +4 -2
  378. package/dist/models/snapshot.mjs.map +1 -1
  379. package/dist/models/token.d.mts +0 -2
  380. package/dist/models/token.mjs +1 -2
  381. package/dist/models/usage.d.mts +0 -2
  382. package/dist/models/usage.mjs +1 -2
  383. package/dist/models/user.d.mts +0 -2
  384. package/dist/models/user.mjs +1 -2
  385. package/dist/models/webhook.d.mts +0 -2
  386. package/dist/models/webhook.mjs +1 -2
  387. package/dist/models.d.mts +5 -2
  388. package/dist/models.mjs +5 -2
  389. package/dist/options.d.mts +0 -2
  390. package/dist/options.mjs +2 -4
  391. package/dist/options.mjs.map +1 -1
  392. package/dist/paths.d.mts +3 -3
  393. package/dist/paths.mjs +7 -3
  394. package/dist/paths.mjs.map +1 -1
  395. package/dist/retention/ai.d.mts +1 -3
  396. package/dist/retention/ai.mjs +0 -1
  397. package/dist/retention/pending.mjs +1 -2
  398. package/dist/retention/published-gzip-sweep.mjs +28 -0
  399. package/dist/retention/published-gzip-sweep.mjs.map +1 -0
  400. package/dist/retention/screenshot-sweep.d.mts +0 -2
  401. package/dist/retention/screenshot-sweep.mjs +1 -2
  402. package/dist/retention.d.mts +0 -2
  403. package/dist/retention.mjs +5 -2
  404. package/dist/retention.mjs.map +1 -1
  405. package/dist/schema/baseline.d.mts +2 -2
  406. package/dist/schema/build-component.d.mts +0 -2
  407. package/dist/schema/build-review.d.mts +0 -2
  408. package/dist/schema/build.d.mts +0 -2
  409. package/dist/schema/capture-attempt.d.mts +0 -2
  410. package/dist/schema/capture-job.d.mts +0 -2
  411. package/dist/schema/capture-shard.d.mts +0 -2
  412. package/dist/schema/comment.d.mts +0 -2
  413. package/dist/schema/connection.d.mts +0 -2
  414. package/dist/schema/content-ref.d.mts +0 -2
  415. package/dist/schema/design.d.mts +0 -2
  416. package/dist/schema/insight.d.mts +0 -2
  417. package/dist/schema/label.d.mts +0 -2
  418. package/dist/schema/member.d.mts +0 -2
  419. package/dist/schema/notification.d.mts +0 -2
  420. package/dist/schema/project-group-mapping.d.mts +0 -2
  421. package/dist/schema/project.d.mts +0 -2
  422. package/dist/schema/site-setting.d.mts +0 -2
  423. package/dist/schema/snapshot.d.mts +5 -2
  424. package/dist/schema/token.d.mts +0 -2
  425. package/dist/schema/usage.d.mts +0 -2
  426. package/dist/schema/user.d.mts +0 -2
  427. package/dist/schema/webhook.d.mts +0 -2
  428. package/dist/schema.d.mts +4 -6
  429. package/dist/serverless.d.mts +0 -2
  430. package/dist/serverless.mjs +1 -2
  431. package/dist/test-helpers/build-reference-contract.d.mts +14 -0
  432. package/dist/test-helpers/build-reference-contract.mjs +66 -0
  433. package/dist/test-helpers/build-reference-contract.mjs.map +1 -0
  434. package/dist/test-helpers/create-project.d.mts +0 -2
  435. package/dist/test-helpers/create-project.mjs +1 -2
  436. package/dist/test-helpers/database-contract.d.mts +0 -2
  437. package/dist/test-helpers/database-contract.mjs +37 -2
  438. package/dist/test-helpers/database-contract.mjs.map +1 -1
  439. package/dist/test-helpers/deferred.d.mts +0 -2
  440. package/dist/test-helpers/deferred.mjs +1 -2
  441. package/dist/test-helpers/design-contract.d.mts +0 -2
  442. package/dist/test-helpers/design-contract.mjs +1 -2
  443. package/dist/test-helpers/fake-adapters.d.mts +0 -2
  444. package/dist/test-helpers/fake-adapters.mjs +1 -2
  445. package/dist/test-helpers/fake-database.d.mts +0 -2
  446. package/dist/test-helpers/fake-database.mjs +7 -2
  447. package/dist/test-helpers/fake-database.mjs.map +1 -1
  448. package/dist/test-helpers/fake-design.d.mts +0 -2
  449. package/dist/test-helpers/fake-design.mjs +1 -2
  450. package/dist/test-helpers/fake-notifier.d.mts +0 -2
  451. package/dist/test-helpers/fake-notifier.mjs +1 -2
  452. package/dist/test-helpers/fake-storage.d.mts +0 -2
  453. package/dist/test-helpers/fake-storage.mjs +1 -2
  454. package/dist/test-helpers/fake-tables.mjs +5 -2
  455. package/dist/test-helpers/fake-tables.mjs.map +1 -1
  456. package/dist/test-helpers/manual-clock.d.mts +0 -2
  457. package/dist/test-helpers/manual-clock.mjs +1 -2
  458. package/dist/test-helpers/notifier-contract.d.mts +0 -2
  459. package/dist/test-helpers/notifier-contract.mjs +1 -2
  460. package/dist/test-helpers/queue-contract.d.mts +0 -2
  461. package/dist/test-helpers/queue-contract.mjs +1 -2
  462. package/dist/test-helpers/queue-multi-instance.d.mts +0 -2
  463. package/dist/test-helpers/queue-multi-instance.mjs +1 -2
  464. package/dist/test-helpers/runner-contract.d.mts +0 -2
  465. package/dist/test-helpers/runner-contract.mjs +1 -2
  466. package/dist/test-helpers/sql-chunks.mjs +2 -2
  467. package/dist/test-helpers/sql-chunks.mjs.map +1 -1
  468. package/dist/test-helpers/storage-contract.d.mts +0 -2
  469. package/dist/test-helpers/storage-contract.mjs +1 -2
  470. package/dist/test-helpers.d.mts +2 -1
  471. package/dist/test-helpers.mjs +2 -1
  472. package/dist/tracing.mjs +36 -6
  473. package/dist/tracing.mjs.map +1 -1
  474. package/dist/types.d.mts +0 -2
  475. package/dist/types.mjs +1 -2
  476. package/dist/urls.d.mts +0 -2
  477. package/dist/urls.mjs +1 -2
  478. package/dist/utils/adaptive-concurrency.d.mts +0 -2
  479. package/dist/utils/adaptive-concurrency.mjs +1 -2
  480. package/dist/utils/connect-retry.d.mts +2 -2
  481. package/dist/utils/connect-retry.mjs +2 -3
  482. package/dist/utils/connect-retry.mjs.map +1 -1
  483. package/dist/utils/encrypt.d.mts +0 -2
  484. package/dist/utils/encrypt.mjs +1 -2
  485. package/dist/utils/environment.d.mts +0 -2
  486. package/dist/utils/environment.mjs +1 -2
  487. package/dist/utils/hash.d.mts +0 -2
  488. package/dist/utils/hash.mjs +1 -2
  489. package/dist/utils/http.d.mts +13 -3
  490. package/dist/utils/http.mjs +79 -17
  491. package/dist/utils/http.mjs.map +1 -1
  492. package/dist/utils/map-adaptive.d.mts +0 -2
  493. package/dist/utils/map-adaptive.mjs +1 -2
  494. package/dist/utils/map-limit.d.mts +0 -2
  495. package/dist/utils/map-limit.mjs +1 -2
  496. package/dist/utils/mime.d.mts +0 -2
  497. package/dist/utils/mime.mjs +1 -2
  498. package/dist/utils/page-limiter.d.mts +0 -2
  499. package/dist/utils/page-limiter.mjs +1 -2
  500. package/dist/utils/path-security.d.mts +0 -2
  501. package/dist/utils/path-security.mjs +2 -4
  502. package/dist/utils/path-security.mjs.map +1 -1
  503. package/dist/utils/public-origin.d.mts +0 -2
  504. package/dist/utils/public-origin.mjs +1 -2
  505. package/dist/utils/redact.d.mts +0 -2
  506. package/dist/utils/redact.mjs +1 -2
  507. package/dist/utils/remote-environment.d.mts +40 -3
  508. package/dist/utils/remote-environment.mjs +52 -3
  509. package/dist/utils/remote-environment.mjs.map +1 -1
  510. package/dist/utils/resources.d.mts +0 -2
  511. package/dist/utils/resources.mjs +1 -2
  512. package/dist/utils/screenshot-store.d.mts +0 -2
  513. package/dist/utils/screenshot-store.mjs +1 -2
  514. package/dist/utils/session-gate.d.mts +7 -6
  515. package/dist/utils/session-gate.mjs +28 -12
  516. package/dist/utils/session-gate.mjs.map +1 -1
  517. package/dist/utils/storybook-token.d.mts +11 -3
  518. package/dist/utils/storybook-token.mjs +10 -9
  519. package/dist/utils/storybook-token.mjs.map +1 -1
  520. package/dist/utils/storybook-url.d.mts +0 -2
  521. package/dist/utils/storybook-url.mjs +1 -2
  522. package/dist/utils/timing.d.mts +0 -2
  523. package/dist/utils/timing.mjs +1 -2
  524. package/dist/utils/ulid.d.mts +0 -2
  525. package/dist/utils/ulid.mjs +1 -2
  526. package/dist/utils/zip-read.mjs +1 -2
  527. package/dist/utils/zip-write.d.mts +0 -2
  528. package/dist/utils/zip-write.mjs +1 -2
  529. package/dist/utils.d.mts +5 -5
  530. package/dist/utils.mjs +5 -5
  531. package/package.json +6 -6
@@ -2,7 +2,6 @@ import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { Logger } from "../logger.mjs";
4
4
  import { Buffer } from "node:buffer";
5
- //#region src/utils/http.d.ts
6
5
  /**
7
6
  * Fetch JSON with timeout, retries, and structured errors.
8
7
  *
@@ -32,6 +31,18 @@ declare function httpBytes(url: string, options?: HttpRequestOptions): Promise<B
32
31
  * @returns The raw response body text.
33
32
  */
34
33
  declare function httpText(url: string, options?: HttpTextOptions): Promise<string>;
34
+ /**
35
+ * Send a request and resolve with the response status, without ever reading the response body (it
36
+ * is cancelled on every response). Use it when the answer is only "did it succeed", such as webhook
37
+ * delivery, so a receiver cannot make the process buffer an arbitrarily large body. Retries,
38
+ * timeouts, redaction and tracing are those of {@link httpText}. A non-2xx response throws
39
+ * {@link HttpError} with an empty `body`.
40
+ *
41
+ * @param url - Absolute request URL.
42
+ * @param options - Method, headers, JSON body, timeout, retries, logger.
43
+ * @returns The 2xx response status.
44
+ */
45
+ declare function httpStatus(url: string, options?: HttpTextOptions): Promise<number>;
35
46
  /** Failed HTTP response: status code plus a snippet of the response body. */
36
47
  declare class HttpError extends Error {
37
48
  readonly status: number;
@@ -69,6 +80,5 @@ interface HttpTextOptions {
69
80
  signal?: AbortSignal;
70
81
  logger?: Logger;
71
82
  }
72
- //#endregion
73
- export { HttpError, HttpRequestOptions, HttpTextOptions, httpBytes, httpJson, httpText };
83
+ export { HttpError, HttpRequestOptions, HttpTextOptions, httpBytes, httpJson, httpStatus, httpText };
74
84
  //# sourceMappingURL=http.d.mts.map
@@ -1,9 +1,9 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { injectTraceContext, withSpan } from "../tracing.mjs";
4
+ import { injectTraceContext, setSpanMessage, spanException, withSpan } from "../tracing.mjs";
5
5
  import { Buffer } from "node:buffer";
6
- //#region src/utils/http.ts
6
+ // src/utils/http.ts
7
7
  /**
8
8
  * Fetch JSON with timeout, retries, and structured errors.
9
9
  *
@@ -39,6 +39,20 @@ async function httpBytes(url, options = {}) {
39
39
  async function httpText(url, options = {}) {
40
40
  return await requestWithRetry(url, options, options.retries ?? DEFAULT_RETRIES, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, doFetchText);
41
41
  }
42
+ /**
43
+ * Send a request and resolve with the response status, without ever reading the response body (it
44
+ * is cancelled on every response). Use it when the answer is only "did it succeed", such as webhook
45
+ * delivery, so a receiver cannot make the process buffer an arbitrarily large body. Retries,
46
+ * timeouts, redaction and tracing are those of {@link httpText}. A non-2xx response throws
47
+ * {@link HttpError} with an empty `body`.
48
+ *
49
+ * @param url - Absolute request URL.
50
+ * @param options - Method, headers, JSON body, timeout, retries, logger.
51
+ * @returns The 2xx response status.
52
+ */
53
+ async function httpStatus(url, options = {}) {
54
+ return await requestWithRetry(url, options, options.retries ?? DEFAULT_RETRIES, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, doFetchStatus);
55
+ }
42
56
  /** Failed HTTP response: status code plus a snippet of the response body. */
43
57
  var HttpError = class extends Error {
44
58
  status;
@@ -91,25 +105,54 @@ async function requestWithRetry(url, options, maxAttempts, timeoutMs, perform) {
91
105
  for (let attempt = 1;; attempt += 1) try {
92
106
  return await perform(url, options, timeoutMs);
93
107
  } catch (error) {
94
- span.recordException(error instanceof Error ? error : String(error));
108
+ describeFailure(span, error, url);
95
109
  if (options.signal?.aborted || !shouldRetry(error, attempt, maxAttempts)) throw error;
110
+ span.recordException(spanException(error));
96
111
  span.setAttribute("http.retry.count", attempt);
97
112
  await backoff(error, attempt, url, options.logger);
98
113
  }
99
114
  }, {
100
115
  "http.request.method": options.method ?? "GET",
101
- "url.full": redactUrl(url)
116
+ "url.full": redactUrlForLog(url)
102
117
  });
103
118
  }
104
- /** Strip the query string (tokens, signatures) from a URL for span attributes. */
105
- function redactUrl(url) {
106
- const query = url.indexOf("?");
107
- return query === -1 ? url : url.slice(0, query);
119
+ /**
120
+ * The form of a request URL that may appear in logs and span attributes: its origin
121
+ * (`protocol//host`) and nothing else. The path of an incoming webhook (Slack, Teams) is the secret,
122
+ * a bot token can be the first segment (Telegram), and other URLs carry credentials in the userinfo
123
+ * or the query, so the path, query, fragment and userinfo are dropped by construction. There is no
124
+ * opt-out.
125
+ */
126
+ function redactUrlForLog(url) {
127
+ try {
128
+ const { protocol, host } = new URL(url);
129
+ return `${protocol}//${host}`;
130
+ } catch {
131
+ return "[invalid url]";
132
+ }
133
+ }
134
+ /**
135
+ * What a span may say about a failed request. An HTTP failure carries its status and never the
136
+ * response body, which can echo the request URL in any encoding. Any other failure keeps its
137
+ * message with the request URL replaced by its origin, because native `fetch` repeats the whole
138
+ * URL, userinfo included, when it refuses one. The error a caller catches is not changed.
139
+ */
140
+ function describeFailure(span, error, url) {
141
+ if (error instanceof HttpError) {
142
+ span.setAttribute("http.response.status_code", error.status);
143
+ setSpanMessage(error, `HTTP ${String(error.status)}`);
144
+ } else if (error instanceof Error) setSpanMessage(error, scrubUrl(error.message, url));
145
+ }
146
+ /** `text` with the request URL, as given and as normalized, replaced by its origin. */
147
+ function scrubUrl(text, url) {
148
+ const origin = redactUrlForLog(url);
149
+ const normalized = URL.canParse(url) ? new URL(url).href : url;
150
+ return text.replaceAll(url, origin).replaceAll(normalized, origin);
108
151
  }
109
152
  async function backoff(error, attempt, url, logger) {
110
153
  const delay = retryDelayMs(error, attempt);
111
154
  logger?.warn({
112
- url,
155
+ url: redactUrlForLog(url),
113
156
  attempt,
114
157
  delayMs: delay
115
158
  }, "http request failed, retrying");
@@ -137,15 +180,20 @@ async function doFetchJson(url, options, timeoutMs) {
137
180
  async function doFetchText(url, options, timeoutMs) {
138
181
  return await (await doFetchResponse(url, options, timeoutMs)).text();
139
182
  }
183
+ async function doFetchStatus(url, options, timeoutMs) {
184
+ const response = await doFetchResponse(url, options, timeoutMs, false);
185
+ await discardBody(response);
186
+ return response.status;
187
+ }
140
188
  async function doFetchBytes(url, options, timeoutMs) {
141
189
  const response = await doFetchResponse(url, options, timeoutMs);
142
190
  return Buffer.from(await response.arrayBuffer());
143
191
  }
144
- async function doFetchResponse(url, options, timeoutMs) {
192
+ async function doFetchResponse(url, options, timeoutMs, readErrorBody = true) {
145
193
  const request = buildRequest(options);
146
194
  injectTraceContext(request.headers);
147
195
  options.logger?.debug({
148
- url,
196
+ url: redactUrlForLog(url),
149
197
  method: request.method
150
198
  }, "http request");
151
199
  const response = await fetch(url, {
@@ -154,13 +202,27 @@ async function doFetchResponse(url, options, timeoutMs) {
154
202
  body: request.body,
155
203
  signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs)
156
204
  });
157
- if (!response.ok) {
158
- const text = await response.text();
159
- throw new HttpError(response.status, text.slice(0, MAX_BODY_SNIPPET), response.headers);
160
- }
205
+ if (!response.ok) throw await failureOf(response, readErrorBody);
161
206
  return response;
162
207
  }
163
- //#endregion
164
- export { HttpError, httpBytes, httpJson, httpText };
208
+ /**
209
+ * Cancel an unread body. Cleanup only: a rejected cancel must never replace the request's outcome,
210
+ * or a delivered request would look failed and be retried.
211
+ */
212
+ async function discardBody(response) {
213
+ try {
214
+ await response.body?.cancel();
215
+ } catch {}
216
+ }
217
+ /** The {@link HttpError} for a non-2xx response; the body is read (and capped) only on request. */
218
+ async function failureOf(response, readBody) {
219
+ if (!readBody) {
220
+ await discardBody(response);
221
+ return new HttpError(response.status, "", response.headers);
222
+ }
223
+ const text = await response.text();
224
+ return new HttpError(response.status, text.slice(0, MAX_BODY_SNIPPET), response.headers);
225
+ }
226
+ export { HttpError, httpBytes, httpJson, httpStatus, httpText, redactUrlForLog };
165
227
 
166
228
  //# sourceMappingURL=http.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"http.mjs","names":[],"sources":["../../src/utils/http.ts"],"sourcesContent":["import { Buffer } from \"node:buffer\";\nimport type { Logger } from \"../logger.ts\";\nimport { injectTraceContext, withSpan } from \"../tracing.ts\";\n\n/**\n * Fetch JSON with timeout, retries, and structured errors.\n *\n * Retries network failures, 429, and 502/503/504 (honoring `Retry-After`);\n * every other status throws {@link HttpError} immediately.\n *\n * @param url - Absolute request URL.\n * @param options - Method, headers, JSON body, timeout, retries, logger.\n * @returns The parsed JSON response body.\n */\nexport async function httpJson<T>(url: string, options: HttpRequestOptions = {}): Promise<T> {\n return await requestWithRetry<T>(\n url,\n options,\n options.retries ?? DEFAULT_RETRIES,\n options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n doFetchJson,\n );\n}\n\n/**\n * Download a binary response (images) with the same timeout, retry, and\n * `Retry-After` semantics as {@link httpJson}.\n *\n * @param url - Absolute request URL.\n * @param options - Method, headers, timeout, retries, logger.\n * @returns The raw response bytes.\n */\nexport async function httpBytes(url: string, options: HttpRequestOptions = {}): Promise<Buffer> {\n return await requestWithRetry<Buffer>(\n url,\n options,\n options.retries ?? DEFAULT_RETRIES,\n options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n doFetchBytes,\n );\n}\n\n/**\n * POST/PUT plain payloads (e.g. Slack/Teams webhook cards) with the same\n * timeout, retry, and `Retry-After` semantics as {@link httpJson}.\n *\n * @param url - Absolute request URL.\n * @param options - Method, headers, text body, timeout, retries, logger.\n * @returns The raw response body text.\n */\nexport async function httpText(url: string, options: HttpTextOptions = {}): Promise<string> {\n return await requestWithRetry<string>(\n url,\n options,\n options.retries ?? DEFAULT_RETRIES,\n options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n doFetchText,\n );\n}\n\n/** Failed HTTP response: status code plus a snippet of the response body. */\nexport class HttpError extends Error {\n readonly status: number;\n readonly body: string;\n readonly headers: Headers;\n\n constructor(status: number, body: string, headers?: Headers) {\n super(`HTTP ${status}: ${body}`);\n this.name = \"HttpError\";\n this.status = status;\n this.body = body;\n this.headers = headers ?? new Headers();\n }\n}\n\n/** Options for {@link httpJson}. */\nexport interface HttpRequestOptions {\n method?: string;\n headers?: Record<string, string>;\n /** Body serialized as JSON with a `content-type` header. */\n json?: unknown;\n /** Body serialized as `application/x-www-form-urlencoded` (ignored when `json` is set). */\n form?: Record<string, string>;\n /** Per-attempt timeout in milliseconds (default 30s). */\n timeoutMs?: number;\n /** Total attempts including the first (default 3). */\n retries?: number;\n /** Aborts the request (and any further retry) when it fires. */\n signal?: AbortSignal;\n logger?: Logger;\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/** Options for {@link httpText}. */\nexport interface HttpTextOptions {\n method?: string;\n headers?: Record<string, string>;\n /** Body serialized as JSON with a `content-type` header. */\n json?: unknown;\n /** Per-attempt timeout in milliseconds (default 30s). */\n timeoutMs?: number;\n /** Total attempts including the first (default 3). */\n retries?: number;\n /** Aborts the request (and any further retry) when it fires. */\n signal?: AbortSignal;\n logger?: Logger;\n}\nconst DEFAULT_RETRIES = 3;\nconst MAX_RETRY_AFTER_MS = 30_000;\nconst BACKOFF_BASE_MS = 1000;\nconst MAX_BODY_SNIPPET = 500;\n\n/** Whether a failure is worth retrying: network errors, 429, and 502/503/504. */\nfunction shouldRetry(error: unknown, attempt: number, maxAttempts: number): boolean {\n if (attempt >= maxAttempts) {\n return false;\n }\n if (!(error instanceof HttpError)) {\n return true;\n }\n return error.status === 429 || (error.status >= 502 && error.status <= 504);\n}\n\n/** Parse a Retry-After value (seconds or HTTP-date) into milliseconds. */\nfunction parseRetryAfter(value: string | null): number | null {\n if (!value) {\n return null;\n }\n const seconds = Number(value);\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.min(seconds * 1000, MAX_RETRY_AFTER_MS);\n }\n const date = Date.parse(value);\n if (!Number.isNaN(date)) {\n return Math.min(Math.max(date - Date.now(), 0), MAX_RETRY_AFTER_MS);\n }\n return null;\n}\n\n/** Delay before the next attempt: Retry-After when present, else 1s/2s/4s. */\nfunction retryDelayMs(error: unknown, attempt: number): number {\n if (error instanceof HttpError) {\n const retryAfter = parseRetryAfter(error.headers.get(\"retry-after\"));\n if (retryAfter !== null) {\n return retryAfter;\n }\n }\n return Math.min(BACKOFF_BASE_MS * 2 ** (attempt - 1), MAX_RETRY_AFTER_MS);\n}\n\n/** Sleep helper for retry backoff. */\nasync function sleep(ms: number): Promise<void> {\n await new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\n/* oxlint-disable eslint/no-await-in-loop -- retry attempts run sequentially by design */\nasync function requestWithRetry<T>(\n url: string,\n options: HttpRequestOptions,\n maxAttempts: number,\n timeoutMs: number,\n perform: (url: string, options: HttpRequestOptions, timeoutMs: number) => Promise<T>,\n): Promise<T> {\n return await withSpan(\n \"http.client\",\n async (span) => {\n for (let attempt = 1; ; attempt += 1) {\n try {\n return await perform(url, options, timeoutMs);\n } catch (error) {\n span.recordException(error instanceof Error ? error : String(error));\n if (options.signal?.aborted || !shouldRetry(error, attempt, maxAttempts)) {\n throw error;\n }\n span.setAttribute(\"http.retry.count\", attempt);\n await backoff(error, attempt, url, options.logger);\n }\n }\n },\n { \"http.request.method\": options.method ?? \"GET\", \"url.full\": redactUrl(url) },\n );\n}\n/* oxlint-enable eslint/no-await-in-loop */\n\n/** Strip the query string (tokens, signatures) from a URL for span attributes. */\nfunction redactUrl(url: string): string {\n const query = url.indexOf(\"?\");\n return query === -1 ? url : url.slice(0, query);\n}\n\nasync function backoff(\n error: unknown,\n attempt: number,\n url: string,\n logger?: Logger,\n): Promise<void> {\n const delay = retryDelayMs(error, attempt);\n logger?.warn({ url, attempt, delayMs: delay }, \"http request failed, retrying\");\n await sleep(delay);\n}\n\nfunction buildRequest(options: HttpRequestOptions): {\n headers: Record<string, string>;\n body: string | undefined;\n method: string;\n} {\n const headers: Record<string, string> = { ...options.headers };\n let body: string | undefined;\n if (options.json !== undefined) {\n headers[\"content-type\"] = \"application/json\";\n body = JSON.stringify(options.json);\n } else if (options.form) {\n headers[\"content-type\"] = \"application/x-www-form-urlencoded\";\n body = new URLSearchParams(options.form).toString();\n }\n return { headers, body, method: options.method ?? \"GET\" };\n}\n\nasync function doFetchJson<T>(\n url: string,\n options: HttpRequestOptions,\n timeoutMs: number,\n): Promise<T> {\n const response = await doFetchResponse(url, options, timeoutMs);\n return (await response.json()) as T;\n}\n\nasync function doFetchText(\n url: string,\n options: HttpRequestOptions,\n timeoutMs: number,\n): Promise<string> {\n const response = await doFetchResponse(url, options, timeoutMs);\n return await response.text();\n}\n\nasync function doFetchBytes(\n url: string,\n options: HttpRequestOptions,\n timeoutMs: number,\n): Promise<Buffer> {\n const response = await doFetchResponse(url, options, timeoutMs);\n return Buffer.from(await response.arrayBuffer());\n}\n\nasync function doFetchResponse(\n url: string,\n options: HttpRequestOptions,\n timeoutMs: number,\n): Promise<Response> {\n const request = buildRequest(options);\n injectTraceContext(request.headers);\n options.logger?.debug({ url, method: request.method }, \"http request\");\n const response = await fetch(url, {\n method: request.method,\n headers: request.headers,\n body: request.body,\n signal: options.signal\n ? AbortSignal.any([options.signal, AbortSignal.timeout(timeoutMs)])\n : AbortSignal.timeout(timeoutMs),\n });\n if (!response.ok) {\n const text = await response.text();\n throw new HttpError(response.status, text.slice(0, MAX_BODY_SNIPPET), response.headers);\n }\n return response;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"http.mjs","names":[],"sources":["../../src/utils/http.ts"],"sourcesContent":["import type { Span } from \"@opentelemetry/api\";\nimport { Buffer } from \"node:buffer\";\nimport type { Logger } from \"../logger.ts\";\nimport { injectTraceContext, setSpanMessage, spanException, withSpan } from \"../tracing.ts\";\n\n/**\n * Fetch JSON with timeout, retries, and structured errors.\n *\n * Retries network failures, 429, and 502/503/504 (honoring `Retry-After`);\n * every other status throws {@link HttpError} immediately.\n *\n * @param url - Absolute request URL.\n * @param options - Method, headers, JSON body, timeout, retries, logger.\n * @returns The parsed JSON response body.\n */\nexport async function httpJson<T>(url: string, options: HttpRequestOptions = {}): Promise<T> {\n return await requestWithRetry<T>(\n url,\n options,\n options.retries ?? DEFAULT_RETRIES,\n options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n doFetchJson,\n );\n}\n\n/**\n * Download a binary response (images) with the same timeout, retry, and\n * `Retry-After` semantics as {@link httpJson}.\n *\n * @param url - Absolute request URL.\n * @param options - Method, headers, timeout, retries, logger.\n * @returns The raw response bytes.\n */\nexport async function httpBytes(url: string, options: HttpRequestOptions = {}): Promise<Buffer> {\n return await requestWithRetry<Buffer>(\n url,\n options,\n options.retries ?? DEFAULT_RETRIES,\n options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n doFetchBytes,\n );\n}\n\n/**\n * POST/PUT plain payloads (e.g. Slack/Teams webhook cards) with the same\n * timeout, retry, and `Retry-After` semantics as {@link httpJson}.\n *\n * @param url - Absolute request URL.\n * @param options - Method, headers, text body, timeout, retries, logger.\n * @returns The raw response body text.\n */\nexport async function httpText(url: string, options: HttpTextOptions = {}): Promise<string> {\n return await requestWithRetry<string>(\n url,\n options,\n options.retries ?? DEFAULT_RETRIES,\n options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n doFetchText,\n );\n}\n\n/**\n * Send a request and resolve with the response status, without ever reading the response body (it\n * is cancelled on every response). Use it when the answer is only \"did it succeed\", such as webhook\n * delivery, so a receiver cannot make the process buffer an arbitrarily large body. Retries,\n * timeouts, redaction and tracing are those of {@link httpText}. A non-2xx response throws\n * {@link HttpError} with an empty `body`.\n *\n * @param url - Absolute request URL.\n * @param options - Method, headers, JSON body, timeout, retries, logger.\n * @returns The 2xx response status.\n */\nexport async function httpStatus(url: string, options: HttpTextOptions = {}): Promise<number> {\n return await requestWithRetry<number>(\n url,\n options,\n options.retries ?? DEFAULT_RETRIES,\n options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n doFetchStatus,\n );\n}\n\n/** Failed HTTP response: status code plus a snippet of the response body. */\nexport class HttpError extends Error {\n readonly status: number;\n readonly body: string;\n readonly headers: Headers;\n\n constructor(status: number, body: string, headers?: Headers) {\n super(`HTTP ${status}: ${body}`);\n this.name = \"HttpError\";\n this.status = status;\n this.body = body;\n this.headers = headers ?? new Headers();\n }\n}\n\n/** Options for {@link httpJson}. */\nexport interface HttpRequestOptions {\n method?: string;\n headers?: Record<string, string>;\n /** Body serialized as JSON with a `content-type` header. */\n json?: unknown;\n /** Body serialized as `application/x-www-form-urlencoded` (ignored when `json` is set). */\n form?: Record<string, string>;\n /** Per-attempt timeout in milliseconds (default 30s). */\n timeoutMs?: number;\n /** Total attempts including the first (default 3). */\n retries?: number;\n /** Aborts the request (and any further retry) when it fires. */\n signal?: AbortSignal;\n logger?: Logger;\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/** Options for {@link httpText}. */\nexport interface HttpTextOptions {\n method?: string;\n headers?: Record<string, string>;\n /** Body serialized as JSON with a `content-type` header. */\n json?: unknown;\n /** Per-attempt timeout in milliseconds (default 30s). */\n timeoutMs?: number;\n /** Total attempts including the first (default 3). */\n retries?: number;\n /** Aborts the request (and any further retry) when it fires. */\n signal?: AbortSignal;\n logger?: Logger;\n}\nconst DEFAULT_RETRIES = 3;\nconst MAX_RETRY_AFTER_MS = 30_000;\nconst BACKOFF_BASE_MS = 1000;\nconst MAX_BODY_SNIPPET = 500;\n\n/** Whether a failure is worth retrying: network errors, 429, and 502/503/504. */\nfunction shouldRetry(error: unknown, attempt: number, maxAttempts: number): boolean {\n if (attempt >= maxAttempts) {\n return false;\n }\n if (!(error instanceof HttpError)) {\n return true;\n }\n return error.status === 429 || (error.status >= 502 && error.status <= 504);\n}\n\n/** Parse a Retry-After value (seconds or HTTP-date) into milliseconds. */\nfunction parseRetryAfter(value: string | null): number | null {\n if (!value) {\n return null;\n }\n const seconds = Number(value);\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.min(seconds * 1000, MAX_RETRY_AFTER_MS);\n }\n const date = Date.parse(value);\n if (!Number.isNaN(date)) {\n return Math.min(Math.max(date - Date.now(), 0), MAX_RETRY_AFTER_MS);\n }\n return null;\n}\n\n/** Delay before the next attempt: Retry-After when present, else 1s/2s/4s. */\nfunction retryDelayMs(error: unknown, attempt: number): number {\n if (error instanceof HttpError) {\n const retryAfter = parseRetryAfter(error.headers.get(\"retry-after\"));\n if (retryAfter !== null) {\n return retryAfter;\n }\n }\n return Math.min(BACKOFF_BASE_MS * 2 ** (attempt - 1), MAX_RETRY_AFTER_MS);\n}\n\n/** Sleep helper for retry backoff. */\nasync function sleep(ms: number): Promise<void> {\n await new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\n/* oxlint-disable eslint/no-await-in-loop -- retry attempts run sequentially by design */\nasync function requestWithRetry<T>(\n url: string,\n options: HttpRequestOptions,\n maxAttempts: number,\n timeoutMs: number,\n perform: (url: string, options: HttpRequestOptions, timeoutMs: number) => Promise<T>,\n): Promise<T> {\n return await withSpan(\n \"http.client\",\n async (span) => {\n for (let attempt = 1; ; attempt += 1) {\n try {\n return await perform(url, options, timeoutMs);\n } catch (error) {\n describeFailure(span, error, url);\n if (options.signal?.aborted || !shouldRetry(error, attempt, maxAttempts)) {\n // The final failure is recorded once, by `withSpan`, in the same redacted form.\n throw error;\n }\n span.recordException(spanException(error));\n span.setAttribute(\"http.retry.count\", attempt);\n await backoff(error, attempt, url, options.logger);\n }\n }\n },\n { \"http.request.method\": options.method ?? \"GET\", \"url.full\": redactUrlForLog(url) },\n );\n}\n/* oxlint-enable eslint/no-await-in-loop */\n\n/**\n * The form of a request URL that may appear in logs and span attributes: its origin\n * (`protocol//host`) and nothing else. The path of an incoming webhook (Slack, Teams) is the secret,\n * a bot token can be the first segment (Telegram), and other URLs carry credentials in the userinfo\n * or the query, so the path, query, fragment and userinfo are dropped by construction. There is no\n * opt-out.\n */\nexport function redactUrlForLog(url: string): string {\n try {\n const { protocol, host } = new URL(url);\n return `${protocol}//${host}`;\n } catch {\n return \"[invalid url]\";\n }\n}\n\n/**\n * What a span may say about a failed request. An HTTP failure carries its status and never the\n * response body, which can echo the request URL in any encoding. Any other failure keeps its\n * message with the request URL replaced by its origin, because native `fetch` repeats the whole\n * URL, userinfo included, when it refuses one. The error a caller catches is not changed.\n */\nfunction describeFailure(span: Span, error: unknown, url: string): void {\n if (error instanceof HttpError) {\n span.setAttribute(\"http.response.status_code\", error.status);\n setSpanMessage(error, `HTTP ${String(error.status)}`);\n } else if (error instanceof Error) {\n setSpanMessage(error, scrubUrl(error.message, url));\n }\n}\n\n/** `text` with the request URL, as given and as normalized, replaced by its origin. */\nfunction scrubUrl(text: string, url: string): string {\n const origin = redactUrlForLog(url);\n const normalized = URL.canParse(url) ? new URL(url).href : url;\n return text.replaceAll(url, origin).replaceAll(normalized, origin);\n}\n\nasync function backoff(\n error: unknown,\n attempt: number,\n url: string,\n logger?: Logger,\n): Promise<void> {\n const delay = retryDelayMs(error, attempt);\n logger?.warn(\n { url: redactUrlForLog(url), attempt, delayMs: delay },\n \"http request failed, retrying\",\n );\n await sleep(delay);\n}\n\nfunction buildRequest(options: HttpRequestOptions): {\n headers: Record<string, string>;\n body: string | undefined;\n method: string;\n} {\n const headers: Record<string, string> = { ...options.headers };\n let body: string | undefined;\n if (options.json !== undefined) {\n headers[\"content-type\"] = \"application/json\";\n body = JSON.stringify(options.json);\n } else if (options.form) {\n headers[\"content-type\"] = \"application/x-www-form-urlencoded\";\n body = new URLSearchParams(options.form).toString();\n }\n return { headers, body, method: options.method ?? \"GET\" };\n}\n\nasync function doFetchJson<T>(\n url: string,\n options: HttpRequestOptions,\n timeoutMs: number,\n): Promise<T> {\n const response = await doFetchResponse(url, options, timeoutMs);\n return (await response.json()) as T;\n}\n\nasync function doFetchText(\n url: string,\n options: HttpRequestOptions,\n timeoutMs: number,\n): Promise<string> {\n const response = await doFetchResponse(url, options, timeoutMs);\n return await response.text();\n}\n\nasync function doFetchStatus(\n url: string,\n options: HttpRequestOptions,\n timeoutMs: number,\n): Promise<number> {\n const response = await doFetchResponse(url, options, timeoutMs, false);\n await discardBody(response);\n return response.status;\n}\n\nasync function doFetchBytes(\n url: string,\n options: HttpRequestOptions,\n timeoutMs: number,\n): Promise<Buffer> {\n const response = await doFetchResponse(url, options, timeoutMs);\n return Buffer.from(await response.arrayBuffer());\n}\n\nasync function doFetchResponse(\n url: string,\n options: HttpRequestOptions,\n timeoutMs: number,\n readErrorBody = true,\n): Promise<Response> {\n const request = buildRequest(options);\n injectTraceContext(request.headers);\n options.logger?.debug({ url: redactUrlForLog(url), method: request.method }, \"http request\");\n const response = await fetch(url, {\n method: request.method,\n headers: request.headers,\n body: request.body,\n signal: options.signal\n ? AbortSignal.any([options.signal, AbortSignal.timeout(timeoutMs)])\n : AbortSignal.timeout(timeoutMs),\n });\n if (!response.ok) {\n throw await failureOf(response, readErrorBody);\n }\n return response;\n}\n\n/**\n * Cancel an unread body. Cleanup only: a rejected cancel must never replace the request's outcome,\n * or a delivered request would look failed and be retried.\n */\nasync function discardBody(response: Response): Promise<void> {\n try {\n await response.body?.cancel();\n } catch {\n // The status is already known; there is nothing left to do with the body.\n }\n}\n\n/** The {@link HttpError} for a non-2xx response; the body is read (and capped) only on request. */\nasync function failureOf(response: Response, readBody: boolean): Promise<HttpError> {\n if (!readBody) {\n await discardBody(response);\n return new HttpError(response.status, \"\", response.headers);\n }\n const text = await response.text();\n return new HttpError(response.status, text.slice(0, MAX_BODY_SNIPPET), response.headers);\n}\n"],"mappings":"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"}
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { AdaptiveConcurrency } from "./adaptive-concurrency.mjs";
4
- //#region src/utils/map-adaptive.d.ts
5
4
  /**
6
5
  * Like `mapLimit`, but the limit is read live from `controller` and every finished item is fed
7
6
  * back to it. `fn` reports a timeout it handled itself with `timedOut: true`.
@@ -10,6 +9,5 @@ declare function mapAdaptive<T, R>(items: readonly T[], controller: AdaptiveConc
10
9
  value: R;
11
10
  timedOut?: boolean;
12
11
  }>): Promise<R[]>;
13
- //#endregion
14
12
  export { mapAdaptive };
15
13
  //# sourceMappingURL=map-adaptive.d.mts.map
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/utils/map-adaptive.ts
4
+ // src/utils/map-adaptive.ts
5
5
  /**
6
6
  * Like `mapLimit`, but the limit is read live from `controller` and every finished item is fed
7
7
  * back to it. `fn` reports a timeout it handled itself with `timedOut: true`.
@@ -66,7 +66,6 @@ var AdaptiveRun = class {
66
66
  this.pump();
67
67
  }
68
68
  };
69
- //#endregion
70
69
  export { mapAdaptive };
71
70
 
72
71
  //# sourceMappingURL=map-adaptive.mjs.map
@@ -1,10 +1,8 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/utils/map-limit.d.ts
4
3
  /** Split `items` into consecutive batches of at most `size` (a non-positive size means one batch). */
5
4
  declare function chunk<T>(items: readonly T[], size: number): T[][];
6
5
  /** Run `fn` over `items` with at most `limit` in flight; results keep input order. */
7
6
  declare function mapLimit<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
8
- //#endregion
9
7
  export { chunk, mapLimit };
10
8
  //# sourceMappingURL=map-limit.d.mts.map
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/utils/map-limit.ts
4
+ // src/utils/map-limit.ts
5
5
  /** Split `items` into consecutive batches of at most `size` (a non-positive size means one batch). */
6
6
  function chunk(items, size) {
7
7
  const step = size > 0 ? Math.max(1, Math.floor(size)) : items.length || 1;
@@ -26,7 +26,6 @@ async function mapLimit(items, limit, fn) {
26
26
  }));
27
27
  return results;
28
28
  }
29
- //#endregion
30
29
  export { chunk, mapLimit };
31
30
 
32
31
  //# sourceMappingURL=map-limit.mjs.map
@@ -1,9 +1,7 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/utils/mime.d.ts
4
3
  /** Shared MIME types for storybook static serving. */
5
4
  declare const MIME_TYPES: Record<string, string>;
6
5
  declare function mimeFor(filePath: string): string;
7
- //#endregion
8
6
  export { MIME_TYPES, mimeFor };
9
7
  //# sourceMappingURL=mime.d.mts.map
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/utils/mime.ts
4
+ // src/utils/mime.ts
5
5
  /** Shared MIME types for storybook static serving. */
6
6
  const MIME_TYPES = {
7
7
  ".css": "text/css; charset=utf-8",
@@ -27,7 +27,6 @@ function mimeFor(filePath) {
27
27
  const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
28
28
  return MIME_TYPES[ext] ?? "application/octet-stream";
29
29
  }
30
- //#endregion
31
30
  export { MIME_TYPES, mimeFor };
32
31
 
33
32
  //# sourceMappingURL=mime.mjs.map
@@ -2,7 +2,6 @@ import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { Logger } from "../logger.mjs";
4
4
  import { SessionGate } from "./session-gate.mjs";
5
- //#region src/utils/page-limiter.d.ts
6
5
  /** Options for {@link createPageLimiter}. */
7
6
  interface PageLimiterOptions {
8
7
  /** Explicit page count (config or run input). Fixed: the adaptive controller stays off. */
@@ -32,6 +31,5 @@ interface PageLimiter {
32
31
  * recommendation and let latency, event-loop lag and timeouts move it up to the adaptive ceiling.
33
32
  */
34
33
  declare function createPageLimiter(options: PageLimiterOptions): PageLimiter;
35
- //#endregion
36
34
  export { PageLimiter, PageLimiterOptions, createPageLimiter };
37
35
  //# sourceMappingURL=page-limiter.d.mts.map
@@ -5,7 +5,7 @@ import { mapLimit } from "./map-limit.mjs";
5
5
  import { detectResources, maxPageConcurrency, recommendedPageConcurrency } from "./resources.mjs";
6
6
  import { AdaptiveConcurrency } from "./adaptive-concurrency.mjs";
7
7
  import { mapAdaptive } from "./map-adaptive.mjs";
8
- //#region src/utils/page-limiter.ts
8
+ // src/utils/page-limiter.ts
9
9
  /**
10
10
  * The session gate's size for a remote target; a fixed limit when `concurrency` is given; otherwise start at the CPU/memory/engine-sized
11
11
  * recommendation and let latency, event-loop lag and timeouts move it up to the adaptive ceiling.
@@ -50,7 +50,6 @@ function createPageLimiter(options) {
50
50
  };
51
51
  }
52
52
  function noop() {}
53
- //#endregion
54
53
  export { createPageLimiter };
55
54
 
56
55
  //# sourceMappingURL=page-limiter.mjs.map
@@ -1,12 +1,10 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/utils/path-security.d.ts
4
3
  /** Whether a path segment is safe (no traversal). */
5
4
  declare function isSafeSegment(segment: string): boolean;
6
5
  /** Whether joining root with candidate stays inside root (no traversal). */
7
6
  declare function isPathSafe(root: string, candidate: string): boolean;
8
7
  /** Check if a path built from root + entry would escape root (for zip extraction). */
9
8
  declare function isBlockedTarget(root: string, entryPath: string): boolean;
10
- //#endregion
11
9
  export { isBlockedTarget, isPathSafe, isSafeSegment };
12
10
  //# sourceMappingURL=path-security.d.mts.map
@@ -1,8 +1,7 @@
1
- import "node:path";
1
+ import { join, normalize, sep } from "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { join, normalize, sep } from "node:path";
5
- //#region src/utils/path-security.ts
4
+ // src/utils/path-security.ts
6
5
  /** Whether a path segment is safe (no traversal). */
7
6
  function isSafeSegment(segment) {
8
7
  return segment !== ".." && !segment.startsWith("/") && !segment.includes("\\") && !segment.includes("..");
@@ -18,7 +17,6 @@ function isBlockedTarget(root, entryPath) {
18
17
  const target = normalize(join(root, entryPath));
19
18
  return target !== root && !target.startsWith(normalize(root) + sep);
20
19
  }
21
- //#endregion
22
20
  export { isBlockedTarget, isPathSafe, isSafeSegment };
23
21
 
24
22
  //# sourceMappingURL=path-security.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"path-security.mjs","names":[],"sources":["../../src/utils/path-security.ts"],"sourcesContent":["import { join, normalize, sep } from \"node:path\";\n\n/** Whether a path segment is safe (no traversal). */\nexport function isSafeSegment(segment: string): boolean {\n return (\n segment !== \"..\" &&\n !segment.startsWith(\"/\") &&\n !segment.includes(\"\\\\\") &&\n !segment.includes(\"..\")\n );\n}\n\n/** Whether joining root with candidate stays inside root (no traversal). */\nexport function isPathSafe(root: string, candidate: string): boolean {\n const normalizedRoot = normalize(root);\n const normalizedCandidate = normalize(candidate);\n return (\n normalizedCandidate === normalizedRoot ||\n normalizedCandidate.startsWith(normalizedRoot + sep) ||\n normalizedCandidate.startsWith(`${normalizedRoot}/`)\n );\n}\n\n/** Check if a path built from root + entry would escape root (for zip extraction). */\nexport function isBlockedTarget(root: string, entryPath: string): boolean {\n const target = normalize(join(root, entryPath));\n return target !== root && !target.startsWith(normalize(root) + sep);\n}\n"],"mappings":";;;;;;AAGA,SAAgB,cAAc,SAA0B;CACtD,OACE,YAAY,QACZ,CAAC,QAAQ,WAAW,GAAG,KACvB,CAAC,QAAQ,SAAS,IAAI,KACtB,CAAC,QAAQ,SAAS,IAAI;AAE1B;;AAGA,SAAgB,WAAW,MAAc,WAA4B;CACnE,MAAM,iBAAiB,UAAU,IAAI;CACrC,MAAM,sBAAsB,UAAU,SAAS;CAC/C,OACE,wBAAwB,kBACxB,oBAAoB,WAAW,iBAAiB,GAAG,KACnD,oBAAoB,WAAW,GAAG,eAAe,EAAE;AAEvD;;AAGA,SAAgB,gBAAgB,MAAc,WAA4B;CACxE,MAAM,SAAS,UAAU,KAAK,MAAM,SAAS,CAAC;CAC9C,OAAO,WAAW,QAAQ,CAAC,OAAO,WAAW,UAAU,IAAI,IAAI,GAAG;AACpE"}
1
+ {"version":3,"file":"path-security.mjs","names":[],"sources":["../../src/utils/path-security.ts"],"sourcesContent":["import { join, normalize, sep } from \"node:path\";\n\n/** Whether a path segment is safe (no traversal). */\nexport function isSafeSegment(segment: string): boolean {\n return (\n segment !== \"..\" &&\n !segment.startsWith(\"/\") &&\n !segment.includes(\"\\\\\") &&\n !segment.includes(\"..\")\n );\n}\n\n/** Whether joining root with candidate stays inside root (no traversal). */\nexport function isPathSafe(root: string, candidate: string): boolean {\n const normalizedRoot = normalize(root);\n const normalizedCandidate = normalize(candidate);\n return (\n normalizedCandidate === normalizedRoot ||\n normalizedCandidate.startsWith(normalizedRoot + sep) ||\n normalizedCandidate.startsWith(`${normalizedRoot}/`)\n );\n}\n\n/** Check if a path built from root + entry would escape root (for zip extraction). */\nexport function isBlockedTarget(root: string, entryPath: string): boolean {\n const target = normalize(join(root, entryPath));\n return target !== root && !target.startsWith(normalize(root) + sep);\n}\n"],"mappings":";;;;;AAGA,SAAgB,cAAc,SAA0B;CACtD,OACE,YAAY,QACZ,CAAC,QAAQ,WAAW,GAAG,KACvB,CAAC,QAAQ,SAAS,IAAI,KACtB,CAAC,QAAQ,SAAS,IAAI;AAE1B;;AAGA,SAAgB,WAAW,MAAc,WAA4B;CACnE,MAAM,iBAAiB,UAAU,IAAI;CACrC,MAAM,sBAAsB,UAAU,SAAS;CAC/C,OACE,wBAAwB,kBACxB,oBAAoB,WAAW,iBAAiB,GAAG,KACnD,oBAAoB,WAAW,GAAG,eAAe,EAAE;AAEvD;;AAGA,SAAgB,gBAAgB,MAAc,WAA4B;CACxE,MAAM,SAAS,UAAU,KAAK,MAAM,SAAS,CAAC;CAC9C,OAAO,WAAW,QAAQ,CAAC,OAAO,WAAW,UAAU,IAAI,IAAI,GAAG;AACpE"}
@@ -1,6 +1,5 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/utils/public-origin.d.ts
4
3
  /** Finding the address a deployment is reached at without being told (ADR 0007, decision 6). */
5
4
  /** Environment variables, as `process.env` has them. */
6
5
  type Env = Readonly<Record<string, string | undefined>>;
@@ -15,6 +14,5 @@ declare function normalizeOrigin(value?: string): string | undefined;
15
14
  * domain. Cloud Run, AWS App Runner, Lambda function URLs and Cloudflare Workers publish nothing.
16
15
  */
17
16
  declare function detectPublicBaseUrl(env: Env): string | undefined;
18
- //#endregion
19
17
  export { Env, detectPublicBaseUrl, normalizeOrigin };
20
18
  //# sourceMappingURL=public-origin.d.mts.map
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/utils/public-origin.ts
4
+ // src/utils/public-origin.ts
5
5
  /**
6
6
  * The origin (`scheme://host[:port]`) of `value`, or `undefined` when it is not an http(s) URL
7
7
  * without credentials. A bare host (`app.example.com`), as platforms often publish, reads as https.
@@ -69,7 +69,6 @@ function fromContainerApps(env) {
69
69
  const suffix = env["CONTAINER_APP_ENV_DNS_SUFFIX"];
70
70
  return app && suffix ? normalizeOrigin(`${app}.${suffix}`) : void 0;
71
71
  }
72
- //#endregion
73
72
  export { detectPublicBaseUrl, normalizeOrigin };
74
73
 
75
74
  //# sourceMappingURL=public-origin.mjs.map
@@ -1,6 +1,5 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/utils/redact.d.ts
4
3
  /**
5
4
  * Secret redaction for adapter errors, health details, and logs.
6
5
  *
@@ -12,6 +11,5 @@ import __tsdown_shims_url from 'node:url';
12
11
  declare function redactSecrets(text: string): string;
13
12
  /** Redact secrets, then truncate to `maxLength`. */
14
13
  declare function sanitizeErrorText(text: string, maxLength: number): string;
15
- //#endregion
16
14
  export { redactSecrets, sanitizeErrorText };
17
15
  //# sourceMappingURL=redact.d.mts.map
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { STORYBOOK_TOKEN_PREFIX, STORYBOOK_TOKEN_SOURCE } from "./storybook-token.mjs";
5
- //#region src/utils/redact.ts
5
+ // src/utils/redact.ts
6
6
  /**
7
7
  * Secret redaction for adapter errors, health details, and logs.
8
8
  *
@@ -39,7 +39,6 @@ function sanitizeErrorText(text, maxLength) {
39
39
  const redacted = redactSecrets(text);
40
40
  return redacted.length > maxLength ? redacted.slice(0, maxLength) : redacted;
41
41
  }
42
- //#endregion
43
42
  export { redactSecrets, sanitizeErrorText };
44
43
 
45
44
  //# sourceMappingURL=redact.mjs.map
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { CaptureEnvironment } from "../adapter/capture-runner.mjs";
4
- //#region src/utils/remote-environment.d.ts
5
4
  /** What the probe page reports. */
6
5
  interface BrowserProbe {
7
6
  /** OS as the page sees it (`navigator.userAgentData.platform`, else `navigator.platform`). */
@@ -24,6 +23,45 @@ declare const FONT_PROBE_FAMILIES: readonly string[];
24
23
  * for a family that does not exist, so it cannot tell.
25
24
  */
26
25
  declare function inPageProbe(families: string[]): BrowserProbe;
26
+ /** How a {@link boundedProbe} ended. */
27
+ type ProbeOutcome<T> = {
28
+ status: "done";
29
+ value: T;
30
+ } |
31
+ /** Still running after `timeoutMs`: the environment is unknown, the run goes on. */
32
+ {
33
+ status: "timeout";
34
+ } |
35
+ /** The run was cancelled: the run stops, it is never read as "unknown environment". */
36
+ {
37
+ status: "cancelled";
38
+ } |
39
+ /** The probe threw (a service outage, a page that would not open): unknown, with a warning. */
40
+ {
41
+ status: "failed";
42
+ error: unknown;
43
+ };
44
+ /** What a probe's work can ask about its own fate. */
45
+ interface ProbeControl {
46
+ /**
47
+ * True once the probe timed out or was cancelled. Work that opens a page must check it after
48
+ * every await and release what it opened, because nothing waits for it any more.
49
+ */
50
+ stopped: () => boolean;
51
+ }
52
+ /**
53
+ * Run probe work under a time limit and a cancel signal (ADR 0007, decision 2). A probe that hangs
54
+ * on an unresponsive service must not hold a render, and a cancel must interrupt it instead of
55
+ * waiting for the service. The work is never awaited past the limit and never leaves an unhandled
56
+ * rejection; releasing the page it opened is the caller's job (`ProbeControl.stopped`, or closing
57
+ * what it holds once this returns). A failure after the signal fired reads as cancelled, since
58
+ * closing the browser is how a cancel interrupts it.
59
+ */
60
+ declare function boundedProbe<T>(options: {
61
+ run: (control: ProbeControl) => Promise<T>;
62
+ timeoutMs: number;
63
+ signal?: AbortSignal | undefined;
64
+ }): Promise<ProbeOutcome<T>>;
27
65
  /**
28
66
  * The environment of a remote render: the driver's browser version and what the page reported. A
29
67
  * field the page could not give is left out, except the OS, which falls back to what the service
@@ -34,6 +72,5 @@ declare function remoteEnvironment(options: {
34
72
  probe?: BrowserProbe | undefined;
35
73
  declaredPlatform?: string | undefined;
36
74
  }): CaptureEnvironment;
37
- //#endregion
38
- export { BrowserProbe, FONT_PROBE_FAMILIES, inPageProbe, remoteEnvironment };
75
+ export { BrowserProbe, FONT_PROBE_FAMILIES, ProbeControl, ProbeOutcome, boundedProbe, inPageProbe, remoteEnvironment };
39
76
  //# sourceMappingURL=remote-environment.d.mts.map
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { sha256 } from "./hash.mjs";
5
- //#region src/utils/remote-environment.ts
5
+ // src/utils/remote-environment.ts
6
6
  /**
7
7
  * Families probed for the fonts digest: system fonts that differ between operating systems and
8
8
  * image builds, so a provider-side change moves the hash. A fixed list; changing it re-baselines
@@ -85,6 +85,56 @@ function inPageProbe(families) {
85
85
  return probe;
86
86
  }
87
87
  /**
88
+ * Run probe work under a time limit and a cancel signal (ADR 0007, decision 2). A probe that hangs
89
+ * on an unresponsive service must not hold a render, and a cancel must interrupt it instead of
90
+ * waiting for the service. The work is never awaited past the limit and never leaves an unhandled
91
+ * rejection; releasing the page it opened is the caller's job (`ProbeControl.stopped`, or closing
92
+ * what it holds once this returns). A failure after the signal fired reads as cancelled, since
93
+ * closing the browser is how a cancel interrupts it.
94
+ */
95
+ async function boundedProbe(options) {
96
+ const { signal, timeoutMs } = options;
97
+ if (signal?.aborted) return { status: "cancelled" };
98
+ const halt = { halted: false };
99
+ return await settle(options.run({ stopped: () => halt.halted }).then((value) => ({
100
+ status: "done",
101
+ value
102
+ }), (error) => ({
103
+ status: "failed",
104
+ error
105
+ })), limitOf(timeoutMs, signal), signal, halt);
106
+ }
107
+ /** Wait for the work or the limit, mark the work stopped if it lost, and drop the timer and listener. */
108
+ async function settle(work, limit, signal, halt) {
109
+ try {
110
+ const outcome = await Promise.race([work, limit.reached]);
111
+ halt.halted = outcome.status === "timeout" || outcome.status === "cancelled";
112
+ return outcome.status === "failed" && signal?.aborted ? { status: "cancelled" } : outcome;
113
+ } finally {
114
+ limit.clear();
115
+ }
116
+ }
117
+ /** A promise that settles on the timeout or the abort, and a way to drop both. */
118
+ function limitOf(timeoutMs, signal) {
119
+ let timer;
120
+ let onAbort;
121
+ return {
122
+ reached: new Promise((resolve) => {
123
+ timer = setTimeout(() => {
124
+ resolve({ status: "timeout" });
125
+ }, timeoutMs);
126
+ onAbort = () => {
127
+ resolve({ status: "cancelled" });
128
+ };
129
+ signal?.addEventListener("abort", onAbort, { once: true });
130
+ }),
131
+ clear: () => {
132
+ clearTimeout(timer);
133
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
134
+ }
135
+ };
136
+ }
137
+ /**
88
138
  * The environment of a remote render: the driver's browser version and what the page reported. A
89
139
  * field the page could not give is left out, except the OS, which falls back to what the service
90
140
  * was configured to render on; an unknown fingerprint component is never a change (ADR 0005).
@@ -108,7 +158,6 @@ function normalizePlatform(value) {
108
158
  if (text.startsWith("mac") || text === "iphone" || text === "ipad") return "macos";
109
159
  return text.startsWith("linux") ? "linux" : text;
110
160
  }
111
- //#endregion
112
- export { FONT_PROBE_FAMILIES, inPageProbe, remoteEnvironment };
161
+ export { FONT_PROBE_FAMILIES, boundedProbe, inPageProbe, remoteEnvironment };
113
162
 
114
163
  //# sourceMappingURL=remote-environment.mjs.map