@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,238 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
2
+ import { pipeline } from 'node:stream/promises'
3
+ import { Buffer } from 'node:buffer'
4
+ import type { ManagedHttpDeps } from './http.ts'
5
+ import type { ManagedBundle } from './db.ts'
6
+ import type { OpenedBlob } from './blob-store.ts'
7
+ import type { WorkspaceShareSnapshot } from './workspace-shares.ts'
8
+ import { sendJson, sendLines, writeResponse } from './http-response.ts'
9
+ import { hashToken } from './crypto.ts'
10
+ import { TeamReportsError, loadTeamAnnotations, loadTeamReportsResponse, teamReportVisibility, teamResponsePart } from './team-reports.ts'
11
+ import { triageWireEntry } from './triage-response.ts'
12
+ import { MAX_PACKAGE_INVENTORY_BYTES } from './bundle-cache.ts'
13
+ import { backfillBundleSummaries, bundleSummaries } from './bundle-catalog.ts'
14
+ import { ADVISORIES_TIMEOUT_MS, fetchBundleAdvisories } from './bundle-advisories.ts'
15
+ import { auditCache, auditedRepos } from './upstream-cache.ts'
16
+ import { serveTeamFeed } from './team-feed.ts'
17
+ import { type ComparedSide, PrettyError, compared, prettyRequest, prettyText } from './pretty-print.ts'
18
+ import { renameDiffOf, renameDiffRequest, renamedPairs } from './rename-diff.ts'
19
+ import { typeOnlyFiles } from './type-only.ts'
20
+ import { comparisonBody } from './comparison.ts'
21
+ import { sharedTeamApp, withWorkspaceSummary } from './team-app.ts'
22
+
23
+ function json(res: ServerResponse, status: number, body: unknown): void {
24
+ sendJson(res, status, body, { 'cache-control': 'private, no-store', 'referrer-policy': 'no-referrer' })
25
+ }
26
+
27
+ // Called before ALL other routing, including OAuth, discovery, static and the
28
+ // combined server fallback. A supplied capability never inherits cookie auth.
29
+ export async function handlePublicWorkspace(req: IncomingMessage, res: ServerResponse, deps: ManagedHttpDeps, url: URL): Promise<void> {
30
+ const token = req.headers['x-deepview-share']
31
+ if (!deps.config.allowShare || typeof token !== 'string' || !/^[A-Za-z0-9_-]{43}$/u.test(token)) {
32
+ json(res, 401, { error: 'invalid-share' }); return
33
+ }
34
+ const method = req.method ?? 'GET'
35
+ if (method !== 'GET' && method !== 'HEAD') { json(res, 403, { error: 'share-read-only' }); return }
36
+ const shareRoute = /^\/api\/shares\/([A-Za-z0-9_-]+)\/workspace$/u.exec(url.pathname)
37
+ const teamRoute = /^\/api\/teams\/([^/]+)\/(shared|reports|trees|feed|annotations)$/u.exec(url.pathname)
38
+ const reportRoute = /^\/api\/reports\/([^/]+)\/(triage|comments|sources)$/u.exec(url.pathname)
39
+ const bundleRoute = /^\/api\/bundles\/([^/]+)\/(metadata|contents|download|advisories|pretty|rename-diff|type-only|comparison)$/u.exec(url.pathname)
40
+ if (!shareRoute && !teamRoute && !reportRoute && !bundleRoute) { json(res, 403, { error: 'share-scope-required' }); return }
41
+ const tokenHash = hashToken(token)
42
+ const snapshot = await deps.db.getWorkspaceShare(tokenHash)
43
+ if (!snapshot) { json(res, 401, { error: 'invalid-share' }); return }
44
+ const recheck = async () => {
45
+ const current = deps.config.allowShare && await deps.db.getWorkspaceShare(tokenHash)
46
+ if (!current || JSON.stringify(current) !== JSON.stringify(snapshot)) throw new TeamReportsError(404, 'workspace-changed')
47
+ }
48
+ const send = async (body: unknown) => { await recheck(); json(res, 200, body) }
49
+ const sendTeam = async () => {
50
+ const [summaries, workspace] = await Promise.all([bundleSummaries(snapshot.team.bundles, deps.bundleCache), sharedTeamApp(deps.db, deps.reportStore, snapshot)])
51
+ await send({ user: snapshot.user, team: { ...withWorkspaceSummary(snapshot.team, workspace),
52
+ bundles: snapshot.team.bundles.map(bundle => ({ ...bundle, ...(summaries.get(bundle.integrity) ?? { summary: null, summaryRetryAt: null }) })),
53
+ } })
54
+ await backfillBundleSummaries(snapshot.team.bundles, deps.bundleCache)
55
+ }
56
+ const stream = async (stored: OpenedBlob, encoding: string | null, contentType = 'application/json') => {
57
+ try { await recheck() } catch (error) { stored.stream.destroy(); throw error }
58
+ res.writeHead(200, { 'content-type': contentType, 'cache-control': 'private, no-store',
59
+ 'x-content-type-options': 'nosniff', ...(encoding ? { 'content-encoding': encoding } : {}),
60
+ ...(stored.size == null ? {} : { 'content-length': String(stored.size) }),
61
+ })
62
+ if (method === 'HEAD') { stored.stream.destroy(); res.end(); return }
63
+ try { await pipeline(stored.stream, res) } catch { res.destroy() }
64
+ }
65
+ if (url.searchParams.has('team') && (url.searchParams.getAll('team').length !== 1 || url.searchParams.get('team') !== snapshot.teamId)) {
66
+ json(res, 404, { error: 'no-team' }); return
67
+ }
68
+ if (shareRoute) {
69
+ // The short hash is a recognizable link ID, never an access grant. Resolve
70
+ // with the full token so colliding IDs or slugs cannot broaden its scope.
71
+ // Existing links used the team ID as their fragment prefix.
72
+ if (shareRoute[1] !== tokenHash.slice(0, 8) && shareRoute[1] !== snapshot.teamId) {
73
+ json(res, 404, { error: 'no-share' }); return
74
+ }
75
+ await sendTeam(); return
76
+ }
77
+ if (teamRoute) {
78
+ if (teamRoute[1] !== snapshot.teamId) { json(res, 404, { error: 'no-team' }); return }
79
+ if (teamRoute[2] === 'feed') {
80
+ if (method !== 'GET') { json(res, 405, { error: 'method-not-allowed' }); return }
81
+ await serveTeamFeed(res, deps, snapshot, recheck, { readState: () => deps.db.getWorkspaceShareFeedState(tokenHash) }); return
82
+ }
83
+ if (teamRoute[2] === 'annotations') {
84
+ await send(await loadTeamAnnotations(deps.db, deps.reportStore, snapshot, url.searchParams.get('reportId'))); return
85
+ }
86
+ if (teamRoute[2] === 'shared') { await sendTeam(); return }
87
+ const body = await loadTeamReportsResponse(deps.db, deps.reportStore, snapshot, teamResponsePart(teamRoute[2]!, url))
88
+ await recheck()
89
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'private, no-store', 'referrer-policy': 'no-referrer' })
90
+ writeResponse(res, body); return
91
+ }
92
+ if (reportRoute) {
93
+ const id = reportRoute[1]!, part = reportRoute[2]!
94
+ if (!snapshot.reports.some(report => report.id === id)) { json(res, 404, { error: 'no-report' }); return }
95
+ const visible = await teamReportVisibility(deps.db, deps.reportStore, snapshot, id)
96
+ await recheck()
97
+ if (part === 'sources') { await sources(res, deps, snapshot, id, visible.sourcePaths, recheck, stream); return }
98
+ if (part === 'comments') { await send({ comments: await deps.db.listComments([...visible.ids]) }); return }
99
+ const entries = Object.fromEntries((await deps.db.listTriage([...visible.ids])).map(row => [row.findingId, triageWireEntry(row)]))
100
+ await send({ entries }); return
101
+ }
102
+ const bundle = snapshot.bundles.find(item => item.id === bundleRoute![1])
103
+ if (!bundle) { json(res, 404, { error: 'no-bundle' }); return }
104
+ if (bundleRoute![2] === 'advisories' && !snapshot.permissions.security) { json(res, 403, { error: 'security-access-required' }); return }
105
+ if (bundleRoute![2] === 'rename-diff' || bundleRoute![2] === 'type-only' || bundleRoute![2] === 'comparison' || (bundleRoute![2] === 'pretty' && url.searchParams.has('other'))) {
106
+ if (method !== 'GET') { json(res, 405, { error: 'method-not-allowed' }); return }
107
+ await serveCompared(res, deps, bundle, snapshot.bundles.find(item => item.id === url.searchParams.get('other')), bundleRoute![2], url, recheck, stream); return
108
+ }
109
+ await serveBundle(res, deps, bundle, bundleRoute![2]!, url, recheck, stream)
110
+ }
111
+
112
+ // What a bundle is compared with `other`, another of the link's bundles, by:
113
+ // their files paired, as one comparison; one of their JavaScript files diffed
114
+ // with renamed names hidden; or the files that changed between them, or the
115
+ // pairs of those, or those that changed in types alone, a line each.
116
+ function serveCompared(res: ServerResponse, deps: ManagedHttpDeps, bundle: ManagedBundle, other: ManagedBundle | undefined, part: string, url: URL,
117
+ recheck: () => Promise<void>, stream: (stored: OpenedBlob, encoding: string | null, contentType?: string) => Promise<void>) {
118
+ if (part === 'comparison') return serveComparison(res, deps, bundle, other, url, stream)
119
+ if (part === 'rename-diff' && url.searchParams.has('path')) return serveRenameDiff(res, deps, bundle, other, url, stream)
120
+ return serveChanges(res, deps, bundle, other, recheck, part === 'pretty' ? compared : part === 'type-only' ? typeOnlyFiles : renamedPairs,
121
+ part === 'pretty' ? '' : url.searchParams.get('scope') ?? '')
122
+ }
123
+
124
+ // One of a bundle's JavaScript files diffed with one of `other`, another of
125
+ // the link's bundles (rename-diff.ts).
126
+ async function serveRenameDiff(res: ServerResponse, deps: ManagedHttpDeps, bundle: ManagedBundle, other: ManagedBundle | undefined, url: URL,
127
+ stream: (stored: OpenedBlob, encoding: string | null, contentType?: string) => Promise<void>) {
128
+ const request = renameDiffRequest(url.searchParams)
129
+ if (!request) { json(res, 400, { error: 'bad-file' }); return }
130
+ if (!other) { json(res, 404, { error: 'no-bundle' }); return }
131
+ const { prettyCache } = deps
132
+ if (!prettyCache) { json(res, 503, { error: 'unavailable' }); return }
133
+ let diff
134
+ try {
135
+ diff = await renameDiffOf(request, async () => [await prettyText(await prettyCache.bundle(bundle, request.base)),
136
+ await prettyText(await prettyCache.bundle(other, request.other))])
137
+ } catch (err) {
138
+ if (err instanceof PrettyError) { json(res, err.status, { error: err.message }); return }
139
+ throw err
140
+ }
141
+ await stream(diff, 'br')
142
+ }
143
+
144
+ // A bundle's files paired with `other`'s, another of the link's bundles, as
145
+ // Compare pairs them (comparison.ts), within a reason `scope` where given.
146
+ async function serveComparison(res: ServerResponse, deps: ManagedHttpDeps, bundle: ManagedBundle, other: ManagedBundle | undefined, url: URL,
147
+ stream: (stored: OpenedBlob, encoding: string | null, contentType?: string) => Promise<void>) {
148
+ if (!other) { json(res, 404, { error: 'no-bundle' }); return }
149
+ const { prettyCache } = deps
150
+ if (!prettyCache) { json(res, 503, { error: 'unavailable' }); return }
151
+ let body
152
+ try { body = await comparisonBody(await prettyCache.bundleDetails(bundle), await prettyCache.bundleDetails(other), url.searchParams.get('scope') ?? '') }
153
+ catch (err) {
154
+ if (err instanceof PrettyError) { json(res, err.status, { error: err.message }); return }
155
+ throw err
156
+ }
157
+ await stream(body, 'br')
158
+ }
159
+
160
+ // The files that changed between a bundle and `other`, another of the link's
161
+ // bundles, both sides' pretty-printed (pretty-print.ts compared), or the pairs
162
+ // of them its viewer diffs with renamed names hidden (rename-diff.ts
163
+ // renamedPairs), those within a reason `scope` where given, the link checked
164
+ // again before each line.
165
+ async function serveChanges(res: ServerResponse, deps: ManagedHttpDeps, bundle: ManagedBundle, other: ManagedBundle | undefined, recheck: () => Promise<void>,
166
+ linesOf: (sides: [ComparedSide, ComparedSide]) => AsyncIterable<unknown>, scope: string) {
167
+ if (!other) { json(res, 404, { error: 'no-bundle' }); return }
168
+ if (!deps.prettyCache) { json(res, 503, { error: 'unavailable' }); return }
169
+ let lines
170
+ try { lines = linesOf(await deps.prettyCache.bundleSides(bundle, other, scope)) }
171
+ catch (err) {
172
+ if (err instanceof PrettyError) { json(res, err.status, { error: err.message }); return }
173
+ throw err
174
+ }
175
+ await recheck()
176
+ await sendLines(res, lines, () => recheck().then(() => true, () => false))
177
+ }
178
+
179
+ async function serveBundle(res: ServerResponse, deps: ManagedHttpDeps, bundle: ManagedBundle, part: string, url: URL,
180
+ recheck: () => Promise<void>, stream: (stored: OpenedBlob, encoding: string | null, contentType?: string) => Promise<void>) {
181
+ if (part === 'download') {
182
+ const stored = await deps.bundleStore.open(bundle.id, bundle.kind)
183
+ if (!stored) { json(res, 503, { error: 'unavailable' }); return }
184
+ await stream(stored, bundle.kind === 'sourcemap' ? 'br' : null, 'application/octet-stream'); return
185
+ }
186
+ if (part === 'pretty') {
187
+ const file = prettyRequest(url.searchParams)
188
+ if (!file) { json(res, 400, { error: 'bad-file' }); return }
189
+ if (!deps.prettyCache) { json(res, 503, { error: 'unavailable' }); return }
190
+ let pretty
191
+ try { pretty = await deps.prettyCache.bundle(bundle, file) }
192
+ catch (err) {
193
+ if (err instanceof PrettyError) { json(res, err.status, { error: err.message }); return }
194
+ throw err
195
+ }
196
+ await stream(pretty, 'br', 'text/plain; charset=utf-8'); return
197
+ }
198
+ if (!deps.bundleCache) { json(res, 503, { error: 'unavailable' }); return }
199
+ if (part === 'advisories') {
200
+ if (bundle.kind !== 'stasis') { json(res, 422, { error: 'unsupported-bundle' }); return }
201
+ const inventory = await deps.bundleCache.advisoryInventory(bundle, url.searchParams.get('reason') ?? '')
202
+ await recheck()
203
+ if (inventory === null) { json(res, 413, { error: 'payload-too-large' }); return }
204
+ if (inventory === undefined) { json(res, 400, { error: 'unknown-reason' }); return }
205
+ if (Buffer.byteLength(JSON.stringify(inventory)) > MAX_PACKAGE_INVENTORY_BYTES) { json(res, 413, { error: 'payload-too-large' }); return }
206
+ const signal = AbortSignal.timeout(ADVISORIES_TIMEOUT_MS)
207
+ const repoAdvisories = url.searchParams.get('repoAdvisories') === 'true'
208
+ const result = await fetchBundleAdvisories(inventory.packages, signal, {
209
+ debug: deps.config.debug, repoAdvisories, details: url.searchParams.get('details') === 'true',
210
+ cache: auditCache(deps.config.upstreamCacheDir, deps.db, signal, deps.config.debug, auditedRepos(inventory.packages, repoAdvisories)),
211
+ })
212
+ await recheck()
213
+ json(res, result.status, result.status === 200 ? { ...inventory, advisories: result.body } : result.body); return
214
+ }
215
+ let cached
216
+ try { cached = await deps.bundleCache.open(bundle, part as 'metadata' | 'contents') }
217
+ catch { json(res, 422, { error: 'bundle-unavailable' }); return }
218
+ await stream(cached, 'br')
219
+ }
220
+
221
+ async function sources(res: ServerResponse, deps: ManagedHttpDeps, snapshot: WorkspaceShareSnapshot,
222
+ id: string, paths: Set<string>, recheck: () => Promise<void>, stream: (stored: OpenedBlob, encoding: string | null) => Promise<void>) {
223
+ const report = await deps.db.getReport(id)
224
+ const bundle = report?.bundleId ? await deps.db.getBundle(report.bundleId) : null
225
+ const empty = async () => { await recheck(); res.writeHead(204, { 'cache-control': 'private, no-store' }); res.end() }
226
+ if (!report || !bundle || !['stasis', 'sourcemap'].includes(bundle.kind ?? '') || bundle.repoId !== report.repoId) { await empty(); return }
227
+ if (!deps.reportSourcesCache) { json(res, 503, { error: 'unavailable' }); return }
228
+ let cached
229
+ try { cached = await deps.reportSourcesCache.open(report, bundle, snapshot.permissions, paths) }
230
+ catch { await empty(); return }
231
+ if (!cached) { await empty(); return }
232
+ const current = await deps.db.getReport(id), currentBundle = await deps.db.getBundle(bundle.id)
233
+ if (JSON.stringify(current) !== JSON.stringify(report) || JSON.stringify(currentBundle) !== JSON.stringify(bundle)
234
+ || snapshot.reports.find(item => item.id === id)?.repo.github !== cached.repo.github) {
235
+ cached.stream.destroy(); throw new TeamReportsError(404, 'workspace-changed')
236
+ }
237
+ await stream(cached, 'gzip')
238
+ }
@@ -0,0 +1,161 @@
1
+ // The reviews of dependencies an open pull request's comments make, as the
2
+ // host's parser reads them (reviewed-dependencies.ts `parseComments`): its
3
+ // comments, its reviews' own, whatever their state, and those on lines of
4
+ // its diff, read of GitHub's GraphQL API, as many as MAX_COMMENTS, with
5
+ // whether it is open, kept in this instance KEPT_MS; none of one not open;
6
+ // the parser's answer to the same kept PARSED_KEPT_MS.
7
+ // A review counts only where its user is one of their authors and the app's
8
+ // user that account is (by its numeric id, else its login, its linked
9
+ // accounts' too) publishes reviews to a team counting for the repository
10
+ // (`reviewers`): by that user, as their reviews here are, each once among
11
+ // them (pull-dependency-reviews.ts), as of its time, else their latest
12
+ // comment's. They count for the pull request alone, never stored.
13
+ import { createHash } from 'node:crypto'
14
+ import { ACTOR, type GraphAccount, actorOf } from './github-accounts.ts'
15
+ import { type Lru, lru } from './lru.ts'
16
+ import type { CommentReviews } from './pull-dependency-reviews.ts'
17
+ import type { PublishedReview } from './repository-dependency-reviews.ts'
18
+ import { githubGraphql } from './repository-history.ts'
19
+ import type { CommentReview, ParseCommentsInput, ReviewComment, ReviewVerdict, ReviewedDependencies } from './reviewed-dependencies.ts'
20
+
21
+ const MAX_COMMENTS = 1000
22
+ const KEPT_MS = 60_000
23
+ const PARSED_KEPT_MS = 10 * 60_000
24
+ const VERDICTS = new Set<string>(['accept', 'accept-with-nits', 'request-changes', 'reject'])
25
+
26
+ // A comment as read: when it was written, and last edited, where it was.
27
+ const COMMENT = `id author { ${ACTOR} } body createdAt lastEditedAt url`
28
+ // Its comments and its reviews, each a page on from its cursor, each only
29
+ // while it has more (`readComments`, `readReviews`).
30
+ const QUERY = `query($owner: String!, $name: String!, $number: Int!, $comments: String, $reviews: String, $readComments: Boolean!, $readReviews: Boolean!) {
31
+ repository(owner: $owner, name: $name) { pullRequest(number: $number) { state
32
+ comments(first: 100, after: $comments) @include(if: $readComments) { pageInfo { hasNextPage endCursor } nodes { ${COMMENT} } }
33
+ reviews(first: 50, after: $reviews) @include(if: $readReviews) { pageInfo { hasNextPage endCursor }
34
+ nodes { ${COMMENT} state comments(first: 100) { pageInfo { hasNextPage endCursor } nodes { ${COMMENT} } } } }
35
+ } } }`
36
+ // A review's comments on lines of its diff, a page on from its cursor.
37
+ const REVIEW_COMMENTS = `query($id: ID!, $after: String) { node(id: $id) { ... on PullRequestReview {
38
+ comments(first: 100, after: $after) { pageInfo { hasNextPage endCursor } nodes { ${COMMENT} } } } } }`
39
+
40
+ // A comment as read, and its author's account.
41
+ interface Authored { comment: ReviewComment; account: { login: string | null; id: number | null } }
42
+ type Page = { pageInfo?: { hasNextPage?: unknown; endCursor?: unknown }; nodes?: unknown }
43
+ type Node = { id?: unknown; author?: GraphAccount; body?: unknown; state?: unknown; createdAt?: unknown; lastEditedAt?: unknown; url?: unknown; comments?: Page }
44
+
45
+ // A pull request's comments as read, and whether it is open.
46
+ interface PullComments { open: boolean; read: Authored[] }
47
+ const comments: Lru<{ at: number } & PullComments> = lru(200)
48
+ const answers: Lru<{ at: number; reviews: CommentReview[] }> = lru(2000)
49
+
50
+ function authored(node: Node, kind: ReviewComment['kind']): Authored[] {
51
+ if (typeof node?.id !== 'string' || typeof node.body !== 'string' || typeof node.createdAt !== 'string') return []
52
+ const account = actorOf(node.author)
53
+ return [{ account, comment: { kind, id: node.id, author: account.login, body: node.body, ...typeof node.state === 'string' ? { state: node.state } : {},
54
+ createdAt: node.createdAt, ...typeof node.lastEditedAt === 'string' ? { editedAt: node.lastEditedAt } : {}, url: typeof node.url === 'string' ? node.url : '' } }]
55
+ }
56
+ const nodes = (list: unknown): Node[] => Array.isArray(list) ? list as Node[] : []
57
+ // The cursor a page ends at, where more follow it.
58
+ const next = (page: Page | undefined) => page?.pageInfo?.hasNextPage === true && typeof page.pageInfo.endCursor === 'string' ? page.pageInfo.endCursor : null
59
+
60
+ // Review `review`'s comments on lines of its diff: the page read with it,
61
+ // then those after it read of GitHub, till as many as `room` are.
62
+ async function inlineComments(review: Node, room: number, token: string, fetchImpl: typeof fetch): Promise<Authored[]> {
63
+ const read = nodes(review.comments?.nodes).flatMap(inline => authored(inline, 'review-comment'))
64
+ for (let after = typeof review.id === 'string' ? next(review.comments) : null; after !== null && read.length < room;) {
65
+ const data = await githubGraphql(REVIEW_COMMENTS, { id: review.id, after }, token, fetchImpl) as { node?: { comments?: Page } | null } | null
66
+ read.push(...nodes(data?.node?.comments?.nodes).flatMap(inline => authored(inline, 'review-comment')))
67
+ after = next(data?.node?.comments)
68
+ }
69
+ return read
70
+ }
71
+
72
+ // Pull request `number`'s comments, its reviews' and those on its diff, each
73
+ // review's all of them, and whether it is open, as kept, else read of GitHub
74
+ // a page at a time.
75
+ export async function pullComments(fullName: string, token: string, number: number, fetchImpl: typeof fetch = globalThis.fetch): Promise<PullComments> {
76
+ const key = `${fullName}\0${number}`
77
+ const known = comments.get(key)
78
+ if (known && Date.now() - known.at < KEPT_MS) return known
79
+ const [owner, name] = fullName.split('/') as [string, string]
80
+ const read: Authored[] = []
81
+ let open: boolean | null = null
82
+ // Each connection read on from its cursor till it has no more, the other
83
+ // read on without it, never again from its first page.
84
+ let cursors: { comments: string | null; reviews: string | null } = { comments: null, reviews: null }
85
+ let reading = { comments: true, reviews: true }
86
+ while ((reading.comments || reading.reviews) && read.length < MAX_COMMENTS) {
87
+ const data = await githubGraphql(QUERY, { owner, name, number, ...cursors, readComments: reading.comments, readReviews: reading.reviews }, token, fetchImpl) as
88
+ { repository?: { pullRequest?: { state?: unknown; comments?: Page; reviews?: Page } | null } | null } | null
89
+ const pull = data?.repository?.pullRequest
90
+ if (!pull) break
91
+ open ??= pull.state === 'OPEN'
92
+ for (const node of nodes(pull.comments?.nodes)) read.push(...authored(node, 'comment'))
93
+ for (const node of nodes(pull.reviews?.nodes)) {
94
+ read.push(...authored(node, 'review'))
95
+ read.push(...await inlineComments(node, MAX_COMMENTS - read.length, token, fetchImpl))
96
+ }
97
+ cursors = { comments: reading.comments ? next(pull.comments) : null, reviews: reading.reviews ? next(pull.reviews) : null }
98
+ reading = { comments: cursors.comments !== null, reviews: cursors.reviews !== null }
99
+ }
100
+ const kept = { open: open === true, read: read.slice(0, MAX_COMMENTS) }
101
+ comments.set(key, { at: Date.now(), ...kept })
102
+ return kept
103
+ }
104
+
105
+ // The parser's reviews of `input`, as kept for the same, else asked.
106
+ async function parsed(parse: NonNullable<ReviewedDependencies['parseComments']>, input: ParseCommentsInput): Promise<CommentReview[]> {
107
+ const key = createHash('sha256').update(JSON.stringify(input)).digest('base64url')
108
+ const known = answers.get(key)
109
+ if (known && Date.now() - known.at < PARSED_KEPT_MS) return known.reviews
110
+ const reviews = await parse(structuredClone(input))
111
+ if (!Array.isArray(reviews)) throw new TypeError('parseComments answered with no list')
112
+ answers.set(key, { at: Date.now(), reviews })
113
+ return reviews
114
+ }
115
+
116
+ const when = (time: unknown) => typeof time === 'number' ? time : typeof time === 'string' ? Date.parse(time) : Number.NaN
117
+ // The reviews `answer` makes that count, by the users `reviewers` names,
118
+ // each dependency's of each hash its integrity names, as of the time it
119
+ // tells, else its user's latest comment's, as last written or edited.
120
+ function counted(answer: CommentReview[], read: Authored[], reviewers: Map<string, string>): PublishedReview[] {
121
+ const latest = new Map<string, number>()
122
+ for (const { comment } of read) {
123
+ const login = comment.author?.toLowerCase()
124
+ if (login) latest.set(login, Math.max(latest.get(login) ?? 0, when(comment.createdAt) || 0, when(comment.editedAt) || 0))
125
+ }
126
+ return answer.flatMap(review => {
127
+ const login = typeof review?.user === 'string' ? review.user.toLowerCase() : ''
128
+ const reviewer = reviewers.get(login)
129
+ if (!reviewer || !latest.has(login) || !Array.isArray(review.dependencies)) return []
130
+ const time = Number.isFinite(when(review.time)) ? when(review.time) : latest.get(login)!
131
+ return review.dependencies.flatMap(dependency => {
132
+ const { name, version, integrity, patchHash, status } = dependency ?? {}
133
+ if (typeof name !== 'string' || typeof version !== 'string' || typeof integrity !== 'string' || !VERDICTS.has(status)) return []
134
+ return integrity.split(/\s+/u).filter(Boolean).map(hash => ({ package: name, version, integrity: hash, ...typeof patchHash === 'string' && patchHash ? { patchHash } : {},
135
+ basePackage: null, baseVersion: null, reviewer, login: review.user as string, status: status as ReviewVerdict, full: true, time }))
136
+ })
137
+ })
138
+ }
139
+
140
+ // The reviews pull request `number`'s comments make, as the host's parser
141
+ // reads them, of the versions still wanting one, none where it isn't open
142
+ // (the parser then not asked); `reviewers` the app's users
143
+ // the accounts are, by their logins in lower case, of those whose reviews
144
+ // count for the repository.
145
+ export function commentReviews({ fullName, number, token, parse, reviewers, fetchImpl = globalThis.fetch }: {
146
+ fullName: string; number: number; token: string; parse: NonNullable<ReviewedDependencies['parseComments']>
147
+ reviewers: (accounts: { login: string; githubId: number | null }[]) => Promise<Map<string, string>>; fetchImpl?: typeof fetch
148
+ }): CommentReviews {
149
+ return async wanting => {
150
+ try {
151
+ const { open, read } = await pullComments(fullName, token, number, fetchImpl)
152
+ if (!open) return { reviews: [], failed: false }
153
+ const accounts = new Map(read.flatMap(({ account }) => account.login ? [[account.login.toLowerCase(), { login: account.login, githubId: account.id }]] : []))
154
+ const answer = await parsed(parse, { repository: fullName, number, comments: read.map(({ comment }) => comment), dependencies: wanting })
155
+ return { reviews: counted(answer, read, await reviewers([...accounts.values()])), failed: false }
156
+ } catch (error) {
157
+ console.warn('reviewed dependencies: comments of', `${fullName}#${number}`, 'not read:', error instanceof Error ? error.message : error)
158
+ return { reviews: [], failed: true }
159
+ }
160
+ }
161
+ }