@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.51
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.
- package/LICENSE-APACHE +202 -0
- package/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +3 -0
- package/client/dependency-paths.js +36 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +115 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +357 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-compare-versions.js +172 -0
- package/common/bundle-comparison.d.ts +8 -0
- package/common/bundle-comparison.js +62 -0
- package/common/bundle-metadata.d.ts +15 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +333 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- package/common/dependency-reviews.d.ts +18 -0
- package/common/dependency-reviews.js +64 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +57 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/agent-tasks.ts +292 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/finding-detail.d.ts +3 -0
- package/common/managed/finding-detail.js +66 -0
- package/common/managed/github-agents.ts +114 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/npm-packages.d.ts +7 -0
- package/common/managed/npm-packages.js +51 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +109 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +35 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +359 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +6 -0
- package/common/managed/team-app.js +47 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/security-links.d.ts +5 -0
- package/common/security-links.js +33 -0
- package/common/server-info.ts +46 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +39 -0
- package/common/workspace-groups.js +176 -0
- package/out/activity.js +4 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2K6UN3PC.js +3 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-2ZDZWLFY.js +1 -0
- package/out/chunk-4HI2MSAV.js +17 -0
- package/out/chunk-6LNQTKAV.js +52 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7JBSSRD7.js +3 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-FMXWXDR4.js +36 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HT2SSA3V.js +1 -0
- package/out/chunk-IPAH4LKF.js +232 -0
- package/out/chunk-IYPY47MC.js +1 -0
- package/out/chunk-IZ7POWI3.js +18 -0
- package/out/chunk-JCIIBLKR.js +1 -0
- package/out/chunk-KOGNQDSJ.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PBZPYIG7.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-PWFUOLZK.js +17 -0
- package/out/chunk-PXK7EU4V.js +1 -0
- package/out/chunk-QB4CBOX2.js +1 -0
- package/out/chunk-R3NZFXVR.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TQJDYHVE.js +2 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-VDRXHIHN.js +3 -0
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WCPJDAYA.js +10 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WRAJVD35.js +1 -0
- package/out/chunk-WYLQWYLA.js +1 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +33 -0
- package/out/client-sync.js +11 -17
- package/out/compare.js +35 -0
- package/out/dialogs.js +55 -0
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +14 -10
- package/out/scan.js +2 -0
- package/out/stasis.svg +20 -0
- package/out/terminal.js +366 -39
- package/out/view.css +1 -1
- package/out/view.js +19 -144
- package/package.json +364 -59
- package/risk/actions/findings.js +161 -0
- package/risk/actions/resolve.js +219 -0
- package/risk/actions/scripts/commands.js +182 -0
- package/risk/actions/scripts/grade.js +113 -0
- package/risk/actions/scripts/packages.js +123 -0
- package/risk/actions/scripts/shell.js +169 -0
- package/risk/actions/uses.js +76 -0
- package/risk/actions/workflow.js +110 -0
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +177 -0
- package/risk/files/runs-code.js +615 -0
- package/risk/files/script.js +118 -0
- package/risk/files/stamps.js +47 -0
- package/risk/index.d.ts +254 -0
- package/risk/index.js +11 -0
- package/risk/npm/assess.js +285 -0
- package/risk/npm/combined.js +68 -0
- package/risk/npm/compare.js +257 -0
- package/risk/npm/dependencies.js +172 -0
- package/risk/npm/diff.js +79 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +445 -0
- package/risk/npm/held.js +63 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +60 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +388 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/score.js +59 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +96 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +487 -0
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +50 -42
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +44 -13
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +19 -27
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +3 -3
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +60 -33
- package/{server → server-e2e}/static.ts +31 -21
- package/{server → server-e2e}/sync-handlers.ts +64 -30
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +258 -0
- package/server-managed/agent-merges.ts +109 -0
- package/server-managed/app-agents.ts +23 -0
- package/server-managed/avatar-store.ts +13 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +115 -0
- package/server-managed/bundle-build.ts +200 -0
- package/server-managed/bundle-cache.ts +211 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +119 -0
- package/server-managed/cache-storage.ts +20 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +47 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/commit-coauthors.ts +106 -0
- package/server-managed/commit-lockfiles.ts +76 -0
- package/server-managed/comparison.ts +52 -0
- package/server-managed/config.ts +164 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1923 -0
- package/server-managed/db-neon.ts +468 -0
- package/server-managed/db-schema.ts +238 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +143 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +98 -0
- package/server-managed/dependency-reviews.ts +364 -0
- package/server-managed/github-accounts.ts +60 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +376 -0
- package/server-managed/github-errors.ts +70 -0
- package/server-managed/github-graphql.ts +39 -0
- package/server-managed/github-issue-links.ts +140 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-link-oauth.ts +69 -0
- package/server-managed/github-links.ts +204 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +238 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/github-requests.ts +136 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +76 -0
- package/server-managed/http.ts +5503 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +192 -0
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +162 -0
- package/server-managed/listed-activity.ts +39 -0
- package/server-managed/locked-packages.ts +152 -0
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +78 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +194 -0
- package/server-managed/npm-insights.ts +449 -0
- package/server-managed/npm-kept.ts +225 -0
- package/server-managed/npm-loads.ts +264 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +454 -0
- package/server-managed/npm-patch.ts +325 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +285 -0
- package/server-managed/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +75 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +434 -0
- package/server-managed/public-workspace.ts +238 -0
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +351 -0
- package/server-managed/pull-dependency-reviews.ts +334 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/queue.ts +39 -0
- package/server-managed/release-commits.ts +84 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +279 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +85 -0
- package/server-managed/repository-actions.ts +218 -0
- package/server-managed/repository-agents.ts +448 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +294 -0
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +215 -0
- package/server-managed/repository-dependency-reviews.ts +166 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-history.ts +432 -0
- package/server-managed/repository-issues.ts +195 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-pulls.ts +212 -0
- package/server-managed/repository-pulse.ts +460 -0
- package/server-managed/repository-risk-assess.ts +105 -0
- package/server-managed/repository-risk-audit.ts +79 -0
- package/server-managed/repository-risk-read.ts +170 -0
- package/server-managed/repository-risk.ts +491 -0
- package/server-managed/repository-squashes.ts +168 -0
- package/server-managed/request-timing.ts +63 -0
- package/server-managed/reviewed-dependencies.ts +149 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/scan-downloads.ts +87 -0
- package/server-managed/session.ts +132 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +71 -0
- package/server-managed/sql-postgres.ts +34 -0
- package/server-managed/sql.ts +88 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +183 -0
- package/server-managed/storage-encryption.ts +290 -0
- package/server-managed/storage-maintenance.ts +259 -0
- package/server-managed/storage-payload.ts +121 -0
- package/server-managed/storage-stores.ts +91 -0
- package/server-managed/storage.ts +82 -0
- package/server-managed/team-app.ts +134 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +180 -0
- package/server-managed/team-npm-scopes.ts +106 -0
- package/server-managed/team-reports.ts +319 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +105 -0
- package/server-managed/uploads.ts +106 -0
- package/server-managed/upstream-cache.ts +243 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +169 -0
- package/server.ts +70 -0
- package/server/index.ts +0 -480
- package/server/lifecycle.ts +0 -208
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/auth.ts +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/objstore/sign.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/sse-session.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /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
|
+
}
|