@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,460 @@
|
|
|
1
|
+
// What the code viewer's Pulse tab shows of a repository
|
|
2
|
+
// (GET /api/admin/repositories/pulse): how its default branch moves, week by
|
|
3
|
+
// week over the last year: its commits each day, the lines they add and
|
|
4
|
+
// remove, those an agent made or co-authored, who made them and when in the
|
|
5
|
+
// week, counted from its commits as the Commits tab reads and keeps them
|
|
6
|
+
// (repository-history.ts historyFrom), so that only those not kept yet are
|
|
7
|
+
// read of GitHub, a few queries an answer, the client asking again for the
|
|
8
|
+
// rest; a year past YEAR_COMMITS is counted by GitHub's statistics instead,
|
|
9
|
+
// which GitHub computes when first asked and again once the default branch
|
|
10
|
+
// moves, answering 202 till they are (told pending, StatsPending, for the
|
|
11
|
+
// client to ask again). And, since
|
|
12
|
+
// the start of the week `weeks` weeks back, and over the same days of the
|
|
13
|
+
// week `weeks` weeks earlier, the pull requests merged and opened and the
|
|
14
|
+
// issues closed and opened, how many and the latest of each.
|
|
15
|
+
import { agentOperated, githubAgent } from '../common/managed/github-agents.ts'
|
|
16
|
+
import { GithubApiError, githubJson } from './github-app.ts'
|
|
17
|
+
import { shareGithubRead } from './github-requests.ts'
|
|
18
|
+
import { coauthorsOf } from './commit-coauthors.ts'
|
|
19
|
+
import { ACTOR, type GraphAccount, type HistoryCommit, type HistoryPages, accountOf, actorOf, githubGraphql, historyFrom } from './repository-history.ts'
|
|
20
|
+
|
|
21
|
+
export const PULSE_WEEKS = [1, 4, 13, 52]
|
|
22
|
+
// What a year counted from its commits holds, its version: kept years of
|
|
23
|
+
// another are counted again.
|
|
24
|
+
export const PULSE_YEAR_VERSION = 12
|
|
25
|
+
const WEEK_MS = 7 * 24 * 60 * 60_000
|
|
26
|
+
const DAY_MS = 24 * 60 * 60_000
|
|
27
|
+
const MAX_CONTRIBUTORS = 100
|
|
28
|
+
const DIRECT_LISTED = 500
|
|
29
|
+
export const LISTED = 10
|
|
30
|
+
// Merged pull requests and closed issues are searched by when each was last
|
|
31
|
+
// updated, GitHub sorting by no merge or close: as many as this are read, and
|
|
32
|
+
// the latest merged or closed of them listed.
|
|
33
|
+
const SEARCHED = 50
|
|
34
|
+
// A year of more commits than this is counted by GitHub's statistics: read a
|
|
35
|
+
// page at a time, it would take a hundred queries. Of those not kept, as
|
|
36
|
+
// many queries are made an answer as YEAR_QUERIES, each reading on from as
|
|
37
|
+
// many commits as YEAR_STARTS.
|
|
38
|
+
export const YEAR_COMMITS = 10_000
|
|
39
|
+
const YEAR_QUERIES = 5
|
|
40
|
+
const YEAR_STARTS = 10
|
|
41
|
+
// Commits are read on from as far as this before the year, a commit's clock
|
|
42
|
+
// not always later than its parents': one dated before the year may have a
|
|
43
|
+
// parent dated in it.
|
|
44
|
+
const YEAR_SKEW_MS = 30 * DAY_MS
|
|
45
|
+
const MAX_TITLE = 1000
|
|
46
|
+
const SHA = /^[a-f\d]{40}$/u
|
|
47
|
+
|
|
48
|
+
// GitHub still computing a statistic.
|
|
49
|
+
export class StatsPending extends Error {
|
|
50
|
+
constructor() { super('github-stats-pending') }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A week: its commits each day (Sunday first) and in all, the lines they
|
|
54
|
+
// added and removed, and, counted from its commits, each day's lines
|
|
55
|
+
// (`dayLines`, `[added, removed]`), the commits that release the repository
|
|
56
|
+
// each day (`releaseDays`, release-commits.ts) and, of the rest, those an
|
|
57
|
+
// agent made or co-authored (`agentDays`), and those pushed to the branch
|
|
58
|
+
// directly (`directDays`, agent-merges.ts directCommits), which GitHub's
|
|
59
|
+
// statistics don't count.
|
|
60
|
+
export interface PulseWeek { week: number; days: number[]; commits: number; additions: number | null; deletions: number | null; dayLines?: [number, number][]
|
|
61
|
+
agentDays?: number[]; releaseDays?: number[]; directDays?: number[] }
|
|
62
|
+
// A contributor, by their login and their account's numeric id (`id`, where
|
|
63
|
+
// GitHub named it), else by the name their commits carry, and their weeks,
|
|
64
|
+
// each `[week, commits, additions, deletions]`, those with any; counted from
|
|
65
|
+
// their commits, their days too, each `[day, commits, additions, deletions,
|
|
66
|
+
// agents, releases, direct]`, `releases` those that release the repository,
|
|
67
|
+
// `agents`, of the rest, those an agent took part in (withAgent), and
|
|
68
|
+
// `direct` those pushed to the branch directly.
|
|
69
|
+
export interface PulseContributor { login: string | null; id?: number | null; name: string | null; weeks: [number, number, number, number][]
|
|
70
|
+
days?: [number, number, number, number, number, number, number][] }
|
|
71
|
+
// A commit pushed to the branch directly, as Pulse lists it: its id, when it
|
|
72
|
+
// was committed, its subject, its author, by account, else name, and what it
|
|
73
|
+
// is: a release, else one an agent took part in, else one without.
|
|
74
|
+
export interface PulseCommit { sha: string; at: number; subject: string; login: string | null; id: number | null; name: string | null
|
|
75
|
+
kind: 'agent' | 'without' | 'release' }
|
|
76
|
+
// Commits by day of the week and hour (`punch[day][hour]`, Sunday first), in
|
|
77
|
+
// each commit's own time zone, and, counted from its commits, each period's
|
|
78
|
+
// (`punches`, by its weeks); `lines` false where GitHub counts none, null
|
|
79
|
+
// while it is counting them; counted from its commits, the latest pushed to
|
|
80
|
+
// the branch directly (`directCommits`, as many as DIRECT_LISTED); and,
|
|
81
|
+
// counted from its commits read no further back than a period needed, when
|
|
82
|
+
// from it is counted whole (`from`), absent for the year whole.
|
|
83
|
+
export interface PulseStats { weeks: PulseWeek[]; contributors: PulseContributor[]; punch: number[][]; punches?: Record<number, number[][]>; lines: boolean | null
|
|
84
|
+
directCommits?: PulseCommit[]; from?: number }
|
|
85
|
+
// The lines added and removed a week, and each contributor's, as GitHub's
|
|
86
|
+
// statistics count them, `lines` false where it counts none.
|
|
87
|
+
export interface PulseLines { weeks: [number, number, number][]; contributors: { login: string; weeks: [number, number, number][] }[]; lines: boolean }
|
|
88
|
+
// The days (`from`, `to`, both days, UTC; `to` none, on) and the authors (GitHub
|
|
89
|
+
// logins, any of them) a period's pull requests and issues are narrowed to.
|
|
90
|
+
export interface PulseNarrowing { from: number; to: number | null; authors: string[] }
|
|
91
|
+
// The year's commits read so far: all of them, or as far back as asked
|
|
92
|
+
// (`commits`, `from`), else how many (`read`), and whether past
|
|
93
|
+
// YEAR_COMMITS (`tooMany`).
|
|
94
|
+
export type YearCommits = { complete: true; commits: HistoryCommit[]; passed: HistoryCommit[]; from: number | null } | { complete: false; read: number; tooMany: boolean }
|
|
95
|
+
|
|
96
|
+
const count = (value: unknown) => Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : 0
|
|
97
|
+
const timeOf = (value: unknown) => {
|
|
98
|
+
const at = typeof value === 'string' ? Date.parse(value) : Number.NaN
|
|
99
|
+
return Number.isSafeInteger(at) ? at : null
|
|
100
|
+
}
|
|
101
|
+
const repoApi = (fullName: string) => `https://api.github.com/repos/${fullName.split('/').map(encodeURIComponent).join('/')}`
|
|
102
|
+
const emptyPunch = () => Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => 0))
|
|
103
|
+
|
|
104
|
+
// One statistic: its rows once GitHub has computed it, 'pending' while it
|
|
105
|
+
// computes them (202), none for a repository with no commits (204, no body),
|
|
106
|
+
// 'too-large' where it counts none (lines past 10,000 commits, 422).
|
|
107
|
+
function statistic(fullName: string, token: string | null, kind: string, fetchImpl: typeof fetch) {
|
|
108
|
+
// Stats and lines both need contributors. Share by the original transport;
|
|
109
|
+
// the per-call status wrapper below would otherwise give each its own read.
|
|
110
|
+
return shareGithubRead(fetchImpl, JSON.stringify(['statistic', fullName, token, kind]), () => readStatistic(fullName, token, kind, fetchImpl))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function readStatistic(fullName: string, token: string | null, kind: string, fetchImpl: typeof fetch): Promise<unknown[] | 'pending' | 'too-large'> {
|
|
114
|
+
let status = 0
|
|
115
|
+
const answered: typeof fetch = async (input, init) => {
|
|
116
|
+
const res = await fetchImpl(input, init)
|
|
117
|
+
status = res.status
|
|
118
|
+
return res
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const data = await githubJson(`${repoApi(fullName)}/stats/${kind}`, token, answered)
|
|
122
|
+
if (status === 202) return 'pending'
|
|
123
|
+
if (!Array.isArray(data)) throw new GithubApiError(502, 'github-malformed')
|
|
124
|
+
return data
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (status === 202) return 'pending'
|
|
127
|
+
if (status === 204) return []
|
|
128
|
+
if (err instanceof GithubApiError && err.message === 'github-status-422') return 'too-large'
|
|
129
|
+
throw err
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// `kinds` of GitHub's statistics, each asked once, together; any still
|
|
134
|
+
// computed, StatsPending.
|
|
135
|
+
async function statistics(fullName: string, token: string | null, kinds: string[], fetchImpl: typeof fetch) {
|
|
136
|
+
const read = await Promise.all(kinds.map(kind => statistic(fullName, token, kind, fetchImpl)))
|
|
137
|
+
if (read.includes('pending')) throw new StatsPending()
|
|
138
|
+
return read.map(rows => rows === 'too-large' ? null : rows as unknown[])
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type ActivityRow = { week?: unknown; days?: unknown; total?: unknown }
|
|
142
|
+
type ContributorRow = { author?: { login?: unknown; id?: unknown } | null; weeks?: { w?: unknown; a?: unknown; d?: unknown; c?: unknown }[] }
|
|
143
|
+
|
|
144
|
+
// The default branch's commits over the last year as GitHub's statistics
|
|
145
|
+
// count them, for a year past YEAR_COMMITS: a week a row, the oldest first,
|
|
146
|
+
// its commits each day (Sunday first) and in all; its contributors (as many
|
|
147
|
+
// as MAX_CONTRIBUTORS, the most commits that year first) and their weeks;
|
|
148
|
+
// and its commits by day of the week and hour, in each commit's own time
|
|
149
|
+
// zone. Lines are GitHub's to count apart (repositoryLines).
|
|
150
|
+
export async function repositoryStats(fullName: string, token: string | null, fetchImpl: typeof fetch = globalThis.fetch): Promise<PulseStats> {
|
|
151
|
+
const [activityRows = null, contributorRows = null, punchRows = null] = await statistics(fullName, token, ['commit_activity', 'contributors', 'punch_card'], fetchImpl)
|
|
152
|
+
const weeks = ((activityRows ?? []) as ActivityRow[]).flatMap(row => {
|
|
153
|
+
if (!Number.isSafeInteger(row?.week)) return []
|
|
154
|
+
const days = Array.from({ length: 7 }, (_, day) => count(Array.isArray(row.days) ? row.days[day] : 0))
|
|
155
|
+
return [{ week: (row.week as number) * 1000, days, commits: count(row.total), additions: null, deletions: null }]
|
|
156
|
+
}).toSorted((a, b) => a.week - b.week)
|
|
157
|
+
const first = weeks[0]?.week ?? Number.POSITIVE_INFINITY
|
|
158
|
+
const contributors = ((contributorRows ?? []) as ContributorRow[]).flatMap(row => {
|
|
159
|
+
const { login, id } = accountOf(row?.author)
|
|
160
|
+
const theirs = (row?.weeks ?? []).flatMap(item => {
|
|
161
|
+
const week = count(item?.w) * 1000
|
|
162
|
+
return week >= first && count(item?.c) > 0 ? [[week, count(item?.c), 0, 0] as [number, number, number, number]] : []
|
|
163
|
+
})
|
|
164
|
+
return login && theirs.length > 0 ? [{ login, id, name: null, weeks: theirs, commits: theirs.reduce((sum, item) => sum + item[1], 0) }] : []
|
|
165
|
+
}).toSorted((a, b) => b.commits - a.commits).slice(0, MAX_CONTRIBUTORS).map(({ login, id, name, weeks: theirs }) => ({ login, id, name, weeks: theirs }))
|
|
166
|
+
const punch = emptyPunch()
|
|
167
|
+
for (const row of (punchRows ?? []) as unknown[][]) {
|
|
168
|
+
if (Array.isArray(row) && Number.isSafeInteger(row[0]) && Number.isSafeInteger(row[1]) && punch[row[0] as number]?.[row[1] as number] !== undefined) {
|
|
169
|
+
punch[row[0] as number]![row[1] as number] = count(row[2])
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { weeks, contributors, punch, lines: null }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The lines added and removed each week since `since`, and each
|
|
176
|
+
// contributor's, as GitHub's statistics count them; none, `lines` false,
|
|
177
|
+
// where it counts none (past 10,000 commits, where it counts each
|
|
178
|
+
// contributor's as none too).
|
|
179
|
+
export async function repositoryLines(fullName: string, token: string | null, since: number, fetchImpl: typeof fetch = globalThis.fetch): Promise<PulseLines> {
|
|
180
|
+
const [frequency = null, contributorRows = null] = await statistics(fullName, token, ['code_frequency', 'contributors'], fetchImpl)
|
|
181
|
+
if (frequency === null) return { weeks: [], contributors: [], lines: false }
|
|
182
|
+
const weeks = frequency.flatMap(row => Array.isArray(row) && Number.isSafeInteger(row[0]) && (row[0] as number) * 1000 >= since
|
|
183
|
+
? [[(row[0] as number) * 1000, count(row[1]), count(Math.abs(Number(row[2])))] as [number, number, number]] : [])
|
|
184
|
+
const contributors = ((contributorRows ?? []) as ContributorRow[]).flatMap(row => {
|
|
185
|
+
const { login } = accountOf(row?.author)
|
|
186
|
+
const theirs = (row?.weeks ?? []).flatMap(item => count(item?.w) * 1000 >= since && count(item?.a) + count(item?.d) > 0
|
|
187
|
+
? [[count(item?.w) * 1000, count(item?.a), count(item?.d)] as [number, number, number]] : [])
|
|
188
|
+
return login && theirs.length > 0 ? [{ login, weeks: theirs }] : []
|
|
189
|
+
})
|
|
190
|
+
return { weeks, contributors, lines: true }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// The default branch's head, null for a repository with none.
|
|
194
|
+
export async function defaultHead(fullName: string, token: string, fetchImpl: typeof fetch = globalThis.fetch): Promise<string | null> {
|
|
195
|
+
const [owner, name] = fullName.split('/') as [string, string]
|
|
196
|
+
const data = await githubGraphql('query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { defaultBranchRef { target { oid } } } }',
|
|
197
|
+
{ owner, name }, token, fetchImpl) as { repository?: { defaultBranchRef?: { target?: { oid?: unknown } | null } | null } | null } | null
|
|
198
|
+
if (!data?.repository) throw new GithubApiError(404, 'github-not-found')
|
|
199
|
+
const oid = data.repository.defaultBranchRef?.target?.oid
|
|
200
|
+
return typeof oid === 'string' && SHA.test(oid) ? oid : null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The commits from `head` made since `since`, as the Commits tab reads and
|
|
204
|
+
// keeps them (`pages`, historyFrom): on from the parents of those since (or
|
|
205
|
+
// as much as YEAR_SKEW_MS before), not yet listed, YEAR_STARTS at a time,
|
|
206
|
+
// till none is left; the pages kept read
|
|
207
|
+
// of the store, those not of GitHub, as many queries of them as
|
|
208
|
+
// YEAR_QUERIES, the rest left for the next to ask (`complete` false, how
|
|
209
|
+
// many are `read`); more than YEAR_COMMITS, too many (`tooMany`). Read
|
|
210
|
+
// whole, those it was read on past besides (`passed`): undated, or dated
|
|
211
|
+
// before it, a clock behind, the branch's commits since beneath them. Read
|
|
212
|
+
// no further than `need` asks, those since it read whole (`complete`, `from`
|
|
213
|
+
// the newest of those read whose parents are not, every commit after it
|
|
214
|
+
// read; null, the year read whole).
|
|
215
|
+
export async function yearCommits(fullName: string, token: string, head: string, since: number, pages: HistoryPages,
|
|
216
|
+
fetchImpl: typeof fetch = globalThis.fetch, need = since): Promise<YearCommits> {
|
|
217
|
+
const found = new Map<string, HistoryCommit>()
|
|
218
|
+
const tried = new Set<string>()
|
|
219
|
+
const within = (commit: HistoryCommit) => commit.committedAt === null || commit.committedAt >= since - YEAR_SKEW_MS
|
|
220
|
+
const dated = (commit: HistoryCommit) => commit.committedAt !== null && commit.committedAt >= since
|
|
221
|
+
const read = () => [...found.values()].filter(dated).length
|
|
222
|
+
const passed = () => [...found.values()].filter(commit => !dated(commit) && within(commit))
|
|
223
|
+
let queries = 0
|
|
224
|
+
let starts = [head]
|
|
225
|
+
while (starts.length > 0) {
|
|
226
|
+
// Those not read yet, the head before any, else the parents of those
|
|
227
|
+
// read, no later than the newest of them (but for a clock behind): read
|
|
228
|
+
// whole after it.
|
|
229
|
+
const unread = new Set(starts)
|
|
230
|
+
const from = found.size === 0 ? Number.POSITIVE_INFINITY : Math.max(Number.NEGATIVE_INFINITY, ...[...found.values()]
|
|
231
|
+
.filter(commit => commit.parents.some(sha => unread.has(sha))).map(commit => commit.committedAt ?? Number.POSITIVE_INFINITY))
|
|
232
|
+
if (need > since && from < need) return { complete: true, commits: [...found.values()].filter(dated), passed: passed(), from }
|
|
233
|
+
if (queries >= YEAR_QUERIES) return { complete: false, read: read(), tooMany: false }
|
|
234
|
+
const batch = starts.slice(0, YEAR_STARTS)
|
|
235
|
+
for (const sha of batch) tried.add(sha)
|
|
236
|
+
const got = await historyFrom(fullName, token, batch, pages, fetchImpl)
|
|
237
|
+
if (got.read > 0) queries++
|
|
238
|
+
for (const commit of got.commits) if (!found.has(commit.sha)) found.set(commit.sha, commit)
|
|
239
|
+
if (read() > YEAR_COMMITS) return { complete: false, read: read(), tooMany: true }
|
|
240
|
+
starts = [...new Set([...found.values()].filter(within).flatMap(commit => commit.parents))].filter(sha => !found.has(sha) && !tried.has(sha))
|
|
241
|
+
}
|
|
242
|
+
return { complete: true, commits: [...found.values()].filter(dated), passed: passed(), from: null }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Whether an author is a GitHub App's bot: by its login (`<app>[bot]`, as a
|
|
246
|
+
// commit's no-reply address names it, github-accounts.ts commitAccount),
|
|
247
|
+
// else, for one GitHub names no account for, by the name it commits as.
|
|
248
|
+
const bot = (login: string | null | undefined, name: string | null | undefined) => /\[bot\]$/iu.test(login ?? name ?? '')
|
|
249
|
+
|
|
250
|
+
// Whether an agent made `commit` or co-authored it: its author one of
|
|
251
|
+
// GitHub's agents (common/managed/github-agents.ts githubAgent), or any of
|
|
252
|
+
// its co-authors an agent (commit-coauthors.ts coauthorsOf), as Commits
|
|
253
|
+
// marks them, or any bot, whichever GitHub App's; or one an agent's pull request brought (`merges`,
|
|
254
|
+
// agent-merges.ts agentCommits); or one an account agents work as pushed to
|
|
255
|
+
// the branch directly (`direct`, github-agents.ts agentOperated).
|
|
256
|
+
const withAgent = (commit: HistoryCommit, merges: ReadonlySet<string>, direct: ReadonlySet<string>) => merges.has(commit.sha)
|
|
257
|
+
|| (direct.has(commit.sha) && agentOperated(commit.authorLogin, commit.authorId ?? null))
|
|
258
|
+
|| githubAgent(commit.authorLogin, commit.authorId ?? null, commit.authorName) !== null || bot(commit.authorLogin, commit.authorName)
|
|
259
|
+
|| coauthorsOf(commit).some(coauthor => Boolean(coauthor.agent) || bot(coauthor.login, coauthor.name))
|
|
260
|
+
|
|
261
|
+
// The year from `first` (pulseSince(52, …)) of `commits`, as GitHub's
|
|
262
|
+
// statistics count them, merges left out: a week a row, its commits each day
|
|
263
|
+
// (UTC) and in all and the lines they added and removed, in all and each day,
|
|
264
|
+
// those that release the repository (`releases`, release-commits.ts) and,
|
|
265
|
+
// of the rest, those an agent made or co-authored, merged from an agent's
|
|
266
|
+
// pull request (`merges`) or pushed directly as an account agents work as
|
|
267
|
+
// (`direct`, withAgent), each day;
|
|
268
|
+
// its contributors,
|
|
269
|
+
// by account (its id, by the login it committed as first listed, the newest,
|
|
270
|
+
// whatever it was renamed from; a login's commits read before ids were, the
|
|
271
|
+
// one account's that committed as it, where only one did), else login, each
|
|
272
|
+
// matched as GitHub matches logins, regardless of case, else name (as many
|
|
273
|
+
// as MAX_CONTRIBUTORS, the most commits first),
|
|
274
|
+
// and their weeks and days, each day's releases and commits with agents too;
|
|
275
|
+
// and its commits by day of the week and hour, in each author's own time
|
|
276
|
+
// zone (else UTC, where GitHub told none), the year's and each period's
|
|
277
|
+
// (PULSE_WEEKS, its last weeks).
|
|
278
|
+
export function commitStats(commits: HistoryCommit[], first: number, merges: ReadonlySet<string> = new Set(), releases: ReadonlySet<string> = new Set(),
|
|
279
|
+
direct: ReadonlySet<string> = new Set()): PulseStats {
|
|
280
|
+
const weeks = Array.from({ length: 52 }, (_, at) => ({ week: first + at * WEEK_MS, days: Array.from({ length: 7 }, () => 0), commits: 0, additions: 0, deletions: 0,
|
|
281
|
+
dayLines: Array.from({ length: 7 }, () => [0, 0] as [number, number]), agentDays: Array.from({ length: 7 }, () => 0), releaseDays: Array.from({ length: 7 }, () => 0),
|
|
282
|
+
directDays: Array.from({ length: 7 }, () => 0) }))
|
|
283
|
+
type Counts = Map<number, number[]>
|
|
284
|
+
const people = new Map<string, { login: string | null; logins: Set<string>; id: number | null; name: string | null; weeks: Counts; days: Counts; commits: number }>()
|
|
285
|
+
const punches = Object.fromEntries(PULSE_WEEKS.map(span => [span, emptyPunch()]))
|
|
286
|
+
// Each commit's author, by the key their commits are counted by, and of a
|
|
287
|
+
// login an account took on, that account (`merged`).
|
|
288
|
+
const keys = new Map<string, string>()
|
|
289
|
+
const merged = new Map<string, { login: string | null; id: number | null; name: string | null }>()
|
|
290
|
+
const add = (counts: Counts, at: number, made: number[]) => counts.set(at, made.map((value, index) => (counts.get(at)?.[index] ?? 0) + value))
|
|
291
|
+
for (const commit of commits) {
|
|
292
|
+
if (commit.parents.length > 1 || commit.committedAt === null || commit.committedAt < first) continue
|
|
293
|
+
const at = Math.floor((commit.committedAt - first) / WEEK_MS)
|
|
294
|
+
const week = weeks[at]
|
|
295
|
+
if (!week) continue
|
|
296
|
+
const [additions, deletions] = [commit.additions ?? 0, commit.deletions ?? 0]
|
|
297
|
+
const weekday = new Date(commit.committedAt).getUTCDay()
|
|
298
|
+
week.days[weekday]!++
|
|
299
|
+
week.dayLines[weekday]![0] += additions
|
|
300
|
+
week.dayLines[weekday]![1] += deletions
|
|
301
|
+
week.commits++
|
|
302
|
+
week.additions += additions
|
|
303
|
+
week.deletions += deletions
|
|
304
|
+
const release = releases.has(commit.sha)
|
|
305
|
+
const agent = !release && withAgent(commit, merges, direct)
|
|
306
|
+
if (release) week.releaseDays[weekday]!++
|
|
307
|
+
if (direct.has(commit.sha)) week.directDays[weekday]!++
|
|
308
|
+
if (agent) week.agentDays[weekday]!++
|
|
309
|
+
const local = new Date(commit.authoredAt != null && commit.authorZone != null ? commit.authoredAt + commit.authorZone * 60_000 : commit.committedAt)
|
|
310
|
+
for (const span of PULSE_WEEKS) if (at >= 52 - span) punches[span]![local.getUTCDay()]![local.getUTCHours()]!++
|
|
311
|
+
const key = commit.authorLogin && commit.authorId != null ? `#${commit.authorId}` : commit.authorLogin ? `@${commit.authorLogin.toLowerCase()}`
|
|
312
|
+
: commit.authorName ? `=${commit.authorName}` : null
|
|
313
|
+
if (!key) continue
|
|
314
|
+
keys.set(commit.sha, key)
|
|
315
|
+
const person = people.get(key) ?? { login: commit.authorLogin, logins: new Set<string>(), id: commit.authorId ?? null, name: commit.authorName,
|
|
316
|
+
weeks: new Map(), days: new Map(), commits: 0 }
|
|
317
|
+
if (commit.authorLogin) person.logins.add(commit.authorLogin.toLowerCase())
|
|
318
|
+
add(person.weeks, week.week, [1, additions, deletions])
|
|
319
|
+
add(person.days, Math.floor(commit.committedAt / DAY_MS) * DAY_MS, [1, additions, deletions, agent ? 1 : 0, release ? 1 : 0, direct.has(commit.sha) ? 1 : 0])
|
|
320
|
+
person.commits++
|
|
321
|
+
people.set(key, person)
|
|
322
|
+
}
|
|
323
|
+
// A login one account committed as, those of its commits read before ids
|
|
324
|
+
// were that account's; one more did, theirs apart, no one's.
|
|
325
|
+
const accounts = Map.groupBy([...people.values()].filter(person => person.id !== null).flatMap(person => [...person.logins].map(login => ({ login, person }))),
|
|
326
|
+
({ login }) => login)
|
|
327
|
+
for (const [key, person] of people) {
|
|
328
|
+
const [owner, ...others] = person.login !== null && person.id === null ? accounts.get(person.login.toLowerCase()) ?? [] : []
|
|
329
|
+
if (!owner || others.length > 0) continue
|
|
330
|
+
const account = owner.person
|
|
331
|
+
for (const [week, counts] of person.weeks) add(account.weeks, week, counts)
|
|
332
|
+
for (const [day, counts] of person.days) add(account.days, day, counts)
|
|
333
|
+
account.commits += person.commits
|
|
334
|
+
people.delete(key)
|
|
335
|
+
merged.set(key, account)
|
|
336
|
+
}
|
|
337
|
+
const rows = (counts: Counts) => [...counts].toSorted(([a], [b]) => a - b).map(([at, made]) => [at, ...made])
|
|
338
|
+
const contributors = [...people.values()].toSorted((a, b) => b.commits - a.commits).slice(0, MAX_CONTRIBUTORS).map(({ login, id, name, weeks: theirs, days }) => ({
|
|
339
|
+
login, id, name, weeks: rows(theirs) as [number, number, number, number][], days: rows(days) as [number, number, number, number, number, number, number][] }))
|
|
340
|
+
// Each pushed directly by the contributor its author's commits are counted
|
|
341
|
+
// as, whatever account it named.
|
|
342
|
+
const directCommits = commits.filter(commit => direct.has(commit.sha) && commit.committedAt !== null && commit.committedAt >= first)
|
|
343
|
+
.toSorted((a, b) => b.committedAt! - a.committedAt!).slice(0, DIRECT_LISTED).map((commit): PulseCommit => {
|
|
344
|
+
const { sha, committedAt, subject, authorLogin, authorId, authorName } = commit
|
|
345
|
+
const author = people.get(keys.get(sha) ?? '') ?? merged.get(keys.get(sha) ?? '') ?? { login: authorLogin, id: authorId ?? null, name: authorName }
|
|
346
|
+
const kind = releases.has(sha) ? 'release' : withAgent(commit, merges, direct) ? 'agent' : 'without'
|
|
347
|
+
return { sha, at: committedAt!, subject, login: author.login, id: author.id, name: author.name, kind }
|
|
348
|
+
})
|
|
349
|
+
return { weeks, contributors, punch: punches[52]!, punches, lines: true, directCommits }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// The year's statistics as a period of `weeks` weeks shows them: when its
|
|
353
|
+
// commits are made, the period's where they were counted from its commits
|
|
354
|
+
// (`periodPunch`), else the year's or more, as GitHub's statistics count it;
|
|
355
|
+
// the commits pushed to the branch directly, the period's alone.
|
|
356
|
+
export function periodStats(stats: PulseStats, weeks: number) {
|
|
357
|
+
const { punches, directCommits, ...rest } = stats
|
|
358
|
+
const punch = punches?.[weeks]
|
|
359
|
+
const since = stats.weeks.at(-weeks)?.week ?? Number.NEGATIVE_INFINITY
|
|
360
|
+
return { ...rest, punch: punch ?? rest.punch, periodPunch: Boolean(punch), ...directCommits ? { directCommits: directCommits.filter(commit => commit.at >= since) } : {} }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// GitHub's statistics of a year past YEAR_COMMITS (repositoryStats) with
|
|
364
|
+
// its `lines` added in, each week's and each contributor's, by login; null
|
|
365
|
+
// while GitHub is counting them, `lines` null.
|
|
366
|
+
export function withLines(stats: PulseStats, lines: PulseLines | null): PulseStats {
|
|
367
|
+
if (!lines) return { ...stats, lines: null }
|
|
368
|
+
if (!lines.lines) return { ...stats, lines: false }
|
|
369
|
+
const weekly = new Map(lines.weeks.map(([week, additions, deletions]) => [week, [additions, deletions]]))
|
|
370
|
+
const theirs = new Map(lines.contributors.map(person => [person.login, new Map(person.weeks.map(([week, additions, deletions]) => [week, [additions, deletions]]))]))
|
|
371
|
+
return {
|
|
372
|
+
...stats,
|
|
373
|
+
weeks: stats.weeks.map(week => ({ ...week, additions: weekly.get(week.week)?.[0] ?? 0, deletions: weekly.get(week.week)?.[1] ?? 0 })),
|
|
374
|
+
contributors: stats.contributors.map(person => ({ ...person, weeks: person.weeks.map(([week, made]) => {
|
|
375
|
+
const added = person.login ? theirs.get(person.login)?.get(week) : undefined
|
|
376
|
+
return [week, made, added?.[0] ?? 0, added?.[1] ?? 0] as [number, number, number, number]
|
|
377
|
+
}) })),
|
|
378
|
+
lines: true,
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Where a period of `weeks` weeks starts, as GitHub's statistics count weeks:
|
|
383
|
+
// the Sunday (UTC) that many weeks back, this week the last of them.
|
|
384
|
+
export function pulseSince(weeks: number, now: number) {
|
|
385
|
+
const today = Math.floor(now / DAY_MS) * DAY_MS
|
|
386
|
+
const sunday = today - new Date(today).getUTCDay() * DAY_MS
|
|
387
|
+
return sunday - (weeks - 1) * WEEK_MS
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const SEARCH = (name: string, fragment: string, first = 0) => `${name}: search(type: ISSUE, query: $${name}, first: ${Math.max(first, 1)}) {
|
|
391
|
+
issueCount${first > 0 ? ` nodes { ...${fragment} }` : ''}
|
|
392
|
+
}`
|
|
393
|
+
const ACTIVITY_QUERY = `query($merged: String!, $mergedBefore: String!, $opened: String!, $openedBefore: String!,
|
|
394
|
+
$closed: String!, $closedBefore: String!, $created: String!, $createdBefore: String!) {
|
|
395
|
+
${SEARCH('merged', 'pull', SEARCHED)}
|
|
396
|
+
${SEARCH('mergedBefore', 'pull')}
|
|
397
|
+
${SEARCH('opened', 'pull', LISTED)}
|
|
398
|
+
${SEARCH('openedBefore', 'pull')}
|
|
399
|
+
${SEARCH('closed', 'issue', SEARCHED)}
|
|
400
|
+
${SEARCH('closedBefore', 'issue')}
|
|
401
|
+
${SEARCH('created', 'issue', LISTED)}
|
|
402
|
+
${SEARCH('createdBefore', 'issue')}
|
|
403
|
+
}
|
|
404
|
+
fragment pull on PullRequest { number title pullState: state isDraft createdAt mergedAt author { ${ACTOR} } }
|
|
405
|
+
fragment issue on Issue { number title issueState: state stateReason createdAt closedAt author { ${ACTOR} } }`
|
|
406
|
+
|
|
407
|
+
type SearchNode = { number?: unknown; title?: unknown; pullState?: unknown; issueState?: unknown; isDraft?: unknown; stateReason?: unknown
|
|
408
|
+
createdAt?: unknown; mergedAt?: unknown; closedAt?: unknown; author?: GraphAccount } | null
|
|
409
|
+
type Search = { issueCount?: unknown; nodes?: SearchNode[] } | null | undefined
|
|
410
|
+
const REASONS: Record<string, string> = { COMPLETED: 'completed', NOT_PLANNED: 'not-planned', DUPLICATE: 'duplicate' }
|
|
411
|
+
|
|
412
|
+
// An issue or pull request a search lists, as the popup's preview of it
|
|
413
|
+
// takes one, `at` when it was merged, opened or closed.
|
|
414
|
+
function listed(node: SearchNode, at: unknown) {
|
|
415
|
+
if (!node || !Number.isSafeInteger(node.number) || (node.number as number) <= 0) return null
|
|
416
|
+
const title = typeof node.title === 'string' ? node.title.trim().slice(0, MAX_TITLE) : ''
|
|
417
|
+
const author = actorOf(node.author)
|
|
418
|
+
const common = { number: node.number as number, title, author: author.login, authorId: author.id, at: timeOf(at), createdAt: timeOf(node.createdAt) }
|
|
419
|
+
return node.pullState === undefined
|
|
420
|
+
? { ...common, kind: 'issues', state: node.issueState === 'OPEN' ? 'open' : 'closed', reason: REASONS[String(node.stateReason)] ?? null }
|
|
421
|
+
: { ...common, kind: 'pulls', state: node.pullState === 'MERGED' ? 'merged' : node.pullState === 'OPEN' ? 'open' : 'closed', draft: node.isDraft === true }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// The pull requests merged and opened and the issues closed and opened since
|
|
425
|
+
// the start of the week `weeks` weeks back (`since`), and over the same days
|
|
426
|
+
// of the week `weeks` weeks earlier (`previous`, its count: a week's Sunday to
|
|
427
|
+
// Thursday beside the week before's, not the days just before, whose days of
|
|
428
|
+
// the week differ), the latest of each listed, as many as LISTED: those
|
|
429
|
+
// opened as GitHub sorts them, those merged or closed the latest of the
|
|
430
|
+
// SEARCHED most recently updated, which are the latest of all where the
|
|
431
|
+
// period holds no more, or the last listed was merged or closed after the
|
|
432
|
+
// SEARCHED-th was last updated. A search counts by the day (UTC). Narrowed
|
|
433
|
+
// (`narrowing`), those of its days from `from` to `to` (both days, UTC;
|
|
434
|
+
// none, on), and those its `authors` (GitHub logins) opened, any of them.
|
|
435
|
+
export async function repositoryActivity(fullName: string, token: string, weeks: number, now: number = Date.now(), fetchImpl: typeof fetch = globalThis.fetch,
|
|
436
|
+
narrowing: PulseNarrowing | null = null) {
|
|
437
|
+
const since = pulseSince(weeks, now)
|
|
438
|
+
const back = weeks * WEEK_MS
|
|
439
|
+
const day = (at: number) => new Date(at).toISOString().slice(0, 10)
|
|
440
|
+
const current = narrowing?.to == null ? `>=${day(narrowing?.from ?? since)}` : `${day(narrowing.from)}..${day(narrowing.to)}`
|
|
441
|
+
const previous = `${day(since - back)}..${day(now - back)}`
|
|
442
|
+
const repo = `repo:${fullName}`
|
|
443
|
+
// An app's login (`name[bot]`) is searched for as GitHub names its app.
|
|
444
|
+
const by = (narrowing?.authors ?? []).map(login => ` author:${login.endsWith('[bot]') ? `app/${login.slice(0, -5)}` : login}`).join('')
|
|
445
|
+
const asked = (what: string, range: string, sort: string) => `${repo} ${what}${range}${by} sort:${sort}`
|
|
446
|
+
const kinds = {
|
|
447
|
+
merged: ['is:pr is:merged merged:', 'updated-desc'], opened: ['is:pr created:', 'created-desc'],
|
|
448
|
+
closed: ['is:issue is:closed closed:', 'updated-desc'], created: ['is:issue created:', 'created-desc'],
|
|
449
|
+
} as const
|
|
450
|
+
const variables = Object.fromEntries(Object.entries(kinds).flatMap(([name, [what, sort]]) => [[name, asked(what, current, sort)],
|
|
451
|
+
[`${name}Before`, asked(what, previous, sort)]]))
|
|
452
|
+
const data = await githubGraphql(ACTIVITY_QUERY, variables, token, fetchImpl) as Record<string, Search> | null
|
|
453
|
+
const at = { merged: 'mergedAt', opened: 'createdAt', closed: 'closedAt', created: 'createdAt' } as const
|
|
454
|
+
const told = Object.fromEntries((Object.keys(kinds) as (keyof typeof kinds)[]).map(name => [name, {
|
|
455
|
+
count: count(data?.[name]?.issueCount),
|
|
456
|
+
previous: count(data?.[`${name}Before`]?.issueCount),
|
|
457
|
+
items: (data?.[name]?.nodes ?? []).flatMap(node => listed(node, node?.[at[name]]) ?? []).toSorted((a, b) => (b.at ?? 0) - (a.at ?? 0)).slice(0, LISTED),
|
|
458
|
+
}]))
|
|
459
|
+
return { since, previousSince: since - back, ...told }
|
|
460
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// A package's risk line, as a repository risk scan tells it
|
|
2
|
+
// (repository-risk.ts): its version's document read and assessed, as risk
|
|
3
|
+
// tells one npm removed where npm has none; and how a failure is told.
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
5
|
+
import { NpmRiskError, SEVERITIES, type Severity } from '../risk/index.js'
|
|
6
|
+
import { GithubApiError } from './github-errors.ts'
|
|
7
|
+
import { NpmPackageError, type NpmVersionDocument } from './npm-packages.ts'
|
|
8
|
+
import type { RepositoryRiskLine } from './repository-risk.ts'
|
|
9
|
+
|
|
10
|
+
// An assessment refused as busy (`npm-busy`, by npm-risk.ts or the npm reads
|
|
11
|
+
// beneath it), or that npm asked to ask later (risk's `upstream-busy`, as its
|
|
12
|
+
// downloads API answers past a burst of requests), is asked again after a
|
|
13
|
+
// wait, up to this many times.
|
|
14
|
+
export const BUSY_RETRIES = 20
|
|
15
|
+
export const BUSY_WAIT_MS = 1000
|
|
16
|
+
|
|
17
|
+
// How long a package's risk line, or a lockfile's advisories, is told as it
|
|
18
|
+
// is: after it, a risk line is told stale and assessed again, as risk tells
|
|
19
|
+
// again what of a package changes in time, and advisories asked again, as npm
|
|
20
|
+
// publishes them (repository-risk.ts).
|
|
21
|
+
export const RISK_KEPT_MS = 60 * 60_000
|
|
22
|
+
|
|
23
|
+
// The most severe of `findings`, or null for none.
|
|
24
|
+
export function highestSeverity(findings: readonly { severity: Severity }[]): Severity | null {
|
|
25
|
+
return SEVERITIES.find(severity => findings.some(finding => finding.severity === severity)) ?? null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Why a step failed, as a line says it: an error's code, and for a lockfile
|
|
29
|
+
// refused, what of it its reader refused.
|
|
30
|
+
export type RiskFailure = { error: string; detail?: string }
|
|
31
|
+
type RiskLine = Extract<RepositoryRiskLine, { type: 'risk' }>
|
|
32
|
+
|
|
33
|
+
// What an error is known by: a GitHub or npm failure's code, or `failed`;
|
|
34
|
+
// GitHub not reached with what failed (githubUnreachable), while `doing` what
|
|
35
|
+
// of a repository, and risk's with what it asked of npm and how that failed,
|
|
36
|
+
// where they say.
|
|
37
|
+
export function failureOf(error: unknown, doing?: string): RiskFailure {
|
|
38
|
+
if (error instanceof GithubApiError && error.detail) return { error: error.message, detail: doing ? `${doing}: ${error.detail}` : error.detail }
|
|
39
|
+
if (error instanceof GithubApiError || error instanceof NpmPackageError) return { error: error.message }
|
|
40
|
+
if (!(error instanceof NpmRiskError)) return { error: 'failed' }
|
|
41
|
+
return error.message === error.code ? { error: error.code } : { error: error.code, detail: error.message }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const isBusy = (error: unknown) => (error instanceof NpmPackageError && error.status === 429) || (error instanceof NpmRiskError && error.code === 'upstream-busy')
|
|
45
|
+
|
|
46
|
+
// A package to assess: npm's of its name and version, or, with an
|
|
47
|
+
// `integrity`, a mirror's held to it.
|
|
48
|
+
export interface AssessedPackage { name: string; version: string; integrity?: string }
|
|
49
|
+
|
|
50
|
+
export type Assessment = { published: string | null; findings: { id: string; severity: Severity; takenDown?: unknown }[]; score: { score: number } }
|
|
51
|
+
|
|
52
|
+
// What of a scan's source assessing a package asks (RepositoryRiskSource).
|
|
53
|
+
export interface AssessingSource {
|
|
54
|
+
version(name: string, version: string, signal: AbortSignal): Promise<NpmVersionDocument>
|
|
55
|
+
assess(doc: NpmVersionDocument, teamScoped: boolean, signal: AbortSignal): Promise<Assessment>
|
|
56
|
+
removed(name: string, version: string, teamScoped: boolean, signal: AbortSignal): Promise<Assessment>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A package's risk line, its version's document kept and the assessment asked
|
|
60
|
+
// again after a wait while the server is busy; null once the scan stops. A
|
|
61
|
+
// version npm has no document of is told as risk tells one npm removed
|
|
62
|
+
// (removedOf), its malware keeping whether its package was taken down.
|
|
63
|
+
export async function assessPackage(source: AssessingSource, signal: AbortSignal, pkg: AssessedPackage, teamScoped: boolean): Promise<RiskLine | null> {
|
|
64
|
+
const at = { type: 'risk', ...pkg } as const
|
|
65
|
+
let doc: NpmVersionDocument | NpmPackageError | undefined
|
|
66
|
+
for (let attempt = 0; ; attempt++) {
|
|
67
|
+
try {
|
|
68
|
+
const read = doc ??= await documentOf(source, pkg, signal)
|
|
69
|
+
if (!(read instanceof NpmPackageError) && pkg.integrity !== undefined && !pkg.integrity.split(/\s+/u).includes(read.dist.integrity)) return { ...at, error: 'integrity-mismatch' }
|
|
70
|
+
const { published, findings, score } = read instanceof NpmPackageError ? await removedOf(source, pkg, teamScoped, signal, read) : await assessedOf(source, pkg, teamScoped, signal, read)
|
|
71
|
+
const told = findings.map(({ id, severity, takenDown }) => ({ id, severity, ...takenDown === true ? { takenDown: true as const } : {} }))
|
|
72
|
+
return { ...at, published, severity: highestSeverity(told), score: score.score, findings: told }
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (signal.aborted) return null
|
|
75
|
+
if (!isBusy(error) || attempt >= BUSY_RETRIES) return { ...at, ...failureOf(error) }
|
|
76
|
+
try { await delay(BUSY_WAIT_MS * Math.min(attempt + 1, 5), undefined, { signal }) } catch { return null }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// npm's document of a version, or its answer that it has none (a 404).
|
|
82
|
+
async function documentOf(source: AssessingSource, pkg: AssessedPackage, signal: AbortSignal): Promise<NpmVersionDocument | NpmPackageError> {
|
|
83
|
+
try { return await source.version(pkg.name, pkg.version, signal) } catch (error) {
|
|
84
|
+
if (error instanceof NpmPackageError && error.status === 404) return error
|
|
85
|
+
throw error
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// A version's assessment; where risk finds npm no longer has it, as a document
|
|
90
|
+
// read from where versions' are kept doesn't tell, as one npm removed.
|
|
91
|
+
async function assessedOf(source: AssessingSource, pkg: AssessedPackage, teamScoped: boolean, signal: AbortSignal, doc: NpmVersionDocument): Promise<Assessment> {
|
|
92
|
+
try { return await source.assess(doc, teamScoped, signal) } catch (error) {
|
|
93
|
+
if (!(error instanceof NpmRiskError && error.code === 'version-not-found')) throw error
|
|
94
|
+
return removedOf(source, pkg, teamScoped, signal, new NpmPackageError(404, 'package-not-found'))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A version npm has no document of, as risk tells one npm removed, as it
|
|
99
|
+
// removes every one it finds malware in (`removed`): `gone`, npm's answer of
|
|
100
|
+
// none, thrown where nothing tells of it.
|
|
101
|
+
async function removedOf(source: AssessingSource, pkg: AssessedPackage, teamScoped: boolean, signal: AbortSignal, gone: NpmPackageError): Promise<Assessment> {
|
|
102
|
+
try { return await source.removed(pkg.name, pkg.version, teamScoped, signal) } catch (error) {
|
|
103
|
+
throw error instanceof NpmRiskError && (error.code === 'version-not-found' || error.code === 'package-not-found') ? gone : error
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// A lockfile's advisories, as a repository risk scan asks them
|
|
2
|
+
// (repository-risk.ts): npm's registry's of its packages of the registry or
|
|
3
|
+
// a mirror, asked at once, as `npm audit` asks, each told with whether it is
|
|
4
|
+
// of malware (risk's npmMalwareAdvisory); kept by the lockfile's blob, in the
|
|
5
|
+
// instance's memory and the source's `audits`, for RISK_KEPT_MS, as npm
|
|
6
|
+
// publishes advisories, or why they couldn't be had for AUDIT_FAILED_KEPT_MS.
|
|
7
|
+
import type { Advisory } from '@preventive/upstream/advisories.js'
|
|
8
|
+
import { isExactVersion } from '@preventive/upstream/semver.js'
|
|
9
|
+
import { npmMalwareAdvisory } from '../risk/index.js'
|
|
10
|
+
import { isNpmPackageName } from '../common/managed/npm-packages.js'
|
|
11
|
+
import type { LockedPackage } from './locked-packages.ts'
|
|
12
|
+
import type { Lru } from './lru.ts'
|
|
13
|
+
import type { KeptStore } from './npm-kept.ts'
|
|
14
|
+
import { RISK_KEPT_MS, type RiskFailure, failureOf } from './repository-risk-assess.ts'
|
|
15
|
+
|
|
16
|
+
export type AdvisoryRow = Advisory & { malware: boolean }
|
|
17
|
+
// A lockfile's advisories, or why they couldn't be had.
|
|
18
|
+
export type Audit = AdvisoryRow[] | RiskFailure
|
|
19
|
+
|
|
20
|
+
export interface AuditSource {
|
|
21
|
+
// npm's registry's advisories of these packages' versions, asked at once.
|
|
22
|
+
advisories(packages: { name: string; versions: string[] }[], signal: AbortSignal): Promise<Advisory[]>
|
|
23
|
+
// Where audits are kept beyond this instance, by the lockfile's blob.
|
|
24
|
+
audits?: KeptStore<Audit>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// How long a lockfile's advisories that couldn't be had are told so, rather
|
|
28
|
+
// than asked again: a scan's next parts don't wait on an audit that failed,
|
|
29
|
+
// as by timing out, again and again.
|
|
30
|
+
const AUDIT_FAILED_KEPT_MS = 10 * 60_000
|
|
31
|
+
|
|
32
|
+
// Whether an audit kept `at` is told as it is: its advisories within
|
|
33
|
+
// RISK_KEPT_MS, why they couldn't be had within AUDIT_FAILED_KEPT_MS.
|
|
34
|
+
const isFresh = ({ at, audit }: { at: number; audit: Audit }) => Date.now() - at < (Array.isArray(audit) ? RISK_KEPT_MS : AUDIT_FAILED_KEPT_MS)
|
|
35
|
+
|
|
36
|
+
// The audits of lockfiles, each of its `packages` by its `key` (auditLockfile),
|
|
37
|
+
// `inTurn` with others, asked at once: each null where it has none to ask
|
|
38
|
+
// of, or once `signal` aborts. Each is the caller's to tell once it may.
|
|
39
|
+
export function auditLockfiles(source: AuditSource, memory: Lru<{ at: number; audit: Audit }>, signal: AbortSignal,
|
|
40
|
+
lockfiles: { key: string; packages: readonly LockedPackage[] | null }[], inTurn: <T>(work: () => Promise<T>) => Promise<T>): Promise<Audit | null>[] {
|
|
41
|
+
return lockfiles.map(({ key, packages }) => {
|
|
42
|
+
const audit = packages ? inTurn(() => (signal.aborted ? Promise.resolve(null) : auditLockfile(source, memory, signal, key, packages))) : Promise.resolve(null)
|
|
43
|
+
// One no longer waited on, as the scan stopped, fails alone.
|
|
44
|
+
audit.catch(() => {})
|
|
45
|
+
return audit
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The audit of a lockfile's `packages`, by its `key` as kept (`memory`, then
|
|
50
|
+
// the source's), else asked and kept; null where it locks none to ask of, or
|
|
51
|
+
// once `signal` aborts.
|
|
52
|
+
export async function auditLockfile(source: AuditSource, memory: Lru<{ at: number; audit: Audit }>, signal: AbortSignal, key: string,
|
|
53
|
+
packages: readonly LockedPackage[]): Promise<Audit | null> {
|
|
54
|
+
let kept = memory.get(key)
|
|
55
|
+
if (!kept || !isFresh(kept)) {
|
|
56
|
+
const stored = await source.audits?.read(key)
|
|
57
|
+
kept = stored ? { at: stored.at, audit: stored.value } : undefined
|
|
58
|
+
}
|
|
59
|
+
if (kept && isFresh(kept)) { memory.set(key, kept); return kept.audit }
|
|
60
|
+
const asked = new Map<string, Set<string>>()
|
|
61
|
+
for (const { name, version, source: from } of packages) {
|
|
62
|
+
if ((from !== 'registry' && from !== 'mirror') || !isNpmPackageName(name) || !isExactVersion(version)) continue
|
|
63
|
+
if (!asked.has(name)) asked.set(name, new Set())
|
|
64
|
+
asked.get(name)!.add(version)
|
|
65
|
+
}
|
|
66
|
+
if (asked.size === 0) return null
|
|
67
|
+
let audit: Audit
|
|
68
|
+
try {
|
|
69
|
+
audit = (await source.advisories([...asked].map(([name, versions]) => ({ name, versions: [...versions] })), signal))
|
|
70
|
+
.map(advisory => ({ ...advisory, malware: npmMalwareAdvisory(advisory) }))
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (signal.aborted) return null
|
|
73
|
+
audit = failureOf(error)
|
|
74
|
+
}
|
|
75
|
+
const now = Date.now()
|
|
76
|
+
memory.set(key, { at: now, audit })
|
|
77
|
+
await source.audits?.write(key, audit, now)
|
|
78
|
+
return audit
|
|
79
|
+
}
|