@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.50

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 (366) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/README.md +16 -0
  3. package/api/reap.ts +17 -0
  4. package/cli.js +6 -0
  5. package/client/dependency-paths.d.ts +3 -0
  6. package/client/dependency-paths.js +36 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +115 -0
  10. package/common/bundle-commit.d.ts +1 -0
  11. package/common/bundle-commit.js +4 -0
  12. package/common/bundle-compare-diff.d.ts +3 -0
  13. package/common/bundle-compare-diff.js +357 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-compare-versions.js +172 -0
  17. package/common/bundle-comparison.d.ts +8 -0
  18. package/common/bundle-comparison.js +62 -0
  19. package/common/bundle-metadata.d.ts +15 -0
  20. package/common/bundle-metadata.js +226 -0
  21. package/common/bundle-pkg-of.d.ts +1 -0
  22. package/common/bundle-pkg-of.js +90 -0
  23. package/common/bundle-reasons.d.ts +2 -0
  24. package/common/bundle-reasons.js +21 -0
  25. package/common/bundle-source-links.d.ts +6 -0
  26. package/common/bundle-source-links.js +216 -0
  27. package/common/bundle-sourcemap.d.ts +10 -0
  28. package/common/bundle-sourcemap.js +100 -0
  29. package/common/bundle-sources.d.ts +7 -0
  30. package/common/bundle-sources.js +333 -0
  31. package/common/bundle-stats.js +41 -0
  32. package/common/bundle-tabs.js +1 -0
  33. package/common/code-language.js +65 -0
  34. package/common/default-scan-models.ts +31 -0
  35. package/common/dependency-reviews.d.ts +18 -0
  36. package/common/dependency-reviews.js +64 -0
  37. package/common/finding-duplicates.js +58 -0
  38. package/common/finding-filters.js +274 -0
  39. package/common/finding-id.js +47 -0
  40. package/common/github-issue-labels.d.ts +1 -0
  41. package/common/github-issue-labels.js +11 -0
  42. package/common/github-pr.ts +56 -0
  43. package/common/gzip.js +59 -0
  44. package/common/line-diff.d.ts +8 -0
  45. package/common/line-diff.js +57 -0
  46. package/common/linked-workspace-groups.js +63 -0
  47. package/common/managed/agent-tasks.ts +292 -0
  48. package/common/managed/comments.ts +34 -0
  49. package/common/managed/finding-detail.d.ts +2 -0
  50. package/common/managed/finding-detail.js +32 -0
  51. package/common/managed/github-agents.ts +114 -0
  52. package/common/managed/github-logins.ts +49 -0
  53. package/common/managed/npm-packages.d.ts +5 -0
  54. package/common/managed/npm-packages.js +34 -0
  55. package/common/managed/permissions.ts +31 -0
  56. package/common/managed/report-content.ts +72 -0
  57. package/common/managed/report-filter.ts +109 -0
  58. package/common/managed/repository-alias.ts +65 -0
  59. package/common/managed/roles.ts +35 -0
  60. package/common/managed/routes.d.ts +2 -0
  61. package/common/managed/routes.js +347 -0
  62. package/common/managed/scan-models.ts +6 -0
  63. package/common/managed/team-app.d.ts +5 -0
  64. package/common/managed/team-app.js +31 -0
  65. package/common/managed/triage.ts +83 -0
  66. package/common/managed/upload-seal.ts +126 -0
  67. package/common/pretty-print.d.ts +5 -0
  68. package/common/pretty-print.js +24 -0
  69. package/common/revalidation-conflicts.js +48 -0
  70. package/common/revalidation-input-groups.js +56 -0
  71. package/common/save-error-reason.ts +20 -7
  72. package/common/scan-server.ts +13 -0
  73. package/common/security-links.d.ts +5 -0
  74. package/common/security-links.js +33 -0
  75. package/common/server-info.ts +46 -0
  76. package/common/utf8.d.ts +3 -0
  77. package/common/utf8.js +45 -0
  78. package/common/workspace-app.js +39 -0
  79. package/common/workspace-groups.js +176 -0
  80. package/out/activity.js +4 -0
  81. package/out/brotli-fallback.js +3 -3
  82. package/out/chunk-2WDZPXVL.js +3 -0
  83. package/out/chunk-2XUEZHIP.js +28 -0
  84. package/out/chunk-2ZDZWLFY.js +1 -0
  85. package/out/chunk-4HI2MSAV.js +17 -0
  86. package/out/chunk-4XMP7YVH.js +36 -0
  87. package/out/chunk-6LNQTKAV.js +52 -0
  88. package/out/chunk-6S2U2UYG.js +232 -0
  89. package/out/chunk-75YLHBCM.js +3 -0
  90. package/out/chunk-7BKKYVWR.js +7 -0
  91. package/out/chunk-7C7DWL7I.js +1 -0
  92. package/out/chunk-7VAWINQF.js +6 -0
  93. package/out/chunk-C4Z6ZBXV.js +10 -0
  94. package/out/chunk-FC65TPUB.js +1 -0
  95. package/out/chunk-FKBXNUW4.js +1 -0
  96. package/out/chunk-GDPCLSOS.js +2 -0
  97. package/out/chunk-GO7FT4OD.js +1 -0
  98. package/out/chunk-HTBYHSPG.js +1 -0
  99. package/out/chunk-IYPY47MC.js +1 -0
  100. package/out/chunk-IZ7POWI3.js +18 -0
  101. package/out/chunk-JCIIBLKR.js +1 -0
  102. package/out/chunk-NRPC74AM.js +1 -0
  103. package/out/chunk-OOJM4CTU.js +1 -0
  104. package/out/chunk-PBZPYIG7.js +1 -0
  105. package/out/chunk-PNHBZGDD.js +1 -0
  106. package/out/chunk-PWFUOLZK.js +17 -0
  107. package/out/chunk-PXK7EU4V.js +1 -0
  108. package/out/chunk-QB4CBOX2.js +1 -0
  109. package/out/chunk-R3NZFXVR.js +1 -0
  110. package/out/chunk-R5O6FAW4.js +2 -0
  111. package/out/chunk-SXV2T5C4.js +80 -0
  112. package/out/chunk-TQJDYHVE.js +2 -0
  113. package/out/chunk-TUII34K2.js +1 -0
  114. package/out/chunk-UK7RGJT6.js +3 -0
  115. package/out/chunk-VFWMQ3LG.js +2 -0
  116. package/out/chunk-VH4XMZIW.js +2 -0
  117. package/out/chunk-WCPJDAYA.js +10 -0
  118. package/out/chunk-WFYLMF6Z.js +10 -0
  119. package/out/chunk-WYLQWYLA.js +1 -0
  120. package/out/client-managed-import.js +4 -0
  121. package/out/client-managed.js +33 -0
  122. package/out/client-sync.js +11 -17
  123. package/out/compare.js +35 -0
  124. package/out/dialogs.js +55 -0
  125. package/out/github-issue-authorized.html +3 -0
  126. package/out/github-issue-authorized.js +1 -0
  127. package/out/graph.js +17 -4
  128. package/out/index.html +90 -8
  129. package/out/prism.js +14 -10
  130. package/out/scan.js +2 -0
  131. package/out/stasis.svg +20 -0
  132. package/out/terminal.js +366 -39
  133. package/out/view.css +1 -1
  134. package/out/view.js +19 -144
  135. package/package.json +362 -59
  136. package/risk/actions/findings.js +161 -0
  137. package/risk/actions/resolve.js +219 -0
  138. package/risk/actions/scripts/commands.js +182 -0
  139. package/risk/actions/scripts/grade.js +113 -0
  140. package/risk/actions/scripts/packages.js +123 -0
  141. package/risk/actions/scripts/shell.js +169 -0
  142. package/risk/actions/uses.js +76 -0
  143. package/risk/actions/workflow.js +110 -0
  144. package/risk/files/characters.js +196 -0
  145. package/risk/files/hosts.js +205 -0
  146. package/risk/files/readability.js +177 -0
  147. package/risk/files/runs-code.js +615 -0
  148. package/risk/files/script.js +118 -0
  149. package/risk/files/stamps.js +47 -0
  150. package/risk/index.d.ts +254 -0
  151. package/risk/index.js +11 -0
  152. package/risk/npm/assess.js +285 -0
  153. package/risk/npm/combined.js +68 -0
  154. package/risk/npm/compare.js +257 -0
  155. package/risk/npm/dependencies.js +172 -0
  156. package/risk/npm/diff.js +79 -0
  157. package/risk/npm/downloads.js +84 -0
  158. package/risk/npm/files.js +445 -0
  159. package/risk/npm/held.js +63 -0
  160. package/risk/npm/install-scripts.js +34 -0
  161. package/risk/npm/malware.js +36 -0
  162. package/risk/npm/manifest.js +65 -0
  163. package/risk/npm/new-dependency.js +60 -0
  164. package/risk/npm/old-version.js +108 -0
  165. package/risk/npm/registry.js +388 -0
  166. package/risk/npm/release.js +85 -0
  167. package/risk/npm/score.js +59 -0
  168. package/risk/npm/specs.js +124 -0
  169. package/risk/npm/tarball.js +96 -0
  170. package/server-common/database-config.ts +16 -0
  171. package/server-common/initialize.ts +18 -0
  172. package/server-common/npm-advisories.ts +101 -0
  173. package/{server → server-common}/origin.ts +5 -5
  174. package/server-common/reap.ts +48 -0
  175. package/server-common/scan-config.ts +19 -0
  176. package/server-common/standalone.ts +29 -0
  177. package/server-common/storage-crypto.ts +182 -0
  178. package/server-common/storage-log.ts +38 -0
  179. package/server-common/vercel-blob.ts +103 -0
  180. package/server-e2e/app.ts +487 -0
  181. package/{server → server-e2e}/bus-receiver.ts +2 -1
  182. package/{server → server-e2e}/cli.js +9 -4
  183. package/{server → server-e2e}/config.ts +50 -42
  184. package/{server → server-e2e}/db-neon.ts +2 -2
  185. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  186. package/{server → server-e2e}/db-stmt.ts +2 -2
  187. package/{server → server-e2e}/db.ts +7 -7
  188. package/{server → server-e2e}/http.ts +44 -13
  189. package/{server → server-e2e}/hub.ts +40 -9
  190. package/server-e2e/index.ts +17 -0
  191. package/server-e2e/lifecycle.ts +95 -0
  192. package/{server → server-e2e}/neon-driver.ts +2 -2
  193. package/{server → server-e2e}/npm-proxy.ts +12 -145
  194. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  195. package/{server → server-e2e}/objstore/blob.ts +1 -1
  196. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  197. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  198. package/{server → server-e2e}/objstore/init.ts +19 -27
  199. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  200. package/server-e2e/objstore/rest-deny.ts +28 -0
  201. package/server-e2e/objstore/rest-mint.ts +224 -0
  202. package/{server → server-e2e}/objstore/rest.ts +3 -3
  203. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  204. package/{server → server-e2e}/objstore/store.ts +7 -7
  205. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  206. package/{server → server-e2e}/pubsub.ts +12 -9
  207. package/{server → server-e2e}/sign.ts +4 -4
  208. package/{server → server-e2e}/sse-server.ts +60 -33
  209. package/{server → server-e2e}/static.ts +31 -21
  210. package/{server → server-e2e}/sync-handlers.ts +64 -30
  211. package/{server → server-e2e}/ws-server.ts +20 -9
  212. package/server-managed/activity.ts +258 -0
  213. package/server-managed/agent-merges.ts +109 -0
  214. package/server-managed/app-agents.ts +21 -0
  215. package/server-managed/avatar-store.ts +13 -0
  216. package/server-managed/blob-store.ts +17 -0
  217. package/server-managed/brotli.ts +10 -0
  218. package/server-managed/bundle-advisories.ts +120 -0
  219. package/server-managed/bundle-advisory-inventory.ts +77 -0
  220. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  221. package/server-managed/bundle-build-diagnostics.js +23 -0
  222. package/server-managed/bundle-build-leases.ts +41 -0
  223. package/server-managed/bundle-build-worker.js +115 -0
  224. package/server-managed/bundle-build.ts +200 -0
  225. package/server-managed/bundle-cache.ts +211 -0
  226. package/server-managed/bundle-catalog.ts +29 -0
  227. package/server-managed/bundle-commits.ts +140 -0
  228. package/server-managed/bundle-store.ts +24 -0
  229. package/server-managed/bundle-summary-cache.ts +130 -0
  230. package/server-managed/bundle.ts +119 -0
  231. package/server-managed/cache-storage.ts +20 -0
  232. package/server-managed/cli.js +21 -0
  233. package/server-managed/combined.ts +47 -0
  234. package/server-managed/comments.ts +151 -0
  235. package/server-managed/commit-coauthors.ts +106 -0
  236. package/server-managed/commit-lockfiles.ts +73 -0
  237. package/server-managed/comparison.ts +41 -0
  238. package/server-managed/config.ts +164 -0
  239. package/server-managed/content-access.ts +15 -0
  240. package/server-managed/crypto.ts +25 -0
  241. package/server-managed/db-methods.ts +1923 -0
  242. package/server-managed/db-neon.ts +458 -0
  243. package/server-managed/db-schema.ts +238 -0
  244. package/server-managed/db-table-names.ts +22 -0
  245. package/server-managed/db.ts +142 -0
  246. package/server-managed/dependabot-access.ts +60 -0
  247. package/server-managed/dependabot-alerts.ts +185 -0
  248. package/server-managed/dependency-review-comments.ts +100 -0
  249. package/server-managed/dependency-reviews.ts +346 -0
  250. package/server-managed/github-accounts.ts +60 -0
  251. package/server-managed/github-actions.ts +71 -0
  252. package/server-managed/github-app.ts +415 -0
  253. package/server-managed/github-errors.ts +70 -0
  254. package/server-managed/github-graphql.ts +39 -0
  255. package/server-managed/github-issue-links.ts +140 -0
  256. package/server-managed/github-issue-oauth.ts +47 -0
  257. package/server-managed/github-issues.ts +152 -0
  258. package/server-managed/github-link-oauth.ts +69 -0
  259. package/server-managed/github-links.ts +204 -0
  260. package/server-managed/github-metadata.ts +213 -0
  261. package/server-managed/github-oauth.ts +238 -0
  262. package/server-managed/github-pulls.ts +227 -0
  263. package/server-managed/github-requests.ts +39 -0
  264. package/server-managed/github-users.ts +30 -0
  265. package/server-managed/http-response.ts +76 -0
  266. package/server-managed/http.ts +5329 -0
  267. package/server-managed/import-triage.ts +51 -0
  268. package/server-managed/index.ts +192 -0
  269. package/server-managed/kept-briefly.ts +37 -0
  270. package/server-managed/kept.ts +28 -0
  271. package/server-managed/link-reports.ts +162 -0
  272. package/server-managed/locked-packages.ts +129 -0
  273. package/server-managed/lockfile-scope.ts +49 -0
  274. package/server-managed/lru.ts +30 -0
  275. package/server-managed/managed-issues.ts +78 -0
  276. package/server-managed/management-catalog.ts +45 -0
  277. package/server-managed/management.ts +99 -0
  278. package/server-managed/npm-delta-worker.js +23 -0
  279. package/server-managed/npm-delta.ts +194 -0
  280. package/server-managed/npm-insights.ts +449 -0
  281. package/server-managed/npm-kept.ts +215 -0
  282. package/server-managed/npm-loads.ts +264 -0
  283. package/server-managed/npm-manifest.ts +115 -0
  284. package/server-managed/npm-packages.ts +454 -0
  285. package/server-managed/npm-provenance.ts +123 -0
  286. package/server-managed/npm-risk.ts +285 -0
  287. package/server-managed/object-storage-disk.ts +148 -0
  288. package/server-managed/object-storage-vercel.ts +67 -0
  289. package/server-managed/object-storage.ts +75 -0
  290. package/server-managed/package-entry-points.ts +51 -0
  291. package/server-managed/pretty-print.ts +434 -0
  292. package/server-managed/public-workspace.ts +238 -0
  293. package/server-managed/pull-comment-reviews.ts +161 -0
  294. package/server-managed/pull-dependencies.ts +348 -0
  295. package/server-managed/pull-dependency-reviews.ts +328 -0
  296. package/server-managed/pull-reviews.ts +205 -0
  297. package/server-managed/queue.ts +39 -0
  298. package/server-managed/release-commits.ts +84 -0
  299. package/server-managed/rename-diff-worker.js +26 -0
  300. package/server-managed/rename-diff.ts +279 -0
  301. package/server-managed/repo-path.ts +21 -0
  302. package/server-managed/report-migration.ts +35 -0
  303. package/server-managed/report-query.ts +10 -0
  304. package/server-managed/report-response.ts +16 -0
  305. package/server-managed/report-sources.ts +178 -0
  306. package/server-managed/report-store.ts +85 -0
  307. package/server-managed/repository-actions.ts +218 -0
  308. package/server-managed/repository-agents.ts +355 -0
  309. package/server-managed/repository-aliases.ts +91 -0
  310. package/server-managed/repository-browser.ts +294 -0
  311. package/server-managed/repository-changes.ts +284 -0
  312. package/server-managed/repository-code.ts +215 -0
  313. package/server-managed/repository-dependency-reviews.ts +165 -0
  314. package/server-managed/repository-discovery.ts +82 -0
  315. package/server-managed/repository-history.ts +432 -0
  316. package/server-managed/repository-issues.ts +195 -0
  317. package/server-managed/repository-policy.ts +27 -0
  318. package/server-managed/repository-pulls.ts +156 -0
  319. package/server-managed/repository-pulse.ts +460 -0
  320. package/server-managed/repository-risk-assess.ts +105 -0
  321. package/server-managed/repository-risk-audit.ts +79 -0
  322. package/server-managed/repository-risk-read.ts +170 -0
  323. package/server-managed/repository-risk.ts +491 -0
  324. package/server-managed/repository-squashes.ts +168 -0
  325. package/server-managed/request-timing.ts +63 -0
  326. package/server-managed/reviewed-dependencies.ts +150 -0
  327. package/server-managed/revisions.ts +35 -0
  328. package/server-managed/scan-downloads.ts +87 -0
  329. package/server-managed/session.ts +132 -0
  330. package/server-managed/slugs.ts +38 -0
  331. package/server-managed/solidity-entry-points.ts +71 -0
  332. package/server-managed/sql-postgres.ts +34 -0
  333. package/server-managed/sql.ts +88 -0
  334. package/server-managed/static.ts +28 -0
  335. package/server-managed/storage-command.ts +16 -0
  336. package/server-managed/storage-db.ts +183 -0
  337. package/server-managed/storage-encryption.ts +290 -0
  338. package/server-managed/storage-maintenance.ts +259 -0
  339. package/server-managed/storage-payload.ts +121 -0
  340. package/server-managed/storage-stores.ts +91 -0
  341. package/server-managed/storage.ts +82 -0
  342. package/server-managed/team-app.ts +134 -0
  343. package/server-managed/team-catalog.ts +7 -0
  344. package/server-managed/team-feed.ts +180 -0
  345. package/server-managed/team-npm-scopes.ts +106 -0
  346. package/server-managed/team-reports.ts +307 -0
  347. package/server-managed/triage-response.ts +16 -0
  348. package/server-managed/type-only-worker.js +19 -0
  349. package/server-managed/type-only.ts +105 -0
  350. package/server-managed/uploads.ts +106 -0
  351. package/server-managed/upstream-cache.ts +243 -0
  352. package/server-managed/view-as.ts +58 -0
  353. package/server-managed/workspace-shares.ts +169 -0
  354. package/server.ts +70 -0
  355. package/server/index.ts +0 -480
  356. package/server/lifecycle.ts +0 -208
  357. /package/{LICENSE → LICENSE-MIT} +0 -0
  358. /package/{server → server-e2e}/auth.ts +0 -0
  359. /package/{server → server-e2e}/config.example.json +0 -0
  360. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  361. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  362. /package/{server → server-e2e}/objstore/sign.ts +0 -0
  363. /package/{server → server-e2e}/peer.ts +0 -0
  364. /package/{server → server-e2e}/sse-session.ts +0 -0
  365. /package/{server → server-e2e}/util.ts +0 -0
  366. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,348 @@
