@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +2 -0
- package/common/managed/finding-detail.js +32 -0
- package/common/managed/github-agents.ts +114 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -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 +347 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +5 -0
- package/common/managed/team-app.js +31 -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-2WDZPXVL.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-4XMP7YVH.js +36 -0
- package/out/chunk-6LNQTKAV.js +52 -0
- package/out/chunk-6S2U2UYG.js +232 -0
- package/out/chunk-75YLHBCM.js +3 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-FC65TPUB.js +1 -0
- package/out/chunk-FKBXNUW4.js +1 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HTBYHSPG.js +1 -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-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-UK7RGJT6.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-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 +362 -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 +21 -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 +73 -0
- package/server-managed/comparison.ts +41 -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 +458 -0
- package/server-managed/db-schema.ts +238 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +142 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +100 -0
- package/server-managed/dependency-reviews.ts +346 -0
- package/server-managed/github-accounts.ts +60 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +415 -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 +39 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +76 -0
- package/server-managed/http.ts +5329 -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/locked-packages.ts +129 -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 +215 -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-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 +348 -0
- package/server-managed/pull-dependency-reviews.ts +328 -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 +355 -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 +165 -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 +156 -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 +150 -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 +307 -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,215 @@
|
|
|
1
|
+
// What the npm viewer and repository risk scans keep in the database
|
|
2
|
+
// (managed_upstream_cache, upstream-cache.ts), beyond the instance, so that
|
|
3
|
+
// neither asks for it again on every read, nor after a restart or on another
|
|
4
|
+
// instance: public versions' documents, of the fields read of them
|
|
5
|
+
// (npm-manifest.ts npmVersionFields), for a day (readNpmVersion asks npm again
|
|
6
|
+
// then, as a version can be deprecated, its package made private, or itself
|
|
7
|
+
// removed), and when each was published, for a month (npmPublishTimeStore);
|
|
8
|
+
// and, for scans, each package's risk line, for as long as a scan
|
|
9
|
+
// keeps it, and what a scan read of GitHub, for a day, and of npm's
|
|
10
|
+
// advisories, for an hour (repository-risk.ts); and what the code viewer read
|
|
11
|
+
// of a repository's GitHub, its issues, pull requests and agents' work, for a
|
|
12
|
+
// day (http.ts keptAnswer), and its history, a page from a commit at a time,
|
|
13
|
+
// for a month (repository-history.ts historyFrom), as are the files each of
|
|
14
|
+
// its commits changes and holds (repository-changes.ts), its trees as the
|
|
15
|
+
// code viewer reads them (codeTreeStore), the packages a pull request's
|
|
16
|
+
// lockfiles add (pull-dependencies.ts), and the lockfiles a commit lists and
|
|
17
|
+
// each one's packages, as its dependency review reads them
|
|
18
|
+
// (commit-lockfiles.ts); and the commit the code viewer keeps its
|
|
19
|
+
// directories at, for as long as it does (codeHeadStore). Each store removes
|
|
20
|
+
// its own rows kept longer than that as it writes, at most once an hour a
|
|
21
|
+
// database (pruneKept), so that rows no read will take again don't pile up.
|
|
22
|
+
// The database is an optimization: a failure reads as a miss, and writes
|
|
23
|
+
// nothing.
|
|
24
|
+
import { createHash } from 'node:crypto'
|
|
25
|
+
import { plainObject } from './npm-manifest.ts'
|
|
26
|
+
import { type NpmPublishTimeStore, type NpmVersionStore, VERSION_KEPT_MS } from './npm-packages.ts'
|
|
27
|
+
import type { UpstreamCacheStore } from './upstream-cache.ts'
|
|
28
|
+
|
|
29
|
+
// A value kept by key, as JSON, `at` when it was.
|
|
30
|
+
export interface KeptStore<T> {
|
|
31
|
+
read(key: string): Promise<{ at: number; value: T } | null>
|
|
32
|
+
// Those of `keys` kept, by key, in one read of the database.
|
|
33
|
+
readMany(keys: string[]): Promise<Map<string, { at: number; value: T }>>
|
|
34
|
+
// Those of `first` kept; where none is, those of `then`, in one read of the
|
|
35
|
+
// database (`first` whether they are `first`'s).
|
|
36
|
+
readFirst(first: string[], then: string[]): Promise<{ first: boolean; found: Map<string, { at: number; value: T }> }>
|
|
37
|
+
write(key: string, value: T, at: number): Promise<void>
|
|
38
|
+
// Removes what is kept by `key`.
|
|
39
|
+
remove(key: string): Promise<void>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// When each database last had the rows under each prefix removed.
|
|
43
|
+
const PRUNE_EVERY_MS = 60 * 60_000
|
|
44
|
+
const pruned = new WeakMap<UpstreamCacheStore, Map<string, number>>()
|
|
45
|
+
// Who sees each database's prunes through (index.ts: its app, which waits
|
|
46
|
+
// for them before closing it, and on serverless keeps its invocation alive
|
|
47
|
+
// till they settle), as a write no longer waits for one.
|
|
48
|
+
const pruneOwners = new WeakMap<UpstreamCacheStore, (prune: Promise<unknown>) => void>()
|
|
49
|
+
export function ownKeptPrunes(db: UpstreamCacheStore, own: (prune: Promise<unknown>) => void) {
|
|
50
|
+
pruneOwners.set(db, own)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Removes the rows under `prefix` kept `keptMs` before `at`, at most once an
|
|
54
|
+
// hour a database, as rows under it are written: here, and risk's downloads
|
|
55
|
+
// (npm-risk.ts), each one run handed to the database's owner. A failure is
|
|
56
|
+
// `warn`ed of, and removes nothing.
|
|
57
|
+
export function pruneKept(db: UpstreamCacheStore, prefix: string, keptMs: number, at: number, warn: (error: unknown) => void): Promise<void> {
|
|
58
|
+
const times = pruned.get(db) ?? pruned.set(db, new Map()).get(db)!
|
|
59
|
+
if (at - (times.get(prefix) ?? -Infinity) < PRUNE_EVERY_MS) return Promise.resolve()
|
|
60
|
+
times.set(prefix, at)
|
|
61
|
+
const prune = (async () => { try { await db.deleteUpstreamCacheEntries(prefix, at - keptMs) } catch (error) { warn(error) } })()
|
|
62
|
+
pruneOwners.get(db)?.(prune)
|
|
63
|
+
return prune
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Values of `type`, each read for `keptMs` after it was kept.
|
|
67
|
+
function keptStore<T>(db: UpstreamCacheStore, type: string, keptMs: number, debug: boolean): KeptStore<T> {
|
|
68
|
+
const warn = (what: string, error: unknown) => { if (debug) console.warn(`managed: kept ${type} ${what} failed:`, error) }
|
|
69
|
+
// Kept for good (Infinity): none removed.
|
|
70
|
+
const prune = (at: number) => keptMs === Infinity ? Promise.resolve() : pruneKept(db, `${type}/`, keptMs, at, error => { warn('prune', error) })
|
|
71
|
+
// Keys of any length or character, as the database's: hashed.
|
|
72
|
+
const keyOf = (key: string) => `${type}/${createHash('sha256').update(key).digest('hex')}`
|
|
73
|
+
const valueOf = (row: string | null | undefined) => {
|
|
74
|
+
const kept: unknown = JSON.parse(row ?? 'null')
|
|
75
|
+
return plainObject(kept) && typeof kept['at'] === 'number' && 'value' in kept ? { at: kept['at'], value: kept['value'] as T } : null
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
async read(key) {
|
|
79
|
+
try { return valueOf(await db.getUpstreamCacheEntry(keyOf(key))) } catch (error) { warn('read', error); return null }
|
|
80
|
+
},
|
|
81
|
+
async readMany(keys) {
|
|
82
|
+
try {
|
|
83
|
+
const rows = await db.getUpstreamCacheEntries(keys.map(keyOf))
|
|
84
|
+
return new Map(keys.flatMap(key => { const kept = valueOf(rows.get(keyOf(key))); return kept ? [[key, kept] as const] : [] }))
|
|
85
|
+
} catch (error) { warn('read', error); return new Map() }
|
|
86
|
+
},
|
|
87
|
+
async readFirst(first, then) {
|
|
88
|
+
try {
|
|
89
|
+
const { first: answered, entries } = await db.getFirstUpstreamCacheEntries(first.map(keyOf), then.map(keyOf))
|
|
90
|
+
const asked = answered ? first : then
|
|
91
|
+
return { first: answered, found: new Map(asked.flatMap(key => { const kept = valueOf(entries.get(keyOf(key))); return kept ? [[key, kept] as const] : [] })) }
|
|
92
|
+
} catch (error) { warn('read', error); return { first: false, found: new Map() } }
|
|
93
|
+
},
|
|
94
|
+
async write(key, value, at) {
|
|
95
|
+
try { await db.setUpstreamCacheEntry(keyOf(key), JSON.stringify({ at, value }), at) } catch (error) { warn('write', error) }
|
|
96
|
+
// Behind the write, not before it is done: the prune reads the whole
|
|
97
|
+
// table (no index serves its prefix), which no writer need wait on.
|
|
98
|
+
// Its hour is marked before it runs, so writes meanwhile start none,
|
|
99
|
+
// and its database's owner sees it through (ownKeptPrunes).
|
|
100
|
+
void prune(at)
|
|
101
|
+
},
|
|
102
|
+
async remove(key) {
|
|
103
|
+
try { await db.deleteUpstreamCacheEntries(keyOf(key), Number.MAX_SAFE_INTEGER) } catch (error) { warn('remove', error) }
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function npmVersionStore(db: UpstreamCacheStore, debug = false): NpmVersionStore {
|
|
109
|
+
const kept = keptStore<Record<string, unknown>>(db, 'npm/version-documents', VERSION_KEPT_MS, debug)
|
|
110
|
+
return {
|
|
111
|
+
async read(name, version) {
|
|
112
|
+
const hit = await kept.read(`${name}@${version}`)
|
|
113
|
+
return hit && plainObject(hit.value) ? { at: hit.at, json: hit.value } : null
|
|
114
|
+
},
|
|
115
|
+
async readMany(versions) {
|
|
116
|
+
const hits = await kept.readMany(versions.map(({ name, version }) => `${name}@${version}`))
|
|
117
|
+
return new Map([...hits].flatMap(([key, hit]) => plainObject(hit.value) ? [[key, { at: hit.at, json: hit.value }] as const] : []))
|
|
118
|
+
},
|
|
119
|
+
write: (name, version, json) => kept.write(`${name}@${version}`, json, Date.now()),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// When public versions were published (npm-packages.ts npmPublishTime), which
|
|
124
|
+
// never changes once they are: kept a month, and kept again by the next view
|
|
125
|
+
// of the version past it.
|
|
126
|
+
const PUBLISH_TIME_KEPT_MS = 30 * 24 * 60 * 60_000
|
|
127
|
+
export function npmPublishTimeStore(db: UpstreamCacheStore, debug = false): NpmPublishTimeStore {
|
|
128
|
+
const kept = keptStore<string>(db, 'npm/publish-times', PUBLISH_TIME_KEPT_MS, debug)
|
|
129
|
+
return {
|
|
130
|
+
async read(name, version) {
|
|
131
|
+
const hit = await kept.read(`${name}@${version}`)
|
|
132
|
+
return typeof hit?.value === 'string' ? hit.value : null
|
|
133
|
+
},
|
|
134
|
+
write: (name, version, at) => kept.write(`${name}@${version}`, at, Date.now()),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Risk lines, each kept `keptMs`, for good where Infinity, a scan telling one
|
|
139
|
+
// past RISK_KEPT_MS as stale (repository-risk.ts).
|
|
140
|
+
export const riskLineStore = <T>(db: UpstreamCacheStore, keptMs: number, debug = false): KeptStore<T> => keptStore<T>(db, 'repository-risk/lines', keptMs, debug)
|
|
141
|
+
// What a scan read, each read for keptMs (repository-risk.ts): of GitHub, a
|
|
142
|
+
// repository's lockfiles as listed at a commit (`listings`) and a lockfile's
|
|
143
|
+
// packages by its blob (`lockfiles`), for SCAN_READ_KEPT_MS; of npm, a
|
|
144
|
+
// lockfile's advisories by its blob (`advisories`), for RISK_KEPT_MS; the
|
|
145
|
+
// repositories a scan in parts told whole (`progress`); a repository's
|
|
146
|
+
// Actions' files by the commit read (`action-files`), and the actions and
|
|
147
|
+
// reusable workflows of others they name, each resolved (`actions`), of
|
|
148
|
+
// public repositories alone (repository-actions.ts).
|
|
149
|
+
export const scanReadStore = <T>(db: UpstreamCacheStore, type: 'listings' | 'lockfiles' | 'advisories' | 'progress' | 'action-files' | 'actions', keptMs: number, debug = false): KeptStore<T> =>
|
|
150
|
+
keptStore<T>(db, `repository-risk/${type}`, keptMs, debug)
|
|
151
|
+
// What the code viewer read of a repository (http.ts readWholeRepository), by
|
|
152
|
+
// its id and what was asked, each kept REPOSITORY_READ_KEPT_MS.
|
|
153
|
+
export const REPOSITORY_READ_KEPT_MS = 24 * 60 * 60_000
|
|
154
|
+
export const repositoryReadStore = (db: UpstreamCacheStore, debug = false): KeptStore<object> =>
|
|
155
|
+
keptStore<object>(db, 'repository/reads', REPOSITORY_READ_KEPT_MS, debug)
|
|
156
|
+
// Pages of a repository's history (repository-history.ts historyFrom), by its
|
|
157
|
+
// id and the commit each starts at, which never change: kept a month after
|
|
158
|
+
// they were last read from GitHub.
|
|
159
|
+
export const HISTORY_PAGE_KEPT_MS = 30 * 24 * 60 * 60_000
|
|
160
|
+
export const historyPageStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/history', HISTORY_PAGE_KEPT_MS, debug)
|
|
161
|
+
// The pull requests squashed into a repository's commits, by the commit
|
|
162
|
+
// (repository-squashes.ts squashedPulls): kept as its history's pages are.
|
|
163
|
+
export const historySquashStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/squashes', HISTORY_PAGE_KEPT_MS, debug)
|
|
164
|
+
// Whether a pull request brought a commit, by its repository and commit, as
|
|
165
|
+
// the Agents tab looks it up of those an agent marked (repository-agents.ts
|
|
166
|
+
// pulledCommits), kept as the history is.
|
|
167
|
+
export const commitPullStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/commit-pulls', HISTORY_PAGE_KEPT_MS, debug)
|
|
168
|
+
// A repository's pull requests, the most recently updated first, as the
|
|
169
|
+
// Pulse and the Agents tab read them (repository-pulls.ts readPulls), by its
|
|
170
|
+
// id: kept as its history's pages are, those updated since read on.
|
|
171
|
+
export const pullListStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/pulls', HISTORY_PAGE_KEPT_MS, debug)
|
|
172
|
+
// The files each of a repository's commits changes (repository-changes.ts
|
|
173
|
+
// repositoryChanges), by the commit, which never change: kept as its
|
|
174
|
+
// history's pages are.
|
|
175
|
+
export const historyChangeStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/changes', HISTORY_PAGE_KEPT_MS, debug)
|
|
176
|
+
// The files a repository's commit holds (repository-changes.ts
|
|
177
|
+
// repositoryTree), by the commit, which never change: kept as its history's
|
|
178
|
+
// pages are.
|
|
179
|
+
export const historyTreeStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/trees', HISTORY_PAGE_KEPT_MS, debug)
|
|
180
|
+
// What the code viewer read of a repository's trees (http.ts
|
|
181
|
+
// repositoryTreesOf), which never changes once a commit or tree names it: the
|
|
182
|
+
// tree at a directory of a commit, by the repository's id, the commit and the
|
|
183
|
+
// directory (`trees`), and how many files a tree holds and their bytes, by
|
|
184
|
+
// the repository's id and the tree's (`tree-files`): kept as its history's
|
|
185
|
+
// pages are.
|
|
186
|
+
export const codeTreeStore = <T>(db: UpstreamCacheStore, type: 'trees' | 'tree-files', debug = false): KeptStore<T> =>
|
|
187
|
+
keptStore<T>(db, `repository/code-${type}`, HISTORY_PAGE_KEPT_MS, debug)
|
|
188
|
+
// The commit the code viewer keeps a repository's directories at in byte
|
|
189
|
+
// storage, its default branch's head as last read, and the repository's
|
|
190
|
+
// name they are kept under (repository-code.ts repositoryCodeKept), by the
|
|
191
|
+
// repository's id: kept until the head moves, the repository is renamed or
|
|
192
|
+
// it is removed, one for each repository.
|
|
193
|
+
export const codeHeadStore = (db: UpstreamCacheStore, debug = false): KeptStore<{ commit: string; repo: string }> =>
|
|
194
|
+
keptStore<{ commit: string; repo: string }>(db, 'repository/code-heads', Infinity, debug)
|
|
195
|
+
// The packages a repository's pull request's lockfiles add
|
|
196
|
+
// (pull-dependencies.ts), by the commits of its base and head, which never
|
|
197
|
+
// change: kept as its history's pages are.
|
|
198
|
+
export const pullDependencyStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/pull-dependencies', HISTORY_PAGE_KEPT_MS, debug)
|
|
199
|
+
// The versions a repository's lockfiles locked at the commit its dependencies
|
|
200
|
+
// are reviewed since (pull-dependency-reviews.ts reviewBaseline), by the
|
|
201
|
+
// commit, which never change: kept as its history's pages are, never stored
|
|
202
|
+
// as reviews.
|
|
203
|
+
// The lines a version changes against another, or none (npm-delta.ts), by
|
|
204
|
+
// both sides' integrities, each kept HISTORY_PAGE_KEPT_MS.
|
|
205
|
+
export const npmDeltaStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'npm/deltas', HISTORY_PAGE_KEPT_MS, debug)
|
|
206
|
+
export const reviewBaselineStore = (db: UpstreamCacheStore, debug = false): KeptStore<{ versions: string[]; unread: string[] }> =>
|
|
207
|
+
keptStore<{ versions: string[]; unread: string[] }>(db, 'repository/review-baselines', HISTORY_PAGE_KEPT_MS, debug)
|
|
208
|
+
// What a repository's dependency review read of GitHub (pull-dependencies.ts
|
|
209
|
+
// LockfileStore): the lockfiles a commit lists (`listings`), by the commit,
|
|
210
|
+
// and a lockfile's packages with what their reviews are held to
|
|
211
|
+
// (`lockfiles`), by its blob, which never change: kept as its history's
|
|
212
|
+
// pages are, apart from a scan's (scanReadStore), which reads them without
|
|
213
|
+
// what their reviews are held to.
|
|
214
|
+
export const lockfileReadStore = <T>(db: UpstreamCacheStore, type: 'listings' | 'lockfiles', debug = false): KeptStore<T> =>
|
|
215
|
+
keptStore<T>(db, `repository/review-${type}`, HISTORY_PAGE_KEPT_MS, debug)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Loading a published version for the npm package viewer (npm-packages.ts):
|
|
2
|
+
// its tarball, kept on disk as bundle builds keep theirs, else read from the
|
|
3
|
+
// registry, its files unpacked from it, and the viewer's response built from
|
|
4
|
+
// them, each shared by the readers asking for it meanwhile and held to a few
|
|
5
|
+
// versions at once per process; its files kept a little while after.
|
|
6
|
+
import { Buffer } from 'node:buffer'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { join, resolve } from 'node:path'
|
|
9
|
+
import { CompressionError, decompress } from '@preventive/archive/compression.js'
|
|
10
|
+
import { encodeBrotli } from './brotli.ts'
|
|
11
|
+
import { keptBriefly } from './kept-briefly.ts'
|
|
12
|
+
import { MAX_NPM_PACKAGE_BYTES, MAX_NPM_PACKAGE_FILES, NPM_REGISTRY, NpmPackageError, type NpmPackageFile, type NpmVersionDocument, type TarReading,
|
|
13
|
+
askUpstream, npmFileRows, npmPublishTime, npmToken, readNpmTar } from './npm-packages.ts'
|
|
14
|
+
import { keepFile, readKeptFile, upstreamCachePath } from './upstream-cache.ts'
|
|
15
|
+
|
|
16
|
+
// The tar stream a package's files may come in, headers and padding included.
|
|
17
|
+
// A tarball larger than that cannot unpack within it, so it is refused as it
|
|
18
|
+
// arrives.
|
|
19
|
+
export const MAX_TAR_BYTES = 96 * 1024 * 1024
|
|
20
|
+
const TARBALL_TIMEOUT_MS = 120_000
|
|
21
|
+
// Loads in flight per process, those of one version (its tarball, its files,
|
|
22
|
+
// its response) as one, as they read its tarball and unpack it once between
|
|
23
|
+
// them; each can hold a tarball, its tar, its files and their encoded JSON.
|
|
24
|
+
const MAX_ACTIVE_LOADS = 4
|
|
25
|
+
// How long a public version's files are kept once unpacked, and what they
|
|
26
|
+
// may take between them, the least lately read going first past it:
|
|
27
|
+
// Compare's requests for a pair of versions, and the Code tab's, come one
|
|
28
|
+
// after another, each loading the same files. A private version's are never
|
|
29
|
+
// kept, as nothing else of it is past its readers.
|
|
30
|
+
const UNPACKED_KEPT_MS = 60_000
|
|
31
|
+
const MAX_UNPACKED_BYTES = 128 * 1024 * 1024
|
|
32
|
+
|
|
33
|
+
// A load's result, shared by its readers, and a reader's release of it.
|
|
34
|
+
export interface NpmLoad<T> { result: Promise<T>; release: () => void }
|
|
35
|
+
|
|
36
|
+
interface Load { job: Promise<unknown>; readers: number; settled: boolean; slot: string }
|
|
37
|
+
|
|
38
|
+
// Loads by what they read; readers asking for the same share one. A load
|
|
39
|
+
// keeps its place among the active ones until its work is done and each
|
|
40
|
+
// reader has released it, as a reader does once its response is written or
|
|
41
|
+
// abandoned, so what responses still hold counts too. Places by the slot
|
|
42
|
+
// their loads share, with how many hold each and how many places it takes.
|
|
43
|
+
const loads = new Map<string, Load>()
|
|
44
|
+
const slots = new Map<string, { holding: number; places: number }>()
|
|
45
|
+
const placesTaken = () => [...slots.values()].reduce((sum, slot) => sum + slot.places, 0)
|
|
46
|
+
|
|
47
|
+
const refused = <T>(status: number, code: string): NpmLoad<T> => {
|
|
48
|
+
const result = Promise.reject(new NpmPackageError(status, code))
|
|
49
|
+
result.catch(() => {})
|
|
50
|
+
return { result, release() {} }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// What names a version's tarball, and so its files: its integrity, its
|
|
54
|
+
// shasum and URL, and whether it is read with the token.
|
|
55
|
+
const tarballKey = (doc: NpmVersionDocument) => `${doc.dist.integrity} ${doc.dist.shasum ?? ''} ${doc.dist.tarball} ${doc.private}`
|
|
56
|
+
|
|
57
|
+
function shared<T>(what: string, doc: NpmVersionDocument, work: () => Promise<T>): NpmLoad<T> {
|
|
58
|
+
const { unpackedSize, fileCount } = doc.dist
|
|
59
|
+
if ((unpackedSize ?? 0) > MAX_NPM_PACKAGE_BYTES || (fileCount ?? 0) > MAX_NPM_PACKAGE_FILES) return refused(413, 'package-too-large')
|
|
60
|
+
const slot = tarballKey(doc)
|
|
61
|
+
return sharedLoad(`${what} ${slot}`, work, 'npm-busy', { slot })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The places among the active ones a load takes whose files hold `files` and
|
|
65
|
+
// `bytes`: as many as packages they would fill, up to every one, each place
|
|
66
|
+
// holding what a package may.
|
|
67
|
+
export function loadPlaces({ files, bytes }: { files: number; bytes: number }): number {
|
|
68
|
+
return Math.min(MAX_ACTIVE_LOADS, Math.max(1, Math.ceil(files / MAX_NPM_PACKAGE_FILES), Math.ceil(bytes / MAX_NPM_PACKAGE_BYTES)))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A load among the active ones, by `key`, refused as `busy` past them: a
|
|
72
|
+
// repository directory's (repository-code.ts) as a package's, each holding
|
|
73
|
+
// alike a tarball, its tar, its files and their encoded JSON. Loads of one
|
|
74
|
+
// `slot` take one place between them, or `places` (loadPlaces) where they
|
|
75
|
+
// hold more.
|
|
76
|
+
export function sharedLoad<T>(key: string, work: () => Promise<T>, busy: string,
|
|
77
|
+
{ slot = key, places = 1 }: { slot?: string; places?: number } = {}): NpmLoad<T> {
|
|
78
|
+
let load = loads.get(key)
|
|
79
|
+
if (!load) {
|
|
80
|
+
if (!slots.has(slot) && placesTaken() + places > MAX_ACTIVE_LOADS) return refused(429, busy)
|
|
81
|
+
const created: Load = { job: work(), readers: 0, settled: false, slot }
|
|
82
|
+
const finish = () => { settle(key, created) }
|
|
83
|
+
created.job.then(finish, finish)
|
|
84
|
+
loads.set(key, created)
|
|
85
|
+
const taken = slots.get(slot)
|
|
86
|
+
slots.set(slot, { holding: (taken?.holding ?? 0) + 1, places: taken?.places ?? places })
|
|
87
|
+
load = created
|
|
88
|
+
}
|
|
89
|
+
const held = load
|
|
90
|
+
held.readers++
|
|
91
|
+
let released = false
|
|
92
|
+
return {
|
|
93
|
+
result: held.job as Promise<T>,
|
|
94
|
+
release() {
|
|
95
|
+
if (released) return
|
|
96
|
+
released = true
|
|
97
|
+
held.readers--
|
|
98
|
+
done(key, held)
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function settle(key: string, load: Load) {
|
|
104
|
+
load.settled = true
|
|
105
|
+
done(key, load)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A load gone from the active ones, and its place with it, once its work is
|
|
109
|
+
// done and its readers have released it.
|
|
110
|
+
function done(key: string, load: Load) {
|
|
111
|
+
if (load.readers !== 0 || !load.settled || loads.get(key) !== load) return
|
|
112
|
+
loads.delete(key)
|
|
113
|
+
const taken = slots.get(load.slot)!
|
|
114
|
+
if (taken.holding === 1) slots.delete(load.slot)
|
|
115
|
+
else taken.holding--
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The sha512 an integrity names, in base64, or null where it names none.
|
|
119
|
+
const sha512Of = (integrity: string) => /(?:^|\s)sha512-([\d+/A-Za-z]{86}==)(?=\s|$)/u.exec(integrity)?.[1] ?? null
|
|
120
|
+
// Hashed off the event loop: a tarball runs to tens of MiB.
|
|
121
|
+
const digest = async (algorithm: string, bytes: Uint8Array) => Buffer.from(await crypto.subtle.digest(algorithm, bytes as Uint8Array<ArrayBuffer>))
|
|
122
|
+
// Whether `bytes` are those of the sha512 an integrity names and, where there
|
|
123
|
+
// is one, of the shasum (a sha1, in hex).
|
|
124
|
+
async function hashedAs(bytes: Uint8Array, sha512: string, shasum: string | null): Promise<boolean> {
|
|
125
|
+
const [own512, own1] = await Promise.all([digest('SHA-512', bytes), shasum === null ? null : digest('SHA-1', bytes)])
|
|
126
|
+
return own512.toString('base64') === sha512 && (own1 === null || own1.toString('hex') === shasum)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Upstream's disk cache (setCacheDir), where tarballs are kept between
|
|
130
|
+
// loads, or null to keep none and read none.
|
|
131
|
+
let tarballCache: string | null = null
|
|
132
|
+
|
|
133
|
+
export function setNpmTarballCache(dir: string | null) {
|
|
134
|
+
tarballCache = dir === null ? null : resolve(dir)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Where npm's own cache files content by its sha512, short of an .npmrc
|
|
138
|
+
// moving the cache.
|
|
139
|
+
function npmCachePath(hash: string): string | null {
|
|
140
|
+
const configured = process.env['npm_config_cache'] || process.env['NPM_CONFIG_CACHE']
|
|
141
|
+
let root
|
|
142
|
+
try {
|
|
143
|
+
root = configured ? resolve(configured.replace(/^~(?=[/\\])/u, homedir()))
|
|
144
|
+
: process.platform === 'win32' ? join(process.env['LOCALAPPDATA'] || homedir(), 'npm-cache') : join(homedir(), '.npm')
|
|
145
|
+
} catch { return null }
|
|
146
|
+
const hex = Buffer.from(hash, 'base64').toString('hex')
|
|
147
|
+
return join(root, '_cacache', 'content-v2', 'sha512', hex.slice(0, 2), hex.slice(2, 4), hex.slice(4))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The version's tarball from the registry, at the package's own path: with
|
|
151
|
+
// the token only for a private version, and its size bounded as it arrives.
|
|
152
|
+
async function downloadTarball(url: string, isPrivate: boolean): Promise<Buffer> {
|
|
153
|
+
const token = isPrivate ? npmToken() : null
|
|
154
|
+
const bytes = await askUpstream(url, { headers: token ? { authorization: `Bearer ${token}` } : {}, limit: MAX_TAR_BYTES, ms: TARBALL_TIMEOUT_MS, absent: [401, 403, 404] })
|
|
155
|
+
.catch((err: unknown) => { throw err instanceof NpmPackageError && err.message === 'upstream-too-large' ? new NpmPackageError(413, 'package-too-large') : err })
|
|
156
|
+
if (bytes === null) throw new NpmPackageError(404, 'package-not-found')
|
|
157
|
+
return bytes
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The version's tarball, its bytes held to the document's sha512, and to its
|
|
161
|
+
// shasum where it tells one, wherever they come from: kept in the cache,
|
|
162
|
+
// where bundle builds keep what they fetch with the token, or in npm's; else
|
|
163
|
+
// downloaded, and then kept. Bytes matching the hashes the reader's own
|
|
164
|
+
// document names are the tarball that reader may download, wherever they
|
|
165
|
+
// were kept and whoever fetched them.
|
|
166
|
+
async function readTarball(doc: NpmVersionDocument): Promise<Uint8Array> {
|
|
167
|
+
const expected = sha512Of(doc.dist.integrity), shasum = doc.dist.shasum ?? null, url = doc.dist.tarball
|
|
168
|
+
if (!url.startsWith(`${NPM_REGISTRY}/${doc.name}/-/`) || URL.parse(url)?.href !== url || expected === null
|
|
169
|
+
|| (shasum !== null && !/^[\da-f]{40}$/u.test(shasum))) throw new NpmPackageError(502, 'upstream-invalid')
|
|
170
|
+
const kept = tarballCache === null ? null : upstreamCachePath(tarballCache, 'npm/tarballs', `${doc.name}@${doc.version}.tgz`)
|
|
171
|
+
if (kept !== null) {
|
|
172
|
+
for (const path of [kept, npmCachePath(expected)]) {
|
|
173
|
+
const bytes = path === null ? null : await readKeptFile(path, MAX_TAR_BYTES)
|
|
174
|
+
if (bytes && await hashedAs(bytes, expected, shasum)) return bytes
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const bytes = await downloadTarball(url, doc.private)
|
|
178
|
+
if (!await hashedAs(bytes, expected, shasum)) throw new NpmPackageError(502, 'upstream-invalid')
|
|
179
|
+
if (kept !== null) await keepFile(kept, bytes)
|
|
180
|
+
return bytes
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Tarballs being read, and versions' files being unpacked, by what names
|
|
184
|
+
// them (tarballKey): readers asking for one meanwhile share it, its bytes
|
|
185
|
+
// those each one's own document names.
|
|
186
|
+
const reading = new Map<string, Promise<Uint8Array>>()
|
|
187
|
+
const unpacking = new Map<string, Promise<Unpacked>>()
|
|
188
|
+
|
|
189
|
+
function sharedRead<T>(pending: Map<string, Promise<T>>, key: string, read: () => Promise<T>): Promise<T> {
|
|
190
|
+
let job = pending.get(key)
|
|
191
|
+
if (!job) {
|
|
192
|
+
const started = read().finally(() => { if (pending.get(key) === started) pending.delete(key) })
|
|
193
|
+
pending.set(key, job = started)
|
|
194
|
+
}
|
|
195
|
+
return job
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const sharedTarball = (doc: NpmVersionDocument) => sharedRead(reading, tarballKey(doc), () => readTarball(doc))
|
|
199
|
+
|
|
200
|
+
// A version's files, and the size of the tarball they came in.
|
|
201
|
+
interface Unpacked { files: NpmPackageFile[]; tarballSize: number }
|
|
202
|
+
|
|
203
|
+
// Public versions' files lately unpacked, by tarballKey.
|
|
204
|
+
const unpacked = keptBriefly<Unpacked>({ keptMs: UNPACKED_KEPT_MS, maxBytes: MAX_UNPACKED_BYTES })
|
|
205
|
+
|
|
206
|
+
// The version's files, kept from a load of them a little while since, else
|
|
207
|
+
// unpacked from its tarball (and kept, a public version's): bytes its
|
|
208
|
+
// reader's own document names, as its tarball's are.
|
|
209
|
+
function unpackedOf(doc: NpmVersionDocument): Promise<Unpacked> {
|
|
210
|
+
const key = tarballKey(doc)
|
|
211
|
+
const kept = unpacked.get(key)
|
|
212
|
+
if (kept) return Promise.resolve(kept)
|
|
213
|
+
return sharedRead(unpacking, key, async () => {
|
|
214
|
+
const tarball = await sharedTarball(doc)
|
|
215
|
+
const value = { files: await readTarballFiles(tarball), tarballSize: tarball.byteLength }
|
|
216
|
+
if (!doc.private) unpacked.keep(key, value, value.files.reduce((sum, file) => sum + file.path.length + file.bytes.byteLength, 0))
|
|
217
|
+
return value
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// A gzipped tarball's files, its top directory left out, as npm extracts
|
|
222
|
+
// a package's: a repository tree's as GitHub archives it, too, its symlinks
|
|
223
|
+
// let climb as readNpmTar's `symlinksAbove` says, and its tar stream and
|
|
224
|
+
// files bounded as `options` give, a package's bounds unless given.
|
|
225
|
+
export async function readTarballFiles(tarball: Uint8Array, { maxTarBytes = MAX_TAR_BYTES, ...options }: TarReading & { maxTarBytes?: number } = {}): Promise<NpmPackageFile[]> {
|
|
226
|
+
let tar: Uint8Array
|
|
227
|
+
try { tar = await decompress(tarball, 'gzip', { limit: maxTarBytes }) }
|
|
228
|
+
catch (err) {
|
|
229
|
+
const limited = err instanceof CompressionError && err.limited
|
|
230
|
+
throw new NpmPackageError(limited ? 413 : 422, limited ? 'package-too-large' : 'bad-tarball')
|
|
231
|
+
}
|
|
232
|
+
return readNpmTar(tar, options)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// The version's tarball, for download.
|
|
236
|
+
export function loadNpmTarball(doc: NpmVersionDocument): NpmLoad<Uint8Array> {
|
|
237
|
+
return shared('tarball', doc, () => sharedTarball(doc))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// The version's files, for one of them to be pretty-printed (pretty-print.ts).
|
|
241
|
+
export function loadNpmFiles(doc: NpmVersionDocument): NpmLoad<NpmPackageFile[]> {
|
|
242
|
+
return shared('files', doc, async () => (await unpackedOf(doc)).files)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// The version as the viewer reads it, as brotli-encoded JSON: `{ name,
|
|
246
|
+
// version, private, integrity, tarballSize, published, manifest, files,
|
|
247
|
+
// known }`, its files as npmFileRows gives them, and with them, as its files
|
|
248
|
+
// are encoded, when it was published where that is kept (`published`, asked
|
|
249
|
+
// then, else null) and what `known` gives: what is kept about its package
|
|
250
|
+
// (npm-insights.ts npmKnown), which the viewer need not then ask for. Encoding
|
|
251
|
+
// is where a load holds the most, so it takes its place among the active
|
|
252
|
+
// loads until its body is done, and readers asking meanwhile share that body,
|
|
253
|
+
// the first one's manifest, `published` and `known` in it; the body is not
|
|
254
|
+
// kept once its readers are done, its files a little while (unpackedOf).
|
|
255
|
+
export function loadNpmPackageBody(doc: NpmVersionDocument, known: () => object = () => ({}),
|
|
256
|
+
published: () => Promise<string | null> = npmPublishTime(doc).read): NpmLoad<Buffer> {
|
|
257
|
+
return shared('package', doc, async () => {
|
|
258
|
+
const { files, tarballSize } = await unpackedOf(doc)
|
|
259
|
+
return encodeBrotli(Buffer.from(JSON.stringify({
|
|
260
|
+
name: doc.name, version: doc.version, private: doc.private, integrity: doc.dist.integrity, tarballSize,
|
|
261
|
+
published: await published(), manifest: doc.manifest, files: npmFileRows(files), known: known(),
|
|
262
|
+
})))
|
|
263
|
+
})
|
|
264
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// What a published version's document tells of it as the npm viewer's
|
|
2
|
+
// Overview shows it (npmManifest), and the fields of the document read for it
|
|
3
|
+
// and for the version itself (npm-packages.ts readNpmVersion), the rest
|
|
4
|
+
// stripped where it is kept (npmVersionFields).
|
|
5
|
+
import publishers from '@preventive/publishers/npm.js'
|
|
6
|
+
import { getRepo } from '@preventive/upstream/package.js'
|
|
7
|
+
|
|
8
|
+
export const plainObject = (value: unknown): value is Record<string, unknown> => Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
9
|
+
export const stringRecord = (value: unknown) => plainObject(value)
|
|
10
|
+
? Object.fromEntries(Object.entries(value).filter(([, item]) => typeof item === 'string')) as Record<string, string> : undefined
|
|
11
|
+
|
|
12
|
+
interface NpmPerson { name: string | null; email: string | null }
|
|
13
|
+
const NPM_ACCOUNT = /^[\w.-]{1,214}$/u
|
|
14
|
+
// The npm accounts known to publish: those with standing on one of npm's
|
|
15
|
+
// most downloaded packages (@preventive/publishers).
|
|
16
|
+
const KNOWN_PUBLISHERS = new Set(publishers.map(name => name.toLowerCase()))
|
|
17
|
+
|
|
18
|
+
// One of package.json's people: `{ name, email, url }`, or `Name <email>
|
|
19
|
+
// (url)` in one string.
|
|
20
|
+
function npmPerson(value: unknown): NpmPerson | null {
|
|
21
|
+
const text = (part: unknown) => typeof part === 'string' && part.trim() !== '' ? part.trim() : null
|
|
22
|
+
if (plainObject(value)) return { name: text(value['name']), email: text(value['email']) }
|
|
23
|
+
if (typeof value !== 'string') return null
|
|
24
|
+
const parts = /^([^<(]*)(?:<([^>]*)>)?\s*(?:\([^)]*\))?\s*$/u.exec(value)
|
|
25
|
+
return parts ? { name: text(parts[1]), email: text(parts[2]) } : { name: text(value), email: null }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The GitLab project (`group/project`, subgroups and all) a package names as
|
|
29
|
+
// its repository, in any of the forms npm takes (`gitlab:group/project`,
|
|
30
|
+
// `git@gitlab.com:…`, `git+https://gitlab.com/….git`); null for another.
|
|
31
|
+
// GitLab's are only on gitlab.com, as npm's provenance has them.
|
|
32
|
+
function gitlabProject(repository: unknown): string | null {
|
|
33
|
+
const url = typeof repository === 'string' ? repository : plainObject(repository) && typeof repository['url'] === 'string' ? repository['url'] : ''
|
|
34
|
+
if (url.length > 2048) return null
|
|
35
|
+
const project = /^(?:gitlab:|git@gitlab\.com:|(?:git\+)?(?:https|ssh|git):\/\/(?:git@)?gitlab\.com\/)([\w.-]+(?:\/[\w.-]+)+)\/?$/u
|
|
36
|
+
.exec(url.split('/-/')[0]!)?.[1]?.replace(/\.git$/u, '')
|
|
37
|
+
return project && !project.split('/').some(part => /^\.+$/u.test(part)) ? project : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The fields the Overview shows, in the shapes package.json gives them; each
|
|
41
|
+
// left out where the document has none, or has another shape. A homepage that
|
|
42
|
+
// leads only where its repository does (the repository, its directory there,
|
|
43
|
+
// or its readme, npm's homepage where the package names none) is left out.
|
|
44
|
+
export function npmManifest(json: Record<string, unknown>): Record<string, unknown> {
|
|
45
|
+
const pick: Record<string, unknown> = {}
|
|
46
|
+
for (const key of ['description', 'license', 'homepage', 'main', 'module', 'types', 'type', 'deprecated', 'gitHead']) {
|
|
47
|
+
if (typeof json[key] === 'string') pick[key] = json[key]
|
|
48
|
+
}
|
|
49
|
+
// Its author's name, and their npm account where it can be told: the
|
|
50
|
+
// publisher's or a maintainer's whose email or name is the author's. Emails
|
|
51
|
+
// only match people up; none is kept.
|
|
52
|
+
const author = npmPerson(json['author'])
|
|
53
|
+
if (author?.name) pick['author'] = author.name
|
|
54
|
+
const accounts = [json['_npmUser'], ...Array.isArray(json['maintainers']) ? json['maintainers'] : []].map(npmPerson)
|
|
55
|
+
.filter((person): person is NpmPerson & { name: string } => person?.name != null && NPM_ACCOUNT.test(person.name))
|
|
56
|
+
// The npm account that published it, for its profile; or, published by
|
|
57
|
+
// a CI npm trusts (npm's trusted publishing), the CI, as npm names it
|
|
58
|
+
// (`GitHub Actions`).
|
|
59
|
+
const publisher = npmPerson(json['_npmUser'])?.name
|
|
60
|
+
if (publisher && NPM_ACCOUNT.test(publisher)) pick['publisher'] = publisher
|
|
61
|
+
else if (publisher && publisher.length <= 64 && plainObject(json['_npmUser']) && plainObject(json['_npmUser']['trustedPublisher'])) pick['trustedPublisher'] = publisher
|
|
62
|
+
const same = (a: string | null, b: string | null) => a !== null && b !== null && a.toLowerCase() === b.toLowerCase()
|
|
63
|
+
const account = author && accounts.find(person => same(person.email, author.email) || same(person.name, author.name))
|
|
64
|
+
if (account) pick['authorAccount'] = account.name
|
|
65
|
+
// Which of those accounts are known to publish, for the Overview to tell
|
|
66
|
+
// them from the rest.
|
|
67
|
+
const known = [...new Set([publisher, account?.name])].filter(name => name && KNOWN_PUBLISHERS.has(name.toLowerCase()))
|
|
68
|
+
if (known.length > 0) pick['knownAccounts'] = known
|
|
69
|
+
if (Array.isArray(json['keywords'])) pick['keywords'] = json['keywords'].filter(item => typeof item === 'string').slice(0, 50)
|
|
70
|
+
if (typeof json['bin'] === 'string') pick['bin'] = { [String(json['name'])]: json['bin'] }
|
|
71
|
+
else if (stringRecord(json['bin'])) pick['bin'] = stringRecord(json['bin'])
|
|
72
|
+
for (const key of ['dependencies', 'peerDependencies', 'optionalDependencies', 'engines']) {
|
|
73
|
+
const record = stringRecord(json[key])
|
|
74
|
+
if (record && Object.keys(record).length > 0) pick[key] = record
|
|
75
|
+
}
|
|
76
|
+
if (json['hasInstallScript'] === true) pick['hasInstallScript'] = true
|
|
77
|
+
// Whether npm keeps a provenance statement for it (npm-provenance.ts).
|
|
78
|
+
if (plainObject(json['dist']) && plainObject(json['dist']['attestations']) && plainObject(json['dist']['attestations']['provenance'])) pick['provenance'] = true
|
|
79
|
+
// The scripts npm runs installing it from the registry; not `prepare`,
|
|
80
|
+
// which runs only for a local or git install.
|
|
81
|
+
const scripts = stringRecord(json['scripts'])
|
|
82
|
+
const install = scripts ? ['preinstall', 'install', 'postinstall'].filter(script => script in scripts) : []
|
|
83
|
+
if (install.length > 0) pick['installScripts'] = Object.fromEntries(install.map(script => [script, scripts![script]]))
|
|
84
|
+
// A GitLab project it names, as its provenance's source is matched with
|
|
85
|
+
// the repository it names, GitHub's below.
|
|
86
|
+
const gitlab = gitlabProject(json['repository'])
|
|
87
|
+
if (gitlab) pick['gitlab'] = gitlab
|
|
88
|
+
try {
|
|
89
|
+
const repo = getRepo(json)
|
|
90
|
+
if (repo.github) {
|
|
91
|
+
pick['github'] = { github: repo.github, ...(repo.directory === undefined ? {} : { directory: repo.directory }) }
|
|
92
|
+
const home = typeof pick['homepage'] === 'string' ? getRepo({ homepage: pick['homepage'] }) : {}
|
|
93
|
+
if (home.github?.toLowerCase() === repo.github.toLowerCase() && (home.directory ?? '') === (repo.directory ?? '')) delete pick['homepage']
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
return pick
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Of a version's document, the fields readNpmVersion and npmManifest read,
|
|
100
|
+
// of `dist` and `scripts` only theirs, and of a provenance statement only that
|
|
101
|
+
// there is one: what is kept of it (npm-kept.ts), so that both read a kept one
|
|
102
|
+
// as they read npm's.
|
|
103
|
+
const VERSION_FIELDS = ['name', 'version', 'description', 'license', 'homepage', 'main', 'module', 'types', 'type', 'deprecated', 'gitHead', 'author', '_npmUser',
|
|
104
|
+
'maintainers', 'keywords', 'bin', 'dependencies', 'peerDependencies', 'optionalDependencies', 'engines', 'hasInstallScript', 'repository', 'bugs']
|
|
105
|
+
const DIST_FIELDS = ['tarball', 'integrity', 'shasum', 'unpackedSize', 'fileCount']
|
|
106
|
+
const INSTALL_SCRIPTS = ['preinstall', 'install', 'postinstall']
|
|
107
|
+
const picked = (json: Record<string, unknown>, keys: string[]) => Object.fromEntries(keys.filter(key => Object.hasOwn(json, key)).map(key => [key, json[key]]))
|
|
108
|
+
|
|
109
|
+
export function npmVersionFields(json: Record<string, unknown>): Record<string, unknown> {
|
|
110
|
+
const dist = plainObject(json['dist']) ? json['dist'] : {}
|
|
111
|
+
const provenance = plainObject(dist['attestations']) && plainObject(dist['attestations']['provenance'])
|
|
112
|
+
const scripts = picked(stringRecord(json['scripts']) ?? {}, INSTALL_SCRIPTS)
|
|
113
|
+
return { ...picked(json, VERSION_FIELDS), dist: { ...picked(dist, DIST_FIELDS), ...provenance && { attestations: { provenance: {} } } },
|
|
114
|
+
...Object.keys(scripts).length > 0 && { scripts } }
|
|
115
|
+
}
|