1
+ // The package versions a pull request's lockfiles lock that they didn't
2
+ // before it, as the code viewer's Pull requests tab shows them on an open
3
+ // one's tile and its popup on any (GET
4
+ // /api/admin/repositories/pull-dependencies): read only where it changes a
5
+ // lockfile of npm's ecosystem that @preventive/lockfile reads
6
+ // (package-lock.json and npm-shrinkwrap.json of `lockfileVersion: 3`,
7
+ // pnpm-lock.yaml of pnpm 9 to 12 and Yarn 1's yarn.lock, locked-packages.ts),
8
+ // and Bun's, which it doesn't (bun.lock and bun.lockb, its directory told
9
+ // `lockfile-unsupported`); none under a node_modules nor a test's fixtures
10
+ // (isTestFixture), which no install takes. Its files are GitHub's comparison of its base with its head
11
+ // (GET /repos/:owner/:repo/compare/:base...:head), which names where its
12
+ // branch left its base, its merge base, as GitHub's own Files changed
13
+ // compares from it, and lists as many as a commit's files are read
14
+ // (repository-changes.ts MAX_FILES, 300, the most it lists); one changing as
15
+ // many, from its own list of them, a page at a time, as many as PULL_FILES.
16
+ // Each directory's lockfiles it changes are read at its merge base and at its
17
+ // head, a lockfile replaced by another, Yarn's by pnpm's say, read as the
18
+ // one it replaced; a version new to a directory is one its lockfiles lock at
19
+ // the head that none of them locked at the merge base, from where it is
20
+ // installed: a package new to it, or a version of one they locked others of,
21
+ // a bump's, told with those (`was`). What a pair of commits holds never
22
+ // changes, so what is told of one is kept by them (`kept`), and each
23
+ // lockfile read is kept in this instance's memory by its commit
24
+ // (`lockfileReads`): the pull requests branching off the same commit, as a
25
+ // bot's do, read their base's once. One read as a commit lists it, with its
26
+ // blob's id (commit-lockfiles.ts), is kept by its blob instead, and beyond
27
+ // this instance where its reader keeps it (LockfileStore): what a blob holds
28
+ // never changes either, and a push changing no lockfile changes none.
29
+ import { createClient } from '@preventive/upstream/github.js'
30
+ import { GithubApiError, githubJson } from './github-app.ts'
31
+ import { upstreamGithub } from './github-errors.ts'
32
+ import { compareVersions, isExactVersion } from '@preventive/upstream/semver.js'
33
+ import { type LockedPackage, isTestFixture, lockedPackages } from './locked-packages.ts'
34
+ import { type Lru, lru } from './lru.ts'
35
+ import { type ChangedFile, commitChanges } from './repository-changes.ts'
36
+
37
+ // The files of a pull request read from its own list past those compared,
38
+ // FILES_PAGE a page, as many as GitHub lists (3,000).
39
+ const FILES_PAGE = 100
40
+ export const PULL_FILES = 3000
41
+ // The directories whose lockfiles are read, those past them left out
42
+ // (`cut`), and the packages told new of each, those past them counted.
43
+ export const MAX_DIRECTORIES = 20
44
+ export const MAX_ADDED = 200
45
+ // The versions a directory's lockfiles lock at the head kept with what they
46
+ // add, as many as MAX_LOCKED, those past them counted, for those not new to
47
+ // be reviewed as well (pull-dependency-reviews.ts).
48
+ export const MAX_LOCKED = 20_000
49
+ // Lockfiles read at once.
50
+ const READ_PARALLEL = 4
51
+ // The rules what is told was told by, kept with it (http.ts
52
+ // pullDependencyKept), and the lockfiles read for it (lockfileKept): one
53
+ // told or read by others is told or read again.
54
+ export const PULL_DEPENDENCIES_READ = 3
55
+ const SHA = /^[a-f\d]{40}$/u
56
+
57
+ type Format = 'npm' | 'pnpm' | 'yarn' | 'bun'
58
+
59
+ // The format of the lockfile of npm's ecosystem at `path`, as
60
+ // findRepoLockfiles names it (with pnpm's of a branch,
61
+ // pnpm-lock.<branch>.yaml, and of before pnpm 3, shrinkwrap.yaml, which its
62
+ // reader refuses); null for another file, and for one under a node_modules or
63
+ // of a test's fixtures. Bun's alone isn't read.
64
+ export function lockfileFormat(path: string): Format | null {
65
+ const dirs = path.split('/')
66
+ const name = dirs.pop() ?? ''
67
+ if (dirs.includes('node_modules') || isTestFixture(path)) return null
68
+ if (name === 'package-lock.json' || name === 'npm-shrinkwrap.json') return 'npm'
69
+ if (/^pnpm-lock(?:\..+)?\.yaml$/u.test(name) || name === 'shrinkwrap.yaml') return 'pnpm'
70
+ if (name === 'bun.lock' || name === 'bun.lockb') return 'bun'
71
+ return name === 'yarn.lock' ? 'yarn' : null
72
+ }
73
+
74
+ // Whether a pull request changing `paths` changes a lockfile read here: null
75
+ // where none of them is one but they are fewer than it changes
76
+ // (`changed`), which it may yet.
77
+ export function touchesLockfile(paths: string[], changed: number): boolean | null {
78
+ if (paths.some(path => lockfileFormat(path) !== null)) return true
79
+ return changed > paths.length ? null : false
80
+ }
81
+
82
+ // A package version new to a directory, as its lockfile locks it, with its
83
+ // integrity, which its reviews are held to; of a package its lockfiles locked
84
+ // before, the versions of it they did (`was`, the highest first), none of one
85
+ // new to it.
86
+ export type AddedPackage = LockedPackage & { was?: string[] }
87
+ // A directory's lockfiles the pull request changes (`paths`, those it adds,
88
+ // modifies, removes or renames, by either name): the package versions they
89
+ // lock new to it, by name and version (as many as MAX_LOCKED, told as many
90
+ // as MAX_ADDED, `more` past them), and all they lock (`locked`, as many as MAX_LOCKED, how many more
91
+ // `moreLocked`); or why they couldn't be told, a lockfile's reader refusing one
92
+ // (`lockfile-unreadable`, with what it said), one of a format not read
93
+ // (`lockfile-unsupported`, Bun's) or GitHub having none at a commit compared.
94
+ export type DirectoryDependencies = { paths: string[] } & ({ added: AddedPackage[]; more: number; locked: LockedPackage[]; moreLocked: number }
95
+ | { error: string; detail?: string })
96
+ // What a pull request's lockfiles add: the commits compared (`base`, its
97
+ // merge base, and `head`); each directory's, of those it changes a lockfile
98
+ // in; and `cut` where it changes more files than GitHub lists, or lockfiles
99
+ // in more directories than are read, with, where only those cut it and they
100
+ // are few enough, the paths of those directories' lockfiles (`omitted`), for
101
+ // a review holding to none of them to fail in nothing.
102
+ export interface PullDependencies { base: string; head: string; lockfiles: DirectoryDependencies[]; cut: boolean; omitted?: string[] }
103
+ const MAX_OMITTED = 1000
104
+
105
+ // What is told of a pull request, by the commits it is of, `${base}...${head}`.
106
+ export interface PullDependencyStore {
107
+ read(key: string): Promise<PullDependencies | null>
108
+ write(key: string, value: PullDependencies): Promise<void>
109
+ }
110
+
111
+ // A lockfile's packages, or why its reader refused it, by its repository and
112
+ // readKey, read in this instance; by the packages each holds, as the scan's
113
+ // are (repository-risk.ts riskCaches).
114
+ export type LockfileRead = { packages: LockedPackage[] } | { error: string; detail?: string }
115
+ const lockfileReads: Lru<LockfileRead> = lru(200_000, read => 'packages' in read ? read.packages.length + 1 : 1)
116
+ // Those being read, by the token reading them too, lest each asking at once
117
+ // read them, with whether GitHub was asked.
118
+ const lockfilesReading = new Map<string, Promise<{ read: LockfileRead; github: boolean }>>()
119
+ // A lockfile at a commit as findRepoLockfiles lists it, by its path, with its
120
+ // format (lockfileFormat) and its blob's id (`sha`).
121
+ export interface ListedLockfile { path: string; format: Format; sha: string }
122
+ // What a repository's lockfiles read for its dependency review keep beyond
123
+ // this instance (http.ts lockfileKept), by what never changes: a commit's
124
+ // lockfiles as listed (`listings`, by the commit), and a lockfile's read
125
+ // (`lockfiles`, by its readKey). A read GitHub failed is kept nowhere.
126
+ export interface LockfileStore {
127
+ listings: { read(commit: string): Promise<ListedLockfile[] | null>; write(commit: string, listed: ListedLockfile[]): Promise<void> }
128
+ lockfiles: { read(key: string): Promise<LockfileRead | null>; write(key: string, read: LockfileRead): Promise<void> }
129
+ }
130
+ // Set where GitHub was asked for what a read reads (`github`), not all of it
131
+ // held in this instance or kept beyond it.
132
+ export interface GithubAsked { github: boolean }
133
+
134
+ // A lockfile's read, by its blob where it is listed with its id (`sha`),
135
+ // which with its format alone tell how it reads; else by the commit and its
136
+ // path.
137
+ const readKey = (ref: string, { path, format, sha }: { path: string; format: Format; sha?: string }) =>
138
+ sha ? `blob\0${format}\0${sha}` : `commit\0${ref}\0${path}`
139
+
140
+ const api = (fullName: string) => `https://api.github.com/repos/${fullName.split('/').map(encodeURIComponent).join('/')}`
141
+
142
+ // The pull request's base's and head's commits, as GitHub has them now: its
143
+ // base's moving on as it does, its head's with each push, both kept once it
144
+ // is merged or closed.
145
+ export interface PullCommits { base: string; head: string }
146
+ async function pullCommits(fullName: string, token: string, number: number, fetchImpl: typeof fetch): Promise<PullCommits> {
147
+ const pull = await githubJson(`${api(fullName)}/pulls/${number}`, token, fetchImpl) as
148
+ { number?: unknown; base?: { sha?: unknown; repo?: { full_name?: unknown } | null }; head?: { sha?: unknown } } | null
149
+ const base = pull?.base?.sha, head = pull?.head?.sha, repo = pull?.base?.repo?.full_name
150
+ if (pull?.number !== number || typeof base !== 'string' || !SHA.test(base) || typeof head !== 'string' || !SHA.test(head)
151
+ || typeof repo !== 'string' || repo.toLowerCase() !== fullName.toLowerCase()) throw new GithubApiError(502, 'github-malformed')
152
+ return { base, head }
153
+ }
154
+
155
+ // The pull request's merge base and the files it changes from it: as GitHub
156
+ // compares its base with its head, else, where that lists as many as it lists
157
+ // at most, from the pull request's own list, a page at a time; `cut` where
158
+ // that lists as many as it lists at most, too. That list is of its commits
159
+ // now: null where they are no longer `base` and `head` (`current`).
160
+ async function pullFiles(fullName: string, token: string, number: number, base: string, head: string, fetchImpl: typeof fetch,
161
+ current: () => Promise<boolean>) {
162
+ const compared = await githubJson(`${api(fullName)}/compare/${base}...${head}?per_page=1`, token, fetchImpl) as { merge_base_commit?: { sha?: unknown } } | null
163
+ const mergeBase = compared?.merge_base_commit?.sha
164
+ if (typeof mergeBase !== 'string' || !SHA.test(mergeBase)) throw new GithubApiError(502, 'github-malformed')
165
+ const { files, more } = commitChanges(compared)
166
+ if (!more) return { mergeBase, files, cut: false }
167
+ if (!await current()) return null
168
+ const listed: ChangedFile[] = []
169
+ for (let page = 1; listed.length < PULL_FILES; page++) {
170
+ const data = await githubJson(`${api(fullName)}/pulls/${number}/files?per_page=${FILES_PAGE}&page=${page}`, token, fetchImpl)
171
+ if (!Array.isArray(data)) throw new GithubApiError(502, 'github-malformed')
172
+ listed.push(...commitChanges({ files: data }).files)
173
+ if (data.length < FILES_PAGE) return { mergeBase, files: listed, cut: false }
174
+ }
175
+ return { mergeBase, files: listed, cut: true }
176
+ }
177
+
178
+ // The lockfiles read here a pull request changes (`files`), by directory:
179
+ // each as it was (`before`, one modified or removed, or a rename's path
180
+ // before) and as it is (`after`, one added or modified, or a rename's path
181
+ // now), and every path of them (`paths`), as many directories as
182
+ // MAX_DIRECTORIES.
183
+ function lockfilesChanged(files: ChangedFile[]) {
184
+ type Directory = { before: { path: string; format: Format }[]; after: { path: string; format: Format }[]; paths: Set<string> }
185
+ const directories = new Map<string, Directory>()
186
+ const take = (side: 'before' | 'after', path: string | undefined) => {
187
+ const format = path ? lockfileFormat(path) : null
188
+ if (!path || !format) return
189
+ const dir = path.slice(0, Math.max(path.lastIndexOf('/'), 0))
190
+ const directory = directories.get(dir) ?? directories.set(dir, { before: [], after: [], paths: new Set() }).get(dir)!
191
+ directory[side].push({ path, format })
192
+ directory.paths.add(path)
193
+ }
194
+ for (const [path, status, from] of files) {
195
+ take('before', status === 'R' ? from : status === 'A' ? undefined : path)
196
+ take('after', status === 'D' ? undefined : path)
197
+ }
198
+ const all = [...directories.entries()].toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
199
+ return { directories: all.slice(0, MAX_DIRECTORIES).map(([, directory]) => directory), cut: all.length > MAX_DIRECTORIES,
200
+ omitted: all.slice(MAX_DIRECTORIES).flatMap(([, directory]) => [...directory.paths]).toSorted() }
201
+ }
202
+
203
+ // A lockfile's packages at `ref`, as read in this instance, else as kept
204
+ // beyond it (`kept`), else shared with a read of it with the same token still
205
+ // under way, else of GitHub, and kept; a lockfile its reader refuses, why,
206
+ // kept as well, its reader refusing it again; `asked` told where GitHub was.
207
+ // Any failure to read it of GitHub but its having none there is the whole
208
+ // answer's, as it would be every file's; neither is kept.
209
+ export async function readLockfile(fullName: string, token: string, ref: string, lockfile: { path: string; format: Format; sha?: string },
210
+ kept: LockfileStore | null = null, asked?: GithubAsked): Promise<LockfileRead> {
211
+ if (lockfile.format === 'bun') return { error: 'lockfile-unsupported' }
212
+ const key = readKey(ref, lockfile)
213
+ const known = lockfileReads.get(`${fullName}\0${key}`)
214
+ if (known) return known
215
+ const shared = `${token}\0${fullName}\0${key}`
216
+ const reading = lockfilesReading.get(shared) ?? readOf(fullName, token, ref, lockfile, key, kept).finally(() => lockfilesReading.delete(shared))
217
+ lockfilesReading.set(shared, reading)
218
+ // One failing was asked of GitHub too, though a caller swallows its
219
+ // failure, as a baseline's does: its access is asked again all the same.
220
+ let told
221
+ try { told = await reading } catch (err) {
222
+ if (asked) asked.github = true
223
+ throw err
224
+ }
225
+ if (told.github && asked) asked.github = true
226
+ return told.read
227
+ }
228
+ async function readOf(fullName: string, token: string, ref: string, { path, format }: { path: string; format: Format }, key: string,
229
+ kept: LockfileStore | null): Promise<{ read: LockfileRead; github: boolean }> {
230
+ const stored = await kept?.lockfiles.read(key)
231
+ if (stored) { lockfileReads.set(`${fullName}\0${key}`, stored); return { read: stored, github: false } }
232
+ let text: string
233
+ try {
234
+ text = await upstreamGithub(() => createClient({ token, userAgent: 'deepview-triage' }).getRepoFile({ repo: fullName, path, ref }))
235
+ } catch (err) {
236
+ if (err instanceof GithubApiError && err.message === 'github-not-found') return { read: { error: 'github-not-found' }, github: true }
237
+ throw err
238
+ }
239
+ let read: LockfileRead
240
+ try { read = { packages: lockedPackages(format, text, { review: true }) } } catch (error) {
241
+ read = { error: 'lockfile-unreadable', ...error instanceof Error ? { detail: error.message.slice(0, 500) } : {} }
242
+ }
243
+ lockfileReads.set(`${fullName}\0${key}`, read)
244
+ await kept?.lockfiles.write(key, read)
245
+ return { read, github: true }
246
+ }
247
+
248
+ // Runs `run` for each of `items`, as many at once as `parallel`, each's
249
+ // result in its place.
250
+ export async function eachOf<T, R>(items: T[], parallel: number, run: (item: T) => Promise<R>): Promise<R[]> {
251
+ const results: R[] = []
252
+ let next = 0
253
+ await Promise.all(Array.from({ length: Math.min(parallel, items.length) }, async () => {
254
+ while (next < items.length) {
255
+ const at = next++
256
+ results[at] = await run(items[at]!)
257
+ }
258
+ }))
259
+ return results
260
+ }
261
+
262
+ // Versions by semver, the highest first, any semver can't read after them.
263
+ function byVersion(a: string, b: string) {
264
+ const [exactA, exactB] = [isExactVersion(a), isExactVersion(b)]
265
+ if (exactA && exactB) return compareVersions(b, a)
266
+ if (exactA === exactB) return a < b ? -1 : a > b ? 1 : 0
267
+ return exactA ? -1 : 1
268
+ }
269
+ const byName = (a: AddedPackage, b: AddedPackage) => a.name < b.name ? -1 : a.name > b.name ? 1 : -byVersion(a.version ?? '', b.version ?? '')
270
+ // A package as it is installed: its name and version, where from, and its
271
+ // patch, a patched version's new where its patch is; and as it is locked,
272
+ // with what its reviews are held to too.
273
+ export const lockedAs = (pkg: LockedPackage) => `${pkg.source}\0${pkg.name}@${pkg.version ?? ''}\0${pkg.from ?? ''}\0${pkg.patchHash ?? ''}`
274
+ export const lockedWith = (pkg: LockedPackage) => `${lockedAs(pkg)}\0${pkg.integrity ?? ''}\0${pkg.shasum ?? ''}\0${pkg.lockfile ?? ''}`
275
+
276
+ // What a directory's lockfiles add, from each read before and after: the
277
+ // package versions after none before has, each once, by name and version,
278
+ // each with the versions of its name before, those past MAX_ADDED counted
279
+ // (`more`), not told but reviewed as they are; and every one after, each
280
+ // integrity it is locked with (`locked`), as many as MAX_LOCKED; or the first
281
+ // failure among them.
282
+ function addedIn(paths: string[], before: LockfileRead[], after: LockfileRead[]): DirectoryDependencies {
283
+ const failed = [...before, ...after].find(read => 'error' in read)
284
+ if (failed && 'error' in failed) return { paths, error: failed.error, ...failed.detail ? { detail: failed.detail } : {} }
285
+ const known = new Set<string>()
286
+ const versions = new Map<string, Set<string>>()
287
+ for (const pkg of before.flatMap(read => 'packages' in read ? read.packages : [])) {
288
+ known.add(lockedAs(pkg))
289
+ const of = versions.get(pkg.name) ?? versions.set(pkg.name, new Set()).get(pkg.name)!
290
+ if (pkg.version) of.add(pkg.version)
291
+ }
292
+ const added = new Map<string, AddedPackage>()
293
+ const locked = new Map<string, LockedPackage>()
294
+ for (const read of after) {
295
+ for (const pkg of 'packages' in read ? read.packages : []) {
296
+ const key = lockedAs(pkg)
297
+ const held = locked.get(lockedWith(pkg))
298
+ if (!held) locked.set(lockedWith(pkg), { ...pkg })
299
+ else if (held.dev && !pkg.dev) delete held.dev
300
+ if (known.has(key)) continue
301
+ const had = added.get(key)
302
+ const was = versions.get(pkg.name)
303
+ if (!had) added.set(key, { ...pkg, ...was ? { was: [...was].toSorted(byVersion) } : {} })
304
+ else if (had.dev && !pkg.dev) delete had.dev
305
+ }
306
+ }
307
+ const all = [...added.values()].toSorted(byName)
308
+ return { paths, added: all.slice(0, MAX_LOCKED), more: Math.max(0, all.length - MAX_ADDED),
309
+ locked: [...locked.values()].slice(0, MAX_LOCKED), moreLocked: Math.max(0, locked.size - MAX_LOCKED) }
310
+ }
311
+
312
+ // What pull request `number` adds to its lockfiles' packages (PullDependencies),
313
+ // kept by its base's and head's commits, else read of GitHub and kept; none
314
+ // where it changes no lockfile read here.
315
+ export async function pullDependencies(fullName: string, token: string, number: number, kept: PullDependencyStore,
316
+ fetchImpl: typeof fetch = globalThis.fetch): Promise<PullDependencies> {
317
+ return (await pullDependenciesAt(fullName, token, number, kept, fetchImpl)).told
318
+ }
319
+ // The same, with the commits it is kept by (`commits`), its base's and
320
+ // head's, by which `kept` reads it: those `given`, as a page of its
321
+ // repository's open pull requests read them (repository-issues.ts), else as
322
+ // GitHub has them now. Commits given that its own list of its files is no
323
+ // longer of are read as GitHub has them now.
324
+ export async function pullDependenciesAt(fullName: string, token: string, number: number, kept: PullDependencyStore,
325
+ fetchImpl: typeof fetch = globalThis.fetch, given?: PullCommits): Promise<{ commits: string; told: PullDependencies }> {
326
+ const { base, head } = given ?? await pullCommits(fullName, token, number, fetchImpl)
327
+ const key = `${base}...${head}`
328
+ const held = await kept.read(key)
329
+ if (held) return { commits: key, told: held }
330
+ const current = async () => {
331
+ const now = given && await pullCommits(fullName, token, number, fetchImpl)
332
+ return !now || (now.base === base && now.head === head)
333
+ }
334
+ const pulled = await pullFiles(fullName, token, number, base, head, fetchImpl, current)
335
+ if (!pulled) return pullDependenciesAt(fullName, token, number, kept, fetchImpl)
336
+ const { mergeBase, files, cut: filesCut } = pulled
337
+ const { directories, cut, omitted } = lockfilesChanged(files)
338
+ const reads = directories.flatMap(({ before, after }) => [...before.map(lockfile => [mergeBase, lockfile] as const), ...after.map(lockfile => [head, lockfile] as const)])
339
+ const read = await eachOf(reads, READ_PARALLEL, ([ref, lockfile]) => readLockfile(fullName, token, ref, lockfile))
340
+ let at = 0
341
+ const lockfiles = directories.map(({ before, after, paths }) => {
342
+ const readBefore = read.slice(at, at += before.length)
343
+ return addedIn([...paths].toSorted(), readBefore, read.slice(at, at += after.length))
344
+ })
345
+ const told = { base: mergeBase, head, lockfiles, cut: filesCut || cut, ...cut && !filesCut && omitted.length <= MAX_OMITTED ? { omitted } : {} }
346
+ await kept.write(key, told)
347
+ return { commits: key, told }
348
+ }